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 }