github.com/freiheit-com/kuberpult@v1.24.2-0.20240328135542-315d5630abe6/services/rollout-service/pkg/versions/versions.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 versions
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"strconv"
    23  	"time"
    24  
    25  	"github.com/argoproj/argo-cd/v2/pkg/apiclient/application"
    26  	"github.com/freiheit-com/kuberpult/services/rollout-service/pkg/argo"
    27  
    28  	"github.com/argoproj/argo-cd/v2/util/grpc"
    29  	api "github.com/freiheit-com/kuberpult/pkg/api/v1"
    30  	"github.com/freiheit-com/kuberpult/pkg/auth"
    31  	"github.com/freiheit-com/kuberpult/pkg/logger"
    32  	"github.com/freiheit-com/kuberpult/pkg/setup"
    33  	"go.uber.org/zap"
    34  	"google.golang.org/grpc/codes"
    35  	"k8s.io/utils/lru"
    36  )
    37  
    38  // This is a the user that the rollout service uses to query the versions.
    39  // It is not written to the repository.
    40  var RolloutServiceUser auth.User = auth.User{
    41  	DexAuthContext: nil,
    42  	Email:          "kuberpult-rollout-service@local",
    43  	Name:           "kuberpult-rollout-service",
    44  }
    45  
    46  type VersionClient interface {
    47  	GetVersion(ctx context.Context, revision, environment, application string) (*VersionInfo, error)
    48  	ConsumeEvents(ctx context.Context, processor VersionEventProcessor, hr *setup.HealthReporter) error
    49  	GetArgoProcessor() *argo.ArgoAppProcessor
    50  }
    51  
    52  type versionClient struct {
    53  	overviewClient api.OverviewServiceClient
    54  	versionClient  api.VersionServiceClient
    55  	cache          *lru.Cache
    56  	ArgoProcessor  argo.ArgoAppProcessor
    57  }
    58  
    59  type VersionInfo struct {
    60  	Version        uint64
    61  	SourceCommitId string
    62  	DeployedAt     time.Time
    63  }
    64  
    65  func (v *VersionInfo) Equal(w *VersionInfo) bool {
    66  	if v == nil {
    67  		return w == nil
    68  	}
    69  	if w == nil {
    70  		return false
    71  	}
    72  	return v.Version == w.Version
    73  }
    74  
    75  var ErrNotFound error = fmt.Errorf("not found")
    76  var ZeroVersion VersionInfo
    77  
    78  // GetVersion implements VersionClient
    79  func (v *versionClient) GetVersion(ctx context.Context, revision, environment, application string) (*VersionInfo, error) {
    80  	ctx = auth.WriteUserToGrpcContext(ctx, RolloutServiceUser)
    81  	tr, err := v.tryGetVersion(ctx, revision, environment, application)
    82  	if err == nil {
    83  		return tr, nil
    84  	}
    85  	info, err := v.versionClient.GetVersion(ctx, &api.GetVersionRequest{
    86  		GitRevision: revision,
    87  		Environment: environment,
    88  		Application: application,
    89  	})
    90  	if err != nil {
    91  		return nil, err
    92  	}
    93  	return &VersionInfo{
    94  		Version:        info.Version,
    95  		SourceCommitId: info.SourceCommitId,
    96  		DeployedAt:     info.DeployedAt.AsTime(),
    97  	}, nil
    98  }
    99  
   100  // Tries getting the version from cache
   101  func (v *versionClient) tryGetVersion(ctx context.Context, revision, environment, application string) (*VersionInfo, error) {
   102  	var overview *api.GetOverviewResponse
   103  	entry, ok := v.cache.Get(revision)
   104  	if !ok {
   105  		return nil, ErrNotFound
   106  	}
   107  	overview = entry.(*api.GetOverviewResponse)
   108  	for _, group := range overview.GetEnvironmentGroups() {
   109  		for _, env := range group.GetEnvironments() {
   110  			if env.Name == environment {
   111  				app := env.Applications[application]
   112  				if app == nil {
   113  					return &ZeroVersion, nil
   114  				}
   115  				return &VersionInfo{
   116  					Version:        app.Version,
   117  					SourceCommitId: sourceCommitId(overview, app),
   118  					DeployedAt:     deployedAt(app),
   119  				}, nil
   120  			}
   121  		}
   122  	}
   123  	return &ZeroVersion, nil
   124  }
   125  
   126  func deployedAt(app *api.Environment_Application) time.Time {
   127  	if app.DeploymentMetaData == nil {
   128  		return time.Time{}
   129  	}
   130  	deployTime := app.DeploymentMetaData.DeployTime
   131  	if deployTime != "" {
   132  		dt, err := strconv.ParseInt(deployTime, 10, 64)
   133  		if err != nil {
   134  			return time.Time{}
   135  		}
   136  		return time.Unix(dt, 0).UTC()
   137  	}
   138  	return time.Time{}
   139  }
   140  
   141  func team(overview *api.GetOverviewResponse, app string) string {
   142  	a := overview.Applications[app]
   143  	if a == nil {
   144  		return ""
   145  	}
   146  	return a.Team
   147  }
   148  
   149  func sourceCommitId(overview *api.GetOverviewResponse, app *api.Environment_Application) string {
   150  	a := overview.Applications[app.Name]
   151  	if a == nil {
   152  		return ""
   153  	}
   154  	for _, rel := range a.Releases {
   155  		if rel.Version == app.Version {
   156  			return rel.SourceCommitId
   157  		}
   158  	}
   159  	return ""
   160  }
   161  
   162  type KuberpultEvent struct {
   163  	Environment      string
   164  	Application      string
   165  	EnvironmentGroup string
   166  	IsProduction     bool
   167  	Team             string
   168  	Version          *VersionInfo
   169  }
   170  
   171  type VersionEventProcessor interface {
   172  	ProcessKuberpultEvent(ctx context.Context, ev KuberpultEvent)
   173  }
   174  
   175  type key struct {
   176  	Environment string
   177  	Application string
   178  }
   179  
   180  func (v *versionClient) ConsumeEvents(ctx context.Context, processor VersionEventProcessor, hr *setup.HealthReporter) error {
   181  	ctx = auth.WriteUserToGrpcContext(ctx, RolloutServiceUser)
   182  	versions := map[key]uint64{}
   183  	environmentGroups := map[key]string{}
   184  	teams := map[key]string{}
   185  	return hr.Retry(ctx, func() error {
   186  		client, err := v.overviewClient.StreamOverview(ctx, &api.GetOverviewRequest{
   187  			GitRevision: "",
   188  		})
   189  		if err != nil {
   190  			return fmt.Errorf("overview.connect: %w", err)
   191  		}
   192  		hr.ReportReady("consuming")
   193  		for {
   194  			select {
   195  			case <-ctx.Done():
   196  				return nil
   197  			default:
   198  			}
   199  			overview, err := client.Recv()
   200  			if err != nil {
   201  				grpcErr := grpc.UnwrapGRPCStatus(err)
   202  				if grpcErr != nil {
   203  					if grpcErr.Code() == codes.Canceled {
   204  						return nil
   205  					}
   206  				}
   207  				return fmt.Errorf("overview.recv: %w", err)
   208  			}
   209  			l := logger.FromContext(ctx).With(zap.String("git.revision", overview.GitRevision))
   210  			v.cache.Add(overview.GitRevision, overview)
   211  			l.Info("overview.get")
   212  			seen := make(map[key]uint64, len(versions))
   213  			for _, envGroup := range overview.EnvironmentGroups {
   214  				for _, env := range envGroup.Environments {
   215  					for _, app := range env.Applications {
   216  						dt := deployedAt(app)
   217  						sc := sourceCommitId(overview, app)
   218  						tm := team(overview, app.Name)
   219  
   220  						l.Info("version.process", zap.String("application", app.Name), zap.String("environment", env.Name), zap.Uint64("version", app.Version), zap.Time("deployedAt", dt))
   221  						k := key{env.Name, app.Name}
   222  						seen[k] = app.Version
   223  						environmentGroups[k] = envGroup.EnvironmentGroupName
   224  						teams[k] = tm
   225  						if versions[k] == app.Version {
   226  							continue
   227  						}
   228  
   229  						processor.ProcessKuberpultEvent(ctx, KuberpultEvent{
   230  							Application:      app.Name,
   231  							Environment:      env.Name,
   232  							EnvironmentGroup: envGroup.EnvironmentGroupName,
   233  							Team:             tm,
   234  							IsProduction:     env.Priority == api.Priority_PROD,
   235  							Version: &VersionInfo{
   236  								Version:        app.Version,
   237  								SourceCommitId: sc,
   238  								DeployedAt:     dt,
   239  							},
   240  						})
   241  
   242  					}
   243  				}
   244  			}
   245  			l.Info("version.push")
   246  			v.ArgoProcessor.Push(ctx, overview)
   247  			// Send events with version 0 for deleted applications so that we can react
   248  			// to apps getting deleted.
   249  			for k := range versions {
   250  				if seen[k] == 0 {
   251  					processor.ProcessKuberpultEvent(ctx, KuberpultEvent{
   252  						IsProduction:     false,
   253  						Application:      k.Application,
   254  						Environment:      k.Environment,
   255  						EnvironmentGroup: environmentGroups[k],
   256  						Team:             teams[k],
   257  						Version: &VersionInfo{
   258  							Version:        0,
   259  							SourceCommitId: "",
   260  							DeployedAt:     time.Time{},
   261  						},
   262  					})
   263  				}
   264  			}
   265  			versions = seen
   266  		}
   267  	})
   268  }
   269  
   270  func New(oclient api.OverviewServiceClient, vclient api.VersionServiceClient, appClient application.ApplicationServiceClient, manageArgoApplicationEnabled bool, manageArgoApplicationFilter []string) VersionClient {
   271  	result := &versionClient{
   272  		cache:          lru.New(20),
   273  		overviewClient: oclient,
   274  		versionClient:  vclient,
   275  		ArgoProcessor:  argo.New(appClient, manageArgoApplicationEnabled, manageArgoApplicationFilter),
   276  	}
   277  	return result
   278  }
   279  
   280  func (v *versionClient) GetArgoProcessor() *argo.ArgoAppProcessor {
   281  	return &v.ArgoProcessor
   282  }