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

Commit aef17d3

Browse files
authored
Merge pull request #1483 from yiqigao217/newcondition
Handle bypassed webhook rules on excluded namespaces
2 parents 62115a0 + 880ca85 commit aef17d3

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)