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 }