diff --git a/cmd/run.go b/cmd/run.go index 751c29c9..06030d22 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -236,7 +236,7 @@ func newRunCommand() *cobra.Command { runCmd.Flags().IntVar(&cfg.MaxConcurrency, "max-concurrency", 10, "maximum number of update threads to run concurrently") runCmd.Flags().StringVar(&cfg.ArgocdNamespace, "argocd-namespace", "", "namespace where ArgoCD runs in (current namespace by default)") runCmd.Flags().StringSliceVar(&cfg.AppNamePatterns, "match-application-name", nil, "patterns to match application name against") - runCmd.Flags().StringVar(&cfg.AppLabel, "match-application-label", "", "label to match application labels against") + runCmd.Flags().StringVar(&cfg.AppLabel, "match-application-label", "", "label selector to match application labels against") runCmd.Flags().BoolVar(&warmUpCache, "warmup-cache", true, "whether to perform a cache warm-up on startup") runCmd.Flags().StringVar(&cfg.GitCommitUser, "git-commit-user", env.GetStringVal("GIT_COMMIT_USER", "argocd-image-updater"), "Username to use for Git commits") runCmd.Flags().StringVar(&cfg.GitCommitMail, "git-commit-email", env.GetStringVal("GIT_COMMIT_EMAIL", "noreply@argoproj.io"), "E-Mail address to use for Git commits") @@ -267,7 +267,7 @@ func runImageUpdater(cfg *ImageUpdaterConfig, warmUp bool) (argocd.ImageUpdaterR } cfg.ArgoClient = argoClient - apps, err := cfg.ArgoClient.ListApplications() + apps, err := cfg.ArgoClient.ListApplications(cfg.AppLabel) if err != nil { log.WithContext(). AddField("argocd_server", cfg.ClientOpts.ServerAddr). @@ -281,7 +281,7 @@ func runImageUpdater(cfg *ImageUpdaterConfig, warmUp bool) (argocd.ImageUpdaterR // Get the list of applications that are allowed for updates, that is, those // applications which have correct annotation. - appList, err := argocd.FilterApplicationsForUpdate(apps, cfg.AppNamePatterns, cfg.AppLabel) + appList, err := argocd.FilterApplicationsForUpdate(apps, cfg.AppNamePatterns) if err != nil { return result, err } diff --git a/docs/install/reference.md b/docs/install/reference.md index 9229ecc5..77d77586 100644 --- a/docs/install/reference.md +++ b/docs/install/reference.md @@ -116,15 +116,16 @@ style wildcards, i.e. `*-staging` would match any application name with a suffix of `-staging`. Can be specified multiple times to define more than one pattern, from which at least one has to match. -**--match-application-label *label* ** +**--match-application-label *selector* ** Only process applications that have a valid annotation and match the given -*label*. The *label* is a string that matches the standard kubernetes label -syntax of `key=value`. For e.g, `custom.label/name=xyz` would be a valid label -that can be supplied through this parameter. Any applications carrying this +*label* selector. The *selector* is a string that matches the standard kubernetes label +[label selector syntax][]. For e.g, `custom.label/name=xyz` would be a valid label +that can be supplied through this parameter. Any applications carrying this exact label will be considered as candidates for image updates. This parameter -currently does not support pattern matching on label values (e.g `customer.label/name=*-staging`) -and only accepts a single label to match applications against. +currently does not support pattern matching on label values (e.g `customer.label/name=*-staging`). +You can specify equality, inequality, or set based requirements or a combination. +For e.g., `app,app!=foo,custom.label/name=xyz,customer in (a,b,c)` **--max-concurrency *number* ** @@ -142,3 +143,5 @@ Load the registry configuration from file at *path*. Defaults to the path `/app/config/registries.conf`. If no configuration should be loaded, and the default configuration should be used instead, specify the empty string, i.e. `--registries-conf-path=""`. + +[label selector syntax]: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors \ No newline at end of file diff --git a/pkg/argocd/argocd.go b/pkg/argocd/argocd.go index 8ad85d55..cb7c5fe4 100644 --- a/pkg/argocd/argocd.go +++ b/pkg/argocd/argocd.go @@ -29,8 +29,8 @@ func (client *k8sClient) GetApplication(ctx context.Context, appName string) (*v return client.kubeClient.ApplicationsClientset.ArgoprojV1alpha1().Applications(client.kubeClient.Namespace).Get(ctx, appName, v1.GetOptions{}) } -func (client *k8sClient) ListApplications() ([]v1alpha1.Application, error) { - list, err := client.kubeClient.ApplicationsClientset.ArgoprojV1alpha1().Applications(client.kubeClient.Namespace).List(context.TODO(), v1.ListOptions{}) +func (client *k8sClient) ListApplications(labelSelector string) ([]v1alpha1.Application, error) { + list, err := client.kubeClient.ApplicationsClientset.ArgoprojV1alpha1().Applications(client.kubeClient.Namespace).List(context.TODO(), v1.ListOptions{LabelSelector: labelSelector}) if err != nil { return nil, err } @@ -71,7 +71,7 @@ type argoCD struct { // ArgoCD is the interface for accessing Argo CD functions we need type ArgoCD interface { GetApplication(ctx context.Context, appName string) (*v1alpha1.Application, error) - ListApplications() ([]v1alpha1.Application, error) + ListApplications(labelSelector string) ([]v1alpha1.Application, error) UpdateSpec(ctx context.Context, spec *application.ApplicationUpdateSpecRequest) (*v1alpha1.ApplicationSpec, error) } @@ -145,34 +145,10 @@ func nameMatchesPattern(name string, patterns []string) bool { return false } -// Match app labels against provided filter label -func matchAppLabels(appName string, appLabels map[string]string, filterLabel string) bool { - - if filterLabel == "" { - return true - } - - filterLabelMap, err := parseLabel(filterLabel) - if err != nil { - log.Errorf("Unable match app labels against %s: %s", filterLabel, err) - return false - } - - for filterLabelKey, filterLabelValue := range filterLabelMap { - log.Tracef("Matching application name %s against label %s", appName, filterLabel) - if appLabelValue, ok := appLabels[filterLabelKey]; ok { - if appLabelValue == filterLabelValue { - return true - } - } - } - return false -} - // Retrieve a list of applications from ArgoCD that qualify for image updates // Application needs either to be of type Kustomize or Helm and must have the // correct annotation in order to be considered. -func FilterApplicationsForUpdate(apps []v1alpha1.Application, patterns []string, appLabel string) (map[string]ApplicationImages, error) { +func FilterApplicationsForUpdate(apps []v1alpha1.Application, patterns []string) (map[string]ApplicationImages, error) { var appsForUpdate = make(map[string]ApplicationImages) for _, app := range apps { @@ -199,12 +175,6 @@ func FilterApplicationsForUpdate(apps []v1alpha1.Application, patterns []string, continue } - // Check if application carries requested label - if !matchAppLabels(app.GetName(), app.GetLabels(), appLabel) { - logCtx.Debugf("Skipping app '%s' because it does not carry requested label", appNSName) - continue - } - logCtx.Tracef("processing app '%s' of type '%v'", appNSName, sourceType) imageList := parseImageList(annotations) appImages := ApplicationImages{} @@ -231,20 +201,6 @@ func parseImageList(annotations map[string]string) *image.ContainerImageList { return &results } -func parseLabel(inputLabel string) (map[string]string, error) { - var selectedLabels map[string]string - const labelFieldDelimiter = "=" - if inputLabel != "" { - selectedLabels = map[string]string{} - fields := strings.Split(inputLabel, labelFieldDelimiter) - if len(fields) != 2 { - return nil, fmt.Errorf("labels should have key%svalue, but instead got: %s", labelFieldDelimiter, inputLabel) - } - selectedLabels[fields[0]] = fields[1] - } - return selectedLabels, nil -} - // GetApplication gets the application named appName from Argo CD API func (client *argoCD) GetApplication(ctx context.Context, appName string) (*v1alpha1.Application, error) { conn, appClient, err := client.Client.NewApplicationClient() @@ -267,7 +223,7 @@ func (client *argoCD) GetApplication(ctx context.Context, appName string) (*v1al // ListApplications returns a list of all application names that the API user // has access to. -func (client *argoCD) ListApplications() ([]v1alpha1.Application, error) { +func (client *argoCD) ListApplications(labelSelector string) ([]v1alpha1.Application, error) { conn, appClient, err := client.Client.NewApplicationClient() metrics.Clients().IncreaseArgoCDClientRequest(client.Client.ClientOptions().ServerAddr, 1) if err != nil { @@ -277,7 +233,7 @@ func (client *argoCD) ListApplications() ([]v1alpha1.Application, error) { defer conn.Close() metrics.Clients().IncreaseArgoCDClientRequest(client.Client.ClientOptions().ServerAddr, 1) - apps, err := appClient.List(context.TODO(), &application.ApplicationQuery{}) + apps, err := appClient.List(context.TODO(), &application.ApplicationQuery{Selector: &labelSelector}) if err != nil { metrics.Clients().IncreaseArgoCDClientError(client.Client.ClientOptions().ServerAddr, 1) return nil, err diff --git a/pkg/argocd/argocd_test.go b/pkg/argocd/argocd_test.go index 87ec54f9..9f531a6d 100644 --- a/pkg/argocd/argocd_test.go +++ b/pkg/argocd/argocd_test.go @@ -491,7 +491,7 @@ func Test_FilterApplicationsForUpdate(t *testing.T) { }, }, } - filtered, err := FilterApplicationsForUpdate(applicationList, []string{}, "") + filtered, err := FilterApplicationsForUpdate(applicationList, []string{}) require.NoError(t, err) require.Len(t, filtered, 1) require.Contains(t, filtered, "argocd/app1") @@ -543,55 +543,13 @@ func Test_FilterApplicationsForUpdate(t *testing.T) { }, }, } - filtered, err := FilterApplicationsForUpdate(applicationList, []string{"app*"}, "") + filtered, err := FilterApplicationsForUpdate(applicationList, []string{"app*"}) require.NoError(t, err) require.Len(t, filtered, 2) require.Contains(t, filtered, "argocd/app1") require.Contains(t, filtered, "argocd/app2") assert.Len(t, filtered["argocd/app1"].Images, 2) }) - - t.Run("Filter for applications with label", func(t *testing.T) { - applicationList := []v1alpha1.Application{ - // Annotated and carries required label - { - ObjectMeta: v1.ObjectMeta{ - Name: "app1", - Namespace: "argocd", - Annotations: map[string]string{ - common.ImageUpdaterAnnotation: "nginx, quay.io/dexidp/dex:v1.23.0", - }, - Labels: map[string]string{ - "custom.label/name": "xyz", - }, - }, - Spec: v1alpha1.ApplicationSpec{}, - Status: v1alpha1.ApplicationStatus{ - SourceType: v1alpha1.ApplicationSourceTypeKustomize, - }, - }, - // Annotated but does not carry required label - { - ObjectMeta: v1.ObjectMeta{ - Name: "app2", - Namespace: "argocd", - Annotations: map[string]string{ - common.ImageUpdaterAnnotation: "nginx, quay.io/dexidp/dex:v1.23.0", - }, - }, - Spec: v1alpha1.ApplicationSpec{}, - Status: v1alpha1.ApplicationStatus{ - SourceType: v1alpha1.ApplicationSourceTypeHelm, - }, - }, - } - filtered, err := FilterApplicationsForUpdate(applicationList, []string{}, "custom.label/name=xyz") - require.NoError(t, err) - require.Len(t, filtered, 1) - require.Contains(t, filtered, "argocd/app1") - assert.Len(t, filtered["argocd/app1"].Images, 2) - }) - } func Test_GetHelmParamAnnotations(t *testing.T) { @@ -1147,7 +1105,7 @@ func TestKubernetesClient(t *testing.T) { require.NoError(t, err) t.Run("List applications", func(t *testing.T) { - apps, err := client.ListApplications() + apps, err := client.ListApplications("") require.NoError(t, err) require.Len(t, apps, 1) diff --git a/pkg/argocd/mocks/ArgoCD.go b/pkg/argocd/mocks/ArgoCD.go index f4b81301..7a413230 100644 --- a/pkg/argocd/mocks/ArgoCD.go +++ b/pkg/argocd/mocks/ArgoCD.go @@ -48,7 +48,7 @@ func (_m *ArgoCD) GetApplication(ctx context.Context, appName string) (*v1alpha1 } // ListApplications provides a mock function with given fields: -func (_m *ArgoCD) ListApplications() ([]v1alpha1.Application, error) { +func (_m *ArgoCD) ListApplications(labelSelector string) ([]v1alpha1.Application, error) { ret := _m.Called() if len(ret) == 0 {