github.com/freiheit-com/kuberpult@v1.24.2-0.20240328135542-315d5630abe6/services/rollout-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 "crypto/tls" 22 "crypto/x509" 23 "fmt" 24 "net/url" 25 "time" 26 27 "github.com/argoproj/argo-cd/v2/pkg/apiclient" 28 argoio "github.com/argoproj/argo-cd/v2/util/io" 29 api "github.com/freiheit-com/kuberpult/pkg/api/v1" 30 "github.com/freiheit-com/kuberpult/pkg/logger" 31 pkgmetrics "github.com/freiheit-com/kuberpult/pkg/metrics" 32 "github.com/freiheit-com/kuberpult/pkg/setup" 33 "github.com/freiheit-com/kuberpult/pkg/tracing" 34 "github.com/freiheit-com/kuberpult/services/rollout-service/pkg/metrics" 35 "github.com/freiheit-com/kuberpult/services/rollout-service/pkg/notifier" 36 "github.com/freiheit-com/kuberpult/services/rollout-service/pkg/revolution" 37 "github.com/freiheit-com/kuberpult/services/rollout-service/pkg/service" 38 "github.com/freiheit-com/kuberpult/services/rollout-service/pkg/versions" 39 grpc_zap "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap" 40 "github.com/kelseyhightower/envconfig" 41 "go.uber.org/zap" 42 "google.golang.org/grpc" 43 "google.golang.org/grpc/credentials" 44 "google.golang.org/grpc/credentials/insecure" 45 "google.golang.org/grpc/reflection" 46 "google.golang.org/protobuf/types/known/emptypb" 47 "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" 48 49 grpctrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/google.golang.org/grpc" 50 ) 51 52 type Config struct { 53 CdServer string `default:"kuberpult-cd-service:8443"` 54 CdServerSecure bool `default:"false" split_words:"true"` 55 EnableTracing bool `default:"false" split_words:"true"` 56 57 ArgocdServer string `split_words:"true"` 58 ArgocdInsecure bool `default:"false" split_words:"true"` 59 ArgocdToken string `split_words:"true"` 60 ArgocdRefreshEnabled bool `split_words:"true"` 61 ArgocdRefreshConcurrency int `default:"50" split_words:"true"` 62 63 RevolutionDoraEnabled bool `split_words:"true"` 64 RevolutionDoraUrl string `split_words:"true" default:""` 65 RevolutionDoraToken string `split_words:"true" default:""` 66 RevolutionDoraConcurrency int `default:"10" split_words:"true"` 67 RevolutionDoraMaxEventAge time.Duration `default:"0" split_words:"true"` 68 69 ManageArgoApplicationsEnabled bool `split_words:"true" default:"true"` 70 ManageArgoApplicationsFilter []string `split_words:"true" default:"sreteam"` 71 72 ManifestRepoUrl string `default:"" split_words:"true"` 73 Branch string `default:"" split_words:"true"` 74 } 75 76 func (config *Config) ClientConfig() (apiclient.ClientOptions, error) { 77 var opts apiclient.ClientOptions 78 opts.ConfigPath = "" 79 u, err := url.ParseRequestURI(config.ArgocdServer) 80 if err != nil { 81 return opts, fmt.Errorf("invalid argocd server url: %w", err) 82 } 83 opts.ServerAddr = u.Host 84 opts.PlainText = u.Scheme == "http" 85 opts.UserAgent = "kuberpult" 86 opts.Insecure = config.ArgocdInsecure 87 opts.AuthToken = config.ArgocdToken 88 return opts, nil 89 } 90 91 func (config *Config) RevolutionConfig() (revolution.Config, error) { 92 if config.RevolutionDoraUrl == "" { 93 return revolution.Config{}, fmt.Errorf("KUBERPULT_REVOLUTION_DORA_URL must be a valid url") 94 } 95 if config.RevolutionDoraToken == "" { 96 return revolution.Config{}, fmt.Errorf("KUBERPULT_REVOLUTION_DORA_TOKEN must not be empty") 97 } 98 return revolution.Config{ 99 URL: config.RevolutionDoraUrl, 100 Token: []byte(config.RevolutionDoraToken), 101 Concurrency: config.RevolutionDoraConcurrency, 102 MaxEventAge: config.RevolutionDoraMaxEventAge, 103 }, nil 104 } 105 106 func RunServer() { 107 var config Config 108 err := logger.Wrap(context.Background(), func(ctx context.Context) error { 109 err := envconfig.Process("kuberpult", &config) 110 if err != nil { 111 logger.FromContext(ctx).Fatal("config.parse", zap.Error(err)) 112 } 113 return runServer(ctx, config) 114 }) 115 if err != nil { 116 fmt.Printf("error: %v %#v", err, err) 117 } 118 } 119 120 func getGrpcClients(ctx context.Context, config Config) (api.OverviewServiceClient, api.VersionServiceClient, error) { 121 var cred credentials.TransportCredentials = insecure.NewCredentials() 122 if config.CdServerSecure { 123 systemRoots, err := x509.SystemCertPool() 124 if err != nil { 125 msg := "failed to read CA certificates" 126 return nil, nil, fmt.Errorf(msg) 127 } 128 //exhaustruct:ignore 129 cred = credentials.NewTLS(&tls.Config{ 130 RootCAs: systemRoots, 131 }) 132 } 133 134 grpcClientOpts := []grpc.DialOption{ 135 grpc.WithTransportCredentials(cred), 136 } 137 if config.EnableTracing { 138 grpcClientOpts = append(grpcClientOpts, 139 grpc.WithStreamInterceptor( 140 grpctrace.StreamClientInterceptor(grpctrace.WithServiceName(tracing.ServiceName("kuberpult-rollout-service"))), 141 ), 142 grpc.WithUnaryInterceptor( 143 grpctrace.UnaryClientInterceptor(grpctrace.WithServiceName(tracing.ServiceName("kuberpult-rollout-service"))), 144 ), 145 ) 146 } 147 148 con, err := grpc.Dial(config.CdServer, grpcClientOpts...) 149 if err != nil { 150 return nil, nil, fmt.Errorf("error dialing %s: %w", config.CdServer, err) 151 } 152 153 return api.NewOverviewServiceClient(con), api.NewVersionServiceClient(con), nil 154 } 155 156 func runServer(ctx context.Context, config Config) error { 157 grpcServerLogger := logger.FromContext(ctx).Named("grpc_server") 158 grpcStreamInterceptors := []grpc.StreamServerInterceptor{ 159 grpc_zap.StreamServerInterceptor(grpcServerLogger), 160 } 161 grpcUnaryInterceptors := []grpc.UnaryServerInterceptor{ 162 grpc_zap.UnaryServerInterceptor(grpcServerLogger), 163 } 164 165 if config.EnableTracing { 166 tracer.Start() 167 defer tracer.Stop() 168 grpcStreamInterceptors = append(grpcStreamInterceptors, 169 grpctrace.StreamServerInterceptor(grpctrace.WithServiceName("rollout-service")), 170 ) 171 grpcUnaryInterceptors = append(grpcUnaryInterceptors, 172 grpctrace.UnaryServerInterceptor(grpctrace.WithServiceName("rollout-service")), 173 ) 174 } 175 176 opts, err := config.ClientConfig() 177 if err != nil { 178 return err 179 } 180 181 logger.FromContext(ctx).Info("argocd.connecting", zap.String("argocd.addr", opts.ServerAddr)) 182 client, err := apiclient.NewClient(&opts) 183 if err != nil { 184 return fmt.Errorf("connecting to argocd %s: %w", opts.ServerAddr, err) 185 } 186 closer, versionClient, err := client.NewVersionClient() 187 if err != nil { 188 return fmt.Errorf("connecting to argocd version: %w", err) 189 } 190 defer argoio.Close(closer) 191 version, err := versionClient.Version(ctx, &emptypb.Empty{}) 192 if err != nil { 193 return fmt.Errorf("retrieving argocd version: %w", err) 194 } 195 logger.FromContext(ctx).Info("argocd.connected", zap.String("argocd.version", version.Version)) 196 closer, appClient, err := client.NewApplicationClient() 197 if err != nil { 198 return fmt.Errorf("connecting to argocd app: %w", err) 199 } 200 defer argoio.Close(closer) 201 202 overviewGrpc, versionGrpc, err := getGrpcClients(ctx, config) 203 if err != nil { 204 return fmt.Errorf("connecting to cd service %q: %w", config.CdServer, err) 205 } 206 broadcast := service.New() 207 shutdownCh := make(chan struct{}) 208 versionC := versions.New(overviewGrpc, versionGrpc, appClient, config.ManageArgoApplicationsEnabled, config.ManageArgoApplicationsFilter) 209 dispatcher := service.NewDispatcher(broadcast, versionC) 210 backgroundTasks := []setup.BackgroundTaskConfig{ 211 { 212 Shutdown: nil, 213 Name: "consume argocd events", 214 Run: func(ctx context.Context, health *setup.HealthReporter) error { 215 return service.ConsumeEvents(ctx, appClient, dispatcher, health) 216 }, 217 }, 218 { 219 Shutdown: nil, 220 Name: "consume kuberpult events", 221 Run: func(ctx context.Context, health *setup.HealthReporter) error { 222 return versionC.ConsumeEvents(ctx, broadcast, health) 223 }, 224 }, 225 { 226 Shutdown: nil, 227 Name: "consume self-manage events", 228 Run: func(ctx context.Context, health *setup.HealthReporter) error { 229 return versionC.GetArgoProcessor().Consume(ctx, health) 230 }, 231 }, 232 { 233 Shutdown: nil, 234 Name: "consume argo events", 235 Run: func(ctx context.Context, health *setup.HealthReporter) error { 236 return versionC.GetArgoProcessor().ConsumeArgo(ctx, health) 237 }, 238 }, 239 { 240 Shutdown: nil, 241 Name: "dispatch argocd events", 242 Run: dispatcher.Work, 243 }, 244 } 245 246 if config.ArgocdRefreshEnabled { 247 248 backgroundTasks = append(backgroundTasks, setup.BackgroundTaskConfig{ 249 Shutdown: nil, 250 Name: "refresh argocd", 251 Run: func(ctx context.Context, health *setup.HealthReporter) error { 252 notify := notifier.New(appClient, config.ArgocdRefreshConcurrency) 253 return notifier.Subscribe(ctx, notify, broadcast, health) 254 }, 255 }) 256 } 257 258 if config.RevolutionDoraEnabled { 259 revolutionConfig, err := config.RevolutionConfig() 260 if err != nil { 261 return err 262 } 263 revolutionDora := revolution.New(revolutionConfig) 264 backgroundTasks = append(backgroundTasks, setup.BackgroundTaskConfig{ 265 Shutdown: nil, 266 Name: "revolution dora", 267 Run: func(ctx context.Context, health *setup.HealthReporter) error { 268 health.ReportReady("pushing") 269 return revolutionDora.Subscribe(ctx, broadcast) 270 }, 271 }) 272 } 273 274 backgroundTasks = append(backgroundTasks, setup.BackgroundTaskConfig{ 275 Shutdown: nil, 276 Name: "create metrics", 277 Run: func(ctx context.Context, health *setup.HealthReporter) error { 278 health.ReportReady("reporting") 279 return metrics.Metrics(ctx, broadcast, pkgmetrics.FromContext(ctx), nil, func() {}) 280 }, 281 }) 282 283 setup.Run(ctx, setup.ServerConfig{ 284 HTTP: []setup.HTTPConfig{ 285 { 286 Register: nil, 287 BasicAuth: nil, 288 Shutdown: nil, 289 Port: "8080", 290 }, 291 }, 292 Background: backgroundTasks, 293 GRPC: &setup.GRPCConfig{ 294 Shutdown: nil, 295 Port: "8443", 296 Opts: []grpc.ServerOption{ 297 grpc.ChainStreamInterceptor(grpcStreamInterceptors...), 298 grpc.ChainUnaryInterceptor(grpcUnaryInterceptors...), 299 }, 300 Register: func(srv *grpc.Server) { 301 api.RegisterRolloutServiceServer(srv, broadcast) 302 reflection.Register(srv) 303 }, 304 }, 305 Shutdown: func(ctx context.Context) error { 306 close(shutdownCh) 307 return nil 308 }, 309 }) 310 return nil 311 }