github.com/freiheit-com/kuberpult@v1.24.2-0.20240328135542-315d5630abe6/services/cd-service/pkg/service/overview.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 service 18 19 import ( 20 "context" 21 "errors" 22 "fmt" 23 "os" 24 "sync" 25 "sync/atomic" 26 27 "github.com/freiheit-com/kuberpult/pkg/grpc" 28 "github.com/freiheit-com/kuberpult/pkg/logger" 29 "go.uber.org/zap" 30 31 "github.com/freiheit-com/kuberpult/services/cd-service/pkg/mapper" 32 33 git "github.com/libgit2/git2go/v34" 34 "google.golang.org/grpc/codes" 35 "google.golang.org/grpc/status" 36 "google.golang.org/protobuf/types/known/timestamppb" 37 38 api "github.com/freiheit-com/kuberpult/pkg/api/v1" 39 "github.com/freiheit-com/kuberpult/services/cd-service/pkg/notify" 40 "github.com/freiheit-com/kuberpult/services/cd-service/pkg/repository" 41 ) 42 43 type OverviewServiceServer struct { 44 Repository repository.Repository 45 RepositoryConfig repository.RepositoryConfig 46 Shutdown <-chan struct{} 47 48 notify notify.Notify 49 50 init sync.Once 51 response atomic.Value 52 } 53 54 func (o *OverviewServiceServer) GetOverview( 55 ctx context.Context, 56 in *api.GetOverviewRequest) (*api.GetOverviewResponse, error) { 57 if in.GitRevision != "" { 58 oid, err := git.NewOid(in.GitRevision) 59 if err != nil { 60 return nil, grpc.PublicError(ctx, fmt.Errorf("getOverview: could not find revision %v: %v", in.GitRevision, err)) 61 } 62 state, err := o.Repository.StateAt(oid) 63 if err != nil { 64 var gerr *git.GitError 65 if errors.As(err, &gerr) { 66 if gerr.Code == git.ErrorCodeNotFound { 67 return nil, status.Error(codes.NotFound, "not found") 68 } 69 } 70 return nil, err 71 } 72 return o.getOverview(ctx, state) 73 } 74 return o.getOverview(ctx, o.Repository.State()) 75 } 76 77 func (o *OverviewServiceServer) getOverview( 78 ctx context.Context, 79 s *repository.State) (*api.GetOverviewResponse, error) { 80 var rev string 81 if s.Commit != nil { 82 rev = s.Commit.Id().String() 83 } 84 result := api.GetOverviewResponse{ 85 Branch: "", 86 ManifestRepoUrl: "", 87 Applications: map[string]*api.Application{}, 88 EnvironmentGroups: []*api.EnvironmentGroup{}, 89 GitRevision: rev, 90 } 91 result.ManifestRepoUrl = o.RepositoryConfig.URL 92 result.Branch = o.RepositoryConfig.Branch 93 if envs, err := s.GetEnvironmentConfigs(); err != nil { 94 return nil, grpc.InternalError(ctx, err) 95 } else { 96 result.EnvironmentGroups = mapper.MapEnvironmentsToGroups(envs) 97 for envName, config := range envs { 98 var groupName = mapper.DeriveGroupName(config, envName) 99 var envInGroup = getEnvironmentInGroup(result.EnvironmentGroups, groupName, envName) 100 //exhaustruct:ignore 101 argocd := &api.EnvironmentConfig_ArgoCD{} 102 if config.ArgoCd != nil { 103 argocd = mapper.TransformArgocd(*config.ArgoCd) 104 } 105 env := api.Environment{ 106 DistanceToUpstream: 0, 107 Priority: api.Priority_PROD, 108 Name: envName, 109 Config: &api.EnvironmentConfig{ 110 Upstream: mapper.TransformUpstream(config.Upstream), 111 Argocd: argocd, 112 EnvironmentGroup: &groupName, 113 }, 114 Locks: map[string]*api.Lock{}, 115 Applications: map[string]*api.Environment_Application{}, 116 } 117 envInGroup.Config = env.Config 118 if locks, err := s.GetEnvironmentLocks(envName); err != nil { 119 return nil, err 120 } else { 121 for lockId, lock := range locks { 122 env.Locks[lockId] = &api.Lock{ 123 Message: lock.Message, 124 LockId: lockId, 125 CreatedAt: timestamppb.New(lock.CreatedAt), 126 CreatedBy: &api.Actor{ 127 Name: lock.CreatedBy.Name, 128 Email: lock.CreatedBy.Email, 129 }, 130 } 131 } 132 envInGroup.Locks = env.Locks 133 } 134 if apps, err := s.GetEnvironmentApplications(envName); err != nil { 135 return nil, err 136 } else { 137 for _, appName := range apps { 138 app := api.Environment_Application{ 139 Version: 0, 140 QueuedVersion: 0, 141 UndeployVersion: false, 142 ArgoCd: nil, 143 Name: appName, 144 Locks: map[string]*api.Lock{}, 145 DeploymentMetaData: &api.Environment_Application_DeploymentMetaData{ 146 DeployAuthor: "", 147 DeployTime: "", 148 }, 149 } 150 var version *uint64 151 if version, err = s.GetEnvironmentApplicationVersion(envName, appName); err != nil && !errors.Is(err, os.ErrNotExist) { 152 return nil, err 153 } else { 154 if version == nil { 155 app.Version = 0 156 } else { 157 app.Version = *version 158 } 159 } 160 if queuedVersion, err := s.GetQueuedVersion(envName, appName); err != nil && !errors.Is(err, os.ErrNotExist) { 161 return nil, err 162 } else { 163 if queuedVersion == nil { 164 app.QueuedVersion = 0 165 } else { 166 app.QueuedVersion = *queuedVersion 167 } 168 } 169 app.UndeployVersion = false 170 if version != nil { 171 if release, err := s.GetApplicationRelease(appName, *version); err != nil && !errors.Is(err, os.ErrNotExist) { 172 return nil, err 173 } else if release != nil { 174 app.UndeployVersion = release.UndeployVersion 175 } 176 } 177 if appLocks, err := s.GetEnvironmentApplicationLocks(envName, appName); err != nil { 178 return nil, err 179 } else { 180 for lockId, lock := range appLocks { 181 app.Locks[lockId] = &api.Lock{ 182 Message: lock.Message, 183 LockId: lockId, 184 CreatedAt: timestamppb.New(lock.CreatedAt), 185 CreatedBy: &api.Actor{ 186 Name: lock.CreatedBy.Name, 187 Email: lock.CreatedBy.Email, 188 }, 189 } 190 } 191 } 192 if config.ArgoCd != nil { 193 if syncWindows, err := mapper.TransformSyncWindows(config.ArgoCd.SyncWindows, appName); err != nil { 194 return nil, err 195 } else { 196 app.ArgoCd = &api.Environment_Application_ArgoCD{ 197 SyncWindows: syncWindows, 198 } 199 } 200 } 201 deployAuthor, deployTime, err := s.GetDeploymentMetaData(envName, appName) 202 if err != nil { 203 return nil, err 204 } 205 app.DeploymentMetaData.DeployAuthor = deployAuthor 206 if deployTime.IsZero() { 207 app.DeploymentMetaData.DeployTime = "" 208 } else { 209 app.DeploymentMetaData.DeployTime = fmt.Sprintf("%d", deployTime.Unix()) 210 } 211 env.Applications[appName] = &app 212 } 213 } 214 envInGroup.Applications = env.Applications 215 } 216 } 217 if apps, err := s.GetApplications(); err != nil { 218 return nil, err 219 } else { 220 for _, appName := range apps { 221 app := api.Application{ 222 UndeploySummary: 0, 223 Warnings: nil, 224 Name: appName, 225 Releases: []*api.Release{}, 226 SourceRepoUrl: "", 227 Team: "", 228 } 229 if rels, err := s.GetApplicationReleases(appName); err != nil { 230 return nil, err 231 } else { 232 for _, id := range rels { 233 if rel, err := s.GetApplicationRelease(appName, id); err != nil { 234 return nil, err 235 } else { 236 release := rel.ToProto() 237 release.Version = id 238 239 app.Releases = append(app.Releases, release) 240 } 241 } 242 } 243 if team, err := s.GetApplicationTeamOwner(appName); err != nil { 244 return nil, err 245 } else { 246 app.Team = team 247 } 248 if url, err := s.GetApplicationSourceRepoUrl(appName); err != nil { 249 return nil, err 250 } else { 251 app.SourceRepoUrl = url 252 } 253 app.UndeploySummary = deriveUndeploySummary(appName, result.EnvironmentGroups) 254 app.Warnings = CalculateWarnings(ctx, app.Name, result.EnvironmentGroups) 255 result.Applications[appName] = &app 256 } 257 } 258 return &result, nil 259 } 260 261 /* 262 CalculateWarnings returns warnings for the User to be displayed in the UI. 263 For really unusual configurations, these will be logged and not returned. 264 */ 265 func CalculateWarnings(ctx context.Context, appName string, groups []*api.EnvironmentGroup) []*api.Warning { 266 result := make([]*api.Warning, 0) 267 for e := 0; e < len(groups); e++ { 268 group := groups[e] 269 for i := 0; i < len(groups[e].Environments); i++ { 270 env := group.Environments[i] 271 if env.Config.Upstream == nil || env.Config.Upstream.Environment == nil { 272 // if the env has no upstream, there's nothing to warn about 273 continue 274 } 275 upstreamEnvName := env.Config.GetUpstream().Environment 276 upstreamEnv := getEnvironmentByName(groups, *upstreamEnvName) 277 if upstreamEnv == nil { 278 // this is already checked on startup and therefore shouldn't happen here 279 continue 280 } 281 282 appInEnv := env.Applications[appName] 283 if appInEnv == nil { 284 // appName is not deployed here, ignore it 285 continue 286 } 287 versionInEnv := appInEnv.Version 288 appInUpstreamEnv := upstreamEnv.Applications[appName] 289 if appInUpstreamEnv == nil { 290 // appName is not deployed upstream... that's unusual! 291 var warning = api.Warning{ 292 WarningType: &api.Warning_UpstreamNotDeployed{ 293 UpstreamNotDeployed: &api.UpstreamNotDeployed{ 294 UpstreamEnvironment: *upstreamEnvName, 295 ThisVersion: versionInEnv, 296 ThisEnvironment: env.Name, 297 }, 298 }, 299 } 300 result = append(result, &warning) 301 continue 302 } 303 versionInUpstreamEnv := appInUpstreamEnv.Version 304 305 if versionInEnv > versionInUpstreamEnv && len(appInEnv.Locks) == 0 { 306 var warning = api.Warning{ 307 WarningType: &api.Warning_UnusualDeploymentOrder{ 308 UnusualDeploymentOrder: &api.UnusualDeploymentOrder{ 309 UpstreamVersion: versionInUpstreamEnv, 310 UpstreamEnvironment: *upstreamEnvName, 311 ThisVersion: versionInEnv, 312 ThisEnvironment: env.Name, 313 }, 314 }, 315 } 316 result = append(result, &warning) 317 } 318 } 319 } 320 return result 321 } 322 323 func deriveUndeploySummary(appName string, groups []*api.EnvironmentGroup) api.UndeploySummary { 324 var allNormal = true 325 var allUndeploy = true 326 for _, group := range groups { 327 for _, environment := range group.Environments { 328 var app, exists = environment.Applications[appName] 329 if !exists { 330 continue 331 } 332 if app.Version == 0 { 333 // if the app exists but nothing is deployed, we ignore this 334 continue 335 } 336 if app.UndeployVersion { 337 allNormal = false 338 } else { 339 allUndeploy = false 340 } 341 } 342 } 343 if allUndeploy { 344 return api.UndeploySummary_UNDEPLOY 345 } 346 if allNormal { 347 return api.UndeploySummary_NORMAL 348 } 349 return api.UndeploySummary_MIXED 350 351 } 352 353 func getEnvironmentInGroup(groups []*api.EnvironmentGroup, groupNameToReturn string, envNameToReturn string) *api.Environment { 354 for _, currentGroup := range groups { 355 if currentGroup.EnvironmentGroupName == groupNameToReturn { 356 for _, currentEnv := range currentGroup.Environments { 357 if currentEnv.Name == envNameToReturn { 358 return currentEnv 359 } 360 } 361 } 362 } 363 return nil 364 } 365 366 func getEnvironmentByName(groups []*api.EnvironmentGroup, envNameToReturn string) *api.Environment { 367 for _, currentGroup := range groups { 368 for _, currentEnv := range currentGroup.Environments { 369 if currentEnv.Name == envNameToReturn { 370 return currentEnv 371 } 372 } 373 } 374 return nil 375 } 376 377 func (o *OverviewServiceServer) StreamOverview(in *api.GetOverviewRequest, 378 stream api.OverviewService_StreamOverviewServer) error { 379 ch, unsubscribe := o.subscribe() 380 defer unsubscribe() 381 done := stream.Context().Done() 382 for { 383 select { 384 case <-o.Shutdown: 385 return nil 386 case <-ch: 387 ov := o.response.Load().(*api.GetOverviewResponse) 388 if err := stream.Send(ov); err != nil { 389 // if we don't log this here, the details will be lost - so this is an exception to the rule "either return an error or log it". 390 // for example if there's an invalid encoding, grpc will just give a generic error like 391 // "error while marshaling: string field contains invalid UTF-8" 392 // but it won't tell us which field has the issue. This is then very hard to debug further. 393 logger.FromContext(stream.Context()).Error("error sending overview response:", zap.Error(err), zap.String("overview", fmt.Sprintf("%+v", ov))) 394 return err 395 } 396 397 case <-done: 398 return nil 399 } 400 } 401 } 402 403 func (o *OverviewServiceServer) subscribe() (<-chan struct{}, notify.Unsubscribe) { 404 o.init.Do(func() { 405 ch, unsub := o.Repository.Notify().Subscribe() 406 // Channels obtained from subscribe are by default triggered 407 // 408 // This means, we have to wait here until the first overview is loaded. 409 <-ch 410 o.update(o.Repository.State()) 411 go func() { 412 defer unsub() 413 for { 414 select { 415 case <-o.Shutdown: 416 return 417 case <-ch: 418 o.update(o.Repository.State()) 419 } 420 } 421 }() 422 }) 423 return o.notify.Subscribe() 424 } 425 426 func (o *OverviewServiceServer) update(s *repository.State) { 427 r, err := o.getOverview(context.Background(), s) 428 if err != nil { 429 panic(err) 430 } 431 o.response.Store(r) 432 o.notify.Notify() 433 }