Skip to content

Commit 5ce596c

Browse files
author
Paulo Gomes
committed
fuzz: Refactor Fuzz tests based on Go native fuzzing.
Moving into Go Native, the adhoc changes and on-demand build is no longer necessary. Previously calls to r.EventRecorder.AnnotatedEventf resulted in panic. The new dummy recorder resolves the problem without impacting resource consumption. A new make target `fuzz-native` was introduced, to loop through all fuzz tests for the duration of time specified via the environment variable `FUZZ_TIME`. Signed-off-by: Paulo Gomes <paulo.gomes@weave.works>
1 parent 5c28d56 commit 5ce596c

7 files changed

+296
-168
lines changed

Makefile

+12-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ CRD_OPTIONS ?= crd:crdVersions=v1
77
REPOSITORY_ROOT := $(shell git rev-parse --show-toplevel)
88
BUILD_DIR := $(REPOSITORY_ROOT)/build
99

10+
# FUZZ_TIME defines the max amount of time, in Go Duration,
11+
# each fuzzer should run for.
12+
FUZZ_TIME ?= 1m
13+
1014
# If gobin not set, create one on ./build and add to path.
1115
ifeq (,$(shell go env GOBIN))
1216
GOBIN=$(BUILD_DIR)/gobin
@@ -142,7 +146,7 @@ rm -rf $$TMP_DIR ;\
142146
}
143147
endef
144148

145-
# Build fuzzers
149+
# Build fuzzers used by oss-fuzz.
146150
fuzz-build:
147151
rm -rf $(BUILD_DIR)/fuzz/
148152
mkdir -p $(BUILD_DIR)/fuzz/out/
@@ -154,10 +158,16 @@ fuzz-build:
154158
-v "$(BUILD_DIR)/fuzz/out":/out \
155159
local-fuzzing:latest
156160

157-
# Run each fuzzer once to ensure they are working
161+
# Run each fuzzer once to ensure they will work when executed by oss-fuzz.
158162
fuzz-smoketest: fuzz-build
159163
docker run --rm \
160164
-v "$(BUILD_DIR)/fuzz/out":/out \
161165
-v "$(REPOSITORY_ROOT)/tests/fuzz/oss_fuzz_run.sh":/runner.sh \
162166
local-fuzzing:latest \
163167
bash -c "/runner.sh"
168+
169+
# Run fuzz tests for the duration set in FUZZ_TIME.
170+
fuzz-native:
171+
KUBEBUILDER_ASSETS=$(KUBEBUILDER_ASSETS) \
172+
FUZZ_TIME=$(FUZZ_TIME) \
173+
./tests/fuzz/native_go_run.sh

controllers/helmrelease_controller_test.go

+224
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434
"sigs.k8s.io/yaml"
3535

3636
v2 "github.com/fluxcd/helm-controller/api/v2beta1"
37+
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
3738
)
3839

3940
func TestHelmReleaseReconciler_composeValues(t *testing.T) {
@@ -446,6 +447,208 @@ func TestValuesReferenceValidation(t *testing.T) {
446447
}
447448
}
448449

450+
func FuzzHelmReleaseReconciler_composeValues(f *testing.F) {
451+
scheme := testScheme()
452+
453+
tests := []struct {
454+
targetPath string
455+
valuesKey string
456+
hrValues string
457+
createObject bool
458+
secretData []byte
459+
configData string
460+
}{
461+
{
462+
targetPath: "flat",
463+
valuesKey: "custom-values.yaml",
464+
secretData: []byte(`flat:
465+
nested: value
466+
nested: value
467+
`),
468+
configData: `flat: value
469+
nested:
470+
configuration: value
471+
`,
472+
hrValues: `
473+
other: values
474+
`,
475+
createObject: true,
476+
},
477+
{
478+
targetPath: "'flat'",
479+
valuesKey: "custom-values.yaml",
480+
secretData: []byte(`flat:
481+
nested: value
482+
nested: value
483+
`),
484+
configData: `flat: value
485+
nested:
486+
configuration: value
487+
`,
488+
hrValues: `
489+
other: values
490+
`,
491+
createObject: true,
492+
},
493+
{
494+
targetPath: "flat[0]",
495+
secretData: []byte(``),
496+
configData: `flat: value`,
497+
hrValues: `
498+
other: values
499+
`,
500+
createObject: true,
501+
},
502+
{
503+
secretData: []byte(`flat:
504+
nested: value
505+
nested: value
506+
`),
507+
configData: `flat: value
508+
nested:
509+
configuration: value
510+
`,
511+
hrValues: `
512+
other: values
513+
`,
514+
createObject: true,
515+
},
516+
{
517+
targetPath: "some-value",
518+
hrValues: `
519+
other: values
520+
`,
521+
createObject: false,
522+
},
523+
}
524+
525+
for _, tt := range tests {
526+
f.Add(tt.targetPath, tt.valuesKey, tt.hrValues, tt.createObject, tt.secretData, tt.configData)
527+
}
528+
529+
f.Fuzz(func(t *testing.T,
530+
targetPath, valuesKey, hrValues string, createObject bool, secretData []byte, configData string) {
531+
532+
// objectName represents a core Kubernetes name (Secret/ConfigMap) which is validated
533+
// upstream, and also validated by us in the OpenAPI-based validation set in
534+
// v2.ValuesReference. Therefore a static value here suffices, and instead we just
535+
// play with the objects presence/absence.
536+
objectName := "values"
537+
resources := []runtime.Object{}
538+
539+
if createObject {
540+
resources = append(resources,
541+
valuesConfigMap(objectName, map[string]string{valuesKey: configData}),
542+
valuesSecret(objectName, map[string][]byte{valuesKey: secretData}),
543+
)
544+
}
545+
546+
references := []v2.ValuesReference{
547+
{
548+
Kind: "ConfigMap",
549+
Name: objectName,
550+
ValuesKey: valuesKey,
551+
TargetPath: targetPath,
552+
},
553+
{
554+
Kind: "Secret",
555+
Name: objectName,
556+
ValuesKey: valuesKey,
557+
TargetPath: targetPath,
558+
},
559+
}
560+
561+
c := fake.NewFakeClientWithScheme(scheme, resources...)
562+
r := &HelmReleaseReconciler{Client: c}
563+
var values *apiextensionsv1.JSON
564+
if hrValues != "" {
565+
v, _ := yaml.YAMLToJSON([]byte(hrValues))
566+
values = &apiextensionsv1.JSON{Raw: v}
567+
}
568+
569+
hr := v2.HelmRelease{
570+
Spec: v2.HelmReleaseSpec{
571+
ValuesFrom: references,
572+
Values: values,
573+
},
574+
}
575+
576+
// OpenAPI-based validation on schema is not verified here.
577+
// Therefore some false positives may be arise, as the apiserver
578+
// would not allow such values to make their way into the control plane.
579+
//
580+
// Testenv could be used so the fuzzing covers the entire E2E.
581+
// The downsize being the resource and time cost per test would be a lot higher.
582+
//
583+
// Another approach could be to add validation to reject invalid inputs before
584+
// the r.composeValues call.
585+
_, _ = r.composeValues(logr.NewContext(context.TODO(), logr.Discard()), hr)
586+
})
587+
}
588+
589+
func FuzzHelmReleaseReconciler_reconcile(f *testing.F) {
590+
scheme := testScheme()
591+
tests := []struct {
592+
valuesKey string
593+
hrValues string
594+
secretData []byte
595+
configData string
596+
}{
597+
{
598+
valuesKey: "custom-values.yaml",
599+
secretData: []byte(`flat:
600+
nested: value
601+
nested: value
602+
`),
603+
configData: `flat: value
604+
nested:
605+
configuration: value
606+
`,
607+
hrValues: `
608+
other: values
609+
`,
610+
},
611+
}
612+
613+
for _, tt := range tests {
614+
f.Add(tt.valuesKey, tt.hrValues, tt.secretData, tt.configData)
615+
}
616+
617+
f.Fuzz(func(t *testing.T,
618+
valuesKey, hrValues string, secretData []byte, configData string) {
619+
620+
var values *apiextensionsv1.JSON
621+
if hrValues != "" {
622+
v, _ := yaml.YAMLToJSON([]byte(hrValues))
623+
values = &apiextensionsv1.JSON{Raw: v}
624+
}
625+
626+
hr := v2.HelmRelease{
627+
Spec: v2.HelmReleaseSpec{
628+
Values: values,
629+
},
630+
}
631+
632+
hc := sourcev1.HelmChart{}
633+
hc.ObjectMeta.Name = hr.GetHelmChartName()
634+
hc.ObjectMeta.Namespace = hr.Spec.Chart.GetNamespace(hr.Namespace)
635+
636+
resources := []runtime.Object{
637+
valuesConfigMap("values", map[string]string{valuesKey: configData}),
638+
valuesSecret("values", map[string][]byte{valuesKey: secretData}),
639+
&hc,
640+
}
641+
642+
c := fake.NewFakeClientWithScheme(scheme, resources...)
643+
r := &HelmReleaseReconciler{
644+
Client: c,
645+
EventRecorder: &DummyRecorder{},
646+
}
647+
648+
_, _, _ = r.reconcile(logr.NewContext(context.TODO(), logr.Discard()), hr)
649+
})
650+
}
651+
449652
func valuesSecret(name string, data map[string][]byte) *corev1.Secret {
450653
return &corev1.Secret{
451654
ObjectMeta: metav1.ObjectMeta{Name: name},
@@ -459,3 +662,24 @@ func valuesConfigMap(name string, data map[string]string) *corev1.ConfigMap {
459662
Data: data,
460663
}
461664
}
665+
666+
func testScheme() *runtime.Scheme {
667+
scheme := runtime.NewScheme()
668+
_ = corev1.AddToScheme(scheme)
669+
_ = v2.AddToScheme(scheme)
670+
_ = sourcev1.AddToScheme(scheme)
671+
return scheme
672+
}
673+
674+
// DummyRecorder serves as a dummy for kuberecorder.EventRecorder.
675+
type DummyRecorder struct{}
676+
677+
func (r *DummyRecorder) Event(object runtime.Object, eventtype, reason, message string) {
678+
}
679+
680+
func (r *DummyRecorder) Eventf(object runtime.Object, eventtype, reason, messageFmt string, args ...interface{}) {
681+
}
682+
683+
func (r *DummyRecorder) AnnotatedEventf(object runtime.Object, annotations map[string]string,
684+
eventtype, reason string, messageFmt string, args ...interface{}) {
685+
}

tests/fuzz/Dockerfile.builder

+5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
FROM golang:1.18 AS go
2+
13
FROM gcr.io/oss-fuzz-base/base-builder-go
24

5+
# ensures golang 1.18 to enable go native fuzzing.
6+
COPY --from=go /usr/local/go /usr/local/
7+
38
COPY ./ $GOPATH/src/github.com/fluxcd/helm-controller/
49
COPY ./tests/fuzz/oss_fuzz_build.sh $SRC/build.sh
510

0 commit comments

Comments
 (0)