github.com/freiheit-com/kuberpult@v1.24.2-0.20240328135542-315d5630abe6/services/cd-service/pkg/cmd/server.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  package cmd
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"net/http"
    23  	"os"
    24  	"strings"
    25  	"time"
    26  
    27  	"gopkg.in/DataDog/dd-trace-go.v1/profiler"
    28  
    29  	"github.com/freiheit-com/kuberpult/services/cd-service/pkg/argocd/reposerver"
    30  	"github.com/freiheit-com/kuberpult/services/cd-service/pkg/interceptors"
    31  
    32  	"github.com/DataDog/datadog-go/v5/statsd"
    33  	api "github.com/freiheit-com/kuberpult/pkg/api/v1"
    34  	"github.com/freiheit-com/kuberpult/pkg/auth"
    35  	"github.com/freiheit-com/kuberpult/pkg/logger"
    36  	"github.com/freiheit-com/kuberpult/pkg/setup"
    37  	"github.com/freiheit-com/kuberpult/pkg/tracing"
    38  	"github.com/freiheit-com/kuberpult/services/cd-service/pkg/repository"
    39  	"github.com/freiheit-com/kuberpult/services/cd-service/pkg/service"
    40  	grpc_zap "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap"
    41  	"github.com/kelseyhightower/envconfig"
    42  	"go.uber.org/zap"
    43  	"google.golang.org/grpc"
    44  	"google.golang.org/grpc/reflection"
    45  	grpctrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/google.golang.org/grpc"
    46  	httptrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/net/http"
    47  	"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
    48  )
    49  
    50  const datadogNameCd = "kuberpult-cd-service"
    51  
    52  type Config struct {
    53  	// these will be mapped to "KUBERPULT_GIT_URL", etc.
    54  	GitUrl                   string        `required:"true" split_words:"true"`
    55  	GitBranch                string        `default:"master" split_words:"true"`
    56  	BootstrapMode            bool          `default:"false" split_words:"true"`
    57  	GitCommitterEmail        string        `default:"kuberpult@freiheit.com" split_words:"true"`
    58  	GitCommitterName         string        `default:"kuberpult" split_words:"true"`
    59  	GitSshKey                string        `default:"/etc/ssh/identity" split_words:"true"`
    60  	GitSshKnownHosts         string        `default:"/etc/ssh/ssh_known_hosts" split_words:"true"`
    61  	GitNetworkTimeout        time.Duration `default:"1m" split_words:"true"`
    62  	GitWriteCommitData       bool          `default:"false" split_words:"true"`
    63  	PgpKeyRingPath           string        `split_words:"true"`
    64  	AzureEnableAuth          bool          `default:"false" split_words:"true"`
    65  	DexEnabled               bool          `default:"false" split_words:"true"`
    66  	DexRbacPolicyPath        string        `split_words:"true"`
    67  	EnableTracing            bool          `default:"false" split_words:"true"`
    68  	EnableMetrics            bool          `default:"false" split_words:"true"`
    69  	EnableEvents             bool          `default:"false" split_words:"true"`
    70  	DogstatsdAddr            string        `default:"127.0.0.1:8125" split_words:"true"`
    71  	EnableProfiling          bool          `default:"false" split_words:"true"`
    72  	DatadogApiKeyLocation    string        `default:"" split_words:"true"`
    73  	EnableSqlite             bool          `default:"true" split_words:"true"`
    74  	DexMock                  bool          `default:"false" split_words:"true"`
    75  	DexMockRole              string        `default:"Developer" split_words:"true"`
    76  	ArgoCdServer             string        `default:"" split_words:"true"`
    77  	ArgoCdInsecure           bool          `default:"false" split_words:"true"`
    78  	GitWebUrl                string        `default:"" split_words:"true"`
    79  	GitMaximumCommitsPerPush uint          `default:"1" split_words:"true"`
    80  	MaximumQueueSize         uint          `default:"5" split_words:"true"`
    81  }
    82  
    83  func (c *Config) storageBackend() repository.StorageBackend {
    84  	if c.EnableSqlite {
    85  		return repository.SqliteBackend
    86  	} else {
    87  		return repository.GitBackend
    88  	}
    89  }
    90  
    91  func RunServer() {
    92  	err := logger.Wrap(context.Background(), func(ctx context.Context) error {
    93  
    94  		var c Config
    95  
    96  		err := envconfig.Process("kuberpult", &c)
    97  		if err != nil {
    98  			logger.FromContext(ctx).Fatal("config.parse.error", zap.Error(err))
    99  		}
   100  
   101  		if c.EnableProfiling {
   102  			ddFilename := c.DatadogApiKeyLocation
   103  			if ddFilename == "" {
   104  				logger.FromContext(ctx).Fatal("config.profiler.apikey.notfound", zap.Error(err))
   105  			}
   106  			fileContentBytes, err := os.ReadFile(ddFilename)
   107  			if err != nil {
   108  				logger.FromContext(ctx).Fatal("config.profiler.apikey.file", zap.Error(err))
   109  			}
   110  			fileContent := string(fileContentBytes)
   111  			err = profiler.Start(profiler.WithAPIKey(fileContent), profiler.WithService(datadogNameCd))
   112  			if err != nil {
   113  				logger.FromContext(ctx).Fatal("config.profiler.error", zap.Error(err))
   114  			}
   115  			defer profiler.Stop()
   116  		}
   117  
   118  		var reader auth.GrpcContextReader
   119  		if c.DexMock {
   120  			if !c.DexEnabled {
   121  				logger.FromContext(ctx).Fatal("dexEnabled must be true if dexMock is true")
   122  			}
   123  			//if c.DexMockRole = nil {
   124  			//	logger.FromContext(ctx).Fatal("dexMockRole must be set to a role (e.g 'DEVELOPER'")
   125  			//}
   126  			reader = &auth.DummyGrpcContextReader{Role: c.DexMockRole}
   127  		} else {
   128  			reader = &auth.DexGrpcContextReader{DexEnabled: c.DexEnabled}
   129  		}
   130  		dexRbacPolicy, err := auth.ReadRbacPolicy(c.DexEnabled, c.DexRbacPolicyPath)
   131  		if err != nil {
   132  			logger.FromContext(ctx).Fatal("dex.read.error", zap.Error(err))
   133  		}
   134  
   135  		grpcServerLogger := logger.FromContext(ctx).Named("grpc_server")
   136  		httpServerLogger := logger.FromContext(ctx).Named("http_server")
   137  
   138  		// Unary interceptor. Only parses the Role information if Dex is enabled.
   139  		unaryUserContextInterceptor := func(ctx context.Context,
   140  			req interface{},
   141  			info *grpc.UnaryServerInfo,
   142  			handler grpc.UnaryHandler) (interface{}, error) {
   143  			return interceptors.UnaryUserContextInterceptor(ctx, req, info, handler, reader)
   144  		}
   145  
   146  		grpcStreamInterceptors := []grpc.StreamServerInterceptor{
   147  			grpc_zap.StreamServerInterceptor(grpcServerLogger),
   148  		}
   149  		grpcUnaryInterceptors := []grpc.UnaryServerInterceptor{
   150  			grpc_zap.UnaryServerInterceptor(grpcServerLogger),
   151  			unaryUserContextInterceptor,
   152  		}
   153  
   154  		if c.EnableTracing {
   155  			tracer.Start()
   156  			defer tracer.Stop()
   157  
   158  			grpcStreamInterceptors = append(grpcStreamInterceptors,
   159  				grpctrace.StreamServerInterceptor(grpctrace.WithServiceName(tracing.ServiceName(datadogNameCd))),
   160  			)
   161  			grpcUnaryInterceptors = append(grpcUnaryInterceptors,
   162  				grpctrace.UnaryServerInterceptor(grpctrace.WithServiceName(tracing.ServiceName(datadogNameCd))),
   163  			)
   164  		}
   165  
   166  		if c.EnableMetrics {
   167  			ddMetrics, err := statsd.New(c.DogstatsdAddr, statsd.WithNamespace("Kuberpult"))
   168  			if err != nil {
   169  				logger.FromContext(ctx).Fatal("datadog.metrics.error", zap.Error(err))
   170  			}
   171  			ctx = context.WithValue(ctx, repository.DdMetricsKey, ddMetrics)
   172  		}
   173  
   174  		// If the tracer is not started, calling this function is a no-op.
   175  		span, ctx := tracer.StartSpanFromContext(ctx, "Start server")
   176  
   177  		if strings.HasPrefix(c.GitUrl, "https") {
   178  			logger.FromContext(ctx).Fatal("git.url.protocol.unsupported",
   179  				zap.String("url", c.GitUrl),
   180  				zap.String("details", "https is not supported for git communication, only ssh is supported"))
   181  		}
   182  		if c.GitMaximumCommitsPerPush == 0 {
   183  			logger.FromContext(ctx).Fatal("git.config",
   184  				zap.String("details", "the maximum number of commits per push must be at least 1"),
   185  			)
   186  		}
   187  		if c.MaximumQueueSize < 2 || c.MaximumQueueSize > 100 {
   188  			logger.FromContext(ctx).Fatal("cd.config",
   189  				zap.String("details", "the size of the queue must be between 2 and 100"),
   190  			)
   191  		}
   192  		cfg := repository.RepositoryConfig{
   193  			WebhookResolver: nil,
   194  			URL:             c.GitUrl,
   195  			Path:            "./repository",
   196  			CommitterEmail:  c.GitCommitterEmail,
   197  			CommitterName:   c.GitCommitterName,
   198  			Credentials: repository.Credentials{
   199  				SshKey: c.GitSshKey,
   200  			},
   201  			Certificates: repository.Certificates{
   202  				KnownHostsFile: c.GitSshKnownHosts,
   203  			},
   204  			Branch:                 c.GitBranch,
   205  			GcFrequency:            20,
   206  			BootstrapMode:          c.BootstrapMode,
   207  			EnvironmentConfigsPath: "./environment_configs.json",
   208  			StorageBackend:         c.storageBackend(),
   209  			ArgoInsecure:           c.ArgoCdInsecure,
   210  			ArgoWebhookUrl:         c.ArgoCdServer,
   211  			WebURL:                 c.GitWebUrl,
   212  			NetworkTimeout:         c.GitNetworkTimeout,
   213  			DogstatsdEvents:        c.EnableMetrics,
   214  			WriteCommitData:        c.GitWriteCommitData,
   215  			MaximumCommitsPerPush:  c.GitMaximumCommitsPerPush,
   216  			MaximumQueueSize:       c.MaximumQueueSize,
   217  		}
   218  		repo, repoQueue, err := repository.New2(ctx, cfg)
   219  		if err != nil {
   220  			logger.FromContext(ctx).Fatal("repository.new.error", zap.Error(err), zap.String("git.url", c.GitUrl), zap.String("git.branch", c.GitBranch))
   221  		}
   222  
   223  		repositoryService := &service.Service{
   224  			Repository: repo,
   225  		}
   226  
   227  		span.Finish()
   228  
   229  		// Shutdown channel is used to terminate server side streams.
   230  		shutdownCh := make(chan struct{})
   231  		setup.Run(ctx, setup.ServerConfig{
   232  			HTTP: []setup.HTTPConfig{
   233  				{
   234  					BasicAuth: nil,
   235  					Shutdown:  nil,
   236  					Port:      "8080",
   237  					Register: func(mux *http.ServeMux) {
   238  						handler := logger.WithHttpLogger(httpServerLogger, repositoryService)
   239  						if c.EnableTracing {
   240  							handler = httptrace.WrapHandler(handler, datadogNameCd, "/")
   241  						}
   242  						mux.Handle("/", handler)
   243  					},
   244  				},
   245  			},
   246  			GRPC: &setup.GRPCConfig{
   247  				Shutdown: nil,
   248  				Port:     "8443",
   249  				Opts: []grpc.ServerOption{
   250  					grpc.ChainStreamInterceptor(grpcStreamInterceptors...),
   251  					grpc.ChainUnaryInterceptor(grpcUnaryInterceptors...),
   252  				},
   253  				Register: func(srv *grpc.Server) {
   254  					api.RegisterBatchServiceServer(srv, &service.BatchServer{
   255  						Repository: repo,
   256  						RBACConfig: auth.RBACConfig{
   257  							DexEnabled: c.DexEnabled,
   258  							Policy:     dexRbacPolicy,
   259  						},
   260  						Config: service.BatchServerConfig{
   261  							WriteCommitData: c.GitWriteCommitData,
   262  						},
   263  					})
   264  
   265  					overviewSrv := &service.OverviewServiceServer{
   266  						Repository:       repo,
   267  						RepositoryConfig: cfg,
   268  						Shutdown:         shutdownCh,
   269  					}
   270  					api.RegisterOverviewServiceServer(srv, overviewSrv)
   271  					api.RegisterGitServiceServer(srv, &service.GitServer{Config: cfg, OverviewService: overviewSrv})
   272  					api.RegisterVersionServiceServer(srv, &service.VersionServiceServer{Repository: repo})
   273  					api.RegisterEnvironmentServiceServer(srv, &service.EnvironmentServiceServer{Repository: repo})
   274  					api.RegisterReleaseTrainPrognosisServiceServer(srv, &service.ReleaseTrainPrognosisServer{
   275  						Repository: repo,
   276  						RBACConfig: auth.RBACConfig{
   277  							DexEnabled: c.DexEnabled,
   278  							Policy:     dexRbacPolicy,
   279  						},
   280  					})
   281  					reflection.Register(srv)
   282  					reposerver.Register(srv, repo, cfg)
   283  
   284  				},
   285  			},
   286  			Background: []setup.BackgroundTaskConfig{
   287  				{
   288  					Shutdown: nil,
   289  					Name:     "ddmetrics",
   290  					Run: func(ctx context.Context, reporter *setup.HealthReporter) error {
   291  						reporter.ReportReady("sending metrics")
   292  						repository.RegularlySendDatadogMetrics(repo, 300, func(repository2 repository.Repository) {
   293  							repository.GetRepositoryStateAndUpdateMetrics(ctx, repository2)
   294  						})
   295  						return nil
   296  					},
   297  				},
   298  				{
   299  					Shutdown: nil,
   300  					Name:     "push queue",
   301  					Run:      repoQueue,
   302  				},
   303  			},
   304  			Shutdown: func(ctx context.Context) error {
   305  				close(shutdownCh)
   306  				return nil
   307  			},
   308  		})
   309  
   310  		return nil
   311  	})
   312  	if err != nil {
   313  		fmt.Printf("error in logger.wrap: %v %#v", err, err)
   314  	}
   315  }