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  }