Skip to content
This repository was archived by the owner on Apr 17, 2025. It is now read-only.

Commit 880ca85

Browse files
committed
Handle bypassed webhook rules on excluded namespaces
Fixes 1443 Remove any HNC CRs (hierarchyconfiguration and anchor) from the excluded namespaces if they are successfully created bypassing the webhook. Set "ActivitiesHalted: IllegalParent" condition if a namespace sets an excluded namespace as a parent, e.g. ``` $ kubectl hns describe a Hierarchy configuration for namespace a Parent: kube-system Children: - d (subnamespace) Conditions: - ActivitiesHalted (IllegalParent): Parent "kube-system" is an excluded namespace ``` Subnamespace anchor using an excluded namespace name would get `Forbidden` state. Update the `kubectl hns describe` to show: ``` $ kubectl hns describe parent Hierarchy configuration for namespace parent No parent Children: - d (Conflict subnamespace) - e (subnamespace) - f - kube-system (Forbidden subnamespace) No conditions ``` Tested by `make test` since there's no wehbook in the integration tests.
1 parent 8730dce commit 880ca85

File tree

8 files changed

+140
-31
lines changed

8 files changed

+140
-31
lines changed

incubator/hnc/api/v1alpha2/hierarchy_types.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ const (
5959
ReasonDeletingCRD string = "DeletingCRD"
6060
ReasonInCycle string = "InCycle"
6161
ReasonParentMissing string = "ParentMissing"
62+
ReasonIllegalParent string = "IllegalParent"
6263
ReasonAnchorMissing string = "SubnamespaceAnchorMissing"
6364
)
6465

@@ -71,6 +72,7 @@ var AllConditions = map[string][]string{
7172
ReasonDeletingCRD,
7273
ReasonInCycle,
7374
ReasonParentMissing,
75+
ReasonIllegalParent,
7476
},
7577
ConditionBadConfiguration: {
7678
ReasonAnchorMissing,

incubator/hnc/internal/kubectl/describe.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ var describeCmd = &cobra.Command{
3939
nnm := args[0]
4040
fmt.Printf("Hierarchy configuration for namespace %s\n", nnm)
4141
hier := client.getHierarchy(nnm)
42-
anms := client.getAnchorNames(nnm)
42+
as := client.getAnchorStatus(nnm)
4343

4444
// Parent
4545
if hier.Spec.Parent != "" {
@@ -53,11 +53,11 @@ var describeCmd = &cobra.Command{
5353
for _, cn := range hier.Status.Children {
5454
childrenAndStatus[cn] = ""
5555
}
56-
for _, cn := range anms {
56+
for cn, st := range as {
5757
if _, ok := childrenAndStatus[cn]; ok {
5858
childrenAndStatus[cn] = "subnamespace"
5959
} else {
60-
childrenAndStatus[cn] = "missing subnamespace"
60+
childrenAndStatus[cn] = st + " subnamespace"
6161
}
6262
}
6363
if len(childrenAndStatus) > 0 {

incubator/hnc/internal/kubectl/root.go

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import (
2525
"github.com/spf13/cobra"
2626
"k8s.io/apimachinery/pkg/api/errors"
2727
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
28-
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2928
"k8s.io/apimachinery/pkg/runtime/serializer"
3029
"k8s.io/cli-runtime/pkg/genericclioptions"
3130
"k8s.io/client-go/kubernetes"
@@ -43,12 +42,13 @@ var rootCmd *cobra.Command
4342
var client Client
4443

4544
type realClient struct{}
45+
type anchorStatus map[string]string
4646

4747
type Client interface {
4848
getHierarchy(nnm string) *api.HierarchyConfiguration
4949
updateHierarchy(hier *api.HierarchyConfiguration, reason string)
5050
createAnchor(nnm string, hnnm string)
51-
getAnchorNames(nnm string) []string
51+
getAnchorStatus(nnm string) anchorStatus
5252
getHNCConfig() *api.HNCConfiguration
5353
updateHNCConfig(*api.HNCConfiguration)
5454
}
@@ -137,28 +137,25 @@ func (cl *realClient) getHierarchy(nnm string) *api.HierarchyConfiguration {
137137
return hier
138138
}
139139

140-
func (cl *realClient) getAnchorNames(nnm string) []string {
140+
func (cl *realClient) getAnchorStatus(nnm string) anchorStatus {
141141
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
142142
defer cancel()
143143

144-
var anms []string
145-
146144
// List all the anchors in the namespace.
147-
ul := &unstructured.UnstructuredList{}
148-
ul.SetKind(api.AnchorKind)
149-
ul.SetAPIVersion(api.AnchorAPIVersion)
150-
err := hncClient.Get().Resource(api.Anchors).Namespace(nnm).Do(ctx).Into(ul)
145+
al := &api.SubnamespaceAnchorList{}
146+
err := hncClient.Get().Resource(api.Anchors).Namespace(nnm).Do(ctx).Into(al)
151147
if err != nil && !errors.IsNotFound(err) {
152148
fmt.Printf("Error listing subnamespace anchors for %s: %s\n", nnm, err)
153149
os.Exit(1)
154150
}
155151

156-
// Create a list of strings of the anchor names.
157-
for _, inst := range ul.Items {
158-
anms = append(anms, inst.GetName())
152+
// Create the map from anchor names to their status.
153+
as := make(anchorStatus)
154+
for _, inst := range al.Items {
155+
as[inst.Name] = string(inst.Status.State)
159156
}
160157

161-
return anms
158+
return as
162159
}
163160

164161
func (cl *realClient) updateHierarchy(hier *api.HierarchyConfiguration, reason string) {

incubator/hnc/internal/reconcilers/anchor.go

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package reconcilers
1818
import (
1919
"context"
2020
"errors"
21+
"fmt"
2122

2223
"github.com/go-logr/logr"
2324
corev1 "k8s.io/api/core/v1"
@@ -72,12 +73,21 @@ func (r *AnchorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
7273
return ctrl.Result{}, err
7374
}
7475

75-
// Report "Forbidden" state and early exit if the namespace is not allowed to have subnamespaces
76-
// but has bypassed the webhook and successfully created the anchor. Forbidden anchors won't have
77-
// finalizers.
78-
// TODO refactor/split the ExcludedNamespaces map for 1) reconciler exclusion and 2) subnamespaces exclusion
79-
// purposes. See issue: https://github.com/kubernetes-sigs/multi-tenancy/issues/495
76+
// Always delete anchor (and any other HNC CRs) in the excluded namespaces and
77+
// early exit.
8078
if config.ExcludedNamespaces[pnm] {
79+
// Since the anchors in the excluded namespaces are never synced by HNC,
80+
// there are no finalizers on the anchors that we can delete them without
81+
// removing the finalizers first.
82+
log.Info("Deleting anchor in an excluded namespace")
83+
return ctrl.Result{}, r.deleteInstance(ctx, log, inst)
84+
}
85+
86+
// Report "Forbidden" state and early exit if the anchor name is an excluded
87+
// namespace that should not be created as a subnamespace, but the webhook has
88+
// been bypassed and the anchor has been successfully created. Forbidden
89+
// anchors won't have finalizers.
90+
if config.ExcludedNamespaces[nm] {
8191
inst.Status.State = api.Forbidden
8292
return ctrl.Result{}, r.writeInstance(ctx, log, inst)
8393
}
@@ -335,6 +345,15 @@ func (r *AnchorReconciler) writeInstance(ctx context.Context, log logr.Logger, i
335345
return nil
336346
}
337347

348+
// deleteInstance deletes the anchor instance. Note: Make sure there's no
349+
// finalizers on the instance before calling this function.
350+
func (r *AnchorReconciler) deleteInstance(ctx context.Context, log logr.Logger, inst *api.SubnamespaceAnchor) error {
351+
if err := r.Delete(ctx, inst); err != nil {
352+
return fmt.Errorf("while deleting on apiserver: %w", err)
353+
}
354+
return nil
355+
}
356+
338357
// getNamespace returns the namespace if it exists, or returns an invalid, blank, unnamed one if it
339358
// doesn't. This allows it to be trivially identified as a namespace that doesn't exist, and also
340359
// allows us to easily modify it if we want to create it.

incubator/hnc/internal/reconcilers/anchor_test.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ var _ = Describe("Anchor", func() {
2222
BeforeEach(func() {
2323
fooName = createNS(ctx, "foo")
2424
barName = createNSName("bar")
25+
config.ExcludedNamespaces = nil
2526
})
2627

2728
It("should create an subnamespace and update the hierarchy according to the anchor", func() {
@@ -50,11 +51,18 @@ var _ = Describe("Anchor", func() {
5051
Eventually(getAnchorState(ctx, fooName, barName)).Should(Equal(api.Ok))
5152
})
5253

53-
It("should set the anchor.status.state to Forbidden if the parent is not allowed to have subnamespaces", func() {
54+
It("should remove the anchor in an excluded namespace", func() {
5455
config.ExcludedNamespaces = map[string]bool{"kube-system": true}
5556
kube_system_anchor_bar := newAnchor(barName, "kube-system")
5657
updateAnchor(ctx, kube_system_anchor_bar)
57-
Eventually(getAnchorState(ctx, "kube-system", barName)).Should(Equal(api.Forbidden))
58+
Eventually(canGetAnchor(ctx, barName, "kube-system")).Should(Equal(false))
59+
})
60+
61+
It("should set the anchor.status.state to Forbidden if the subnamespace is an excluded namespace", func() {
62+
config.ExcludedNamespaces = map[string]bool{"kube-system": true}
63+
foo_anchor_kube_system := newAnchor("kube-system", fooName)
64+
updateAnchor(ctx, foo_anchor_kube_system)
65+
Eventually(getAnchorState(ctx, fooName, "kube-system")).Should(Equal(api.Forbidden))
5866
})
5967

6068
It("should set the anchor.status.state to Conflict if a namespace of the same name already exists", func() {
@@ -122,6 +130,17 @@ func getAnchorWithOffset(offset int, ctx context.Context, pnm, nm string) *api.S
122130
return anchor
123131
}
124132

133+
func canGetAnchor(ctx context.Context, pnm, nm string) func() bool {
134+
return func() bool {
135+
nsn := types.NamespacedName{Name: nm, Namespace: pnm}
136+
anchor := &api.SubnamespaceAnchor{}
137+
if err := k8sClient.Get(ctx, nsn, anchor); err != nil {
138+
return false
139+
}
140+
return true
141+
}
142+
}
143+
125144
func updateAnchor(ctx context.Context, anchor *api.SubnamespaceAnchor) {
126145
if anchor.CreationTimestamp.IsZero() {
127146
ExpectWithOffset(1, k8sClient.Create(ctx, anchor)).Should(Succeed())

incubator/hnc/internal/reconcilers/hierarchy_config.go

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -85,17 +85,21 @@ type HierarchyConfigReconciler struct {
8585

8686
// Reconcile sets up some basic variables and then calls the business logic.
8787
func (r *HierarchyConfigReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
88-
if config.ExcludedNamespaces[req.Namespace] {
89-
return ctrl.Result{}, nil
88+
ns := req.NamespacedName.Namespace
89+
log := loggerWithRID(r.Log).WithValues("ns", ns)
90+
91+
// Always delete hierarchyconfiguration (and any other HNC CRs) in the
92+
// excluded namespaces and early exit.
93+
if config.ExcludedNamespaces[ns] {
94+
// Since singletons in the excluded namespaces are never synced by HNC, there
95+
// are no finalizers on the singletons that we can delete them without
96+
// removing the finalizers first.
97+
return ctrl.Result{}, r.deleteSingletonIfExists(ctx, log, ns)
9098
}
9199

92100
stats.StartHierConfigReconcile()
93101
defer stats.StopHierConfigReconcile()
94102

95-
ns := req.NamespacedName.Namespace
96-
97-
log := loggerWithRID(r.Log).WithValues("ns", ns)
98-
99103
return ctrl.Result{}, r.reconcile(ctx, log, ns)
100104
}
101105

@@ -373,8 +377,11 @@ func (r *HierarchyConfigReconciler) syncParent(log logr.Logger, inst *api.Hierar
373377

374378
// Sync this namespace with its current parent.
375379
curParent := r.Forest.Get(inst.Spec.Parent)
376-
if curParent != nil && !curParent.Exists() {
377-
log.Info("The parent doesn't appear to exist (or hasn't been synced yet)", "parent", inst.Spec.Parent)
380+
if config.ExcludedNamespaces[inst.Spec.Parent] {
381+
log.Info("Setting ConditionActivitiesHalted: excluded namespace set as parent", "parent", inst.Spec.Parent)
382+
ns.SetCondition(api.ConditionActivitiesHalted, api.ReasonIllegalParent, fmt.Sprintf("Parent %q is an excluded namespace", inst.Spec.Parent))
383+
} else if curParent != nil && !curParent.Exists() {
384+
log.Info("Setting ConditionActivitiesHalted: parent doesn't exist (or hasn't been synced yet)", "parent", inst.Spec.Parent)
378385
ns.SetCondition(api.ConditionActivitiesHalted, api.ReasonParentMissing, fmt.Sprintf("Parent %q does not exist", inst.Spec.Parent))
379386
}
380387

@@ -581,6 +588,37 @@ func (r *HierarchyConfigReconciler) writeHierarchy(ctx context.Context, log logr
581588
return true, nil
582589
}
583590

591+
// deleteSingletonIfExists deletes the singleton in the namespace if it exists.
592+
// Note: Make sure there's no finalizers on the singleton before calling this
593+
// function.
594+
func (r *HierarchyConfigReconciler) deleteSingletonIfExists(ctx context.Context, log logr.Logger, nm string) error {
595+
inst, deletingCRD, err := r.getSingleton(ctx, nm)
596+
if err != nil {
597+
return err
598+
}
599+
600+
// Early exit if the singleton doesn't exist.
601+
if inst.CreationTimestamp.IsZero() {
602+
return nil
603+
}
604+
605+
// If the CRD is being deleted, we don't need to delete it separately. It will
606+
// be deleted with the CRD.
607+
if deletingCRD {
608+
log.Info("HC in excluded namespace is already being deleted")
609+
return nil
610+
}
611+
log.Info("Deleting illegal HC in excluded namespace")
612+
613+
stats.WriteHierConfig()
614+
if err := r.Delete(ctx, inst); err != nil {
615+
log.Error(err, "while deleting on apiserver")
616+
return err
617+
}
618+
619+
return nil
620+
}
621+
584622
func (r *HierarchyConfigReconciler) writeNamespace(ctx context.Context, log logr.Logger, orig, inst *corev1.Namespace) (bool, error) {
585623
if reflect.DeepEqual(orig, inst) {
586624
return false, nil

incubator/hnc/internal/reconcilers/hierarchy_config_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ var _ = Describe("Hierarchy", func() {
2222
BeforeEach(func() {
2323
fooName = createNS(ctx, "foo")
2424
barName = createNS(ctx, "bar")
25+
config.ExcludedNamespaces = nil
2526
})
2627

2728
It("should set a child on the parent", func() {
@@ -31,6 +32,28 @@ var _ = Describe("Hierarchy", func() {
3132
Eventually(hasChild(ctx, barName, fooName)).Should(Equal(true))
3233
})
3334

35+
It("should remove the hierarchyconfiguration singleton in an excluded namespacee", func() {
36+
// Set the excluded-namespace "kube-system"'s parent to "bar".
37+
config.ExcludedNamespaces = map[string]bool{"kube-system": true}
38+
exHier := newHierarchy("kube-system")
39+
exHier.Spec.Parent = barName
40+
updateHierarchy(ctx, exHier)
41+
42+
// Verify the hierarchyconfiguration singleton is deleted.
43+
Eventually(canGetHierarchy(ctx, "kube-system")).Should(Equal(false))
44+
})
45+
46+
It("should set IllegalParent condition if the parent is an excluded namespace", func() {
47+
// Set bar's parent to the excluded-namespace "kube-system".
48+
config.ExcludedNamespaces = map[string]bool{"kube-system": true}
49+
barHier := newHierarchy(barName)
50+
barHier.Spec.Parent = "kube-system"
51+
updateHierarchy(ctx, barHier)
52+
// Bar singleton should have "IllegalParent" and no "ParentMissing" condition.
53+
Eventually(hasCondition(ctx, barName, api.ConditionActivitiesHalted, api.ReasonIllegalParent)).Should(Equal(true))
54+
Eventually(hasCondition(ctx, barName, api.ConditionActivitiesHalted, api.ReasonParentMissing)).Should(Equal(false))
55+
})
56+
3457
It("should set ParentMissing condition if the parent is missing", func() {
3558
// Set up the parent-child relationship
3659
barHier := newHierarchy(barName)

incubator/hnc/internal/reconcilers/test_helpers_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,17 @@ func getHierarchy(ctx context.Context, nm string) *api.HierarchyConfiguration {
5151
return hier
5252
}
5353

54+
func canGetHierarchy(ctx context.Context, nm string) func() bool {
55+
return func() bool {
56+
nnm := types.NamespacedName{Namespace: nm, Name: api.Singleton}
57+
hier := &api.HierarchyConfiguration{}
58+
if err := k8sClient.Get(ctx, nnm, hier); err != nil {
59+
return false
60+
}
61+
return true
62+
}
63+
}
64+
5465
func updateHierarchy(ctx context.Context, h *api.HierarchyConfiguration) {
5566
if h.CreationTimestamp.IsZero() {
5667
ExpectWithOffset(1, k8sClient.Create(ctx, h)).Should(Succeed())

0 commit comments

Comments
 (0)