Skip to content

Commit

Permalink
feat: equality-based and set-based filtering by label keys and values…
Browse files Browse the repository at this point in the history
… for list, sync, delete and wait App commands (#10548)

Signed-off-by: maheshbaliga <mahesh.baliga@infracloud.io>

Signed-off-by: maheshbaliga <mahesh.baliga@infracloud.io>
  • Loading branch information
maheshbaliga committed Oct 3, 2022
1 parent 6d5a223 commit a8e2fb9
Show file tree
Hide file tree
Showing 7 changed files with 161 additions and 27 deletions.
43 changes: 30 additions & 13 deletions cmd/argocd/commands/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -1132,13 +1132,18 @@ func NewApplicationDeleteCommand(clientOpts *argocdclient.ClientOptions) *cobra.
var command = &cobra.Command{
Use: "delete APPNAME",
Short: "Delete an application",
Example: `argocd app delete app1
# Delete multiple apps
argocd app delete app1 app2
# Delete apps by label
argocd app delete -l foo=bar`,
Example: ` # Delete an app
argocd app delete my-app
# Delete multiple apps
argocd app delete my-app other-app
# Delete apps by label
argocd app delete -l app.kubernetes.io/instance=my-app
argocd app delete -l app.kubernetes.io/instance!=my-app
argocd app delete -l app.kubernetes.io/instance
argocd app delete -l '!app.kubernetes.io/instance'
argocd app delete -l 'app.kubernetes.io/instance notin (my-app,other-app)'`,
Run: func(c *cobra.Command, args []string) {
ctx := c.Context()

Expand Down Expand Up @@ -1207,7 +1212,7 @@ argocd app delete -l foo=bar`,
command.Flags().BoolVar(&cascade, "cascade", true, "Perform a cascaded deletion of all application resources")
command.Flags().StringVarP(&propagationPolicy, "propagation-policy", "p", "foreground", "Specify propagation policy for deletion of application's resources. One of: foreground|background")
command.Flags().BoolVarP(&noPrompt, "yes", "y", false, "Turn off prompting to confirm cascaded deletion of application resources")
command.Flags().StringVarP(&selector, "selector", "l", "", "Delete all apps with matching label")
command.Flags().StringVarP(&selector, "selector", "l", "", "Delete all apps with matching label. Supports '=', '==', '!=', in, notin, exists & not exists. Matching apps must satisfy all of the specified label constraints.")
return command
}

Expand Down Expand Up @@ -1266,7 +1271,11 @@ func NewApplicationListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
argocd app list
# List apps by label, in this example we listing apps that are children of another app (aka app-of-apps)
argocd app list -l app.kubernetes.io/instance=my-app`,
argocd app list -l app.kubernetes.io/instance=my-app
argocd app list -l app.kubernetes.io/instance!=my-app
argocd app list -l app.kubernetes.io/instance
argocd app list -l '!app.kubernetes.io/instance'
argocd app list -l 'app.kubernetes.io/instance notin (my-app,other-app)'`,
Run: func(c *cobra.Command, args []string) {
ctx := c.Context()

Expand Down Expand Up @@ -1315,7 +1324,7 @@ func NewApplicationListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
},
}
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: wide|name|json|yaml")
command.Flags().StringVarP(&selector, "selector", "l", "", "List apps by label")
command.Flags().StringVarP(&selector, "selector", "l", "", "List apps by label. Supports '=', '==', '!=', in, notin, exists & not exists. Matching apps must satisfy all of the specified label constraints.")
command.Flags().StringArrayVarP(&projects, "project", "p", []string{}, "Filter by project name")
command.Flags().StringVarP(&repo, "repo", "r", "", "List apps by source repo URL")
command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Only list applications in namespace")
Expand Down Expand Up @@ -1436,7 +1445,11 @@ func NewApplicationWaitCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
argocd app wait my-app other-app
# Wait for apps by label, in this example we waiting for apps that are children of another app (aka app-of-apps)
argocd app wait -l app.kubernetes.io/instance=apps`,
argocd app wait -l app.kubernetes.io/instance=my-app
argocd app wait -l app.kubernetes.io/instance!=my-app
argocd app wait -l app.kubernetes.io/instance
argocd app wait -l '!app.kubernetes.io/instance'
argocd app wait -l 'app.kubernetes.io/instance notin (my-app,other-app)'`,
Run: func(c *cobra.Command, args []string) {
ctx := c.Context()

Expand Down Expand Up @@ -1468,7 +1481,7 @@ func NewApplicationWaitCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
command.Flags().BoolVar(&watch.health, "health", false, "Wait for health")
command.Flags().BoolVar(&watch.suspended, "suspended", false, "Wait for suspended")
command.Flags().BoolVar(&watch.degraded, "degraded", false, "Wait for degraded")
command.Flags().StringVarP(&selector, "selector", "l", "", "Wait for apps by label")
command.Flags().StringVarP(&selector, "selector", "l", "", "Wait for apps by label. Supports '=', '==', '!=', in, notin, exists & not exists. Matching apps must satisfy all of the specified label constraints.")
command.Flags().StringArrayVar(&resources, "resource", []string{}, fmt.Sprintf("Sync only specific resources as GROUP%sKIND%sNAME. Fields may be blank. This option may be specified repeatedly", resourceFieldDelimiter, resourceFieldDelimiter))
command.Flags().BoolVar(&watch.operation, "operation", false, "Wait for pending operations")
command.Flags().UintVar(&timeout, "timeout", defaultCheckTimeoutSeconds, "Time out after this many seconds")
Expand Down Expand Up @@ -1520,6 +1533,10 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
# Sync apps by label, in this example we sync apps that are children of another app (aka app-of-apps)
argocd app sync -l app.kubernetes.io/instance=my-app
argocd app sync -l app.kubernetes.io/instance!=my-app
argocd app sync -l app.kubernetes.io/instance
argocd app sync -l '!app.kubernetes.io/instance'
argocd app sync -l 'app.kubernetes.io/instance notin (my-app,other-app)'
# Sync a specific resource
# Resource should be formatted as GROUP:KIND:NAME. If no GROUP is specified then :KIND:NAME
Expand Down Expand Up @@ -1751,7 +1768,7 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
command.Flags().BoolVar(&prune, "prune", false, "Allow deleting unexpected resources")
command.Flags().StringVar(&revision, "revision", "", "Sync to a specific revision. Preserves parameter overrides")
command.Flags().StringArrayVar(&resources, "resource", []string{}, fmt.Sprintf("Sync only specific resources as GROUP%sKIND%sNAME. Fields may be blank. This option may be specified repeatedly", resourceFieldDelimiter, resourceFieldDelimiter))
command.Flags().StringVarP(&selector, "selector", "l", "", "Sync apps that match this label")
command.Flags().StringVarP(&selector, "selector", "l", "", "Sync apps that match this label. Supports '=', '==', '!=', in, notin, exists & not exists. Matching apps must satisfy all of the specified label constraints.")
command.Flags().StringArrayVar(&labels, "label", []string{}, "Sync only specific resources with a label. This option may be specified repeatedly.")
command.Flags().UintVar(&timeout, "timeout", defaultCheckTimeoutSeconds, "Time out after this many seconds")
command.Flags().Int64Var(&retryLimit, "retry-limit", 0, "Max number of allowed sync retries")
Expand Down
17 changes: 11 additions & 6 deletions docs/user-guide/commands/argocd_app_delete.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,18 @@ argocd app delete APPNAME [flags]
### Examples

```
argocd app delete app1
# Delete an app
argocd app delete my-app
# Delete multiple apps
argocd app delete app1 app2
# Delete multiple apps
argocd app delete my-app other-app
# Delete apps by label
argocd app delete -l foo=bar
# Delete apps by label
argocd app delete -l app.kubernetes.io/instance=my-app
argocd app delete -l app.kubernetes.io/instance!=my-app
argocd app delete -l app.kubernetes.io/instance
argocd app delete -l '!app.kubernetes.io/instance'
argocd app delete -l 'app.kubernetes.io/instance notin (my-app,other-app)'
```

### Options
Expand All @@ -24,7 +29,7 @@ argocd app delete -l foo=bar
--cascade Perform a cascaded deletion of all application resources (default true)
-h, --help help for delete
-p, --propagation-policy string Specify propagation policy for deletion of application's resources. One of: foreground|background (default "foreground")
-l, --selector string Delete all apps with matching label
-l, --selector string Delete all apps with matching label. Supports '=', '==', '!=', in, notin, exists & not exists. Matching apps must satisfy all of the specified label constraints.
-y, --yes Turn off prompting to confirm cascaded deletion of application resources
```

Expand Down
6 changes: 5 additions & 1 deletion docs/user-guide/commands/argocd_app_list.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ argocd app list [flags]
# List apps by label, in this example we listing apps that are children of another app (aka app-of-apps)
argocd app list -l app.kubernetes.io/instance=my-app
argocd app list -l app.kubernetes.io/instance!=my-app
argocd app list -l app.kubernetes.io/instance
argocd app list -l '!app.kubernetes.io/instance'
argocd app list -l 'app.kubernetes.io/instance notin (my-app,other-app)'
```

### Options
Expand All @@ -25,7 +29,7 @@ argocd app list [flags]
-o, --output string Output format. One of: wide|name|json|yaml (default "wide")
-p, --project stringArray Filter by project name
-r, --repo string List apps by source repo URL
-l, --selector string List apps by label
-l, --selector string List apps by label. Supports '=', '==', '!=', in, notin, exists & not exists. Matching apps must satisfy all of the specified label constraints.
```

### Options inherited from parent commands
Expand Down
6 changes: 5 additions & 1 deletion docs/user-guide/commands/argocd_app_sync.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ argocd app sync [APPNAME... | -l selector | --project project-name] [flags]
# Sync apps by label, in this example we sync apps that are children of another app (aka app-of-apps)
argocd app sync -l app.kubernetes.io/instance=my-app
argocd app sync -l app.kubernetes.io/instance!=my-app
argocd app sync -l app.kubernetes.io/instance
argocd app sync -l '!app.kubernetes.io/instance'
argocd app sync -l 'app.kubernetes.io/instance notin (my-app,other-app)'
# Sync a specific resource
# Resource should be formatted as GROUP:KIND:NAME. If no GROUP is specified then :KIND:NAME
Expand Down Expand Up @@ -48,7 +52,7 @@ argocd app sync [APPNAME... | -l selector | --project project-name] [flags]
--retry-backoff-max-duration duration Max retry backoff duration. Input needs to be a duration (e.g. 2m, 1h) (default 3m0s)
--retry-limit int Max number of allowed sync retries
--revision string Sync to a specific revision. Preserves parameter overrides
-l, --selector string Sync apps that match this label
-l, --selector string Sync apps that match this label. Supports '=', '==', '!=', in, notin, exists & not exists. Matching apps must satisfy all of the specified label constraints.
--server-side Use server-side apply while syncing the application
--strategy string Sync strategy (one of: apply|hook)
--timeout uint Time out after this many seconds
Expand Down
8 changes: 6 additions & 2 deletions docs/user-guide/commands/argocd_app_wait.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ argocd app wait [APPNAME.. | -l selector] [flags]
argocd app wait my-app other-app
# Wait for apps by label, in this example we waiting for apps that are children of another app (aka app-of-apps)
argocd app wait -l app.kubernetes.io/instance=apps
argocd app wait -l app.kubernetes.io/instance=my-app
argocd app wait -l app.kubernetes.io/instance!=my-app
argocd app wait -l app.kubernetes.io/instance
argocd app wait -l '!app.kubernetes.io/instance'
argocd app wait -l 'app.kubernetes.io/instance notin (my-app,other-app)'
```

### Options
Expand All @@ -27,7 +31,7 @@ argocd app wait [APPNAME.. | -l selector] [flags]
-h, --help help for wait
--operation Wait for pending operations
--resource stringArray Sync only specific resources as GROUP:KIND:NAME. Fields may be blank. This option may be specified repeatedly
-l, --selector string Wait for apps by label
-l, --selector string Wait for apps by label. Supports '=', '==', '!=', in, notin, exists & not exists. Matching apps must satisfy all of the specified label constraints.
--suspended Wait for suspended
--sync Wait for sync
--timeout uint Time out after this many seconds
Expand Down
8 changes: 4 additions & 4 deletions server/application/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,15 +132,15 @@ func NewServer(

// List returns list of applications
func (s *Server) List(ctx context.Context, q *application.ApplicationQuery) (*appv1.ApplicationList, error) {
labelsMap, err := labels.ConvertSelectorToLabelsMap(q.GetSelector())
selector, err := labels.Parse(q.GetSelector())
if err != nil {
return nil, fmt.Errorf("error converting selector to labels map: %w", err)
return nil, fmt.Errorf("error parsing the selector: %w", err)
}
var apps []*appv1.Application
if q.GetAppNamespace() == "" {
apps, err = s.appLister.List(labelsMap.AsSelector())
apps, err = s.appLister.List(selector)
} else {
apps, err = s.appLister.Applications(q.GetAppNamespace()).List(labelsMap.AsSelector())
apps, err = s.appLister.Applications(q.GetAppNamespace()).List(selector)
}
if err != nil {
return nil, fmt.Errorf("error listing apps with selectors: %w", err)
Expand Down
100 changes: 100 additions & 0 deletions server/application/application_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,106 @@ func createTestApp(testApp string, opts ...func(app *appsv1.Application)) *appsv
return &app
}

func TestListAppsInNamespaceWithLabels(t *testing.T) {
appServer := newTestAppServer(newTestApp(func(app *appsv1.Application) {
app.Name = "App1"
app.ObjectMeta.Namespace = "test-namespace"
app.SetLabels(map[string]string{"key1": "value1", "key2": "value1"})
}), newTestApp(func(app *appsv1.Application) {
app.Name = "App2"
app.ObjectMeta.Namespace = "test-namespace"
app.SetLabels(map[string]string{"key1": "value2"})
}), newTestApp(func(app *appsv1.Application) {
app.Name = "App3"
app.ObjectMeta.Namespace = "test-namespace"
app.SetLabels(map[string]string{"key1": "value3"})
}))
appServer.ns = "test-namespace"
appQuery := application.ApplicationQuery{}
namespace := "test-namespace"
appQuery.AppNamespace = &namespace
testListAppsWithLabels(t, appQuery, appServer)
}

func TestListAppsInDefaultNSWithLabels(t *testing.T) {
appServer := newTestAppServer(newTestApp(func(app *appsv1.Application) {
app.Name = "App1"
app.SetLabels(map[string]string{"key1": "value1", "key2": "value1"})
}), newTestApp(func(app *appsv1.Application) {
app.Name = "App2"
app.SetLabels(map[string]string{"key1": "value2"})
}), newTestApp(func(app *appsv1.Application) {
app.Name = "App3"
app.SetLabels(map[string]string{"key1": "value3"})
}))
appQuery := application.ApplicationQuery{}
testListAppsWithLabels(t, appQuery, appServer)
}

func testListAppsWithLabels(t *testing.T, appQuery application.ApplicationQuery, appServer *Server) {
validTests := []struct {
testName string
label string
expectedResult []string
}{
{testName: "Equality based filtering using '=' operator",
label: "key1=value1",
expectedResult: []string{"App1"}},
{testName: "Equality based filtering using '==' operator",
label: "key1==value1",
expectedResult: []string{"App1"}},
{testName: "Equality based filtering using '!=' operator",
label: "key1!=value1",
expectedResult: []string{"App2", "App3"}},
{testName: "Set based filtering using 'in' operator",
label: "key1 in (value1, value3)",
expectedResult: []string{"App1", "App3"}},
{testName: "Set based filtering using 'notin' operator",
label: "key1 notin (value1, value3)",
expectedResult: []string{"App2"}},
{testName: "Set based filtering using 'exists' operator",
label: "key1",
expectedResult: []string{"App1", "App2", "App3"}},
{testName: "Set based filtering using 'not exists' operator",
label: "!key2",
expectedResult: []string{"App2", "App3"}},
}
//test valid scenarios
for _, validTest := range validTests {
t.Run(validTest.testName, func(t *testing.T) {
appQuery.Selector = &validTest.label
res, err := appServer.List(context.Background(), &appQuery)
assert.NoError(t, err)
apps := []string{}
for i := range res.Items {
apps = append(apps, res.Items[i].Name)
}
assert.Equal(t, validTest.expectedResult, apps)
})
}

invalidTests := []struct {
testName string
label string
errorMesage string
}{
{testName: "Set based filtering using '>' operator",
label: "key1>value1",
errorMesage: "error parsing the selector"},
{testName: "Set based filtering using '<' operator",
label: "key1<value1",
errorMesage: "error parsing the selector"},
}
//test invalid scenarios
for _, invalidTest := range invalidTests {
t.Run(invalidTest.testName, func(t *testing.T) {
appQuery.Selector = &invalidTest.label
_, err := appServer.List(context.Background(), &appQuery)
assert.ErrorContains(t, err, invalidTest.errorMesage)
})
}
}

func TestListApps(t *testing.T) {
appServer := newTestAppServer(newTestApp(func(app *appsv1.Application) {
app.Name = "bcd"
Expand Down

0 comments on commit a8e2fb9

Please sign in to comment.