Skip to content

Commit b9c5dcf

Browse files
committed
Add validation of managed labels/annotations
Closes kubernetes-retired#128. Part of kubernetes-retired#47. This adds validation of managed labels and annotations. Integration tests should be added for this new feature overall, that includes verifying that validations work. Tested: Added unit tests for validations. Ran integration tests, but no additions to them in this PR.
1 parent aeca2e7 commit b9c5dcf

File tree

2 files changed

+145
-10
lines changed

2 files changed

+145
-10
lines changed

internal/hierarchyconfig/validator.go

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,16 @@ import (
77
"strings"
88

99
"github.com/go-logr/logr"
10-
k8sadm "k8s.io/api/admission/v1"
1110
authnv1 "k8s.io/api/authentication/v1"
1211
authzv1 "k8s.io/api/authorization/v1"
1312
corev1 "k8s.io/api/core/v1"
1413
"k8s.io/apimachinery/pkg/api/errors"
14+
apierrors "k8s.io/apimachinery/pkg/api/errors"
1515
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1616
"k8s.io/apimachinery/pkg/runtime/schema"
1717
"k8s.io/apimachinery/pkg/types"
18+
"k8s.io/apimachinery/pkg/util/validation"
19+
"k8s.io/apimachinery/pkg/util/validation/field"
1820
"sigs.k8s.io/controller-runtime/pkg/client"
1921
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
2022

@@ -129,6 +131,38 @@ func (v *Validator) handle(ctx context.Context, log logr.Logger, req *request) a
129131
return webhooks.Deny(metav1.StatusReasonForbidden, reason)
130132
}
131133

134+
allErrs := field.ErrorList{}
135+
136+
fldPath := field.NewPath("spec", "labels")
137+
for _, l := range req.hc.Spec.Labels {
138+
if err := validateMetaKey(l.Key, fldPath); err != nil {
139+
allErrs = append(allErrs, err)
140+
} else if !config.IsManagedLabel(l.Key) { // Only validate managed if key is valid
141+
fldErr := field.Invalid(fldPath, l.Key, "not a managed label and cannot be configured")
142+
allErrs = append(allErrs, fldErr)
143+
}
144+
if err := validateLabelValue(l.Key, l.Value, fldPath); err != nil {
145+
allErrs = append(allErrs, err)
146+
}
147+
}
148+
149+
fldPath = field.NewPath("spec", "annotations")
150+
for _, a := range req.hc.Spec.Annotations {
151+
if err := validateMetaKey(a.Key, fldPath); err != nil {
152+
allErrs = append(allErrs, err)
153+
} else if !config.IsManagedAnnotation(a.Key) { // Only validate managed if key is valid
154+
fldErr := field.Invalid(fldPath, a.Key, "not a managed annotation and cannot be configured")
155+
allErrs = append(allErrs, fldErr)
156+
}
157+
// No validation for annotation values; only limitation seems to be the total length (256Kb) of all annotations
158+
}
159+
160+
if len(allErrs) > 0 {
161+
gk := schema.GroupKind{Group: api.GroupVersion.Group, Kind: "HierarchyConfiguration"}
162+
err := apierrors.NewInvalid(gk, req.hc.Name, allErrs)
163+
return webhooks.DenyFromAPIError(err)
164+
}
165+
132166
// Do all checks that require holding the in-memory lock. Generate a list of server checks we
133167
// should perform once the lock is released.
134168
serverChecks, resp := v.checkForest(log, req.hc)
@@ -514,14 +548,20 @@ func (r *realClient) IsAdmin(ctx context.Context, ui *authnv1.UserInfo, nnm stri
514548
return sar.Status.Allowed, err
515549
}
516550

517-
// allow is a replacement for controller-runtime's admission.Allowed() that allows you to set the
518-
// message (human-readable) as opposed to the reason (machine-readable).
551+
func validateMetaKey(k string, path *field.Path) *field.Error {
552+
if errs := validation.IsQualifiedName(k); len(errs) != 0 {
553+
return field.Invalid(path, k, strings.Join(errs, "; "))
554+
}
555+
return nil
556+
}
557+
558+
func validateLabelValue(k, v string, path *field.Path) *field.Error {
559+
if errs := validation.IsValidLabelValue(v); len(errs) != 0 {
560+
return field.Invalid(path.Key(k), v, strings.Join(errs, "; "))
561+
}
562+
return nil
563+
}
564+
519565
func allow(msg string) admission.Response {
520-
return admission.Response{AdmissionResponse: k8sadm.AdmissionResponse{
521-
Allowed: true,
522-
Result: &metav1.Status{
523-
Code: 0,
524-
Message: msg,
525-
},
526-
}}
566+
return webhooks.Allow(msg)
527567
}

internal/hierarchyconfig/validator_test.go

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

20+
func TestManagedMetaPermissions(t *testing.T) {
21+
f := foresttest.Create("-") // a
22+
h := &Validator{Forest: f}
23+
l := zap.New()
24+
25+
tests := []struct {
26+
name string
27+
labels []api.MetaKVP
28+
annotations []api.MetaKVP
29+
allowed bool
30+
}{
31+
{name: "managed label", labels: []api.MetaKVP{{Key: "label.com/team"}}, allowed: true},
32+
{name: "unmanaged label", labels: []api.MetaKVP{{Key: "kubernetes.io/metadata.name"}}},
33+
{name: "managed annotation", annotations: []api.MetaKVP{{Key: "annot.com/log-index"}}, allowed: true},
34+
{name: "unmanaged annotation", annotations: []api.MetaKVP{{Key: "openshift.io/sa.scc.uid-range"}}},
35+
}
36+
for _, tc := range tests {
37+
t.Run(tc.name, func(t *testing.T) {
38+
// Setup
39+
g := NewWithT(t)
40+
// In this test we only allow labels/annotations with the defined prefixes
41+
err := config.SetManagedMeta([]string{"label\\.com/.*"}, []string{"annot\\.com/.*"})
42+
g.Expect(err).ToNot(HaveOccurred())
43+
44+
hc := &api.HierarchyConfiguration{Spec: api.HierarchyConfigurationSpec{}}
45+
hc.ObjectMeta.Name = api.Singleton
46+
hc.ObjectMeta.Namespace = "a"
47+
hc.Spec.Labels = tc.labels
48+
hc.Spec.Annotations = tc.annotations
49+
req := &request{hc: hc}
50+
51+
got := h.handle(context.Background(), l, req)
52+
53+
logResult(t, got.AdmissionResponse.Result)
54+
g.Expect(got.AdmissionResponse.Allowed).Should(Equal(tc.allowed))
55+
})
56+
}
57+
}
58+
59+
func TestManagedMetaSyntax(t *testing.T) {
60+
f := foresttest.Create("-") // a
61+
h := &Validator{Forest: f}
62+
l := zap.New()
63+
64+
tests := []struct {
65+
name string
66+
labels []api.MetaKVP
67+
annotations []api.MetaKVP
68+
allowed bool
69+
}{
70+
{name: "ok: prefixed label key", labels: []api.MetaKVP{{Key: "foo.bar/team", Value: "v"}}, allowed: true},
71+
{name: "ok: bare label key", labels: []api.MetaKVP{{Key: "team", Value: "v"}}, allowed: true},
72+
{name: "invalid: label prefix key", labels: []api.MetaKVP{{Key: "foo;bar/team", Value: "v"}}},
73+
{name: "invalid: label name key", labels: []api.MetaKVP{{Key: "foo.bar/-team", Value: "v"}}},
74+
{name: "invalid: empty label key", labels: []api.MetaKVP{{Key: "", Value: "v"}}},
75+
76+
{name: "ok: label value", labels: []api.MetaKVP{{Key: "k", Value: "foo"}}, allowed: true},
77+
{name: "ok: empty label value", labels: []api.MetaKVP{{Key: "k", Value: ""}}, allowed: true},
78+
{name: "ok: label value special char", labels: []api.MetaKVP{{Key: "k", Value: "f-oo"}}, allowed: true},
79+
{name: "invalid: label value", labels: []api.MetaKVP{{Key: "k", Value: "-foo"}}},
80+
81+
{name: "ok: prefixed annotation key", annotations: []api.MetaKVP{{Key: "foo.bar/team", Value: "v"}}, allowed: true},
82+
{name: "ok: bare annotation key", annotations: []api.MetaKVP{{Key: "team", Value: "v"}}, allowed: true},
83+
{name: "invalid: annotation prefix key", annotations: []api.MetaKVP{{Key: "foo;bar/team", Value: "v"}}},
84+
{name: "invalid: annotation name key", annotations: []api.MetaKVP{{Key: "foo.bar/-team", Value: "v"}}},
85+
{name: "invalid: empty annotation key", annotations: []api.MetaKVP{{Key: "", Value: "v"}}},
86+
87+
{name: "ok: annotation value", annotations: []api.MetaKVP{{Key: "k", Value: "foo"}}, allowed: true},
88+
{name: "ok: empty annotation value", annotations: []api.MetaKVP{{Key: "k", Value: ""}}, allowed: true},
89+
{name: "ok: special annotation value", annotations: []api.MetaKVP{{Key: "k", Value: ";$+:;/*'\""}}, allowed: true},
90+
}
91+
for _, tc := range tests {
92+
t.Run(tc.name, func(t *testing.T) {
93+
// Setup
94+
g := NewWithT(t)
95+
// For this test we accept any label or annotation not starting with 'h'
96+
// to allow almost anything - except the hnc.x-k8s.io labels/annotations
97+
err := config.SetManagedMeta([]string{"[^h].*"}, []string{"[^h].*"})
98+
g.Expect(err).ToNot(HaveOccurred())
99+
100+
hc := &api.HierarchyConfiguration{Spec: api.HierarchyConfigurationSpec{}}
101+
hc.ObjectMeta.Name = api.Singleton
102+
hc.ObjectMeta.Namespace = "a"
103+
hc.Spec.Labels = tc.labels
104+
hc.Spec.Annotations = tc.annotations
105+
req := &request{hc: hc}
106+
107+
got := h.handle(context.Background(), l, req)
108+
109+
logResult(t, got.AdmissionResponse.Result)
110+
g.Expect(got.AdmissionResponse.Allowed).Should(Equal(tc.allowed))
111+
})
112+
}
113+
}
114+
20115
func TestStructure(t *testing.T) {
21116
f := foresttest.Create("-a-") // a <- b; c
22117
h := &Validator{Forest: f}

0 commit comments

Comments
 (0)