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  }