package server import ( "context" "fmt" "net" "net/http" "strings" "time" "github.com/gin-gonic/gin" guuid "github.com/google/uuid" "github.com/prometheus/client_golang/prometheus/promhttp" "edge-infra.dev/pkg/f8n/ipranger" "edge-infra.dev/pkg/lib/logging" ) type Server struct { Router *gin.Engine cfg Config r *ipranger.IPRanger } type Option func(*Config) error type Config struct { port int listener net.Listener } func toConfig(options []Option) (Config, error) { cfg := Config{ port: 8080, } var errs []string for _, option := range options { err := option(&cfg) if err != nil { errs = append(errs, err.Error()) } } if len(errs) > 0 { return Config{}, fmt.Errorf("error(s) building server config: %v", strings.Join(errs, "; ")) } return cfg, nil } type NetcfgQuery struct { Region string `form:"region" binding:"required"` Project string `form:"project" binding:"required"` Cluster string `form:"cluster" binding:"required"` } type NetcfgReq struct{} type NetcfgResp struct { Network string `json:"network"` Subnetwork string `json:"subnetwork"` Netmask string `json:"netmask"` } func toNetcfgResp(subnet ipranger.Subnet) NetcfgResp { return NetcfgResp{ Network: subnet.Network, Subnetwork: subnet.Ref, Netmask: fmt.Sprintf("/%d", ipranger.Netmask), } } type ErrResp struct { Message string `json:"message"` } // HealthzEndpoints contains the Server type's http health endpoints for k8s that returns 200 OK. var HealthzEndpoints = []string{"/healthz", "/ipranger/healthz"} // MetricsEndpoints contains the Server type's http endpoints for time series metrics (prometheus). var MetricsEndpoints = []string{"/metrics", "/ipranger/metrics"} const NetcfgEndpoint = "/api/v1/netcfg" func NewServer(options ...Option) (*Server, error) { ctx := context.Background() cfg, err := toConfig(options) if err != nil { return nil, err } ranger, err := ipranger.New(ctx) if err != nil { return nil, err } s := &Server{ Router: gin.New(), cfg: cfg, r: ranger, } // http req/res logging s.Router.Use(LoggerWithOperationID(logging.NewLogger())) // healthz and metrics builtins for _, hzep := range HealthzEndpoints { s.Router.GET(hzep, s.HealthHandlerFunction) } metricsHandlerFunc := gin.WrapH(promhttp.Handler()) for _, mep := range MetricsEndpoints { s.Router.GET(mep, metricsHandlerFunc) } s.Router.GET(NetcfgEndpoint, s.HandleGetV1Netcfg) return s, nil } // Run runs the Gin server on the configured port or listener. func (s *Server) Run() error { if s.cfg.listener != nil { return s.Router.RunListener(s.cfg.listener) } addr := fmt.Sprintf(":%d", s.cfg.port) return s.Router.Run(addr) } // HealthHandlerFunction returns status `200 OK`. func (s *Server) HealthHandlerFunction(c *gin.Context) { c.JSON(http.StatusOK, "UP") } func (s *Server) HandleGetV1Netcfg(c *gin.Context) { log := getLogger(c) var q NetcfgQuery qerr := c.ShouldBindQuery(&q) if qerr != nil { c.JSON(http.StatusBadRequest, ErrResp{ Message: "invalid query string", }) return } region := regionalLocation(q.Region) subnet, err := s.r.FindOrCreateSubnet(q.Project, region) if err != nil { log.Error(err, "error finding or creating new subnet", "region", q.Region) c.JSON(http.StatusInternalServerError, ErrResp{ Message: "failed to get or create GCP subnet", }) return } c.JSON(http.StatusOK, toNetcfgResp(subnet)) } func LoggerWithOperationID(log *logging.EdgeLogger) func(*gin.Context) { return func(c *gin.Context) { // initialize contextual logger opID := guuid.New().String() log := log.WithValues(logging.OperationKeysAndValues(opID)...). WithValues("method", c.Request.Method). WithValues("url", c.Request.URL) setLogger(c, log) // http incoming request log.Info("http request", "url", c.Request.URL) // process handlers t := time.Now() c.Next() delta := time.Since(t) // http outgoing response log.Info( "http response", "status", c.Writer.Status(), "elapsed_ms", delta.Milliseconds(), "bytes", c.Writer.Size(), ) } } const loggerCtxKey = "logger" func getLogger(c *gin.Context) *logging.EdgeLogger { log, ok := c.Get(loggerCtxKey) if ok { return log.(*logging.EdgeLogger) } return logging.NewLogger() } func setLogger(c *gin.Context, l *logging.EdgeLogger) { c.Set(loggerCtxKey, l) } // ensures the location is regional, compared to zonal. // e.g., us-east1 is regional, us-east1-a is zonal. subnetworks // are always regional, but a GKE cluster location may be given // as zonal. func regionalLocation(loc string) string { locsplit := strings.Split(loc, "-") return strings.Join(locsplit[:2], "-") }