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  }