Skip to content

Commit 89daab6

Browse files
authored
feat: support filtering apps by label in CLI (#368)
1 parent 527aecb commit 89daab6

File tree

5 files changed

+101
-4
lines changed

5 files changed

+101
-4
lines changed

cmd/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ type ImageUpdaterConfig struct {
4040
MetricsPort int
4141
RegistriesConf string
4242
AppNamePatterns []string
43+
AppLabel string
4344
GitCommitUser string
4445
GitCommitMail string
4546
GitCommitMessage *template.Template

cmd/run.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ func newRunCommand() *cobra.Command {
218218
runCmd.Flags().IntVar(&cfg.MaxConcurrency, "max-concurrency", 10, "maximum number of update threads to run concurrently")
219219
runCmd.Flags().StringVar(&cfg.ArgocdNamespace, "argocd-namespace", "", "namespace where ArgoCD runs in (current namespace by default)")
220220
runCmd.Flags().StringSliceVar(&cfg.AppNamePatterns, "match-application-name", nil, "patterns to match application name against")
221+
runCmd.Flags().StringVar(&cfg.AppLabel, "match-application-label", "", "label to match application labels against")
221222
runCmd.Flags().BoolVar(&warmUpCache, "warmup-cache", true, "whether to perform a cache warm-up on startup")
222223
runCmd.Flags().StringVar(&cfg.GitCommitUser, "git-commit-user", env.GetStringVal("GIT_COMMIT_USER", "argocd-image-updater"), "Username to use for Git commits")
223224
runCmd.Flags().StringVar(&cfg.GitCommitMail, "git-commit-email", env.GetStringVal("GIT_COMMIT_EMAIL", "[email protected]"), "E-Mail address to use for Git commits")
@@ -259,7 +260,7 @@ func runImageUpdater(cfg *ImageUpdaterConfig, warmUp bool) (argocd.ImageUpdaterR
259260

260261
// Get the list of applications that are allowed for updates, that is, those
261262
// applications which have correct annotation.
262-
appList, err := argocd.FilterApplicationsForUpdate(apps, cfg.AppNamePatterns)
263+
appList, err := argocd.FilterApplicationsForUpdate(apps, cfg.AppNamePatterns, cfg.AppLabel)
263264
if err != nil {
264265
return result, err
265266
}

docs/install/running.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,16 @@ style wildcards, i.e. `*-staging` would match any application name with a
116116
suffix of `-staging`. Can be specified multiple times to define more than
117117
one pattern, from which at least one has to match.
118118

119+
**--match-application-label *label* **
120+
121+
Only process applications that have a valid annotation and match the given
122+
*label*. The *label* is a string that matches the standard kubernetes label
123+
syntax of `key=value`. For e.g, `custom.label/name=xyz` would be a valid label
124+
that can be supplied through this parameter. Any applications carrying this
125+
exact label will be considered as candidates for image updates. This parameter
126+
currently does not support patten matching on label values (e.g `customer.label/name=*-staging`)
127+
and only accepts a single label to match applications against.
128+
119129
**--max-concurrency *number* **
120130

121131
Process a maximum of *number* applications concurrently. To disable concurrent

pkg/argocd/argocd.go

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,10 +145,34 @@ func nameMatchesPattern(name string, patterns []string) bool {
145145
return false
146146
}
147147

148+
// Match app labels against provided filter label
149+
func matchAppLabels(appName string, appLabels map[string]string, filterLabel string) bool {
150+
151+
if filterLabel == "" {
152+
return true
153+
}
154+
155+
filterLabelMap, err := parseLabel(filterLabel)
156+
if err != nil {
157+
log.Errorf("Unable match app labels against %s: %s", filterLabel, err)
158+
return false
159+
}
160+
161+
for filterLabelKey, filterLabelValue := range filterLabelMap {
162+
log.Tracef("Matching application name %s against label %s", appName, filterLabel)
163+
if appLabelValue, ok := appLabels[filterLabelKey]; ok {
164+
if appLabelValue == filterLabelValue {
165+
return true
166+
}
167+
}
168+
}
169+
return false
170+
}
171+
148172
// Retrieve a list of applications from ArgoCD that qualify for image updates
149173
// Application needs either to be of type Kustomize or Helm and must have the
150174
// correct annotation in order to be considered.
151-
func FilterApplicationsForUpdate(apps []v1alpha1.Application, patterns []string) (map[string]ApplicationImages, error) {
175+
func FilterApplicationsForUpdate(apps []v1alpha1.Application, patterns []string, appLabel string) (map[string]ApplicationImages, error) {
152176
var appsForUpdate = make(map[string]ApplicationImages)
153177

154178
for _, app := range apps {
@@ -172,6 +196,12 @@ func FilterApplicationsForUpdate(apps []v1alpha1.Application, patterns []string)
172196
continue
173197
}
174198

199+
// Check if application carries requested label
200+
if !matchAppLabels(app.GetName(), app.GetLabels(), appLabel) {
201+
logCtx.Debugf("Skipping app '%s' because it does not carry requested label", app.GetName())
202+
continue
203+
}
204+
175205
logCtx.Tracef("processing app '%s' of type '%v'", app.GetName(), app.Status.SourceType)
176206
imageList := parseImageList(annotations)
177207
appImages := ApplicationImages{}
@@ -198,6 +228,20 @@ func parseImageList(annotations map[string]string) *image.ContainerImageList {
198228
return &results
199229
}
200230

231+
func parseLabel(inputLabel string) (map[string]string, error) {
232+
var selectedLabels map[string]string
233+
const labelFieldDelimiter = "="
234+
if inputLabel != "" {
235+
selectedLabels = map[string]string{}
236+
fields := strings.Split(inputLabel, labelFieldDelimiter)
237+
if len(fields) != 2 {
238+
return nil, fmt.Errorf("labels should have key%svalue, but instead got: %s", labelFieldDelimiter, inputLabel)
239+
}
240+
selectedLabels[fields[0]] = fields[1]
241+
}
242+
return selectedLabels, nil
243+
}
244+
201245
// GetApplication gets the application named appName from Argo CD API
202246
func (client *argoCD) GetApplication(ctx context.Context, appName string) (*v1alpha1.Application, error) {
203247
conn, appClient, err := client.Client.NewApplicationClient()

pkg/argocd/argocd_test.go

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ func Test_FilterApplicationsForUpdate(t *testing.T) {
203203
},
204204
},
205205
}
206-
filtered, err := FilterApplicationsForUpdate(applicationList, []string{})
206+
filtered, err := FilterApplicationsForUpdate(applicationList, []string{}, "")
207207
require.NoError(t, err)
208208
require.Len(t, filtered, 1)
209209
require.Contains(t, filtered, "app1")
@@ -255,14 +255,55 @@ func Test_FilterApplicationsForUpdate(t *testing.T) {
255255
},
256256
},
257257
}
258-
filtered, err := FilterApplicationsForUpdate(applicationList, []string{"app*"})
258+
filtered, err := FilterApplicationsForUpdate(applicationList, []string{"app*"}, "")
259259
require.NoError(t, err)
260260
require.Len(t, filtered, 2)
261261
require.Contains(t, filtered, "app1")
262262
require.Contains(t, filtered, "app2")
263263
assert.Len(t, filtered["app1"].Images, 2)
264264
})
265265

266+
t.Run("Filter for applications with label", func(t *testing.T) {
267+
applicationList := []v1alpha1.Application{
268+
// Annotated and carries required label
269+
{
270+
ObjectMeta: v1.ObjectMeta{
271+
Name: "app1",
272+
Namespace: "argocd",
273+
Annotations: map[string]string{
274+
common.ImageUpdaterAnnotation: "nginx, quay.io/dexidp/dex:v1.23.0",
275+
},
276+
Labels: map[string]string{
277+
"custom.label/name": "xyz",
278+
},
279+
},
280+
Spec: v1alpha1.ApplicationSpec{},
281+
Status: v1alpha1.ApplicationStatus{
282+
SourceType: v1alpha1.ApplicationSourceTypeKustomize,
283+
},
284+
},
285+
// Annotated but does not carry required label
286+
{
287+
ObjectMeta: v1.ObjectMeta{
288+
Name: "app2",
289+
Namespace: "argocd",
290+
Annotations: map[string]string{
291+
common.ImageUpdaterAnnotation: "nginx, quay.io/dexidp/dex:v1.23.0",
292+
},
293+
},
294+
Spec: v1alpha1.ApplicationSpec{},
295+
Status: v1alpha1.ApplicationStatus{
296+
SourceType: v1alpha1.ApplicationSourceTypeHelm,
297+
},
298+
},
299+
}
300+
filtered, err := FilterApplicationsForUpdate(applicationList, []string{}, "custom.label/name=xyz")
301+
require.NoError(t, err)
302+
require.Len(t, filtered, 1)
303+
require.Contains(t, filtered, "app1")
304+
assert.Len(t, filtered["app1"].Images, 2)
305+
})
306+
266307
}
267308

268309
func Test_GetHelmParamAnnotations(t *testing.T) {

0 commit comments

Comments
 (0)