...

Source file src/edge-infra.dev/pkg/f8n/ipranger/server/server.go

Documentation: edge-infra.dev/pkg/f8n/ipranger/server

     1  package server
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"net"
     7  	"net/http"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/gin-gonic/gin"
    12  	guuid "github.com/google/uuid"
    13  	"github.com/prometheus/client_golang/prometheus/promhttp"
    14  
    15  	"edge-infra.dev/pkg/f8n/ipranger"
    16  	"edge-infra.dev/pkg/lib/logging"
    17  )
    18  
    19  type Server struct {
    20  	Router *gin.Engine
    21  	cfg    Config
    22  	r      *ipranger.IPRanger
    23  }
    24  
    25  type Option func(*Config) error
    26  
    27  type Config struct {
    28  	port     int
    29  	listener net.Listener
    30  }
    31  
    32  func toConfig(options []Option) (Config, error) {
    33  	cfg := Config{
    34  		port: 8080,
    35  	}
    36  	var errs []string
    37  	for _, option := range options {
    38  		err := option(&cfg)
    39  		if err != nil {
    40  			errs = append(errs, err.Error())
    41  		}
    42  	}
    43  	if len(errs) > 0 {
    44  		return Config{}, fmt.Errorf("error(s) building server config: %v", strings.Join(errs, "; "))
    45  	}
    46  	return cfg, nil
    47  }
    48  
    49  type NetcfgQuery struct {
    50  	Region  string `form:"region" binding:"required"`
    51  	Project string `form:"project" binding:"required"`
    52  	Cluster string `form:"cluster" binding:"required"`
    53  }
    54  
    55  type NetcfgReq struct{}
    56  
    57  type NetcfgResp struct {
    58  	Network    string `json:"network"`
    59  	Subnetwork string `json:"subnetwork"`
    60  	Netmask    string `json:"netmask"`
    61  }
    62  
    63  func toNetcfgResp(subnet ipranger.Subnet) NetcfgResp {
    64  	return NetcfgResp{
    65  		Network:    subnet.Network,
    66  		Subnetwork: subnet.Ref,
    67  		Netmask:    fmt.Sprintf("/%d", ipranger.Netmask),
    68  	}
    69  }
    70  
    71  type ErrResp struct {
    72  	Message string `json:"message"`
    73  }
    74  
    75  // HealthzEndpoints contains the Server type's http health endpoints for k8s that returns 200 OK.
    76  var HealthzEndpoints = []string{"/healthz", "/ipranger/healthz"}
    77  
    78  // MetricsEndpoints contains the Server type's http endpoints for time series metrics (prometheus).
    79  var MetricsEndpoints = []string{"/metrics", "/ipranger/metrics"}
    80  
    81  const NetcfgEndpoint = "/api/v1/netcfg"
    82  
    83  func NewServer(options ...Option) (*Server, error) {
    84  	ctx := context.Background()
    85  	cfg, err := toConfig(options)
    86  	if err != nil {
    87  		return nil, err
    88  	}
    89  
    90  	ranger, err := ipranger.New(ctx)
    91  	if err != nil {
    92  		return nil, err
    93  	}
    94  
    95  	s := &Server{
    96  		Router: gin.New(),
    97  		cfg:    cfg,
    98  		r:      ranger,
    99  	}
   100  
   101  	// http req/res logging
   102  	s.Router.Use(LoggerWithOperationID(logging.NewLogger()))
   103  
   104  	// healthz and metrics builtins
   105  	for _, hzep := range HealthzEndpoints {
   106  		s.Router.GET(hzep, s.HealthHandlerFunction)
   107  	}
   108  	metricsHandlerFunc := gin.WrapH(promhttp.Handler())
   109  	for _, mep := range MetricsEndpoints {
   110  		s.Router.GET(mep, metricsHandlerFunc)
   111  	}
   112  
   113  	s.Router.GET(NetcfgEndpoint, s.HandleGetV1Netcfg)
   114  
   115  	return s, nil
   116  }
   117  
   118  // Run runs the Gin server on the configured port or listener.
   119  func (s *Server) Run() error {
   120  	if s.cfg.listener != nil {
   121  		return s.Router.RunListener(s.cfg.listener)
   122  	}
   123  	addr := fmt.Sprintf(":%d", s.cfg.port)
   124  	return s.Router.Run(addr)
   125  }
   126  
   127  // HealthHandlerFunction returns status `200 OK`.
   128  func (s *Server) HealthHandlerFunction(c *gin.Context) {
   129  	c.JSON(http.StatusOK, "UP")
   130  }
   131  
   132  func (s *Server) HandleGetV1Netcfg(c *gin.Context) {
   133  	log := getLogger(c)
   134  	var q NetcfgQuery
   135  	qerr := c.ShouldBindQuery(&q)
   136  	if qerr != nil {
   137  		c.JSON(http.StatusBadRequest, ErrResp{
   138  			Message: "invalid query string",
   139  		})
   140  		return
   141  	}
   142  	region := regionalLocation(q.Region)
   143  	subnet, err := s.r.FindOrCreateSubnet(q.Project, region)
   144  	if err != nil {
   145  		log.Error(err, "error finding or creating new subnet", "region", q.Region)
   146  		c.JSON(http.StatusInternalServerError, ErrResp{
   147  			Message: "failed to get or create GCP subnet",
   148  		})
   149  		return
   150  	}
   151  
   152  	c.JSON(http.StatusOK, toNetcfgResp(subnet))
   153  }
   154  
   155  func LoggerWithOperationID(log *logging.EdgeLogger) func(*gin.Context) {
   156  	return func(c *gin.Context) {
   157  		// initialize contextual logger
   158  		opID := guuid.New().String()
   159  		log := log.WithValues(logging.OperationKeysAndValues(opID)...).
   160  			WithValues("method", c.Request.Method).
   161  			WithValues("url", c.Request.URL)
   162  		setLogger(c, log)
   163  
   164  		// http incoming request
   165  		log.Info("http request", "url", c.Request.URL)
   166  
   167  		// process handlers
   168  		t := time.Now()
   169  		c.Next()
   170  		delta := time.Since(t)
   171  
   172  		// http outgoing response
   173  		log.Info(
   174  			"http response",
   175  			"status", c.Writer.Status(),
   176  			"elapsed_ms", delta.Milliseconds(),
   177  			"bytes", c.Writer.Size(),
   178  		)
   179  	}
   180  }
   181  
   182  const loggerCtxKey = "logger"
   183  
   184  func getLogger(c *gin.Context) *logging.EdgeLogger {
   185  	log, ok := c.Get(loggerCtxKey)
   186  	if ok {
   187  		return log.(*logging.EdgeLogger)
   188  	}
   189  	return logging.NewLogger()
   190  }
   191  
   192  func setLogger(c *gin.Context, l *logging.EdgeLogger) {
   193  	c.Set(loggerCtxKey, l)
   194  }
   195  
   196  // ensures the location is regional, compared to zonal.
   197  // e.g., us-east1 is regional, us-east1-a is zonal. subnetworks
   198  // are always regional, but a GKE cluster location may be given
   199  // as zonal.
   200  func regionalLocation(loc string) string {
   201  	locsplit := strings.Split(loc, "-")
   202  	return strings.Join(locsplit[:2], "-")
   203  }
   204  

View as plain text