Skip to content

Commit 8d1afa6

Browse files
author
Paulo Gomes
authored
Merge pull request #565 from pjbgf/fix-broken-fuzz
build: Fix cifuzz and improve fuzz tests' reliability
2 parents 3a13ca2 + 9f31507 commit 8d1afa6

File tree

4 files changed

+360
-272
lines changed

4 files changed

+360
-272
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
//go:build gofuzz_libfuzzer
2+
// +build gofuzz_libfuzzer
3+
4+
/*
5+
Copyright 2020 The Flux authors
6+
7+
Licensed under the Apache License, Version 2.0 (the "License");
8+
you may not use this file except in compliance with the License.
9+
You may obtain a copy of the License at
10+
11+
http://www.apache.org/licenses/LICENSE-2.0
12+
13+
Unless required by applicable law or agreed to in writing, software
14+
distributed under the License is distributed on an "AS IS" BASIS,
15+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
See the License for the specific language governing permissions and
17+
limitations under the License.
18+
*/
19+
20+
package controllers
21+
22+
import (
23+
"context"
24+
"testing"
25+
26+
"github.com/go-logr/logr"
27+
corev1 "k8s.io/api/core/v1"
28+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
29+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
30+
"k8s.io/apimachinery/pkg/runtime"
31+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
32+
"sigs.k8s.io/yaml"
33+
34+
v2 "github.com/fluxcd/helm-controller/api/v2beta1"
35+
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
36+
)
37+
38+
func FuzzHelmReleaseReconciler_composeValues(f *testing.F) {
39+
scheme := testScheme()
40+
41+
tests := []struct {
42+
targetPath string
43+
valuesKey string
44+
hrValues string
45+
createObject bool
46+
secretData []byte
47+
configData string
48+
}{
49+
{
50+
targetPath: "flat",
51+
valuesKey: "custom-values.yaml",
52+
secretData: []byte(`flat:
53+
nested: value
54+
nested: value
55+
`),
56+
configData: `flat: value
57+
nested:
58+
configuration: value
59+
`,
60+
hrValues: `
61+
other: values
62+
`,
63+
createObject: true,
64+
},
65+
{
66+
targetPath: "'flat'",
67+
valuesKey: "custom-values.yaml",
68+
secretData: []byte(`flat:
69+
nested: value
70+
nested: value
71+
`),
72+
configData: `flat: value
73+
nested:
74+
configuration: value
75+
`,
76+
hrValues: `
77+
other: values
78+
`,
79+
createObject: true,
80+
},
81+
{
82+
targetPath: "flat[0]",
83+
secretData: []byte(``),
84+
configData: `flat: value`,
85+
hrValues: `
86+
other: values
87+
`,
88+
createObject: true,
89+
},
90+
{
91+
secretData: []byte(`flat:
92+
nested: value
93+
nested: value
94+
`),
95+
configData: `flat: value
96+
nested:
97+
configuration: value
98+
`,
99+
hrValues: `
100+
other: values
101+
`,
102+
createObject: true,
103+
},
104+
{
105+
targetPath: "some-value",
106+
hrValues: `
107+
other: values
108+
`,
109+
createObject: false,
110+
},
111+
}
112+
113+
for _, tt := range tests {
114+
f.Add(tt.targetPath, tt.valuesKey, tt.hrValues, tt.createObject, tt.secretData, tt.configData)
115+
}
116+
117+
f.Fuzz(func(t *testing.T,
118+
targetPath, valuesKey, hrValues string, createObject bool, secretData []byte, configData string) {
119+
120+
// objectName represents a core Kubernetes name (Secret/ConfigMap) which is validated
121+
// upstream, and also validated by us in the OpenAPI-based validation set in
122+
// v2.ValuesReference. Therefore a static value here suffices, and instead we just
123+
// play with the objects presence/absence.
124+
objectName := "values"
125+
resources := []runtime.Object{}
126+
127+
if createObject {
128+
resources = append(resources,
129+
valuesConfigMap(objectName, map[string]string{valuesKey: configData}),
130+
valuesSecret(objectName, map[string][]byte{valuesKey: secretData}),
131+
)
132+
}
133+
134+
references := []v2.ValuesReference{
135+
{
136+
Kind: "ConfigMap",
137+
Name: objectName,
138+
ValuesKey: valuesKey,
139+
TargetPath: targetPath,
140+
},
141+
{
142+
Kind: "Secret",
143+
Name: objectName,
144+
ValuesKey: valuesKey,
145+
TargetPath: targetPath,
146+
},
147+
}
148+
149+
c := fake.NewFakeClientWithScheme(scheme, resources...)
150+
r := &HelmReleaseReconciler{Client: c}
151+
var values *apiextensionsv1.JSON
152+
if hrValues != "" {
153+
v, _ := yaml.YAMLToJSON([]byte(hrValues))
154+
values = &apiextensionsv1.JSON{Raw: v}
155+
}
156+
157+
hr := v2.HelmRelease{
158+
Spec: v2.HelmReleaseSpec{
159+
ValuesFrom: references,
160+
Values: values,
161+
},
162+
}
163+
164+
// OpenAPI-based validation on schema is not verified here.
165+
// Therefore some false positives may be arise, as the apiserver
166+
// would not allow such values to make their way into the control plane.
167+
//
168+
// Testenv could be used so the fuzzing covers the entire E2E.
169+
// The downsize being the resource and time cost per test would be a lot higher.
170+
//
171+
// Another approach could be to add validation to reject invalid inputs before
172+
// the r.composeValues call.
173+
_, _ = r.composeValues(logr.NewContext(context.TODO(), logr.Discard()), hr)
174+
})
175+
}
176+
177+
func FuzzHelmReleaseReconciler_reconcile(f *testing.F) {
178+
scheme := testScheme()
179+
tests := []struct {
180+
valuesKey string
181+
hrValues string
182+
secretData []byte
183+
configData string
184+
}{
185+
{
186+
valuesKey: "custom-values.yaml",
187+
secretData: []byte(`flat:
188+
nested: value
189+
nested: value
190+
`),
191+
configData: `flat: value
192+
nested:
193+
configuration: value
194+
`,
195+
hrValues: `
196+
other: values
197+
`,
198+
},
199+
}
200+
201+
for _, tt := range tests {
202+
f.Add(tt.valuesKey, tt.hrValues, tt.secretData, tt.configData)
203+
}
204+
205+
f.Fuzz(func(t *testing.T,
206+
valuesKey, hrValues string, secretData []byte, configData string) {
207+
208+
var values *apiextensionsv1.JSON
209+
if hrValues != "" {
210+
v, _ := yaml.YAMLToJSON([]byte(hrValues))
211+
values = &apiextensionsv1.JSON{Raw: v}
212+
}
213+
214+
hr := v2.HelmRelease{
215+
Spec: v2.HelmReleaseSpec{
216+
Values: values,
217+
},
218+
}
219+
220+
hc := sourcev1.HelmChart{}
221+
hc.ObjectMeta.Name = hr.GetHelmChartName()
222+
hc.ObjectMeta.Namespace = hr.Spec.Chart.GetNamespace(hr.Namespace)
223+
224+
resources := []runtime.Object{
225+
valuesConfigMap("values", map[string]string{valuesKey: configData}),
226+
valuesSecret("values", map[string][]byte{valuesKey: secretData}),
227+
&hc,
228+
}
229+
230+
c := fake.NewFakeClientWithScheme(scheme, resources...)
231+
r := &HelmReleaseReconciler{
232+
Client: c,
233+
EventRecorder: &DummyRecorder{},
234+
}
235+
236+
_, _, _ = r.reconcile(logr.NewContext(context.TODO(), logr.Discard()), hr)
237+
})
238+
}
239+
240+
func valuesSecret(name string, data map[string][]byte) *corev1.Secret {
241+
return &corev1.Secret{
242+
ObjectMeta: metav1.ObjectMeta{Name: name},
243+
Data: data,
244+
}
245+
}
246+
247+
func valuesConfigMap(name string, data map[string]string) *corev1.ConfigMap {
248+
return &corev1.ConfigMap{
249+
ObjectMeta: metav1.ObjectMeta{Name: name},
250+
Data: data,
251+
}
252+
}
253+
254+
func testScheme() *runtime.Scheme {
255+
scheme := runtime.NewScheme()
256+
_ = corev1.AddToScheme(scheme)
257+
_ = v2.AddToScheme(scheme)
258+
_ = sourcev1.AddToScheme(scheme)
259+
return scheme
260+
}
261+
262+
// DummyRecorder serves as a dummy for kuberecorder.EventRecorder.
263+
type DummyRecorder struct{}
264+
265+
func (r *DummyRecorder) Event(object runtime.Object, eventtype, reason, message string) {
266+
}
267+
268+
func (r *DummyRecorder) Eventf(object runtime.Object, eventtype, reason, messageFmt string, args ...interface{}) {
269+
}
270+
271+
func (r *DummyRecorder) AnnotatedEventf(object runtime.Object, annotations map[string]string,
272+
eventtype, reason string, messageFmt string, args ...interface{}) {
273+
}

0 commit comments

Comments
 (0)