diff --git a/apis/kustomize/kustomize_types.go b/apis/kustomize/kustomize_types.go index 3fe5417f..678b8b46 100644 --- a/apis/kustomize/kustomize_types.go +++ b/apis/kustomize/kustomize_types.go @@ -139,8 +139,8 @@ type CustomHealthCheck struct { // +required APIVersion string `json:"apiVersion"` // Kind of the custom resource under evaluation. - // +required - Kind string `json:"kind"` + // +optional + Kind string `json:"kind,omitempty"` HealthCheckExpressions `json:",inline"` } diff --git a/runtime/cel/status_reader.go b/runtime/cel/status_reader.go index 04d9eecc..ea00f938 100644 --- a/runtime/cel/status_reader.go +++ b/runtime/cel/status_reader.go @@ -45,6 +45,9 @@ func NewStatusReader(healthchecks []kustomize.CustomHealthCheck) (func(meta.REST evaluators := make(map[schema.GroupKind]*StatusEvaluator, len(healthchecks)) for i, hc := range healthchecks { gk := schema.FromAPIVersionAndKind(hc.APIVersion, hc.Kind).GroupKind() + if hc.Kind == "" { + gk = schema.GroupKind{Group: gk.Group} + } if _, ok := evaluators[gk]; ok { return nil, fmt.Errorf( "duplicate custom health check for GroupKind %s at healthchecks[%d]", gk.String(), i) @@ -67,8 +70,9 @@ func NewStatusReader(healthchecks []kustomize.CustomHealthCheck) (func(meta.REST // Supports returns true if the StatusReader supports the given GroupKind. func (g *StatusReader) Supports(gk schema.GroupKind) bool { - _, ok := g.evaluators[gk] - return ok + _, supportsGroup := g.evaluators[schema.GroupKind{Group: gk.Group}] + _, supportsKind := g.evaluators[gk] + return supportsGroup || supportsKind } // ReadStatus reads the status of the resource with the given metadata. @@ -104,9 +108,16 @@ func (g *StatusReader) ReadStatusForObject(ctx context.Context, reader engine.Cl } // genericStatusReader returns the underlying generic status reader. +// Callers must ensure Supports(gk) is true before invoking this; the lookup +// below assumes an evaluator exists and would panic otherwise. Gating is done +// in the callers (ReadStatus, ReadStatusForObject) so they can return errors. func (g *StatusReader) genericStatusReader(ctx context.Context, gk schema.GroupKind) engine.StatusReader { statusFunc := func(u *unstructured.Unstructured) (*status.Result, error) { - return g.evaluators[gk].Evaluate(ctx, u) + e, ok := g.evaluators[gk] + if !ok { + e, ok = g.evaluators[schema.GroupKind{Group: gk.Group}] + } + return e.Evaluate(ctx, u) } return kstatusreaders.NewGenericStatusReader(g.mapper, statusFunc) } diff --git a/runtime/cel/status_reader_test.go b/runtime/cel/status_reader_test.go index ff27b341..9fb0f179 100644 --- a/runtime/cel/status_reader_test.go +++ b/runtime/cel/status_reader_test.go @@ -69,6 +69,38 @@ func TestStatusReader_Supports(t *testing.T) { }, result: false, }, + { + name: "group-only healthcheck supports any kind in that group", + supportedGK: schema.GroupKind{ + Group: "test", + }, + gk: schema.GroupKind{ + Group: "test", + Kind: "AnyKind", + }, + result: true, + }, + { + name: "group-only healthcheck does not support other groups", + supportedGK: schema.GroupKind{ + Group: "test", + }, + gk: schema.GroupKind{ + Group: "other", + Kind: "AnyKind", + }, + result: false, + }, + { + name: "group-only healthcheck supports GK with empty Kind in that group", + supportedGK: schema.GroupKind{ + Group: "test", + }, + gk: schema.GroupKind{ + Group: "test", + }, + result: true, + }, } { t.Run(tt.name, func(t *testing.T) { t.Parallel() @@ -239,6 +271,93 @@ func TestStatusReader_ReadStatusForObject(t *testing.T) { } } +func TestStatusReader_ReadStatusForObject_GroupOnlyHealthCheck(t *testing.T) { + g := NewWithT(t) + + // Register a single group-only healthcheck (empty Kind) for group "bitnami.com". + // It should apply to any Kind in that group. + ctor, err := cel.NewStatusReader([]kustomize.CustomHealthCheck{{ + APIVersion: "bitnami.com/v1alpha1", + HealthCheckExpressions: kustomize.HealthCheckExpressions{ + Current: "data.current", + }, + }}) + g.Expect(err).NotTo(HaveOccurred()) + + sr := ctor(nil) + + for _, kind := range []string{"SealedSecret", "AnotherKind"} { + result, err := sr.ReadStatusForObject(context.Background(), nil, &unstructured.Unstructured{ + Object: map[string]any{ + "apiVersion": "bitnami.com/v1alpha1", + "kind": kind, + "data": map[string]any{ + "current": true, + }, + }, + }) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(result.Status).To(Equal(status.CurrentStatus)) + } + + // A resource from a different group must not be supported. + _, err = sr.ReadStatusForObject(context.Background(), nil, &unstructured.Unstructured{ + Object: map[string]any{ + "apiVersion": "other.com/v1", + "kind": "Foo", + "data": map[string]any{"current": true}, + }, + }) + g.Expect(err).To(MatchError(ContainSubstring("the GroupKind Foo.other.com is not supported"))) +} + +func TestStatusReader_ReadStatusForObject_SpecificKindOverridesGroupOnly(t *testing.T) { + g := NewWithT(t) + + // Register a group-only healthcheck that would always return Failed, + // plus a specific-kind healthcheck that returns Current. The specific + // one must take precedence for its Kind. + ctor, err := cel.NewStatusReader([]kustomize.CustomHealthCheck{ + { + APIVersion: "bitnami.com/v1alpha1", + HealthCheckExpressions: kustomize.HealthCheckExpressions{ + Failed: "true", + Current: "false", + }, + }, + { + APIVersion: "bitnami.com/v1alpha1", + Kind: "SealedSecret", + HealthCheckExpressions: kustomize.HealthCheckExpressions{ + Current: "true", + }, + }, + }) + g.Expect(err).NotTo(HaveOccurred()) + + sr := ctor(nil) + + // SealedSecret hits the specific-kind evaluator -> Current. + result, err := sr.ReadStatusForObject(context.Background(), nil, &unstructured.Unstructured{ + Object: map[string]any{ + "apiVersion": "bitnami.com/v1alpha1", + "kind": "SealedSecret", + }, + }) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(result.Status).To(Equal(status.CurrentStatus)) + + // A different Kind in the same group falls back to the group-only evaluator -> Failed. + result, err = sr.ReadStatusForObject(context.Background(), nil, &unstructured.Unstructured{ + Object: map[string]any{ + "apiVersion": "bitnami.com/v1alpha1", + "kind": "OtherKind", + }, + }) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(result.Status).To(Equal(status.FailedStatus)) +} + func TestNewStatusReader_DuplicateGroupKindError(t *testing.T) { g := NewWithT(t) @@ -265,6 +384,30 @@ func TestNewStatusReader_DuplicateGroupKindError(t *testing.T) { g.Expect(result).To(BeNil()) } +func TestNewStatusReader_DuplicateGroupOnlyError(t *testing.T) { + g := NewWithT(t) + + result, err := cel.NewStatusReader([]kustomize.CustomHealthCheck{ + { + APIVersion: "bitnami.com/v1alpha1", + HealthCheckExpressions: kustomize.HealthCheckExpressions{ + Current: "true", + }, + }, + { + APIVersion: "bitnami.com/v1alpha1", + HealthCheckExpressions: kustomize.HealthCheckExpressions{ + Current: "true", + }, + }, + }) + + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring("duplicate custom health check for GroupKind")) + g.Expect(err.Error()).To(ContainSubstring("healthchecks[1]")) + g.Expect(result).To(BeNil()) +} + func TestNewStatusReader_CELCompileError(t *testing.T) { g := NewWithT(t)