Skip to content

Commit

Permalink
feat!: Filter labels on the server instead of client to allow more la…
Browse files Browse the repository at this point in the history
…bel filtering options (#832)

Signed-off-by: Joshua Novick <joshua.novick@hunters.ai>
  • Loading branch information
jnovick authored Aug 14, 2024
1 parent 65698c5 commit 0ad5b94
Show file tree
Hide file tree
Showing 5 changed files with 22 additions and 105 deletions.
6 changes: 3 additions & 3 deletions cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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).
Expand All @@ -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
}
Expand Down
15 changes: 9 additions & 6 deletions docs/install/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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* **

Expand All @@ -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
56 changes: 6 additions & 50 deletions pkg/argocd/argocd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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{}
Expand All @@ -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()
Expand All @@ -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 {
Expand All @@ -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
Expand Down
48 changes: 3 additions & 45 deletions pkg/argocd/argocd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -1064,7 +1022,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)

Expand Down
2 changes: 1 addition & 1 deletion pkg/argocd/mocks/ArgoCD.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 0ad5b94

Please sign in to comment.