diff --git a/go.mod b/go.mod index ba6866d7..e9b87a29 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691 golang.org/x/sync v0.3.0 gopkg.in/yaml.v2 v2.4.0 + helm.sh/helm v2.17.0+incompatible k8s.io/api v0.24.2 k8s.io/apimachinery v0.24.2 k8s.io/client-go v1.5.2 @@ -41,7 +42,9 @@ require ( github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/BurntSushi/toml v0.3.1 // indirect github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd // indirect + github.com/Masterminds/semver v1.5.0 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect github.com/acomagu/bufpipe v1.0.4 // indirect @@ -53,6 +56,7 @@ require ( github.com/chai2010/gettext-go v0.0.0-20170215093142-bf70f2a70fb1 // indirect github.com/cloudflare/circl v1.3.3 // indirect github.com/coreos/go-oidc/v3 v3.4.0 // indirect + github.com/cyphar/filepath-securejoin v0.2.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect @@ -160,6 +164,7 @@ require ( k8s.io/cli-runtime v0.24.2 // indirect k8s.io/component-base v0.24.2 // indirect k8s.io/component-helpers v0.24.2 // indirect + k8s.io/helm v2.17.0+incompatible // indirect k8s.io/klog/v2 v2.90.1 // indirect k8s.io/kube-aggregator v0.24.2 // indirect k8s.io/kube-openapi v0.0.0-20220627174259-011e075b9cb8 // indirect diff --git a/go.sum b/go.sum index c853ae56..c96da5b4 100644 --- a/go.sum +++ b/go.sum @@ -414,12 +414,15 @@ github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+Z github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/GoogleCloudPlatform/k8s-cloud-provider v1.16.1-0.20210702024009-ea6160c1d0e3/go.mod h1:8XasY4ymP2V/tn2OOV9ZadmiTE1FIB/h3W+yNlPttKw= github.com/JeffAshton/win_pdh v0.0.0-20161109143554-76bb4ee9f0ab/go.mod h1:3VYc5hodBMJ5+l/7J4xAyMeuM2PNuepvHlGs8yilUCA= github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd h1:sjQovDkwrZp8u+gxLtPgKGjk5hCxuy2hrRejBTA9xFU= github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E= +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Microsoft/go-winio v0.4.15/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= @@ -553,6 +556,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cyphar/filepath-securejoin v0.2.3 h1:YX6ebbZCZP7VkM3scTTokDgBL2TY741X51MTk3ycuNI= github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -1910,6 +1914,8 @@ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= +helm.sh/helm v2.17.0+incompatible h1:cSe3FaQOpRWLDXvTObQNj0P7WI98IG5yloU6tQVls2k= +helm.sh/helm v2.17.0+incompatible/go.mod h1:0Xbc6ErzwWH9qC55X1+hE3ZwhM3atbhCm/NbFZw5i+4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -1942,6 +1948,8 @@ k8s.io/csi-translation-lib v0.24.2/go.mod h1:pdHc2CYLViQYYsOqOp79hjKYi8J4NZ7vpiV k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/gengo v0.0.0-20211129171323-c02415ce4185/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +k8s.io/helm v2.17.0+incompatible h1:Bpn6o1wKLYqKM3+Osh8e+1/K2g/GsQJ4F4yNF2+deao= +k8s.io/helm v2.17.0+incompatible/go.mod h1:LZzlS4LQBHfciFOurYBFkCMTaZ0D1l+p0teMg7TSULI= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= diff --git a/pkg/argocd/argocd.go b/pkg/argocd/argocd.go index d7c9394e..f4ab9521 100644 --- a/pkg/argocd/argocd.go +++ b/pkg/argocd/argocd.go @@ -527,6 +527,10 @@ func getApplicationType(app *v1alpha1.Application) ApplicationType { return ApplicationTypeKustomize } else if sourceType == v1alpha1.ApplicationSourceTypeHelm { return ApplicationTypeHelm + } else if st, set := app.Annotations[common.WriteBackTargetAnnotation]; set && + strings.HasPrefix(st, common.HelmPrefix) && + sourceType == v1alpha1.ApplicationSourceTypePlugin { + return ApplicationTypeHelm } else { return ApplicationTypeUnsupported } diff --git a/pkg/argocd/git.go b/pkg/argocd/git.go index 4f1de571..ecc0b53d 100644 --- a/pkg/argocd/git.go +++ b/pkg/argocd/git.go @@ -8,8 +8,10 @@ import ( "os" "path" "path/filepath" + "strings" "text/template" + "helm.sh/helm/pkg/chartutil" "sigs.k8s.io/kustomize/api/konfig" "sigs.k8s.io/kustomize/api/types" kyaml "sigs.k8s.io/kustomize/kyaml/yaml" @@ -297,6 +299,62 @@ func writeOverrides(app *v1alpha1.Application, wbc *WriteBackConfig, gitC git.Cl var _ changeWriter = writeOverrides +// writeHelm writes any changes required for updating one or more images to a values.yaml +func writeHelm(app *v1alpha1.Application, wbc *WriteBackConfig, gitC git.Client) (err error, skip bool) { + logCtx := log.WithContext().AddField("application", app.GetName()) + + fileName := filepath.Join(gitC.Root(), wbc.HelmBase) + + logCtx.Infof("updating valuesfile %s", fileName) + + valuesFile, err := chartutil.ReadValuesFile(fileName) + if err != nil { + panic(err) + } + + // As we've defined the ApplicationSourceTypePlugin combined with WriteBackTargetAnnotation starting with Helm + // it is actually safe to use app.Spec.Source.Helm.Parameters for updating values.yaml + parameters := app.Spec.Source.Helm.Parameters + + // Update all image tag and image repository values + for _, parameter := range parameters { + fullYamlPath := strings.Split(parameter.Name, ".") + yamlPath := strings.Join(fullYamlPath[0:len(fullYamlPath)-1], ".") // remove the last path to be able to update the value of it later on + + image, err := valuesFile.Table(yamlPath) + if err != nil { + logCtx.Errorf("could not find path %s", yamlPath) + } + + valuePath := fullYamlPath[len(fullYamlPath)-1] + image[valuePath] = parameter.Value + } + + // Save the valuesfile back to the original file; first truncate the file and write back the content + newValuesFile, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + logCtx.Errorf("Cannot open file %s to write values to", fileName) + return err, false + } + defer newValuesFile.Close() + + err = newValuesFile.Truncate(0) + if err != nil { + logCtx.Errorf("Cannot truncate valuesfile %s", fileName) + return err, false + } + + err = valuesFile.Encode(newValuesFile) + if err != nil { + logCtx.Errorf("Unable to write data into the valuesfile %s", fileName) + return err, false + } + + return nil, false +} + +var _ changeWriter = writeHelm + // writeKustomization writes any changes required for updating one or more images to a kustomization.yml func writeKustomization(app *v1alpha1.Application, wbc *WriteBackConfig, gitC git.Client) (err error, skip bool) { logCtx := log.WithContext().AddField("application", app.GetName()) diff --git a/pkg/argocd/update.go b/pkg/argocd/update.go index 2910d99d..0ee0ee7e 100644 --- a/pkg/argocd/update.go +++ b/pkg/argocd/update.go @@ -69,6 +69,7 @@ type WriteBackConfig struct { GitCommitEmail string GitCommitMessage string KustomizeBase string + HelmBase string Target string GitRepo string } @@ -488,6 +489,9 @@ func getWriteBackConfig(app *v1alpha1.Application, kubeClient *kube.KubernetesCl if target, ok := app.Annotations[common.WriteBackTargetAnnotation]; ok && strings.HasPrefix(target, common.KustomizationPrefix) { wbc.KustomizeBase = parseTarget(target, app.Spec.Source.Path) } + if target, ok := app.Annotations[common.WriteBackTargetAnnotation]; ok && strings.HasPrefix(target, common.HelmPrefix) { + wbc.HelmBase = parseHelmTarget(target, app.Spec.Source.Path) + } if err := parseGitConfig(app, kubeClient, wbc, creds); err != nil { return nil, err } @@ -514,6 +518,17 @@ func parseTarget(target string, sourcePath string) (kustomizeBase string) { } } +// parseHelmTarget is a temporary function to avoid slice-out-of-bounds errors when common.KustomizationPrefix isn't set +func parseHelmTarget(target string, sourcePath string) (kustomizeBase string) { + if target == common.HelmPrefix { + return filepath.Join(sourcePath, common.DefaultHelmTargetFile) + } else if base := target[len(common.HelmPrefix)+1:]; strings.HasPrefix(base, "/") { + return base[1:] + } else { + return filepath.Join(sourcePath, base) + } +} + func parseGitConfig(app *v1alpha1.Application, kubeClient *kube.KubernetesClient, wbc *WriteBackConfig, creds string) error { branch, ok := app.Annotations[common.GitBranchAnnotation] if ok { @@ -565,6 +580,8 @@ func commitChanges(app *v1alpha1.Application, wbc *WriteBackConfig, changeList [ // if the kustomize base is set, the target is a kustomization if wbc.KustomizeBase != "" { return commitChangesGit(app, wbc, changeList, writeKustomization) + } else if wbc.HelmBase != "" { + return commitChangesGit(app, wbc, changeList, writeHelm) } return commitChangesGit(app, wbc, changeList, writeOverrides) default: diff --git a/pkg/common/constants.go b/pkg/common/constants.go index 5abf6da6..b6dd17b2 100644 --- a/pkg/common/constants.go +++ b/pkg/common/constants.go @@ -53,11 +53,15 @@ const ( GitRepositoryAnnotation = ImageUpdaterAnnotationPrefix + "/git-repository" WriteBackTargetAnnotation = ImageUpdaterAnnotationPrefix + "/write-back-target" KustomizationPrefix = "kustomization" + HelmPrefix = "helm" ) // DefaultTargetFilePattern configurations related to the write-back functionality const DefaultTargetFilePattern = ".argocd-source-%s.yaml" +// DefaultHelmTargetFile configurations related to the write-back using helm functionality +const DefaultHelmTargetFile = "values.yaml" + // The default Git commit message's template const DefaultGitCommitMessage = `build: automatic update of {{ .AppName }}