Skip to content

Commit 3e31634

Browse files
committed
Add validation of managed labels/annotations
Part of kubernetes-retired#47. This adds validation of managed labels and annotations. Tested: Added unit tests for validations. Ran e2e-tests, but no additions/modifications to those in this PR.
1 parent aeca2e7 commit 3e31634

File tree

2 files changed

+118
-0
lines changed

2 files changed

+118
-0
lines changed

internal/hierarchyconfig/validator.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@ import (
1212
authzv1 "k8s.io/api/authorization/v1"
1313
corev1 "k8s.io/api/core/v1"
1414
"k8s.io/apimachinery/pkg/api/errors"
15+
apierrors "k8s.io/apimachinery/pkg/api/errors"
1516
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1617
"k8s.io/apimachinery/pkg/runtime/schema"
1718
"k8s.io/apimachinery/pkg/types"
19+
"k8s.io/apimachinery/pkg/util/validation"
20+
"k8s.io/apimachinery/pkg/util/validation/field"
1821
"sigs.k8s.io/controller-runtime/pkg/client"
1922
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
2023

@@ -129,6 +132,13 @@ func (v *Validator) handle(ctx context.Context, log logr.Logger, req *request) a
129132
return webhooks.Deny(metav1.StatusReasonForbidden, reason)
130133
}
131134

135+
allErrs := validateManagedMeta(req.hc)
136+
if len(allErrs) > 0 {
137+
gk := schema.GroupKind{Group: api.GroupVersion.Group, Kind: "HierarchyConfiguration"}
138+
err := apierrors.NewInvalid(gk, req.hc.Name, allErrs)
139+
return webhooks.DenyFromAPIError(err)
140+
}
141+
132142
// Do all checks that require holding the in-memory lock. Generate a list of server checks we
133143
// should perform once the lock is released.
134144
serverChecks, resp := v.checkForest(log, req.hc)
@@ -514,6 +524,49 @@ func (r *realClient) IsAdmin(ctx context.Context, ui *authnv1.UserInfo, nnm stri
514524
return sar.Status.Allowed, err
515525
}
516526

527+
func validateManagedMeta(hc *api.HierarchyConfiguration) field.ErrorList {
528+
allErrs := field.ErrorList{}
529+
530+
fldPath := field.NewPath("spec", "labels")
531+
for _, l := range hc.Spec.Labels {
532+
if fldErr := validateMetaKey(l.Key, fldPath); fldErr != nil {
533+
allErrs = append(allErrs, fldErr)
534+
} else if !config.IsManagedLabel(l.Key) { // Only validate managed if key is valid
535+
fldErr := field.Invalid(fldPath, l.Key, "not a managed label and cannot be configured")
536+
allErrs = append(allErrs, fldErr)
537+
}
538+
if fldErr := validateLabelValue(l.Key, l.Value, fldPath); fldErr != nil {
539+
allErrs = append(allErrs, fldErr)
540+
}
541+
}
542+
543+
fldPath = field.NewPath("spec", "annotations")
544+
for _, a := range hc.Spec.Annotations {
545+
if fldErr := validateMetaKey(a.Key, fldPath); fldErr != nil {
546+
allErrs = append(allErrs, fldErr)
547+
} else if !config.IsManagedAnnotation(a.Key) { // Only validate managed if key is valid
548+
fldErr := field.Invalid(fldPath, a.Key, "not a managed annotation and cannot be configured")
549+
allErrs = append(allErrs, fldErr)
550+
}
551+
// No validation of annotation values; only limitation seems to be the total length (256Kb) of all annotations
552+
}
553+
return allErrs
554+
}
555+
556+
func validateMetaKey(k string, path *field.Path) *field.Error {
557+
if errs := validation.IsQualifiedName(k); len(errs) != 0 {
558+
return field.Invalid(path, k, strings.Join(errs, "; "))
559+
}
560+
return nil
561+
}
562+
563+
func validateLabelValue(k, v string, path *field.Path) *field.Error {
564+
if errs := validation.IsValidLabelValue(v); len(errs) != 0 {
565+
return field.Invalid(path.Key(k), v, strings.Join(errs, "; "))
566+
}
567+
return nil
568+
}
569+
517570
// allow is a replacement for controller-runtime's admission.Allowed() that allows you to set the
518571
// message (human-readable) as opposed to the reason (machine-readable).
519572
func allow(msg string) admission.Response {

internal/hierarchyconfig/validator_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,71 @@ import (
1717
"sigs.k8s.io/hierarchical-namespaces/internal/objects"
1818
)
1919

20+
func TestManagedMeta(t *testing.T) {
21+
f := foresttest.Create("-") // a
22+
h := &Validator{Forest: f}
23+
l := zap.New()
24+
// For this test we accept any label or annotation not starting with 'h',
25+
// to allow almost any meta - except the hnc.x-k8s.io labels/annotations,
26+
// which cannot be managed anyway. And allows us to use that for testing.
27+
if err := config.SetManagedMeta([]string{"[^h].*"}, []string{"[^h].*"}); err != nil {
28+
panic(err)
29+
}
30+
defer func() {
31+
_ = config.SetManagedMeta(nil, nil)
32+
}()
33+
34+
tests := []struct {
35+
name string
36+
labels []api.MetaKVP
37+
annotations []api.MetaKVP
38+
allowed bool
39+
}{
40+
{name: "managed label", labels: []api.MetaKVP{{Key: "label.com/team"}}, allowed: true},
41+
{name: "unmanaged label", labels: []api.MetaKVP{{Key: api.LabelIncludedNamespace}}},
42+
{name: "managed annotation", annotations: []api.MetaKVP{{Key: "annot.com/log-index"}}, allowed: true},
43+
{name: "unmanaged annotation", annotations: []api.MetaKVP{{Key: api.AnnotationManagedBy}}},
44+
45+
{name: "ok: prefixed label key", labels: []api.MetaKVP{{Key: "foo.bar/team", Value: "v"}}, allowed: true},
46+
{name: "ok: bare label key", labels: []api.MetaKVP{{Key: "team", Value: "v"}}, allowed: true},
47+
{name: "invalid: label prefix key", labels: []api.MetaKVP{{Key: "foo;bar/team", Value: "v"}}},
48+
{name: "invalid: label name key", labels: []api.MetaKVP{{Key: "foo.bar/-team", Value: "v"}}},
49+
{name: "invalid: empty label key", labels: []api.MetaKVP{{Key: "", Value: "v"}}},
50+
51+
{name: "ok: label value", labels: []api.MetaKVP{{Key: "k", Value: "foo"}}, allowed: true},
52+
{name: "ok: empty label value", labels: []api.MetaKVP{{Key: "k", Value: ""}}, allowed: true},
53+
{name: "ok: label value special char", labels: []api.MetaKVP{{Key: "k", Value: "f-oo"}}, allowed: true},
54+
{name: "invalid: label value", labels: []api.MetaKVP{{Key: "k", Value: "-foo"}}},
55+
56+
{name: "ok: prefixed annotation key", annotations: []api.MetaKVP{{Key: "foo.bar/team", Value: "v"}}, allowed: true},
57+
{name: "ok: bare annotation key", annotations: []api.MetaKVP{{Key: "team", Value: "v"}}, allowed: true},
58+
{name: "invalid: annotation prefix key", annotations: []api.MetaKVP{{Key: "foo;bar/team", Value: "v"}}},
59+
{name: "invalid: annotation name key", annotations: []api.MetaKVP{{Key: "foo.bar/-team", Value: "v"}}},
60+
{name: "invalid: empty annotation key", annotations: []api.MetaKVP{{Key: "", Value: "v"}}},
61+
62+
{name: "ok: annotation value", annotations: []api.MetaKVP{{Key: "k", Value: "foo"}}, allowed: true},
63+
{name: "ok: empty annotation value", annotations: []api.MetaKVP{{Key: "k", Value: ""}}, allowed: true},
64+
{name: "ok: special annotation value", annotations: []api.MetaKVP{{Key: "k", Value: ";$+:;/*'\""}}, allowed: true},
65+
}
66+
for _, tc := range tests {
67+
t.Run(tc.name, func(t *testing.T) {
68+
// Setup
69+
g := NewWithT(t)
70+
hc := &api.HierarchyConfiguration{Spec: api.HierarchyConfigurationSpec{}}
71+
hc.ObjectMeta.Name = api.Singleton
72+
hc.ObjectMeta.Namespace = "a"
73+
hc.Spec.Labels = tc.labels
74+
hc.Spec.Annotations = tc.annotations
75+
req := &request{hc: hc}
76+
77+
got := h.handle(context.Background(), l, req)
78+
79+
logResult(t, got.AdmissionResponse.Result)
80+
g.Expect(got.AdmissionResponse.Allowed).Should(Equal(tc.allowed))
81+
})
82+
}
83+
}
84+
2085
func TestStructure(t *testing.T) {
2186
f := foresttest.Create("-a-") // a <- b; c
2287
h := &Validator{Forest: f}

0 commit comments

Comments
 (0)