github.com/freiheit-com/kuberpult@v1.24.2-0.20240328135542-315d5630abe6/services/cd-service/pkg/repository/repository.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  	"bufio"
    21  	"bytes"
    22  	"context"
    23  	"crypto/tls"
    24  	"encoding/json"
    25  	"errors"
    26  	"fmt"
    27  	"io"
    28  	"net/http"
    29  	"os"
    30  	"os/exec"
    31  	"path/filepath"
    32  	"regexp"
    33  	"sort"
    34  	"strconv"
    35  	"strings"
    36  	"sync"
    37  	"time"
    38  
    39  	"github.com/freiheit-com/kuberpult/pkg/grpc"
    40  	"google.golang.org/protobuf/types/known/timestamppb"
    41  
    42  	v1alpha1 "github.com/freiheit-com/kuberpult/services/cd-service/pkg/argocd/v1alpha1"
    43  	"github.com/freiheit-com/kuberpult/services/cd-service/pkg/mapper"
    44  
    45  	"github.com/DataDog/datadog-go/v5/statsd"
    46  	backoff "github.com/cenkalti/backoff/v4"
    47  	api "github.com/freiheit-com/kuberpult/pkg/api/v1"
    48  	"github.com/freiheit-com/kuberpult/pkg/auth"
    49  	"github.com/freiheit-com/kuberpult/pkg/setup"
    50  	"github.com/freiheit-com/kuberpult/services/cd-service/pkg/argocd"
    51  	"github.com/freiheit-com/kuberpult/services/cd-service/pkg/config"
    52  	"github.com/freiheit-com/kuberpult/services/cd-service/pkg/fs"
    53  	"github.com/freiheit-com/kuberpult/services/cd-service/pkg/notify"
    54  	"github.com/freiheit-com/kuberpult/services/cd-service/pkg/sqlitestore"
    55  	"go.uber.org/zap"
    56  	"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
    57  
    58  	"github.com/freiheit-com/kuberpult/pkg/logger"
    59  	billy "github.com/go-git/go-billy/v5"
    60  	"github.com/go-git/go-billy/v5/util"
    61  	git "github.com/libgit2/git2go/v34"
    62  )
    63  
    64  type contextKey string
    65  
    66  const DdMetricsKey contextKey = "ddMetrics"
    67  
    68  // A Repository provides a multiple reader / single writer access to a git repository.
    69  type Repository interface {
    70  	Apply(ctx context.Context, transformers ...Transformer) error
    71  	Push(ctx context.Context, pushAction func() error) error
    72  	ApplyTransformersInternal(ctx context.Context, transformers ...Transformer) ([]string, *State, []*TransformerResult, *TransformerBatchApplyError)
    73  	State() *State
    74  	StateAt(oid *git.Oid) (*State, error)
    75  	Notify() *notify.Notify
    76  }
    77  
    78  type TransformerBatchApplyError struct {
    79  	TransformerError error // the error that caused the batch to fail. nil if no error happened
    80  	Index            int   // the index of the transformer that caused the batch to fail or -1 if the error happened outside one specific transformer
    81  }
    82  
    83  func (err *TransformerBatchApplyError) Error() string {
    84  	if err == nil {
    85  		return ""
    86  	}
    87  	if err.Index < 0 {
    88  		return fmt.Sprintf("error not specific to one transformer of this batch: %s", err.TransformerError.Error())
    89  	}
    90  	return fmt.Sprintf("error at index %d of transformer batch: %s", err.Index, err.TransformerError.Error())
    91  }
    92  
    93  func (err *TransformerBatchApplyError) Is(target error) bool {
    94  	tgt, ok := target.(*TransformerBatchApplyError)
    95  	if !ok {
    96  		return false
    97  	}
    98  	if err == nil {
    99  		return target == nil
   100  	}
   101  	if target == nil {
   102  		return false
   103  	}
   104  	// now both target and err are guaranteed to be non-nil
   105  	if err.Index != tgt.Index {
   106  		return false
   107  	}
   108  	return errors.Is(err.TransformerError, tgt.TransformerError)
   109  }
   110  
   111  func defaultBackOffProvider() backoff.BackOff {
   112  	eb := backoff.NewExponentialBackOff()
   113  	eb.MaxElapsedTime = 7 * time.Second
   114  	return backoff.WithMaxRetries(eb, 6)
   115  }
   116  
   117  var (
   118  	ddMetrics statsd.ClientInterface
   119  )
   120  
   121  type StorageBackend int
   122  
   123  const (
   124  	DefaultBackend StorageBackend = 0
   125  	GitBackend     StorageBackend = iota
   126  	SqliteBackend  StorageBackend = iota
   127  )
   128  
   129  const (
   130  	maxArgoRequests = 3 // note that this happens inside a request, we cannot retry too much!
   131  )
   132  
   133  type repository struct {
   134  	// Mutex gurading the writer
   135  	writeLock    sync.Mutex
   136  	writesDone   uint
   137  	queue        queue
   138  	config       *RepositoryConfig
   139  	credentials  *credentialsStore
   140  	certificates *certificateStore
   141  
   142  	repository *git.Repository
   143  
   144  	// Mutex guarding head
   145  	headLock sync.Mutex
   146  
   147  	notify notify.Notify
   148  
   149  	backOffProvider func() backoff.BackOff
   150  }
   151  
   152  type WebhookResolver interface {
   153  	Resolve(insecure bool, req *http.Request) (*http.Response, error)
   154  }
   155  
   156  type DefaultWebhookResolver struct{}
   157  
   158  func (r DefaultWebhookResolver) Resolve(insecure bool, req *http.Request) (*http.Response, error) {
   159  	//exhaustruct:ignore
   160  	TLSClientConfig := &tls.Config{
   161  		InsecureSkipVerify: insecure,
   162  	}
   163  	//exhaustruct:ignore
   164  	tr := &http.Transport{
   165  		TLSClientConfig: TLSClientConfig,
   166  	}
   167  	//exhaustruct:ignore
   168  	client := &http.Client{
   169  		Transport: tr,
   170  	}
   171  	return client.Do(req)
   172  }
   173  
   174  type RepositoryConfig struct {
   175  	// Mandatory Config
   176  	// the URL used for git checkout, (ssh protocol)
   177  	URL  string
   178  	Path string
   179  	// Optional Config
   180  	Credentials    Credentials
   181  	Certificates   Certificates
   182  	CommitterEmail string
   183  	CommitterName  string
   184  	// default branch is master
   185  	Branch string
   186  	// network timeout
   187  	NetworkTimeout time.Duration
   188  	//
   189  	GcFrequency    uint
   190  	StorageBackend StorageBackend
   191  	// Bootstrap mode controls where configurations are read from
   192  	// true: read from json file at EnvironmentConfigsPath
   193  	// false: read from config files in manifest repo
   194  	BootstrapMode          bool
   195  	EnvironmentConfigsPath string
   196  	ArgoInsecure           bool
   197  	// if set, kuberpult will generate push events to argoCd whenever it writes to the manifest repo:
   198  	ArgoWebhookUrl string
   199  	// the url to the git repo, like the browser requires it (https protocol)
   200  	WebURL          string
   201  	DogstatsdEvents bool
   202  	WriteCommitData bool
   203  	WebhookResolver WebhookResolver
   204  
   205  	MaximumCommitsPerPush uint
   206  
   207  	MaximumQueueSize uint
   208  }
   209  
   210  func openOrCreate(path string, storageBackend StorageBackend) (*git.Repository, error) {
   211  	repo2, err := git.OpenRepositoryExtended(path, git.RepositoryOpenNoSearch, path)
   212  	if err != nil {
   213  		var gerr *git.GitError
   214  		if errors.As(err, &gerr) {
   215  			if gerr.Code == git.ErrorCodeNotFound {
   216  				err = os.MkdirAll(path, 0777)
   217  				if err != nil {
   218  					return nil, err
   219  				}
   220  				repo2, err = git.InitRepository(path, true)
   221  				if err != nil {
   222  					return nil, err
   223  				}
   224  			} else {
   225  				return nil, err
   226  			}
   227  		} else {
   228  			return nil, err
   229  		}
   230  	}
   231  	if storageBackend == SqliteBackend {
   232  		sqlitePath := filepath.Join(path, "odb.sqlite")
   233  		be, err := sqlitestore.NewOdbBackend(sqlitePath)
   234  		if err != nil {
   235  			return nil, fmt.Errorf("creating odb backend: %w", err)
   236  		}
   237  		odb, err := repo2.Odb()
   238  		if err != nil {
   239  			return nil, fmt.Errorf("gettting odb: %w", err)
   240  		}
   241  		// Prioriority 99 ensures that libgit prefers this backend for writing over its buildin backends.
   242  		err = odb.AddBackend(be, 99)
   243  		if err != nil {
   244  			return nil, fmt.Errorf("setting odb backend: %w", err)
   245  		}
   246  	}
   247  	return repo2, err
   248  }
   249  
   250  func GetTags(cfg RepositoryConfig, repoName string, ctx context.Context) (tags []*api.TagData, err error) {
   251  	repo, err := openOrCreate(repoName, cfg.StorageBackend)
   252  	if err != nil {
   253  		return nil, fmt.Errorf("unable to open/create repo: %v", err)
   254  	}
   255  
   256  	var credentials *credentialsStore
   257  	var certificates *certificateStore
   258  	if strings.HasPrefix(cfg.URL, "./") || strings.HasPrefix(cfg.URL, "/") {
   259  	} else {
   260  		credentials, err = cfg.Credentials.load()
   261  		if err != nil {
   262  			return nil, fmt.Errorf("failure to load credentials: %v", err)
   263  		}
   264  		certificates, err = cfg.Certificates.load()
   265  		if err != nil {
   266  			return nil, fmt.Errorf("failure to load certificates: %v", err)
   267  		}
   268  	}
   269  
   270  	fetchSpec := fmt.Sprintf("+refs/heads/%s:refs/remotes/origin/%s", cfg.Branch, cfg.Branch)
   271  	//exhaustruct:ignore
   272  	RemoteCallbacks := git.RemoteCallbacks{
   273  		CredentialsCallback:      credentials.CredentialsCallback(ctx),
   274  		CertificateCheckCallback: certificates.CertificateCheckCallback(ctx),
   275  	}
   276  	fetchOptions := git.FetchOptions{
   277  		Prune:           git.FetchPruneUnspecified,
   278  		UpdateFetchhead: false,
   279  		Headers:         nil,
   280  		ProxyOptions: git.ProxyOptions{
   281  			Type: git.ProxyTypeNone,
   282  			Url:  "",
   283  		},
   284  		RemoteCallbacks: RemoteCallbacks,
   285  		DownloadTags:    git.DownloadTagsAll,
   286  	}
   287  	remote, err := repo.Remotes.CreateAnonymous(cfg.URL)
   288  	if err != nil {
   289  		return nil, fmt.Errorf("failure to create anonymous remote: %v", err)
   290  	}
   291  	err = remote.Fetch([]string{fetchSpec}, &fetchOptions, "fetching")
   292  	if err != nil {
   293  		return nil, fmt.Errorf("failure to fetch: %v", err)
   294  	}
   295  
   296  	tagsList, err := repo.Tags.List()
   297  	if err != nil {
   298  		return nil, fmt.Errorf("unable to list tags: %v", err)
   299  	}
   300  
   301  	sort.Strings(tagsList)
   302  	iters, err := repo.NewReferenceIteratorGlob("refs/tags/*")
   303  	if err != nil {
   304  		return nil, fmt.Errorf("unable to get list of tags: %v", err)
   305  	}
   306  	for {
   307  		tagObject, err := iters.Next()
   308  		if err != nil {
   309  			break
   310  		}
   311  		tagRef, lookupErr := repo.LookupTag(tagObject.Target())
   312  		if lookupErr != nil {
   313  			tagCommit, err := repo.LookupCommit(tagObject.Target())
   314  			// If LookupTag fails, fallback to LookupCommit
   315  			// to cover all tags, annotated and lightweight
   316  			if err != nil {
   317  				return nil, fmt.Errorf("unable to lookup tag [%s]: %v - original err: %v", tagObject.Name(), err, lookupErr)
   318  			}
   319  			tags = append(tags, &api.TagData{Tag: tagObject.Name(), CommitId: tagCommit.Id().String()})
   320  		} else {
   321  			tags = append(tags, &api.TagData{Tag: tagObject.Name(), CommitId: tagRef.Id().String()})
   322  		}
   323  	}
   324  
   325  	return tags, nil
   326  }
   327  
   328  // Opens a repository. The repository is initialized and updated in the background.
   329  func New(ctx context.Context, cfg RepositoryConfig) (Repository, error) {
   330  	repo, bg, err := New2(ctx, cfg)
   331  	if err != nil {
   332  		return nil, err
   333  	}
   334  	go bg(ctx, nil) //nolint: errcheck
   335  	return repo, err
   336  }
   337  
   338  func New2(ctx context.Context, cfg RepositoryConfig) (Repository, setup.BackgroundFunc, error) {
   339  	logger := logger.FromContext(ctx)
   340  
   341  	ddMetricsFromCtx := ctx.Value(DdMetricsKey)
   342  	if ddMetricsFromCtx != nil {
   343  		ddMetrics = ddMetricsFromCtx.(statsd.ClientInterface)
   344  	} else {
   345  		logger.Sugar().Warnf("could not load ddmetrics from context - running without datadog metrics")
   346  	}
   347  
   348  	if cfg.Branch == "" {
   349  		cfg.Branch = "master"
   350  	}
   351  	if cfg.CommitterEmail == "" {
   352  		cfg.CommitterEmail = "kuberpult@example.com"
   353  	}
   354  	if cfg.CommitterName == "" {
   355  		cfg.CommitterName = "kuberpult"
   356  	}
   357  	if cfg.StorageBackend == DefaultBackend {
   358  		cfg.StorageBackend = SqliteBackend
   359  	}
   360  	if cfg.NetworkTimeout == 0 {
   361  		cfg.NetworkTimeout = time.Minute
   362  	}
   363  	if cfg.MaximumCommitsPerPush == 0 {
   364  		cfg.MaximumCommitsPerPush = 1
   365  
   366  	}
   367  	if cfg.MaximumQueueSize == 0 {
   368  		cfg.MaximumQueueSize = 5
   369  	}
   370  
   371  	var credentials *credentialsStore
   372  	var certificates *certificateStore
   373  	var err error
   374  	if strings.HasPrefix(cfg.URL, "./") || strings.HasPrefix(cfg.URL, "/") {
   375  		logger.Debug("git url indicates a local directory. Ignoring credentials and certificates.")
   376  	} else {
   377  		credentials, err = cfg.Credentials.load()
   378  		if err != nil {
   379  			return nil, nil, err
   380  		}
   381  		certificates, err = cfg.Certificates.load()
   382  		if err != nil {
   383  			return nil, nil, err
   384  		}
   385  	}
   386  
   387  	if repo2, err := openOrCreate(cfg.Path, cfg.StorageBackend); err != nil {
   388  		return nil, nil, err
   389  	} else {
   390  		// configure remotes
   391  		if remote, err := repo2.Remotes.CreateAnonymous(cfg.URL); err != nil {
   392  			return nil, nil, err
   393  		} else {
   394  			result := &repository{
   395  				writesDone:      0,
   396  				headLock:        sync.Mutex{},
   397  				notify:          notify.Notify{},
   398  				writeLock:       sync.Mutex{},
   399  				config:          &cfg,
   400  				credentials:     credentials,
   401  				certificates:    certificates,
   402  				repository:      repo2,
   403  				queue:           makeQueueN(cfg.MaximumQueueSize),
   404  				backOffProvider: defaultBackOffProvider,
   405  			}
   406  			result.headLock.Lock()
   407  
   408  			defer result.headLock.Unlock()
   409  			fetchSpec := fmt.Sprintf("+refs/heads/%s:refs/remotes/origin/%s", cfg.Branch, cfg.Branch)
   410  			//exhaustruct:ignore
   411  			RemoteCallbacks := git.RemoteCallbacks{
   412  				UpdateTipsCallback: func(refname string, a *git.Oid, b *git.Oid) error {
   413  					logger.Debug("git.fetched",
   414  						zap.String("refname", refname),
   415  						zap.String("revision.new", b.String()),
   416  					)
   417  					return nil
   418  				},
   419  				CredentialsCallback:      credentials.CredentialsCallback(ctx),
   420  				CertificateCheckCallback: certificates.CertificateCheckCallback(ctx),
   421  			}
   422  			fetchOptions := git.FetchOptions{
   423  				Prune:           git.FetchPruneUnspecified,
   424  				UpdateFetchhead: false,
   425  				DownloadTags:    git.DownloadTagsUnspecified,
   426  				Headers:         nil,
   427  				ProxyOptions: git.ProxyOptions{
   428  					Type: git.ProxyTypeNone,
   429  					Url:  "",
   430  				},
   431  				RemoteCallbacks: RemoteCallbacks,
   432  			}
   433  			err := remote.Fetch([]string{fetchSpec}, &fetchOptions, "fetching")
   434  			if err != nil {
   435  				return nil, nil, err
   436  			}
   437  			var rev *git.Oid
   438  			if remoteRef, err := repo2.References.Lookup(fmt.Sprintf("refs/remotes/origin/%s", cfg.Branch)); err != nil {
   439  				var gerr *git.GitError
   440  				if errors.As(err, &gerr) && gerr.Code == git.ErrorCodeNotFound {
   441  					// not found
   442  					// nothing to do
   443  				} else {
   444  					return nil, nil, err
   445  				}
   446  			} else {
   447  				rev = remoteRef.Target()
   448  				if _, err := repo2.References.Create(fmt.Sprintf("refs/heads/%s", cfg.Branch), rev, true, "reset branch"); err != nil {
   449  					return nil, nil, err
   450  				}
   451  			}
   452  
   453  			// check that we can build the current state
   454  			state, err := result.StateAt(nil)
   455  			if err != nil {
   456  				return nil, nil, err
   457  			}
   458  
   459  			// Check configuration for errors and abort early if any:
   460  			_, err = state.GetEnvironmentConfigsAndValidate(ctx)
   461  			if err != nil {
   462  				return nil, nil, err
   463  			}
   464  
   465  			return result, result.ProcessQueue, nil
   466  		}
   467  	}
   468  }
   469  
   470  func (r *repository) ProcessQueue(ctx context.Context, health *setup.HealthReporter) error {
   471  	defer func() {
   472  		close(r.queue.transformerBatches)
   473  		for e := range r.queue.transformerBatches {
   474  			e.finish(ctx.Err())
   475  		}
   476  	}()
   477  	tick := time.Tick(r.config.NetworkTimeout) //nolint: staticcheck
   478  	ttl := r.config.NetworkTimeout * 3
   479  	for {
   480  		/*
   481  			One tricky issue is that `git push` can take a while depending on the git hoster and the connection
   482  			(plus we do have relatively big and many commits).
   483  			This can lead to the situation that "everything hangs", because there is one push running already -
   484  			but only one push is possible at a time.
   485  			There is also no good way to cancel a `git push`.
   486  
   487  			To circumvent this, we report health with a "time to live" - meaning if we don't report anything within the time frame,
   488  			the health will turn to "failed" and then the pod will automatically restart (in kubernetes).
   489  		*/
   490  		health.ReportHealthTtl(setup.HealthReady, "processing queue", &ttl)
   491  		select {
   492  		case <-tick:
   493  			// this triggers a for loop every `NetworkTimeout` to refresh the readiness
   494  		case <-ctx.Done():
   495  			return nil
   496  		case e := <-r.queue.transformerBatches:
   497  			r.ProcessQueueOnce(ctx, e, defaultPushUpdate, DefaultPushActionCallback)
   498  		}
   499  	}
   500  }
   501  
   502  func (r *repository) applyTransformerBatches(transformerBatches []transformerBatch, allowFetchAndReset bool) ([]transformerBatch, error, *TransformerResult) {
   503  	//exhaustruct:ignore
   504  	var changes = &TransformerResult{}
   505  	for i := 0; i < len(transformerBatches); {
   506  		e := transformerBatches[i]
   507  		subChanges, applyErr := r.ApplyTransformers(e.ctx, e.transformers...)
   508  		changes.Combine(subChanges)
   509  		if applyErr != nil {
   510  			if errors.Is(applyErr.TransformerError, InvalidJson) && allowFetchAndReset {
   511  				// Invalid state. fetch and reset and redo
   512  				err := r.FetchAndReset(e.ctx)
   513  				if err != nil {
   514  					return transformerBatches, err, nil
   515  				}
   516  				return r.applyTransformerBatches(transformerBatches, false)
   517  			} else {
   518  				e.finish(applyErr)
   519  				// here, we keep all transformerBatches "behind i".
   520  				// these are the transformerBatches that have not been applied yet
   521  				transformerBatches = append(transformerBatches[:i], transformerBatches[i+1:]...)
   522  			}
   523  		} else {
   524  			i++
   525  		}
   526  	}
   527  	return transformerBatches, nil, changes
   528  }
   529  
   530  var panicError = errors.New("Panic")
   531  
   532  func (r *repository) useRemote(callback func(*git.Remote) error) error {
   533  	remote, err := r.repository.Remotes.CreateAnonymous(r.config.URL)
   534  	if err != nil {
   535  		return fmt.Errorf("opening remote %q: %w", r.config.URL, err)
   536  	}
   537  	ctx, cancel := context.WithTimeout(context.Background(), r.config.NetworkTimeout)
   538  	defer cancel()
   539  	errCh := make(chan error, 1)
   540  	go func() {
   541  		// Usually we call `defer` right after resource allocation (`CreateAnonymous`).
   542  		// The issue with that is that the `callback` requires the remote, and cannot be cancelled properly.
   543  		// So `callback` may run longer than `useRemote`, and if at that point `Disconnect` was already called, we get a `panic`.
   544  		defer remote.Disconnect()
   545  		errCh <- callback(remote)
   546  	}()
   547  	select {
   548  	case <-ctx.Done():
   549  		return ctx.Err()
   550  	case err := <-errCh:
   551  		return err
   552  	}
   553  }
   554  
   555  func (r *repository) drainQueue() []transformerBatch {
   556  	if r.config.MaximumCommitsPerPush < 2 {
   557  		return nil
   558  	}
   559  	limit := r.config.MaximumCommitsPerPush - 1
   560  	transformerBatches := []transformerBatch{}
   561  	for uint(len(transformerBatches)) < limit {
   562  		select {
   563  		case f := <-r.queue.transformerBatches:
   564  			// Check that the item is not already cancelled
   565  			GaugeQueueSize(f.ctx, len(r.queue.transformerBatches))
   566  			select {
   567  			case <-f.ctx.Done():
   568  				f.finish(f.ctx.Err())
   569  			default:
   570  				transformerBatches = append(transformerBatches, f)
   571  			}
   572  		default:
   573  			return transformerBatches
   574  		}
   575  	}
   576  	return transformerBatches
   577  }
   578  
   579  // It returns always nil
   580  // success is set to true if the push was successful
   581  func defaultPushUpdate(branch string, success *bool) git.PushUpdateReferenceCallback {
   582  	return func(refName string, status string) error {
   583  		var expectedRefName = fmt.Sprintf("refs/heads/%s", branch)
   584  		// if we were successful the status is empty and the ref contains our branch:
   585  		*success = refName == expectedRefName && status == ""
   586  		return nil
   587  	}
   588  }
   589  
   590  type PushActionFunc func() error
   591  type PushActionCallbackFunc func(git.PushOptions, *repository) PushActionFunc
   592  
   593  // DefaultPushActionCallback is public for testing reasons only.
   594  func DefaultPushActionCallback(pushOptions git.PushOptions, r *repository) PushActionFunc {
   595  	return func() error {
   596  		return r.useRemote(func(remote *git.Remote) error {
   597  			return remote.Push([]string{fmt.Sprintf("refs/heads/%s:refs/heads/%s", r.config.Branch, r.config.Branch)}, &pushOptions)
   598  		})
   599  	}
   600  }
   601  
   602  type PushUpdateFunc func(string, *bool) git.PushUpdateReferenceCallback
   603  
   604  func (r *repository) ProcessQueueOnce(ctx context.Context, e transformerBatch, callback PushUpdateFunc, pushAction PushActionCallbackFunc) {
   605  	logger := logger.FromContext(ctx)
   606  	var err error = panicError
   607  
   608  	// Check that the first transformerBatch is not already canceled
   609  	select {
   610  	case <-e.ctx.Done():
   611  		e.finish(e.ctx.Err())
   612  		return
   613  	default:
   614  	}
   615  
   616  	transformerBatches := []transformerBatch{e}
   617  	defer func() {
   618  		for _, el := range transformerBatches {
   619  			el.finish(err)
   620  		}
   621  	}()
   622  
   623  	// Try to fetch more items from the queue in order to push more things together
   624  	transformerBatches = append(transformerBatches, r.drainQueue()...)
   625  
   626  	var pushSuccess = true
   627  
   628  	//exhaustruct:ignore
   629  	RemoteCallbacks := git.RemoteCallbacks{
   630  		CredentialsCallback:         r.credentials.CredentialsCallback(e.ctx),
   631  		CertificateCheckCallback:    r.certificates.CertificateCheckCallback(e.ctx),
   632  		PushUpdateReferenceCallback: callback(r.config.Branch, &pushSuccess),
   633  	}
   634  	pushOptions := git.PushOptions{
   635  		PbParallelism: 0,
   636  		Headers:       nil,
   637  		ProxyOptions: git.ProxyOptions{
   638  			Type: git.ProxyTypeNone,
   639  			Url:  "",
   640  		},
   641  		RemoteCallbacks: RemoteCallbacks,
   642  	}
   643  
   644  	// Apply the items
   645  	transformerBatches, err, changes := r.applyTransformerBatches(transformerBatches, true)
   646  	if err != nil {
   647  		return
   648  	}
   649  
   650  	if len(transformerBatches) == 0 {
   651  		return
   652  	}
   653  
   654  	// Try pushing once
   655  	err = r.Push(e.ctx, pushAction(pushOptions, r))
   656  	if err != nil {
   657  		gerr, ok := err.(*git.GitError)
   658  		// If it doesn't work because the branch diverged, try reset and apply again.
   659  		if ok && gerr.Code == git.ErrorCodeNonFastForward {
   660  			err = r.FetchAndReset(e.ctx)
   661  			if err != nil {
   662  				return
   663  			}
   664  			// Apply the items
   665  			transformerBatches, err, changes = r.applyTransformerBatches(transformerBatches, false)
   666  			if err != nil || len(transformerBatches) == 0 {
   667  				return
   668  			}
   669  			if pushErr := r.Push(e.ctx, pushAction(pushOptions, r)); pushErr != nil {
   670  				err = pushErr
   671  			}
   672  		} else if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
   673  			err = grpc.CanceledError(ctx, err)
   674  		} else {
   675  			logger.Error(fmt.Sprintf("error while pushing: %s", err))
   676  			err = grpc.PublicError(ctx, fmt.Errorf("could not push to manifest repository '%s' on branch '%s' - this indicates that the ssh key does not have write access", r.config.URL, r.config.Branch))
   677  		}
   678  	} else {
   679  		if !pushSuccess {
   680  			err = fmt.Errorf("failed to push - this indicates that branch protection is enabled in '%s' on branch '%s'", r.config.URL, r.config.Branch)
   681  		}
   682  	}
   683  	span, ctx := tracer.StartSpanFromContext(e.ctx, "PostPush")
   684  	defer span.Finish()
   685  
   686  	ddSpan, ctx := tracer.StartSpanFromContext(ctx, "SendMetrics")
   687  	if r.config.DogstatsdEvents {
   688  		ddError := UpdateDatadogMetrics(ctx, r.State(), changes, time.Now())
   689  		if ddError != nil {
   690  			logger.Warn(fmt.Sprintf("Could not send datadog metrics/events %v", ddError))
   691  		}
   692  	}
   693  	ddSpan.Finish()
   694  
   695  	if r.config.ArgoWebhookUrl != "" {
   696  		r.sendWebhookToArgoCd(ctx, logger, changes)
   697  	}
   698  
   699  	r.notify.Notify()
   700  }
   701  
   702  func (r *repository) sendWebhookToArgoCd(ctx context.Context, logger *zap.Logger, changes *TransformerResult) {
   703  	var modified = []string{}
   704  	for i := range changes.ChangedApps {
   705  		change := changes.ChangedApps[i]
   706  		// we may need to add the root app in some circumstances - so far it doesn't seem necessary, so we just add the manifest.yaml:
   707  		manifestFilename := fmt.Sprintf("environments/%s/applications/%s/manifests/manifests.yaml", change.Env, change.App)
   708  		modified = append(modified, manifestFilename)
   709  		logger.Info(fmt.Sprintf("ArgoWebhookUrl: adding modified: %s", manifestFilename))
   710  	}
   711  	var deleted = []string{}
   712  	for i := range changes.DeletedRootApps {
   713  		change := changes.DeletedRootApps[i]
   714  		// we may need to add the root app in some circumstances - so far it doesn't seem necessary, so we just add the manifest.yaml:
   715  		rootAppFilename := fmt.Sprintf("argocd/%s/%s.yaml", "v1alpha1", change.Env)
   716  		deleted = append(deleted, rootAppFilename)
   717  		logger.Info(fmt.Sprintf("ArgoWebhookUrl: adding modified: %s", rootAppFilename))
   718  	}
   719  
   720  	argoResult := ArgoWebhookData{
   721  		htmlUrl:  r.config.WebURL, // if this does not match, argo will completely ignore the request and return 200
   722  		revision: "refs/heads/" + r.config.Branch,
   723  		change: changeInfo{
   724  			payloadBefore: "",
   725  			payloadAfter:  changes.Commits.Current.String(),
   726  		},
   727  		defaultBranch: r.config.Branch, // this is questionable, because we don't actually know the default branch, but it seems to work fine in practice
   728  		Commits: []commit{
   729  			{
   730  				Added:    []string{},
   731  				Modified: modified,
   732  				Removed:  deleted,
   733  			},
   734  		},
   735  	}
   736  	if changes.Commits.Previous != nil {
   737  		argoResult.change.payloadBefore = changes.Commits.Previous.String()
   738  	}
   739  
   740  	span, ctx := tracer.StartSpanFromContext(ctx, "Webhook-Retries")
   741  	defer span.Finish()
   742  	success := false
   743  	var err error = nil
   744  	for i := 1; i <= maxArgoRequests; i++ {
   745  		err, shouldRetry := doWebhookPostRequest(ctx, argoResult, r.config, i)
   746  		if err != nil && shouldRetry {
   747  			logger.Warn(fmt.Sprintf("ProcessQueueOnce: error sending webhook on try %d: %v", i, err))
   748  			if shouldRetry {
   749  				// we're still in a request here, we can't wait too long:
   750  				time.Sleep(time.Duration(100*i) * time.Millisecond)
   751  			} else {
   752  				break
   753  			}
   754  		} else {
   755  			logger.Info(fmt.Sprintf("ProcessQueueOnce: argo webhook was send successfully on try %d!", i))
   756  			success = true
   757  			break
   758  		}
   759  	}
   760  	span.SetTag("success", success)
   761  	if !success {
   762  		logger.Error(fmt.Sprintf("ProcessQueueOnce: error sending webhook after all %d tries: %v", maxArgoRequests, err))
   763  	}
   764  }
   765  
   766  func contains(s []int, e int) bool {
   767  	for _, a := range s {
   768  		if a == e {
   769  			return true
   770  		}
   771  	}
   772  	return false
   773  }
   774  
   775  func doWebhookPostRequest(ctx context.Context, data ArgoWebhookData, repoConfig *RepositoryConfig, retryCounter int) (error, bool) {
   776  	span, ctx := tracer.StartSpanFromContext(ctx, "Webhook")
   777  	span.SetTag("changeAfter", data.change.payloadAfter)
   778  	span.SetTag("changeBefore", data.change.payloadBefore)
   779  	span.SetTag("try", retryCounter)
   780  	defer span.Finish()
   781  	url := repoConfig.ArgoWebhookUrl + "/api/webhook"
   782  	l := logger.FromContext(ctx)
   783  	l.Info(fmt.Sprintf("doWebhookPostRequest: URL: %s", url))
   784  
   785  	//exhaustruct:ignore
   786  	Repository := v1alpha1.Repository{
   787  		HTMLURL:       data.htmlUrl,
   788  		DefaultBranch: data.defaultBranch,
   789  	}
   790  	//exhaustruct:ignore
   791  	var argoFormat = v1alpha1.PushPayload{
   792  		Ref:        data.revision,
   793  		Before:     data.change.payloadBefore,
   794  		After:      data.change.payloadAfter,
   795  		Repository: Repository,
   796  		Commits:    toArgoCommits(data.Commits),
   797  	}
   798  
   799  	jsonBytes, err := json.MarshalIndent(argoFormat, " ", " ")
   800  	if err != nil {
   801  		return err, false
   802  	}
   803  	l.Info(fmt.Sprintf("doWebhookPostRequest argo format: %s", string(jsonBytes)))
   804  	req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonBytes))
   805  	if err != nil {
   806  		return fmt.Errorf("Could not create new request: %s", err.Error()), false
   807  	}
   808  	req.Header.Set("Content-Type", "application/json")
   809  
   810  	// now pretend that we are GitHub by adding this header, otherwise argo will ignore our request:
   811  	req.Header.Set("X-GitHub-Event", "push")
   812  
   813  	var webhookResolver WebhookResolver = DefaultWebhookResolver{}
   814  	if repoConfig.WebhookResolver != nil {
   815  		webhookResolver = repoConfig.WebhookResolver
   816  	}
   817  	resp, err := webhookResolver.Resolve(repoConfig.ArgoInsecure, req)
   818  	if err != nil {
   819  		return fmt.Errorf("doWebhookPostRequest: could not send request to '%s': %s", url, err.Error()), false
   820  	}
   821  	defer resp.Body.Close()
   822  
   823  	//l.Warn(fmt.Sprintf("response Status: %d", resp.StatusCode))
   824  	l.Info(fmt.Sprintf("response headers: %s", resp.Header))
   825  	body, err := io.ReadAll(resp.Body)
   826  	if err != nil {
   827  		// weird but we kinda do not care about the body:
   828  		l.Warn(fmt.Sprintf("doWebhookPostRequest: could not read body: %s - continuing anyway", err.Error()))
   829  	}
   830  	validResponseCodes := []int{200}
   831  	if resp.StatusCode >= 500 {
   832  		return fmt.Errorf("doWebhookPostRequest: invalid status code from argo: %d", resp.StatusCode), true
   833  	}
   834  
   835  	if contains(validResponseCodes, resp.StatusCode) {
   836  		l.Info(fmt.Sprintf("doWebhookPostRequest: response Body: %s", string(body)))
   837  		return nil, false
   838  	}
   839  	// in any other case we should not do a retry (e.g. status 4xx):
   840  	l.Warn(fmt.Sprintf("doWebhookPostRequest: response Body: %s", string(body)))
   841  	return fmt.Errorf("doWebhookPostRequest: invalid status code from argo: %d", resp.StatusCode), false
   842  }
   843  
   844  func toArgoCommits(commits []commit) []v1alpha1.Commit {
   845  	var result = []v1alpha1.Commit{}
   846  	for i := range commits {
   847  		c := commits[i]
   848  		result = append(result, v1alpha1.Commit{
   849  			// ArgoCd ignores most fields, so we can ignore them too.
   850  			// Source: function "affectedRevisionInfo" in https://github.com/argoproj/argo-cd/blob/master/util/webhook/webhook.go#L141
   851  			Sha:       "",
   852  			ID:        "",
   853  			NodeID:    "",
   854  			TreeID:    "",
   855  			Distinct:  false,
   856  			Message:   "",
   857  			Timestamp: "",
   858  			URL:       "",
   859  			Author: struct {
   860  				Name     string `json:"name"`
   861  				Email    string `json:"email"`
   862  				Username string `json:"username"`
   863  			}{
   864  				Name:     "",
   865  				Email:    "",
   866  				Username: "",
   867  			},
   868  			Committer: struct {
   869  				Name     string `json:"name"`
   870  				Email    string `json:"email"`
   871  				Username string `json:"username"`
   872  			}{
   873  				Name:     "",
   874  				Email:    "",
   875  				Username: "",
   876  			},
   877  			Added:    c.Added,
   878  			Removed:  c.Removed,
   879  			Modified: c.Modified,
   880  		})
   881  	}
   882  	return result
   883  }
   884  
   885  type changeInfo struct {
   886  	payloadBefore string
   887  	payloadAfter  string
   888  }
   889  type commit struct {
   890  	Added    []string
   891  	Modified []string
   892  	Removed  []string
   893  }
   894  
   895  type ArgoWebhookData struct {
   896  	htmlUrl       string
   897  	revision      string // aka "ref"
   898  	change        changeInfo
   899  	defaultBranch string
   900  	Commits       []commit
   901  }
   902  
   903  func (r *repository) ApplyTransformersInternal(ctx context.Context, transformers ...Transformer) ([]string, *State, []*TransformerResult, *TransformerBatchApplyError) {
   904  	if state, err := r.StateAt(nil); err != nil {
   905  		return nil, nil, nil, &TransformerBatchApplyError{TransformerError: fmt.Errorf("%s: %w", "failure in StateAt", err), Index: -1}
   906  	} else {
   907  		var changes []*TransformerResult = nil
   908  		commitMsg := []string{}
   909  		ctxWithTime := WithTimeNow(ctx, time.Now())
   910  		for i, t := range transformers {
   911  			if msg, subChanges, err := RunTransformer(ctxWithTime, t, state); err != nil {
   912  				applyErr := TransformerBatchApplyError{
   913  					TransformerError: err,
   914  					Index:            i,
   915  				}
   916  				return nil, nil, nil, &applyErr
   917  			} else {
   918  				commitMsg = append(commitMsg, msg)
   919  				changes = append(changes, subChanges)
   920  			}
   921  		}
   922  		return commitMsg, state, changes, nil
   923  	}
   924  }
   925  
   926  type AppEnv struct {
   927  	App  string
   928  	Env  string
   929  	Team string
   930  }
   931  
   932  type RootApp struct {
   933  	Env string
   934  	//argocd/v1alpha1/development2.yaml
   935  }
   936  
   937  type TransformerResult struct {
   938  	ChangedApps     []AppEnv
   939  	DeletedRootApps []RootApp
   940  	Commits         *CommitIds
   941  }
   942  
   943  type CommitIds struct {
   944  	Previous *git.Oid
   945  	Current  *git.Oid
   946  }
   947  
   948  func (r *TransformerResult) AddAppEnv(app string, env string, team string) {
   949  	r.ChangedApps = append(r.ChangedApps, AppEnv{
   950  		App:  app,
   951  		Env:  env,
   952  		Team: team,
   953  	})
   954  }
   955  
   956  func (r *TransformerResult) AddRootApp(env string) {
   957  	r.DeletedRootApps = append(r.DeletedRootApps, RootApp{
   958  		Env: env,
   959  	})
   960  }
   961  
   962  func (r *TransformerResult) Combine(other *TransformerResult) {
   963  	if other == nil {
   964  		return
   965  	}
   966  	for i := range other.ChangedApps {
   967  		a := other.ChangedApps[i]
   968  		r.AddAppEnv(a.App, a.Env, a.Team)
   969  	}
   970  	for i := range other.DeletedRootApps {
   971  		a := other.DeletedRootApps[i]
   972  		r.AddRootApp(a.Env)
   973  	}
   974  	if r.Commits == nil {
   975  		r.Commits = other.Commits
   976  	}
   977  }
   978  
   979  func CombineArray(others []*TransformerResult) *TransformerResult {
   980  	//exhaustruct:ignore
   981  	var r *TransformerResult = &TransformerResult{}
   982  	for i := range others {
   983  		r.Combine(others[i])
   984  	}
   985  	return r
   986  }
   987  
   988  func (r *repository) ApplyTransformers(ctx context.Context, transformers ...Transformer) (*TransformerResult, *TransformerBatchApplyError) {
   989  	commitMsg, state, changes, applyErr := r.ApplyTransformersInternal(ctx, transformers...)
   990  	if applyErr != nil {
   991  		return nil, applyErr
   992  	}
   993  	if err := r.afterTransform(ctx, *state); err != nil {
   994  		return nil, &TransformerBatchApplyError{TransformerError: fmt.Errorf("%s: %w", "failure in afterTransform", err), Index: -1}
   995  	}
   996  
   997  	treeId, insertError := state.Filesystem.(*fs.TreeBuilderFS).Insert()
   998  	if insertError != nil {
   999  		return nil, &TransformerBatchApplyError{TransformerError: insertError, Index: -1}
  1000  	}
  1001  	committer := &git.Signature{
  1002  		Name:  r.config.CommitterName,
  1003  		Email: r.config.CommitterEmail,
  1004  		When:  time.Now(),
  1005  	}
  1006  
  1007  	user, readUserErr := auth.ReadUserFromContext(ctx)
  1008  
  1009  	if readUserErr != nil {
  1010  		return nil, &TransformerBatchApplyError{
  1011  			TransformerError: readUserErr,
  1012  			Index:            -1,
  1013  		}
  1014  	}
  1015  
  1016  	author := &git.Signature{
  1017  		Name:  user.Name,
  1018  		Email: user.Email,
  1019  		When:  time.Now(),
  1020  	}
  1021  
  1022  	var rev *git.Oid
  1023  	// the commit can be nil, if it's the first commit in the repo
  1024  	if state.Commit != nil {
  1025  		rev = state.Commit.Id()
  1026  	}
  1027  	oldCommitId := rev
  1028  
  1029  	newCommitId, createErr := r.repository.CreateCommitFromIds(
  1030  		fmt.Sprintf("refs/heads/%s", r.config.Branch),
  1031  		author,
  1032  		committer,
  1033  		strings.Join(commitMsg, "\n"),
  1034  		treeId,
  1035  		rev,
  1036  	)
  1037  	if createErr != nil {
  1038  		return nil, &TransformerBatchApplyError{
  1039  			TransformerError: fmt.Errorf("%s: %w", "createCommitFromIds failed", createErr),
  1040  			Index:            -1,
  1041  		}
  1042  	}
  1043  	result := CombineArray(changes)
  1044  	result.Commits = &CommitIds{
  1045  		Current:  newCommitId,
  1046  		Previous: nil,
  1047  	}
  1048  	if oldCommitId != nil {
  1049  		result.Commits.Previous = oldCommitId
  1050  	}
  1051  	return result, nil
  1052  }
  1053  
  1054  func (r *repository) FetchAndReset(ctx context.Context) error {
  1055  	fetchSpec := fmt.Sprintf("+refs/heads/%s:refs/remotes/origin/%s", r.config.Branch, r.config.Branch)
  1056  	logger := logger.FromContext(ctx)
  1057  	//exhaustruct:ignore
  1058  	RemoteCallbacks := git.RemoteCallbacks{
  1059  		UpdateTipsCallback: func(refname string, a *git.Oid, b *git.Oid) error {
  1060  			logger.Debug("git.fetched",
  1061  				zap.String("refname", refname),
  1062  				zap.String("revision.new", b.String()),
  1063  			)
  1064  			return nil
  1065  		},
  1066  		CredentialsCallback:      r.credentials.CredentialsCallback(ctx),
  1067  		CertificateCheckCallback: r.certificates.CertificateCheckCallback(ctx),
  1068  	}
  1069  	fetchOptions := git.FetchOptions{
  1070  		Prune:           git.FetchPruneUnspecified,
  1071  		UpdateFetchhead: false,
  1072  		DownloadTags:    git.DownloadTagsUnspecified,
  1073  		Headers:         nil,
  1074  		ProxyOptions: git.ProxyOptions{
  1075  			Type: git.ProxyTypeNone,
  1076  			Url:  "",
  1077  		},
  1078  		RemoteCallbacks: RemoteCallbacks,
  1079  	}
  1080  	err := r.useRemote(func(remote *git.Remote) error {
  1081  		return remote.Fetch([]string{fetchSpec}, &fetchOptions, "fetching")
  1082  	})
  1083  	if err != nil {
  1084  		return err
  1085  	}
  1086  	var zero git.Oid
  1087  	var rev *git.Oid = &zero
  1088  	if remoteRef, err := r.repository.References.Lookup(fmt.Sprintf("refs/remotes/origin/%s", r.config.Branch)); err != nil {
  1089  		var gerr *git.GitError
  1090  		if errors.As(err, &gerr) && gerr.Code == git.ErrorCodeNotFound {
  1091  			// not found
  1092  			// nothing to do
  1093  		} else {
  1094  			return err
  1095  		}
  1096  	} else {
  1097  		rev = remoteRef.Target()
  1098  		if _, err := r.repository.References.Create(fmt.Sprintf("refs/heads/%s", r.config.Branch), rev, true, "reset branch"); err != nil {
  1099  			return err
  1100  		}
  1101  	}
  1102  	obj, err := r.repository.Lookup(rev)
  1103  	if err != nil {
  1104  		return err
  1105  	}
  1106  	commit, err := obj.AsCommit()
  1107  	if err != nil {
  1108  		return err
  1109  	}
  1110  	//exhaustruct:ignore
  1111  	err = r.repository.ResetToCommit(commit, git.ResetSoft, &git.CheckoutOptions{Strategy: git.CheckoutForce})
  1112  	if err != nil {
  1113  		return err
  1114  	}
  1115  	return nil
  1116  }
  1117  
  1118  func (r *repository) Apply(ctx context.Context, transformers ...Transformer) error {
  1119  	defer func() {
  1120  		r.writesDone = r.writesDone + uint(len(transformers))
  1121  		r.maybeGc(ctx)
  1122  	}()
  1123  	eCh := r.applyDeferred(ctx, transformers...)
  1124  	select {
  1125  	case err := <-eCh:
  1126  		return err
  1127  	case <-ctx.Done():
  1128  		return ctx.Err()
  1129  	}
  1130  }
  1131  
  1132  func (r *repository) applyDeferred(ctx context.Context, transformers ...Transformer) <-chan error {
  1133  	return r.queue.add(ctx, transformers)
  1134  }
  1135  
  1136  // Push returns an 'error' for typing reasons, really it is always a git.GitError
  1137  func (r *repository) Push(ctx context.Context, pushAction func() error) error {
  1138  
  1139  	span, ctx := tracer.StartSpanFromContext(ctx, "Apply")
  1140  	defer span.Finish()
  1141  
  1142  	eb := r.backOffProvider()
  1143  	return backoff.Retry(
  1144  		func() error {
  1145  			span, _ := tracer.StartSpanFromContext(ctx, "Push")
  1146  			defer span.Finish()
  1147  			err := pushAction()
  1148  			if err != nil {
  1149  				gerr, ok := err.(*git.GitError)
  1150  				if ok && gerr.Code == git.ErrorCodeNonFastForward {
  1151  					return backoff.Permanent(err)
  1152  				}
  1153  			}
  1154  			return err
  1155  		},
  1156  		eb,
  1157  	)
  1158  }
  1159  
  1160  func (r *repository) afterTransform(ctx context.Context, state State) error {
  1161  	span, ctx := tracer.StartSpanFromContext(ctx, "afterTransform")
  1162  	defer span.Finish()
  1163  
  1164  	configs, err := state.GetEnvironmentConfigs()
  1165  	if err != nil {
  1166  		return err
  1167  	}
  1168  	for env, config := range configs {
  1169  		if config.ArgoCd != nil {
  1170  			err := r.updateArgoCdApps(ctx, &state, env, config)
  1171  			if err != nil {
  1172  				return err
  1173  			}
  1174  		}
  1175  	}
  1176  	return nil
  1177  }
  1178  
  1179  func (r *repository) updateArgoCdApps(ctx context.Context, state *State, env string, config config.EnvironmentConfig) error {
  1180  	span, ctx := tracer.StartSpanFromContext(ctx, "updateArgoCdApps")
  1181  	defer span.Finish()
  1182  	fs := state.Filesystem
  1183  	if apps, err := state.GetEnvironmentApplications(env); err != nil {
  1184  		return err
  1185  	} else {
  1186  		spanCollectData, _ := tracer.StartSpanFromContext(ctx, "collectData")
  1187  		defer spanCollectData.Finish()
  1188  		appData := []argocd.AppData{}
  1189  		sort.Strings(apps)
  1190  		for _, appName := range apps {
  1191  			if err != nil {
  1192  				return err
  1193  			}
  1194  			team, err := state.GetApplicationTeamOwner(appName)
  1195  			if err != nil {
  1196  				return err
  1197  			}
  1198  			version, err := state.GetEnvironmentApplicationVersion(env, appName)
  1199  			if err != nil {
  1200  				if errors.Is(err, os.ErrNotExist) {
  1201  					// if the app does not exist, we skip it
  1202  					// (It may not exist at all, or just hasn't been deployed to this environment yet)
  1203  					continue
  1204  				}
  1205  				return err
  1206  			}
  1207  			if version == nil || *version == 0 {
  1208  				// if nothing is deployed, ignore it
  1209  				continue
  1210  			}
  1211  			appData = append(appData, argocd.AppData{
  1212  				AppName:  appName,
  1213  				TeamName: team,
  1214  			})
  1215  		}
  1216  		spanCollectData.Finish()
  1217  
  1218  		spanRenderAndWrite, ctx := tracer.StartSpanFromContext(ctx, "RenderAndWrite")
  1219  		defer spanRenderAndWrite.Finish()
  1220  		if manifests, err := argocd.Render(ctx, r.config.URL, r.config.Branch, config, env, appData); err != nil {
  1221  			return err
  1222  		} else {
  1223  			spanWrite, _ := tracer.StartSpanFromContext(ctx, "Write")
  1224  			defer spanWrite.Finish()
  1225  			for apiVersion, content := range manifests {
  1226  				if err := fs.MkdirAll(fs.Join("argocd", string(apiVersion)), 0777); err != nil {
  1227  					return err
  1228  				}
  1229  				target := fs.Join("argocd", string(apiVersion), fmt.Sprintf("%s.yaml", env))
  1230  				if err := util.WriteFile(fs, target, content, 0666); err != nil {
  1231  					return err
  1232  				}
  1233  			}
  1234  		}
  1235  	}
  1236  	return nil
  1237  }
  1238  
  1239  func (r *repository) State() *State {
  1240  	s, err := r.StateAt(nil)
  1241  	if err != nil {
  1242  		panic(err)
  1243  	}
  1244  	return s
  1245  }
  1246  
  1247  func (r *repository) StateAt(oid *git.Oid) (*State, error) {
  1248  	var commit *git.Commit
  1249  	if oid == nil {
  1250  		if obj, err := r.repository.RevparseSingle(fmt.Sprintf("refs/heads/%s", r.config.Branch)); err != nil {
  1251  			var gerr *git.GitError
  1252  			if errors.As(err, &gerr) {
  1253  				if gerr.Code == git.ErrorCodeNotFound {
  1254  					return &State{
  1255  						Commit:                 nil,
  1256  						Filesystem:             fs.NewEmptyTreeBuildFS(r.repository),
  1257  						BootstrapMode:          r.config.BootstrapMode,
  1258  						EnvironmentConfigsPath: r.config.EnvironmentConfigsPath,
  1259  					}, nil
  1260  				}
  1261  			}
  1262  			return nil, err
  1263  		} else {
  1264  			commit, err = obj.AsCommit()
  1265  			if err != nil {
  1266  				return nil, err
  1267  			}
  1268  		}
  1269  	} else {
  1270  		var err error
  1271  		commit, err = r.repository.LookupCommit(oid)
  1272  		if err != nil {
  1273  			return nil, err
  1274  		}
  1275  	}
  1276  	return &State{
  1277  		Filesystem:             fs.NewTreeBuildFS(r.repository, commit.TreeId()),
  1278  		Commit:                 commit,
  1279  		BootstrapMode:          r.config.BootstrapMode,
  1280  		EnvironmentConfigsPath: r.config.EnvironmentConfigsPath,
  1281  	}, nil
  1282  }
  1283  
  1284  func (r *repository) Notify() *notify.Notify {
  1285  	return &r.notify
  1286  }
  1287  
  1288  type ObjectCount struct {
  1289  	Count       uint64
  1290  	Size        uint64
  1291  	InPack      uint64
  1292  	Packs       uint64
  1293  	SizePack    uint64
  1294  	Garbage     uint64
  1295  	SizeGarbage uint64
  1296  }
  1297  
  1298  func (r *repository) countObjects(ctx context.Context) (ObjectCount, error) {
  1299  	var stats ObjectCount
  1300  	/*
  1301  		The output of `git count-objects` looks like this:
  1302  			count: 0
  1303  			size: 0
  1304  			in-pack: 635
  1305  			packs: 1
  1306  			size-pack: 2845
  1307  			prune-packable: 0
  1308  			garbage: 0
  1309  			size-garbage: 0
  1310  	*/
  1311  	cmd := exec.CommandContext(ctx, "git", "count-objects", "--verbose")
  1312  	cmd.Dir = r.config.Path
  1313  	out, err := cmd.Output()
  1314  	if err != nil {
  1315  		return stats, err
  1316  	}
  1317  	scanner := bufio.NewScanner(bytes.NewReader(out))
  1318  	for scanner.Scan() {
  1319  		var (
  1320  			token string
  1321  			value uint64
  1322  		)
  1323  		if _, err := fmt.Sscan(scanner.Text(), &token, &value); err != nil {
  1324  			return stats, err
  1325  		}
  1326  		switch token {
  1327  		case "count:":
  1328  			stats.Count = value
  1329  		case "size:":
  1330  			stats.Size = value
  1331  		case "in-packs:":
  1332  			stats.InPack = value
  1333  		case "packs:":
  1334  			stats.Packs = value
  1335  		case "size-pack:":
  1336  			stats.SizePack = value
  1337  		case "garbage:":
  1338  			stats.Garbage = value
  1339  		case "size-garbage":
  1340  			stats.SizeGarbage = value
  1341  		}
  1342  	}
  1343  	return stats, nil
  1344  }
  1345  
  1346  func (r *repository) maybeGc(ctx context.Context) {
  1347  	if r.config.StorageBackend == SqliteBackend || r.config.GcFrequency == 0 || r.writesDone < r.config.GcFrequency {
  1348  		return
  1349  	}
  1350  	log := logger.FromContext(ctx)
  1351  	r.writesDone = 0
  1352  	timeBefore := time.Now()
  1353  	statsBefore, _ := r.countObjects(ctx)
  1354  	cmd := exec.CommandContext(ctx, "git", "repack", "-a", "-d")
  1355  	cmd.Dir = r.config.Path
  1356  	err := cmd.Run()
  1357  	if err != nil {
  1358  		log.Fatal("git.repack", zap.Error(err))
  1359  		return
  1360  	}
  1361  	statsAfter, _ := r.countObjects(ctx)
  1362  	log.Info("git.repack", zap.Duration("duration", time.Since(timeBefore)), zap.Uint64("collected", statsBefore.Count-statsAfter.Count))
  1363  }
  1364  
  1365  type State struct {
  1366  	Filesystem             billy.Filesystem
  1367  	Commit                 *git.Commit
  1368  	BootstrapMode          bool
  1369  	EnvironmentConfigsPath string
  1370  }
  1371  
  1372  func (s *State) Releases(application string) ([]uint64, error) {
  1373  	if entries, err := s.Filesystem.ReadDir(s.Filesystem.Join("applications", application, "releases")); err != nil {
  1374  		return nil, err
  1375  	} else {
  1376  		result := make([]uint64, 0, len(entries))
  1377  		for _, e := range entries {
  1378  			if i, err := strconv.ParseUint(e.Name(), 10, 64); err != nil {
  1379  				// just skip
  1380  			} else {
  1381  				result = append(result, i)
  1382  			}
  1383  		}
  1384  		return result, nil
  1385  	}
  1386  }
  1387  
  1388  func (s *State) ReleaseManifests(application string, release uint64) (map[string]string, error) {
  1389  	base := s.Filesystem.Join("applications", application, "releases", strconv.FormatUint(release, 10), "environments")
  1390  	if entries, err := s.Filesystem.ReadDir(base); err != nil {
  1391  		return nil, err
  1392  	} else {
  1393  		result := make(map[string]string, len(entries))
  1394  		for _, e := range entries {
  1395  			if buf, err := readFile(s.Filesystem, s.Filesystem.Join(base, e.Name(), "manifests.yaml")); err != nil {
  1396  				return nil, err
  1397  			} else {
  1398  				result[e.Name()] = string(buf)
  1399  			}
  1400  		}
  1401  		return result, nil
  1402  	}
  1403  }
  1404  
  1405  type Actor struct {
  1406  	Name  string
  1407  	Email string
  1408  }
  1409  
  1410  type Lock struct {
  1411  	Message   string
  1412  	CreatedBy Actor
  1413  	CreatedAt time.Time
  1414  }
  1415  
  1416  func readLock(fs billy.Filesystem, lockDir string) (*Lock, error) {
  1417  	lock := &Lock{
  1418  		Message: "",
  1419  		CreatedBy: Actor{
  1420  			Name:  "",
  1421  			Email: "",
  1422  		},
  1423  		CreatedAt: time.Time{},
  1424  	}
  1425  
  1426  	if cnt, err := readFile(fs, fs.Join(lockDir, "message")); err != nil {
  1427  		if !os.IsNotExist(err) {
  1428  			return nil, err
  1429  		}
  1430  	} else {
  1431  		lock.Message = string(cnt)
  1432  	}
  1433  
  1434  	if cnt, err := readFile(fs, fs.Join(lockDir, "created_by_email")); err != nil {
  1435  		if !os.IsNotExist(err) {
  1436  			return nil, err
  1437  		}
  1438  	} else {
  1439  		lock.CreatedBy.Email = string(cnt)
  1440  	}
  1441  
  1442  	if cnt, err := readFile(fs, fs.Join(lockDir, "created_by_name")); err != nil {
  1443  		if !os.IsNotExist(err) {
  1444  			return nil, err
  1445  		}
  1446  	} else {
  1447  		lock.CreatedBy.Name = string(cnt)
  1448  	}
  1449  
  1450  	if cnt, err := readFile(fs, fs.Join(lockDir, "created_at")); err != nil {
  1451  		if !os.IsNotExist(err) {
  1452  			return nil, err
  1453  		}
  1454  	} else {
  1455  		if createdAt, err := time.Parse(time.RFC3339, strings.TrimSpace(string(cnt))); err != nil {
  1456  			return nil, err
  1457  		} else {
  1458  			lock.CreatedAt = createdAt
  1459  		}
  1460  	}
  1461  
  1462  	return lock, nil
  1463  }
  1464  
  1465  func (s *State) GetEnvLocksDir(environment string) string {
  1466  	return s.Filesystem.Join("environments", environment, "locks")
  1467  }
  1468  
  1469  func (s *State) GetEnvLockDir(environment string, lockId string) string {
  1470  	return s.Filesystem.Join(s.GetEnvLocksDir(environment), lockId)
  1471  }
  1472  
  1473  func (s *State) GetAppLocksDir(environment string, application string) string {
  1474  	return s.Filesystem.Join("environments", environment, "applications", application, "locks")
  1475  }
  1476  
  1477  func (s *State) GetEnvironmentLocks(environment string) (map[string]Lock, error) {
  1478  	base := s.GetEnvLocksDir(environment)
  1479  	if entries, err := s.Filesystem.ReadDir(base); err != nil {
  1480  		return nil, err
  1481  	} else {
  1482  		result := make(map[string]Lock, len(entries))
  1483  		for _, e := range entries {
  1484  			if !e.IsDir() {
  1485  				return nil, fmt.Errorf("error getting environment locks: found file in the locks directory. run migration script to generate correct metadata")
  1486  			}
  1487  			if lock, err := readLock(s.Filesystem, s.Filesystem.Join(base, e.Name())); err != nil {
  1488  				return nil, err
  1489  			} else {
  1490  				result[e.Name()] = *lock
  1491  			}
  1492  		}
  1493  		return result, nil
  1494  	}
  1495  }
  1496  
  1497  func (s *State) GetEnvironmentApplicationLocks(environment, application string) (map[string]Lock, error) {
  1498  	base := s.GetAppLocksDir(environment, application)
  1499  	if entries, err := s.Filesystem.ReadDir(base); err != nil {
  1500  		return nil, err
  1501  	} else {
  1502  		result := make(map[string]Lock, len(entries))
  1503  		for _, e := range entries {
  1504  			if !e.IsDir() {
  1505  				return nil, fmt.Errorf("error getting application locks: found file in the locks directory. run migration script to generate correct metadata")
  1506  			}
  1507  			if lock, err := readLock(s.Filesystem, s.Filesystem.Join(base, e.Name())); err != nil {
  1508  				return nil, err
  1509  			} else {
  1510  				result[e.Name()] = *lock
  1511  			}
  1512  		}
  1513  		return result, nil
  1514  	}
  1515  }
  1516  
  1517  func (s *State) GetDeploymentMetaData(environment, application string) (string, time.Time, error) {
  1518  	base := s.Filesystem.Join("environments", environment, "applications", application)
  1519  	author, err := readFile(s.Filesystem, s.Filesystem.Join(base, "deployed_by"))
  1520  	if err != nil {
  1521  		if os.IsNotExist(err) {
  1522  			// for backwards compatibility, we do not return an error here
  1523  			return "", time.Time{}, nil
  1524  		} else {
  1525  			return "", time.Time{}, err
  1526  		}
  1527  	}
  1528  
  1529  	time_utc, err := readFile(s.Filesystem, s.Filesystem.Join(base, "deployed_at_utc"))
  1530  	if err != nil {
  1531  		if os.IsNotExist(err) {
  1532  			return string(author), time.Time{}, nil
  1533  		} else {
  1534  			return "", time.Time{}, err
  1535  		}
  1536  	}
  1537  
  1538  	deployedAt, err := time.Parse("2006-01-02 15:04:05 -0700 MST", strings.TrimSpace(string(time_utc)))
  1539  	if err != nil {
  1540  		return "", time.Time{}, err
  1541  	}
  1542  
  1543  	return string(author), deployedAt, nil
  1544  }
  1545  
  1546  func (s *State) DeleteAppLockIfEmpty(ctx context.Context, environment string, application string) error {
  1547  	dir := s.GetAppLocksDir(environment, application)
  1548  	_, err := s.DeleteDirIfEmpty(dir)
  1549  	return err
  1550  }
  1551  
  1552  func (s *State) DeleteEnvLockIfEmpty(ctx context.Context, environment string) error {
  1553  	dir := s.GetEnvLocksDir(environment)
  1554  	_, err := s.DeleteDirIfEmpty(dir)
  1555  	return err
  1556  }
  1557  
  1558  type SuccessReason int64
  1559  
  1560  const (
  1561  	NoReason SuccessReason = iota
  1562  	DirDoesNotExist
  1563  	DirNotEmpty
  1564  )
  1565  
  1566  // DeleteDirIfEmpty if it's empty. If the dir does not exist or is not empty, nothing happens.
  1567  // Errors are only returned if the read or delete operations fail.
  1568  // Returns SuccessReason for unit testing.
  1569  func (s *State) DeleteDirIfEmpty(directoryName string) (SuccessReason, error) {
  1570  	fileInfos, err := s.Filesystem.ReadDir(directoryName)
  1571  	if err != nil {
  1572  		return NoReason, fmt.Errorf("DeleteDirIfEmpty: failed to read directory %q: %w", directoryName, err)
  1573  	}
  1574  	if fileInfos == nil {
  1575  		// directory does not exist, nothing to do
  1576  		return DirDoesNotExist, nil
  1577  	}
  1578  	if len(fileInfos) == 0 {
  1579  		// directory exists, and is empty: delete it
  1580  		err = s.Filesystem.Remove(directoryName)
  1581  		if err != nil {
  1582  			return NoReason, fmt.Errorf("DeleteDirIfEmpty: failed to delete directory %q: %w", directoryName, err)
  1583  		}
  1584  		return NoReason, nil
  1585  	}
  1586  	return DirNotEmpty, nil
  1587  }
  1588  
  1589  func (s *State) GetQueuedVersion(environment string, application string) (*uint64, error) {
  1590  	return s.readSymlink(environment, application, queueFileName)
  1591  }
  1592  
  1593  func (s *State) DeleteQueuedVersion(environment string, application string) error {
  1594  	queuedVersion := s.Filesystem.Join("environments", environment, "applications", application, queueFileName)
  1595  	return s.Filesystem.Remove(queuedVersion)
  1596  }
  1597  
  1598  func (s *State) DeleteQueuedVersionIfExists(environment string, application string) error {
  1599  	queuedVersion, err := s.GetQueuedVersion(environment, application)
  1600  	if err != nil {
  1601  		return err
  1602  	}
  1603  	if queuedVersion == nil {
  1604  		return nil // nothing to do
  1605  	}
  1606  	return s.DeleteQueuedVersion(environment, application)
  1607  }
  1608  
  1609  func (s *State) GetEnvironmentApplicationVersion(environment, application string) (*uint64, error) {
  1610  	return s.readSymlink(environment, application, "version")
  1611  }
  1612  
  1613  // returns nil if there is no file
  1614  func (s *State) readSymlink(environment string, application string, symlinkName string) (*uint64, error) {
  1615  	version := s.Filesystem.Join("environments", environment, "applications", application, symlinkName)
  1616  	if lnk, err := s.Filesystem.Readlink(version); err != nil {
  1617  		if errors.Is(err, os.ErrNotExist) {
  1618  			// if the link does not exist, we return nil
  1619  			return nil, nil
  1620  		}
  1621  		return nil, fmt.Errorf("failed reading symlink %q: %w", version, err)
  1622  	} else {
  1623  		target := s.Filesystem.Join("environments", environment, "applications", application, lnk)
  1624  		if stat, err := s.Filesystem.Stat(target); err != nil {
  1625  			// if the file that the link points to does not exist, that's an error
  1626  			return nil, fmt.Errorf("failed stating %q: %w", target, err)
  1627  		} else {
  1628  			res, err := strconv.ParseUint(stat.Name(), 10, 64)
  1629  			return &res, err
  1630  		}
  1631  	}
  1632  }
  1633  
  1634  var InvalidJson = errors.New("JSON file is not valid")
  1635  
  1636  func envExists(envConfigs map[string]config.EnvironmentConfig, envNameToSearchFor string) bool {
  1637  	if _, found := envConfigs[envNameToSearchFor]; found {
  1638  		return true
  1639  	}
  1640  	return false
  1641  }
  1642  
  1643  func (s *State) GetEnvironmentConfigsAndValidate(ctx context.Context) (map[string]config.EnvironmentConfig, error) {
  1644  	logger := logger.FromContext(ctx)
  1645  	envConfigs, err := s.GetEnvironmentConfigs()
  1646  	if err != nil {
  1647  		return nil, err
  1648  	}
  1649  	if len(envConfigs) == 0 {
  1650  		logger.Warn("No environment configurations found. Check git settings like the branch name. Kuberpult cannot operate without environments.")
  1651  	}
  1652  	for envName, env := range envConfigs {
  1653  		if env.Upstream == nil || env.Upstream.Environment == "" {
  1654  			continue
  1655  		}
  1656  		upstreamEnv := env.Upstream.Environment
  1657  		if !envExists(envConfigs, upstreamEnv) {
  1658  			logger.Warn(fmt.Sprintf("The environment '%s' has upstream '%s' configured, but the environment '%s' does not exist.", envName, upstreamEnv, upstreamEnv))
  1659  		}
  1660  	}
  1661  	envGroups := mapper.MapEnvironmentsToGroups(envConfigs)
  1662  	for _, group := range envGroups {
  1663  		grpDist := group.Environments[0].DistanceToUpstream
  1664  		for _, env := range group.Environments {
  1665  			if env.DistanceToUpstream != grpDist {
  1666  				logger.Warn(fmt.Sprintf("The environment group '%s' has multiple environments setup with different distances to upstream", group.EnvironmentGroupName))
  1667  			}
  1668  		}
  1669  	}
  1670  	return envConfigs, err
  1671  }
  1672  
  1673  func (s *State) GetEnvironmentConfigs() (map[string]config.EnvironmentConfig, error) {
  1674  	if s.BootstrapMode {
  1675  		result := map[string]config.EnvironmentConfig{}
  1676  		buf, err := os.ReadFile(s.EnvironmentConfigsPath)
  1677  		if err != nil {
  1678  			if errors.Is(err, os.ErrNotExist) {
  1679  				return result, nil
  1680  			}
  1681  			return nil, err
  1682  		}
  1683  		err = json.Unmarshal(buf, &result)
  1684  		if err != nil {
  1685  			return nil, err
  1686  		}
  1687  		return result, nil
  1688  	} else {
  1689  		envs, err := s.Filesystem.ReadDir("environments")
  1690  		if err != nil {
  1691  			return nil, err
  1692  		}
  1693  		result := map[string]config.EnvironmentConfig{}
  1694  		for _, env := range envs {
  1695  			c, err := s.GetEnvironmentConfig(env.Name())
  1696  			if err != nil {
  1697  				return nil, err
  1698  
  1699  			}
  1700  			result[env.Name()] = *c
  1701  		}
  1702  		return result, nil
  1703  	}
  1704  }
  1705  
  1706  func (s *State) GetEnvironmentConfig(environmentName string) (*config.EnvironmentConfig, error) {
  1707  	fileName := s.Filesystem.Join("environments", environmentName, "config.json")
  1708  	var config config.EnvironmentConfig
  1709  	if err := decodeJsonFile(s.Filesystem, fileName, &config); err != nil {
  1710  		if !errors.Is(err, os.ErrNotExist) {
  1711  			return nil, fmt.Errorf("%s : %w", fileName, InvalidJson)
  1712  		}
  1713  	}
  1714  	return &config, nil
  1715  }
  1716  
  1717  func (s *State) GetEnvironmentConfigsForGroup(envGroup string) ([]string, error) {
  1718  	allEnvConfigs, err := s.GetEnvironmentConfigs()
  1719  	if err != nil {
  1720  		return nil, err
  1721  	}
  1722  	groupEnvNames := []string{}
  1723  	for env := range allEnvConfigs {
  1724  		envConfig := allEnvConfigs[env]
  1725  		g := envConfig.EnvironmentGroup
  1726  		if g != nil && *g == envGroup {
  1727  			groupEnvNames = append(groupEnvNames, env)
  1728  		}
  1729  	}
  1730  	if len(groupEnvNames) == 0 {
  1731  		return nil, fmt.Errorf("No environment found with given group '%s'", envGroup)
  1732  	}
  1733  	sort.Strings(groupEnvNames)
  1734  	return groupEnvNames, nil
  1735  }
  1736  
  1737  func (s *State) GetEnvironmentApplications(environment string) ([]string, error) {
  1738  	appDir := s.Filesystem.Join("environments", environment, "applications")
  1739  	return names(s.Filesystem, appDir)
  1740  }
  1741  
  1742  func (s *State) GetApplications() ([]string, error) {
  1743  	return names(s.Filesystem, "applications")
  1744  }
  1745  
  1746  func (s *State) GetApplicationReleases(application string) ([]uint64, error) {
  1747  	if ns, err := names(s.Filesystem, s.Filesystem.Join("applications", application, "releases")); err != nil {
  1748  		return nil, err
  1749  	} else {
  1750  		result := make([]uint64, 0, len(ns))
  1751  		for _, n := range ns {
  1752  			if i, err := strconv.ParseUint(n, 10, 64); err == nil {
  1753  				result = append(result, i)
  1754  			}
  1755  		}
  1756  		sort.Slice(result, func(i, j int) bool {
  1757  			return result[i] < result[j]
  1758  		})
  1759  		return result, nil
  1760  	}
  1761  }
  1762  
  1763  type Release struct {
  1764  	Version uint64
  1765  	/**
  1766  	"UndeployVersion=true" means that this version is empty, and has no manifest that could be deployed.
  1767  	It is intended to help cleanup old services within the normal release cycle (e.g. dev->staging->production).
  1768  	*/
  1769  	UndeployVersion bool
  1770  	SourceAuthor    string
  1771  	SourceCommitId  string
  1772  	SourceMessage   string
  1773  	CreatedAt       time.Time
  1774  	DisplayVersion  string
  1775  }
  1776  
  1777  func (rel *Release) ToProto() *api.Release {
  1778  	if rel == nil {
  1779  		return nil
  1780  	}
  1781  	return &api.Release{
  1782  		PrNumber:        extractPrNumber(rel.SourceMessage),
  1783  		Version:         rel.Version,
  1784  		SourceAuthor:    rel.SourceAuthor,
  1785  		SourceCommitId:  rel.SourceCommitId,
  1786  		SourceMessage:   rel.SourceMessage,
  1787  		UndeployVersion: rel.UndeployVersion,
  1788  		CreatedAt:       timestamppb.New(rel.CreatedAt),
  1789  		DisplayVersion:  rel.DisplayVersion,
  1790  	}
  1791  }
  1792  
  1793  func extractPrNumber(sourceMessage string) string {
  1794  	re := regexp.MustCompile(`\(#(\d+)\)`)
  1795  	res := re.FindAllStringSubmatch(sourceMessage, -1)
  1796  
  1797  	if len(res) == 0 {
  1798  		return ""
  1799  	} else {
  1800  		return res[len(res)-1][1]
  1801  	}
  1802  }
  1803  
  1804  func (s *State) IsUndeployVersion(application string, version uint64) (bool, error) {
  1805  	base := releasesDirectoryWithVersion(s.Filesystem, application, version)
  1806  	_, err := s.Filesystem.Stat(base)
  1807  	if err != nil {
  1808  		return false, wrapFileError(err, base, "could not call stat")
  1809  	}
  1810  	if _, err := readFile(s.Filesystem, s.Filesystem.Join(base, "undeploy")); err != nil {
  1811  		if !os.IsNotExist(err) {
  1812  			return false, err
  1813  		}
  1814  		return false, nil
  1815  	}
  1816  	return true, nil
  1817  }
  1818  
  1819  func (s *State) GetApplicationRelease(application string, version uint64) (*Release, error) {
  1820  	base := releasesDirectoryWithVersion(s.Filesystem, application, version)
  1821  	_, err := s.Filesystem.Stat(base)
  1822  	if err != nil {
  1823  		return nil, wrapFileError(err, base, "could not call stat")
  1824  	}
  1825  	release := Release{
  1826  		Version:         version,
  1827  		UndeployVersion: false,
  1828  		SourceAuthor:    "",
  1829  		SourceCommitId:  "",
  1830  		SourceMessage:   "",
  1831  		CreatedAt:       time.Time{},
  1832  		DisplayVersion:  "",
  1833  	}
  1834  	if cnt, err := readFile(s.Filesystem, s.Filesystem.Join(base, "source_commit_id")); err != nil {
  1835  		if !os.IsNotExist(err) {
  1836  			return nil, err
  1837  		}
  1838  	} else {
  1839  		release.SourceCommitId = string(cnt)
  1840  	}
  1841  	if cnt, err := readFile(s.Filesystem, s.Filesystem.Join(base, "source_author")); err != nil {
  1842  		if !os.IsNotExist(err) {
  1843  			return nil, err
  1844  		}
  1845  	} else {
  1846  		release.SourceAuthor = string(cnt)
  1847  	}
  1848  	if cnt, err := readFile(s.Filesystem, s.Filesystem.Join(base, "source_message")); err != nil {
  1849  		if !os.IsNotExist(err) {
  1850  			return nil, err
  1851  		}
  1852  	} else {
  1853  		release.SourceMessage = string(cnt)
  1854  	}
  1855  	if displayVersion, err := readFile(s.Filesystem, s.Filesystem.Join(base, "display_version")); err != nil {
  1856  		if !os.IsNotExist(err) {
  1857  			return nil, err
  1858  		}
  1859  		release.DisplayVersion = ""
  1860  	} else {
  1861  		release.DisplayVersion = string(displayVersion)
  1862  	}
  1863  	isUndeploy, err := s.IsUndeployVersion(application, version)
  1864  	if err != nil {
  1865  		return nil, err
  1866  	}
  1867  	release.UndeployVersion = isUndeploy
  1868  	if cnt, err := readFile(s.Filesystem, s.Filesystem.Join(base, "created_at")); err != nil {
  1869  		if !os.IsNotExist(err) {
  1870  			return nil, err
  1871  		}
  1872  	} else {
  1873  		if releaseTime, err := time.Parse(time.RFC3339, strings.TrimSpace(string(cnt))); err != nil {
  1874  			return nil, err
  1875  		} else {
  1876  			release.CreatedAt = releaseTime
  1877  		}
  1878  	}
  1879  	return &release, nil
  1880  }
  1881  
  1882  func (s *State) GetApplicationReleaseManifests(application string, version uint64) (map[string]*api.Manifest, error) {
  1883  	dir := manifestDirectoryWithReleasesVersion(s.Filesystem, application, version)
  1884  
  1885  	entries, err := s.Filesystem.ReadDir(dir)
  1886  	if err != nil {
  1887  		return nil, fmt.Errorf("reading manifest directory: %w", err)
  1888  	}
  1889  	manifests := map[string]*api.Manifest{}
  1890  	for _, entry := range entries {
  1891  		if !entry.IsDir() {
  1892  			continue
  1893  		}
  1894  		manifestPath := filepath.Join(dir, entry.Name(), "manifests.yaml")
  1895  		file, err := s.Filesystem.Open(manifestPath)
  1896  		if err != nil {
  1897  			return nil, fmt.Errorf("failed to open %s: %w", manifestPath, err)
  1898  		}
  1899  		content, err := io.ReadAll(file)
  1900  		if err != nil {
  1901  			return nil, fmt.Errorf("failed to read %s: %w", manifestPath, err)
  1902  		}
  1903  
  1904  		manifests[entry.Name()] = &api.Manifest{
  1905  			Environment: entry.Name(),
  1906  			Content:     string(content),
  1907  		}
  1908  	}
  1909  	return manifests, nil
  1910  }
  1911  
  1912  func (s *State) GetApplicationTeamOwner(application string) (string, error) {
  1913  	appDir := applicationDirectory(s.Filesystem, application)
  1914  	appTeam := s.Filesystem.Join(appDir, "team")
  1915  
  1916  	if team, err := readFile(s.Filesystem, appTeam); err != nil {
  1917  		if os.IsNotExist(err) {
  1918  			return "", nil
  1919  		} else {
  1920  			return "", fmt.Errorf("error while reading team owner file for application %v found: %w", application, err)
  1921  		}
  1922  	} else {
  1923  		return string(team), nil
  1924  	}
  1925  }
  1926  
  1927  func (s *State) GetApplicationSourceRepoUrl(application string) (string, error) {
  1928  	appDir := applicationDirectory(s.Filesystem, application)
  1929  	appSourceRepoUrl := s.Filesystem.Join(appDir, "sourceRepoUrl")
  1930  
  1931  	if url, err := readFile(s.Filesystem, appSourceRepoUrl); err != nil {
  1932  		if os.IsNotExist(err) {
  1933  			return "", nil
  1934  		} else {
  1935  			return "", fmt.Errorf("error while reading sourceRepoUrl file for application %v found: %w", application, err)
  1936  		}
  1937  	} else {
  1938  		return string(url), nil
  1939  	}
  1940  }
  1941  
  1942  func names(fs billy.Filesystem, path string) ([]string, error) {
  1943  	files, err := fs.ReadDir(path)
  1944  	if err != nil {
  1945  		return nil, err
  1946  	}
  1947  	result := make([]string, 0, len(files))
  1948  	for _, app := range files {
  1949  		result = append(result, app.Name())
  1950  	}
  1951  	return result, nil
  1952  }
  1953  
  1954  func decodeJsonFile(fs billy.Filesystem, path string, out interface{}) error {
  1955  	if file, err := fs.Open(path); err != nil {
  1956  		return wrapFileError(err, path, "could not decode json file")
  1957  	} else {
  1958  		defer file.Close()
  1959  		dec := json.NewDecoder(file)
  1960  		return dec.Decode(out)
  1961  	}
  1962  }
  1963  
  1964  func readFile(fs billy.Filesystem, path string) ([]byte, error) {
  1965  	if file, err := fs.Open(path); err != nil {
  1966  		return nil, err
  1967  	} else {
  1968  		defer file.Close()
  1969  		return io.ReadAll(file)
  1970  	}
  1971  }
  1972  
  1973  // ProcessQueue checks if there is something in the queue
  1974  // deploys if necessary
  1975  // deletes the queue
  1976  func (s *State) ProcessQueue(ctx context.Context, fs billy.Filesystem, environment string, application string) (string, error) {
  1977  	queuedVersion, err := s.GetQueuedVersion(environment, application)
  1978  	queueDeploymentMessage := ""
  1979  	if err != nil {
  1980  		// could not read queued version.
  1981  		return "", err
  1982  	} else {
  1983  		if queuedVersion == nil {
  1984  			// if there is no version queued, that's not an issue, just do nothing:
  1985  			return "", nil
  1986  		}
  1987  
  1988  		currentlyDeployedVersion, err := s.GetEnvironmentApplicationVersion(environment, application)
  1989  		if err != nil {
  1990  			return "", err
  1991  		}
  1992  
  1993  		if currentlyDeployedVersion != nil && *queuedVersion == *currentlyDeployedVersion {
  1994  			// delete queue, it's outdated! But if we can't, that's not really a problem, as it would be overwritten
  1995  			// whenever the next deployment happens:
  1996  			err = s.DeleteQueuedVersion(environment, application)
  1997  			return fmt.Sprintf("deleted queued version %d because it was already deployed. app=%q env=%q", *queuedVersion, application, environment), err
  1998  		}
  1999  	}
  2000  	return queueDeploymentMessage, nil
  2001  }