github.com/freiheit-com/kuberpult@v1.24.2-0.20240328135542-315d5630abe6/services/cd-service/pkg/argocd/render.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 argocd
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"path/filepath"
    23  	"strings"
    24  
    25  	"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
    26  
    27  	"sigs.k8s.io/yaml"
    28  
    29  	"github.com/freiheit-com/kuberpult/services/cd-service/pkg/argocd/v1alpha1"
    30  	"github.com/freiheit-com/kuberpult/services/cd-service/pkg/config"
    31  )
    32  
    33  type ApiVersion string
    34  
    35  const V1Alpha1 ApiVersion = "v1alpha1"
    36  
    37  type AppData struct {
    38  	AppName  string
    39  	TeamName string
    40  }
    41  
    42  func Render(ctx context.Context, gitUrl string, gitBranch string, config config.EnvironmentConfig, env string, appsData []AppData) (map[ApiVersion][]byte, error) {
    43  	span, _ := tracer.StartSpanFromContext(ctx, "Render")
    44  	defer span.Finish()
    45  
    46  	if config.ArgoCd == nil {
    47  		return nil, fmt.Errorf("no ArgoCd configured for environment %s", env)
    48  	}
    49  	result := map[ApiVersion][]byte{}
    50  	if content, err := RenderV1Alpha1(gitUrl, gitBranch, config, env, appsData); err != nil {
    51  		return nil, err
    52  	} else {
    53  		result[V1Alpha1] = content
    54  	}
    55  	return result, nil
    56  }
    57  
    58  func RenderV1Alpha1(gitUrl string, gitBranch string, config config.EnvironmentConfig, env string, appsData []AppData) ([]byte, error) {
    59  	applicationNs := ""
    60  	if config.ArgoCd.Destination.Namespace != nil {
    61  		applicationNs = *config.ArgoCd.Destination.Namespace
    62  	} else if config.ArgoCd.Destination.ApplicationNamespace != nil {
    63  		applicationNs = *config.ArgoCd.Destination.ApplicationNamespace
    64  	}
    65  	applicationDestination := v1alpha1.ApplicationDestination{
    66  		Name:      config.ArgoCd.Destination.Name,
    67  		Namespace: applicationNs,
    68  		Server:    config.ArgoCd.Destination.Server,
    69  	}
    70  	buf := []string{}
    71  	syncWindows := v1alpha1.SyncWindows{}
    72  	for _, w := range config.ArgoCd.SyncWindows {
    73  		apps := []string{"*"}
    74  		if len(w.Apps) > 0 {
    75  			apps = w.Apps
    76  		}
    77  		syncWindows = append(syncWindows, &v1alpha1.SyncWindow{
    78  			Applications: apps,
    79  			Schedule:     w.Schedule,
    80  			Duration:     w.Duration,
    81  			Kind:         w.Kind,
    82  			ManualSync:   true,
    83  		})
    84  	}
    85  	accessEntries := []v1alpha1.AccessEntry{}
    86  	for _, w := range config.ArgoCd.ClusterResourceWhitelist {
    87  		accessEntries = append(accessEntries, v1alpha1.AccessEntry{
    88  			Kind:  w.Kind,
    89  			Group: w.Group,
    90  		})
    91  	}
    92  
    93  	appProjectNs := ""
    94  	appProjectDestination := applicationDestination
    95  	if config.ArgoCd.Destination.Namespace != nil {
    96  		appProjectNs = *config.ArgoCd.Destination.Namespace
    97  	} else if config.ArgoCd.Destination.AppProjectNamespace != nil {
    98  		appProjectNs = *config.ArgoCd.Destination.AppProjectNamespace
    99  	}
   100  	appProjectDestination.Namespace = appProjectNs
   101  
   102  	project := v1alpha1.AppProject{
   103  		TypeMeta: v1alpha1.AppProjectTypeMeta,
   104  		ObjectMeta: v1alpha1.ObjectMeta{
   105  			Annotations: nil,
   106  			Labels:      nil,
   107  			Finalizers:  nil,
   108  			Name:        env,
   109  		},
   110  		Spec: v1alpha1.AppProjectSpec{
   111  			Description:              env,
   112  			SourceRepos:              []string{"*"},
   113  			Destinations:             []v1alpha1.ApplicationDestination{appProjectDestination},
   114  			SyncWindows:              syncWindows,
   115  			ClusterResourceWhitelist: accessEntries,
   116  		},
   117  	}
   118  	if content, err := yaml.Marshal(&project); err != nil {
   119  		return nil, err
   120  	} else {
   121  		buf = append(buf, string(content))
   122  	}
   123  	ignoreDifferences := make([]v1alpha1.ResourceIgnoreDifferences, len(config.ArgoCd.IgnoreDifferences))
   124  	for index, value := range config.ArgoCd.IgnoreDifferences {
   125  		ignoreDifferences[index] = v1alpha1.ResourceIgnoreDifferences(value)
   126  	}
   127  	syncOptions := config.ArgoCd.SyncOptions
   128  	for _, appData := range appsData {
   129  		appManifest, err := RenderAppEnv(gitUrl, gitBranch, config.ArgoCd.ApplicationAnnotations, env, appData, applicationDestination, ignoreDifferences, syncOptions)
   130  		if err != nil {
   131  			return nil, err
   132  		}
   133  		buf = append(buf, appManifest)
   134  	}
   135  	return ([]byte)(strings.Join(buf, "---\n")), nil
   136  }
   137  
   138  func RenderAppEnv(gitUrl string, gitBranch string, applicationAnnotations map[string]string, env string, appData AppData, destination v1alpha1.ApplicationDestination, ignoreDifferences []v1alpha1.ResourceIgnoreDifferences, syncOptions v1alpha1.SyncOptions) (string, error) {
   139  	name := appData.AppName
   140  	annotations := map[string]string{}
   141  	labels := map[string]string{}
   142  	manifestPath := filepath.Join("environments", env, "applications", name, "manifests")
   143  	for k, v := range applicationAnnotations {
   144  		annotations[k] = v
   145  	}
   146  	annotations["com.freiheit.kuberpult/team"] = appData.TeamName
   147  	annotations["com.freiheit.kuberpult/application"] = name
   148  	annotations["com.freiheit.kuberpult/environment"] = env
   149  	// This annotation is so that argoCd does not invalidate *everything* in the whole repo when receiving a git webhook.
   150  	// It has to start with a "/" to be absolute to the git repo.
   151  	// See https://argo-cd.readthedocs.io/en/stable/operator-manual/high_availability/#webhook-and-manifest-paths-annotation
   152  	annotations["argocd.argoproj.io/manifest-generate-paths"] = "/" + manifestPath
   153  	labels["com.freiheit.kuberpult/team"] = appData.TeamName
   154  	app := v1alpha1.Application{
   155  		TypeMeta: v1alpha1.ApplicationTypeMeta,
   156  		ObjectMeta: v1alpha1.ObjectMeta{
   157  			Name:        fmt.Sprintf("%s-%s", env, name),
   158  			Annotations: annotations,
   159  			Labels:      labels,
   160  			Finalizers:  calculateFinalizers(),
   161  		},
   162  		Spec: v1alpha1.ApplicationSpec{
   163  			Project: env,
   164  			Source: v1alpha1.ApplicationSource{
   165  				RepoURL:        gitUrl,
   166  				Path:           manifestPath,
   167  				TargetRevision: gitBranch,
   168  			},
   169  			Destination: destination,
   170  			SyncPolicy: &v1alpha1.SyncPolicy{
   171  				Automated: &v1alpha1.SyncPolicyAutomated{
   172  					Prune:    true,
   173  					SelfHeal: true,
   174  					// We always allow empty, because it makes it easier to delete apps/environments
   175  					AllowEmpty: true,
   176  				},
   177  				SyncOptions: syncOptions,
   178  			},
   179  			IgnoreDifferences: ignoreDifferences,
   180  		},
   181  	}
   182  	if content, err := yaml.Marshal(&app); err != nil {
   183  		return "", err
   184  	} else {
   185  		return string(content), nil
   186  	}
   187  }
   188  
   189  func calculateFinalizers() []string {
   190  	return []string{
   191  		"resources-finalizer.argocd.argoproj.io",
   192  	}
   193  }