diff --git a/cmd/flux/get.go b/cmd/flux/get.go index 53c4721f..09fb7da9 100644 --- a/cmd/flux/get.go +++ b/cmd/flux/get.go @@ -78,7 +78,7 @@ func init() { getCmd.PersistentFlags().BoolVarP(&getArgs.noHeader, "no-header", "", false, "skip the header when printing the results") getCmd.PersistentFlags().BoolVarP(&getArgs.watch, "watch", "w", false, "After listing/getting the requested object, watch for changes.") getCmd.PersistentFlags().StringVar(&getArgs.statusSelector, "status-selector", "", - "specify the status condition name and the desired state to filter the get result, e.g. ready=false") + "specify the status condition name and the desired state to filter the get result, e.g. ready=false or ready!=true") getCmd.PersistentFlags().StringVarP(&getArgs.labelSelector, "label-selector", "l", "", "filter objects by label selector") rootCmd.AddCommand(getCmd) @@ -228,20 +228,31 @@ func namespaceNameOrAny(allNamespaces bool, namespaceName string) string { } func getRowsToPrint(getAll bool, list summarisable) ([][]string, error) { - noFilter := true + filter := func(i int) bool { return true } var conditionType, conditionStatus string if getArgs.statusSelector != "" { - parts := strings.SplitN(getArgs.statusSelector, "=", 2) + // Support both type=status (match) and type!=status (negated match). + // "!=" must be checked first since it also contains "=". + separator := "=" + filter = func(i int) bool { + return list.statusSelectorMatches(i, conditionType, conditionStatus) + } + if strings.Contains(getArgs.statusSelector, "!=") { + separator = "!=" + filter = func(i int) bool { + return !list.statusSelectorMatches(i, conditionType, conditionStatus) + } + } + parts := strings.SplitN(getArgs.statusSelector, separator, 2) if len(parts) != 2 { - return nil, fmt.Errorf("expected status selector in type=status format, but found: %s", getArgs.statusSelector) + return nil, fmt.Errorf("expected status selector in type=status or type!=status format, but found: %s", getArgs.statusSelector) } conditionType = parts[0] conditionStatus = parts[1] - noFilter = false } var rows [][]string for i := 0; i < list.len(); i++ { - if noFilter || list.statusSelectorMatches(i, conditionType, conditionStatus) { + if filter(i) { row := list.summariseItem(i, getArgs.allNamespaces, getAll) rows = append(rows, row) } diff --git a/cmd/flux/get_test.go b/cmd/flux/get_test.go index 30d74ab2..7376ff04 100644 --- a/cmd/flux/get_test.go +++ b/cmd/flux/get_test.go @@ -63,6 +63,46 @@ func Test_GetCmd(t *testing.T) { } } +func Test_GetCmdStatusSelector(t *testing.T) { + tmpl := map[string]string{ + "fluxns": allocateNamespace("flux-system"), + } + testEnv.CreateObjectFile("./testdata/get/status_objects.yaml", tmpl, t) + + tests := []struct { + name string + args string + expected string + }{ + { + name: "equal status selector matches one", + args: "--status-selector Ready=True", + expected: "testdata/get/get_status_ready_true.golden", + }, + { + name: "equal status selector matches false", + args: "--status-selector Ready=False", + expected: "testdata/get/get_status_ready_false.golden", + }, + { + name: "not-equal status selector matches all not-true", + args: "--status-selector Ready!=True", + expected: "testdata/get/get_status_ready_not_true.golden", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := cmdTestCase{ + args: "get sources git " + tt.args + " -n " + tmpl["fluxns"], + assert: assertGoldenTemplateFile(tt.expected, nil), + } + + cmd.runTestCmd(t) + }) + } +} + func Test_GetCmdErrors(t *testing.T) { tmpl := map[string]string{ "fluxns": allocateNamespace("flux-system"), @@ -84,6 +124,11 @@ func Test_GetCmdErrors(t *testing.T) { args: "get helmrelease -n " + tmpl["fluxns"], assert: assertError(fmt.Sprintf("no HelmRelease objects found in \"%s\" namespace", tmpl["fluxns"])), }, + { + name: "malformed status selector", + args: "get sources git --status-selector Ready -n " + tmpl["fluxns"], + assert: assertError("expected status selector in type=status or type!=status format, but found: Ready"), + }, } for _, tt := range tests { diff --git a/cmd/flux/testdata/get/get_status_ready_false.golden b/cmd/flux/testdata/get/get_status_ready_false.golden new file mode 100644 index 00000000..326d4ef6 --- /dev/null +++ b/cmd/flux/testdata/get/get_status_ready_false.golden @@ -0,0 +1,2 @@ +NAME REVISION SUSPENDED READY MESSAGE +gr-failed False False failed to checkout and determine revision diff --git a/cmd/flux/testdata/get/get_status_ready_not_true.golden b/cmd/flux/testdata/get/get_status_ready_not_true.golden new file mode 100644 index 00000000..8c5c118c --- /dev/null +++ b/cmd/flux/testdata/get/get_status_ready_not_true.golden @@ -0,0 +1,3 @@ +NAME REVISION SUSPENDED READY MESSAGE +gr-failed False False failed to checkout and determine revision +gr-unknown False Unknown reconciliation in progress diff --git a/cmd/flux/testdata/get/get_status_ready_true.golden b/cmd/flux/testdata/get/get_status_ready_true.golden new file mode 100644 index 00000000..800b8e56 --- /dev/null +++ b/cmd/flux/testdata/get/get_status_ready_true.golden @@ -0,0 +1,2 @@ +NAME REVISION SUSPENDED READY MESSAGE +gr-ready main@sha1:696f056d False True Fetched revision: main@sha1:696f056d diff --git a/cmd/flux/testdata/get/status_objects.yaml b/cmd/flux/testdata/get/status_objects.yaml new file mode 100644 index 00000000..628e4cc7 --- /dev/null +++ b/cmd/flux/testdata/get/status_objects.yaml @@ -0,0 +1,71 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: {{ .fluxns }} +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: GitRepository +metadata: + name: gr-failed + namespace: {{ .fluxns }} +spec: + ref: + branch: main + secretRef: + name: flux-system + url: ssh://git@github.com/example/repo + interval: 5m +status: + conditions: + - lastTransitionTime: "2021-07-20T00:48:16Z" + message: 'failed to checkout and determine revision' + reason: GitOperationFailed + status: "False" + type: Ready +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: GitRepository +metadata: + name: gr-ready + namespace: {{ .fluxns }} +spec: + ref: + branch: main + secretRef: + name: flux-system + url: ssh://git@github.com/example/repo + interval: 5m +status: + artifact: + lastUpdateTime: "2021-08-01T04:28:42Z" + revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f + path: "example" + url: "example" + digest: sha1:696f056df216eea4f9401adbee0ff744d4df390f + conditions: + - lastTransitionTime: "2021-07-20T00:48:16Z" + message: 'Fetched revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f' + reason: GitOperationSucceed + status: "True" + type: Ready +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: GitRepository +metadata: + name: gr-unknown + namespace: {{ .fluxns }} +spec: + ref: + branch: main + secretRef: + name: flux-system + url: ssh://git@github.com/example/repo + interval: 5m +status: + conditions: + - lastTransitionTime: "2021-07-20T00:48:16Z" + message: 'reconciliation in progress' + reason: Progressing + status: "Unknown" + type: Ready