Skip to content

Commit accf00b

Browse files
author
root
committed
Inclusive configuration of resources to propagate
See issue kubernetes-retired#16. To allow inclusive propagation of resources an additional SyncornizationMode called 'Allow' which only enables propagation when a selector is set is added. An 'all' selector is also addded. Tested: e2e-testing covering secrets resource in 'Allow' mode and checking propagation when selectors are set and unset ('select', 'treeSelect', 'none', 'all'). Unit testing is also modified to account for the new 'all' selection Signed-off-by: mzeevi <[email protected]>
1 parent fdf83ab commit accf00b

25 files changed

+397
-147
lines changed

api/v1alpha2/hierarchy_types.go

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const (
3737
AnnotationSelector = AnnotationPropagatePrefix + "/select"
3838
AnnotationTreeSelector = AnnotationPropagatePrefix + "/treeSelect"
3939
AnnotationNoneSelector = AnnotationPropagatePrefix + "/none"
40+
AnnotationAllSelector = AnnotationPropagatePrefix + "/all"
4041

4142
// LabelManagedByStandard will eventually replace our own managed-by annotation (we didn't know
4243
// about this standard label when we invented our own).

api/v1alpha2/hnc_config.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ const (
4646

4747
// Remove all existing propagated copies.
4848
Remove SynchronizationMode = "Remove"
49+
50+
// Allow allows propagation of objects from ancestors to descendants
51+
// and deletes obsolete descendants only if a an annotation is set on the object
52+
Allow SynchronizationMode = "Allow"
4953
)
5054

5155
const (
@@ -94,7 +98,7 @@ type ResourceSpec struct {
9498
// Synchronization mode of the kind. If the field is empty, it will be treated
9599
// as "Propagate".
96100
// +optional
97-
// +kubebuilder:validation:Enum=Propagate;Ignore;Remove
101+
// +kubebuilder:validation:Enum=Propagate;Ignore;Remove;Allow
98102
Mode SynchronizationMode `json:"mode,omitempty"`
99103
}
100104

cmd/manager/main.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ func createManager() ctrl.Manager {
242242
// it turns out to be harmful.
243243
cfg.Burst = int(cfg.QPS * 1.5)
244244
mgr, err := ctrl.NewManager(cfg, ctrl.Options{
245-
NewClient: config.NewCachingClient,
245+
NewClient: config.NewClient(webhooksOnly),
246246
Scheme: scheme,
247247
MetricsBindAddress: metricsAddr,
248248
HealthProbeBindAddress: probeAddr,
@@ -304,7 +304,6 @@ func startControllers(mgr ctrl.Manager, certsReady chan struct{}) {
304304
opts := setup.Options{
305305
NoWebhooks: noWebhooks,
306306
MaxReconciles: maxReconciles,
307-
ReadOnly: webhooksOnly,
308307
HRQ: enableHRQ,
309308
}
310309
setup.Create(setupLog, mgr, f, opts)

config/crd/bases/hnc.x-k8s.io_hncconfigurations.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ spec:
6464
- Propagate
6565
- Ignore
6666
- Remove
67+
- Allow
6768
type: string
6869
resource:
6970
description: Resource to be configured.

docs/releasing.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Releasing HNC
22

33
HNC uses semantic versioning. We use long-lived branches for every minor
4-
release, and each release is tagged in Git. Usually, a release will be preceeded
4+
release, and each release is tagged in Git. Usually, a release will be preceded
55
by one or more release candidates. Therefore, at a high level, the flow to
66
release a new _minor_ version of HNC is:
77

internal/anchor/reconciler.go

+20-42
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"k8s.io/apimachinery/pkg/types"
2727
ctrl "sigs.k8s.io/controller-runtime"
2828
"sigs.k8s.io/controller-runtime/pkg/client"
29+
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
2930
"sigs.k8s.io/controller-runtime/pkg/event"
3031
"sigs.k8s.io/controller-runtime/pkg/handler"
3132
"sigs.k8s.io/controller-runtime/pkg/reconcile"
@@ -51,9 +52,6 @@ type Reconciler struct {
5152
// https://book-v1.book.kubebuilder.io/beyond_basics/controller_watches.html) that is used to
5253
// enqueue additional objects that need updating.
5354
affected chan event.GenericEvent
54-
55-
// ReadOnly disables writebacks
56-
ReadOnly bool
5755
}
5856

5957
// Reconcile sets up some basic variables and then calls the business logic. It currently
@@ -81,9 +79,9 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
8179
// Anchors in unmanaged namespace should be ignored. Make sure it
8280
// doesn't have any finalizers, otherwise, leave it alone.
8381
if why := config.WhyUnmanaged(pnm); why != "" {
84-
if len(inst.ObjectMeta.Finalizers) > 0 {
82+
if controllerutil.ContainsFinalizer(inst, api.MetaGroup) {
8583
log.Info("Removing finalizers from anchor in unmanaged namespace", "reason", why)
86-
inst.ObjectMeta.Finalizers = nil
84+
controllerutil.RemoveFinalizer(inst, api.MetaGroup)
8785
return ctrl.Result{}, r.writeInstance(ctx, log, inst)
8886
}
8987
return ctrl.Result{}, nil
@@ -94,10 +92,10 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
9492
// been bypassed and the anchor has been successfully created. Forbidden
9593
// anchors won't have finalizers.
9694
if why := config.WhyUnmanaged(nm); why != "" {
97-
if inst.Status.State != api.Forbidden || len(inst.ObjectMeta.Finalizers) > 0 {
95+
if inst.Status.State != api.Forbidden || controllerutil.ContainsFinalizer(inst, api.MetaGroup) {
9896
log.Info("Setting forbidden state on anchor with unmanaged name", "reason", why)
9997
inst.Status.State = api.Forbidden
100-
inst.ObjectMeta.Finalizers = nil
98+
controllerutil.RemoveFinalizer(inst, api.MetaGroup)
10199
return ctrl.Result{}, r.writeInstance(ctx, log, inst)
102100
}
103101
return ctrl.Result{}, nil
@@ -113,9 +111,13 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
113111
r.updateState(log, inst, snsInst)
114112

115113
// Handle the case where the anchor is being deleted.
116-
if deleting, err := r.onDeleting(ctx, log, inst, snsInst); deleting {
117-
return ctrl.Result{}, err
114+
if !inst.DeletionTimestamp.IsZero() {
115+
// Stop reconciliation as anchor is being deleted
116+
return ctrl.Result{}, r.onDeleting(ctx, log, inst, snsInst)
118117
}
118+
// Add finalizers on all non-forbidden anchors to ensure it's not deleted until
119+
// after the subnamespace is deleted.
120+
controllerutil.AddFinalizer(inst, api.MetaGroup)
119121

120122
// If the subnamespace doesn't exist, create it.
121123
if inst.Status.State == api.Missing {
@@ -130,9 +132,6 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
130132
}
131133
}
132134

133-
// Add finalizers on all non-forbidden anchors to ensure it's not deleted until
134-
// after the subnamespace is deleted.
135-
inst.ObjectMeta.Finalizers = []string{api.MetaGroup}
136135
return ctrl.Result{}, r.writeInstance(ctx, log, inst)
137136
}
138137

@@ -145,19 +144,14 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
145144
// There are several conditions where we skip step 1 - for example, if we're uninstalling HNC, or
146145
// if allowCascadingDeletion is disabled but the subnamespace has descendants (see
147146
// shouldDeleteSubns for details). In such cases, we move straight to step 2.
148-
func (r *Reconciler) onDeleting(ctx context.Context, log logr.Logger, inst *api.SubnamespaceAnchor, snsInst *corev1.Namespace) (bool, error) {
149-
// Early exit and continue reconciliation if the instance is not being deleted.
150-
if inst.DeletionTimestamp.IsZero() {
151-
return false, nil
152-
}
153-
147+
func (r *Reconciler) onDeleting(ctx context.Context, log logr.Logger, inst *api.SubnamespaceAnchor, snsInst *corev1.Namespace) error {
154148
// We handle deletions differently depending on whether _one_ anchor is being deleted (i.e., the
155149
// user wants to delete the namespace) or whether the Anchor CRD is being deleted, which usually
156150
// means HNC is being uninstalled and we shouldn't delete _any_ namespaces.
157151
deletingCRD, err := crd.IsDeletingCRD(ctx, api.Anchors)
158152
if err != nil {
159153
log.Error(err, "Couldn't determine if CRD is being deleted")
160-
return false, err
154+
return err
161155
}
162156
log.V(1).Info("Anchor is being deleted", "deletingCRD", deletingCRD)
163157

@@ -166,21 +160,21 @@ func (r *Reconciler) onDeleting(ctx context.Context, log logr.Logger, inst *api.
166160
switch {
167161
case r.shouldDeleteSubns(log, inst, snsInst, deletingCRD):
168162
log.Info("Deleting subnamespace due to anchor being deleted")
169-
return true, r.deleteNamespace(ctx, log, snsInst)
163+
return r.deleteNamespace(ctx, log, snsInst)
170164
case r.shouldFinalizeAnchor(log, inst, snsInst):
171165
log.V(1).Info("Unblocking deletion") // V(1) since we'll very shortly show an "anchor deleted" message
172-
inst.ObjectMeta.Finalizers = nil
173-
return true, r.writeInstance(ctx, log, inst)
166+
controllerutil.RemoveFinalizer(inst, api.MetaGroup)
167+
return r.writeInstance(ctx, log, inst)
174168
default:
175169
// There's nothing to do; we're just waiting for something to happen. Print out a log message
176170
// indicating what we're waiting for.
177-
if len(inst.ObjectMeta.Finalizers) > 0 {
171+
if controllerutil.ContainsFinalizer(inst, api.MetaGroup) {
178172
log.Info("Waiting for subnamespace to be fully purged before letting the anchor be deleted")
179173
} else {
180174
// I doubt we'll ever get here but I suppose it's possible
181175
log.Info("Waiting for K8s to delete this anchor (all finalizers are removed)")
182176
}
183-
return true, nil
177+
return nil
184178
}
185179
}
186180

@@ -213,7 +207,7 @@ func (r *Reconciler) shouldDeleteSubns(log logr.Logger, inst *api.SubnamespaceAn
213207
log.V(1).Info("The subnamespace is already being deleted; no need to delete again")
214208
return false
215209
}
216-
if len(inst.ObjectMeta.Finalizers) == 0 {
210+
if !controllerutil.ContainsFinalizer(inst, api.MetaGroup) {
217211
log.V(1).Info("The anchor has already been finalized; do not reconsider deleting the namespace")
218212
return false
219213
}
@@ -257,7 +251,7 @@ func (r *Reconciler) shouldDeleteSubns(log logr.Logger, inst *api.SubnamespaceAn
257251
// deleted, it's in the process of being deleted, etc).
258252
func (r *Reconciler) shouldFinalizeAnchor(log logr.Logger, inst *api.SubnamespaceAnchor, snsInst *corev1.Namespace) bool {
259253
// If the anchor is already finalized, there's no need to do it again.
260-
if len(inst.ObjectMeta.Finalizers) == 0 {
254+
if !controllerutil.ContainsFinalizer(inst, api.MetaGroup) {
261255
return false
262256
}
263257

@@ -348,10 +342,6 @@ func (r *Reconciler) getInstance(ctx context.Context, pnm, nm string) (*api.Subn
348342
}
349343

350344
func (r *Reconciler) writeInstance(ctx context.Context, log logr.Logger, inst *api.SubnamespaceAnchor) error {
351-
if r.ReadOnly {
352-
return nil
353-
}
354-
355345
if inst.CreationTimestamp.IsZero() {
356346
if err := r.Create(ctx, inst); err != nil {
357347
log.Error(err, "while creating on apiserver")
@@ -370,10 +360,6 @@ func (r *Reconciler) writeInstance(ctx context.Context, log logr.Logger, inst *a
370360
// finalizers on the instance before calling this function.
371361
//lint:ignore U1000 Ignore for now, as it may be used again in the future
372362
func (r *Reconciler) deleteInstance(ctx context.Context, inst *api.SubnamespaceAnchor) error {
373-
if r.ReadOnly {
374-
return nil
375-
}
376-
377363
if err := r.Delete(ctx, inst); err != nil {
378364
return fmt.Errorf("while deleting on apiserver: %w", err)
379365
}
@@ -396,10 +382,6 @@ func (r *Reconciler) getNamespace(ctx context.Context, nm string) (*corev1.Names
396382
}
397383

398384
func (r *Reconciler) writeNamespace(ctx context.Context, log logr.Logger, nm, pnm string) error {
399-
if r.ReadOnly {
400-
return nil
401-
}
402-
403385
inst := &corev1.Namespace{}
404386
inst.ObjectMeta.Name = nm
405387
metadata.SetAnnotation(inst, api.SubnamespaceOf, pnm)
@@ -417,10 +399,6 @@ func (r *Reconciler) writeNamespace(ctx context.Context, log logr.Logger, nm, pn
417399
}
418400

419401
func (r *Reconciler) deleteNamespace(ctx context.Context, log logr.Logger, inst *corev1.Namespace) error {
420-
if r.ReadOnly {
421-
return nil
422-
}
423-
424402
if err := r.Delete(ctx, inst); err != nil {
425403
log.Error(err, "While deleting subnamespace")
426404
return err

internal/anchor/validator.go

-3
Original file line numberDiff line numberDiff line change
@@ -97,13 +97,11 @@ func (v *Validator) handle(req *anchorRequest) admission.Response {
9797
// Can't create subnamespaces in unmanaged namespaces
9898
if why := config.WhyUnmanaged(pnm); why != "" {
9999
err := fmt.Errorf("cannot create a subnamespace in the unmanaged namespace %q (%s)", pnm, why)
100-
// TODO(erikgb): Add to list of Invalid field errors?
101100
return webhooks.DenyForbidden(api.SubnamespaceAnchorGR, pnm, err)
102101
}
103102
// Can't create subnamespaces using unmanaged namespace names
104103
if why := config.WhyUnmanaged(cnm); why != "" {
105104
err := fmt.Errorf("cannot create a subnamespace using the unmanaged namespace name %q (%s)", cnm, why)
106-
// TODO(erikgb): Add to list of Invalid field errors?
107105
return webhooks.DenyForbidden(api.SubnamespaceAnchorGR, cnm, err)
108106
}
109107

@@ -113,7 +111,6 @@ func (v *Validator) handle(req *anchorRequest) admission.Response {
113111
childIsMissingAnchor := (cns.Parent().Name() == pnm && cns.IsSub)
114112
if !childIsMissingAnchor {
115113
err := errors.New("cannot create a subnamespace using an existing namespace")
116-
// TODO(erikgb): Add to list of Invalid field errors?
117114
return webhooks.DenyConflict(api.SubnamespaceAnchorGR, cnm, err)
118115
}
119116
}

internal/config/cache_unstructured.go

-28
This file was deleted.

internal/config/client.go

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package config
2+
3+
import (
4+
"context"
5+
6+
authzv1 "k8s.io/api/authorization/v1"
7+
"k8s.io/apimachinery/pkg/api/meta"
8+
"k8s.io/apimachinery/pkg/runtime"
9+
"k8s.io/client-go/rest"
10+
"sigs.k8s.io/controller-runtime/pkg/cache"
11+
"sigs.k8s.io/controller-runtime/pkg/client"
12+
"sigs.k8s.io/controller-runtime/pkg/cluster"
13+
)
14+
15+
// newCachingClient is an alternative implementation of controller-runtime's
16+
// default client for manager.Manager.
17+
// The only difference is that this implementation sets `CacheUnstructured` to `true` to
18+
// cache unstructured objects.
19+
func newCachingClient(cache cache.Cache, config *rest.Config, options client.Options, uncachedObjects ...client.Object) (client.Client, error) {
20+
c, err := client.New(config, options)
21+
if err != nil {
22+
return nil, err
23+
}
24+
25+
return client.NewDelegatingClient(client.NewDelegatingClientInput{
26+
CacheReader: cache,
27+
Client: c,
28+
UncachedObjects: uncachedObjects,
29+
CacheUnstructured: true,
30+
})
31+
}
32+
33+
func NewClient(readOnly bool) cluster.NewClientFunc {
34+
return func(cache cache.Cache, config *rest.Config, options client.Options, uncachedObjects ...client.Object) (client.Client, error) {
35+
c, err := newCachingClient(cache, config, options, uncachedObjects...)
36+
if readOnly {
37+
c = &readOnlyClient{client: c}
38+
}
39+
return c, err
40+
}
41+
}
42+
43+
// readOnlyClient is a client decorator that ensures no write operations are possible.
44+
type readOnlyClient struct {
45+
client client.Client
46+
}
47+
48+
func (c readOnlyClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object) error {
49+
return c.client.Get(ctx, key, obj)
50+
}
51+
52+
func (c readOnlyClient) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error {
53+
return c.client.List(ctx, list, opts...)
54+
}
55+
56+
func (c readOnlyClient) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error {
57+
// Allow callers to create subject access reviews - even in read-only mode.
58+
// Creating a SAR will not modify state in etcd. Used to perform RBAC checks.
59+
if _, ok := obj.(*authzv1.SubjectAccessReview); ok {
60+
return c.client.Create(ctx, obj, opts...)
61+
}
62+
return nil
63+
}
64+
65+
func (c readOnlyClient) Delete(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error {
66+
return nil
67+
}
68+
69+
func (c readOnlyClient) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error {
70+
return nil
71+
}
72+
73+
func (c readOnlyClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error {
74+
return nil
75+
}
76+
77+
func (c readOnlyClient) DeleteAllOf(ctx context.Context, obj client.Object, opts ...client.DeleteAllOfOption) error {
78+
return nil
79+
}
80+
81+
func (c readOnlyClient) Status() client.StatusWriter {
82+
return nopStatusWriter{}
83+
}
84+
85+
func (c readOnlyClient) Scheme() *runtime.Scheme {
86+
return c.client.Scheme()
87+
}
88+
89+
func (c readOnlyClient) RESTMapper() meta.RESTMapper {
90+
return c.client.RESTMapper()
91+
}
92+
93+
type nopStatusWriter struct{}
94+
95+
func (w nopStatusWriter) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error {
96+
return nil
97+
}
98+
99+
func (w nopStatusWriter) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error {
100+
return nil
101+
}

0 commit comments

Comments
 (0)