Skip to content

Commit

Permalink
feat: inversion selection support for the resource filter on sync and…
Browse files Browse the repository at this point in the history
… wait app commands (argoproj#10548)

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

Signed-off-by: maheshbaliga <mahesh.baliga@infracloud.io>
Signed-off-by: emirot <emirot.nolan@gmail.com>
  • Loading branch information
maheshbaliga authored and emirot committed Jan 27, 2023
1 parent 1eb3ccb commit 7069a75
Show file tree
Hide file tree
Showing 9 changed files with 462 additions and 14 deletions.
63 changes: 53 additions & 10 deletions cmd/argocd/commands/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -1376,6 +1376,7 @@ const (
resourceFieldCount = 3
resourceFieldNamespaceDelimiter = "/"
resourceFieldNameWithNamespaceCount = 2
resourceExcludeIndicator = "!"
)

// resource is GROUP:KIND:NAMESPACE/NAME or GROUP:KIND:NAME
Expand All @@ -1400,6 +1401,12 @@ func parseSelectedResources(resources []string) ([]*argoappv1.SyncOperationResou
}

for _, resource := range resources {
isExcluded := false
// check if the resource flag starts with a '!'
if strings.HasPrefix(resource, resourceExcludeIndicator) {
resource = strings.TrimPrefix(resource, resourceExcludeIndicator)
isExcluded = true
}
fields := strings.Split(resource, resourceFieldDelimiter)
if len(fields) != resourceFieldCount {
return nil, fmt.Errorf("Resource should have GROUP%sKIND%sNAME, but instead got: %s", resourceFieldDelimiter, resourceFieldDelimiter, resource)
Expand All @@ -1413,6 +1420,7 @@ func parseSelectedResources(resources []string) ([]*argoappv1.SyncOperationResou
Kind: fields[1],
Name: name,
Namespace: namespace,
Exclude: isExcluded,
})
}
return selectedResources, nil
Expand Down Expand Up @@ -1447,6 +1455,16 @@ func NewApplicationWaitCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
# Wait for multiple apps
argocd app wait my-app other-app
# Wait for apps by resource
# Resource should be formatted as GROUP:KIND:NAME. If no GROUP is specified then :KIND:NAME.
argocd app wait my-app --resource :Service:my-service
argocd app wait my-app --resource argoproj.io:Rollout:my-rollout
argocd app wait my-app --resource '!apps:Deployment:my-service'
argocd app wait my-app --resource apps:Deployment:my-service --resource :Service:my-service
argocd app wait my-app --resource '!*:Service:*'
# Specify namespace if the application has resources with the same name in different namespaces
argocd app wait my-app --resource argoproj.io:Rollout:my-namespace/my-rollout
# 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=my-app
argocd app wait -l app.kubernetes.io/instance!=my-app
Expand Down Expand Up @@ -1485,7 +1503,7 @@ func NewApplicationWaitCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
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. 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().StringArrayVar(&resources, "resource", []string{}, fmt.Sprintf("Sync only specific resources as GROUP%[1]sKIND%[1]sNAME or %[2]sGROUP%[1]sKIND%[1]sNAME. Fields may be blank and '*' can be used. This option may be specified repeatedly", resourceFieldDelimiter, resourceExcludeIndicator))
command.Flags().BoolVar(&watch.operation, "operation", false, "Wait for pending operations")
command.Flags().UintVar(&timeout, "timeout", defaultCheckTimeoutSeconds, "Time out after this many seconds")
return command
Expand Down Expand Up @@ -1545,6 +1563,9 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
# Resource should be formatted as GROUP:KIND:NAME. If no GROUP is specified then :KIND:NAME
argocd app sync my-app --resource :Service:my-service
argocd app sync my-app --resource argoproj.io:Rollout:my-rollout
argocd app sync my-app --resource '!apps:Deployment:my-service'
argocd app sync my-app --resource apps:Deployment:my-service --resource :Service:my-service
argocd app sync my-app --resource '!*:Service:*'
# Specify namespace if the application has resources with the same name in different namespaces
argocd app sync my-app --resource argoproj.io:Rollout:my-namespace/my-rollout`,
Run: func(c *cobra.Command, args []string) {
Expand Down Expand Up @@ -1640,6 +1661,14 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
return
}

// filters out only those resources that needs to be synced
filteredResources := filterAppResources(app, selectedResources)

// if resources are provided and no app resources match, then return error
if len(resources) > 0 && len(filteredResources) == 0 {
log.Fatalf("No matching app resources found for resource filter: %v", strings.Join(resources, ", "))
}

if local != "" {
if app.Spec.GetSource().Plugin != nil && app.Spec.GetSource().Plugin.Name != "" {
log.Warnf(argocommon.ConfigMapPluginCLIDeprecationWarning)
Expand Down Expand Up @@ -1690,7 +1719,7 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
AppNamespace: &appNs,
DryRun: &dryRun,
Revision: &revision,
Resources: selectedResources,
Resources: filteredResources,
Prune: &prune,
Manifests: localObjsStrings,
Infos: getInfos(infos),
Expand Down Expand Up @@ -1770,7 +1799,7 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
command.Flags().BoolVar(&dryRun, "dry-run", false, "Preview apply without affecting cluster")
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().StringArrayVar(&resources, "resource", []string{}, fmt.Sprintf("Sync only specific resources as GROUP%[1]sKIND%[1]sNAME or %[2]sGROUP%[1]sKIND%[1]sNAME. Fields may be blank and '*' can be used. This option may be specified repeatedly", resourceFieldDelimiter, resourceExcludeIndicator))
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")
Expand Down Expand Up @@ -1895,22 +1924,36 @@ func getResourceStates(app *argoappv1.Application, selectedResources []*argoappv
}
// filter out not selected resources
if len(selectedResources) > 0 {
r := []argoappv1.SyncOperationResource{}
for _, res := range selectedResources {
if res != nil {
r = append(r, *res)
}
}
for i := len(states) - 1; i >= 0; i-- {
res := states[i]
if !argo.ContainsSyncResource(res.Name, res.Namespace, schema.GroupVersionKind{Group: res.Group, Kind: res.Kind}, r) {
if !argo.IncludeResource(res.Name, res.Namespace, schema.GroupVersionKind{Group: res.Group, Kind: res.Kind}, selectedResources) {
states = append(states[:i], states[i+1:]...)
}
}
}
return states
}

// filterAppResources selects the app resources that match atleast one of the resource filters.
func filterAppResources(app *argoappv1.Application, selectedResources []*argoappv1.SyncOperationResource) []*argoappv1.SyncOperationResource {
var filteredResources []*argoappv1.SyncOperationResource
if app != nil && len(selectedResources) > 0 {
for i := range app.Status.Resources {
appResource := app.Status.Resources[i]
if (argo.IncludeResource(appResource.Name, appResource.Namespace,
schema.GroupVersionKind{Group: appResource.Group, Kind: appResource.Kind}, selectedResources)) {
filteredResources = append(filteredResources, &argoappv1.SyncOperationResource{
Group: appResource.Group,
Kind: appResource.Kind,
Name: appResource.Name,
Namespace: appResource.Namespace,
})
}
}
}
return filteredResources
}

func groupResourceStates(app *argoappv1.Application, selectedResources []*argoappv1.SyncOperationResource) map[string]*resourceState {
resStates := make(map[string]*resourceState)
for _, result := range getResourceStates(app, selectedResources) {
Expand Down
234 changes: 232 additions & 2 deletions cmd/argocd/commands/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -904,11 +904,220 @@ func Test_unset_nothingToUnset(t *testing.T) {
}
}

func TestFilterAppResources(t *testing.T) {
// App resources
var (
appReplicaSet1 = v1alpha1.ResourceStatus{
Group: "apps",
Kind: "ReplicaSet",
Namespace: "default",
Name: "replicaSet-name1",
}
appReplicaSet2 = v1alpha1.ResourceStatus{
Group: "apps",
Kind: "ReplicaSet",
Namespace: "default",
Name: "replicaSet-name2",
}
appJob = v1alpha1.ResourceStatus{
Group: "batch",
Kind: "Job",
Namespace: "default",
Name: "job-name",
}
appService1 = v1alpha1.ResourceStatus{
Group: "",
Kind: "Service",
Namespace: "default",
Name: "service-name1",
}
appService2 = v1alpha1.ResourceStatus{
Group: "",
Kind: "Service",
Namespace: "default",
Name: "service-name2",
}
appDeployment = v1alpha1.ResourceStatus{
Group: "apps",
Kind: "Deployment",
Namespace: "default",
Name: "deployment-name",
}
)
app := v1alpha1.Application{
Status: v1alpha1.ApplicationStatus{
Resources: []v1alpha1.ResourceStatus{
appReplicaSet1, appReplicaSet2, appJob, appService1, appService2, appDeployment},
},
}
// Resource filters
var (
blankValues = argoappv1.SyncOperationResource{
Group: "",
Kind: "",
Name: "",
Namespace: "",
Exclude: false}
// *:*:*
includeAllResources = argoappv1.SyncOperationResource{
Group: "*",
Kind: "*",
Name: "*",
Namespace: "",
Exclude: false}
// !*:*:*
excludeAllResources = argoappv1.SyncOperationResource{
Group: "*",
Kind: "*",
Name: "*",
Namespace: "",
Exclude: true}
// *:Service:*
includeAllServiceResources = argoappv1.SyncOperationResource{
Group: "*",
Kind: "Service",
Name: "*",
Namespace: "",
Exclude: false}
// !*:Service:*
excludeAllServiceResources = argoappv1.SyncOperationResource{
Group: "*",
Kind: "Service",
Name: "*",
Namespace: "",
Exclude: true}
// apps:ReplicaSet:replicaSet-name1
includeReplicaSet1Resource = argoappv1.SyncOperationResource{
Group: "apps",
Kind: "ReplicaSet",
Name: "replicaSet-name1",
Namespace: "",
Exclude: false}
// !apps:ReplicaSet:replicaSet-name2
excludeReplicaSet2Resource = argoappv1.SyncOperationResource{
Group: "apps",
Kind: "ReplicaSet",
Name: "replicaSet-name2",
Namespace: "",
Exclude: true}
)

// Filtered resources
var (
replicaSet1 = v1alpha1.SyncOperationResource{
Group: "apps",
Kind: "ReplicaSet",
Namespace: "default",
Name: "replicaSet-name1",
}
replicaSet2 = v1alpha1.SyncOperationResource{
Group: "apps",
Kind: "ReplicaSet",
Namespace: "default",
Name: "replicaSet-name2",
}
job = v1alpha1.SyncOperationResource{
Group: "batch",
Kind: "Job",
Namespace: "default",
Name: "job-name",
}
service1 = v1alpha1.SyncOperationResource{
Group: "",
Kind: "Service",
Namespace: "default",
Name: "service-name1",
}
service2 = v1alpha1.SyncOperationResource{
Group: "",
Kind: "Service",
Namespace: "default",
Name: "service-name2",
}
deployment = v1alpha1.SyncOperationResource{
Group: "apps",
Kind: "Deployment",
Namespace: "default",
Name: "deployment-name",
}
)
tests := []struct {
testName string
selectedResources []*argoappv1.SyncOperationResource
expectedResult []*argoappv1.SyncOperationResource
}{
//--resource apps:ReplicaSet:replicaSet-name1 --resource *:Service:*
{testName: "Include ReplicaSet replicaSet-name1 resouce and all service resources",
selectedResources: []*argoappv1.SyncOperationResource{&includeAllServiceResources, &includeReplicaSet1Resource},
expectedResult: []*argoappv1.SyncOperationResource{&replicaSet1, &service1, &service2},
},
//--resource apps:ReplicaSet:replicaSet-name1 --resource !*:Service:*
{testName: "Include ReplicaSet replicaSet-name1 resouce and exclude all service resources",
selectedResources: []*argoappv1.SyncOperationResource{&excludeAllServiceResources, &includeReplicaSet1Resource},
expectedResult: []*argoappv1.SyncOperationResource{&replicaSet1, &replicaSet2, &job, &deployment},
},
// --resource !apps:ReplicaSet:replicaSet-name2 --resource !*:Service:*
{testName: "Exclude ReplicaSet replicaSet-name2 resouce and all service resources",
selectedResources: []*argoappv1.SyncOperationResource{&excludeReplicaSet2Resource, &excludeAllServiceResources},
expectedResult: []*argoappv1.SyncOperationResource{&replicaSet1, &replicaSet2, &job, &service1, &service2, &deployment},
},
// --resource !apps:ReplicaSet:replicaSet-name2
{testName: "Exclude ReplicaSet replicaSet-name2 resouce",
selectedResources: []*argoappv1.SyncOperationResource{&excludeReplicaSet2Resource},
expectedResult: []*argoappv1.SyncOperationResource{&replicaSet1, &job, &service1, &service2, &deployment},
},
// --resource apps:ReplicaSet:replicaSet-name1
{testName: "Include ReplicaSet replicaSet-name1 resouce",
selectedResources: []*argoappv1.SyncOperationResource{&includeReplicaSet1Resource},
expectedResult: []*argoappv1.SyncOperationResource{&replicaSet1},
},
// --resource !*:Service:*
{testName: "Exclude Service resouces",
selectedResources: []*argoappv1.SyncOperationResource{&excludeAllServiceResources},
expectedResult: []*argoappv1.SyncOperationResource{&replicaSet1, &replicaSet2, &job, &deployment},
},
// --resource *:Service:*
{testName: "Include Service resouces",
selectedResources: []*argoappv1.SyncOperationResource{&includeAllServiceResources},
expectedResult: []*argoappv1.SyncOperationResource{&service1, &service2},
},
// --resource !*:*:*
{testName: "Exclude all resouces",
selectedResources: []*argoappv1.SyncOperationResource{&excludeAllResources},
expectedResult: nil,
},
// --resource *:*:*
{testName: "Include all resouces",
selectedResources: []*argoappv1.SyncOperationResource{&includeAllResources},
expectedResult: []*argoappv1.SyncOperationResource{&replicaSet1, &replicaSet2, &job, &service1, &service2, &deployment},
},
{testName: "No Filters",
selectedResources: []*argoappv1.SyncOperationResource{&blankValues},
expectedResult: nil,
},
{testName: "Empty Filter",
selectedResources: []*argoappv1.SyncOperationResource{},
expectedResult: nil,
},
}

for _, test := range tests {
t.Run(test.testName, func(t *testing.T) {
filteredResources := filterAppResources(&app, test.selectedResources)
assert.Equal(t, test.expectedResult, filteredResources)
})
}
}

func TestParseSelectedResources(t *testing.T) {
resources := []string{"v1alpha:Application:test", "v1alpha:Application:namespace/test"}
resources := []string{"v1alpha:Application:test",
"v1alpha:Application:namespace/test",
"!v1alpha:Application:test",
"apps:Deployment:default/test",
"!*:*:*"}
operationResources, err := parseSelectedResources(resources)
assert.NoError(t, err)
assert.Len(t, operationResources, 2)
assert.Len(t, operationResources, 5)
assert.Equal(t, *operationResources[0], v1alpha1.SyncOperationResource{
Namespace: "",
Name: "test",
Expand All @@ -921,6 +1130,27 @@ func TestParseSelectedResources(t *testing.T) {
Kind: "Application",
Group: "v1alpha",
})
assert.Equal(t, *operationResources[2], v1alpha1.SyncOperationResource{
Namespace: "",
Name: "test",
Kind: "Application",
Group: "v1alpha",
Exclude: true,
})
assert.Equal(t, *operationResources[3], v1alpha1.SyncOperationResource{
Namespace: "default",
Name: "test",
Kind: "Deployment",
Group: "apps",
Exclude: false,
})
assert.Equal(t, *operationResources[4], v1alpha1.SyncOperationResource{
Namespace: "",
Name: "*",
Kind: "*",
Group: "*",
Exclude: true,
})
}

func TestParseSelectedResourcesIncorrect(t *testing.T) {
Expand Down

0 comments on commit 7069a75

Please sign in to comment.