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
76 var HealthzEndpoints = []string{"/healthz", "/ipranger/healthz"}
77
78
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
102 s.Router.Use(LoggerWithOperationID(logging.NewLogger()))
103
104
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
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
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
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
165 log.Info("http request", "url", c.Request.URL)
166
167
168 t := time.Now()
169 c.Next()
170 delta := time.Since(t)
171
172
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
197
198
199
200 func regionalLocation(loc string) string {
201 locsplit := strings.Split(loc, "-")
202 return strings.Join(locsplit[:2], "-")
203 }
204
View as plain text