github.com/freiheit-com/kuberpult@v1.24.2-0.20240328135542-315d5630abe6/services/cd-service/pkg/repository/transformer.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 repository
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"errors"
    23  	"fmt"
    24  	"io"
    25  	"io/fs"
    26  	"os"
    27  	"path"
    28  	"sort"
    29  	"strconv"
    30  	"strings"
    31  	"time"
    32  
    33  	"github.com/DataDog/datadog-go/v5/statsd"
    34  	"github.com/freiheit-com/kuberpult/pkg/metrics"
    35  	"github.com/freiheit-com/kuberpult/pkg/ptr"
    36  
    37  	"github.com/freiheit-com/kuberpult/pkg/uuid"
    38  	"github.com/freiheit-com/kuberpult/services/cd-service/pkg/event"
    39  	git "github.com/libgit2/git2go/v34"
    40  
    41  	"github.com/freiheit-com/kuberpult/pkg/grpc"
    42  	"github.com/freiheit-com/kuberpult/pkg/valid"
    43  
    44  	"github.com/freiheit-com/kuberpult/pkg/logger"
    45  
    46  	yaml3 "gopkg.in/yaml.v3"
    47  
    48  	api "github.com/freiheit-com/kuberpult/pkg/api/v1"
    49  	"github.com/freiheit-com/kuberpult/pkg/auth"
    50  	"github.com/freiheit-com/kuberpult/services/cd-service/pkg/config"
    51  	"github.com/freiheit-com/kuberpult/services/cd-service/pkg/mapper"
    52  	billy "github.com/go-git/go-billy/v5"
    53  	"github.com/go-git/go-billy/v5/util"
    54  	"github.com/hexops/gotextdiff"
    55  	"github.com/hexops/gotextdiff/myers"
    56  	"github.com/hexops/gotextdiff/span"
    57  )
    58  
    59  const (
    60  	queueFileName         = "queued_version"
    61  	yamlParsingError      = "# yaml parsing error"
    62  	fieldSourceAuthor     = "source_author"
    63  	fieldSourceMessage    = "source_message"
    64  	fieldSourceCommitId   = "source_commit_id"
    65  	fieldDisplayVersion   = "display_version"
    66  	fieldSourceRepoUrl    = "sourceRepoUrl" // urgh, inconsistent
    67  	fieldCreatedAt        = "created_at"
    68  	fieldTeam             = "team"
    69  	fieldNextCommidId     = "nextCommit"
    70  	fieldPreviousCommitId = "previousCommit"
    71  	// number of old releases that will ALWAYS be kept in addition to the ones that are deployed:
    72  	keptVersionsOnCleanup = 20
    73  )
    74  
    75  func versionToString(Version uint64) string {
    76  	return strconv.FormatUint(Version, 10)
    77  }
    78  
    79  func releasesDirectory(fs billy.Filesystem, application string) string {
    80  	return fs.Join("applications", application, "releases")
    81  }
    82  
    83  func applicationDirectory(fs billy.Filesystem, application string) string {
    84  	return fs.Join("applications", application)
    85  }
    86  
    87  func environmentDirectory(fs billy.Filesystem, environment string) string {
    88  	return fs.Join("environments", environment)
    89  }
    90  
    91  func environmentApplicationDirectory(fs billy.Filesystem, environment, application string) string {
    92  	return fs.Join("environments", environment, "applications", application)
    93  }
    94  
    95  func releasesDirectoryWithVersion(fs billy.Filesystem, application string, version uint64) string {
    96  	return fs.Join(releasesDirectory(fs, application), versionToString(version))
    97  }
    98  
    99  func manifestDirectoryWithReleasesVersion(fs billy.Filesystem, application string, version uint64) string {
   100  	return fs.Join(releasesDirectoryWithVersion(fs, application, version), "environments")
   101  }
   102  
   103  func commitDirectory(fs billy.Filesystem, commit string) string {
   104  	return fs.Join("commits", commit[:2], commit[2:])
   105  }
   106  
   107  func commitApplicationDirectory(fs billy.Filesystem, commit, application string) string {
   108  	return fs.Join(commitDirectory(fs, commit), "applications", application)
   109  }
   110  
   111  func commitEventDir(fs billy.Filesystem, commit, eventId string) string {
   112  	return fs.Join(commitDirectory(fs, commit), "events", eventId)
   113  }
   114  
   115  func GetEnvironmentLocksCount(fs billy.Filesystem, env string) float64 {
   116  	envLocksCount := 0
   117  	envDir := environmentDirectory(fs, env)
   118  	locksDir := fs.Join(envDir, "locks")
   119  	if entries, _ := fs.ReadDir(locksDir); entries != nil {
   120  		envLocksCount += len(entries)
   121  	}
   122  	return float64(envLocksCount)
   123  }
   124  
   125  func GetEnvironmentApplicationLocksCount(fs billy.Filesystem, environment, application string) float64 {
   126  	envAppLocksCount := 0
   127  	appDir := environmentApplicationDirectory(fs, environment, application)
   128  	locksDir := fs.Join(appDir, "locks")
   129  	if entries, _ := fs.ReadDir(locksDir); entries != nil {
   130  		envAppLocksCount += len(entries)
   131  	}
   132  	return float64(envAppLocksCount)
   133  }
   134  
   135  func GaugeEnvLockMetric(fs billy.Filesystem, env string) {
   136  	if ddMetrics != nil {
   137  		ddMetrics.Gauge("env_lock_count", GetEnvironmentLocksCount(fs, env), []string{"env:" + env}, 1) //nolint: errcheck
   138  	}
   139  }
   140  
   141  func GaugeEnvAppLockMetric(fs billy.Filesystem, env, app string) {
   142  	if ddMetrics != nil {
   143  		ddMetrics.Gauge("app_lock_count", GetEnvironmentApplicationLocksCount(fs, env, app), []string{"app:" + app, "env:" + env}, 1) //nolint: errcheck
   144  	}
   145  }
   146  
   147  func GaugeDeploymentMetric(_ context.Context, env, app string, timeInMinutes float64) error {
   148  	if ddMetrics != nil {
   149  		// store the time since the last deployment in minutes:
   150  		err := ddMetrics.Gauge(
   151  			"lastDeployed",
   152  			timeInMinutes,
   153  			[]string{metrics.EventTagApplication + ":" + app, metrics.EventTagEnvironment + ":" + env},
   154  			1)
   155  		return err
   156  	}
   157  	return nil
   158  }
   159  
   160  func sortFiles(gs []os.FileInfo) func(i int, j int) bool {
   161  	return func(i, j int) bool {
   162  		iIndex := gs[i].Name()
   163  		jIndex := gs[j].Name()
   164  		return iIndex < jIndex
   165  	}
   166  }
   167  
   168  func UpdateDatadogMetrics(ctx context.Context, state *State, changes *TransformerResult, now time.Time) error {
   169  	filesystem := state.Filesystem
   170  	if ddMetrics == nil {
   171  		return nil
   172  	}
   173  	configs, err := state.GetEnvironmentConfigs()
   174  	if err != nil {
   175  		return err
   176  	}
   177  	// sorting the environments to get a deterministic order of events:
   178  	var configKeys []string = nil
   179  	for k := range configs {
   180  		configKeys = append(configKeys, k)
   181  	}
   182  	sort.Strings(configKeys)
   183  	for i := range configKeys {
   184  		env := configKeys[i]
   185  		GaugeEnvLockMetric(filesystem, env)
   186  		appsDir := filesystem.Join(environmentDirectory(filesystem, env), "applications")
   187  		if entries, _ := filesystem.ReadDir(appsDir); entries != nil {
   188  			// according to the docs, entries should already be sorted, but turns out it is not, so we sort it:
   189  			sort.Slice(entries, sortFiles(entries))
   190  			for _, app := range entries {
   191  				GaugeEnvAppLockMetric(filesystem, env, app.Name())
   192  
   193  				_, deployedAtTimeUtc, err := state.GetDeploymentMetaData(env, app.Name())
   194  				if err != nil {
   195  					return err
   196  				}
   197  				timeDiff := now.Sub(deployedAtTimeUtc)
   198  				err = GaugeDeploymentMetric(ctx, env, app.Name(), timeDiff.Minutes())
   199  				if err != nil {
   200  					return err
   201  				}
   202  			}
   203  		}
   204  	}
   205  	if changes != nil && ddMetrics != nil {
   206  		for i := range changes.ChangedApps {
   207  			oneChange := changes.ChangedApps[i]
   208  			teamMessage := func() string {
   209  				if oneChange.Team != "" {
   210  					return fmt.Sprintf(" for team %s", oneChange.Team)
   211  				}
   212  				return ""
   213  			}()
   214  			evt := statsd.Event{
   215  				Hostname:       "",
   216  				AggregationKey: "",
   217  				Priority:       "",
   218  				SourceTypeName: "",
   219  				AlertType:      "",
   220  				Title:          "Kuberpult app deployed",
   221  				Text:           fmt.Sprintf("Kuberpult has deployed %s to %s%s", oneChange.App, oneChange.Env, teamMessage),
   222  				Timestamp:      now,
   223  				Tags: []string{
   224  					"kuberpult.application:" + oneChange.App,
   225  					"kuberpult.environment:" + oneChange.Env,
   226  					"kuberpult.team:" + oneChange.Team,
   227  				},
   228  			}
   229  			err := ddMetrics.Event(&evt)
   230  			if err != nil {
   231  				return err
   232  			}
   233  		}
   234  	}
   235  	return nil
   236  }
   237  
   238  func RegularlySendDatadogMetrics(repo Repository, interval time.Duration, callBack func(repository Repository)) {
   239  	metricEventTimer := time.NewTicker(interval * time.Second)
   240  	for range metricEventTimer.C {
   241  		callBack(repo)
   242  	}
   243  }
   244  
   245  func GetRepositoryStateAndUpdateMetrics(ctx context.Context, repo Repository) {
   246  	repoState := repo.State()
   247  	if err := UpdateDatadogMetrics(ctx, repoState, nil, time.Now()); err != nil {
   248  		panic(err.Error())
   249  	}
   250  }
   251  
   252  // A Transformer updates the files in the worktree
   253  type Transformer interface {
   254  	Transform(context.Context, *State, TransformerContext) (commitMsg string, e error)
   255  }
   256  
   257  type TransformerContext interface {
   258  	Execute(Transformer) error
   259  	AddAppEnv(app string, env string, team string)
   260  	DeleteEnvFromApp(app string, env string)
   261  }
   262  
   263  func RunTransformer(ctx context.Context, t Transformer, s *State) (string, *TransformerResult, error) {
   264  	runner := transformerRunner{
   265  		ChangedApps:     nil,
   266  		DeletedRootApps: nil,
   267  		Commits:         nil,
   268  		Context:         ctx,
   269  		State:           s,
   270  		Stack:           [][]string{nil},
   271  	}
   272  	if err := runner.Execute(t); err != nil {
   273  		return "", nil, err
   274  	}
   275  	commitMsg := ""
   276  	if len(runner.Stack[0]) > 0 {
   277  		commitMsg = runner.Stack[0][0]
   278  	}
   279  	return commitMsg, &TransformerResult{
   280  		ChangedApps:     runner.ChangedApps,
   281  		DeletedRootApps: runner.DeletedRootApps,
   282  		Commits:         runner.Commits,
   283  	}, nil
   284  }
   285  
   286  type transformerRunner struct {
   287  	Context context.Context
   288  	State   *State
   289  	// Stores the current stack of commit messages. Each entry of
   290  	// the outer slice corresponds to a step being executed. Each
   291  	// entry of the inner slices correspond to a message generated
   292  	// by that step.
   293  	Stack           [][]string
   294  	ChangedApps     []AppEnv
   295  	DeletedRootApps []RootApp
   296  	Commits         *CommitIds
   297  }
   298  
   299  func (r *transformerRunner) Execute(t Transformer) error {
   300  	r.Stack = append(r.Stack, nil)
   301  	msg, err := t.Transform(r.Context, r.State, r)
   302  	if err != nil {
   303  		return err
   304  	}
   305  	idx := len(r.Stack) - 1
   306  	if len(r.Stack[idx]) != 0 {
   307  		if msg != "" {
   308  			msg = msg + "\n" + strings.Join(r.Stack[idx], "\n")
   309  		} else {
   310  			msg = strings.Join(r.Stack[idx], "\n")
   311  		}
   312  	}
   313  	if msg != "" {
   314  		r.Stack[idx-1] = append(r.Stack[idx-1], msg)
   315  	}
   316  	r.Stack = r.Stack[:idx]
   317  	return nil
   318  }
   319  
   320  func (r *transformerRunner) AddAppEnv(app string, env string, team string) {
   321  	r.ChangedApps = append(r.ChangedApps, AppEnv{
   322  		App:  app,
   323  		Env:  env,
   324  		Team: team,
   325  	})
   326  }
   327  
   328  func (r *transformerRunner) DeleteEnvFromApp(app string, env string) {
   329  	r.ChangedApps = append(r.ChangedApps, AppEnv{
   330  		Team: "",
   331  		App:  app,
   332  		Env:  env,
   333  	})
   334  	r.DeletedRootApps = append(r.DeletedRootApps, RootApp{
   335  		Env: env,
   336  	})
   337  }
   338  
   339  type CreateApplicationVersion struct {
   340  	Authentication
   341  	Version         uint64
   342  	Application     string
   343  	Manifests       map[string]string
   344  	SourceCommitId  string
   345  	SourceAuthor    string
   346  	SourceMessage   string
   347  	SourceRepoUrl   string
   348  	Team            string
   349  	DisplayVersion  string
   350  	WriteCommitData bool
   351  	PreviousCommit  string
   352  	NextCommit      string
   353  }
   354  
   355  type ctxMarkerGenerateUuid struct{}
   356  
   357  var (
   358  	ctxMarkerGenerateUuidKey = &ctxMarkerGenerateUuid{}
   359  )
   360  
   361  func GetLastRelease(fs billy.Filesystem, application string) (uint64, error) {
   362  	var err error
   363  	releasesDir := releasesDirectory(fs, application)
   364  	err = fs.MkdirAll(releasesDir, 0777)
   365  	if err != nil {
   366  		return 0, err
   367  	}
   368  	if entries, err := fs.ReadDir(releasesDir); err != nil {
   369  		return 0, err
   370  	} else {
   371  		var lastRelease uint64 = 0
   372  		for _, e := range entries {
   373  			if i, err := strconv.ParseUint(e.Name(), 10, 64); err != nil {
   374  				//TODO(HVG): decide what to do with bad named releases
   375  			} else {
   376  				if i > lastRelease {
   377  					lastRelease = i
   378  				}
   379  			}
   380  		}
   381  		return lastRelease, nil
   382  	}
   383  }
   384  
   385  func (c *CreateApplicationVersion) Transform(
   386  	ctx context.Context,
   387  	state *State,
   388  	t TransformerContext,
   389  ) (string, error) {
   390  	version, err := c.calculateVersion(state)
   391  	if err != nil {
   392  		return "", err
   393  	}
   394  	fs := state.Filesystem
   395  	if !valid.ApplicationName(c.Application) {
   396  		return "", GetCreateReleaseAppNameTooLong(c.Application, valid.AppNameRegExp, valid.MaxAppNameLen)
   397  	}
   398  	releaseDir := releasesDirectoryWithVersion(fs, c.Application, version)
   399  	appDir := applicationDirectory(fs, c.Application)
   400  	if err = fs.MkdirAll(releaseDir, 0777); err != nil {
   401  		return "", GetCreateReleaseGeneralFailure(err)
   402  	}
   403  
   404  	var checkForInvalidCommitId = func(commitId, helperText string) {
   405  		if !valid.SHA1CommitID(commitId) {
   406  			logger.FromContext(ctx).
   407  				Sugar().
   408  				Warnf("%s commit ID is not a valid SHA1 hash, should be exactly 40 characters [0-9a-fA-F] %s\n", commitId, helperText)
   409  		}
   410  	}
   411  
   412  	checkForInvalidCommitId(c.SourceCommitId, "Source")
   413  	checkForInvalidCommitId(c.PreviousCommit, "Previous")
   414  	checkForInvalidCommitId(c.NextCommit, "Next")
   415  
   416  	configs, err := state.GetEnvironmentConfigs()
   417  	if err != nil {
   418  		if errors.Is(err, InvalidJson) {
   419  			return "", err
   420  		}
   421  		return "", GetCreateReleaseGeneralFailure(err)
   422  	}
   423  
   424  	if c.SourceCommitId != "" {
   425  		c.SourceCommitId = strings.ToLower(c.SourceCommitId)
   426  		if err := util.WriteFile(fs, fs.Join(releaseDir, fieldSourceCommitId), []byte(c.SourceCommitId), 0666); err != nil {
   427  			return "", GetCreateReleaseGeneralFailure(err)
   428  		}
   429  	}
   430  
   431  	if c.SourceAuthor != "" {
   432  		if err := util.WriteFile(fs, fs.Join(releaseDir, fieldSourceAuthor), []byte(c.SourceAuthor), 0666); err != nil {
   433  			return "", GetCreateReleaseGeneralFailure(err)
   434  		}
   435  	}
   436  	if c.SourceMessage != "" {
   437  		if err := util.WriteFile(fs, fs.Join(releaseDir, fieldSourceMessage), []byte(c.SourceMessage), 0666); err != nil {
   438  			return "", GetCreateReleaseGeneralFailure(err)
   439  		}
   440  	}
   441  	if c.DisplayVersion != "" {
   442  		if err := util.WriteFile(fs, fs.Join(releaseDir, fieldDisplayVersion), []byte(c.DisplayVersion), 0666); err != nil {
   443  			return "", GetCreateReleaseGeneralFailure(err)
   444  		}
   445  	}
   446  	if err := util.WriteFile(fs, fs.Join(releaseDir, fieldCreatedAt), []byte(getTimeNow(ctx).Format(time.RFC3339)), 0666); err != nil {
   447  		return "", GetCreateReleaseGeneralFailure(err)
   448  	}
   449  	if c.Team != "" {
   450  		if err := util.WriteFile(fs, fs.Join(appDir, fieldTeam), []byte(c.Team), 0666); err != nil {
   451  			return "", GetCreateReleaseGeneralFailure(err)
   452  		}
   453  	}
   454  	if c.SourceRepoUrl != "" {
   455  		if err := util.WriteFile(fs, fs.Join(appDir, fieldSourceRepoUrl), []byte(c.SourceRepoUrl), 0666); err != nil {
   456  			return "", GetCreateReleaseGeneralFailure(err)
   457  		}
   458  	}
   459  	isLatest, err := isLatestsVersion(state, c.Application, version)
   460  	if err != nil {
   461  		return "", GetCreateReleaseGeneralFailure(err)
   462  	}
   463  	if !isLatest {
   464  		// check that we can actually backfill this version
   465  		oldVersions, err := findOldApplicationVersions(state, c.Application)
   466  		if err != nil {
   467  			return "", GetCreateReleaseGeneralFailure(err)
   468  		}
   469  		for _, oldVersion := range oldVersions {
   470  			if version == oldVersion {
   471  				return "", GetCreateReleaseTooOld()
   472  			}
   473  		}
   474  	}
   475  
   476  	var allEnvsOfThisApp []string = nil
   477  
   478  	for env := range c.Manifests {
   479  		allEnvsOfThisApp = append(allEnvsOfThisApp, env)
   480  	}
   481  	gen := getGenerator(ctx)
   482  	eventUuid := gen.Generate()
   483  	if c.WriteCommitData {
   484  		err = writeCommitData(ctx, c.SourceCommitId, c.SourceMessage, c.Application, eventUuid, allEnvsOfThisApp, c.PreviousCommit, c.NextCommit, fs)
   485  		if err != nil {
   486  			return "", GetCreateReleaseGeneralFailure(err)
   487  		}
   488  	}
   489  
   490  	for env, man := range c.Manifests {
   491  		err := state.checkUserPermissions(ctx, env, c.Application, auth.PermissionCreateRelease, c.Team, c.RBACConfig)
   492  		if err != nil {
   493  			return "", GetCreateReleaseGeneralFailure(err)
   494  		}
   495  		envDir := fs.Join(releaseDir, "environments", env)
   496  
   497  		config, found := configs[env]
   498  		hasUpstream := false
   499  		if found {
   500  			hasUpstream = config.Upstream != nil
   501  		}
   502  
   503  		if err = fs.MkdirAll(envDir, 0777); err != nil {
   504  			return "", GetCreateReleaseGeneralFailure(err)
   505  		}
   506  		if err := util.WriteFile(fs, fs.Join(envDir, "manifests.yaml"), []byte(man), 0666); err != nil {
   507  			return "", GetCreateReleaseGeneralFailure(err)
   508  		}
   509  		teamOwner, err := state.GetApplicationTeamOwner(c.Application)
   510  		if err != nil {
   511  			return "", err
   512  		}
   513  		t.AddAppEnv(c.Application, env, teamOwner)
   514  		if hasUpstream && config.Upstream.Latest && isLatest {
   515  			d := &DeployApplicationVersion{
   516  				SourceTrain:     nil,
   517  				Environment:     env,
   518  				Application:     c.Application,
   519  				Version:         version, // the train should queue deployments, instead of giving up:
   520  				LockBehaviour:   api.LockBehavior_RECORD,
   521  				Authentication:  c.Authentication,
   522  				WriteCommitData: c.WriteCommitData,
   523  			}
   524  			err := t.Execute(d)
   525  			if err != nil {
   526  				_, ok := err.(*LockedError)
   527  				if ok {
   528  					continue // LockedErrors are expected
   529  				} else {
   530  					return "", GetCreateReleaseGeneralFailure(err)
   531  				}
   532  			}
   533  		}
   534  	}
   535  	return fmt.Sprintf("created version %d of %q", version, c.Application), nil
   536  }
   537  
   538  func getGenerator(ctx context.Context) uuid.GenerateUUIDs {
   539  	gen, ok := ctx.Value(ctxMarkerGenerateUuidKey).(uuid.GenerateUUIDs)
   540  	if !ok || gen == nil {
   541  		return uuid.RealUUIDGenerator{}
   542  	}
   543  	return gen
   544  }
   545  
   546  func AddGeneratorToContext(ctx context.Context, gen uuid.GenerateUUIDs) context.Context {
   547  	return context.WithValue(ctx, ctxMarkerGenerateUuidKey, gen)
   548  }
   549  
   550  func writeCommitData(ctx context.Context, sourceCommitId string, sourceMessage string, app string, eventId string, environments []string, previousCommitId string, nextCommitId string, fs billy.Filesystem) error {
   551  	if !valid.SHA1CommitID(sourceCommitId) {
   552  		return nil
   553  	}
   554  	commitDir := commitDirectory(fs, sourceCommitId)
   555  	if err := fs.MkdirAll(commitDir, 0777); err != nil {
   556  		return GetCreateReleaseGeneralFailure(err)
   557  	}
   558  	if err := util.WriteFile(fs, fs.Join(commitDir, ".empty"), make([]byte, 0), 0666); err != nil {
   559  		return GetCreateReleaseGeneralFailure(err)
   560  	}
   561  
   562  	if previousCommitId != "" && valid.SHA1CommitID(previousCommitId) {
   563  		if err := writeNextPrevInfo(ctx, sourceCommitId, strings.ToLower(previousCommitId), fieldPreviousCommitId, app, fs); err != nil {
   564  			return GetCreateReleaseGeneralFailure(err)
   565  		}
   566  	}
   567  	if nextCommitId != "" && valid.SHA1CommitID(nextCommitId) {
   568  		if err := writeNextPrevInfo(ctx, sourceCommitId, strings.ToLower(nextCommitId), fieldNextCommidId, app, fs); err != nil {
   569  			return GetCreateReleaseGeneralFailure(err)
   570  		}
   571  	}
   572  
   573  	commitAppDir := commitApplicationDirectory(fs, sourceCommitId, app)
   574  	if err := fs.MkdirAll(commitAppDir, 0777); err != nil {
   575  		return GetCreateReleaseGeneralFailure(err)
   576  	}
   577  	if err := util.WriteFile(fs, fs.Join(commitDir, ".gitkeep"), make([]byte, 0), 0666); err != nil {
   578  		return err
   579  	}
   580  	if err := util.WriteFile(fs, fs.Join(commitDir, "source_message"), []byte(sourceMessage), 0666); err != nil {
   581  		return GetCreateReleaseGeneralFailure(err)
   582  	}
   583  
   584  	if err := util.WriteFile(fs, fs.Join(commitAppDir, ".gitkeep"), make([]byte, 0), 0666); err != nil {
   585  		return GetCreateReleaseGeneralFailure(err)
   586  	}
   587  	envMap := make(map[string]struct{}, len(environments))
   588  	for _, env := range environments {
   589  		envMap[env] = struct{}{}
   590  	}
   591  	err := writeEvent(eventId, sourceCommitId, fs, &event.NewRelease{
   592  		Environments: envMap,
   593  	})
   594  	if err != nil {
   595  		return fmt.Errorf("error while writing event: %v", err)
   596  	}
   597  	return nil
   598  }
   599  
   600  func writeNextPrevInfo(ctx context.Context, sourceCommitId string, otherCommitId string, fieldSource string, application string, fs billy.Filesystem) error {
   601  
   602  	otherCommitId = strings.ToLower(otherCommitId)
   603  	sourceCommitDir := commitDirectory(fs, sourceCommitId)
   604  
   605  	otherCommitDir := commitDirectory(fs, otherCommitId)
   606  
   607  	if _, err := fs.Stat(otherCommitDir); err != nil {
   608  		logger.FromContext(ctx).Sugar().Warnf(
   609  			"Could not find the previous commit while trying to create a new release for commit %s and application %s. This is expected when `git.enableWritingCommitData` was just turned on, however it should not happen multiple times.", otherCommitId, application, otherCommitDir)
   610  		return nil
   611  	}
   612  
   613  	if err := util.WriteFile(fs, fs.Join(sourceCommitDir, fieldSource), []byte(otherCommitId), 0666); err != nil {
   614  		return err
   615  	}
   616  	fieldOther := ""
   617  	if otherCommitId != "" {
   618  
   619  		if fieldSource == fieldPreviousCommitId {
   620  			fieldOther = fieldNextCommidId
   621  		} else {
   622  			fieldOther = fieldPreviousCommitId
   623  		}
   624  
   625  		//This is a workaround. util.WriteFile does NOT truncate file contents, so we simply delete the file before writing.
   626  		if err := fs.Remove(fs.Join(otherCommitDir, fieldOther)); err != nil && !errors.Is(err, os.ErrNotExist) {
   627  			return err
   628  		}
   629  
   630  		if err := util.WriteFile(fs, fs.Join(otherCommitDir, fieldOther), []byte(sourceCommitId), 0666); err != nil {
   631  			return err
   632  		}
   633  	}
   634  	return nil
   635  }
   636  
   637  func writeEvent(
   638  	eventId string,
   639  	sourceCommitId string,
   640  	filesystem billy.Filesystem,
   641  	ev event.Event,
   642  ) error {
   643  	eventDir := commitEventDir(filesystem, sourceCommitId, eventId)
   644  	if err := event.Write(filesystem, eventDir, ev); err != nil {
   645  		return fmt.Errorf(
   646  			"could not write an event for commit %s for uuid %s, error: %w",
   647  			sourceCommitId, eventId, err)
   648  	}
   649  	return nil
   650  
   651  }
   652  
   653  func (c *CreateApplicationVersion) calculateVersion(state *State) (uint64, error) {
   654  	bfs := state.Filesystem
   655  	if c.Version == 0 {
   656  		lastRelease, err := GetLastRelease(bfs, c.Application)
   657  		if err != nil {
   658  			return 0, err
   659  		}
   660  		return lastRelease + 1, nil
   661  	} else {
   662  		// check that the version doesn't already exist
   663  		dir := releasesDirectoryWithVersion(bfs, c.Application, c.Version)
   664  		_, err := bfs.Stat(dir)
   665  		if err != nil {
   666  			if !errors.Is(err, fs.ErrNotExist) {
   667  				return 0, err
   668  			}
   669  		} else {
   670  			// check if version differs
   671  			return 0, c.sameAsExisting(state, c.Version)
   672  		}
   673  		// TODO: check GC here
   674  		return c.Version, nil
   675  	}
   676  }
   677  
   678  func (c *CreateApplicationVersion) sameAsExisting(state *State, version uint64) error {
   679  	fs := state.Filesystem
   680  	releaseDir := releasesDirectoryWithVersion(fs, c.Application, version)
   681  	appDir := applicationDirectory(fs, c.Application)
   682  	if c.SourceCommitId != "" {
   683  		existingSourceCommitId, err := util.ReadFile(fs, fs.Join(releaseDir, fieldSourceCommitId))
   684  		if err != nil {
   685  			return GetCreateReleaseAlreadyExistsDifferent(api.DifferingField_SOURCE_COMMIT_ID, "")
   686  		}
   687  		existingSourceCommitIdStr := string(existingSourceCommitId)
   688  		if existingSourceCommitIdStr != c.SourceCommitId {
   689  			return GetCreateReleaseAlreadyExistsDifferent(api.DifferingField_SOURCE_COMMIT_ID, createUnifiedDiff(existingSourceCommitIdStr, c.SourceCommitId, ""))
   690  		}
   691  	}
   692  	if c.SourceAuthor != "" {
   693  		existingSourceAuthor, err := util.ReadFile(fs, fs.Join(releaseDir, fieldSourceAuthor))
   694  		if err != nil {
   695  			return GetCreateReleaseAlreadyExistsDifferent(api.DifferingField_SOURCE_AUTHOR, "")
   696  		}
   697  		existingSourceAuthorStr := string(existingSourceAuthor)
   698  		if existingSourceAuthorStr != c.SourceAuthor {
   699  			return GetCreateReleaseAlreadyExistsDifferent(api.DifferingField_SOURCE_AUTHOR, createUnifiedDiff(existingSourceAuthorStr, c.SourceAuthor, ""))
   700  		}
   701  	}
   702  	if c.SourceMessage != "" {
   703  		existingSourceMessage, err := util.ReadFile(fs, fs.Join(releaseDir, fieldSourceMessage))
   704  		if err != nil {
   705  			return GetCreateReleaseAlreadyExistsDifferent(api.DifferingField_SOURCE_MESSAGE, "")
   706  		}
   707  		existingSourceMessageStr := string(existingSourceMessage)
   708  		if existingSourceMessageStr != c.SourceMessage {
   709  			return GetCreateReleaseAlreadyExistsDifferent(api.DifferingField_SOURCE_MESSAGE, createUnifiedDiff(existingSourceMessageStr, c.SourceMessage, ""))
   710  		}
   711  	}
   712  	if c.DisplayVersion != "" {
   713  		existingDisplayVersion, err := util.ReadFile(fs, fs.Join(releaseDir, fieldDisplayVersion))
   714  		if err != nil {
   715  			return GetCreateReleaseAlreadyExistsDifferent(api.DifferingField_DISPLAY_VERSION, "")
   716  		}
   717  		existingDisplayVersionStr := string(existingDisplayVersion)
   718  		if existingDisplayVersionStr != c.DisplayVersion {
   719  			return GetCreateReleaseAlreadyExistsDifferent(api.DifferingField_DISPLAY_VERSION, createUnifiedDiff(existingDisplayVersionStr, c.DisplayVersion, ""))
   720  		}
   721  	}
   722  	if c.Team != "" {
   723  		existingTeam, err := util.ReadFile(fs, fs.Join(appDir, fieldTeam))
   724  		if err != nil {
   725  			return GetCreateReleaseAlreadyExistsDifferent(api.DifferingField_TEAM, "")
   726  		}
   727  		existingTeamStr := string(existingTeam)
   728  		if existingTeamStr != c.Team {
   729  			return GetCreateReleaseAlreadyExistsDifferent(api.DifferingField_TEAM, createUnifiedDiff(existingTeamStr, c.Team, ""))
   730  		}
   731  	}
   732  	if c.SourceRepoUrl != "" {
   733  		existingSourceRepoUrl, err := util.ReadFile(fs, fs.Join(releaseDir, fieldSourceCommitId))
   734  		if err != nil {
   735  			return GetCreateReleaseAlreadyExistsDifferent(api.DifferingField_SOURCE_REPO_URL, "")
   736  		}
   737  		existingSourceRepoUrlStr := string(existingSourceRepoUrl)
   738  		if existingSourceRepoUrlStr != c.SourceRepoUrl {
   739  			return GetCreateReleaseAlreadyExistsDifferent(api.DifferingField_SOURCE_REPO_URL, createUnifiedDiff(existingSourceRepoUrlStr, c.SourceRepoUrl, ""))
   740  		}
   741  	}
   742  	for env, man := range c.Manifests {
   743  		envDir := fs.Join(releaseDir, "environments", env)
   744  		existingMan, err := util.ReadFile(fs, fs.Join(envDir, "manifests.yaml"))
   745  		if err != nil {
   746  			return GetCreateReleaseAlreadyExistsDifferent(api.DifferingField_MANIFESTS, fmt.Sprintf("manifest missing for env %s", env))
   747  		}
   748  		existingManStr := string(existingMan)
   749  		if canonicalizeYaml(existingManStr) != canonicalizeYaml(man) {
   750  			return GetCreateReleaseAlreadyExistsDifferent(api.DifferingField_MANIFESTS, createUnifiedDiff(existingManStr, man, fmt.Sprintf("%s-", env)))
   751  		}
   752  	}
   753  	return GetCreateReleaseAlreadyExistsSame()
   754  }
   755  
   756  type RawNode struct{ *yaml3.Node }
   757  
   758  func (n *RawNode) UnmarshalYAML(node *yaml3.Node) error {
   759  	n.Node = node
   760  	return nil
   761  }
   762  
   763  func canonicalizeYaml(unformatted string) string {
   764  	var target RawNode
   765  	if errDeserial := yaml3.Unmarshal([]byte(unformatted), &target); errDeserial != nil {
   766  		return yamlParsingError // we only use this for comparisons
   767  	}
   768  	if canonicalData, errSerial := yaml3.Marshal(target.Node); errSerial == nil {
   769  		return string(canonicalData)
   770  	} else {
   771  		return yamlParsingError // only for comparisons
   772  	}
   773  }
   774  
   775  func createUnifiedDiff(existingValue string, requestValue string, prefix string) string {
   776  	existingValueStr := string(existingValue)
   777  	existingFilename := fmt.Sprintf("%sexisting", prefix)
   778  	requestFilename := fmt.Sprintf("%srequest", prefix)
   779  	edits := myers.ComputeEdits(span.URIFromPath(existingFilename), existingValueStr, string(requestValue))
   780  	return fmt.Sprint(gotextdiff.ToUnified(existingFilename, requestFilename, existingValueStr, edits))
   781  }
   782  
   783  func isLatestsVersion(state *State, application string, version uint64) (bool, error) {
   784  	rels, err := state.GetApplicationReleases(application)
   785  	if err != nil {
   786  		return false, err
   787  	}
   788  	for _, r := range rels {
   789  		if r > version {
   790  			return false, nil
   791  		}
   792  	}
   793  	return true, nil
   794  }
   795  
   796  type CreateUndeployApplicationVersion struct {
   797  	Authentication
   798  	Application     string
   799  	WriteCommitData bool
   800  }
   801  
   802  func (c *CreateUndeployApplicationVersion) Transform(
   803  	ctx context.Context,
   804  	state *State,
   805  	t TransformerContext,
   806  ) (string, error) {
   807  	fs := state.Filesystem
   808  	lastRelease, err := GetLastRelease(fs, c.Application)
   809  	if err != nil {
   810  		return "", err
   811  	}
   812  	if lastRelease == 0 {
   813  		return "", fmt.Errorf("cannot undeploy non-existing application '%v'", c.Application)
   814  	}
   815  
   816  	releaseDir := releasesDirectoryWithVersion(fs, c.Application, lastRelease+1)
   817  	if err = fs.MkdirAll(releaseDir, 0777); err != nil {
   818  		return "", err
   819  	}
   820  
   821  	configs, err := state.GetEnvironmentConfigs()
   822  	if err != nil {
   823  		return "", err
   824  	}
   825  	// this is a flag to indicate that this is the special "undeploy" version
   826  	if err := util.WriteFile(fs, fs.Join(releaseDir, "undeploy"), []byte(""), 0666); err != nil {
   827  		return "", err
   828  	}
   829  	if err := util.WriteFile(fs, fs.Join(releaseDir, fieldCreatedAt), []byte(getTimeNow(ctx).Format(time.RFC3339)), 0666); err != nil {
   830  		return "", err
   831  	}
   832  	for env := range configs {
   833  		err := state.checkUserPermissions(ctx, env, c.Application, auth.PermissionCreateUndeploy, "", c.RBACConfig)
   834  		if err != nil {
   835  			return "", err
   836  		}
   837  		envDir := fs.Join(releaseDir, "environments", env)
   838  
   839  		config, found := configs[env]
   840  		hasUpstream := false
   841  		if found {
   842  			hasUpstream = config.Upstream != nil
   843  		}
   844  
   845  		if err = fs.MkdirAll(envDir, 0777); err != nil {
   846  			return "", err
   847  		}
   848  		// note that the manifest is empty here!
   849  		// but actually it's not quite empty!
   850  		// The function we are using in DeployApplication version is `util.WriteFile`. And that does not allow overwriting files with empty content.
   851  		// We work around this unusual behavior by writing a space into the file
   852  		if err := util.WriteFile(fs, fs.Join(envDir, "manifests.yaml"), []byte(" "), 0666); err != nil {
   853  			return "", err
   854  		}
   855  		teamOwner, err := state.GetApplicationTeamOwner(c.Application)
   856  		if err != nil {
   857  			return "", err
   858  		}
   859  		t.AddAppEnv(c.Application, env, teamOwner)
   860  		if hasUpstream && config.Upstream.Latest {
   861  			d := &DeployApplicationVersion{
   862  				SourceTrain: nil,
   863  				Environment: env,
   864  				Application: c.Application,
   865  				Version:     lastRelease + 1,
   866  				// the train should queue deployments, instead of giving up:
   867  				LockBehaviour:   api.LockBehavior_RECORD,
   868  				Authentication:  c.Authentication,
   869  				WriteCommitData: c.WriteCommitData,
   870  			}
   871  			err := t.Execute(d)
   872  			if err != nil {
   873  				_, ok := err.(*LockedError)
   874  				if ok {
   875  					continue // locked error are expected
   876  				} else {
   877  					return "", err
   878  				}
   879  			}
   880  		}
   881  	}
   882  	return fmt.Sprintf("created undeploy-version %d of '%v'", lastRelease+1, c.Application), nil
   883  }
   884  
   885  func removeCommit(fs billy.Filesystem, commitID, application string) error {
   886  	errorTemplate := func(message string, err error) error {
   887  		return fmt.Errorf("while removing applicaton %s from commit %s and error was encountered, message: %s, error %w", application, commitID, message, err)
   888  	}
   889  
   890  	commitApplicationDir := commitApplicationDirectory(fs, commitID, application)
   891  	if err := fs.Remove(commitApplicationDir); err != nil {
   892  		if os.IsNotExist(err) {
   893  			// could not read the directory commitApplicationDir - but that's ok, because we don't know
   894  			// if the kuberpult version that accepted this commit in the release endpoint, did already have commit writing enabled.
   895  			// So there's no guarantee that this file ever existed
   896  			return nil
   897  		}
   898  		return errorTemplate(fmt.Sprintf("could not remove the application directory %s", commitApplicationDir), err)
   899  	}
   900  	// check if there are no other services updated by this commit
   901  	// if there are none, start removing the entire branch of the commit
   902  
   903  	deleteDirIfEmpty := func(dir string) error {
   904  		files, err := fs.ReadDir(dir)
   905  		if err != nil {
   906  			return errorTemplate(fmt.Sprintf("could not read the directory %s", dir), err)
   907  		}
   908  		if len(files) == 0 {
   909  			if err = fs.Remove(dir); err != nil {
   910  				return errorTemplate(fmt.Sprintf("could not remove the directory %s", dir), err)
   911  			}
   912  		}
   913  		return nil
   914  	}
   915  
   916  	commitApplicationsDir := path.Dir(commitApplicationDir)
   917  	if err := deleteDirIfEmpty(commitApplicationsDir); err != nil {
   918  		return errorTemplate(fmt.Sprintf("could not remove directory %s", commitApplicationsDir), err)
   919  	}
   920  	commitDir2 := path.Dir(commitApplicationsDir)
   921  
   922  	// if there are no more apps in the "applications" dir, then remove the commit message file and continue cleaning going up
   923  	if _, err := fs.Stat(commitApplicationsDir); err != nil {
   924  		if os.IsNotExist(err) {
   925  			if err := fs.Remove(fs.Join(commitDir2)); err != nil {
   926  				return errorTemplate(fmt.Sprintf("could not remove commit dir %s file", commitDir2), err)
   927  			}
   928  		} else {
   929  			return errorTemplate(fmt.Sprintf("could not stat directory %s with an unexpected error", commitApplicationsDir), err)
   930  		}
   931  	}
   932  
   933  	commitDir1 := path.Dir(commitDir2)
   934  	if err := deleteDirIfEmpty(commitDir1); err != nil {
   935  		return errorTemplate(fmt.Sprintf("could not remove directory %s", commitDir2), err)
   936  	}
   937  
   938  	return nil
   939  }
   940  
   941  type UndeployApplication struct {
   942  	Authentication
   943  	Application string
   944  }
   945  
   946  func (u *UndeployApplication) Transform(
   947  	ctx context.Context,
   948  	state *State,
   949  	t TransformerContext,
   950  ) (string, error) {
   951  	fs := state.Filesystem
   952  	lastRelease, err := GetLastRelease(fs, u.Application)
   953  	if err != nil {
   954  		return "", err
   955  	}
   956  	if lastRelease == 0 {
   957  		return "", fmt.Errorf("UndeployApplication: error cannot undeploy non-existing application '%v'", u.Application)
   958  	}
   959  	isUndeploy, err := state.IsUndeployVersion(u.Application, lastRelease)
   960  	if err != nil {
   961  		return "", err
   962  	}
   963  	if !isUndeploy {
   964  		return "", fmt.Errorf("UndeployApplication: error last release is not un-deployed application version of '%v'", u.Application)
   965  	}
   966  	appDir := applicationDirectory(fs, u.Application)
   967  	configs, err := state.GetEnvironmentConfigs()
   968  	if err != nil {
   969  		return "", err
   970  	}
   971  	for env := range configs {
   972  		err := state.checkUserPermissions(ctx, env, u.Application, auth.PermissionDeployUndeploy, "", u.RBACConfig)
   973  		if err != nil {
   974  			return "", err
   975  		}
   976  		envAppDir := environmentApplicationDirectory(fs, env, u.Application)
   977  		entries, err := fs.ReadDir(envAppDir)
   978  		if err != nil {
   979  			return "", wrapFileError(err, envAppDir, "UndeployApplication: Could not open application directory. Does the app exist?")
   980  		}
   981  		if entries == nil {
   982  			// app was never deployed on this env, so we must ignore it!
   983  			continue
   984  		}
   985  
   986  		appLocksDir := fs.Join(envAppDir, "locks")
   987  		err = fs.Remove(appLocksDir)
   988  		if err != nil {
   989  			return "", fmt.Errorf("UndeployApplication: cannot delete app locks '%v'", appLocksDir)
   990  		}
   991  
   992  		versionDir := fs.Join(envAppDir, "version")
   993  		undeployFile := fs.Join(versionDir, "undeploy")
   994  
   995  		_, err = fs.Stat(versionDir)
   996  		if err != nil && errors.Is(err, os.ErrNotExist) {
   997  			// if the app was never deployed here, that's not a reason to stop
   998  			continue
   999  		}
  1000  
  1001  		_, err = fs.Stat(undeployFile)
  1002  		if err != nil && errors.Is(err, os.ErrNotExist) {
  1003  			return "", fmt.Errorf("UndeployApplication: error cannot un-deploy application '%v' the release '%v' is not un-deployed: '%v'", u.Application, env, undeployFile)
  1004  		}
  1005  
  1006  	}
  1007  	// remove application
  1008  	releasesDir := fs.Join(appDir, "releases")
  1009  	files, err := fs.ReadDir(releasesDir)
  1010  	if err != nil {
  1011  		return "", fmt.Errorf("could not read the releases directory %s %w", releasesDir, err)
  1012  	}
  1013  	for _, file := range files {
  1014  		if file.IsDir() {
  1015  			releaseDir := fs.Join(releasesDir, file.Name())
  1016  			commitIDFile := fs.Join(releaseDir, "source_commit_id")
  1017  			var commitID string
  1018  			dat, err := util.ReadFile(fs, commitIDFile)
  1019  			if err != nil {
  1020  				// release does not have a corresponding commit, which might be the case if it's an undeploy release, no prob
  1021  				continue
  1022  			}
  1023  			commitID = string(dat)
  1024  			if valid.SHA1CommitID(commitID) {
  1025  				if err := removeCommit(fs, commitID, u.Application); err != nil {
  1026  					return "", fmt.Errorf("could not remove the commit: %w", err)
  1027  				}
  1028  			}
  1029  		}
  1030  	}
  1031  	if err = fs.Remove(appDir); err != nil {
  1032  		return "", err
  1033  	}
  1034  	for env := range configs {
  1035  		appDir := environmentApplicationDirectory(fs, env, u.Application)
  1036  		teamOwner, err := state.GetApplicationTeamOwner(u.Application)
  1037  		if err != nil {
  1038  			return "", err
  1039  		}
  1040  		t.AddAppEnv(u.Application, env, teamOwner)
  1041  		// remove environment application
  1042  		if err := fs.Remove(appDir); err != nil && !errors.Is(err, os.ErrNotExist) {
  1043  			return "", fmt.Errorf("UndeployApplication: unexpected error application '%v' environment '%v': '%w'", u.Application, env, err)
  1044  		}
  1045  	}
  1046  	return fmt.Sprintf("application '%v' was deleted successfully", u.Application), nil
  1047  }
  1048  
  1049  type DeleteEnvFromApp struct {
  1050  	Authentication
  1051  	Application string
  1052  	Environment string
  1053  }
  1054  
  1055  func (u *DeleteEnvFromApp) Transform(
  1056  	ctx context.Context,
  1057  	state *State,
  1058  	t TransformerContext,
  1059  ) (string, error) {
  1060  	err := state.checkUserPermissions(ctx, u.Environment, u.Application, auth.PermissionDeleteEnvironmentApplication, "", u.RBACConfig)
  1061  	if err != nil {
  1062  		return "", err
  1063  	}
  1064  	fs := state.Filesystem
  1065  	thisSprintf := func(format string, a ...any) string {
  1066  		return fmt.Sprintf("DeleteEnvFromApp app '%s' on env '%s': %s", u.Application, u.Environment, fmt.Sprintf(format, a...))
  1067  	}
  1068  
  1069  	if u.Application == "" {
  1070  		return "", fmt.Errorf(thisSprintf("Need to provide the application"))
  1071  	}
  1072  
  1073  	if u.Environment == "" {
  1074  		return "", fmt.Errorf(thisSprintf("Need to provide the environment"))
  1075  	}
  1076  
  1077  	envAppDir := environmentApplicationDirectory(fs, u.Environment, u.Application)
  1078  	entries, err := fs.ReadDir(envAppDir)
  1079  	if err != nil {
  1080  		return "", wrapFileError(err, envAppDir, thisSprintf("Could not open application directory. Does the app exist?"))
  1081  	}
  1082  
  1083  	if entries == nil {
  1084  		// app was never deployed on this env, so that's unusual - but for idempotency we treat it just like a success case:
  1085  		return fmt.Sprintf("Attempted to remove environment '%v' from application '%v' but it did not exist.", u.Environment, u.Application), nil
  1086  	}
  1087  
  1088  	err = fs.Remove(envAppDir)
  1089  	if err != nil {
  1090  		return "", wrapFileError(err, envAppDir, thisSprintf("Cannot delete app.'"))
  1091  	}
  1092  
  1093  	t.DeleteEnvFromApp(u.Application, u.Environment)
  1094  	return fmt.Sprintf("Environment '%v' was removed from application '%v' successfully.", u.Environment, u.Application), nil
  1095  }
  1096  
  1097  type CleanupOldApplicationVersions struct {
  1098  	Application string
  1099  }
  1100  
  1101  // Finds old releases for an application
  1102  func findOldApplicationVersions(state *State, name string) ([]uint64, error) {
  1103  	// 1) get release in each env:
  1104  	envConfigs, err := state.GetEnvironmentConfigs()
  1105  	if err != nil {
  1106  		return nil, err
  1107  	}
  1108  	versions, err := state.GetApplicationReleases(name)
  1109  	if err != nil {
  1110  		return nil, err
  1111  	}
  1112  	if len(versions) == 0 {
  1113  		return nil, err
  1114  	}
  1115  	sort.Slice(versions, func(i, j int) bool {
  1116  		return versions[i] < versions[j]
  1117  	})
  1118  	// Use the latest version as oldest deployed version
  1119  	oldestDeployedVersion := versions[len(versions)-1]
  1120  	for env := range envConfigs {
  1121  		version, err := state.GetEnvironmentApplicationVersion(env, name)
  1122  		if err != nil {
  1123  			return nil, err
  1124  		}
  1125  		if version != nil {
  1126  			if *version < oldestDeployedVersion {
  1127  				oldestDeployedVersion = *version
  1128  			}
  1129  		}
  1130  	}
  1131  	positionOfOldestVersion := sort.Search(len(versions), func(i int) bool {
  1132  		return versions[i] >= oldestDeployedVersion
  1133  	})
  1134  
  1135  	if positionOfOldestVersion < (keptVersionsOnCleanup - 1) {
  1136  		return nil, nil
  1137  	}
  1138  	return versions[0 : positionOfOldestVersion-(keptVersionsOnCleanup-1)], err
  1139  }
  1140  
  1141  func (c *CleanupOldApplicationVersions) Transform(
  1142  	ctx context.Context,
  1143  	state *State,
  1144  	t TransformerContext,
  1145  ) (string, error) {
  1146  	fs := state.Filesystem
  1147  	oldVersions, err := findOldApplicationVersions(state, c.Application)
  1148  	if err != nil {
  1149  		return "", fmt.Errorf("cleanup: could not get application releases for app '%s': %w", c.Application, err)
  1150  	}
  1151  
  1152  	msg := ""
  1153  	for _, oldRelease := range oldVersions {
  1154  		// delete oldRelease:
  1155  		releasesDir := releasesDirectoryWithVersion(fs, c.Application, oldRelease)
  1156  		_, err := fs.Stat(releasesDir)
  1157  		if err != nil {
  1158  			return "", wrapFileError(err, releasesDir, "CleanupOldApplicationVersions: could not stat")
  1159  		}
  1160  
  1161  		{
  1162  			commitIDFile := fs.Join(releasesDir, fieldSourceCommitId)
  1163  			dat, err := util.ReadFile(fs, commitIDFile)
  1164  			if err != nil {
  1165  				// not a problem, might be the undeploy commit or the commit has was not specified in CreateApplicationVersion
  1166  			} else {
  1167  				commitID := string(dat)
  1168  				if valid.SHA1CommitID(commitID) {
  1169  					if err := removeCommit(fs, commitID, c.Application); err != nil {
  1170  						return "", wrapFileError(err, releasesDir, "CleanupOldApplicationVersions: could not remove commit path")
  1171  					}
  1172  				}
  1173  			}
  1174  		}
  1175  
  1176  		err = fs.Remove(releasesDir)
  1177  		if err != nil {
  1178  			return "", fmt.Errorf("CleanupOldApplicationVersions: Unexpected error app %s: %w",
  1179  				c.Application, err)
  1180  		}
  1181  		msg = fmt.Sprintf("%sremoved version %d of app %v as cleanup\n", msg, oldRelease, c.Application)
  1182  	}
  1183  	// we only cleanup non-deployed versions, so there are not changes for argoCd here
  1184  	return msg, nil
  1185  }
  1186  
  1187  func wrapFileError(e error, filename string, message string) error {
  1188  	return fmt.Errorf("%s '%s': %w", message, filename, e)
  1189  }
  1190  
  1191  type Authentication struct {
  1192  	RBACConfig auth.RBACConfig
  1193  }
  1194  
  1195  type CreateEnvironmentLock struct {
  1196  	Authentication
  1197  	Environment string
  1198  	LockId      string
  1199  	Message     string
  1200  }
  1201  
  1202  func (s *State) checkUserPermissions(ctx context.Context, env, application, action, team string, RBACConfig auth.RBACConfig) error {
  1203  	if !RBACConfig.DexEnabled {
  1204  		return nil
  1205  	}
  1206  	user, err := auth.ReadUserFromContext(ctx)
  1207  	if err != nil {
  1208  		return fmt.Errorf(fmt.Sprintf("checkUserPermissions: user not found: %v", err))
  1209  	}
  1210  
  1211  	envs, err := s.GetEnvironmentConfigs()
  1212  	if err != nil {
  1213  		return err
  1214  	}
  1215  	var group string
  1216  	for envName, config := range envs {
  1217  		if envName == env {
  1218  			group = mapper.DeriveGroupName(config, env)
  1219  			break
  1220  		}
  1221  	}
  1222  	if group == "" {
  1223  		return fmt.Errorf("group not found for environment: %s", env)
  1224  	}
  1225  	return auth.CheckUserPermissions(RBACConfig, user, env, team, group, application, action)
  1226  }
  1227  
  1228  // checkUserPermissionsCreateEnvironment check the permission for the environment creation action.
  1229  // This is a "special" case because the environment group is already provided on the request.
  1230  func (s *State) checkUserPermissionsCreateEnvironment(ctx context.Context, RBACConfig auth.RBACConfig, envConfig config.EnvironmentConfig) error {
  1231  	if !RBACConfig.DexEnabled {
  1232  		return nil
  1233  	}
  1234  	user, err := auth.ReadUserFromContext(ctx)
  1235  	if err != nil {
  1236  		return fmt.Errorf(fmt.Sprintf("checkUserPermissions: user not found: %v", err))
  1237  	}
  1238  	envGroup := "*"
  1239  	// If an env group is provided on the request, use it on the permission.
  1240  	if envConfig.EnvironmentGroup != nil {
  1241  		envGroup = *(envConfig.EnvironmentGroup)
  1242  	}
  1243  	return auth.CheckUserPermissions(RBACConfig, user, "*", "", envGroup, "*", auth.PermissionCreateEnvironment)
  1244  }
  1245  
  1246  func (c *CreateEnvironmentLock) Transform(
  1247  	ctx context.Context,
  1248  	state *State,
  1249  	t TransformerContext,
  1250  ) (string, error) {
  1251  	err := state.checkUserPermissions(ctx, c.Environment, "*", auth.PermissionCreateLock, "", c.RBACConfig)
  1252  	if err != nil {
  1253  		return "", err
  1254  	}
  1255  	fs := state.Filesystem
  1256  	envDir := fs.Join("environments", c.Environment)
  1257  	if _, err := fs.Stat(envDir); err != nil {
  1258  		return "", fmt.Errorf("error accessing dir %q: %w", envDir, err)
  1259  	}
  1260  	chroot, err := fs.Chroot(envDir)
  1261  	if err != nil {
  1262  		return "", err
  1263  	}
  1264  	if err := createLock(ctx, chroot, c.LockId, c.Message); err != nil {
  1265  		return "", err
  1266  	}
  1267  	GaugeEnvLockMetric(fs, c.Environment)
  1268  	return fmt.Sprintf("Created lock %q on environment %q", c.LockId, c.Environment), nil
  1269  }
  1270  
  1271  func createLock(ctx context.Context, fs billy.Filesystem, lockId, message string) error {
  1272  	locksDir := "locks"
  1273  	if err := fs.MkdirAll(locksDir, 0777); err != nil {
  1274  		return err
  1275  	}
  1276  
  1277  	user, err := auth.ReadUserFromContext(ctx)
  1278  	if err != nil {
  1279  		return err
  1280  	}
  1281  
  1282  	// create lock dir
  1283  	newLockDir := fs.Join(locksDir, lockId)
  1284  	if err := fs.MkdirAll(newLockDir, 0777); err != nil {
  1285  		return err
  1286  	}
  1287  
  1288  	// write message
  1289  	if err := util.WriteFile(fs, fs.Join(newLockDir, "message"), []byte(message), 0666); err != nil {
  1290  		return err
  1291  	}
  1292  
  1293  	// write email
  1294  	if err := util.WriteFile(fs, fs.Join(newLockDir, "created_by_email"), []byte(user.Email), 0666); err != nil {
  1295  		return err
  1296  	}
  1297  
  1298  	// write name
  1299  	if err := util.WriteFile(fs, fs.Join(newLockDir, "created_by_name"), []byte(user.Name), 0666); err != nil {
  1300  		return err
  1301  	}
  1302  
  1303  	// write date in iso format
  1304  	if err := util.WriteFile(fs, fs.Join(newLockDir, fieldCreatedAt), []byte(getTimeNow(ctx).Format(time.RFC3339)), 0666); err != nil {
  1305  		return err
  1306  	}
  1307  	return nil
  1308  }
  1309  
  1310  type DeleteEnvironmentLock struct {
  1311  	Authentication
  1312  	Environment string
  1313  	LockId      string
  1314  }
  1315  
  1316  func (c *DeleteEnvironmentLock) Transform(
  1317  	ctx context.Context,
  1318  	state *State,
  1319  	t TransformerContext,
  1320  ) (string, error) {
  1321  	err := state.checkUserPermissions(ctx, c.Environment, "*", auth.PermissionDeleteLock, "", c.RBACConfig)
  1322  	if err != nil {
  1323  		return "", err
  1324  	}
  1325  	fs := state.Filesystem
  1326  	s := State{
  1327  		Commit:                 nil,
  1328  		BootstrapMode:          false,
  1329  		EnvironmentConfigsPath: "",
  1330  		Filesystem:             fs,
  1331  	}
  1332  	lockDir := s.GetEnvLockDir(c.Environment, c.LockId)
  1333  	_, err = fs.Stat(lockDir)
  1334  	if err != nil {
  1335  		if errors.Is(err, os.ErrNotExist) {
  1336  			return "", grpc.FailedPrecondition(ctx, fmt.Errorf("directory %s for env lock does not exist", lockDir))
  1337  		}
  1338  		return "", err
  1339  	}
  1340  
  1341  	if err := fs.Remove(lockDir); err != nil && !errors.Is(err, os.ErrNotExist) {
  1342  		return "", fmt.Errorf("failed to delete directory %q: %w", lockDir, err)
  1343  	}
  1344  	if err := s.DeleteEnvLockIfEmpty(ctx, c.Environment); err != nil {
  1345  		return "", err
  1346  	}
  1347  
  1348  	apps, err := s.GetEnvironmentApplications(c.Environment)
  1349  	if err != nil {
  1350  		return "", fmt.Errorf("environment applications for %q not found: %v", c.Environment, err.Error())
  1351  	}
  1352  
  1353  	additionalMessageFromDeployment := ""
  1354  	for _, appName := range apps {
  1355  		queueMessage, err := s.ProcessQueue(ctx, fs, c.Environment, appName)
  1356  		if err != nil {
  1357  			return "", err
  1358  		}
  1359  		if queueMessage != "" {
  1360  			additionalMessageFromDeployment = additionalMessageFromDeployment + "\n" + queueMessage
  1361  		}
  1362  	}
  1363  	GaugeEnvLockMetric(fs, c.Environment)
  1364  	return fmt.Sprintf("Deleted lock %q on environment %q%s", c.LockId, c.Environment, additionalMessageFromDeployment), nil
  1365  }
  1366  
  1367  type CreateEnvironmentGroupLock struct {
  1368  	Authentication
  1369  	EnvironmentGroup string
  1370  	LockId           string
  1371  	Message          string
  1372  }
  1373  
  1374  func (c *CreateEnvironmentGroupLock) Transform(
  1375  	ctx context.Context,
  1376  	state *State,
  1377  	t TransformerContext,
  1378  ) (string, error) {
  1379  	err := state.checkUserPermissions(ctx, c.EnvironmentGroup, "*", auth.PermissionCreateLock, "", c.RBACConfig)
  1380  	if err != nil {
  1381  		return "", err
  1382  	}
  1383  	envNamesSorted, err := state.GetEnvironmentConfigsForGroup(c.EnvironmentGroup)
  1384  	if err != nil {
  1385  		return "", grpc.PublicError(ctx, err)
  1386  	}
  1387  	for index := range envNamesSorted {
  1388  		envName := envNamesSorted[index]
  1389  		x := CreateEnvironmentLock{
  1390  			Authentication: c.Authentication,
  1391  			Environment:    envName,
  1392  			LockId:         c.LockId, // the IDs should be the same for all. See `useLocksSimilarTo` in store.tsx
  1393  			Message:        c.Message,
  1394  		}
  1395  		if err := t.Execute(&x); err != nil {
  1396  			return "", err
  1397  		}
  1398  	}
  1399  	return fmt.Sprintf("Creating locks '%s' for environment group '%s':", c.LockId, c.EnvironmentGroup), nil
  1400  }
  1401  
  1402  type DeleteEnvironmentGroupLock struct {
  1403  	Authentication
  1404  	EnvironmentGroup string
  1405  	LockId           string
  1406  }
  1407  
  1408  func (c *DeleteEnvironmentGroupLock) Transform(
  1409  	ctx context.Context,
  1410  	state *State,
  1411  	t TransformerContext,
  1412  ) (string, error) {
  1413  	err := state.checkUserPermissions(ctx, c.EnvironmentGroup, "*", auth.PermissionDeleteLock, "", c.RBACConfig)
  1414  	if err != nil {
  1415  		return "", err
  1416  	}
  1417  	envNamesSorted, err := state.GetEnvironmentConfigsForGroup(c.EnvironmentGroup)
  1418  	if err != nil {
  1419  		return "", grpc.PublicError(ctx, err)
  1420  	}
  1421  	for index := range envNamesSorted {
  1422  		envName := envNamesSorted[index]
  1423  		x := DeleteEnvironmentLock{
  1424  			Authentication: c.Authentication,
  1425  			Environment:    envName,
  1426  			LockId:         c.LockId,
  1427  		}
  1428  		if err := t.Execute(&x); err != nil {
  1429  			return "", err
  1430  		}
  1431  	}
  1432  	return fmt.Sprintf("Deleting locks '%s' for environment group '%s':", c.LockId, c.EnvironmentGroup), nil
  1433  }
  1434  
  1435  type CreateEnvironmentApplicationLock struct {
  1436  	Authentication
  1437  	Environment string
  1438  	Application string
  1439  	LockId      string
  1440  	Message     string
  1441  }
  1442  
  1443  func (c *CreateEnvironmentApplicationLock) Transform(
  1444  	ctx context.Context,
  1445  	state *State,
  1446  	t TransformerContext,
  1447  ) (string, error) {
  1448  	// Note: it's possible to lock an application BEFORE it's even deployed to the environment.
  1449  	err := state.checkUserPermissions(ctx, c.Environment, c.Application, auth.PermissionCreateLock, "", c.RBACConfig)
  1450  	if err != nil {
  1451  		return "", err
  1452  	}
  1453  	fs := state.Filesystem
  1454  	envDir := fs.Join("environments", c.Environment)
  1455  	if _, err := fs.Stat(envDir); err != nil {
  1456  		return "", fmt.Errorf("error accessing dir %q: %w", envDir, err)
  1457  	}
  1458  
  1459  	appDir := fs.Join(envDir, "applications", c.Application)
  1460  	if err := fs.MkdirAll(appDir, 0777); err != nil {
  1461  		return "", err
  1462  	}
  1463  	chroot, err := fs.Chroot(appDir)
  1464  	if err != nil {
  1465  		return "", err
  1466  	}
  1467  	if err := createLock(ctx, chroot, c.LockId, c.Message); err != nil {
  1468  		return "", err
  1469  	}
  1470  	GaugeEnvAppLockMetric(fs, c.Environment, c.Application)
  1471  	// locks are invisible to argoCd, so no changes here
  1472  	return fmt.Sprintf("Created lock %q on environment %q for application %q", c.LockId, c.Environment, c.Application), nil
  1473  }
  1474  
  1475  type DeleteEnvironmentApplicationLock struct {
  1476  	Authentication
  1477  	Environment string
  1478  	Application string
  1479  	LockId      string
  1480  }
  1481  
  1482  func (c *DeleteEnvironmentApplicationLock) Transform(
  1483  	ctx context.Context,
  1484  	state *State,
  1485  	t TransformerContext,
  1486  ) (string, error) {
  1487  	err := state.checkUserPermissions(ctx, c.Environment, c.Application, auth.PermissionDeleteLock, "", c.RBACConfig)
  1488  	if err != nil {
  1489  		return "", err
  1490  	}
  1491  	fs := state.Filesystem
  1492  	lockDir := fs.Join("environments", c.Environment, "applications", c.Application, "locks", c.LockId)
  1493  	_, err = fs.Stat(lockDir)
  1494  	if err != nil {
  1495  		if errors.Is(err, os.ErrNotExist) {
  1496  			return "", grpc.FailedPrecondition(ctx, fmt.Errorf("directory %s for app lock does not exist", lockDir))
  1497  		}
  1498  		return "", err
  1499  	}
  1500  	if err := fs.Remove(lockDir); err != nil && !errors.Is(err, os.ErrNotExist) {
  1501  		return "", fmt.Errorf("failed to delete directory %q: %w", lockDir, err)
  1502  	}
  1503  	s := State{
  1504  		Commit:                 nil,
  1505  		BootstrapMode:          false,
  1506  		EnvironmentConfigsPath: "",
  1507  		Filesystem:             fs,
  1508  	}
  1509  	if err := s.DeleteAppLockIfEmpty(ctx, c.Environment, c.Application); err != nil {
  1510  		return "", err
  1511  	}
  1512  	queueMessage, err := s.ProcessQueue(ctx, fs, c.Environment, c.Application)
  1513  	if err != nil {
  1514  		return "", err
  1515  	}
  1516  	GaugeEnvAppLockMetric(fs, c.Environment, c.Application)
  1517  	return fmt.Sprintf("Deleted lock %q on environment %q for application %q%s", c.LockId, c.Environment, c.Application, queueMessage), nil
  1518  }
  1519  
  1520  type CreateEnvironment struct {
  1521  	Authentication
  1522  	Environment string
  1523  	Config      config.EnvironmentConfig
  1524  }
  1525  
  1526  func (c *CreateEnvironment) Transform(
  1527  	ctx context.Context,
  1528  	state *State,
  1529  	t TransformerContext,
  1530  ) (string, error) {
  1531  	err := state.checkUserPermissionsCreateEnvironment(ctx, c.RBACConfig, c.Config)
  1532  	if err != nil {
  1533  		return "", err
  1534  	}
  1535  	fs := state.Filesystem
  1536  	envDir := fs.Join("environments", c.Environment)
  1537  	// Creation of environment is possible, but configuring it is not if running in bootstrap mode.
  1538  	// Configuration needs to be done by modifying config map in source repo
  1539  	//exhaustruct:ignore
  1540  	defaultConfig := config.EnvironmentConfig{}
  1541  	if state.BootstrapMode && c.Config != defaultConfig {
  1542  		return "", fmt.Errorf("Cannot create or update configuration in bootstrap mode. Please update configuration in config map instead.")
  1543  	}
  1544  	if err := fs.MkdirAll(envDir, 0777); err != nil {
  1545  		return "", err
  1546  	}
  1547  	configFile := fs.Join(envDir, "config.json")
  1548  	file, err := fs.OpenFile(configFile, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666)
  1549  	if err != nil {
  1550  		return "", fmt.Errorf("error creating config: %w", err)
  1551  	}
  1552  	enc := json.NewEncoder(file)
  1553  	enc.SetIndent("", "  ")
  1554  	if err := enc.Encode(c.Config); err != nil {
  1555  		return "", fmt.Errorf("error writing json: %w", err)
  1556  	}
  1557  	// we do not need to inform argoCd when creating an environment, as there are no apps yet
  1558  	return fmt.Sprintf("create environment %q", c.Environment), file.Close()
  1559  }
  1560  
  1561  type QueueApplicationVersion struct {
  1562  	Environment string
  1563  	Application string
  1564  	Version     uint64
  1565  }
  1566  
  1567  func (c *QueueApplicationVersion) Transform(
  1568  	ctx context.Context,
  1569  	state *State,
  1570  	t TransformerContext,
  1571  ) (string, error) {
  1572  	fs := state.Filesystem
  1573  	// Create a symlink to the release
  1574  	applicationDir := fs.Join("environments", c.Environment, "applications", c.Application)
  1575  	if err := fs.MkdirAll(applicationDir, 0777); err != nil {
  1576  		return "", err
  1577  	}
  1578  	queuedVersionFile := fs.Join(applicationDir, queueFileName)
  1579  	if err := fs.Remove(queuedVersionFile); err != nil && !errors.Is(err, os.ErrNotExist) {
  1580  		return "", err
  1581  	}
  1582  	releaseDir := releasesDirectoryWithVersion(fs, c.Application, c.Version)
  1583  	if err := fs.Symlink(fs.Join("..", "..", "..", "..", releaseDir), queuedVersionFile); err != nil {
  1584  		return "", err
  1585  	}
  1586  
  1587  	// TODO SU: maybe check here if that version is already deployed? or somewhere else ... or not at all...
  1588  	return fmt.Sprintf("Queued version %d of app %q in env %q", c.Version, c.Application, c.Environment), nil
  1589  }
  1590  
  1591  type DeployApplicationVersion struct {
  1592  	Authentication
  1593  	Environment     string
  1594  	Application     string
  1595  	Version         uint64
  1596  	LockBehaviour   api.LockBehavior
  1597  	WriteCommitData bool
  1598  	SourceTrain     *DeployApplicationVersionSource
  1599  }
  1600  
  1601  type DeployApplicationVersionSource struct {
  1602  	TargetGroup *string
  1603  	Upstream    string
  1604  }
  1605  
  1606  func (c *DeployApplicationVersion) Transform(
  1607  	ctx context.Context,
  1608  	state *State,
  1609  	t TransformerContext,
  1610  ) (string, error) {
  1611  	err := state.checkUserPermissions(ctx, c.Environment, c.Application, auth.PermissionDeployRelease, "", c.RBACConfig)
  1612  	if err != nil {
  1613  		return "", err
  1614  	}
  1615  	fs := state.Filesystem
  1616  	// Check that the release exist and fetch manifest
  1617  	releaseDir := releasesDirectoryWithVersion(fs, c.Application, c.Version)
  1618  	manifest := fs.Join(releaseDir, "environments", c.Environment, "manifests.yaml")
  1619  	var manifestContent []byte
  1620  	if file, err := fs.Open(manifest); err != nil {
  1621  		return "", wrapFileError(err, manifest, fmt.Sprintf("deployment failed: could not open manifest for app %s with release %d on env %s", c.Application, c.Version, c.Environment))
  1622  	} else {
  1623  		if content, err := io.ReadAll(file); err != nil {
  1624  			return "", err
  1625  		} else {
  1626  			manifestContent = content
  1627  		}
  1628  		file.Close()
  1629  	}
  1630  	lockPreventedDeployment := false
  1631  	if c.LockBehaviour != api.LockBehavior_IGNORE {
  1632  		// Check that the environment is not locked
  1633  		var (
  1634  			envLocks, appLocks map[string]Lock
  1635  			err                error
  1636  		)
  1637  		envLocks, err = state.GetEnvironmentLocks(c.Environment)
  1638  		if err != nil {
  1639  			return "", err
  1640  		}
  1641  		appLocks, err = state.GetEnvironmentApplicationLocks(c.Environment, c.Application)
  1642  		if err != nil {
  1643  			return "", err
  1644  		}
  1645  		if len(envLocks) > 0 || len(appLocks) > 0 {
  1646  			if c.WriteCommitData {
  1647  				var lockType, lockMsg string
  1648  				if len(envLocks) > 0 {
  1649  					lockType = "environment"
  1650  					for _, lock := range envLocks {
  1651  						lockMsg = lock.Message
  1652  						break
  1653  					}
  1654  				} else {
  1655  					lockType = "application"
  1656  					for _, lock := range appLocks {
  1657  						lockMsg = lock.Message
  1658  						break
  1659  					}
  1660  				}
  1661  				if err := addEventForRelease(ctx, fs, releaseDir, &event.LockPreventedDeployment{
  1662  					Application: c.Application,
  1663  					Environment: c.Environment,
  1664  					LockMessage: lockMsg,
  1665  					LockType:    lockType,
  1666  				}); err != nil {
  1667  					return "", err
  1668  				}
  1669  				lockPreventedDeployment = true
  1670  			}
  1671  			switch c.LockBehaviour {
  1672  			case api.LockBehavior_RECORD:
  1673  				q := QueueApplicationVersion{
  1674  					Environment: c.Environment,
  1675  					Application: c.Application,
  1676  					Version:     c.Version,
  1677  				}
  1678  				return q.Transform(ctx, state, t)
  1679  			case api.LockBehavior_FAIL:
  1680  				return "", &LockedError{
  1681  					EnvironmentApplicationLocks: appLocks,
  1682  					EnvironmentLocks:            envLocks,
  1683  				}
  1684  			}
  1685  		}
  1686  	}
  1687  
  1688  	applicationDir := fs.Join("environments", c.Environment, "applications", c.Application)
  1689  	firstDeployment := false
  1690  	versionFile := fs.Join(applicationDir, "version")
  1691  	oldReleaseDir := ""
  1692  
  1693  	//Check if there is a version of target app already deployed on target environment
  1694  	if _, err := fs.Lstat(versionFile); err == nil {
  1695  		//File Exists
  1696  		evaledPath, _ := fs.Readlink(versionFile) //Version is stored as symlink, eval it
  1697  		oldReleaseDir = evaledPath
  1698  	} else {
  1699  		//File does not exist
  1700  		firstDeployment = true
  1701  	}
  1702  
  1703  	// Create a symlink to the release
  1704  	if err := fs.MkdirAll(applicationDir, 0777); err != nil {
  1705  		return "", err
  1706  	}
  1707  	if err := fs.Remove(versionFile); err != nil && !errors.Is(err, os.ErrNotExist) {
  1708  		return "", err
  1709  	}
  1710  	if err := fs.Symlink(fs.Join("..", "..", "..", "..", releaseDir), versionFile); err != nil {
  1711  		return "", err
  1712  	}
  1713  	// Copy the manifest for argocd
  1714  	manifestsDir := fs.Join(applicationDir, "manifests")
  1715  	if err := fs.MkdirAll(manifestsDir, 0777); err != nil {
  1716  		return "", err
  1717  	}
  1718  	manifestFilename := fs.Join(manifestsDir, "manifests.yaml")
  1719  	// note that the manifest is empty here!
  1720  	// but actually it's not quite empty!
  1721  	// The function we are using here is `util.WriteFile`. And that does not allow overwriting files with empty content.
  1722  	// We work around this unusual behavior by writing a space into the file
  1723  	if len(manifestContent) == 0 {
  1724  		manifestContent = []byte(" ")
  1725  	}
  1726  	if err := util.WriteFile(fs, manifestFilename, manifestContent, 0666); err != nil {
  1727  		return "", err
  1728  	}
  1729  	teamOwner, err := state.GetApplicationTeamOwner(c.Application)
  1730  	if err != nil {
  1731  		return "", err
  1732  	}
  1733  	t.AddAppEnv(c.Application, c.Environment, teamOwner)
  1734  
  1735  	user, err := auth.ReadUserFromContext(ctx)
  1736  	if err != nil {
  1737  		return "", err
  1738  	}
  1739  
  1740  	if err := util.WriteFile(fs, fs.Join(applicationDir, "deployed_by"), []byte(user.Name), 0666); err != nil {
  1741  		return "", err
  1742  	}
  1743  	if err := util.WriteFile(fs, fs.Join(applicationDir, "deployed_by_email"), []byte(user.Email), 0666); err != nil {
  1744  		return "", err
  1745  	}
  1746  
  1747  	if err := util.WriteFile(fs, fs.Join(applicationDir, "deployed_at_utc"), []byte(getTimeNow(ctx).UTC().String()), 0666); err != nil {
  1748  		return "", err
  1749  	}
  1750  
  1751  	s := State{
  1752  		Commit:                 nil,
  1753  		BootstrapMode:          false,
  1754  		EnvironmentConfigsPath: "",
  1755  		Filesystem:             fs,
  1756  	}
  1757  	err = s.DeleteQueuedVersionIfExists(c.Environment, c.Application)
  1758  	if err != nil {
  1759  		return "", err
  1760  	}
  1761  	d := &CleanupOldApplicationVersions{
  1762  		Application: c.Application,
  1763  	}
  1764  	if err := t.Execute(d); err != nil {
  1765  		return "", err
  1766  	}
  1767  
  1768  	if c.WriteCommitData { // write the corresponding event
  1769  		if err := addEventForRelease(ctx, fs, releaseDir, createDeploymentEvent(c.Application, c.Environment, c.SourceTrain)); err != nil {
  1770  			return "", err
  1771  		}
  1772  
  1773  		if !firstDeployment && !lockPreventedDeployment {
  1774  			//If not first deployment and current deployment is successful, signal a new replaced by event
  1775  			if newReleaseCommitId, err := getCommitIDFromReleaseDir(ctx, fs, releaseDir); err == nil {
  1776  				if !valid.SHA1CommitID(newReleaseCommitId) {
  1777  					logger.FromContext(ctx).Sugar().Infof(
  1778  						"The source commit ID %s is not a valid/complete SHA1 hash, event cannot be stored.",
  1779  						newReleaseCommitId)
  1780  				} else {
  1781  					if err := addEventForRelease(ctx, fs, oldReleaseDir, createReplacedByEvent(c.Application, c.Environment, newReleaseCommitId)); err != nil {
  1782  						return "", err
  1783  					}
  1784  				}
  1785  			}
  1786  		} else {
  1787  			logger.FromContext(ctx).Sugar().Infof(
  1788  				"Release to replace decteted, but could not retrieve new commit information. Replaced-by event not stored.")
  1789  		}
  1790  	}
  1791  
  1792  	return fmt.Sprintf("deployed version %d of %q to %q", c.Version, c.Application, c.Environment), nil
  1793  }
  1794  
  1795  func getCommitIDFromReleaseDir(ctx context.Context, fs billy.Filesystem, releaseDir string) (string, error) {
  1796  	commitIdPath := fs.Join(releaseDir, "source_commit_id")
  1797  
  1798  	commitIDBytes, err := util.ReadFile(fs, commitIdPath)
  1799  	if err != nil {
  1800  		logger.FromContext(ctx).Sugar().Infof(
  1801  			"Error while reading source commit ID file at %s, error %w. Deployment event not stored.",
  1802  			commitIdPath, err)
  1803  		return "", err
  1804  	}
  1805  	commitID := string(commitIDBytes)
  1806  	// if the stored source commit ID is invalid then we will not be able to store the event (simply)
  1807  	return commitID, nil
  1808  }
  1809  
  1810  func addEventForRelease(ctx context.Context, fs billy.Filesystem, releaseDir string, ev event.Event) error {
  1811  	if commitID, err := getCommitIDFromReleaseDir(ctx, fs, releaseDir); err == nil {
  1812  		gen := getGenerator(ctx)
  1813  		eventUuid := gen.Generate()
  1814  
  1815  		if !valid.SHA1CommitID(commitID) {
  1816  			logger.FromContext(ctx).Sugar().Infof(
  1817  				"The source commit ID %s is not a valid/complete SHA1 hash, event cannot be stored.",
  1818  				commitID)
  1819  			return nil
  1820  		}
  1821  
  1822  		if err := writeEvent(eventUuid, commitID, fs, ev); err != nil {
  1823  			return fmt.Errorf(
  1824  				"could not write an event for commit %s, error: %w",
  1825  				commitID, err)
  1826  			//return fmt.Errorf(
  1827  			//	"could not write an event for commit %s with uuid %s, error: %w",
  1828  			//	commitID, eventUuid, err)
  1829  		}
  1830  	}
  1831  	return nil
  1832  }
  1833  
  1834  func createDeploymentEvent(application, environment string, sourceTrain *DeployApplicationVersionSource) *event.Deployment {
  1835  	ev := event.Deployment{
  1836  		SourceTrainEnvironmentGroup: nil,
  1837  		SourceTrainUpstream:         nil,
  1838  		Application:                 application,
  1839  		Environment:                 environment,
  1840  	}
  1841  	if sourceTrain != nil {
  1842  		if sourceTrain.TargetGroup != nil {
  1843  			ev.SourceTrainEnvironmentGroup = sourceTrain.TargetGroup
  1844  		}
  1845  		ev.SourceTrainUpstream = &sourceTrain.Upstream
  1846  	}
  1847  	return &ev
  1848  }
  1849  
  1850  func createReplacedByEvent(application, environment, commitId string) *event.ReplacedBy {
  1851  	ev := event.ReplacedBy{
  1852  		Application:       application,
  1853  		Environment:       environment,
  1854  		CommitIDtoReplace: commitId,
  1855  	}
  1856  	return &ev
  1857  }
  1858  
  1859  type ReleaseTrain struct {
  1860  	Authentication
  1861  	Target          string
  1862  	Team            string
  1863  	CommitHash      string
  1864  	WriteCommitData bool
  1865  	Repo            Repository
  1866  }
  1867  type Overview struct {
  1868  	App     string
  1869  	Version uint64
  1870  }
  1871  
  1872  func getEnvironmentInGroup(groups []*api.EnvironmentGroup, groupNameToReturn string, envNameToReturn string) *api.Environment {
  1873  	for _, currentGroup := range groups {
  1874  		if currentGroup.EnvironmentGroupName == groupNameToReturn {
  1875  			for _, currentEnv := range currentGroup.Environments {
  1876  				if currentEnv.Name == envNameToReturn {
  1877  					return currentEnv
  1878  				}
  1879  			}
  1880  		}
  1881  	}
  1882  	return nil
  1883  }
  1884  
  1885  func getOverrideVersions(commitHash, upstreamEnvName string, repo Repository) (resp []Overview, err error) {
  1886  	oid, err := git.NewOid(commitHash)
  1887  	if err != nil {
  1888  		return nil, fmt.Errorf("Error creating new oid for commitHash %s: %w", commitHash, err)
  1889  	}
  1890  	s, err := repo.StateAt(oid)
  1891  	if err != nil {
  1892  		var gerr *git.GitError
  1893  		if errors.As(err, &gerr) {
  1894  			if gerr.Code == git.ErrorCodeNotFound {
  1895  				return nil, fmt.Errorf("ErrNotFound: %w", err)
  1896  			}
  1897  		}
  1898  		return nil, fmt.Errorf("unable to get oid: %w", err)
  1899  	}
  1900  	envs, err := s.GetEnvironmentConfigs()
  1901  	if err != nil {
  1902  		return nil, fmt.Errorf("unable to get EnvironmentConfigs for %s: %w", commitHash, err)
  1903  	}
  1904  	result := mapper.MapEnvironmentsToGroups(envs)
  1905  	for envName, config := range envs {
  1906  		var groupName = mapper.DeriveGroupName(config, envName)
  1907  		var envInGroup = getEnvironmentInGroup(result, groupName, envName)
  1908  		if upstreamEnvName != envInGroup.Name || upstreamEnvName != groupName {
  1909  			continue
  1910  		}
  1911  		apps, err := s.GetEnvironmentApplications(envName)
  1912  		if err != nil {
  1913  			return nil, fmt.Errorf("unable to get EnvironmentApplication for env %s: %w", envName, err)
  1914  		}
  1915  		for _, appName := range apps {
  1916  			app := api.Environment_Application{
  1917  				Version:            0,
  1918  				Locks:              nil,
  1919  				QueuedVersion:      0,
  1920  				UndeployVersion:    false,
  1921  				ArgoCd:             nil,
  1922  				DeploymentMetaData: nil,
  1923  				Name:               appName,
  1924  			}
  1925  			version, err := s.GetEnvironmentApplicationVersion(envName, appName)
  1926  			if err != nil && !errors.Is(err, os.ErrNotExist) {
  1927  				return nil, fmt.Errorf("unable to get EnvironmentApplicationVersion for %s: %w", appName, err)
  1928  			}
  1929  			if version == nil {
  1930  				continue
  1931  			}
  1932  			app.Version = *version
  1933  			resp = append(resp, Overview{App: app.Name, Version: app.Version})
  1934  		}
  1935  	}
  1936  	return resp, nil
  1937  }
  1938  
  1939  func (c *ReleaseTrain) getUpstreamLatestApp(upstreamLatest bool, state *State, ctx context.Context, upstreamEnvName, source, commitHash string) (apps []string, appVersions []Overview, err error) {
  1940  	if commitHash != "" {
  1941  		appVersions, err := getOverrideVersions(c.CommitHash, upstreamEnvName, c.Repo)
  1942  		if err != nil {
  1943  			return nil, nil, grpc.PublicError(ctx, fmt.Errorf("could not get app version for commitHash %s for %s: %w", c.CommitHash, c.Target, err))
  1944  		}
  1945  		// check that commit hash is not older than 20 commits in the past
  1946  		for _, app := range appVersions {
  1947  			apps = append(apps, app.App)
  1948  			versions, err := findOldApplicationVersions(state, app.App)
  1949  			if err != nil {
  1950  				return nil, nil, grpc.PublicError(ctx, fmt.Errorf("unable to find findOldApplicationVersions for app %s: %w", app.App, err))
  1951  			}
  1952  			if len(versions) > 0 && versions[0] > app.Version {
  1953  				return nil, nil, grpc.PublicError(ctx, fmt.Errorf("Version for app %s is older than 20 commits when running release train to commitHash %s: %w", app.App, c.CommitHash, err))
  1954  			}
  1955  
  1956  		}
  1957  		return apps, appVersions, nil
  1958  	}
  1959  	if upstreamLatest {
  1960  		apps, err = state.GetApplications()
  1961  		if err != nil {
  1962  			return nil, nil, grpc.PublicError(ctx, fmt.Errorf("could not get all applications for %q: %w", source, err))
  1963  		}
  1964  		return apps, nil, nil
  1965  	}
  1966  	apps, err = state.GetEnvironmentApplications(upstreamEnvName)
  1967  	if err != nil {
  1968  		return nil, nil, grpc.PublicError(ctx, fmt.Errorf("upstream environment (%q) does not have applications: %w", upstreamEnvName, err))
  1969  	}
  1970  	return apps, nil, nil
  1971  }
  1972  
  1973  func getEnvironmentGroupsEnvironmentsOrEnvironment(configs map[string]config.EnvironmentConfig, targetGroupName string) (map[string]config.EnvironmentConfig, bool) {
  1974  	envGroupConfigs := make(map[string]config.EnvironmentConfig)
  1975  	isEnvGroup := false
  1976  
  1977  	for env, config := range configs {
  1978  		if config.EnvironmentGroup != nil && *config.EnvironmentGroup == targetGroupName {
  1979  			isEnvGroup = true
  1980  			envGroupConfigs[env] = config
  1981  		}
  1982  	}
  1983  	if len(envGroupConfigs) == 0 {
  1984  		envConfig, ok := configs[targetGroupName]
  1985  		if ok {
  1986  			envGroupConfigs[targetGroupName] = envConfig
  1987  		}
  1988  	}
  1989  	return envGroupConfigs, isEnvGroup
  1990  }
  1991  
  1992  type ReleaseTrainApplicationPrognosis struct {
  1993  	SkipCause *api.ReleaseTrainAppPrognosis_SkipCause
  1994  	Version   uint64
  1995  }
  1996  
  1997  type ReleaseTrainEnvironmentPrognosis struct {
  1998  	SkipCause *api.ReleaseTrainEnvPrognosis_SkipCause
  1999  	Error     error
  2000  	// map key is the name of the app
  2001  	AppsPrognoses map[string]ReleaseTrainApplicationPrognosis
  2002  }
  2003  
  2004  type ReleaseTrainPrognosisOutcome = uint64
  2005  
  2006  type ReleaseTrainPrognosis struct {
  2007  	Error                error
  2008  	EnvironmentPrognoses map[string]ReleaseTrainEnvironmentPrognosis
  2009  }
  2010  
  2011  func (c *ReleaseTrain) Prognosis(
  2012  	ctx context.Context,
  2013  	state *State,
  2014  ) ReleaseTrainPrognosis {
  2015  	configs, err := state.GetEnvironmentConfigs()
  2016  	if err != nil {
  2017  		return ReleaseTrainPrognosis{
  2018  			Error:                grpc.InternalError(ctx, err),
  2019  			EnvironmentPrognoses: nil,
  2020  		}
  2021  	}
  2022  
  2023  	var targetGroupName = c.Target
  2024  	var envGroupConfigs, isEnvGroup = getEnvironmentGroupsEnvironmentsOrEnvironment(configs, targetGroupName)
  2025  	if len(envGroupConfigs) == 0 {
  2026  		return ReleaseTrainPrognosis{
  2027  			Error:                grpc.PublicError(ctx, fmt.Errorf("could not find environment group or environment configs for '%v'", targetGroupName)),
  2028  			EnvironmentPrognoses: nil,
  2029  		}
  2030  	}
  2031  
  2032  	// this to sort the env, to make sure that for the same input we always got the same output
  2033  	envGroups := make([]string, 0, len(envGroupConfigs))
  2034  	for env := range envGroupConfigs {
  2035  		envGroups = append(envGroups, env)
  2036  	}
  2037  	sort.Strings(envGroups)
  2038  
  2039  	envPrognoses := make(map[string]ReleaseTrainEnvironmentPrognosis)
  2040  
  2041  	for _, envName := range envGroups {
  2042  		var trainGroup *string
  2043  		if isEnvGroup {
  2044  			trainGroup = ptr.FromString(targetGroupName)
  2045  		}
  2046  
  2047  		envReleaseTrain := &envReleaseTrain{
  2048  			Parent:          c,
  2049  			Env:             envName,
  2050  			EnvConfigs:      configs,
  2051  			EnvGroupConfigs: envGroupConfigs,
  2052  			WriteCommitData: c.WriteCommitData,
  2053  			TrainGroup:      trainGroup,
  2054  		}
  2055  
  2056  		envPrognosis := envReleaseTrain.prognosis(ctx, state)
  2057  
  2058  		if envPrognosis.Error != nil {
  2059  			return ReleaseTrainPrognosis{
  2060  				Error:                envPrognosis.Error,
  2061  				EnvironmentPrognoses: nil,
  2062  			}
  2063  		}
  2064  
  2065  		envPrognoses[envName] = envPrognosis
  2066  	}
  2067  
  2068  	return ReleaseTrainPrognosis{
  2069  		Error:                nil,
  2070  		EnvironmentPrognoses: envPrognoses,
  2071  	}
  2072  }
  2073  
  2074  func (c *ReleaseTrain) Transform(
  2075  	ctx context.Context,
  2076  	state *State,
  2077  	t TransformerContext,
  2078  ) (string, error) {
  2079  	prognosis := c.Prognosis(ctx, state)
  2080  
  2081  	if prognosis.Error != nil {
  2082  		return "", prognosis.Error
  2083  	}
  2084  
  2085  	var targetGroupName = c.Target
  2086  	configs, _ := state.GetEnvironmentConfigs()
  2087  	var envGroupConfigs, isEnvGroup = getEnvironmentGroupsEnvironmentsOrEnvironment(configs, targetGroupName)
  2088  
  2089  	// sorting for determinism
  2090  	envNames := make([]string, 0, len(prognosis.EnvironmentPrognoses))
  2091  	for envName := range prognosis.EnvironmentPrognoses {
  2092  		envNames = append(envNames, envName)
  2093  	}
  2094  	sort.Strings(envNames)
  2095  
  2096  	for _, envName := range envNames {
  2097  		var trainGroup *string
  2098  		if isEnvGroup {
  2099  			trainGroup = ptr.FromString(targetGroupName)
  2100  		}
  2101  
  2102  		if err := t.Execute(&envReleaseTrain{
  2103  			Parent:          c,
  2104  			Env:             envName,
  2105  			EnvConfigs:      configs,
  2106  			EnvGroupConfigs: envGroupConfigs,
  2107  			WriteCommitData: c.WriteCommitData,
  2108  			TrainGroup:      trainGroup,
  2109  		}); err != nil {
  2110  			return "", err
  2111  		}
  2112  	}
  2113  
  2114  	return fmt.Sprintf(
  2115  		"Release Train to environment/environment group '%s':\n",
  2116  		targetGroupName), nil
  2117  }
  2118  
  2119  type envReleaseTrain struct {
  2120  	Parent          *ReleaseTrain
  2121  	Env             string
  2122  	EnvConfigs      map[string]config.EnvironmentConfig
  2123  	EnvGroupConfigs map[string]config.EnvironmentConfig
  2124  	WriteCommitData bool
  2125  	TrainGroup      *string
  2126  }
  2127  
  2128  func (c *envReleaseTrain) prognosis(
  2129  	ctx context.Context,
  2130  	state *State,
  2131  ) ReleaseTrainEnvironmentPrognosis {
  2132  	envConfig := c.EnvGroupConfigs[c.Env]
  2133  	if envConfig.Upstream == nil {
  2134  		return ReleaseTrainEnvironmentPrognosis{
  2135  			SkipCause: &api.ReleaseTrainEnvPrognosis_SkipCause{
  2136  				SkipCause: api.ReleaseTrainEnvSkipCause_ENV_HAS_NO_UPSTREAM,
  2137  			},
  2138  			Error:         nil,
  2139  			AppsPrognoses: nil,
  2140  		}
  2141  	}
  2142  
  2143  	err := state.checkUserPermissions(
  2144  		ctx,
  2145  		c.Env,
  2146  		"*",
  2147  		auth.PermissionDeployReleaseTrain,
  2148  		c.Parent.Team,
  2149  		c.Parent.RBACConfig,
  2150  	)
  2151  
  2152  	if err != nil {
  2153  		return ReleaseTrainEnvironmentPrognosis{
  2154  			SkipCause:     nil,
  2155  			Error:         err,
  2156  			AppsPrognoses: nil,
  2157  		}
  2158  	}
  2159  
  2160  	upstreamLatest := envConfig.Upstream.Latest
  2161  	upstreamEnvName := envConfig.Upstream.Environment
  2162  	if !upstreamLatest && upstreamEnvName == "" {
  2163  		return ReleaseTrainEnvironmentPrognosis{
  2164  			SkipCause: &api.ReleaseTrainEnvPrognosis_SkipCause{
  2165  				SkipCause: api.ReleaseTrainEnvSkipCause_ENV_HAS_NO_UPSTREAM_LATEST_OR_UPSTREAM_ENV,
  2166  			},
  2167  			Error:         nil,
  2168  			AppsPrognoses: nil,
  2169  		}
  2170  	}
  2171  
  2172  	if upstreamLatest && upstreamEnvName != "" {
  2173  		return ReleaseTrainEnvironmentPrognosis{
  2174  			SkipCause: &api.ReleaseTrainEnvPrognosis_SkipCause{
  2175  				SkipCause: api.ReleaseTrainEnvSkipCause_ENV_HAS_BOTH_UPSTREAM_LATEST_AND_UPSTREAM_ENV,
  2176  			},
  2177  			Error:         nil,
  2178  			AppsPrognoses: nil,
  2179  		}
  2180  	}
  2181  
  2182  	if !upstreamLatest {
  2183  		_, ok := c.EnvConfigs[upstreamEnvName]
  2184  		if !ok {
  2185  			return ReleaseTrainEnvironmentPrognosis{
  2186  				SkipCause: &api.ReleaseTrainEnvPrognosis_SkipCause{
  2187  					SkipCause: api.ReleaseTrainEnvSkipCause_UPSTREAM_ENV_CONFIG_NOT_FOUND,
  2188  				},
  2189  				Error:         nil,
  2190  				AppsPrognoses: nil,
  2191  			}
  2192  		}
  2193  	}
  2194  
  2195  	envLocks, err := state.GetEnvironmentLocks(c.Env)
  2196  	if err != nil {
  2197  		return ReleaseTrainEnvironmentPrognosis{
  2198  			SkipCause:     nil,
  2199  			Error:         grpc.InternalError(ctx, fmt.Errorf("could not get lock for environment %q: %w", c.Env, err)),
  2200  			AppsPrognoses: nil,
  2201  		}
  2202  	}
  2203  
  2204  	if len(envLocks) > 0 {
  2205  		return ReleaseTrainEnvironmentPrognosis{
  2206  			SkipCause: &api.ReleaseTrainEnvPrognosis_SkipCause{
  2207  				SkipCause: api.ReleaseTrainEnvSkipCause_ENV_IS_LOCKED,
  2208  			},
  2209  			Error:         nil,
  2210  			AppsPrognoses: nil,
  2211  		}
  2212  	}
  2213  
  2214  	source := upstreamEnvName
  2215  	if upstreamLatest {
  2216  		source = "latest"
  2217  	}
  2218  
  2219  	apps, overrideVersions, err := c.Parent.getUpstreamLatestApp(upstreamLatest, state, ctx, upstreamEnvName, source, c.Parent.CommitHash)
  2220  	if err != nil {
  2221  		return ReleaseTrainEnvironmentPrognosis{
  2222  			SkipCause:     nil,
  2223  			Error:         err,
  2224  			AppsPrognoses: nil,
  2225  		}
  2226  	}
  2227  	sort.Strings(apps)
  2228  
  2229  	appsPrognoses := make(map[string]ReleaseTrainApplicationPrognosis)
  2230  
  2231  	for _, appName := range apps {
  2232  		if c.Parent.Team != "" {
  2233  			if team, err := state.GetApplicationTeamOwner(appName); err != nil {
  2234  				return ReleaseTrainEnvironmentPrognosis{
  2235  					SkipCause:     nil,
  2236  					Error:         err,
  2237  					AppsPrognoses: nil,
  2238  				}
  2239  			} else if c.Parent.Team != team {
  2240  				continue
  2241  			}
  2242  		}
  2243  
  2244  		currentlyDeployedVersion, err := state.GetEnvironmentApplicationVersion(c.Env, appName)
  2245  		if err != nil {
  2246  			return ReleaseTrainEnvironmentPrognosis{
  2247  				SkipCause:     nil,
  2248  				Error:         grpc.PublicError(ctx, fmt.Errorf("application %q in env %q does not have a version deployed: %w", appName, c.Env, err)),
  2249  				AppsPrognoses: nil,
  2250  			}
  2251  		}
  2252  
  2253  		var versionToDeploy uint64
  2254  		if overrideVersions != nil {
  2255  			for _, override := range overrideVersions {
  2256  				if override.App == appName {
  2257  					versionToDeploy = override.Version
  2258  				}
  2259  			}
  2260  		} else if upstreamLatest {
  2261  			versionToDeploy, err = GetLastRelease(state.Filesystem, appName)
  2262  			if err != nil {
  2263  				return ReleaseTrainEnvironmentPrognosis{
  2264  					SkipCause:     nil,
  2265  					Error:         grpc.PublicError(ctx, fmt.Errorf("application %q does not have a latest deployed: %w", appName, err)),
  2266  					AppsPrognoses: nil,
  2267  				}
  2268  			}
  2269  		} else {
  2270  			upstreamVersion, err := state.GetEnvironmentApplicationVersion(upstreamEnvName, appName)
  2271  			if err != nil {
  2272  				return ReleaseTrainEnvironmentPrognosis{
  2273  					SkipCause:     nil,
  2274  					Error:         grpc.PublicError(ctx, fmt.Errorf("application %q does not have a version deployed in env %q: %w", appName, upstreamEnvName, err)),
  2275  					AppsPrognoses: nil,
  2276  				}
  2277  			}
  2278  			if upstreamVersion == nil {
  2279  				appsPrognoses[appName] = ReleaseTrainApplicationPrognosis{
  2280  					SkipCause: &api.ReleaseTrainAppPrognosis_SkipCause{
  2281  						SkipCause: api.ReleaseTrainAppSkipCause_APP_HAS_NO_VERSION_IN_UPSTREAM_ENV,
  2282  					},
  2283  					Version: 0,
  2284  				}
  2285  				continue
  2286  			}
  2287  			versionToDeploy = *upstreamVersion
  2288  		}
  2289  		if currentlyDeployedVersion != nil && *currentlyDeployedVersion == versionToDeploy {
  2290  			appsPrognoses[appName] = ReleaseTrainApplicationPrognosis{
  2291  				SkipCause: &api.ReleaseTrainAppPrognosis_SkipCause{
  2292  					SkipCause: api.ReleaseTrainAppSkipCause_APP_ALREADY_IN_UPSTREAM_VERSION,
  2293  				},
  2294  				Version: 0,
  2295  			}
  2296  			continue
  2297  		}
  2298  
  2299  		appLocks, err := state.GetEnvironmentApplicationLocks(c.Env, appName)
  2300  
  2301  		if err != nil {
  2302  			return ReleaseTrainEnvironmentPrognosis{
  2303  				SkipCause:     nil,
  2304  				Error:         err,
  2305  				AppsPrognoses: nil,
  2306  			}
  2307  		}
  2308  
  2309  		if len(appLocks) > 0 {
  2310  			appsPrognoses[appName] = ReleaseTrainApplicationPrognosis{
  2311  				SkipCause: &api.ReleaseTrainAppPrognosis_SkipCause{
  2312  					SkipCause: api.ReleaseTrainAppSkipCause_APP_IS_LOCKED,
  2313  				},
  2314  				Version: 0,
  2315  			}
  2316  			continue
  2317  		}
  2318  
  2319  		fs := state.Filesystem
  2320  
  2321  		releaseDir := releasesDirectoryWithVersion(fs, appName, versionToDeploy)
  2322  		manifest := fs.Join(releaseDir, "environments", c.Env, "manifests.yaml")
  2323  
  2324  		if _, err := fs.Stat(manifest); err != nil {
  2325  			appsPrognoses[appName] = ReleaseTrainApplicationPrognosis{
  2326  				SkipCause: &api.ReleaseTrainAppPrognosis_SkipCause{
  2327  					SkipCause: api.ReleaseTrainAppSkipCause_APP_DOES_NOT_EXIST_IN_ENV,
  2328  				},
  2329  				Version: 0,
  2330  			}
  2331  			continue
  2332  		}
  2333  
  2334  		appsPrognoses[appName] = ReleaseTrainApplicationPrognosis{
  2335  			SkipCause: nil,
  2336  			Version:   versionToDeploy,
  2337  		}
  2338  	}
  2339  
  2340  	return ReleaseTrainEnvironmentPrognosis{
  2341  		SkipCause:     nil,
  2342  		Error:         nil,
  2343  		AppsPrognoses: appsPrognoses,
  2344  	}
  2345  }
  2346  
  2347  func (c *envReleaseTrain) Transform(
  2348  	ctx context.Context,
  2349  	state *State,
  2350  	t TransformerContext,
  2351  ) (string, error) {
  2352  	renderEnvironmentSkipCause := func(SkipCause *api.ReleaseTrainEnvPrognosis_SkipCause) string {
  2353  		envConfig := c.EnvGroupConfigs[c.Env]
  2354  		upstreamEnvName := envConfig.Upstream.Environment
  2355  		switch SkipCause.SkipCause {
  2356  		case api.ReleaseTrainEnvSkipCause_ENV_HAS_NO_UPSTREAM:
  2357  			return fmt.Sprintf("Environment '%q' does not have upstream configured - skipping.", c.Env)
  2358  		case api.ReleaseTrainEnvSkipCause_ENV_HAS_NO_UPSTREAM_LATEST_OR_UPSTREAM_ENV:
  2359  			return fmt.Sprintf("Environment %q does not have upstream.latest or upstream.environment configured - skipping.", c.Env)
  2360  		case api.ReleaseTrainEnvSkipCause_ENV_HAS_BOTH_UPSTREAM_LATEST_AND_UPSTREAM_ENV:
  2361  			return fmt.Sprintf("Environment %q has both upstream.latest and upstream.environment configured - skipping.", c.Env)
  2362  		case api.ReleaseTrainEnvSkipCause_UPSTREAM_ENV_CONFIG_NOT_FOUND:
  2363  			return fmt.Sprintf("Could not find environment config for upstream env %q. Target env was %q", upstreamEnvName, c.Env)
  2364  		case api.ReleaseTrainEnvSkipCause_ENV_IS_LOCKED:
  2365  			return fmt.Sprintf("Target Environment '%s' is locked - skipping.", c.Env)
  2366  		default:
  2367  			return fmt.Sprintf("Environment '%s' is skipped for an unrecognized reason", c.Env)
  2368  		}
  2369  	}
  2370  
  2371  	renderApplicationSkipCause := func(SkipCause *api.ReleaseTrainAppPrognosis_SkipCause, appName string) string {
  2372  		envConfig := c.EnvGroupConfigs[c.Env]
  2373  		upstreamEnvName := envConfig.Upstream.Environment
  2374  		currentlyDeployedVersion, _ := state.GetEnvironmentApplicationVersion(c.Env, appName)
  2375  		switch SkipCause.SkipCause {
  2376  		case api.ReleaseTrainAppSkipCause_APP_HAS_NO_VERSION_IN_UPSTREAM_ENV:
  2377  			return fmt.Sprintf("skipping because there is no version for application %q in env %q \n", appName, upstreamEnvName)
  2378  		case api.ReleaseTrainAppSkipCause_APP_ALREADY_IN_UPSTREAM_VERSION:
  2379  			return fmt.Sprintf("skipping %q because it is already in the version %d\n", appName, currentlyDeployedVersion)
  2380  		case api.ReleaseTrainAppSkipCause_APP_IS_LOCKED:
  2381  			return fmt.Sprintf("skipping application %q in environment %q due to application lock", appName, c.Env)
  2382  		case api.ReleaseTrainAppSkipCause_APP_DOES_NOT_EXIST_IN_ENV:
  2383  			return fmt.Sprintf("skipping application %q in environment %q because it doesn't exist there", appName, c.Env)
  2384  		default:
  2385  			return fmt.Sprintf("skipping application %q in environment %q for an unrecognized reason", appName, c.Env)
  2386  		}
  2387  	}
  2388  
  2389  	prognosis := c.prognosis(ctx, state)
  2390  
  2391  	if prognosis.Error != nil {
  2392  		return "", prognosis.Error
  2393  	}
  2394  	if prognosis.SkipCause != nil {
  2395  		return renderEnvironmentSkipCause(prognosis.SkipCause), nil
  2396  	}
  2397  
  2398  	envConfig := c.EnvGroupConfigs[c.Env]
  2399  	upstreamLatest := envConfig.Upstream.Latest
  2400  	upstreamEnvName := envConfig.Upstream.Environment
  2401  
  2402  	source := upstreamEnvName
  2403  	if upstreamLatest {
  2404  		source = "latest"
  2405  	}
  2406  
  2407  	// now iterate over all apps, deploying all that are not locked
  2408  	var skipped []string
  2409  
  2410  	// sorting for determinism
  2411  	appNames := make([]string, 0, len(prognosis.AppsPrognoses))
  2412  	for appName := range prognosis.AppsPrognoses {
  2413  		appNames = append(appNames, appName)
  2414  	}
  2415  	sort.Strings(appNames)
  2416  
  2417  	for _, appName := range appNames {
  2418  		appPrognosis := prognosis.AppsPrognoses[appName]
  2419  		if appPrognosis.SkipCause != nil {
  2420  			skipped = append(skipped, renderApplicationSkipCause(appPrognosis.SkipCause, appName))
  2421  			continue
  2422  		}
  2423  		d := &DeployApplicationVersion{
  2424  			Environment:     c.Env, // here we deploy to the next env
  2425  			Application:     appName,
  2426  			Version:         appPrognosis.Version,
  2427  			LockBehaviour:   api.LockBehavior_RECORD,
  2428  			Authentication:  c.Parent.Authentication,
  2429  			WriteCommitData: c.WriteCommitData,
  2430  			SourceTrain: &DeployApplicationVersionSource{
  2431  				Upstream:    upstreamEnvName,
  2432  				TargetGroup: c.TrainGroup,
  2433  			},
  2434  		}
  2435  		if err := t.Execute(d); err != nil {
  2436  			return "", grpc.InternalError(ctx, fmt.Errorf("unexpected error while deploying app %q to env %q: %w", appName, c.Env, err))
  2437  		}
  2438  	}
  2439  	teamInfo := ""
  2440  	if c.Parent.Team != "" {
  2441  		teamInfo = " for team '" + c.Parent.Team + "'"
  2442  	}
  2443  	if err := t.Execute(&skippedServices{
  2444  		Messages: skipped,
  2445  	}); err != nil {
  2446  		return "", err
  2447  	}
  2448  	return fmt.Sprintf("Release Train to '%s' environment:\n\n"+
  2449  		"The release train deployed %d services from '%s' to '%s'%s",
  2450  		c.Env, len(prognosis.AppsPrognoses), source, c.Env, teamInfo,
  2451  	), nil
  2452  }
  2453  
  2454  // skippedServices is a helper Transformer to generate the "skipped
  2455  // services" commit log.
  2456  type skippedServices struct {
  2457  	Messages []string
  2458  }
  2459  
  2460  func (c *skippedServices) Transform(
  2461  	ctx context.Context,
  2462  	state *State,
  2463  	t TransformerContext,
  2464  ) (string, error) {
  2465  	if len(c.Messages) == 0 {
  2466  		return "", nil
  2467  	}
  2468  	for _, msg := range c.Messages {
  2469  		if err := t.Execute(&skippedService{Message: msg}); err != nil {
  2470  			return "", err
  2471  		}
  2472  	}
  2473  	return "Skipped services", nil
  2474  }
  2475  
  2476  type skippedService struct {
  2477  	Message string
  2478  }
  2479  
  2480  func (c *skippedService) Transform(
  2481  	ctx context.Context,
  2482  	state *State,
  2483  	t TransformerContext,
  2484  ) (string, error) {
  2485  	return c.Message, nil
  2486  }