Skip to content

Commit 31ee3c0

Browse files
committed
feat(karpenter): add envtest and e2e tests for kubelet config
Add envtest suites for CEL validation rules including threshold ordering, field promotion ratcheting, and generated ratcheting tests. Add e2e test infrastructure for verifying kubelet config on nodes. Signed-off-by: John Kyros <jkyros@redhat.com>
1 parent 5fefe11 commit 31ee3c0

7 files changed

Lines changed: 1592 additions & 0 deletions

File tree

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ karpenter-api: $(CONTROLLER_GEN) $(YQ)
200200
karpenter-operator/hack/adjust-cel.sh
201201
$(CONTROLLER_GEN) $(CRD_OPTIONS) paths="./api/karpenter/..." output:crd:artifacts:config=karpenter-operator/controllers/karpenter/assets
202202
cp karpenter-operator/controllers/karpenter/assets/karpenter.hypershift.openshift.io_openshiftec2nodeclasses.yaml karpenter-operator/controllers/karpenter/assets/zz_generated.crd-manifests/openshiftec2nodeclasses.crd.yaml
203+
$(GO) run ./hack/kubelet-ratcheting-gen/main.go
203204

204205
.PHONY: control-plane-operator
205206
control-plane-operator:
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
// kubelet-ratcheting-gen generates the envtest ratcheting testsuite for KubeletConfiguration.
2+
// It reflects over KubeletConfiguration to enumerate all typed fields, verifies each has a
3+
// fixture value, and writes a testsuite YAML that simulates a pre-upgrade → post-upgrade
4+
// CRD schema swap. Run via: go run ./hack/kubelet-ratcheting-gen/main.go (from repo root).
5+
//
6+
// When adding a new typed field to KubeletConfiguration:
7+
// 1. Add a fixture value to the fixtures map below.
8+
// 2. Run `make karpenter-api` to regenerate the testsuite.
9+
// 3. Commit the updated testsuite YAML.
10+
package main
11+
12+
import (
13+
"fmt"
14+
"os"
15+
"reflect"
16+
"strings"
17+
"text/template"
18+
19+
karpenterv1 "github.com/openshift/hypershift/api/karpenter/v1"
20+
)
21+
22+
const outputPath = "karpenter-operator/controllers/karpenter/assets/tests/" +
23+
"openshiftec2nodeclasses.karpenter.hypershift.openshift.io/" +
24+
"stable.openshiftec2nodeclasses.kubelet-ratcheting.testsuite.yaml"
25+
26+
// fixtures maps each KubeletConfiguration JSON field name to a valid YAML snippet.
27+
// Values must satisfy all CRD validation rules (min/max, CEL cross-field constraints).
28+
// Add an entry here when promoting a new field from overflow to typed.
29+
var fixtures = map[string]string{
30+
"maxPods": "110",
31+
"podsPerCore": "10",
32+
"systemReserved": "cpu: \"100m\"\n memory: \"256Mi\"",
33+
"kubeReserved": "cpu: \"200m\"\n memory: \"512Mi\"",
34+
"evictionHard": "memory.available: \"100Mi\"\n nodefs.available: \"10%\"",
35+
"evictionSoft": "memory.available: \"200Mi\"",
36+
"evictionSoftGracePeriod": "memory.available: \"30s\"",
37+
"evictionMaxPodGracePeriod": "60",
38+
"imageGCHighThresholdPercent": "85",
39+
"imageGCLowThresholdPercent": "80",
40+
"cpuCFSQuota": "true",
41+
}
42+
43+
// mapFields are fields whose fixture value is a YAML mapping (rendered with extra indentation).
44+
var mapFields = map[string]bool{
45+
"systemReserved": true,
46+
"kubeReserved": true,
47+
"evictionHard": true,
48+
"evictionSoft": true,
49+
"evictionSoftGracePeriod": true,
50+
}
51+
52+
const testsuiteTmpl = `# Code generated by hack/kubelet-ratcheting-gen. DO NOT EDIT.
53+
# To regenerate: make karpenter-api
54+
# To add a new field: add a fixture in hack/kubelet-ratcheting-gen/main.go, then regenerate.
55+
apiVersion: apiextensions.k8s.io/v1
56+
name: "OpenshiftEC2NodeClass kubelet ratcheting validation"
57+
crdName: openshiftec2nodeclasses.karpenter.hypershift.openshift.io
58+
version: v1
59+
# Verifies that all currently-typed kubelet fields survive a CRD schema upgrade.
60+
# initialCRDPatches strip the entire kubelet typed-field schema back to a pure
61+
# "type: object, x-kubernetes-preserve-unknown-fields: true" — the state that existed
62+
# before any fields were promoted from overflow. The initial object is created with all
63+
# typed fields set (they land in overflow under the patched schema). The patches are then
64+
# reverted and the update is applied, verifying every field survives the upgrade seamlessly.
65+
tests:
66+
onUpdate:
67+
- name: When all typed kubelet fields were set before they were typed they should remain valid after CRD upgrade
68+
initialCRDPatches:
69+
# Strip all typed field schemas, leaving type: object, x-kubernetes-preserve-unknown-fields: true.
70+
- op: remove
71+
path: /spec/versions/0/schema/openAPIV3Schema/properties/spec/properties/kubelet/properties
72+
# Strip all CEL cross-field validation rules.
73+
- op: remove
74+
path: /spec/versions/0/schema/openAPIV3Schema/properties/spec/properties/kubelet/x-kubernetes-validations
75+
initial: |
76+
apiVersion: karpenter.hypershift.openshift.io/v1
77+
kind: OpenshiftEC2NodeClass
78+
spec:
79+
subnetSelectorTerms:
80+
- tags:
81+
env: test
82+
securityGroupSelectorTerms:
83+
- id: sg-0123456789abcdef0
84+
kubelet:
85+
{{- range .Fields}}
86+
{{.Name}}:{{if .IsMap}}
87+
{{.Value}}{{else}} {{.Value}}{{end}}
88+
{{- end}}
89+
updated: |
90+
apiVersion: karpenter.hypershift.openshift.io/v1
91+
kind: OpenshiftEC2NodeClass
92+
spec:
93+
subnetSelectorTerms:
94+
- tags:
95+
env: test
96+
securityGroupSelectorTerms:
97+
- id: sg-0123456789abcdef0
98+
kubelet:
99+
{{- range .Fields}}
100+
{{.Name}}:{{if .IsMap}}
101+
{{.Value}}{{else}} {{.Value}}{{end}}
102+
{{- end}}
103+
`
104+
105+
type field struct {
106+
Name string
107+
Value string
108+
IsMap bool
109+
}
110+
111+
func main() {
112+
if err := run(); err != nil {
113+
fmt.Fprintf(os.Stderr, "error: %v\n", err)
114+
os.Exit(1)
115+
}
116+
}
117+
118+
func run() error {
119+
fields, err := collectFields()
120+
if err != nil {
121+
return err
122+
}
123+
124+
tmpl, err := template.New("testsuite").Parse(testsuiteTmpl)
125+
if err != nil {
126+
return fmt.Errorf("parsing template: %w", err)
127+
}
128+
129+
f, err := os.Create(outputPath)
130+
if err != nil {
131+
return fmt.Errorf("creating output file %s: %w", outputPath, err)
132+
}
133+
defer f.Close()
134+
135+
if err := tmpl.Execute(f, struct{ Fields []field }{Fields: fields}); err != nil {
136+
return fmt.Errorf("executing template: %w", err)
137+
}
138+
139+
fmt.Printf("generated %s (%d fields)\n", outputPath, len(fields))
140+
return nil
141+
}
142+
143+
// collectFields reflects over KubeletConfiguration, enumerates typed fields by JSON tag,
144+
// and looks up each one in the fixtures map. Missing fixtures cause a hard failure.
145+
func collectFields() ([]field, error) {
146+
t := reflect.TypeOf(karpenterv1.KubeletConfiguration{})
147+
var fields []field
148+
var missing []string
149+
150+
for i := range t.NumField() {
151+
f := t.Field(i)
152+
tag := f.Tag.Get("json")
153+
if tag == "" || tag == "-" {
154+
continue
155+
}
156+
name, _, _ := strings.Cut(tag, ",")
157+
if name == "" || name == "-" {
158+
continue
159+
}
160+
161+
val, ok := fixtures[name]
162+
if !ok {
163+
missing = append(missing, fmt.Sprintf("%s (json:%q)", f.Name, name))
164+
continue
165+
}
166+
fields = append(fields, field{Name: name, Value: val, IsMap: mapFields[name]})
167+
}
168+
169+
if len(missing) > 0 {
170+
return nil, fmt.Errorf(
171+
"no fixture value for the following KubeletConfiguration fields — "+
172+
"add them to the fixtures map in hack/kubelet-ratcheting-gen/main.go:\n %s",
173+
strings.Join(missing, "\n "),
174+
)
175+
}
176+
177+
return fields, nil
178+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
apiVersion: apiextensions.k8s.io/v1
2+
name: "OpenshiftEC2NodeClass kubelet field promotion compatibility"
3+
crdName: openshiftec2nodeclasses.karpenter.hypershift.openshift.io
4+
version: v1
5+
# These tests verify that promoting a kubelet config field from overflow (unknown/pass-through)
6+
# to a typed struct field does not break existing NodeClass objects or reject previously-valid YAML.
7+
tests:
8+
onUpdate:
9+
# When a field that previously lived in overflow becomes a typed field, existing objects that
10+
# used it as an overflow field must still be updatable — mixing typed and overflow fields is valid.
11+
- name: When an overflow field and a newly-typed field coexist on update it should pass
12+
initial: |
13+
apiVersion: karpenter.hypershift.openshift.io/v1
14+
kind: OpenshiftEC2NodeClass
15+
spec:
16+
subnetSelectorTerms:
17+
- tags:
18+
env: test
19+
securityGroupSelectorTerms:
20+
- id: sg-0123456789abcdef0
21+
kubelet:
22+
podPidsLimit: 4096
23+
updated: |
24+
apiVersion: karpenter.hypershift.openshift.io/v1
25+
kind: OpenshiftEC2NodeClass
26+
spec:
27+
subnetSelectorTerms:
28+
- tags:
29+
env: test
30+
securityGroupSelectorTerms:
31+
- id: sg-0123456789abcdef0
32+
kubelet:
33+
maxPods: 110
34+
podPidsLimit: 4096
35+
expected: |
36+
apiVersion: karpenter.hypershift.openshift.io/v1
37+
kind: OpenshiftEC2NodeClass
38+
spec:
39+
subnetSelectorTerms:
40+
- tags:
41+
env: test
42+
securityGroupSelectorTerms:
43+
- id: sg-0123456789abcdef0
44+
kubelet:
45+
maxPods: 110
46+
podPidsLimit: 4096
47+
48+
# When a field was set as overflow before the struct existed and is now set as a typed field,
49+
# the update must be accepted — the field value is the same, only the code path changed.
50+
- name: When a field transitions from overflow to typed on update it should pass
51+
initial: |
52+
apiVersion: karpenter.hypershift.openshift.io/v1
53+
kind: OpenshiftEC2NodeClass
54+
spec:
55+
subnetSelectorTerms:
56+
- tags:
57+
env: test
58+
securityGroupSelectorTerms:
59+
- id: sg-0123456789abcdef0
60+
kubelet:
61+
maxPods: 110
62+
podPidsLimit: 4096
63+
updated: |
64+
apiVersion: karpenter.hypershift.openshift.io/v1
65+
kind: OpenshiftEC2NodeClass
66+
spec:
67+
subnetSelectorTerms:
68+
- tags:
69+
env: test
70+
securityGroupSelectorTerms:
71+
- id: sg-0123456789abcdef0
72+
kubelet:
73+
maxPods: 110
74+
containerLogMaxSize: "50Mi"
75+
expected: |
76+
apiVersion: karpenter.hypershift.openshift.io/v1
77+
kind: OpenshiftEC2NodeClass
78+
spec:
79+
subnetSelectorTerms:
80+
- tags:
81+
env: test
82+
securityGroupSelectorTerms:
83+
- id: sg-0123456789abcdef0
84+
kubelet:
85+
maxPods: 110
86+
containerLogMaxSize: "50Mi"
87+
88+
# When a typed field is removed on update it should be accepted regardless of overflow fields remaining.
89+
- name: When a typed field is removed and only overflow fields remain on update it should pass
90+
initial: |
91+
apiVersion: karpenter.hypershift.openshift.io/v1
92+
kind: OpenshiftEC2NodeClass
93+
spec:
94+
subnetSelectorTerms:
95+
- tags:
96+
env: test
97+
securityGroupSelectorTerms:
98+
- id: sg-0123456789abcdef0
99+
kubelet:
100+
maxPods: 110
101+
podPidsLimit: 4096
102+
updated: |
103+
apiVersion: karpenter.hypershift.openshift.io/v1
104+
kind: OpenshiftEC2NodeClass
105+
spec:
106+
subnetSelectorTerms:
107+
- tags:
108+
env: test
109+
securityGroupSelectorTerms:
110+
- id: sg-0123456789abcdef0
111+
kubelet:
112+
podPidsLimit: 4096
113+
expected: |
114+
apiVersion: karpenter.hypershift.openshift.io/v1
115+
kind: OpenshiftEC2NodeClass
116+
spec:
117+
subnetSelectorTerms:
118+
- tags:
119+
env: test
120+
securityGroupSelectorTerms:
121+
- id: sg-0123456789abcdef0
122+
kubelet:
123+
podPidsLimit: 4096
124+
125+
# When an object with only overflow fields is updated to add typed fields it should pass.
126+
- name: When typed fields are added alongside existing overflow fields on update it should pass
127+
initial: |
128+
apiVersion: karpenter.hypershift.openshift.io/v1
129+
kind: OpenshiftEC2NodeClass
130+
spec:
131+
subnetSelectorTerms:
132+
- tags:
133+
env: test
134+
securityGroupSelectorTerms:
135+
- id: sg-0123456789abcdef0
136+
kubelet:
137+
podPidsLimit: 4096
138+
containerLogMaxSize: "50Mi"
139+
updated: |
140+
apiVersion: karpenter.hypershift.openshift.io/v1
141+
kind: OpenshiftEC2NodeClass
142+
spec:
143+
subnetSelectorTerms:
144+
- tags:
145+
env: test
146+
securityGroupSelectorTerms:
147+
- id: sg-0123456789abcdef0
148+
kubelet:
149+
maxPods: 110
150+
podsPerCore: 10
151+
podPidsLimit: 4096
152+
containerLogMaxSize: "50Mi"
153+
expected: |
154+
apiVersion: karpenter.hypershift.openshift.io/v1
155+
kind: OpenshiftEC2NodeClass
156+
spec:
157+
subnetSelectorTerms:
158+
- tags:
159+
env: test
160+
securityGroupSelectorTerms:
161+
- id: sg-0123456789abcdef0
162+
kubelet:
163+
maxPods: 110
164+
podsPerCore: 10
165+
podPidsLimit: 4096
166+
containerLogMaxSize: "50Mi"
167+
168+
# CEL cross-field validation must still fire on update even when overflow fields are present.
169+
- name: When podsPerCore exceeds maxPods on update it should fail even with overflow fields present
170+
initial: |
171+
apiVersion: karpenter.hypershift.openshift.io/v1
172+
kind: OpenshiftEC2NodeClass
173+
spec:
174+
subnetSelectorTerms:
175+
- tags:
176+
env: test
177+
securityGroupSelectorTerms:
178+
- id: sg-0123456789abcdef0
179+
kubelet:
180+
maxPods: 50
181+
podsPerCore: 10
182+
podPidsLimit: 4096
183+
updated: |
184+
apiVersion: karpenter.hypershift.openshift.io/v1
185+
kind: OpenshiftEC2NodeClass
186+
spec:
187+
subnetSelectorTerms:
188+
- tags:
189+
env: test
190+
securityGroupSelectorTerms:
191+
- id: sg-0123456789abcdef0
192+
kubelet:
193+
maxPods: 10
194+
podsPerCore: 50
195+
podPidsLimit: 4096
196+
expectedError: "podsPerCore must not exceed maxPods"

0 commit comments

Comments
 (0)