From 650e04a90e17870010237ddde07b1ab81e6d5f29 Mon Sep 17 00:00:00 2001 From: Casey Morton Date: Mon, 16 Oct 2023 14:35:28 -0400 Subject: [PATCH 1/6] Comverted all instances of app.Spec.Source to normalize on app.Spec.GetSources[0] instead Signed-off-by: Casey Morton --- pkg/argocd/argocd.go | 27 +++++++----- pkg/argocd/argocd_test.go | 93 +++++++++++++++++++++++++++++++-------- 2 files changed, 91 insertions(+), 29 deletions(-) diff --git a/pkg/argocd/argocd.go b/pkg/argocd/argocd.go index d7c9394e..248686ad 100644 --- a/pkg/argocd/argocd.go +++ b/pkg/argocd/argocd.go @@ -425,15 +425,15 @@ func SetHelmImage(app *v1alpha1.Application, newImage *image.ContainerImage) err } } - if app.Spec.Source.Helm == nil { - app.Spec.Source.Helm = &v1alpha1.ApplicationSourceHelm{} + if app.Spec.GetSources()[0].Helm == nil { + app.Spec.GetSources()[0].Helm = &v1alpha1.ApplicationSourceHelm{} } - if app.Spec.Source.Helm.Parameters == nil { - app.Spec.Source.Helm.Parameters = make([]v1alpha1.HelmParameter, 0) + if app.Spec.GetSources()[0].Helm.Parameters == nil { + app.Spec.GetSources()[0].Helm.Parameters = make([]v1alpha1.HelmParameter, 0) } - app.Spec.Source.Helm.Parameters = mergeHelmParams(app.Spec.Source.Helm.Parameters, mergeParams) + app.Spec.GetSources()[0].Helm.Parameters = mergeHelmParams(app.Spec.GetSources()[0].Helm.Parameters, mergeParams) return nil } @@ -454,22 +454,29 @@ func SetKustomizeImage(app *v1alpha1.Application, newImage *image.ContainerImage log.WithContext().AddField("application", app.GetName()).Tracef("Setting Kustomize parameter %s", ksImageParam) - if app.Spec.Source.Kustomize == nil { - app.Spec.Source.Kustomize = &v1alpha1.ApplicationSourceKustomize{} + var source = app.Spec.GetSources()[0] + if source.Kustomize == nil { + source.Kustomize = &v1alpha1.ApplicationSourceKustomize{} } - for i, kImg := range app.Spec.Source.Kustomize.Images { + for i, kImg := range source.Kustomize.Images { curr := image.NewFromIdentifier(string(kImg)) override := image.NewFromIdentifier(ksImageParam) if curr.ImageName == override.ImageName { curr.ImageAlias = override.ImageAlias - app.Spec.Source.Kustomize.Images[i] = v1alpha1.KustomizeImage(override.String()) + source.Kustomize.Images[i] = v1alpha1.KustomizeImage(override.String()) } } - app.Spec.Source.Kustomize.MergeImage(v1alpha1.KustomizeImage(ksImageParam)) + source.Kustomize.MergeImage(v1alpha1.KustomizeImage(ksImageParam)) + + if app.Spec.HasMultipleSources() { + app.Spec.Sources[0] = source + } else { + app.Spec.Source = &source + } return nil } diff --git a/pkg/argocd/argocd_test.go b/pkg/argocd/argocd_test.go index fd5b5ded..d47662d5 100644 --- a/pkg/argocd/argocd_test.go +++ b/pkg/argocd/argocd_test.go @@ -468,9 +468,9 @@ func Test_SetKustomizeImage(t *testing.T) { img := image.NewFromIdentifier("jannfis/foobar:1.0.1") err := SetKustomizeImage(app, img) require.NoError(t, err) - require.NotNil(t, app.Spec.Source.Kustomize) - assert.Len(t, app.Spec.Source.Kustomize.Images, 1) - assert.Equal(t, v1alpha1.KustomizeImage("jannfis/foobar:1.0.1"), app.Spec.Source.Kustomize.Images[0]) + require.NotNil(t, app.Spec.GetSources()[0].Kustomize) + assert.Len(t, app.Spec.GetSources()[0].Kustomize.Images, 1) + assert.Equal(t, v1alpha1.KustomizeImage("jannfis/foobar:1.0.1"), app.Spec.GetSources()[0].Kustomize.Images[0]) }) t.Run("Test set Kustomize image parameters on Kustomize app with no params set", func(t *testing.T) { @@ -494,9 +494,9 @@ func Test_SetKustomizeImage(t *testing.T) { img := image.NewFromIdentifier("jannfis/foobar:1.0.1") err := SetKustomizeImage(app, img) require.NoError(t, err) - require.NotNil(t, app.Spec.Source.Kustomize) - assert.Len(t, app.Spec.Source.Kustomize.Images, 1) - assert.Equal(t, v1alpha1.KustomizeImage("jannfis/foobar:1.0.1"), app.Spec.Source.Kustomize.Images[0]) + require.NotNil(t, app.Spec.GetSources()[0].Kustomize) + assert.Len(t, app.Spec.GetSources()[0].Kustomize.Images, 1) + assert.Equal(t, v1alpha1.KustomizeImage("jannfis/foobar:1.0.1"), app.Spec.GetSources()[0].Kustomize.Images[0]) }) t.Run("Test set Kustomize image parameters on non-Kustomize app", func(t *testing.T) { @@ -558,9 +558,9 @@ func Test_SetKustomizeImage(t *testing.T) { img := image.NewFromIdentifier("foobar=jannfis/foobar:1.0.1") err := SetKustomizeImage(app, img) require.NoError(t, err) - require.NotNil(t, app.Spec.Source.Kustomize) - assert.Len(t, app.Spec.Source.Kustomize.Images, 1) - assert.Equal(t, v1alpha1.KustomizeImage("foobar=jannfis/foobar:1.0.1"), app.Spec.Source.Kustomize.Images[0]) + require.NotNil(t, app.Spec.GetSources()[0].Kustomize) + assert.Len(t, app.Spec.GetSources()[0].Kustomize.Images, 1) + assert.Equal(t, v1alpha1.KustomizeImage("foobar=jannfis/foobar:1.0.1"), app.Spec.GetSources()[0].Kustomize.Images[0]) }) } @@ -606,12 +606,12 @@ func Test_SetHelmImage(t *testing.T) { err := SetHelmImage(app, img) require.NoError(t, err) - require.NotNil(t, app.Spec.Source.Helm) - assert.Len(t, app.Spec.Source.Helm.Parameters, 2) + require.NotNil(t, app.Spec.GetSources()[0].Helm) + assert.Len(t, app.Spec.GetSources()[0].Helm.Parameters, 2) // Find correct parameter var tagParam v1alpha1.HelmParameter - for _, p := range app.Spec.Source.Helm.Parameters { + for _, p := range app.Spec.GetSources()[0].Helm.Parameters { if p.Name == "image.tag" { tagParam = p break @@ -649,12 +649,67 @@ func Test_SetHelmImage(t *testing.T) { err := SetHelmImage(app, img) require.NoError(t, err) - require.NotNil(t, app.Spec.Source.Helm) - assert.Len(t, app.Spec.Source.Helm.Parameters, 2) + require.NotNil(t, app.Spec.GetSources()[0].Helm) + assert.Len(t, app.Spec.GetSources()[0].Helm.Parameters, 2) // Find correct parameter var tagParam v1alpha1.HelmParameter - for _, p := range app.Spec.Source.Helm.Parameters { + for _, p := range app.Spec.GetSources()[0].Helm.Parameters { + if p.Name == "image.tag" { + tagParam = p + break + } + } + assert.Equal(t, "1.0.1", tagParam.Value) + }) + + t.Run("Test set Helm image parameters on Helm app with multiple sources but existing parameters", func(t *testing.T) { + app := &v1alpha1.Application{ + ObjectMeta: v1.ObjectMeta{ + Name: "test-app", + Namespace: "testns", + Annotations: map[string]string{ + fmt.Sprintf(common.HelmParamImageNameAnnotation, "foobar"): "image.name", + fmt.Sprintf(common.HelmParamImageTagAnnotation, "foobar"): "image.tag", + fmt.Sprintf(common.HelmParamImageNameAnnotation, "baz"): "image.name", + fmt.Sprintf(common.HelmParamImageTagAnnotation, "baz"): "image.tag", + }, + }, + Spec: v1alpha1.ApplicationSpec{ + Sources: v1alpha1.ApplicationSources{ + v1alpha1.ApplicationSource{ + Helm: &v1alpha1.ApplicationSourceHelm{}, + }, + v1alpha1.ApplicationSource{ + Helm: &v1alpha1.ApplicationSourceHelm{}, + }, + }, + }, + Status: v1alpha1.ApplicationStatus{ + SourceTypes: []v1alpha1.ApplicationSourceType{ + v1alpha1.ApplicationSourceTypeHelm, + v1alpha1.ApplicationSourceTypeHelm, + }, + SourceType: v1alpha1.ApplicationSourceTypeHelm, + Summary: v1alpha1.ApplicationSummary{ + Images: []string{ + "jannfis/foobar:1.0.0", + "cjm/baz:2.0.0", + }, + }, + }, + } + + img := image.NewFromIdentifier("foobar=jannfis/foobar:1.0.1") + + err := SetHelmImage(app, img) + require.NoError(t, err) + require.NotNil(t, app.Spec.GetSources()[0].Helm) + assert.Len(t, app.Spec.GetSources()[0].Helm.Parameters, 2) + + // Find correct parameter + var tagParam v1alpha1.HelmParameter + for _, p := range app.Spec.GetSources()[0].Helm.Parameters { if p.Name == "image.tag" { tagParam = p break @@ -703,12 +758,12 @@ func Test_SetHelmImage(t *testing.T) { err := SetHelmImage(app, img) require.NoError(t, err) - require.NotNil(t, app.Spec.Source.Helm) - assert.Len(t, app.Spec.Source.Helm.Parameters, 4) + require.NotNil(t, app.Spec.GetSources()[0].Helm) + assert.Len(t, app.Spec.GetSources()[0].Helm.Parameters, 4) // Find correct parameter var tagParam v1alpha1.HelmParameter - for _, p := range app.Spec.Source.Helm.Parameters { + for _, p := range app.Spec.GetSources()[0].Helm.Parameters { if p.Name == "foobar.image.tag" { tagParam = p break @@ -818,7 +873,7 @@ func TestKubernetesClient_UpdateSpec_Conflict(t *testing.T) { require.NoError(t, err) - assert.Equal(t, "https://github.com/argoproj/argocd-example-apps", spec.Source.RepoURL) + assert.Equal(t, "https://github.com/argoproj/argocd-example-apps", spec.GetSources()[0].RepoURL) } func Test_parseImageList(t *testing.T) { From 93f8f94fd9ece502e037189f74ad6000a962e463 Mon Sep 17 00:00:00 2001 From: Casey Morton Date: Fri, 20 Oct 2023 12:37:36 -0400 Subject: [PATCH 2/6] Initial development to be compatible with multiple application sources Signed-off-by: Casey Morton --- pkg/argocd/argocd.go | 119 +++++++++++++++-------- pkg/argocd/argocd_test.go | 97 ++++-------------- pkg/argocd/git.go | 30 +++--- pkg/argocd/gitcreds.go | 20 ++-- pkg/argocd/update.go | 200 +++++++++++++++++++++++--------------- pkg/argocd/update_test.go | 68 +++++++------ pkg/common/constants.go | 2 +- 7 files changed, 282 insertions(+), 254 deletions(-) diff --git a/pkg/argocd/argocd.go b/pkg/argocd/argocd.go index 248686ad..0f5a11a7 100644 --- a/pkg/argocd/argocd.go +++ b/pkg/argocd/argocd.go @@ -184,12 +184,6 @@ func FilterApplicationsForUpdate(apps []v1alpha1.Application, patterns []string, continue } - // Check for valid application type - if !IsValidApplicationType(&app) { - logCtx.Warnf("skipping app '%s' of type '%s' because it's not of supported source type", app.GetName(), app.Status.SourceType) - continue - } - // Check if application name matches requested patterns if !nameMatchesPattern(app.GetName(), patterns) { logCtx.Debugf("Skipping app '%s' because it does not match requested patterns", app.GetName()) @@ -201,13 +195,20 @@ func FilterApplicationsForUpdate(apps []v1alpha1.Application, patterns []string, logCtx.Debugf("Skipping app '%s' because it does not carry requested label", app.GetName()) continue } + for sourceIndex, _ := range getApplicationTypes(&app) { + // Check for valid application type + if !IsValidApplicationTypeForSource(&app, sourceIndex) { + logCtx.Infof("skipping app '%s' of type '%s' because it's not of supported source type", app.GetName(), GetSourceTypes(app.Status)[sourceIndex]) + continue + } - logCtx.Tracef("processing app '%s' of type '%v'", app.GetName(), app.Status.SourceType) - imageList := parseImageList(annotations) - appImages := ApplicationImages{} - appImages.Application = app - appImages.Images = *imageList - appsForUpdate[app.GetName()] = appImages + logCtx.Tracef("processing app '%s' of type '%v'", app.GetName(), app.Status.SourceType) + imageList := parseImageList(annotations) + appImages := ApplicationImages{} + appImages.Application = app + appImages.Images = *imageList + appsForUpdate[app.GetName()] = appImages + } } return appsForUpdate, nil @@ -378,10 +379,10 @@ func mergeHelmParams(src []v1alpha1.HelmParameter, merge []v1alpha1.HelmParamete return retParams } -// SetHelmImage sets image parameters for a Helm application -func SetHelmImage(app *v1alpha1.Application, newImage *image.ContainerImage) error { - if appType := getApplicationType(app); appType != ApplicationTypeHelm { - return fmt.Errorf("cannot set Helm params on non-Helm application") +// SetHelmImageWithIndex sets image parameters for a Helm application and a specific sourceIndex +func SetHelmImageWithIndex(app *v1alpha1.Application, sourceIndex int, newImage *image.ContainerImage) bool { + if appType := getApplicationTypes(app)[sourceIndex]; appType != ApplicationTypeHelm { + return false } appName := app.GetName() @@ -425,23 +426,24 @@ func SetHelmImage(app *v1alpha1.Application, newImage *image.ContainerImage) err } } - if app.Spec.GetSources()[0].Helm == nil { - app.Spec.GetSources()[0].Helm = &v1alpha1.ApplicationSourceHelm{} + if app.Spec.GetSources()[sourceIndex].Helm == nil { + app.Spec.GetSources()[sourceIndex].Helm = &v1alpha1.ApplicationSourceHelm{} } - if app.Spec.GetSources()[0].Helm.Parameters == nil { - app.Spec.GetSources()[0].Helm.Parameters = make([]v1alpha1.HelmParameter, 0) + if app.Spec.GetSources()[sourceIndex].Helm.Parameters == nil { + app.Spec.GetSources()[sourceIndex].Helm.Parameters = make([]v1alpha1.HelmParameter, 0) } - app.Spec.GetSources()[0].Helm.Parameters = mergeHelmParams(app.Spec.GetSources()[0].Helm.Parameters, mergeParams) + app.Spec.GetSources()[sourceIndex].Helm.Parameters = mergeHelmParams(app.Spec.GetSources()[sourceIndex].Helm.Parameters, mergeParams) - return nil + return true } -// SetKustomizeImage sets a Kustomize image for given application -func SetKustomizeImage(app *v1alpha1.Application, newImage *image.ContainerImage) error { - if appType := getApplicationType(app); appType != ApplicationTypeKustomize { - return fmt.Errorf("cannot set Kustomize image on non-Kustomize application") +// SetKustomizeImageWithIndex sets a Kustomize image for given application +// returns whether that specific source was updated +func SetKustomizeImageWithIndex(app *v1alpha1.Application, sourceIndex int, newImage *image.ContainerImage) bool { + if appType := getApplicationTypes(app)[sourceIndex]; appType != ApplicationTypeKustomize { + return false } var ksImageParam string @@ -454,7 +456,7 @@ func SetKustomizeImage(app *v1alpha1.Application, newImage *image.ContainerImage log.WithContext().AddField("application", app.GetName()).Tracef("Setting Kustomize parameter %s", ksImageParam) - var source = app.Spec.GetSources()[0] + var source = app.Spec.GetSources()[sourceIndex] if source.Kustomize == nil { source.Kustomize = &v1alpha1.ApplicationSourceKustomize{} } @@ -473,12 +475,12 @@ func SetKustomizeImage(app *v1alpha1.Application, newImage *image.ContainerImage source.Kustomize.MergeImage(v1alpha1.KustomizeImage(ksImageParam)) if app.Spec.HasMultipleSources() { - app.Spec.Sources[0] = source + app.Spec.Sources[sourceIndex] = source } else { app.Spec.Source = &source } - return nil + return true } // GetImagesFromApplication returns the list of known images for the given application @@ -503,33 +505,50 @@ func GetImagesFromApplication(app *v1alpha1.Application) image.ContainerImageLis return images } -// GetApplicationTypeByName first retrieves application with given appName and -// returns its application type -func GetApplicationTypeByName(client ArgoCD, appName string) (ApplicationType, error) { +// GetApplicationTypesByName first retrieves application with given appName and +// returns its application types +func GetApplicationTypesByName(client ArgoCD, appName string) ([]ApplicationType, error) { app, err := client.GetApplication(context.TODO(), appName) if err != nil { - return ApplicationTypeUnsupported, err + retval := []ApplicationType{ApplicationTypeUnsupported} + return retval, err } - return getApplicationType(app), nil + return getApplicationTypes(app), nil } -// GetApplicationType returns the type of the ArgoCD application -func GetApplicationType(app *v1alpha1.Application) ApplicationType { - return getApplicationType(app) +// GetApplicationTypeForSource returns the type of the ArgoCD application source +func GetApplicationTypeForSource(app *v1alpha1.Application, sourceIndex int) ApplicationType { + return getApplicationTypes(app)[sourceIndex] } -// IsValidApplicationType returns true if we can update the application -func IsValidApplicationType(app *v1alpha1.Application) bool { - return getApplicationType(app) != ApplicationTypeUnsupported +// IsValidApplicationTypeForSource returns true if we can update the application source +func IsValidApplicationTypeForSource(app *v1alpha1.Application, sourceIndex int) bool { + return getApplicationTypes(app)[sourceIndex] != ApplicationTypeUnsupported } // getApplicationType returns the type of the application -func getApplicationType(app *v1alpha1.Application) ApplicationType { - sourceType := app.Status.SourceType +// writebacktargetannotation with the kustomization prefix forces all sources to be handled as kustomization +func getApplicationTypes(app *v1alpha1.Application) []ApplicationType { + kustomizationWriteBack := false + retval := make([]ApplicationType, 0) + if st, set := app.Annotations[common.WriteBackTargetAnnotation]; set && strings.HasPrefix(st, common.KustomizationPrefix) { - sourceType = v1alpha1.ApplicationSourceTypeKustomize + kustomizationWriteBack = true } + + for _, sourceType := range GetSourceTypes(app.Status) { + if kustomizationWriteBack { + retval = append(retval, ApplicationTypeKustomize) + } else { + retval = append(retval, getApplicationTypeForSourceType(sourceType)) + } + } + + return retval +} + +func getApplicationTypeForSourceType(sourceType v1alpha1.ApplicationSourceType) ApplicationType { if sourceType == v1alpha1.ApplicationSourceTypeKustomize { return ApplicationTypeKustomize } else if sourceType == v1alpha1.ApplicationSourceTypeHelm { @@ -552,3 +571,17 @@ func (a ApplicationType) String() string { return "Unknown" } } + +func HasMultipleSourceTypes(status v1alpha1.ApplicationStatus) bool { + return status.SourceTypes != nil && len(status.SourceTypes) > 0 +} + +func GetSourceTypes(status v1alpha1.ApplicationStatus) []v1alpha1.ApplicationSourceType { + if HasMultipleSourceTypes(status) { + return status.SourceTypes + } + if &status.SourceType != nil { + return []v1alpha1.ApplicationSourceType{status.SourceType} + } + return []v1alpha1.ApplicationSourceType{} +} diff --git a/pkg/argocd/argocd_test.go b/pkg/argocd/argocd_test.go index d47662d5..050a2bf4 100644 --- a/pkg/argocd/argocd_test.go +++ b/pkg/argocd/argocd_test.go @@ -79,7 +79,7 @@ func Test_GetImagesFromApplication(t *testing.T) { }) } -func Test_GetApplicationType(t *testing.T) { +func Test_GetApplicationTypeForSource(t *testing.T) { t.Run("Get application of type Helm", func(t *testing.T) { application := &v1alpha1.Application{ ObjectMeta: v1.ObjectMeta{ @@ -94,7 +94,7 @@ func Test_GetApplicationType(t *testing.T) { }, }, } - appType := GetApplicationType(application) + appType := GetApplicationTypeForSource(application, 0) assert.Equal(t, ApplicationTypeHelm, appType) assert.Equal(t, "Helm", appType.String()) }) @@ -113,7 +113,7 @@ func Test_GetApplicationType(t *testing.T) { }, }, } - appType := GetApplicationType(application) + appType := GetApplicationTypeForSource(application, 0) assert.Equal(t, ApplicationTypeKustomize, appType) assert.Equal(t, "Kustomize", appType.String()) }) @@ -132,7 +132,7 @@ func Test_GetApplicationType(t *testing.T) { }, }, } - appType := GetApplicationType(application) + appType := GetApplicationTypeForSource(application, 0) assert.Equal(t, ApplicationTypeUnsupported, appType) assert.Equal(t, "Unsupported", appType.String()) }) @@ -154,7 +154,7 @@ func Test_GetApplicationType(t *testing.T) { }, }, } - appType := GetApplicationType(application) + appType := GetApplicationTypeForSource(application, 0) assert.Equal(t, ApplicationTypeKustomize, appType) }) @@ -466,8 +466,8 @@ func Test_SetKustomizeImage(t *testing.T) { }, } img := image.NewFromIdentifier("jannfis/foobar:1.0.1") - err := SetKustomizeImage(app, img) - require.NoError(t, err) + updated := SetKustomizeImageWithIndex(app, 0, img) + assert.True(t, updated) require.NotNil(t, app.Spec.GetSources()[0].Kustomize) assert.Len(t, app.Spec.GetSources()[0].Kustomize.Images, 1) assert.Equal(t, v1alpha1.KustomizeImage("jannfis/foobar:1.0.1"), app.Spec.GetSources()[0].Kustomize.Images[0]) @@ -492,8 +492,8 @@ func Test_SetKustomizeImage(t *testing.T) { }, } img := image.NewFromIdentifier("jannfis/foobar:1.0.1") - err := SetKustomizeImage(app, img) - require.NoError(t, err) + updated := SetKustomizeImageWithIndex(app, 0, img) + assert.True(t, updated) require.NotNil(t, app.Spec.GetSources()[0].Kustomize) assert.Len(t, app.Spec.GetSources()[0].Kustomize.Images, 1) assert.Equal(t, v1alpha1.KustomizeImage("jannfis/foobar:1.0.1"), app.Spec.GetSources()[0].Kustomize.Images[0]) @@ -524,8 +524,8 @@ func Test_SetKustomizeImage(t *testing.T) { }, } img := image.NewFromIdentifier("jannfis/foobar:1.0.1") - err := SetKustomizeImage(app, img) - require.Error(t, err) + updated := SetKustomizeImageWithIndex(app, 0, img) + assert.False(t, updated) }) t.Run("Test set Kustomize image parameters with alias name on Kustomize app with param already set", func(t *testing.T) { @@ -556,8 +556,8 @@ func Test_SetKustomizeImage(t *testing.T) { }, } img := image.NewFromIdentifier("foobar=jannfis/foobar:1.0.1") - err := SetKustomizeImage(app, img) - require.NoError(t, err) + updated := SetKustomizeImageWithIndex(app, 0, img) + assert.True(t, updated) require.NotNil(t, app.Spec.GetSources()[0].Kustomize) assert.Len(t, app.Spec.GetSources()[0].Kustomize.Images, 1) assert.Equal(t, v1alpha1.KustomizeImage("foobar=jannfis/foobar:1.0.1"), app.Spec.GetSources()[0].Kustomize.Images[0]) @@ -604,8 +604,8 @@ func Test_SetHelmImage(t *testing.T) { img := image.NewFromIdentifier("foobar=jannfis/foobar:1.0.1") - err := SetHelmImage(app, img) - require.NoError(t, err) + updated := SetHelmImageWithIndex(app, 0, img) + assert.True(t, updated) require.NotNil(t, app.Spec.GetSources()[0].Helm) assert.Len(t, app.Spec.GetSources()[0].Helm.Parameters, 2) @@ -647,63 +647,8 @@ func Test_SetHelmImage(t *testing.T) { img := image.NewFromIdentifier("foobar=jannfis/foobar:1.0.1") - err := SetHelmImage(app, img) - require.NoError(t, err) - require.NotNil(t, app.Spec.GetSources()[0].Helm) - assert.Len(t, app.Spec.GetSources()[0].Helm.Parameters, 2) - - // Find correct parameter - var tagParam v1alpha1.HelmParameter - for _, p := range app.Spec.GetSources()[0].Helm.Parameters { - if p.Name == "image.tag" { - tagParam = p - break - } - } - assert.Equal(t, "1.0.1", tagParam.Value) - }) - - t.Run("Test set Helm image parameters on Helm app with multiple sources but existing parameters", func(t *testing.T) { - app := &v1alpha1.Application{ - ObjectMeta: v1.ObjectMeta{ - Name: "test-app", - Namespace: "testns", - Annotations: map[string]string{ - fmt.Sprintf(common.HelmParamImageNameAnnotation, "foobar"): "image.name", - fmt.Sprintf(common.HelmParamImageTagAnnotation, "foobar"): "image.tag", - fmt.Sprintf(common.HelmParamImageNameAnnotation, "baz"): "image.name", - fmt.Sprintf(common.HelmParamImageTagAnnotation, "baz"): "image.tag", - }, - }, - Spec: v1alpha1.ApplicationSpec{ - Sources: v1alpha1.ApplicationSources{ - v1alpha1.ApplicationSource{ - Helm: &v1alpha1.ApplicationSourceHelm{}, - }, - v1alpha1.ApplicationSource{ - Helm: &v1alpha1.ApplicationSourceHelm{}, - }, - }, - }, - Status: v1alpha1.ApplicationStatus{ - SourceTypes: []v1alpha1.ApplicationSourceType{ - v1alpha1.ApplicationSourceTypeHelm, - v1alpha1.ApplicationSourceTypeHelm, - }, - SourceType: v1alpha1.ApplicationSourceTypeHelm, - Summary: v1alpha1.ApplicationSummary{ - Images: []string{ - "jannfis/foobar:1.0.0", - "cjm/baz:2.0.0", - }, - }, - }, - } - - img := image.NewFromIdentifier("foobar=jannfis/foobar:1.0.1") - - err := SetHelmImage(app, img) - require.NoError(t, err) + updated := SetHelmImageWithIndex(app, 0, img) + assert.True(t, updated) require.NotNil(t, app.Spec.GetSources()[0].Helm) assert.Len(t, app.Spec.GetSources()[0].Helm.Parameters, 2) @@ -756,8 +701,8 @@ func Test_SetHelmImage(t *testing.T) { img := image.NewFromIdentifier("foobar=jannfis/foobar:1.0.1") - err := SetHelmImage(app, img) - require.NoError(t, err) + updated := SetHelmImageWithIndex(app, 0, img) + assert.True(t, updated) require.NotNil(t, app.Spec.GetSources()[0].Helm) assert.Len(t, app.Spec.GetSources()[0].Helm.Parameters, 4) @@ -797,8 +742,8 @@ func Test_SetHelmImage(t *testing.T) { img := image.NewFromIdentifier("foobar=jannfis/foobar:1.0.1") - err := SetHelmImage(app, img) - require.Error(t, err) + updated := SetHelmImageWithIndex(app, 0, img) + assert.False(t, updated) }) } diff --git a/pkg/argocd/git.go b/pkg/argocd/git.go index 4f1de571..e7ac20ec 100644 --- a/pkg/argocd/git.go +++ b/pkg/argocd/git.go @@ -123,15 +123,15 @@ func TemplateBranchName(branchName string, changeList []ChangeEntry) string { } } -type changeWriter func(app *v1alpha1.Application, wbc *WriteBackConfig, gitC git.Client) (err error, skip bool) +type changeWriter func(app *v1alpha1.Application, wbc *WriteBackConfig, sourceIndex int, gitC git.Client) (err error, skip bool) // commitChanges commits any changes required for updating one or more images // after the UpdateApplication cycle has finished. -func commitChangesGit(app *v1alpha1.Application, wbc *WriteBackConfig, changeList []ChangeEntry, write changeWriter) error { +func commitChangesGit(app *v1alpha1.Application, wbc *WriteBackConfig, sourceIndex int, changeList []ChangeEntry, write changeWriter) error { logCtx := log.WithContext().AddField("application", app.GetName()) - creds, err := wbc.GetCreds(app) + creds, err := wbc.GetCreds(app, sourceIndex) if err != nil { - return fmt.Errorf("could not get creds for repo '%s': %v", wbc.GitRepo, err) + return fmt.Errorf("could not get creds for repo '%s': %v", wbc.GitRepos[sourceIndex], err) } var gitC git.Client if wbc.GitClient == nil { @@ -145,7 +145,7 @@ func commitChangesGit(app *v1alpha1.Application, wbc *WriteBackConfig, changeLis logCtx.Errorf("could not remove temp dir: %v", err) } }() - gitC, err = git.NewClientExt(wbc.GitRepo, tempRoot, creds, false, false, "") + gitC, err = git.NewClientExt(wbc.GitRepos[sourceIndex], tempRoot, creds, false, false, "") if err != nil { return err } @@ -173,7 +173,7 @@ func commitChangesGit(app *v1alpha1.Application, wbc *WriteBackConfig, changeLis // config, or taken from the application spec's targetRevision. If the // target revision is set to the special value HEAD, or is the empty // string, we'll try to resolve it to a branch name. - checkOutBranch := app.Spec.Source.TargetRevision + checkOutBranch := app.Spec.GetSources()[sourceIndex].TargetRevision if wbc.GitBranch != "" { checkOutBranch = wbc.GitBranch } @@ -211,7 +211,7 @@ func commitChangesGit(app *v1alpha1.Application, wbc *WriteBackConfig, changeLis return err } - if err, skip := write(app, wbc, gitC); err != nil { + if err, skip := write(app, wbc, sourceIndex, gitC); err != nil { return err } else if skip { return nil @@ -246,10 +246,11 @@ func commitChangesGit(app *v1alpha1.Application, wbc *WriteBackConfig, changeLis return nil } -func writeOverrides(app *v1alpha1.Application, wbc *WriteBackConfig, gitC git.Client) (err error, skip bool) { +func writeOverrides(app *v1alpha1.Application, wbc *WriteBackConfig, sourceIndex int, gitC git.Client) (err error, skip bool) { logCtx := log.WithContext().AddField("application", app.GetName()) + logCtx = log.WithContext().AddField("source", sourceIndex) targetExists := true - targetFile := path.Join(gitC.Root(), wbc.Target) + targetFile := path.Join(gitC.Root(), wbc.Targets[sourceIndex]) _, err = os.Stat(targetFile) if err != nil { if !os.IsNotExist(err) { @@ -269,7 +270,7 @@ func writeOverrides(app *v1alpha1.Application, wbc *WriteBackConfig, gitC git.Cl if err != nil { return err, false } - override, err = marshalParamsOverride(app, originalData) + override, err = marshalParamsOverride(app, sourceIndex, originalData) if err != nil { return } @@ -278,7 +279,7 @@ func writeOverrides(app *v1alpha1.Application, wbc *WriteBackConfig, gitC git.Cl return nil, true } } else { - override, err = marshalParamsOverride(app, nil) + override, err = marshalParamsOverride(app, sourceIndex, nil) if err != nil { return } @@ -292,16 +293,17 @@ func writeOverrides(app *v1alpha1.Application, wbc *WriteBackConfig, gitC git.Cl if !targetExists { err = gitC.Add(targetFile) } + return } var _ changeWriter = writeOverrides // 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) { +func writeKustomization(app *v1alpha1.Application, wbc *WriteBackConfig, sourceIndex int, gitC git.Client) (err error, skip bool) { logCtx := log.WithContext().AddField("application", app.GetName()) - base := filepath.Join(gitC.Root(), wbc.KustomizeBase) + base := filepath.Join(gitC.Root(), wbc.KustomizeBases[sourceIndex]) logCtx.Infof("updating base %s", base) @@ -310,7 +312,7 @@ func writeKustomization(app *v1alpha1.Application, wbc *WriteBackConfig, gitC gi return fmt.Errorf("could not find kustomization in %s", base), false } - filterFunc, err := imagesFilter(app.Spec.Source.Kustomize.Images) + filterFunc, err := imagesFilter(app.Spec.GetSources()[sourceIndex].Kustomize.Images) if err != nil { return err, false } diff --git a/pkg/argocd/gitcreds.go b/pkg/argocd/gitcreds.go index d7996026..26438e62 100644 --- a/pkg/argocd/gitcreds.go +++ b/pkg/argocd/gitcreds.go @@ -17,36 +17,36 @@ import ( func getGitCredsSource(creds string, kubeClient *kube.KubernetesClient, wbc *WriteBackConfig) (GitCredsSource, error) { switch { case creds == "repocreds": - return func(app *v1alpha1.Application) (git.Creds, error) { - return getCredsFromArgoCD(wbc, kubeClient) + return func(app *v1alpha1.Application, sourceIndex int) (git.Creds, error) { + return getCredsFromArgoCD(wbc, sourceIndex, kubeClient) }, nil case strings.HasPrefix(creds, "secret:"): - return func(app *v1alpha1.Application) (git.Creds, error) { - return getCredsFromSecret(wbc, creds[len("secret:"):], kubeClient) + return func(app *v1alpha1.Application, sourceIndex int) (git.Creds, error) { + return getCredsFromSecret(wbc, creds[len("secret:"):], sourceIndex, kubeClient) }, nil } return nil, fmt.Errorf("unexpected credentials format. Expected 'repocreds' or 'secret:/' but got '%s'", creds) } // getCredsFromArgoCD loads repository credentials from Argo CD settings -func getCredsFromArgoCD(wbc *WriteBackConfig, kubeClient *kube.KubernetesClient) (git.Creds, error) { +func getCredsFromArgoCD(wbc *WriteBackConfig, sourceIndex int, kubeClient *kube.KubernetesClient) (git.Creds, error) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() settingsMgr := settings.NewSettingsManager(ctx, kubeClient.Clientset, kubeClient.Namespace) argocdDB := db.NewDB(kubeClient.Namespace, settingsMgr, kubeClient.Clientset) - repo, err := argocdDB.GetRepository(ctx, wbc.GitRepo) + repo, err := argocdDB.GetRepository(ctx, wbc.GitRepos[sourceIndex]) if err != nil { return nil, err } if !repo.HasCredentials() { - return nil, fmt.Errorf("credentials for '%s' are not configured in Argo CD settings", wbc.GitRepo) + return nil, fmt.Errorf("credentials for '%s' are not configured in Argo CD settings", wbc.GitRepos[sourceIndex]) } return repo.GetGitCreds(nil), nil } // getCredsFromSecret loads repository credentials from secret -func getCredsFromSecret(wbc *WriteBackConfig, credentialsSecret string, kubeClient *kube.KubernetesClient) (git.Creds, error) { +func getCredsFromSecret(wbc *WriteBackConfig, credentialsSecret string, sourceIndex int, kubeClient *kube.KubernetesClient) (git.Creds, error) { var credentials map[string][]byte var err error s := strings.SplitN(credentialsSecret, "/", 2) @@ -59,13 +59,13 @@ func getCredsFromSecret(wbc *WriteBackConfig, credentialsSecret string, kubeClie return nil, fmt.Errorf("secret ref must be in format 'namespace/name', but is '%s'", credentialsSecret) } - if ok, _ := git.IsSSHURL(wbc.GitRepo); ok { + if ok, _ := git.IsSSHURL(wbc.GitRepos[sourceIndex]); ok { var sshPrivateKey []byte if sshPrivateKey, ok = credentials["sshPrivateKey"]; !ok { return nil, fmt.Errorf("invalid secret %s: does not contain field sshPrivateKey", credentialsSecret) } return git.NewSSHCreds(string(sshPrivateKey), "", true), nil - } else if git.IsHTTPSURL(wbc.GitRepo) { + } else if git.IsHTTPSURL(wbc.GitRepos[sourceIndex]) { var username, password []byte if username, ok = credentials["username"]; !ok { return nil, fmt.Errorf("invalid secret %s: does not contain field username", credentialsSecret) diff --git a/pkg/argocd/update.go b/pkg/argocd/update.go index 2910d99d..cedc39af 100644 --- a/pkg/argocd/update.go +++ b/pkg/argocd/update.go @@ -47,7 +47,7 @@ type UpdateConfiguration struct { IgnorePlatforms bool } -type GitCredsSource func(app *v1alpha1.Application) (git.Creds, error) +type GitCredsSource func(app *v1alpha1.Application, sourceIndex int) (git.Creds, error) type WriteBackMethod int @@ -68,9 +68,9 @@ type WriteBackConfig struct { GitCommitUser string GitCommitEmail string GitCommitMessage string - KustomizeBase string - Target string - GitRepo string + KustomizeBases []string + Targets []string + GitRepos []string } // The following are helper structs to only marshal the fields we require @@ -139,7 +139,7 @@ func UpdateApplication(updateConf *UpdateConfiguration, state *SyncIterationStat var needUpdate bool = false result := ImageUpdaterResult{} - app := updateConf.UpdateApp.Application.GetName() + appName := updateConf.UpdateApp.Application.GetName() changeList := make([]ChangeEntry, 0) // Get all images that are deployed with the current application @@ -156,7 +156,7 @@ func UpdateApplication(updateConf *UpdateConfiguration, state *SyncIterationStat for _, applicationImage := range updateConf.UpdateApp.Images { updateableImage := applicationImages.ContainsImage(applicationImage, false) if updateableImage == nil { - log.WithContext().AddField("application", app).Debugf("Image '%s' seems not to be live in this application, skipping", applicationImage.ImageName) + log.WithContext().AddField("application", appName).Debugf("Image '%s' seems not to be live in this application, skipping", applicationImage.ImageName) result.NumSkipped += 1 continue } @@ -171,7 +171,7 @@ func UpdateApplication(updateConf *UpdateConfiguration, state *SyncIterationStat result.NumImagesConsidered += 1 imgCtx := log.WithContext(). - AddField("application", app). + AddField("application", appName). AddField("registry", updateableImage.RegistryURL). AddField("image_name", updateableImage.ImageName). AddField("image_tag", updateableImage.ImageTag). @@ -204,7 +204,7 @@ func UpdateApplication(updateConf *UpdateConfiguration, state *SyncIterationStat vc.Options = applicationImage. GetPlatformOptions(updateConf.UpdateApp.Application.Annotations, updateConf.IgnorePlatforms). WithMetadata(vc.Strategy.NeedsMetadata()). - WithLogger(imgCtx.AddField("application", app)) + WithLogger(imgCtx.AddField("application", appName)) // If a strategy needs meta-data and tagsortmode is set for the // registry, let the user know. @@ -325,29 +325,42 @@ func UpdateApplication(updateConf *UpdateConfiguration, state *SyncIterationStat } if needUpdate { - logCtx := log.WithContext().AddField("application", app) + logCtx := log.WithContext().AddField("application", appName) log.Debugf("Using commit message: %s", wbc.GitCommitMessage) if !updateConf.DryRun { - logCtx.Infof("Committing %d parameter update(s) for application %s", result.NumImagesUpdated, app) - err := commitChangesLocked(&updateConf.UpdateApp.Application, wbc, state, changeList) - if err != nil { - logCtx.Errorf("Could not update application spec: %v", err) - result.NumErrors += 1 - result.NumImagesUpdated = 0 + logCtx.Infof("Committing %d parameter update(s) for application %s", result.NumImagesUpdated, appName) + app := &updateConf.UpdateApp.Application + var iterations int + + // if writing back to git, each source might have its own target or kustomize base, so we have to iterate + // once per source. Otherwise, we only have to writeback once. + if wbc.Method == WriteBackGit { + iterations = len(app.Spec.GetSources()) } else { - logCtx.Infof("Successfully updated the live application spec") - if !updateConf.DisableKubeEvents && updateConf.KubeClient != nil { - annotations := map[string]string{} - for i, c := range changeList { - annotations[fmt.Sprintf("argocd-image-updater.image-%d/full-image-name", i)] = c.Image.GetFullNameWithoutTag() - annotations[fmt.Sprintf("argocd-image-updater.image-%d/image-name", i)] = c.Image.ImageName - annotations[fmt.Sprintf("argocd-image-updater.image-%d/old-tag", i)] = c.OldTag.String() - annotations[fmt.Sprintf("argocd-image-updater.image-%d/new-tag", i)] = c.NewTag.String() - } - message := fmt.Sprintf("Successfully updated application '%s'", app) - _, err = updateConf.KubeClient.CreateApplicationEvent(&updateConf.UpdateApp.Application, "ImagesUpdated", message, annotations) - if err != nil { - logCtx.Warnf("Event could not be sent: %v", err) + iterations = 1 + } + + for sourceIndex := 0; sourceIndex < iterations; sourceIndex++ { + err := commitChangesLocked(app, wbc, sourceIndex, state, changeList) + if err != nil { + logCtx.Errorf("Could not update application spec: %v", err) + result.NumErrors += 1 + result.NumImagesUpdated = 0 + } else { + logCtx.Infof("Successfully updated the live application spec") + if !updateConf.DisableKubeEvents && updateConf.KubeClient != nil { + annotations := map[string]string{} + for i, c := range changeList { + annotations[fmt.Sprintf("argocd-image-updater.image-%d/full-image-name", i)] = c.Image.GetFullNameWithoutTag() + annotations[fmt.Sprintf("argocd-image-updater.image-%d/image-name", i)] = c.Image.ImageName + annotations[fmt.Sprintf("argocd-image-updater.image-%d/old-tag", i)] = c.OldTag.String() + annotations[fmt.Sprintf("argocd-image-updater.image-%d/new-tag", i)] = c.NewTag.String() + } + message := fmt.Sprintf("Successfully updated application '%s'", app) + _, err = updateConf.KubeClient.CreateApplicationEvent(app, "ImagesUpdated", message, annotations) + if err != nil { + logCtx.Warnf("Event could not be sent: %v", err) + } } } } @@ -365,34 +378,44 @@ func needsUpdate(updateableImage *image.ContainerImage, applicationImage *image. } func setAppImage(app *v1alpha1.Application, img *image.ContainerImage) error { - var err error - if appType := GetApplicationType(app); appType == ApplicationTypeKustomize { - err = SetKustomizeImage(app, img) - } else if appType == ApplicationTypeHelm { - err = SetHelmImage(app, img) - } else { - err = fmt.Errorf("could not update application %s - neither Helm nor Kustomize application", app) + imageUpdated := false + imageUpdatable := false + for i, appType := range getApplicationTypes(app) { + if appType == ApplicationTypeKustomize { + imageUpdatable = true + imageUpdated = SetKustomizeImageWithIndex(app, i, img) || imageUpdated + } else if appType == ApplicationTypeHelm { + imageUpdatable = true + imageUpdated = SetHelmImageWithIndex(app, i, img) || imageUpdated + } } - return err + + if !imageUpdatable { + return fmt.Errorf("could not update application %s - no Helm or Kustomize sources found", app) + } else if imageUpdatable && !imageUpdated { + return fmt.Errorf("could not update application %s - image does not appear to be used in any eligible sources", app) + } + + return nil } // marshalParamsOverride marshals the parameter overrides of a given application // into YAML bytes -func marshalParamsOverride(app *v1alpha1.Application, originalData []byte) ([]byte, error) { +func marshalParamsOverride(app *v1alpha1.Application, sourceIndex int, originalData []byte) ([]byte, error) { var override []byte var err error - appType := GetApplicationType(app) + appType := getApplicationTypes(app)[sourceIndex] switch appType { case ApplicationTypeKustomize: - if app.Spec.Source.Kustomize == nil { + if app.Spec.GetSources()[sourceIndex].Kustomize == nil { return []byte{}, nil } var params kustomizeOverride newParams := kustomizeOverride{ Kustomize: kustomizeImages{ - Images: &app.Spec.Source.Kustomize.Images, + Images: &app.Spec.GetSources()[sourceIndex].Kustomize.Images, }, } @@ -408,13 +431,13 @@ func marshalParamsOverride(app *v1alpha1.Application, originalData []byte) ([]by mergeKustomizeOverride(¶ms, &newParams) override, err = yaml.Marshal(params) case ApplicationTypeHelm: - if app.Spec.Source.Helm == nil { + if app.Spec.GetSources()[sourceIndex].Helm == nil { return []byte{}, nil } var params helmOverride newParams := helmOverride{ Helm: helmParameters{ - Parameters: app.Spec.Source.Helm.Parameters, + Parameters: app.Spec.GetSources()[sourceIndex].Helm.Parameters, }, } @@ -429,8 +452,8 @@ func marshalParamsOverride(app *v1alpha1.Application, originalData []byte) ([]by } mergeHelmOverride(¶ms, &newParams) override, err = yaml.Marshal(params) - default: - err = fmt.Errorf("unsupported application type") + //default: + // err = fmt.Errorf("unsupported application type") } if err != nil { return nil, err @@ -466,7 +489,7 @@ func getWriteBackConfig(app *v1alpha1.Application, kubeClient *kube.KubernetesCl // Default write-back is to use Argo CD API wbc.Method = WriteBackApplication wbc.ArgoClient = argoClient - wbc.Target = parseDefaultTarget(app.Name, app.Spec.Source.Path) + wbc.Targets = parseDefaultTargets(app.Name, app.Spec.GetSources()) // If we have no update method, just return our default method, ok := app.Annotations[common.WriteBackMethodAnnotation] @@ -486,10 +509,15 @@ func getWriteBackConfig(app *v1alpha1.Application, kubeClient *kube.KubernetesCl case "git": wbc.Method = WriteBackGit if target, ok := app.Annotations[common.WriteBackTargetAnnotation]; ok && strings.HasPrefix(target, common.KustomizationPrefix) { - wbc.KustomizeBase = parseTarget(target, app.Spec.Source.Path) + wbc.KustomizeBases = parseTargets(target, app.Spec.GetSources()) + } else { + wbc.KustomizeBases = make([]string, len(app.Spec.GetSources())) + for i := 0; i < len(app.Spec.GetSources()); i++ { + wbc.KustomizeBases[i] = "" + } } - if err := parseGitConfig(app, kubeClient, wbc, creds); err != nil { - return nil, err + if credErrors := parseGitConfig(app, kubeClient, wbc, creds); credErrors != nil && len(credErrors) > 0 { + return nil, fmt.Errorf("Errors parsing git credentials: %s", method) } default: return nil, fmt.Errorf("invalid update mechanism: %s", method) @@ -498,60 +526,76 @@ func getWriteBackConfig(app *v1alpha1.Application, kubeClient *kube.KubernetesCl return wbc, nil } -func parseDefaultTarget(appName string, path string) string { - defaultTargetFile := fmt.Sprintf(common.DefaultTargetFilePattern, appName) - - return filepath.Join(path, defaultTargetFile) +func parseDefaultTargets(appName string, sources v1alpha1.ApplicationSources) []string { + retval := make([]string, len(sources)) + for i, source := range sources { + defaultTargetFile := fmt.Sprintf(common.DefaultTargetFilePattern, appName, i) + retval[i] = filepath.Join(source.Path, defaultTargetFile) + } + return retval } -func parseTarget(target string, sourcePath string) (kustomizeBase string) { - if target == common.KustomizationPrefix { - return filepath.Join(sourcePath, ".") - } else if base := target[len(common.KustomizationPrefix)+1:]; strings.HasPrefix(base, "/") { - return base[1:] - } else { - return filepath.Join(sourcePath, base) +func parseTargets(target string, sources v1alpha1.ApplicationSources) (kustomizeBases []string) { + retval := make([]string, len(sources)) + for i, source := range sources { + if target == common.KustomizationPrefix { + retval[i] = filepath.Join(source.Path, ".") + } else if base := target[len(common.KustomizationPrefix)+1:]; strings.HasPrefix(base, "/") { + retval[i] = base[1:] + } else { + retval[i] = filepath.Join(source.Path, base) + } } + + return retval } -func parseGitConfig(app *v1alpha1.Application, kubeClient *kube.KubernetesClient, wbc *WriteBackConfig, creds string) error { +func parseGitConfig(app *v1alpha1.Application, kubeClient *kube.KubernetesClient, wbc *WriteBackConfig, creds string) []error { + errors := make([]error, 0) branch, ok := app.Annotations[common.GitBranchAnnotation] if ok { branches := strings.Split(strings.TrimSpace(branch), ":") if len(branches) > 2 { - return fmt.Errorf("invalid format for git-branch annotation: %v", branch) + errors = append(errors, fmt.Errorf("invalid format for git-branch annotation: %v", branch)) } wbc.GitBranch = branches[0] if len(branches) == 2 { wbc.GitWriteBranch = branches[1] } } - wbc.GitRepo = app.Spec.Source.RepoURL - repo, ok := app.Annotations[common.GitRepositoryAnnotation] - if ok { - wbc.GitRepo = repo - } - credsSource, err := getGitCredsSource(creds, kubeClient, wbc) - if err != nil { - return fmt.Errorf("invalid git credentials source: %v", err) + sources := app.Spec.GetSources() + wbc.GitRepos = make([]string, len(sources)) + for i, source := range sources { + wbc.GitRepos[i] = source.RepoURL + repo, ok := app.Annotations[common.GitRepositoryAnnotation] + if ok { + wbc.GitRepos[i] = repo + } + credSource, err := getGitCredsSource(creds, kubeClient, wbc) + if err != nil { + errors = append(errors, fmt.Errorf("invalid git credentials at source %d: %v", i, err)) + wbc.GetCreds = nil + } else { + wbc.GetCreds = credSource + } + } - wbc.GetCreds = credsSource - return nil + return errors } -func commitChangesLocked(app *v1alpha1.Application, wbc *WriteBackConfig, state *SyncIterationState, changeList []ChangeEntry) error { +func commitChangesLocked(app *v1alpha1.Application, wbc *WriteBackConfig, sourceIndex int, state *SyncIterationState, changeList []ChangeEntry) error { if wbc.RequiresLocking() { - lock := state.GetRepositoryLock(wbc.GitRepo) + lock := state.GetRepositoryLock(wbc.GitRepos[sourceIndex]) lock.Lock() defer lock.Unlock() } - return commitChanges(app, wbc, changeList) + return commitChanges(app, wbc, sourceIndex, changeList) } // commitChanges commits any changes required for updating one or more images // after the UpdateApplication cycle has finished. -func commitChanges(app *v1alpha1.Application, wbc *WriteBackConfig, changeList []ChangeEntry) error { +func commitChanges(app *v1alpha1.Application, wbc *WriteBackConfig, sourceIndex int, changeList []ChangeEntry) error { switch wbc.Method { case WriteBackApplication: _, err := wbc.ArgoClient.UpdateSpec(context.TODO(), &application.ApplicationUpdateSpecRequest{ @@ -563,10 +607,10 @@ func commitChanges(app *v1alpha1.Application, wbc *WriteBackConfig, changeList [ } case WriteBackGit: // if the kustomize base is set, the target is a kustomization - if wbc.KustomizeBase != "" { - return commitChangesGit(app, wbc, changeList, writeKustomization) + if wbc.KustomizeBases[sourceIndex] != "" { + return commitChangesGit(app, wbc, sourceIndex, changeList, writeKustomization) } - return commitChangesGit(app, wbc, changeList, writeOverrides) + return commitChangesGit(app, wbc, sourceIndex, changeList, writeOverrides) default: return fmt.Errorf("unknown write back method set: %d", wbc.Method) } diff --git a/pkg/argocd/update_test.go b/pkg/argocd/update_test.go index 6517025b..0e1fb407 100644 --- a/pkg/argocd/update_test.go +++ b/pkg/argocd/update_test.go @@ -1099,7 +1099,7 @@ kustomize: images: - baz `) - yaml, err := marshalParamsOverride(&app, originalData) + yaml, err := marshalParamsOverride(&app, 0, originalData) require.NoError(t, err) assert.NotEmpty(t, yaml) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(yaml))) @@ -1125,7 +1125,7 @@ kustomize: }, } - yaml, err := marshalParamsOverride(&app, nil) + yaml, err := marshalParamsOverride(&app, 0, nil) require.NoError(t, err) assert.Empty(t, yaml) assert.Equal(t, "", strings.TrimSpace(string(yaml))) @@ -1185,7 +1185,7 @@ helm: value: baz forcestring: false `) - yaml, err := marshalParamsOverride(&app, originalData) + yaml, err := marshalParamsOverride(&app, 0, originalData) require.NoError(t, err) assert.NotEmpty(t, yaml) assert.Equal(t, strings.TrimSpace(strings.ReplaceAll(expected, "\t", " ")), strings.TrimSpace(string(yaml))) @@ -1211,7 +1211,7 @@ helm: }, } - yaml, err := marshalParamsOverride(&app, nil) + yaml, err := marshalParamsOverride(&app, 0, nil) require.NoError(t, err) assert.Empty(t, yaml) }) @@ -1242,8 +1242,10 @@ helm: }, } - _, err := marshalParamsOverride(&app, nil) - assert.Error(t, err) + _, err := marshalParamsOverride(&app, 0, nil) + // This condition should not result in an error because it could be possible to have multiple source types + // within the same applicaiton + assert.NoError(t, err) }) } @@ -1412,7 +1414,7 @@ func Test_GetWriteBackConfig(t *testing.T) { require.NoError(t, err) require.NotNil(t, wbc) assert.Equal(t, wbc.Method, WriteBackGit) - assert.Equal(t, wbc.KustomizeBase, "config/bar") + assert.Equal(t, wbc.KustomizeBases[0], "config/bar") }) t.Run("Default write-back config - argocd", func(t *testing.T) { @@ -1514,7 +1516,7 @@ func Test_GetGitCreds(t *testing.T) { wbc, err := getWriteBackConfig(&app, &kubeClient, &argoClient) require.NoError(t, err) - creds, err := wbc.GetCreds(&app) + creds, err := wbc.GetCreds(&app, 0) require.NoError(t, err) require.NotNil(t, creds) // Must have HTTPS creds @@ -1552,7 +1554,7 @@ func Test_GetGitCreds(t *testing.T) { wbc, err := getWriteBackConfig(&app, &kubeClient, &argoClient) require.NoError(t, err) - creds, err := wbc.GetCreds(&app) + creds, err := wbc.GetCreds(&app, 0) require.NoError(t, err) require.NotNil(t, creds) // Must have SSH creds @@ -1604,7 +1606,7 @@ func Test_GetGitCreds(t *testing.T) { wbc, err := getWriteBackConfig(&app, &kubeClient, &argoClient) require.NoError(t, err) - creds, err := wbc.GetCreds(&app) + creds, err := wbc.GetCreds(&app, 0) require.NoError(t, err) require.NotNil(t, creds) // Must have HTTPS creds @@ -1642,7 +1644,7 @@ func Test_GetGitCreds(t *testing.T) { wbc, err := getWriteBackConfig(&app, &kubeClient, &argoClient) require.NoError(t, err) - creds, err := wbc.GetCreds(&app) + creds, err := wbc.GetCreds(&app, 0) require.Error(t, err) require.Nil(t, creds) }) @@ -1678,7 +1680,7 @@ func Test_GetGitCreds(t *testing.T) { wbc, err := getWriteBackConfig(&app, &kubeClient, &argoClient) require.NoError(t, err) - creds, err := wbc.GetCreds(&app) + creds, err := wbc.GetCreds(&app, 0) require.Error(t, err) require.Nil(t, creds) }) @@ -1714,7 +1716,7 @@ func Test_GetGitCreds(t *testing.T) { wbc, err := getWriteBackConfig(&app, &kubeClient, &argoClient) require.NoError(t, err) - creds, err := wbc.GetCreds(&app) + creds, err := wbc.GetCreds(&app, 0) require.Error(t, err) require.Nil(t, creds) }) @@ -1751,9 +1753,9 @@ func Test_GetGitCreds(t *testing.T) { wbc, err := getWriteBackConfig(&app, &kubeClient, &argoClient) require.NoError(t, err) - require.Equal(t, wbc.GitRepo, "git@github.com:example/example.git") + require.Equal(t, wbc.GitRepos[0], "git@github.com:example/example.git") - creds, err := wbc.GetCreds(&app) + creds, err := wbc.GetCreds(&app, 0) require.NoError(t, err) require.NotNil(t, creds) // Must have SSH creds @@ -1804,7 +1806,7 @@ func Test_CommitUpdates(t *testing.T) { require.NoError(t, err) wbc.GitClient = gitMock - err = commitChanges(&app, wbc, nil) + err = commitChanges(&app, wbc, 0, nil) assert.NoError(t, err) }) @@ -1824,7 +1826,7 @@ func Test_CommitUpdates(t *testing.T) { wbc.GitClient = gitMock wbc.GitBranch = "mybranch" - err = commitChanges(&app, wbc, nil) + err = commitChanges(&app, wbc, 0, nil) assert.NoError(t, err) }) @@ -1845,7 +1847,7 @@ func Test_CommitUpdates(t *testing.T) { app.Spec.Source.TargetRevision = "HEAD" wbc.GitBranch = "" - err = commitChanges(app, wbc, nil) + err = commitChanges(app, wbc, 0, nil) assert.NoError(t, err) }) @@ -1873,7 +1875,7 @@ func Test_CommitUpdates(t *testing.T) { } gitMock.On("Checkout", TemplateBranchName(wbc.GitWriteBranch, cl)).Return(nil) - err = commitChanges(&app, wbc, cl) + err = commitChanges(&app, wbc, 0, cl) assert.NoError(t, err) }) @@ -1886,7 +1888,7 @@ func Test_CommitUpdates(t *testing.T) { }} gitMock, dir, cleanup := mockGit(t) defer cleanup() - of := filepath.Join(dir, ".argocd-source-testapp.yaml") + of := filepath.Join(dir, ".argocd-source-testapp-0.yaml") assert.NoError(t, os.WriteFile(of, []byte(` helm: parameters: @@ -1908,7 +1910,7 @@ helm: app.Spec.Source.TargetRevision = "HEAD" wbc.GitBranch = "" - err = commitChanges(app, wbc, nil) + err = commitChanges(app, wbc, 0, nil) assert.NoError(t, err) override, err := os.ReadFile(of) assert.NoError(t, err) @@ -1954,7 +1956,7 @@ replacements: [] app.Spec.Source.TargetRevision = "HEAD" wbc.GitBranch = "" - err = commitChanges(app, wbc, nil) + err = commitChanges(app, wbc, 0, nil) assert.NoError(t, err) kust, err := os.ReadFile(kf) assert.NoError(t, err) @@ -1973,7 +1975,7 @@ replacements: [] // test the merge case too app.Spec.Source.Kustomize.Images = v1alpha1.KustomizeImages{"foo:123", "bar=qux"} - err = commitChanges(app, wbc, nil) + err = commitChanges(app, wbc, 0, nil) assert.NoError(t, err) kust, err = os.ReadFile(kf) assert.NoError(t, err) @@ -2012,7 +2014,7 @@ replacements: [] wbc.GitCommitUser = "someone" wbc.GitCommitEmail = "someone@example.com" - err = commitChanges(app, wbc, nil) + err = commitChanges(app, wbc, 0, nil) assert.NoError(t, err) }) @@ -2039,7 +2041,7 @@ replacements: [] wbc.GitCommitUser = "someone" wbc.GitCommitEmail = "someone@example.com" - err = commitChanges(app, wbc, nil) + err = commitChanges(app, wbc, 0, nil) assert.Errorf(t, err, "could not configure git") }) @@ -2054,7 +2056,7 @@ replacements: [] require.NoError(t, err) wbc.GitClient = gitMock - err = commitChanges(&app, wbc, nil) + err = commitChanges(&app, wbc, 0, nil) assert.Errorf(t, err, "cannot init") }) @@ -2069,7 +2071,7 @@ replacements: [] require.NoError(t, err) wbc.GitClient = gitMock - err = commitChanges(&app, wbc, nil) + err = commitChanges(&app, wbc, 0, nil) assert.Errorf(t, err, "cannot init") }) t.Run("Cannot checkout", func(t *testing.T) { @@ -2083,7 +2085,7 @@ replacements: [] require.NoError(t, err) wbc.GitClient = gitMock - err = commitChanges(&app, wbc, nil) + err = commitChanges(&app, wbc, 0, nil) assert.Errorf(t, err, "cannot checkout") }) @@ -2098,7 +2100,7 @@ replacements: [] require.NoError(t, err) wbc.GitClient = gitMock - err = commitChanges(&app, wbc, nil) + err = commitChanges(&app, wbc, 0, nil) assert.Errorf(t, err, "cannot commit") }) @@ -2113,7 +2115,7 @@ replacements: [] require.NoError(t, err) wbc.GitClient = gitMock - err = commitChanges(&app, wbc, nil) + err = commitChanges(&app, wbc, 0, nil) assert.Errorf(t, err, "cannot push") }) @@ -2132,7 +2134,7 @@ replacements: [] app.Spec.Source.TargetRevision = "HEAD" wbc.GitBranch = "" - err = commitChanges(app, wbc, nil) + err = commitChanges(app, wbc, 0, nil) assert.Errorf(t, err, "failed to resolve ref") }) } @@ -2156,7 +2158,9 @@ func Test_parseTarget(t *testing.T) { for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.expected, parseTarget(tt.target, tt.path)) + sources := make(v1alpha1.ApplicationSources, 1) + sources[0].Path = tt.path + assert.Equal(t, tt.expected, parseTargets(tt.target, sources)[0]) }) } } diff --git a/pkg/common/constants.go b/pkg/common/constants.go index 5abf6da6..cbe23d95 100644 --- a/pkg/common/constants.go +++ b/pkg/common/constants.go @@ -56,7 +56,7 @@ const ( ) // DefaultTargetFilePattern configurations related to the write-back functionality -const DefaultTargetFilePattern = ".argocd-source-%s.yaml" +const DefaultTargetFilePattern = ".argocd-source-%s-%d.yaml" // The default Git commit message's template const DefaultGitCommitMessage = `build: automatic update of {{ .AppName }} From dc3eb82b90ca5e8d4bd3600a884c838a7d1d02bd Mon Sep 17 00:00:00 2001 From: Casey Morton Date: Fri, 20 Oct 2023 14:07:06 -0400 Subject: [PATCH 3/6] Added annotation for image aliases to target a specific source index Signed-off-by: Casey Morton --- pkg/argocd/argocd.go | 4 ++-- pkg/argocd/git.go | 3 +-- pkg/argocd/update.go | 34 ++++++++++++++++++++++++++++------ pkg/common/constants.go | 1 + pkg/image/options.go | 32 +++++++++++++++++++++++++++++++- pkg/image/version.go | 26 +++++++++++++++++--------- 6 files changed, 80 insertions(+), 20 deletions(-) diff --git a/pkg/argocd/argocd.go b/pkg/argocd/argocd.go index 0f5a11a7..c2b8d4d0 100644 --- a/pkg/argocd/argocd.go +++ b/pkg/argocd/argocd.go @@ -198,11 +198,11 @@ func FilterApplicationsForUpdate(apps []v1alpha1.Application, patterns []string, for sourceIndex, _ := range getApplicationTypes(&app) { // Check for valid application type if !IsValidApplicationTypeForSource(&app, sourceIndex) { - logCtx.Infof("skipping app '%s' of type '%s' because it's not of supported source type", app.GetName(), GetSourceTypes(app.Status)[sourceIndex]) + logCtx.Infof("skipping application '%s' source index %d of type '%s' because it's not a supported source type", app.GetName(), sourceIndex, GetSourceTypes(app.Status)[sourceIndex]) continue } - logCtx.Tracef("processing app '%s' of type '%v'", app.GetName(), app.Status.SourceType) + logCtx.Tracef("processing application '%s' source index %s of type '%s'", app.GetName(), sourceIndex, GetSourceTypes(app.Status)[sourceIndex]) imageList := parseImageList(annotations) appImages := ApplicationImages{} appImages.Application = app diff --git a/pkg/argocd/git.go b/pkg/argocd/git.go index e7ac20ec..b96b5c22 100644 --- a/pkg/argocd/git.go +++ b/pkg/argocd/git.go @@ -247,8 +247,7 @@ func commitChangesGit(app *v1alpha1.Application, wbc *WriteBackConfig, sourceInd } func writeOverrides(app *v1alpha1.Application, wbc *WriteBackConfig, sourceIndex int, gitC git.Client) (err error, skip bool) { - logCtx := log.WithContext().AddField("application", app.GetName()) - logCtx = log.WithContext().AddField("source", sourceIndex) + logCtx := log.WithContext().AddField("application", app.GetName()).AddField("source", sourceIndex) targetExists := true targetFile := path.Join(gitC.Root(), wbc.Targets[sourceIndex]) _, err = os.Stat(targetFile) diff --git a/pkg/argocd/update.go b/pkg/argocd/update.go index cedc39af..998991a2 100644 --- a/pkg/argocd/update.go +++ b/pkg/argocd/update.go @@ -201,6 +201,7 @@ func UpdateApplication(updateConf *UpdateConfiguration, state *SyncIterationStat vc.Strategy = applicationImage.GetParameterUpdateStrategy(updateConf.UpdateApp.Application.Annotations) vc.MatchFunc, vc.MatchArgs = applicationImage.GetParameterMatch(updateConf.UpdateApp.Application.Annotations) vc.IgnoreList = applicationImage.GetParameterIgnoreTags(updateConf.UpdateApp.Application.Annotations) + vc.SourceIndex = applicationImage.GetSourceIndex(updateConf.UpdateApp.Application.Annotations) vc.Options = applicationImage. GetPlatformOptions(updateConf.UpdateApp.Application.Annotations, updateConf.IgnorePlatforms). WithMetadata(vc.Strategy.NeedsMetadata()). @@ -378,15 +379,36 @@ func needsUpdate(updateableImage *image.ContainerImage, applicationImage *image. } func setAppImage(app *v1alpha1.Application, img *image.ContainerImage) error { + logCtx := img.LogContext() imageUpdated := false imageUpdatable := false for i, appType := range getApplicationTypes(app) { - if appType == ApplicationTypeKustomize { - imageUpdatable = true - imageUpdated = SetKustomizeImageWithIndex(app, i, img) || imageUpdated - } else if appType == ApplicationTypeHelm { - imageUpdatable = true - imageUpdated = SetHelmImageWithIndex(app, i, img) || imageUpdated + logCtx.Tracef("Examining source index %d for update", i) + if imgSourceIndex := img.GetSourceIndex(app.Annotations); imgSourceIndex == image.SourceIndexAll || i == int(imgSourceIndex) { + logCtx.Debugf("Image source index %d - attempting to update source", i) + if appType == ApplicationTypeKustomize { + logCtx.Debugf("Image source index %d - attempting to update source as Kustomize application type", i) + imageUpdatable = true + if imageSourceUpdated := SetKustomizeImageWithIndex(app, i, img); imageSourceUpdated { + logCtx.Debugf("Image source index %d - successfully updated source as Kustomize image", i) + imageUpdated = true + } else { + logCtx.Debugf("Image source index %d - unable to update Kustomize images - no updatable images found", i) + } + } else if appType == ApplicationTypeHelm { + logCtx.Debugf("Image source index %d - attempting to update source as Helm application type", i) + imageUpdatable = true + if imageSourceUpdated := SetHelmImageWithIndex(app, i, img); imageSourceUpdated { + logCtx.Debugf("Image source index %d - successfully updated source as Helm parameters", i) + imageUpdated = true + } else { + logCtx.Debugf("Image source index %d - unable to update source as helm parameters - no updatable images found", i) + } + } else { + logCtx.Debugf("Image source index %d - skipping update to source due to unsupported application type", i) + } + } else { + logCtx.Tracef("Skipping update of source index %d due to image source index %d", i, imgSourceIndex) } } diff --git a/pkg/common/constants.go b/pkg/common/constants.go index cbe23d95..50336225 100644 --- a/pkg/common/constants.go +++ b/pkg/common/constants.go @@ -35,6 +35,7 @@ const ( UpdateStrategyAnnotation = ImageUpdaterAnnotationPrefix + "/%s.update-strategy" PullSecretAnnotation = ImageUpdaterAnnotationPrefix + "/%s.pull-secret" PlatformsAnnotation = ImageUpdaterAnnotationPrefix + "/%s.platforms" + SourceIndexAnnotation = ImageUpdaterAnnotationPrefix + "/%s.source-index" ) // Application-wide update strategy related annotations diff --git a/pkg/image/options.go b/pkg/image/options.go index af6d12a0..8360f53a 100644 --- a/pkg/image/options.go +++ b/pkg/image/options.go @@ -4,6 +4,7 @@ import ( "fmt" "regexp" "runtime" + "strconv" "strings" "github.com/argoproj-labs/argocd-image-updater/pkg/common" @@ -71,7 +72,7 @@ func (img *ContainerImage) HasForceUpdateOptionAnnotation(annotations map[string return forceUpdateVal == "true" } -// GetParameterSort gets and validates the value for the sort option for the +// GetParameterUpdateStrategy gets and validates the value for the update strategy for the // image from a set of annotations func (img *ContainerImage) GetParameterUpdateStrategy(annotations map[string]string) UpdateStrategy { updateStrategyAnnotations := []string{ @@ -205,6 +206,35 @@ func (img *ContainerImage) GetParameterPullSecret(annotations map[string]string) return credSrc } +func (img *ContainerImage) GetSourceIndex(annotations map[string]string) SourceIndex { + sourceIndexAnnotation := fmt.Sprintf(common.SourceIndexAnnotation, img.normalizedSymbolicName()) + + var sourceIndexVal = "" + if val, ok := annotations[sourceIndexAnnotation]; ok { + sourceIndexVal = val + } + + logCtx := img.LogContext() + if sourceIndexVal == "" { + logCtx.Tracef("No source index found. Defaulting to all.") + // Default is sort by version + return SourceIndexAll + } + logCtx.Tracef("Found source index %s", sourceIndexVal) + return img.ParseSourceIndex(sourceIndexVal) +} + +func (img *ContainerImage) ParseSourceIndex(sourceIndexVal string) SourceIndex { + logCtx := img.LogContext() + + if num, err := strconv.ParseInt(sourceIndexVal, 10, 0); err == nil { + return SourceIndex(num) + } else { + logCtx.Warnf("Invalid sourceIndex. Defaulting to all. Unexpected results may occur.") + return SourceIndexAll + } +} + // GetParameterIgnoreTags retrieves a list of tags to ignore from a comma-separated string func (img *ContainerImage) GetParameterIgnoreTags(annotations map[string]string) []string { ignoreTagsAnnotations := []string{ diff --git a/pkg/image/version.go b/pkg/image/version.go index 7613e4b9..d1285195 100644 --- a/pkg/image/version.go +++ b/pkg/image/version.go @@ -24,6 +24,12 @@ const ( StrategyDigest UpdateStrategy = 3 ) +type SourceIndex int + +const ( + SourceIndexAll SourceIndex = -1 +) + func (us UpdateStrategy) String() string { switch us { case StrategySemVer: @@ -53,12 +59,13 @@ const ( // VersionConstraint defines a constraint for comparing versions type VersionConstraint struct { - Constraint string - MatchFunc MatchFuncFn - MatchArgs interface{} - IgnoreList []string - Strategy UpdateStrategy - Options *options.ManifestOptions + Constraint string + MatchFunc MatchFuncFn + MatchArgs interface{} + IgnoreList []string + Strategy UpdateStrategy + Options *options.ManifestOptions + SourceIndex SourceIndex } type MatchFuncFn func(tagName string, pattern interface{}) bool @@ -70,9 +77,10 @@ func (vc *VersionConstraint) String() string { func NewVersionConstraint() *VersionConstraint { return &VersionConstraint{ - MatchFunc: MatchFuncNone, - Strategy: StrategySemVer, - Options: options.NewManifestOptions(), + MatchFunc: MatchFuncNone, + Strategy: StrategySemVer, + SourceIndex: SourceIndexAll, + Options: options.NewManifestOptions(), } } From 5a8d3b9074c3b1c33abaa2f6f4488595fd5cc140 Mon Sep 17 00:00:00 2001 From: Casey Morton Date: Wed, 25 Oct 2023 11:40:54 -0400 Subject: [PATCH 4/6] Fixed lint issues Signed-off-by: Casey Morton --- pkg/argocd/argocd.go | 9 +++------ pkg/argocd/update.go | 2 +- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/pkg/argocd/argocd.go b/pkg/argocd/argocd.go index c2b8d4d0..f7a494c8 100644 --- a/pkg/argocd/argocd.go +++ b/pkg/argocd/argocd.go @@ -195,14 +195,14 @@ func FilterApplicationsForUpdate(apps []v1alpha1.Application, patterns []string, logCtx.Debugf("Skipping app '%s' because it does not carry requested label", app.GetName()) continue } - for sourceIndex, _ := range getApplicationTypes(&app) { + for sourceIndex := range getApplicationTypes(&app) { // Check for valid application type if !IsValidApplicationTypeForSource(&app, sourceIndex) { logCtx.Infof("skipping application '%s' source index %d of type '%s' because it's not a supported source type", app.GetName(), sourceIndex, GetSourceTypes(app.Status)[sourceIndex]) continue } - logCtx.Tracef("processing application '%s' source index %s of type '%s'", app.GetName(), sourceIndex, GetSourceTypes(app.Status)[sourceIndex]) + logCtx.Tracef("processing application '%s' source index %d of type '%s'", app.GetName(), sourceIndex, GetSourceTypes(app.Status)[sourceIndex]) imageList := parseImageList(annotations) appImages := ApplicationImages{} appImages.Application = app @@ -580,8 +580,5 @@ func GetSourceTypes(status v1alpha1.ApplicationStatus) []v1alpha1.ApplicationSou if HasMultipleSourceTypes(status) { return status.SourceTypes } - if &status.SourceType != nil { - return []v1alpha1.ApplicationSourceType{status.SourceType} - } - return []v1alpha1.ApplicationSourceType{} + return []v1alpha1.ApplicationSourceType{status.SourceType} } diff --git a/pkg/argocd/update.go b/pkg/argocd/update.go index 998991a2..79e912e8 100644 --- a/pkg/argocd/update.go +++ b/pkg/argocd/update.go @@ -538,7 +538,7 @@ func getWriteBackConfig(app *v1alpha1.Application, kubeClient *kube.KubernetesCl wbc.KustomizeBases[i] = "" } } - if credErrors := parseGitConfig(app, kubeClient, wbc, creds); credErrors != nil && len(credErrors) > 0 { + if credErrors := parseGitConfig(app, kubeClient, wbc, creds); len(credErrors) > 0 { return nil, fmt.Errorf("Errors parsing git credentials: %s", method) } default: From 2d13dd7313e6ebcf0c0315aa899185237695eafb Mon Sep 17 00:00:00 2001 From: Casey Morton Date: Wed, 25 Oct 2023 13:26:29 -0400 Subject: [PATCH 5/6] Updated documentation Signed-off-by: Casey Morton --- docs/configuration/images.md | 82 ++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/docs/configuration/images.md b/docs/configuration/images.md index dc492981..962d40b9 100644 --- a/docs/configuration/images.md +++ b/docs/configuration/images.md @@ -353,6 +353,88 @@ If the `.helm.image-spec` annotation is set, the two other annotations `.helm.image-name` and `.helm.image-tag` will be ignored. +## Targeting specific sources in a multi-source application + +!!! warning "Beta Feature" + Multi-source applications are currently considered a beta feature within ArgoCD, therefore ArgoCD Image Updater's support for it should be considered beta as well. For more information on this feature, please consult the [ArgoCD documentation](https://argo-cd.readthedocs.io/en/stable/user-guide/multiple_sources/). + +Typical applications only have a single source associated with them, however with ArgoCD 2.6+ multiple sources can be specified for a single application. This can occasionally serve as an alternative to the Application of Applications pattern. This may also require the ability to limit the scope of an image update to a single source. Consider the following example of a multi-source application definition: + +```yaml +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + annotations: + argocd-image-updater.argoproj.io/image-list: wordpress=bitnami/wordpress:6,nginx=bitnami/nginx:1.25 + argocd-image-updater.argoproj.io/wordpress.helm.image-name: image.repository + argocd-image-updater.argoproj.io/wordpress.helm.image-tag: image.tag + argocd-image-updater.argoproj.io/nginx.source-index: 1 + argocd-image-updater.argoproj.io/nginx.helm.image-name: image.repository + argocd-image-updater.argoproj.io/nginx.helm.source-index: image.tag + name: multi-source-app + namespace: argocd +spec: + destination: + namespace: guestbook + server: https://kubernetes.default.svc + project: default + sources: + - chart: wordpress + repoURL: https://charts.bitnami.com/bitnami + targetRevision: ^18.0.0 + - chart: nginx + repoURL: https://charts.bitnami.com/bitnami + targetRevision: ^15.0.0 +``` + +*Scenario:* Both of these images are controlled by the same value file variables +(`image.repository` and `image.tag`) within their respective Helm charts. In the case +where both the wordpress and nginx images were to be included by child applications, +referenced by a single parent application (Application of Applications pattern), there +would be an opportunity to control each chart values individually with separate values +to be applied to each chart. This would be solved similarly in the scenario where one +or more of the images are pulled in with sub-charts. + +*Solution:* In order to properly scope the overrides for each individual helm chart in +a multi-source application, the `source-index` annotation must be provided for each +image as shown below. + +```yaml +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + annotations: + argocd-image-updater.argoproj.io/image-list: wordpress=bitnami/wordpress:6,nginx=bitnami/nginx:1.25 + argocd-image-updater.argoproj.io/wordpress.source-index: 0 + argocd-image-updater.argoproj.io/wordpress.helm.image-name: image.repository + argocd-image-updater.argoproj.io/wordpress.helm.image-tag: image.tag + argocd-image-updater.argoproj.io/nginx.source-index: 1 + argocd-image-updater.argoproj.io/nginx.helm.image-name: image.repository + argocd-image-updater.argoproj.io/nginx.helm.image-tag: image.tag + name: multi-source-app + namespace: argocd +spec: + destination: + namespace: guestbook + server: https://kubernetes.default.svc + project: default + sources: + - chart: wordpress + repoURL: https://charts.bitnami.com/bitnami + targetRevision: ^18.0.0 + - chart: nginx + repoURL: https://charts.bitnami.com/bitnami + targetRevision: ^15.0.0 +``` + +The general syntax for the `source-index` annotation is: +```yaml +argocd-image-updater.argoproj.io/.source-index: +``` +!!!note + The value of the `source-index` annotation is 0-based, meaning that the first source would be `0`, the second + would be `1`, and so on. + ## Examples ### Following an image's patch branch From b3c0d38cc88062d48fb5fbb3bf80bcfa9fcc211b Mon Sep 17 00:00:00 2001 From: Casey Morton Date: Wed, 25 Oct 2023 13:40:28 -0400 Subject: [PATCH 6/6] Added spelling exceptions Signed-off-by: Casey Morton --- .github/actions/spelling/expect.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 1988a508..893dd378 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -5,6 +5,7 @@ bacd CVE credref DEBU +bitnami dockerhub eec fbd @@ -19,4 +20,5 @@ someimage somerepo ssw wildcards +wordpress yyyy