Skip to content

Commit 56b4f16

Browse files
authored
Merge pull request #1183 from fluxcd/cel-api-group
runtime/cel: allow empty kind in status reader
2 parents 8f83548 + 6b6497f commit 56b4f16

3 files changed

Lines changed: 159 additions & 5 deletions

File tree

apis/kustomize/kustomize_types.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,8 @@ type CustomHealthCheck struct {
139139
// +required
140140
APIVersion string `json:"apiVersion"`
141141
// Kind of the custom resource under evaluation.
142-
// +required
143-
Kind string `json:"kind"`
142+
// +optional
143+
Kind string `json:"kind,omitempty"`
144144

145145
HealthCheckExpressions `json:",inline"`
146146
}

runtime/cel/status_reader.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ func NewStatusReader(healthchecks []kustomize.CustomHealthCheck) (func(meta.REST
4545
evaluators := make(map[schema.GroupKind]*StatusEvaluator, len(healthchecks))
4646
for i, hc := range healthchecks {
4747
gk := schema.FromAPIVersionAndKind(hc.APIVersion, hc.Kind).GroupKind()
48+
if hc.Kind == "" {
49+
gk = schema.GroupKind{Group: gk.Group}
50+
}
4851
if _, ok := evaluators[gk]; ok {
4952
return nil, fmt.Errorf(
5053
"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
6770

6871
// Supports returns true if the StatusReader supports the given GroupKind.
6972
func (g *StatusReader) Supports(gk schema.GroupKind) bool {
70-
_, ok := g.evaluators[gk]
71-
return ok
73+
_, supportsGroup := g.evaluators[schema.GroupKind{Group: gk.Group}]
74+
_, supportsKind := g.evaluators[gk]
75+
return supportsGroup || supportsKind
7276
}
7377

7478
// 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
104108
}
105109

106110
// genericStatusReader returns the underlying generic status reader.
111+
// Callers must ensure Supports(gk) is true before invoking this; the lookup
112+
// below assumes an evaluator exists and would panic otherwise. Gating is done
113+
// in the callers (ReadStatus, ReadStatusForObject) so they can return errors.
107114
func (g *StatusReader) genericStatusReader(ctx context.Context, gk schema.GroupKind) engine.StatusReader {
108115
statusFunc := func(u *unstructured.Unstructured) (*status.Result, error) {
109-
return g.evaluators[gk].Evaluate(ctx, u)
116+
e, ok := g.evaluators[gk]
117+
if !ok {
118+
e, ok = g.evaluators[schema.GroupKind{Group: gk.Group}]
119+
}
120+
return e.Evaluate(ctx, u)
110121
}
111122
return kstatusreaders.NewGenericStatusReader(g.mapper, statusFunc)
112123
}

runtime/cel/status_reader_test.go

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,38 @@ func TestStatusReader_Supports(t *testing.T) {
6969
},
7070
result: false,
7171
},
72+
{
73+
name: "group-only healthcheck supports any kind in that group",
74+
supportedGK: schema.GroupKind{
75+
Group: "test",
76+
},
77+
gk: schema.GroupKind{
78+
Group: "test",
79+
Kind: "AnyKind",
80+
},
81+
result: true,
82+
},
83+
{
84+
name: "group-only healthcheck does not support other groups",
85+
supportedGK: schema.GroupKind{
86+
Group: "test",
87+
},
88+
gk: schema.GroupKind{
89+
Group: "other",
90+
Kind: "AnyKind",
91+
},
92+
result: false,
93+
},
94+
{
95+
name: "group-only healthcheck supports GK with empty Kind in that group",
96+
supportedGK: schema.GroupKind{
97+
Group: "test",
98+
},
99+
gk: schema.GroupKind{
100+
Group: "test",
101+
},
102+
result: true,
103+
},
72104
} {
73105
t.Run(tt.name, func(t *testing.T) {
74106
t.Parallel()
@@ -239,6 +271,93 @@ func TestStatusReader_ReadStatusForObject(t *testing.T) {
239271
}
240272
}
241273

274+
func TestStatusReader_ReadStatusForObject_GroupOnlyHealthCheck(t *testing.T) {
275+
g := NewWithT(t)
276+
277+
// Register a single group-only healthcheck (empty Kind) for group "bitnami.com".
278+
// It should apply to any Kind in that group.
279+
ctor, err := cel.NewStatusReader([]kustomize.CustomHealthCheck{{
280+
APIVersion: "bitnami.com/v1alpha1",
281+
HealthCheckExpressions: kustomize.HealthCheckExpressions{
282+
Current: "data.current",
283+
},
284+
}})
285+
g.Expect(err).NotTo(HaveOccurred())
286+
287+
sr := ctor(nil)
288+
289+
for _, kind := range []string{"SealedSecret", "AnotherKind"} {
290+
result, err := sr.ReadStatusForObject(context.Background(), nil, &unstructured.Unstructured{
291+
Object: map[string]any{
292+
"apiVersion": "bitnami.com/v1alpha1",
293+
"kind": kind,
294+
"data": map[string]any{
295+
"current": true,
296+
},
297+
},
298+
})
299+
g.Expect(err).NotTo(HaveOccurred())
300+
g.Expect(result.Status).To(Equal(status.CurrentStatus))
301+
}
302+
303+
// A resource from a different group must not be supported.
304+
_, err = sr.ReadStatusForObject(context.Background(), nil, &unstructured.Unstructured{
305+
Object: map[string]any{
306+
"apiVersion": "other.com/v1",
307+
"kind": "Foo",
308+
"data": map[string]any{"current": true},
309+
},
310+
})
311+
g.Expect(err).To(MatchError(ContainSubstring("the GroupKind Foo.other.com is not supported")))
312+
}
313+
314+
func TestStatusReader_ReadStatusForObject_SpecificKindOverridesGroupOnly(t *testing.T) {
315+
g := NewWithT(t)
316+
317+
// Register a group-only healthcheck that would always return Failed,
318+
// plus a specific-kind healthcheck that returns Current. The specific
319+
// one must take precedence for its Kind.
320+
ctor, err := cel.NewStatusReader([]kustomize.CustomHealthCheck{
321+
{
322+
APIVersion: "bitnami.com/v1alpha1",
323+
HealthCheckExpressions: kustomize.HealthCheckExpressions{
324+
Failed: "true",
325+
Current: "false",
326+
},
327+
},
328+
{
329+
APIVersion: "bitnami.com/v1alpha1",
330+
Kind: "SealedSecret",
331+
HealthCheckExpressions: kustomize.HealthCheckExpressions{
332+
Current: "true",
333+
},
334+
},
335+
})
336+
g.Expect(err).NotTo(HaveOccurred())
337+
338+
sr := ctor(nil)
339+
340+
// SealedSecret hits the specific-kind evaluator -> Current.
341+
result, err := sr.ReadStatusForObject(context.Background(), nil, &unstructured.Unstructured{
342+
Object: map[string]any{
343+
"apiVersion": "bitnami.com/v1alpha1",
344+
"kind": "SealedSecret",
345+
},
346+
})
347+
g.Expect(err).NotTo(HaveOccurred())
348+
g.Expect(result.Status).To(Equal(status.CurrentStatus))
349+
350+
// A different Kind in the same group falls back to the group-only evaluator -> Failed.
351+
result, err = sr.ReadStatusForObject(context.Background(), nil, &unstructured.Unstructured{
352+
Object: map[string]any{
353+
"apiVersion": "bitnami.com/v1alpha1",
354+
"kind": "OtherKind",
355+
},
356+
})
357+
g.Expect(err).NotTo(HaveOccurred())
358+
g.Expect(result.Status).To(Equal(status.FailedStatus))
359+
}
360+
242361
func TestNewStatusReader_DuplicateGroupKindError(t *testing.T) {
243362
g := NewWithT(t)
244363

@@ -265,6 +384,30 @@ func TestNewStatusReader_DuplicateGroupKindError(t *testing.T) {
265384
g.Expect(result).To(BeNil())
266385
}
267386

387+
func TestNewStatusReader_DuplicateGroupOnlyError(t *testing.T) {
388+
g := NewWithT(t)
389+
390+
result, err := cel.NewStatusReader([]kustomize.CustomHealthCheck{
391+
{
392+
APIVersion: "bitnami.com/v1alpha1",
393+
HealthCheckExpressions: kustomize.HealthCheckExpressions{
394+
Current: "true",
395+
},
396+
},
397+
{
398+
APIVersion: "bitnami.com/v1alpha1",
399+
HealthCheckExpressions: kustomize.HealthCheckExpressions{
400+
Current: "true",
401+
},
402+
},
403+
})
404+
405+
g.Expect(err).To(HaveOccurred())
406+
g.Expect(err.Error()).To(ContainSubstring("duplicate custom health check for GroupKind"))
407+
g.Expect(err.Error()).To(ContainSubstring("healthchecks[1]"))
408+
g.Expect(result).To(BeNil())
409+
}
410+
268411
func TestNewStatusReader_CELCompileError(t *testing.T) {
269412
g := NewWithT(t)
270413

0 commit comments

Comments
 (0)