Skip to content

Commit ee17502

Browse files
author
Paulo Gomes
committed
Add validation to TargetPath and ValuesKey
Formalises the API requirements around TargetPath and ValuesKey, which were the two fields missing validation within ValuesReference. In both cases the validation was introduced at CRD level, so that the apiserver will enforce it. ValuesKey must be a valid Data Key. Therefore the same logic used by upstream Kubernetes is reused here to ensure a valid key is being used. For TargetPath a loose regex is being used to largely represent the expected format. A max length of 250 is now being enforced. This is a breaking change, as invalid TargetPath and ValuesKey will now be rejected by the apiserver, instead of being accepted and potentially failing at reconciliation time. Signed-off-by: Paulo Gomes <paulo.gomes@weave.works>
1 parent 2195310 commit ee17502

File tree

5 files changed

+239
-39
lines changed

5 files changed

+239
-39
lines changed

api/v2beta1/reference_types.go

+6
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,18 @@ type ValuesReference struct {
5959

6060
// ValuesKey is the data key where the values.yaml or a specific value can be
6161
// found at. Defaults to 'values.yaml'.
62+
// When set, must be a valid Data Key, consisting of alphanumeric characters,
63+
// '-', '_' or '.'.
64+
// +kubebuilder:validation:MaxLength=253
65+
// +kubebuilder:validation:Pattern=`^[\-._a-zA-Z0-9]+$`
6266
// +optional
6367
ValuesKey string `json:"valuesKey,omitempty"`
6468

6569
// TargetPath is the YAML dot notation path the value should be merged at. When
6670
// set, the ValuesKey is expected to be a single flat value. Defaults to 'None',
6771
// which results in the values getting merged at the root.
72+
// +kubebuilder:validation:MaxLength=250
73+
// +kubebuilder:validation:Pattern=`^([a-zA-Z0-9_\-.\\\/]|\[[0-9]{1,5}\])+$`
6874
// +optional
6975
TargetPath string `json:"targetPath,omitempty"`
7076

config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml

+6
Original file line numberDiff line numberDiff line change
@@ -697,10 +697,16 @@ spec:
697697
should be merged at. When set, the ValuesKey is expected to
698698
be a single flat value. Defaults to 'None', which results
699699
in the values getting merged at the root.
700+
maxLength: 250
701+
pattern: ^([a-zA-Z0-9_\-.\\\/]|\[[0-9]{1,5}\])+$
700702
type: string
701703
valuesKey:
702704
description: ValuesKey is the data key where the values.yaml
703705
or a specific value can be found at. Defaults to 'values.yaml'.
706+
When set, must be a valid Data Key, consisting of alphanumeric
707+
characters, '-', '_' or '.'.
708+
maxLength: 253
709+
pattern: ^[\-._a-zA-Z0-9]+$
704710
type: string
705711
required:
706712
- kind

controllers/helmrelease_controller_test.go

+205
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,17 @@ package controllers
1919
import (
2020
"context"
2121
"reflect"
22+
"strings"
2223
"testing"
24+
"time"
2325

2426
"github.com/go-logr/logr"
2527
"helm.sh/helm/v3/pkg/chartutil"
2628
corev1 "k8s.io/api/core/v1"
2729
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
2830
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2931
"k8s.io/apimachinery/pkg/runtime"
32+
"sigs.k8s.io/controller-runtime/pkg/client"
3033
"sigs.k8s.io/controller-runtime/pkg/client/fake"
3134
"sigs.k8s.io/yaml"
3235

@@ -278,6 +281,208 @@ invalid`,
278281
}
279282
}
280283

284+
func TestValidation(t *testing.T) {
285+
tests := []struct {
286+
name string
287+
resources []runtime.Object
288+
references []v2.ValuesReference
289+
values string
290+
wantErr bool
291+
}{
292+
{
293+
name: "valid ValuesKey",
294+
resources: []runtime.Object{
295+
valuesSecret("values", map[string][]byte{"single": []byte("true")}),
296+
},
297+
references: []v2.ValuesReference{
298+
{
299+
Kind: "Secret",
300+
Name: "values",
301+
ValuesKey: "any-key_na.me",
302+
},
303+
},
304+
wantErr: false,
305+
},
306+
{
307+
name: "valid ValuesKey: empty",
308+
resources: []runtime.Object{
309+
valuesSecret("values", map[string][]byte{"single": []byte("true")}),
310+
},
311+
references: []v2.ValuesReference{
312+
{
313+
Kind: "Secret",
314+
Name: "values",
315+
ValuesKey: "",
316+
},
317+
},
318+
wantErr: false,
319+
},
320+
{
321+
name: "valid ValuesKey: long",
322+
resources: []runtime.Object{
323+
valuesSecret("values", map[string][]byte{"single": []byte("true")}),
324+
},
325+
references: []v2.ValuesReference{
326+
{
327+
Kind: "Secret",
328+
Name: "values",
329+
ValuesKey: func() string { return strings.Repeat("a", 253) }(),
330+
},
331+
},
332+
wantErr: false,
333+
},
334+
{
335+
name: "invalid ValuesKey",
336+
resources: []runtime.Object{
337+
valuesSecret("values", map[string][]byte{"single": []byte("true")}),
338+
},
339+
references: []v2.ValuesReference{
340+
{
341+
Kind: "Secret",
342+
Name: "values",
343+
ValuesKey: "a($&^%b",
344+
},
345+
},
346+
wantErr: true,
347+
},
348+
{
349+
name: "invalid ValuesKey: too long",
350+
resources: []runtime.Object{
351+
valuesSecret("values", map[string][]byte{"single": []byte("true")}),
352+
},
353+
references: []v2.ValuesReference{
354+
{
355+
Kind: "Secret",
356+
Name: "values",
357+
ValuesKey: func() string { return strings.Repeat("a", 254) }(),
358+
},
359+
},
360+
wantErr: true,
361+
},
362+
{
363+
name: "valid target path: empty",
364+
resources: []runtime.Object{
365+
valuesSecret("values", map[string][]byte{"single": []byte("true")}),
366+
},
367+
references: []v2.ValuesReference{
368+
{
369+
Kind: "Secret",
370+
Name: "values",
371+
TargetPath: "",
372+
},
373+
},
374+
wantErr: false,
375+
},
376+
{
377+
name: "valid target path",
378+
resources: []runtime.Object{
379+
valuesSecret("values", map[string][]byte{"single": []byte("true")}),
380+
},
381+
references: []v2.ValuesReference{
382+
{
383+
Kind: "Secret",
384+
Name: "values",
385+
TargetPath: "list_with.nested-values.and.index[0]",
386+
},
387+
},
388+
wantErr: false,
389+
},
390+
{
391+
name: "valid target path: long",
392+
resources: []runtime.Object{
393+
valuesSecret("values", map[string][]byte{"single": []byte("true")}),
394+
},
395+
references: []v2.ValuesReference{
396+
{
397+
Kind: "Secret",
398+
Name: "values",
399+
TargetPath: func() string { return strings.Repeat("a", 250) }(),
400+
},
401+
},
402+
wantErr: false,
403+
},
404+
{
405+
name: "invalid target path: too long",
406+
resources: []runtime.Object{
407+
valuesSecret("values", map[string][]byte{"single": []byte("true")}),
408+
},
409+
references: []v2.ValuesReference{
410+
{
411+
Kind: "Secret",
412+
Name: "values",
413+
TargetPath: func() string { return strings.Repeat("a", 251) }(),
414+
},
415+
},
416+
wantErr: true,
417+
},
418+
{
419+
name: "invalid target path: wrong index format",
420+
resources: []runtime.Object{
421+
valuesSecret("values", map[string][]byte{"single": []byte("true")}),
422+
},
423+
references: []v2.ValuesReference{
424+
{
425+
Kind: "Secret",
426+
Name: "values",
427+
ValuesKey: "single",
428+
TargetPath: "a[",
429+
},
430+
},
431+
wantErr: true,
432+
},
433+
{
434+
name: "invalid target path",
435+
resources: []runtime.Object{
436+
valuesSecret("values", map[string][]byte{"single": []byte("true")}),
437+
},
438+
references: []v2.ValuesReference{
439+
{
440+
Kind: "Secret",
441+
Name: "values",
442+
ValuesKey: "single",
443+
TargetPath: "a[",
444+
},
445+
},
446+
wantErr: true,
447+
},
448+
}
449+
450+
for _, tt := range tests {
451+
t.Run(tt.name, func(t *testing.T) {
452+
var values *apiextensionsv1.JSON
453+
if tt.values != "" {
454+
v, _ := yaml.YAMLToJSON([]byte(tt.values))
455+
values = &apiextensionsv1.JSON{Raw: v}
456+
}
457+
458+
hr := v2.HelmRelease{
459+
ObjectMeta: metav1.ObjectMeta{
460+
Name: "test",
461+
Namespace: "default",
462+
},
463+
Spec: v2.HelmReleaseSpec{
464+
Interval: metav1.Duration{Duration: 5 * time.Minute},
465+
Chart: v2.HelmChartTemplate{
466+
Spec: v2.HelmChartTemplateSpec{
467+
SourceRef: v2.CrossNamespaceObjectReference{
468+
Name: "something",
469+
},
470+
},
471+
},
472+
ValuesFrom: tt.references,
473+
Values: values,
474+
},
475+
}
476+
477+
err := k8sClient.Create(context.TODO(), &hr, client.DryRunAll)
478+
if (err != nil) != tt.wantErr {
479+
t.Errorf("composeValues() error = %v, wantErr %v", err, tt.wantErr)
480+
return
481+
}
482+
})
483+
}
484+
}
485+
281486
func valuesSecret(name string, data map[string][]byte) *corev1.Secret {
282487
return &corev1.Secret{
283488
ObjectMeta: metav1.ObjectMeta{Name: name},

controllers/suite_test.go

+19-38
Original file line numberDiff line numberDiff line change
@@ -17,67 +17,48 @@ limitations under the License.
1717
package controllers
1818

1919
import (
20+
"fmt"
21+
"os"
2022
"path/filepath"
2123
"testing"
2224

23-
. "github.com/onsi/ginkgo"
24-
. "github.com/onsi/gomega"
25+
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
2526
"k8s.io/client-go/kubernetes/scheme"
2627
"k8s.io/client-go/rest"
2728
"sigs.k8s.io/controller-runtime/pkg/client"
2829
"sigs.k8s.io/controller-runtime/pkg/envtest"
29-
"sigs.k8s.io/controller-runtime/pkg/envtest/printer"
30-
logf "sigs.k8s.io/controller-runtime/pkg/log"
31-
"sigs.k8s.io/controller-runtime/pkg/log/zap"
3230

3331
"github.com/fluxcd/helm-controller/api/v2beta1"
3432
// +kubebuilder:scaffold:imports
3533
)
3634

37-
// These tests use Ginkgo (BDD-style Go testing framework). Refer to
38-
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
39-
4035
var cfg *rest.Config
4136
var k8sClient client.Client
4237
var testEnv *envtest.Environment
4338

44-
func TestAPIs(t *testing.T) {
45-
RegisterFailHandler(Fail)
46-
47-
RunSpecsWithDefaultAndCustomReporters(t,
48-
"Controller Suite",
49-
[]Reporter{printer.NewlineReporter{}})
50-
}
51-
52-
var _ = BeforeSuite(func(done Done) {
53-
logf.SetLogger(
54-
zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)),
55-
)
56-
57-
By("bootstrapping test environment")
39+
func TestMain(m *testing.M) {
5840
testEnv = &envtest.Environment{
5941
CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")},
6042
}
6143

6244
var err error
6345
cfg, err = testEnv.Start()
64-
Expect(err).ToNot(HaveOccurred())
65-
Expect(cfg).ToNot(BeNil())
66-
67-
err = v2beta1.AddToScheme(scheme.Scheme)
68-
Expect(err).NotTo(HaveOccurred())
69-
70-
// +kubebuilder:scaffold:scheme
46+
if err != nil {
47+
panic(fmt.Errorf("failed to start testenv: %v", err))
48+
}
7149

50+
utilruntime.Must(v2beta1.AddToScheme(scheme.Scheme))
7251
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
73-
Expect(err).ToNot(HaveOccurred())
74-
Expect(k8sClient).ToNot(BeNil())
52+
if err != nil {
53+
panic(fmt.Errorf("failed to create k8s client: %v", err))
54+
}
7555

76-
close(done)
77-
}, 60)
56+
code := m.Run()
7857

79-
var _ = AfterSuite(func() {
80-
By("tearing down the test environment")
81-
err := testEnv.Stop()
82-
Expect(err).ToNot(HaveOccurred())
83-
})
58+
err = testEnv.Stop()
59+
if err != nil {
60+
panic(fmt.Errorf("failed to stop testenv: %v", err))
61+
}
62+
63+
os.Exit(code)
64+
}

docs/api/helmrelease.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -2092,7 +2092,9 @@ string
20922092
<td>
20932093
<em>(Optional)</em>
20942094
<p>ValuesKey is the data key where the values.yaml or a specific value can be
2095-
found at. Defaults to &lsquo;values.yaml&rsquo;.</p>
2095+
found at. Defaults to &lsquo;values.yaml&rsquo;.
2096+
When set, must be a valid Data Key, consisting of alphanumeric characters,
2097+
&lsquo;-&rsquo;, &lsquo;_&rsquo; or &lsquo;.&rsquo;.</p>
20962098
</td>
20972099
</tr>
20982100
<tr>

0 commit comments

Comments
 (0)