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  }