diff --git a/go.mod b/go.mod index 4c79d60..583b03d 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.20 require ( github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d + github.com/chanced/caps v1.0.1 github.com/dominikbraun/graph v0.23.0 github.com/fluxcd/flagger v1.29.0 github.com/gookit/color v1.5.2 @@ -11,7 +12,6 @@ require ( github.com/spf13/cobra v1.6.1 github.com/werf/logboek v0.5.5 golang.org/x/crypto v0.7.0 - golang.org/x/text v0.8.0 k8s.io/api v0.26.2 k8s.io/apimachinery v0.26.2 k8s.io/cli-runtime v0.26.2 @@ -60,6 +60,7 @@ require ( golang.org/x/oauth2 v0.5.0 // indirect golang.org/x/sys v0.6.0 // indirect golang.org/x/term v0.6.0 // indirect + golang.org/x/text v0.8.0 // indirect golang.org/x/time v0.3.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.28.1 // indirect diff --git a/go.sum b/go.sum index 3dbe0aa..e59ad6c 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ github.com/avelino/slugify v0.0.0-20180501145920-855f152bd774/go.mod h1:5wi5YYOp github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/chanced/caps v1.0.1 h1:B9LhH2mmAcNjn1argkddu9fRdU+VMrfomRoeXL1DYhg= +github.com/chanced/caps v1.0.1/go.mod h1:SJhRzeYLKJ3OmzyQXhdZ7Etj7lqqWoPtQ1zcSJRtQjs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= diff --git a/pkg/tracker/generic/ready_condition.go b/pkg/tracker/generic/ready_condition.go index ff6d596..02580ee 100644 --- a/pkg/tracker/generic/ready_condition.go +++ b/pkg/tracker/generic/ready_condition.go @@ -3,6 +3,7 @@ package generic import ( "fmt" + "github.com/samber/lo" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "github.com/werf/kubedog/pkg/tracker/indicators" @@ -10,40 +11,38 @@ import ( ) func NewResourceStatusIndicator(object *unstructured.Unstructured) (indicator *indicators.StringEqualConditionIndicator, humanJSONPath string, err error) { - var matchedJSONPath *ResourceStatusJSONPath - for _, readyJSONPath := range ResourceStatusJSONPathsByPriority { - result, found, err := utils.JSONPath(readyJSONPath.JSONPath, object.UnstructuredContent()) - if err != nil { - return nil, "", fmt.Errorf("jsonpath error: %w", err) - } else if !found { + groupKind := object.GroupVersionKind().GroupKind() + + var matchedCondition *ResourceStatusJSONPathCondition + for _, condition := range ResourceStatusJSONPathConditions { + if condition.GroupKind != nil && *condition.GroupKind != groupKind { continue } - var resultIsValidValue bool - for _, validValue := range append(readyJSONPath.PendingValues, readyJSONPath.ReadyValue, readyJSONPath.FailedValue) { - if result == validValue { - resultIsValidValue = true - break - } - } - if !resultIsValidValue { + currentValue, found, err := utils.JSONPath(condition.JSONPath, object.UnstructuredContent()) + if err != nil { + return nil, "", fmt.Errorf("jsonpath error: %w", err) + } else if !found { continue } - path := readyJSONPath - matchedJSONPath = &path - matchedJSONPath.CurrentValue = result + knownValues := lo.Union(condition.ReadyValues, condition.PendingValues, condition.FailedValues) - break + if lo.Contains(knownValues, currentValue) { + matchedCondition = condition + matchedCondition.CurrentValue = currentValue + break + } } - - if matchedJSONPath == nil { + if matchedCondition == nil { return nil, "", nil } - return &indicators.StringEqualConditionIndicator{ - Value: matchedJSONPath.CurrentValue, - TargetValue: matchedJSONPath.ReadyValue, - FailedValue: matchedJSONPath.FailedValue, - }, matchedJSONPath.HumanPath, nil + indicator = &indicators.StringEqualConditionIndicator{ + Value: matchedCondition.CurrentValue, + } + indicator.SetReady(lo.Contains(matchedCondition.ReadyValues, matchedCondition.CurrentValue)) + indicator.SetFailed(lo.Contains(matchedCondition.FailedValues, matchedCondition.CurrentValue)) + + return indicator, matchedCondition.HumanPath, nil } diff --git a/pkg/tracker/generic/resource_state_json_paths.go b/pkg/tracker/generic/resource_state_json_paths.go index eb19912..83b32de 100644 --- a/pkg/tracker/generic/resource_state_json_paths.go +++ b/pkg/tracker/generic/resource_state_json_paths.go @@ -2,128 +2,218 @@ package generic import ( "fmt" + "strings" - "golang.org/x/text/cases" - "golang.org/x/text/language" + "github.com/chanced/caps" + "github.com/samber/lo" + "k8s.io/apimachinery/pkg/runtime/schema" ) -var ResourceStatusJSONPathsByPriority []ResourceStatusJSONPath +var ResourceStatusJSONPathConditions []*ResourceStatusJSONPathCondition -type ResourceStatusJSONPath struct { +type ResourceStatusJSONPathCondition struct { + GroupKind *schema.GroupKind JSONPath string HumanPath string - ReadyValue string - FailedValue string + ReadyValues []string PendingValues []string - CurrentValue string + FailedValues []string + + CurrentValue string } func initResourceStatusJSONPathsByPriority() { - casers := []cases.Caser{cases.Lower(language.Und), cases.Title(language.Und)} - - for _, actionPackByPriority := range [][]string{ - {"ready", "success", "succeeded"}, - {"complete", "completed", "finished"}, - {"available"}, - {"running"}, - {"started", "initialized", "approved"}, - } { - for _, action := range actionPackByPriority { - for _, caser := range casers { - ResourceStatusJSONPathsByPriority = append(ResourceStatusJSONPathsByPriority, ResourceStatusJSONPath{ - JSONPath: fmt.Sprintf(`$.status.conditions[?(@.type==%q)].status`, caser.String(action)), - HumanPath: fmt.Sprintf("status.conditions[type=%s].status", caser.String(action)), - ReadyValue: caser.String("true"), - PendingValues: []string{caser.String("false"), caser.String("unknown")}, - }) - } - } - - for _, action := range actionPackByPriority { - for _, caser := range casers { - ResourceStatusJSONPathsByPriority = append(ResourceStatusJSONPathsByPriority, ResourceStatusJSONPath{ - JSONPath: `$.status.phase`, - HumanPath: "status.phase", - ReadyValue: caser.String(action), - PendingValues: []string{caser.String("pending"), caser.String("unknown")}, - FailedValue: caser.String("failed"), - }) - ResourceStatusJSONPathsByPriority = append(ResourceStatusJSONPathsByPriority, ResourceStatusJSONPath{ - JSONPath: `$.status.currentPhase`, - HumanPath: "status.currentPhase", - ReadyValue: caser.String(action), - PendingValues: []string{caser.String("pending"), caser.String("unknown")}, - FailedValue: caser.String("failed"), - }) - } - } - - for _, action := range actionPackByPriority { - for _, caser := range casers { - ResourceStatusJSONPathsByPriority = append(ResourceStatusJSONPathsByPriority, ResourceStatusJSONPath{ - JSONPath: `$.status.state`, - HumanPath: "status.state", - ReadyValue: caser.String(action), - PendingValues: []string{caser.String("pending"), caser.String("unknown")}, - FailedValue: caser.String("failed"), - }) - ResourceStatusJSONPathsByPriority = append(ResourceStatusJSONPathsByPriority, ResourceStatusJSONPath{ - JSONPath: `$.status.currentState`, - HumanPath: "status.currentState", - ReadyValue: caser.String(action), - PendingValues: []string{caser.String("pending"), caser.String("unknown")}, - FailedValue: caser.String("failed"), - }) - } - } - - for _, action := range actionPackByPriority { - for _, caser := range casers { - ResourceStatusJSONPathsByPriority = append(ResourceStatusJSONPathsByPriority, ResourceStatusJSONPath{ - JSONPath: `$.status.health`, - HumanPath: "status.health", - ReadyValue: caser.String(action), - PendingValues: []string{caser.String("pending"), caser.String("unknown")}, - FailedValue: caser.String("failed"), - }) - ResourceStatusJSONPathsByPriority = append(ResourceStatusJSONPathsByPriority, ResourceStatusJSONPath{ - JSONPath: `$.status.currentHealth`, - HumanPath: "status.currentHealth", - ReadyValue: caser.String(action), - PendingValues: []string{caser.String("pending"), caser.String("unknown")}, - FailedValue: caser.String("failed"), - }) - } - } + buildResourceSpecificConditions() + buildUniversalConditions() + buildLowPriorityConditions() +} + +func buildResourceSpecificConditions() { + ResourceStatusJSONPathConditions = append(ResourceStatusJSONPathConditions, &ResourceStatusJSONPathCondition{ + GroupKind: &schema.GroupKind{Group: "acid.zalan.do", Kind: "postgresql"}, + JSONPath: `$.status.PostgresClusterStatus`, + HumanPath: "status.PostgresClusterStatus", + ReadyValues: casify("Running"), + PendingValues: casify("Creating", "Updating"), + FailedValues: casify("CreateFailed", "UpdateFailed", "SyncFailed"), + }) +} + +func buildUniversalConditions() { + readyValuesByPriority := []string{ + "ready", + "success", + "succeeded", + "complete", + "completed", + "finished", + "finalized", + "done", + "available", + "running", + "ok", + "active", + "live", + "healthy", + "started", + "initialized", + "approved", } - for _, caser := range casers { - ResourceStatusJSONPathsByPriority = append(ResourceStatusJSONPathsByPriority, ResourceStatusJSONPath{ - JSONPath: `$.status.health`, - HumanPath: "status.health", - ReadyValue: caser.String("green"), - PendingValues: []string{caser.String("yellow"), caser.String("red"), caser.String("unknown")}, - }) - ResourceStatusJSONPathsByPriority = append(ResourceStatusJSONPathsByPriority, ResourceStatusJSONPath{ - JSONPath: `$.status.currentHealth`, - HumanPath: "status.currentHealth", - ReadyValue: caser.String("green"), - PendingValues: []string{caser.String("yellow"), caser.String("red"), caser.String("unknown")}, - }) + pendingValuesByPriority := []string{ + "creating", + "updating", + "waiting", + "awaiting", + "pending", + "finishing", + "starting", + "readying", + "in progress", + "progressing", + "initialization", + "initializing", + "approving", + "unknown", } - for _, caser := range casers { - ResourceStatusJSONPathsByPriority = append(ResourceStatusJSONPathsByPriority, ResourceStatusJSONPath{ - JSONPath: `$.status.state`, - HumanPath: "status.state", - ReadyValue: caser.String("valid"), - PendingValues: []string{caser.String("invalid"), caser.String("unknown")}, - }) - ResourceStatusJSONPathsByPriority = append(ResourceStatusJSONPathsByPriority, ResourceStatusJSONPath{ - JSONPath: `$.status.currentState`, - HumanPath: "status.currentState", - ReadyValue: caser.String("valid"), - PendingValues: []string{caser.String("invalid"), caser.String("unknown")}, + failedValuesByPriority := []string{ + "failure", + "failed", + "abort", + "aborted", + "terminated", + "error", + "errored", + "rejection", + "rejected", + } + + for _, readyValue := range readyValuesByPriority { + ResourceStatusJSONPathConditions = append(ResourceStatusJSONPathConditions, &ResourceStatusJSONPathCondition{ + JSONPath: fmt.Sprintf(`$.status.conditions[?(@.type==%q)].status`, casify(readyValue)[0]), + HumanPath: fmt.Sprintf("status.conditions[type=%s].status", casify(readyValue)[0]), + ReadyValues: casify("true"), + PendingValues: casify("false", "unknown"), }) } + + ResourceStatusJSONPathConditions = append(ResourceStatusJSONPathConditions, &ResourceStatusJSONPathCondition{ + JSONPath: `$.status.phase`, + HumanPath: "status.phase", + ReadyValues: casify(readyValuesByPriority...), + PendingValues: casify(pendingValuesByPriority...), + FailedValues: casify(failedValuesByPriority...), + }) + + ResourceStatusJSONPathConditions = append(ResourceStatusJSONPathConditions, &ResourceStatusJSONPathCondition{ + JSONPath: `$.status.currentPhase`, + HumanPath: "status.currentPhase", + ReadyValues: casify(readyValuesByPriority...), + PendingValues: casify(pendingValuesByPriority...), + FailedValues: casify(failedValuesByPriority...), + }) + + ResourceStatusJSONPathConditions = append(ResourceStatusJSONPathConditions, &ResourceStatusJSONPathCondition{ + JSONPath: `$.status.state`, + HumanPath: "status.state", + ReadyValues: casify(readyValuesByPriority...), + PendingValues: casify(pendingValuesByPriority...), + FailedValues: casify(failedValuesByPriority...), + }) + + ResourceStatusJSONPathConditions = append(ResourceStatusJSONPathConditions, &ResourceStatusJSONPathCondition{ + JSONPath: `$.status.currentState`, + HumanPath: "status.currentState", + ReadyValues: casify(readyValuesByPriority...), + PendingValues: casify(pendingValuesByPriority...), + FailedValues: casify(failedValuesByPriority...), + }) + + ResourceStatusJSONPathConditions = append(ResourceStatusJSONPathConditions, &ResourceStatusJSONPathCondition{ + JSONPath: `$.status.status`, + HumanPath: "status.status", + ReadyValues: casify(readyValuesByPriority...), + PendingValues: casify(pendingValuesByPriority...), + FailedValues: casify(failedValuesByPriority...), + }) + + ResourceStatusJSONPathConditions = append(ResourceStatusJSONPathConditions, &ResourceStatusJSONPathCondition{ + JSONPath: `$.status.currentStatus`, + HumanPath: "status.currentStatus", + ReadyValues: casify(readyValuesByPriority...), + PendingValues: casify(pendingValuesByPriority...), + FailedValues: casify(failedValuesByPriority...), + }) + + ResourceStatusJSONPathConditions = append(ResourceStatusJSONPathConditions, &ResourceStatusJSONPathCondition{ + JSONPath: `$.status.health`, + HumanPath: "status.health", + ReadyValues: casify(readyValuesByPriority...), + PendingValues: casify(pendingValuesByPriority...), + FailedValues: casify(failedValuesByPriority...), + }) + + ResourceStatusJSONPathConditions = append(ResourceStatusJSONPathConditions, &ResourceStatusJSONPathCondition{ + JSONPath: `$.status.currentHealth`, + HumanPath: "status.currentHealth", + ReadyValues: casify(readyValuesByPriority...), + PendingValues: casify(pendingValuesByPriority...), + FailedValues: casify(failedValuesByPriority...), + }) +} + +func buildLowPriorityConditions() { + ResourceStatusJSONPathConditions = append(ResourceStatusJSONPathConditions, &ResourceStatusJSONPathCondition{ + JSONPath: `$.status.state`, + HumanPath: "status.state", + ReadyValues: casify("valid"), + PendingValues: casify("invalid", "unknown"), + }) + + ResourceStatusJSONPathConditions = append(ResourceStatusJSONPathConditions, &ResourceStatusJSONPathCondition{ + JSONPath: `$.status.currentState`, + HumanPath: "status.currentState", + ReadyValues: casify("valid"), + PendingValues: casify("invalid", "unknown"), + }) + + ResourceStatusJSONPathConditions = append(ResourceStatusJSONPathConditions, &ResourceStatusJSONPathCondition{ + JSONPath: `$.status.health`, + HumanPath: "status.health", + ReadyValues: casify("green"), + PendingValues: casify("yellow", "red", "unknown"), + }) + + ResourceStatusJSONPathConditions = append(ResourceStatusJSONPathConditions, &ResourceStatusJSONPathCondition{ + JSONPath: `$.status.currentHealth`, + HumanPath: "status.currentHealth", + ReadyValues: casify("green"), + PendingValues: casify("yellow", "red", "unknown"), + }) +} + +func casify(in ...string) []string { + var result []string + + for _, value := range in { + result = append(result, value) + result = append(result, strings.ReplaceAll(value, " ", "")) + result = append(result, caps.ToUpper(strings.ReplaceAll(value, " ", ""))) + result = append(result, caps.ToCamel(value)) + result = append(result, caps.ToKebab(value)) + result = append(result, caps.ToDotNotation(value)) + result = append(result, caps.ToSnake(value)) + result = append(result, caps.ToTitle(value)) + result = append(result, caps.ToUpper(value)) + result = append(result, caps.ToLower(value)) + result = append(result, caps.ToLowerCamel(value)) + result = append(result, caps.ToScreamingDotNotation(value)) + result = append(result, caps.ToScreamingKebab(value)) + result = append(result, caps.ToScreamingSnake(value)) + } + + result = lo.Uniq(result) + + return result } diff --git a/pkg/tracker/indicators/indicators.go b/pkg/tracker/indicators/indicators.go index a936512..fdf352e 100644 --- a/pkg/tracker/indicators/indicators.go +++ b/pkg/tracker/indicators/indicators.go @@ -25,17 +25,36 @@ type StringEqualConditionIndicator struct { Value string TargetValue string FailedValue string + + forceReady *bool + forceFailed *bool } func (indicator *StringEqualConditionIndicator) IsProgressing(prevIndicator *StringEqualConditionIndicator) bool { return (prevIndicator != nil) && (indicator.Value != prevIndicator.Value) } +func (indicator *StringEqualConditionIndicator) SetReady(ready bool) { + indicator.forceReady = &ready +} + func (indicator *StringEqualConditionIndicator) IsReady() bool { + if indicator.forceReady != nil { + return *indicator.forceReady + } + return indicator.Value == indicator.TargetValue } +func (indicator *StringEqualConditionIndicator) SetFailed(failed bool) { + indicator.forceFailed = &failed +} + func (indicator *StringEqualConditionIndicator) IsFailed() bool { + if indicator.forceFailed != nil { + return *indicator.forceFailed + } + return indicator.Value == indicator.FailedValue } diff --git a/pkg/trackers/rollout/multitrack/multitrack_display.go b/pkg/trackers/rollout/multitrack/multitrack_display.go index d96a072..dd9045d 100644 --- a/pkg/trackers/rollout/multitrack/multitrack_display.go +++ b/pkg/trackers/rollout/multitrack/multitrack_display.go @@ -612,7 +612,7 @@ func (mt *multitracker) displayDeploymentsStatusProgress() { func (mt *multitracker) displayGenericsStatusProgress() { t := utils.NewTable([]float64{.43, .14, .43}...) t.SetWidth(logboek.Context(context.Background()).Streams().ContentWidth() - 1) - t.Header("RESOURCE", "NAMESPACE", "CONDITION: CURRENT (DESIRED)") + t.Header("RESOURCE", "NAMESPACE", "WATCHING FOR FIELD") var tableChangesCount int for _, resource := range mt.GenericResources { @@ -661,7 +661,7 @@ func (mt *multitracker) displayGenericsStatusProgress() { currentAndDesiredState = lastStatus.Indicator.FormatTableElem(lastPrintedStatusIndicator, indicators.FormatTableElemOptions{ ShowProgress: showProgress, DisableWarningColors: disableWarningColors, - WithTargetValue: true, + WithTargetValue: false, }) } } else {