Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add compatibility for multiple sources #635

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/actions/spelling/expect.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ bacd
CVE
credref
DEBU
bitnami
dockerhub
eec
fbd
Expand All @@ -19,4 +20,5 @@ someimage
somerepo
ssw
wildcards
wordpress
yyyy
82 changes: 82 additions & 0 deletions docs/configuration/images.md
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,88 @@ If the `<image_alias>.helm.image-spec` annotation is set, the two other
annotations `<image_alias>.helm.image-name` and `<image_alias>.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/<image_alias>.source-index: <index of the source which the image specification should apply>
```
!!!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
Expand Down
129 changes: 83 additions & 46 deletions pkg/argocd/argocd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -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 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)
imageList := parseImageList(annotations)
appImages := ApplicationImages{}
appImages.Application = app
appImages.Images = *imageList
appsForUpdate[app.GetName()] = appImages
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
appImages.Images = *imageList
appsForUpdate[app.GetName()] = appImages
}
}

return appsForUpdate, nil
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -425,23 +426,24 @@ 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()[sourceIndex].Helm == nil {
app.Spec.GetSources()[sourceIndex].Helm = &v1alpha1.ApplicationSourceHelm{}
}

if app.Spec.Source.Helm.Parameters == nil {
app.Spec.Source.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.Source.Helm.Parameters = mergeHelmParams(app.Spec.Source.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
Expand All @@ -454,24 +456,31 @@ 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()[sourceIndex]
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))

return nil
if app.Spec.HasMultipleSources() {
app.Spec.Sources[sourceIndex] = source
} else {
app.Spec.Source = &source
}

return true
}

// GetImagesFromApplication returns the list of known images for the given application
Expand All @@ -496,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 {
Expand All @@ -545,3 +571,14 @@ 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
}
return []v1alpha1.ApplicationSourceType{status.SourceType}
}
Loading
Loading