Skip to content

Commit 4d7f733

Browse files
committed
Allow denying special case of empty node selector on namespace
1 parent 52f59a8 commit 4d7f733

File tree

5 files changed

+57
-14
lines changed

5 files changed

+57
-14
lines changed

config.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ type Config struct {
2626
// AllowedNodeSelectors is a map of allowed node selectors.
2727
// Both the key and the value are anchored regexes.
2828
AllowedNodeSelectors map[string]string
29+
30+
// NamespaceDenyEmptyNodeSelector is a flag to enable or disable the rejection of empty node selectors on namespaces.
31+
// If true this will reject a { "openshift.io/node-selector": "" } annotation.
32+
NamespaceDenyEmptyNodeSelector bool
2933
}
3034

3135
func ConfigFromFile(path string) (Config, error) {

config.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,6 @@ PrivilegedClusterRoles:
1616
# Key and values are anchored regexes
1717
AllowedNodeSelectors:
1818
appuio.io/node-class: flex|plus
19+
# NamespaceDenyEmptyNodeSelector is a flag to enable or disable the rejection of empty node selectors on namespaces.
20+
# If true this will reject a { "openshift.io/node-selector": "" } annotation.
21+
NamespaceDenyEmptyNodeSelector: false

main.go

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -89,14 +89,7 @@ func main() {
8989
PrivilegedGroups: conf.PrivilegedGroups,
9090
PrivilegedClusterRoles: conf.PrivilegedClusterRoles,
9191
}
92-
ans := &validate.AllowedLabels{}
93-
for k, v := range conf.AllowedNodeSelectors {
94-
if err := ans.Add(k, v); err != nil {
95-
setupLog.Error(err, "unable to add allowed node selector")
96-
os.Exit(1)
97-
}
98-
}
99-
registerNodeSelectorValidationWebhooks(mgr, psk, ans)
92+
registerNodeSelectorValidationWebhooks(mgr, psk, conf)
10093

10194
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
10295
setupLog.Error(err, "unable to setup health endpoint")
@@ -114,17 +107,26 @@ func main() {
114107
}
115108
}
116109

117-
func registerNodeSelectorValidationWebhooks(mgr ctrl.Manager, skipper skipper.Skipper, allowedNodeSelectors *validate.AllowedLabels) {
110+
func registerNodeSelectorValidationWebhooks(mgr ctrl.Manager, skipper skipper.Skipper, conf Config) {
111+
ans := &validate.AllowedLabels{}
112+
for k, v := range conf.AllowedNodeSelectors {
113+
if err := ans.Add(k, v); err != nil {
114+
setupLog.Error(err, "unable to add allowed node selector")
115+
os.Exit(1)
116+
}
117+
}
118+
118119
mgr.GetWebhookServer().Register("/validate-namespace-node-selector", &webhook.Admission{
119120
Handler: &webhooks.NamespaceNodeSelectorValidator{
120-
Skipper: skipper,
121-
AllowedNodeSelectors: allowedNodeSelectors,
121+
Skipper: skipper,
122+
AllowedNodeSelectors: ans,
123+
DenyEmptyNodeSelector: conf.NamespaceDenyEmptyNodeSelector,
122124
},
123125
})
124126
mgr.GetWebhookServer().Register("/validate-workload-node-selector", &webhook.Admission{
125127
Handler: &webhooks.WorkloadNodeSelectorValidator{
126128
Skipper: skipper,
127-
AllowedNodeSelectors: allowedNodeSelectors,
129+
AllowedNodeSelectors: ans,
128130
},
129131
})
130132
}

webhooks/namespace_node_selector_validator.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ type NamespaceNodeSelectorValidator struct {
2222

2323
Skipper skipper.Skipper
2424
AllowedNodeSelectors *validate.AllowedLabels
25+
26+
// DenyEmptyNodeSelector denies namespaces with an existing but empty node selector.
27+
DenyEmptyNodeSelector bool
2528
}
2629

2730
// Handle handles the admission requests
@@ -53,11 +56,15 @@ func (v *NamespaceNodeSelectorValidator) Handle(ctx context.Context, req admissi
5356
return admission.Errored(400, err)
5457
}
5558

56-
rawSel := ns.Annotations[OpenshiftNodeSelectorAnnotation]
57-
if rawSel == "" {
59+
rawSel, exists := ns.Annotations[OpenshiftNodeSelectorAnnotation]
60+
if !exists {
5861
l.V(1).Info("allowed: no node selector")
5962
return admission.Allowed("no node selector")
6063
}
64+
if rawSel == "" && v.DenyEmptyNodeSelector {
65+
l.V(1).Info("denied: empty node selector")
66+
return admission.Denied("empty `\"\"` node selector")
67+
}
6168

6269
sel, err := labels.ConvertSelectorToLabelsMap(rawSel)
6370
if err != nil {

webhooks/namespace_node_selector_validator_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,33 @@ func Test_NamespaceNodeSelectorValidator_Handle(t *testing.T) {
4646
}
4747
}
4848

49+
func Test_NamespaceNodeSelectorValidator_DenyEmpty(t *testing.T) {
50+
allowed := &validate.AllowedLabels{}
51+
require.NoError(t, allowed.Add("appuio.io/node-class", "flex|plus"))
52+
53+
subject := NamespaceNodeSelectorValidator{
54+
AllowedNodeSelectors: allowed,
55+
Skipper: skipper.StaticSkipper{},
56+
}
57+
require.NoError(t, subject.InjectDecoder(decoder(t)))
58+
59+
empty := map[string]string{OpenshiftNodeSelectorAnnotation: ""}
60+
61+
t.Run("Allow", func(t *testing.T) {
62+
subject.DenyEmptyNodeSelector = false
63+
resp := subject.Handle(context.Background(), admissionRequestForObject(t, newNamespace("test", nil, empty)))
64+
t.Log("Response:", resp.Result.Reason, resp.Result.Message)
65+
require.True(t, resp.Allowed)
66+
})
67+
68+
t.Run("Deny", func(t *testing.T) {
69+
subject.DenyEmptyNodeSelector = true
70+
resp := subject.Handle(context.Background(), admissionRequestForObject(t, newNamespace("test", nil, empty)))
71+
t.Log("Response:", resp.Result.Reason, resp.Result.Message)
72+
require.False(t, resp.Allowed)
73+
})
74+
}
75+
4976
func newNamespace(name string, labels, annotations map[string]string) *corev1.Namespace {
5077
return &corev1.Namespace{
5178
TypeMeta: metav1.TypeMeta{

0 commit comments

Comments
 (0)