github.com/freiheit-com/kuberpult@v1.24.2-0.20240328135542-315d5630abe6/pkg/setup/setup.go (about) 1 /*This file is part of kuberpult. 2 3 Kuberpult is free software: you can redistribute it and/or modify 4 it under the terms of the Expat(MIT) License as published by 5 the Free Software Foundation. 6 7 Kuberpult is distributed in the hope that it will be useful, 8 but WITHOUT ANY WARRANTY; without even the implied warranty of 9 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 MIT License for more details. 11 12 You should have received a copy of the MIT License 13 along with kuberpult. If not, see <https://directory.fsf.org/wiki/License:Expat>. 14 15 Copyright 2023 freiheit.com*/ 16 17 // Setup implementation shared between all microservices. 18 // If this file is changed it will affect _all_ microservices in the monorepo (and this 19 // is deliberately so). 20 package setup 21 22 import ( 23 "context" 24 "crypto/subtle" 25 "fmt" 26 "net" 27 "net/http" 28 "os" 29 "os/signal" 30 "syscall" 31 32 "go.opentelemetry.io/otel/attribute" 33 "go.opentelemetry.io/otel/metric" 34 "go.uber.org/zap" 35 36 "github.com/freiheit-com/kuberpult/pkg/logger" 37 "github.com/freiheit-com/kuberpult/pkg/metrics" 38 "google.golang.org/grpc" 39 ) 40 41 var ( 42 osSignalChannel = make(chan os.Signal, 1) // System writes here when shutdown 43 ) 44 45 func init() { 46 signal.Notify(osSignalChannel, syscall.SIGINT, syscall.SIGTERM) 47 } 48 49 type shutdown struct { 50 name string 51 fn func(context.Context) error 52 } 53 54 // Setup structure that holds only the shutdown callbacks for all 55 // grpc and http server for endpoints, metrics, health checks, etc. 56 type setup struct { 57 health HealthServer 58 shutdown []shutdown 59 } 60 61 type BasicAuth struct { 62 Username string 63 Password string 64 } 65 66 type GRPCConfig struct { 67 // required 68 Port string 69 Register func(*grpc.Server) 70 Opts []grpc.ServerOption 71 72 // optional 73 Shutdown func(context.Context) error 74 } 75 76 type HTTPConfig struct { 77 // required 78 Port string 79 Register func(*http.ServeMux) 80 81 // optional 82 BasicAuth *BasicAuth 83 Shutdown func(context.Context) error 84 } 85 86 type BackgroundFunc func(context.Context, *HealthReporter) error 87 88 type BackgroundTaskConfig struct { 89 // a function that triggers a graceful shutdown of all other resources after completion 90 Run BackgroundFunc 91 Name string 92 // optional 93 Shutdown func(context.Context) error 94 } 95 96 // Config contains configurations for all servers & tasks will be started. 97 // A startup order is not guaranteed. 98 type ServerConfig struct { 99 GRPC *GRPCConfig 100 HTTP []HTTPConfig 101 // BackgroundTasks are tasks that are running forever, like Pub/sub receiver. If they 102 // finish, a graceful shutdown will be triggered. 103 Background []BackgroundTaskConfig 104 Shutdown func(context.Context) error 105 } 106 107 func Run(ctx context.Context, config ServerConfig) { 108 //exhaustruct:ignore 109 s := &setup{} 110 111 ctx, cancel := context.WithCancel(ctx) 112 pv, handler, _ := metrics.Init() 113 ctx = metrics.WithProvider(ctx, pv) 114 115 _, _ = pv.Meter("setup").Int64ObservableGauge("background_job_ready", metric.WithInt64Callback(func(_ context.Context, o metric.Int64Observer) error { 116 reports := s.health.reports() 117 for name, report := range reports { 118 var value int64 119 if report.isReady(s.health.now()) { 120 value = 1 121 } 122 o.Observe(value, metric.WithAttributes(attribute.String("name", name))) 123 } 124 return nil 125 })) 126 127 // Start the listening on each protocol 128 for _, cfg := range config.HTTP { 129 setupHTTP(ctx, s, cfg, cancel, handler) 130 } 131 if config.GRPC != nil { 132 setupGRPC(ctx, s, *config.GRPC, cancel) 133 } 134 for _, task := range config.Background { 135 setupBackgroundTask(ctx, s, task, cancel) 136 } 137 138 if config.Shutdown != nil { 139 s.RegisterShutdown( 140 "global shutdown handler", 141 config.Shutdown, 142 ) 143 } 144 145 // Listening for shutdown signal 146 s.listenToShutdownSignal(ctx, cancel) 147 } 148 149 func (s *setup) RegisterShutdown(name string, shutdownFN func(ctx context.Context) error) { 150 s.shutdown = append(s.shutdown, shutdown{name: name, fn: shutdownFN}) 151 } 152 153 func (s *setup) listenToShutdownSignal(ctx context.Context, cancelFunc context.CancelFunc) { 154 // Wait for a signal to shutdown all servers. 155 // This should be a blocking call, because program will exit as soon as 156 // the main goroutine returns (so it doesn't wait for other goroutines). 157 // Non-blocking call could lead to unfinished cleanup during the shutdown. 158 // See also https://golang.org/ref/spec#Program_execution 159 select { 160 case <-osSignalChannel: 161 case <-ctx.Done(): 162 } 163 164 // cancel the context 165 cancelFunc() 166 167 // call shutdown hooks 168 gracefulShutdown(ctx, s) 169 } 170 171 func gracefulShutdown(ctx context.Context, s *setup) { 172 for i := len(s.shutdown) - 1; i >= 0; i-- { 173 sd := s.shutdown[i] 174 if err := sd.fn(ctx); err != nil { 175 logger.FromContext(ctx).Error("shutdown.failed", zap.Error(err), zap.String("handler", sd.name)) 176 } 177 } 178 } 179 180 func setupGRPC(ctx context.Context, s *setup, config GRPCConfig, cancel context.CancelFunc) { 181 // Get service listening port 182 addrGRPC := ":" + config.Port 183 184 // Setup a listener for gRPC port 185 grpcL, err := net.Listen("tcp", addrGRPC) 186 if err != nil { 187 logger.FromContext(ctx).Panic("grpc.listen.error", zap.Error(err), zap.String("addr", addrGRPC)) 188 return 189 } 190 s.RegisterShutdown("GRPC net listener", func(context.Context) error { 191 return grpcL.Close() 192 }) 193 194 // Instantiate gRPC server 195 grpcS := grpc.NewServer(config.Opts...) 196 s.RegisterShutdown("GRPC server", func(context.Context) error { 197 grpcS.GracefulStop() 198 return nil 199 }) 200 201 config.Register(grpcS) 202 if config.Shutdown != nil { 203 s.RegisterShutdown("GRPC shutdown handler", config.Shutdown) 204 } 205 206 go serveGRPC(ctx, grpcS, grpcL, cancel) 207 } 208 209 func serveGRPC(ctx context.Context, grpcS *grpc.Server, grpcL net.Listener, cancel context.CancelFunc) { 210 defer cancel() 211 212 if err := grpcS.Serve(grpcL); err != nil { 213 logger.FromContext(ctx).Error("grpc.serve.error", zap.Error(err)) 214 } 215 } 216 217 func setupHTTP(ctx context.Context, s *setup, config HTTPConfig, cancel context.CancelFunc, metricHandler http.Handler) { 218 mux := http.NewServeMux() 219 if config.Register != nil { 220 config.Register(mux) 221 } 222 mux.Handle("/metrics", metricHandler) 223 mux.Handle("/healthz", &s.health) 224 runHTTPHandler(ctx, s, mux, config.Port, config.BasicAuth, config.Shutdown, cancel) 225 } 226 227 func runHTTPHandler(ctx context.Context, s *setup, handler http.Handler, port string, basicAuth *BasicAuth, shutdown func(context.Context) error, cancel context.CancelFunc) { 228 229 if basicAuth != nil { 230 handler = NewBasicAuthHandler(basicAuth, handler) 231 } 232 233 //exhaustruct:ignore 234 httpS := &http.Server{ 235 Handler: handler, 236 } 237 s.RegisterShutdown( 238 fmt.Sprintf("http server on %s", port), 239 func(ctx context.Context) error { 240 return shutdownHTTP(ctx, httpS) 241 }, 242 ) 243 244 if shutdown != nil { 245 s.RegisterShutdown( 246 fmt.Sprintf("http shutdown handler on %s", port), 247 shutdown, 248 ) 249 } 250 251 go serveHTTP(ctx, httpS, port, cancel) 252 } 253 254 var shutdownHTTP = func(ctx context.Context, httpS *http.Server) error { 255 return httpS.Shutdown(ctx) 256 } 257 258 var serveHTTP = func(ctx context.Context, httpS *http.Server, port string, cancel context.CancelFunc) { 259 // if this function returns, the server was stopped, so stop also all the other services 260 defer cancel() 261 262 addr := ":" + port 263 264 httpL, err := net.Listen("tcp", addr) 265 if err != nil { 266 logger.FromContext(ctx).Panic("http.listen.error", zap.Error(err), zap.String("addr", addr)) 267 return 268 } 269 270 if err := httpS.Serve(httpL); err != nil && err != http.ErrServerClosed { 271 logger.FromContext(ctx).Error("http.serve.error", zap.Error(err)) 272 } 273 } 274 275 func setupBackgroundTask(ctx context.Context, s *setup, config BackgroundTaskConfig, cancel context.CancelFunc) { 276 277 if config.Shutdown != nil { 278 s.RegisterShutdown( 279 fmt.Sprintf("shutdown handler for %s", config.Name), 280 config.Shutdown, 281 ) 282 } 283 reporter := s.health.Reporter(config.Name) 284 285 go runBackgroundTask(ctx, config, cancel, reporter) 286 } 287 288 func runBackgroundTask(ctx context.Context, config BackgroundTaskConfig, cancel context.CancelFunc, reporter *HealthReporter) { 289 defer cancel() 290 if err := config.Run(ctx, reporter); err != nil { 291 logger.FromContext(ctx).Error("background.error", zap.Error(err), zap.String("job", config.Name)) 292 } 293 } 294 295 func NewBasicAuthHandler(basicAuth *BasicAuth, chainedHandler http.Handler) http.Handler { 296 return &BasicAuthHandler{ 297 basicAuth: basicAuth, 298 chainedHandler: chainedHandler, 299 } 300 } 301 302 type BasicAuthHandler struct { 303 basicAuth *BasicAuth 304 chainedHandler http.Handler 305 } 306 307 func (h *BasicAuthHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { 308 reqUser, reqPass, ok := req.BasicAuth() 309 if !ok || subtle.ConstantTimeCompare([]byte(reqUser), []byte(h.basicAuth.Username)) != 1 || subtle.ConstantTimeCompare([]byte(reqPass), []byte(h.basicAuth.Password)) != 1 { 310 rw.Header().Set("WWW-Authenticate", `Basic realm="Please enter credentials"`) 311 rw.WriteHeader(401) 312 _, _ = rw.Write([]byte("Unauthorised.\n")) 313 return 314 } 315 h.chainedHandler.ServeHTTP(rw, req) 316 }