Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apis/kustomize/kustomize_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
Expand Down
17 changes: 14 additions & 3 deletions runtime/cel/status_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@
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)
Expand All @@ -67,8 +70,9 @@

// 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.
Expand Down Expand Up @@ -104,9 +108,16 @@
}

// 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}]

Check warning

Code scanning / CodeQL

Useless assignment to local variable Warning

This definition of ok is never used.
Comment thread
matheuscscp marked this conversation as resolved.
Dismissed
}
return e.Evaluate(ctx, u)
}
return kstatusreaders.NewGenericStatusReader(g.mapper, statusFunc)
}
143 changes: 143 additions & 0 deletions runtime/cel/status_reader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)

Expand All @@ -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)

Expand Down
Loading