github.com/freiheit-com/kuberpult@v1.24.2-0.20240328135542-315d5630abe6/services/cd-service/pkg/mapper/environments_config.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 mapper 18 19 import ( 20 "fmt" 21 "path/filepath" 22 "sort" 23 24 api "github.com/freiheit-com/kuberpult/pkg/api/v1" 25 "github.com/freiheit-com/kuberpult/services/cd-service/pkg/config" 26 ) 27 28 type EnvSortOrder = map[string]int 29 30 func MapEnvironmentsToGroups(envs map[string]config.EnvironmentConfig) []*api.EnvironmentGroup { 31 var result = []*api.EnvironmentGroup{} 32 var buckets = map[string]*api.EnvironmentGroup{} 33 // first, group all envs into buckets by groupName 34 for envName, env := range envs { 35 var groupName = DeriveGroupName(env, envName) 36 var groupNameCopy = groupName + "" // without this copy, unexpected pointer things happen :/ 37 var bucket, ok = buckets[groupName] 38 if !ok { 39 bucket = &api.EnvironmentGroup{ 40 DistanceToUpstream: 0, 41 Priority: api.Priority_PROD, 42 EnvironmentGroupName: groupNameCopy, 43 Environments: []*api.Environment{}, 44 } 45 buckets[groupNameCopy] = bucket 46 } 47 var newEnv = &api.Environment{ 48 DistanceToUpstream: 0, 49 Priority: api.Priority_PROD, 50 Name: envName, 51 Config: &api.EnvironmentConfig{ 52 Argocd: nil, 53 Upstream: TransformUpstream(env.Upstream), 54 EnvironmentGroup: &groupNameCopy, 55 }, 56 Locks: map[string]*api.Lock{}, 57 Applications: map[string]*api.Environment_Application{}, 58 } 59 bucket.Environments = append(bucket.Environments, newEnv) 60 } 61 // now we have all environments grouped correctly. 62 // next step, sort envs by distance to prod. 63 // to do that, we first need to calculate the distance to upstream. 64 // 65 tmpDistancesToUpstreamByEnv := map[string]uint32{} 66 rest := []*api.Environment{} 67 68 // we need to sort the buckets here because: 69 // A) `range` of a map is not sorted in golang 70 // B) the result depends on the sort order, even though this happens just in some special cases 71 keys := make([]string, 0) 72 for k := range buckets { 73 keys = append(keys, k) 74 } 75 sort.Strings(keys) 76 for _, k := range keys { 77 var bucket = buckets[k] 78 // first, find all envs with distance 0 79 for i := 0; i < len(bucket.Environments); i++ { 80 var environment = bucket.Environments[i] 81 if environment.Config.Upstream.GetLatest() { 82 environment.DistanceToUpstream = 0 83 tmpDistancesToUpstreamByEnv[environment.Name] = 0 84 } else if environment.Config.Upstream == nil { 85 // the environment has neither an upstream, nor latest configured. We can't determine where it belongs 86 environment.DistanceToUpstream = 100 // we can just pick an arbitrary number 87 tmpDistancesToUpstreamByEnv[environment.Name] = 100 88 } else { 89 upstreamEnv := environment.Config.Upstream.GetEnvironment() 90 if _, exists := envs[upstreamEnv]; !exists { // upstreamEnv is not exists! 91 tmpDistancesToUpstreamByEnv[upstreamEnv] = 666 92 } 93 // and remember the rest: 94 rest = append(rest, environment) 95 } 96 } 97 } 98 // now we have all envs remaining that have upstream.latest == false 99 for len(rest) > 0 { 100 nextRest := []*api.Environment{} 101 for i := 0; i < len(rest); i++ { 102 env := rest[i] 103 upstreamEnv := env.Config.Upstream.GetEnvironment() 104 _, ok := tmpDistancesToUpstreamByEnv[upstreamEnv] 105 if ok { 106 tmpDistancesToUpstreamByEnv[env.Name] = tmpDistancesToUpstreamByEnv[upstreamEnv] + 1 107 env.DistanceToUpstream = tmpDistancesToUpstreamByEnv[env.Name] 108 } else { 109 nextRest = append(nextRest, env) 110 } 111 } 112 if len(rest) == len(nextRest) { 113 // if nothing changed in the previous for-loop, we have an undefined distance. 114 // to avoid an infinite loop, we fill it with an arbitrary number: 115 for i := 0; i < len(rest); i++ { 116 env := rest[i] 117 tmpDistancesToUpstreamByEnv[env.Config.Upstream.GetEnvironment()] = 666 118 } 119 } 120 rest = nextRest 121 } 122 123 // now each environment has a distanceToUpstream. 124 // we set the distanceToUpstream also to each group: 125 for _, bucket := range buckets { 126 bucket.DistanceToUpstream = bucket.Environments[0].DistanceToUpstream 127 } 128 129 // now we can actually sort the environments: 130 for _, bucket := range buckets { 131 sort.Sort(EnvironmentByDistance(bucket.Environments)) 132 } 133 // environments are sorted, now sort the groups: 134 // to do that we first need to convert the map into an array: 135 for _, bucket := range buckets { 136 result = append(result, bucket) 137 } 138 sort.Sort(EnvironmentGroupsByDistance(result)) 139 // now, everything is sorted, so we can calculate the env priorities. For that we convert the data to an array: 140 var tmpEnvs []*api.Environment 141 for i := 0; i < len(result); i++ { 142 var group = result[i] 143 for j := 0; j < len(group.Environments); j++ { 144 tmpEnvs = append(tmpEnvs, group.Environments[j]) 145 } 146 } 147 calculateEnvironmentPriorities(tmpEnvs) // note that `tmpEnvs` were copied by reference - otherwise this function would have no effect on `result` 148 149 { 150 var downstreamDepth uint32 = 0 151 for _, group := range result { 152 downstreamDepth = max(downstreamDepth, group.DistanceToUpstream) 153 } 154 155 for _, group := range result { 156 group.Priority = calculateGroupPriority(group.DistanceToUpstream, downstreamDepth) 157 } 158 } 159 160 return result 161 } 162 163 func calculateGroupPriority(distanceToUpstream, downstreamDepth uint32) api.Priority { 164 lookup := [][]api.Priority{ 165 []api.Priority{api.Priority_YOLO}, 166 []api.Priority{api.Priority_UPSTREAM, api.Priority_PROD}, 167 []api.Priority{api.Priority_UPSTREAM, api.Priority_PRE_PROD, api.Priority_PROD}, 168 []api.Priority{api.Priority_UPSTREAM, api.Priority_PRE_PROD, api.Priority_CANARY, api.Priority_PROD}, 169 []api.Priority{api.Priority_UPSTREAM, api.Priority_OTHER, api.Priority_PRE_PROD, api.Priority_CANARY, api.Priority_PROD}, 170 } 171 if downstreamDepth > uint32(len(lookup)-1) { 172 if distanceToUpstream == 0 { 173 return api.Priority_UPSTREAM 174 } 175 if distanceToUpstream == downstreamDepth { 176 return api.Priority_PROD 177 } 178 if distanceToUpstream == downstreamDepth-1 { 179 return api.Priority_CANARY 180 } 181 if distanceToUpstream == downstreamDepth-2 { 182 return api.Priority_PRE_PROD 183 } 184 return api.Priority_OTHER 185 } 186 return lookup[downstreamDepth][distanceToUpstream] 187 } 188 189 // either the groupName is set in the config, or we use the envName as a default 190 func DeriveGroupName(env config.EnvironmentConfig, envName string) string { 191 var groupName = env.EnvironmentGroup 192 if groupName == nil { 193 groupName = &envName 194 } 195 return *groupName 196 } 197 198 type EnvsByName map[string]*api.Environment 199 200 func getUpstreamEnvironment(env *api.Environment, envsByName EnvsByName) *api.Environment { 201 if env == nil || env.Config == nil || env.Config.Upstream == nil || env.Config.Upstream.Environment == nil { 202 return nil 203 } 204 return envsByName[*env.Config.Upstream.Environment] 205 } 206 207 func calculateEnvironmentPriorities(environments []*api.Environment) { 208 type Childs []string 209 type ChildsByName map[string]Childs 210 var envsByName = make(EnvsByName) 211 var childsByName = make(ChildsByName) 212 // latest is UPSTREAM, so mark them as such, and the rest as OTHER for now 213 // oherwise append us to the list of the childs of the upstream env 214 for i := 0; i < len(environments); i++ { 215 var env = environments[i] 216 envsByName[env.Name] = env 217 if env.Config.Upstream.GetLatest() { 218 env.Priority = api.Priority_UPSTREAM 219 } else { 220 env.Priority = api.Priority_OTHER 221 if env.Config != nil && env.Config.Upstream != nil { 222 var upstream = env.Config.Upstream.Environment 223 if upstream != nil { 224 var upstreamChildsBefore = childsByName[*upstream] 225 childsByName[*upstream] = append(upstreamChildsBefore, env.Name) 226 } 227 } 228 } 229 } 230 // remaining childless envs can now be identified as PROD 231 for i := 0; i < len(environments); i++ { 232 var env = environments[i] 233 if len(childsByName[env.Name]) > 0 { 234 continue 235 } 236 // even if an env is UPSTREAM, if it is a leaf too, it is a Priority_YOLO 237 if env.Priority == api.Priority_UPSTREAM { 238 env.Priority = api.Priority_YOLO 239 } else { 240 env.Priority = api.Priority_PROD 241 } 242 243 // find the two environments before PROD, if available 244 var upstream = getUpstreamEnvironment(env, envsByName) 245 var upstreamsUpstream = getUpstreamEnvironment(upstream, envsByName) 246 247 if upstreamsUpstream == nil || upstreamsUpstream.Priority == api.Priority_UPSTREAM { 248 // we only have at most one environment to mark, so its PRE_PROD 249 if upstream != nil && upstream.Priority != api.Priority_UPSTREAM { 250 upstream.Priority = api.Priority_PRE_PROD 251 } 252 } else { 253 // we have two non-UPSTREAM environments to mark. 254 upstream.Priority = api.Priority_CANARY 255 upstreamsUpstream.Priority = api.Priority_PRE_PROD 256 } 257 } 258 } 259 260 func max(a uint32, b uint32) uint32 { 261 if a > b { 262 return a 263 } 264 return b 265 } 266 267 type EnvironmentByDistance []*api.Environment 268 269 func (s EnvironmentByDistance) Len() int { 270 return len(s) 271 } 272 func (s EnvironmentByDistance) Swap(i, j int) { 273 s[i], s[j] = s[j], s[i] 274 } 275 func (s EnvironmentByDistance) Less(i, j int) bool { 276 // first sort by distance, then by name 277 var di = s[i].DistanceToUpstream 278 var dj = s[j].DistanceToUpstream 279 if di != dj { 280 return di < dj 281 } 282 return s[i].Name < s[j].Name 283 } 284 285 type EnvironmentGroupsByDistance []*api.EnvironmentGroup 286 287 func (s EnvironmentGroupsByDistance) Len() int { 288 return len(s) 289 } 290 func (s EnvironmentGroupsByDistance) Swap(i, j int) { 291 s[i], s[j] = s[j], s[i] 292 } 293 func (s EnvironmentGroupsByDistance) Less(i, j int) bool { 294 // first sort by distance, then by name 295 var di = s[i].Environments[0].DistanceToUpstream 296 var dj = s[j].Environments[0].DistanceToUpstream 297 if dj != di { 298 return di < dj 299 } 300 return s[i].Environments[0].Name < s[j].Environments[0].Name 301 } 302 303 func TransformUpstream(upstream *config.EnvironmentConfigUpstream) *api.EnvironmentConfig_Upstream { 304 if upstream == nil { 305 return nil 306 } 307 if upstream.Latest { 308 return &api.EnvironmentConfig_Upstream{ 309 Environment: nil, 310 Latest: &upstream.Latest, 311 } 312 } 313 if upstream.Environment != "" { 314 return &api.EnvironmentConfig_Upstream{ 315 Latest: nil, 316 Environment: &upstream.Environment, 317 } 318 } 319 return nil 320 } 321 322 func TransformSyncWindows(syncWindows []config.ArgoCdSyncWindow, appName string) ([]*api.Environment_Application_ArgoCD_SyncWindow, error) { 323 var envAppSyncWindows []*api.Environment_Application_ArgoCD_SyncWindow 324 for _, syncWindow := range syncWindows { 325 for _, pattern := range syncWindow.Apps { 326 if match, err := filepath.Match(pattern, appName); err != nil { 327 return nil, fmt.Errorf("failed to match app pattern %s of sync window to %s at %s with duration %s: %w", pattern, syncWindow.Kind, syncWindow.Schedule, syncWindow.Duration, err) 328 } else if match { 329 envAppSyncWindows = append(envAppSyncWindows, &api.Environment_Application_ArgoCD_SyncWindow{ 330 Kind: syncWindow.Kind, 331 Schedule: syncWindow.Schedule, 332 Duration: syncWindow.Duration, 333 }) 334 } 335 } 336 } 337 return envAppSyncWindows, nil 338 } 339 340 func TransformArgocd(config config.EnvironmentConfigArgoCd) *api.EnvironmentConfig_ArgoCD { 341 var syncWindows []*api.EnvironmentConfig_ArgoCD_SyncWindows 342 var accessList []*api.EnvironmentConfig_ArgoCD_AccessEntry 343 var ignoreDifferences []*api.EnvironmentConfig_ArgoCD_IgnoreDifferences 344 345 for _, i := range config.SyncWindows { 346 syncWindow := &api.EnvironmentConfig_ArgoCD_SyncWindows{ 347 Kind: i.Kind, 348 Duration: i.Duration, 349 Schedule: i.Schedule, 350 Applications: i.Apps, 351 } 352 syncWindows = append(syncWindows, syncWindow) 353 } 354 355 for _, i := range config.ClusterResourceWhitelist { 356 access := &api.EnvironmentConfig_ArgoCD_AccessEntry{ 357 Group: i.Group, 358 Kind: i.Kind, 359 } 360 accessList = append(accessList, access) 361 } 362 363 for _, i := range config.IgnoreDifferences { 364 ignoreDiff := &api.EnvironmentConfig_ArgoCD_IgnoreDifferences{ 365 Group: i.Group, 366 Kind: i.Kind, 367 Name: i.Name, 368 Namespace: i.Namespace, 369 JsonPointers: i.JSONPointers, 370 JqPathExpressions: i.JqPathExpressions, 371 ManagedFieldsManagers: i.ManagedFieldsManagers, 372 } 373 ignoreDifferences = append(ignoreDifferences, ignoreDiff) 374 } 375 376 return &api.EnvironmentConfig_ArgoCD{ 377 Destination: &api.EnvironmentConfig_ArgoCD_Destination{ 378 Name: config.Destination.Name, 379 Server: config.Destination.Server, 380 Namespace: config.Destination.Namespace, 381 AppProjectNamespace: config.Destination.AppProjectNamespace, 382 ApplicationNamespace: config.Destination.ApplicationNamespace, 383 }, 384 SyncWindows: syncWindows, 385 AccessList: accessList, 386 IgnoreDifferences: ignoreDifferences, 387 ApplicationAnnotations: config.ApplicationAnnotations, 388 SyncOptions: config.SyncOptions, 389 } 390 }