cmd: support type!=status in get --status-selector
`flux get --status-selector` only supported equality (`type=status`),
so finding objects that are not in a given state required multiple
invocations, e.g. listing everything that is not ready needed both
`Ready=False` and `Ready=Unknown`.
Add support for a negated selector `type!=status`. Since all resource
adapters delegate matching to the shared `statusMatches` helper and
filtering is centralised in `getRowsToPrint`, negation is implemented
purely in the parse/filter layer by inverting the match result. This
covers every resource type and the `--watch` path without touching the
per-resource adapters.
A missing condition is treated as not-matching by `statusMatches` (Flux
considers it "waiting to be reconciled"), so `Ready!=True` also surfaces
objects that have no Ready condition yet, i.e. the complete not-ready set:
flux get all -A --status-selector Ready!=True
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: 3uzbcqje <3uzbcqje@addy.to>
This commit is contained in:
+21
-6
@@ -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)
|
||||
@@ -230,10 +230,18 @@ func namespaceNameOrAny(allNamespaces bool, namespaceName string) string {
|
||||
func getRowsToPrint(getAll bool, list summarisable) ([][]string, error) {
|
||||
noFilter := true
|
||||
var conditionType, conditionStatus string
|
||||
var negate bool
|
||||
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 := "="
|
||||
if strings.Contains(getArgs.statusSelector, "!=") {
|
||||
separator = "!="
|
||||
negate = true
|
||||
}
|
||||
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]
|
||||
@@ -241,10 +249,17 @@ func getRowsToPrint(getAll bool, list summarisable) ([][]string, error) {
|
||||
}
|
||||
var rows [][]string
|
||||
for i := 0; i < list.len(); i++ {
|
||||
if noFilter || list.statusSelectorMatches(i, conditionType, conditionStatus) {
|
||||
row := list.summariseItem(i, getArgs.allNamespaces, getAll)
|
||||
rows = append(rows, row)
|
||||
if !noFilter {
|
||||
matches := list.statusSelectorMatches(i, conditionType, conditionStatus)
|
||||
if negate {
|
||||
matches = !matches
|
||||
}
|
||||
if !matches {
|
||||
continue
|
||||
}
|
||||
}
|
||||
row := list.summariseItem(i, getArgs.allNamespaces, getAll)
|
||||
rows = append(rows, row)
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
NAME REVISION SUSPENDED READY MESSAGE
|
||||
gr-failed False False failed to checkout and determine revision
|
||||
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
NAME REVISION SUSPENDED READY MESSAGE
|
||||
gr-ready main@sha1:696f056d False True Fetched revision: main@sha1:696f056d
|
||||
+71
@@ -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
|
||||
Reference in New Issue
Block a user