Skip to content

Commit 2503562

Browse files
committed
Inclusive configuration of resources to propagate
See issue kubernetes-retired#16. To allow inclusive propagation of resources an additional `SyncornizationMode` called 'AllowPropagate' which only enables propagation when a selector is set is added. An 'all' selector is also addded. Tested: e2e-testing covering secrets resource in 'AllowPropagate' 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 347c5a3 commit 2503562

17 files changed

+282
-41
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

+6-2
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const (
3131
)
3232

3333
// SynchronizationMode describes propagation mode of objects of the same kind.
34-
// The only three modes currently supported are "Propagate", "Ignore", and "Remove".
34+
// The only four modes currently supported are "Propagate", "AllowPropagate", "Ignore", and "Remove".
3535
// See detailed definition below. An unsupported mode will be treated as "ignore".
3636
type SynchronizationMode string
3737

@@ -46,6 +46,10 @@ const (
4646

4747
// Remove all existing propagated copies.
4848
Remove SynchronizationMode = "Remove"
49+
50+
// AllowPropagate allows propagation of objects from ancestors to descendants
51+
// and deletes obsolete descendants only if a an annotation is set on the object
52+
AllowPropagate SynchronizationMode = "AllowPropagate"
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;AllowPropagate
98102
Mode SynchronizationMode `json:"mode,omitempty"`
99103
}
100104

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+
- AllowPropagate
6768
type: string
6869
resource:
6970
description: Resource to be configured.

internal/forest/forest.go

+3
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ type TypeSyncer interface {
5151
// GetMode gets the propagation mode of objects that are handled by the reconciler who implements the interface.
5252
GetMode() api.SynchronizationMode
5353

54+
// CanPropagate returns true if Propagate mode or AllowPropagate mode is set
55+
CanPropagate() bool
56+
5457
// GetNumPropagatedObjects returns the number of propagated objects on the apiserver.
5558
GetNumPropagatedObjects() int
5659
}

internal/hierarchyconfig/validator.go

+8-3
Original file line numberDiff line numberDiff line change
@@ -246,16 +246,21 @@ func (v *Validator) getConflictingObjects(newParent, ns *forest.Namespace) []str
246246
if newParent == nil {
247247
return nil
248248
}
249-
// Traverse all the types with 'Propagate' mode to find any conflicts.
249+
// Traverse all the types with 'Propagate' mode or 'Allow' mode to find any conflicts.
250250
conflicts := []string{}
251251
for _, t := range v.Forest.GetTypeSyncers() {
252-
if t.GetMode() == api.Propagate {
252+
if t.CanPropagate() {
253253
conflicts = append(conflicts, v.getConflictingObjectsOfType(t.GetGVK(), newParent, ns)...)
254254
}
255255
}
256256
return conflicts
257257
}
258258

259+
func (v *Validator) isPropagateType(gvk metav1.GroupVersionKind) bool {
260+
ts := v.Forest.GetTypeSyncerFromGroupKind(schema.GroupKind{Group: gvk.Group, Kind: gvk.Kind})
261+
return ts != nil && ts.GetMode() == api.Propagate
262+
}
263+
259264
// getConflictingObjectsOfType returns a list of namespaced objects if there's
260265
// any conflict between the new ancestors and the descendants.
261266
func (v *Validator) getConflictingObjectsOfType(gvk schema.GroupVersionKind, newParent, ns *forest.Namespace) []string {
@@ -266,7 +271,7 @@ func (v *Validator) getConflictingObjectsOfType(gvk schema.GroupVersionKind, new
266271
// If the user has chosen not to propagate the object to this descendant,
267272
// then it should not be included in conflict checks
268273
o := v.Forest.Get(nnm.Namespace).GetSourceObject(gvk, nnm.Name)
269-
if ok, _ := selectors.ShouldPropagate(o, o.GetLabels()); ok {
274+
if ok, _ := selectors.ShouldPropagate(o, o.GetLabels(), v.isPropagateType(metav1.GroupVersionKind(gvk))); ok {
270275
newAnsSrcObjs[nnm.Name] = true
271276
}
272277
}

internal/hncconfig/reconciler.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,7 @@ func (r *Reconciler) writeCondition(inst *api.HNCConfiguration, tp, reason, msg
361361
}
362362

363363
// setTypeStatuses adds Status.Resources for types configured in the spec. Only the status of types
364-
// in `Propagate` and `Remove` modes will be recorded. The Status.Resources is sorted in
364+
// in `Propagate`, `Remove` and `AllowPropagate` modes will be recorded. The Status.Resources is sorted in
365365
// alphabetical order based on Group and Resource.
366366
func (r *Reconciler) setTypeStatuses(inst *api.HNCConfiguration) {
367367
// We lock the forest here so that other reconcilers cannot modify the
@@ -394,7 +394,7 @@ func (r *Reconciler) setTypeStatuses(inst *api.HNCConfiguration) {
394394
}
395395

396396
// Only add NumSourceObjects if we are propagating objects of this type.
397-
if ts.GetMode() == api.Propagate {
397+
if ts.CanPropagate() {
398398
numSrc := 0
399399
nms := r.Forest.GetNamespaceNames()
400400
for _, nm := range nms {

internal/hncconfig/validator.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
"github.com/go-logr/logr"
1010
k8sadm "k8s.io/api/admission/v1"
11+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1112
"k8s.io/apimachinery/pkg/runtime/schema"
1213
"k8s.io/apimachinery/pkg/util/validation/field"
1314
"k8s.io/client-go/rest"
@@ -155,6 +156,11 @@ func (v *Validator) checkConflictsForGVK(gvk schema.GroupVersionKind) []string {
155156
return conflicts
156157
}
157158

159+
func (v *Validator) isPropagateType(gvk metav1.GroupVersionKind) bool {
160+
ts := v.Forest.GetTypeSyncerFromGroupKind(schema.GroupKind{Group: gvk.Group, Kind: gvk.Kind})
161+
return ts != nil && ts.GetMode() == api.Propagate
162+
}
163+
158164
// checkConflictsForTree check for all the gvk objects in the given namespaces, to see if they
159165
// will be potentially overwritten by the objects on the ancestor namespaces
160166
func (v *Validator) checkConflictsForTree(gvk schema.GroupVersionKind, ao ancestorObjects, ns *forest.Namespace) []string {
@@ -167,7 +173,7 @@ func (v *Validator) checkConflictsForTree(gvk schema.GroupVersionKind, ao ancest
167173
for _, nnm := range objs[onm] {
168174
// check if the existing ns will propagate this object to the current ns
169175
inst := v.Forest.Get(nnm).GetSourceObject(gvk, onm)
170-
if ok, _ := selectors.ShouldPropagate(inst, ns.GetLabels()); ok {
176+
if ok, _ := selectors.ShouldPropagate(inst, ns.GetLabels(), v.isPropagateType(metav1.GroupVersionKind(gvk))); ok {
171177
conflicts = append(conflicts, fmt.Sprintf(" Object %q in namespace %q would overwrite the one in %q", onm, nnm, ns.Name()))
172178
}
173179
}

internal/hrq/rqreconciler.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -202,8 +202,7 @@ func (r *ResourceQuotaReconciler) deleteSingleton(ctx context.Context, log logr.
202202
// and its ancestors.
203203
// - Updates `hrq.used.local` of the current namespace and `hrq.used.subtree`
204204
// of the current namespace and its ancestors based on `ResourceQuota.Status.Used`.
205-
func (r *ResourceQuotaReconciler) syncWithForest(log logr.Logger,
206-
inst *v1.ResourceQuota) bool {
205+
func (r *ResourceQuotaReconciler) syncWithForest(log logr.Logger, inst *v1.ResourceQuota) bool {
207206
r.Forest.Lock()
208207
defer r.Forest.Unlock()
209208
ns := r.Forest.Get(inst.ObjectMeta.Namespace)

internal/kubectl/configdescribe.go

+2
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ var configDescribeCmd = &cobra.Command{
3737
action = "Propagating"
3838
case api.Remove:
3939
action = "Removing"
40+
case api.AllowPropagate:
41+
action = "AllowPropagate"
4042
default:
4143
action = "Ignoring"
4244
}

internal/kubectl/configset.go

+7-7
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ import (
2727
)
2828

2929
var setResourceCmd = &cobra.Command{
30-
Use: fmt.Sprintf("set-resource RESOURCE [--group GROUP] [--force] --mode <%s|%s|%s>",
31-
api.Propagate, api.Remove, api.Ignore),
30+
Use: fmt.Sprintf("set-resource RESOURCE [--group GROUP] [--force] --mode <%s|%s|%s|%s>",
31+
api.Propagate, api.Remove, api.Ignore, api.AllowPropagate),
3232
Short: "Sets the HNC configuration of a specific resource",
3333
Example: fmt.Sprintf(" # Set configuration of a core type\n" +
3434
" kubectl hns config set-resource secrets --mode Ignore\n\n" +
@@ -40,16 +40,16 @@ var setResourceCmd = &cobra.Command{
4040
flags := cmd.Flags()
4141
group, _ := flags.GetString("group")
4242
modeStr, _ := flags.GetString("mode")
43-
mode := api.SynchronizationMode(cases.Title(language.English).String(modeStr))
43+
mode := api.SynchronizationMode(cases.Title(language.English, cases.NoLower).String(modeStr))
4444
force, _ := flags.GetBool("force")
4545
config := client.getHNCConfig()
4646

4747
exist := false
4848
for i := 0; i < len(config.Spec.Resources); i++ {
4949
r := &config.Spec.Resources[i]
5050
if r.Group == group && r.Resource == resource {
51-
if r.Mode == api.Ignore && mode == api.Propagate && !force {
52-
fmt.Printf("Switching directly from 'Ignore' to 'Propagate' mode could cause existing %q objects in "+
51+
if r.Mode == api.Ignore && mode == api.Propagate && !force || r.Mode == api.Ignore && mode == api.AllowPropagate && !force {
52+
fmt.Printf("Switching directly from 'Ignore' to 'Propagate' mode or 'AllowPropagate' mode could cause existing %q objects in "+
5353
"child namespaces to be overwritten by objects from ancestor namespaces.\n", resource)
5454
fmt.Println("If you are sure you want to proceed with this operation, use the '--force' flag.")
5555
fmt.Println("If you are not sure and would like to see what source objects would be overwritten," +
@@ -78,7 +78,7 @@ var setResourceCmd = &cobra.Command{
7878

7979
func newSetResourceCmd() *cobra.Command {
8080
setResourceCmd.Flags().String("group", "", "The group of the resource; may be omitted for core resources (or explicitly set to the empty string)")
81-
setResourceCmd.Flags().String("mode", "", "The synchronization mode: one of Propagate, Remove or Ignore")
82-
setResourceCmd.Flags().BoolP("force", "f", false, "Allow the synchronization mode to be changed directly from Ignore to Propagate despite the dangers of doing so")
81+
setResourceCmd.Flags().String("mode", "", "The synchronization mode: one of Propagate, Remove, Ignore and AllowPropagate")
82+
setResourceCmd.Flags().BoolP("force", "f", false, "Allow the synchronization mode to be changed directly from Ignore to Propagate or AllowPropagate despite the dangers of doing so")
8383
return setResourceCmd
8484
}

internal/objects/reconciler.go

+14-7
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ func (r *Reconciler) GetMode() api.SynchronizationMode {
165165
// treated as api.Ignore.
166166
func GetValidateMode(mode api.SynchronizationMode, log logr.Logger) api.SynchronizationMode {
167167
switch mode {
168-
case api.Propagate, api.Ignore, api.Remove:
168+
case api.Propagate, api.Ignore, api.Remove, api.AllowPropagate:
169169
return mode
170170
case "":
171171
log.Info("Sync mode is unset; using default 'Propagate'")
@@ -198,6 +198,11 @@ func (r *Reconciler) SetMode(ctx context.Context, log logr.Logger, mode api.Sync
198198
return nil
199199
}
200200

201+
// CanPropagate returns true if Propagate mode or AllowPropagate mode is set
202+
func (r *Reconciler) CanPropagate() bool {
203+
return (r.GetMode() == api.Propagate || r.GetMode() == api.AllowPropagate)
204+
}
205+
201206
// GetNumPropagatedObjects returns the number of propagated objects of the GVK handled by this object reconciler.
202207
func (r *Reconciler) GetNumPropagatedObjects() int {
203208
r.propagatedObjectsLock.Lock()
@@ -388,11 +393,13 @@ func (r *Reconciler) shouldSyncAsPropagated(log logr.Logger, inst *unstructured.
388393
}
389394

390395
// If there's a conflicting source in the ancestors (excluding itself) and the
391-
// the type has 'Propagate' mode, the object will be overwritten.
396+
// the type has 'Propagate' mode or 'AllowPropagate' mode, the object will be overwritten.
392397
mode := r.Forest.GetTypeSyncer(r.GVK).GetMode()
393-
if mode == api.Propagate && srcInst != nil {
394-
log.Info("Conflicting object found in ancestors namespace; will overwrite this object", "conflictingAncestor", srcInst.GetNamespace())
395-
return true, srcInst
398+
if mode == api.Propagate || mode == api.AllowPropagate {
399+
if srcInst != nil {
400+
log.Info("Conflicting object found in ancestors namespace; will overwrite this object", "conflictingAncestor", srcInst.GetNamespace())
401+
return true, srcInst
402+
}
396403
}
397404

398405
return false, nil
@@ -463,7 +470,7 @@ func (r *Reconciler) syncPropagated(inst, srcInst *unstructured.Unstructured) (s
463470
func (r *Reconciler) syncSource(log logr.Logger, src *unstructured.Unstructured) {
464471
// Update or create a copy of the source object in the forest. We now store
465472
// all the source objects in the forests no matter if the mode is 'Propagate'
466-
// or not, because HNCConfig webhook will also check the non-'Propagate' mode
473+
// or not, because HNCConfig webhook will also check the non-'Propagate' or 'Allow' modes
467474
// source objects in the forest to see if a mode change is allowed.
468475
ns := r.Forest.Get(src.GetNamespace())
469476

@@ -712,7 +719,7 @@ func hasPropagatedLabel(inst *unstructured.Unstructured) bool {
712719
// - Service Account token secrets
713720
func (r *Reconciler) shouldPropagateSource(log logr.Logger, inst *unstructured.Unstructured, dst string) bool {
714721
nsLabels := r.Forest.Get(dst).GetLabels()
715-
if ok, err := selectors.ShouldPropagate(inst, nsLabels); err != nil {
722+
if ok, err := selectors.ShouldPropagate(inst, nsLabels, r.Mode == api.Propagate); err != nil {
716723
log.Error(err, "Cannot propagate")
717724
r.EventRecorder.Event(inst, "Warning", api.EventCannotParseSelector, err.Error())
718725
return false

internal/objects/reconciler_test.go

+48
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ var _ = Describe("Exceptions", func() {
5151
selector string
5252
treeSelector string
5353
noneSelector string
54+
allSelector string
5455
want []string
5556
notWant []string
5657
}{{
@@ -120,6 +121,20 @@ var _ = Describe("Exceptions", func() {
120121
noneSelector: "true",
121122
want: []string{},
122123
notWant: []string{c1, c2, c3},
124+
}, {
125+
name: "not propagate when allSelector and noneSelector are both true",
126+
noneSelector: "true",
127+
allSelector: "all",
128+
want: []string{},
129+
notWant: []string{c1, c2, c3},
130+
}, {
131+
name: "only propagate the intersection of four selectors",
132+
selector: c1 + api.LabelTreeDepthSuffix,
133+
treeSelector: c1 + ", " + c2,
134+
noneSelector: "true",
135+
allSelector: "true",
136+
want: []string{},
137+
notWant: []string{c1, c2, c3},
123138
}}
124139

125140
for _, tc := range tests {
@@ -146,6 +161,7 @@ var _ = Describe("Exceptions", func() {
146161
api.AnnotationSelector: tc.selector,
147162
api.AnnotationTreeSelector: tc.treeSelector,
148163
api.AnnotationNoneSelector: tc.noneSelector,
164+
api.AnnotationAllSelector: tc.allSelector,
149165
})
150166
for _, ns := range tc.want {
151167
ns = ReplaceStrings(ns, names)
@@ -171,6 +187,7 @@ var _ = Describe("Exceptions", func() {
171187
selector string
172188
treeSelector string
173189
noneSelector string
190+
allSelector string
174191
want []string
175192
notWant []string
176193
}{{
@@ -188,6 +205,11 @@ var _ = Describe("Exceptions", func() {
188205
noneSelector: "true",
189206
want: []string{},
190207
notWant: []string{c1, c2, c3},
208+
}, {
209+
name: "update allSelector",
210+
allSelector: "true",
211+
want: []string{c1, c2, c3},
212+
notWant: []string{},
191213
}}
192214

193215
for _, tc := range tests {
@@ -207,6 +229,7 @@ var _ = Describe("Exceptions", func() {
207229
SetParent(ctx, names[c3], names[p])
208230
tc.selector = ReplaceStrings(tc.selector, names)
209231
tc.treeSelector = ReplaceStrings(tc.treeSelector, names)
232+
tc.allSelector = ReplaceStrings(tc.allSelector, names)
210233

211234
// Create a Role and verify it's propagated
212235
MakeObject(ctx, api.RoleResource, names[p], "testrole")
@@ -219,6 +242,7 @@ var _ = Describe("Exceptions", func() {
219242
api.AnnotationSelector: tc.selector,
220243
api.AnnotationTreeSelector: tc.treeSelector,
221244
api.AnnotationNoneSelector: tc.noneSelector,
245+
api.AnnotationAllSelector: tc.allSelector,
222246
})
223247
// make sure the changes are propagated
224248
for _, ns := range tc.notWant {
@@ -724,6 +748,30 @@ var _ = Describe("Basic propagation", func() {
724748
return nil
725749
}).Should(Succeed(), "waiting for annot-a to be unpropagated")
726750
})
751+
752+
It("should avoid propagating when no selector is set if the sync mode is 'AllowPropagate'", func() {
753+
AddToHNCConfig(ctx, "", "secrets", api.AllowPropagate)
754+
// Set tree as bar -> foo(root).
755+
SetParent(ctx, barName, fooName)
756+
MakeObject(ctx, "secrets", fooName, "foo-sec")
757+
Eventually(HasObject(ctx, "secrets", fooName, "foo-sec")).Should(BeTrue())
758+
759+
// Ensure the object is not propagated
760+
Consistently(HasObject(ctx, "secrets", barName, "foo-sec")).Should(BeFalse())
761+
762+
// Update the secret object with the treeSelector annotation
763+
UpdateObjectWithAnnotations(ctx, "secrets", fooName, "foo-sec", map[string]string{
764+
api.AnnotationTreeSelector: barName,
765+
})
766+
767+
// Ensure the object is now propagated
768+
Eventually(HasObject(ctx, "secrets", barName, "foo-sec")).Should(BeTrue())
769+
Expect(ObjectInheritedFrom(ctx, "secrets", barName, "foo-sec")).Should(Equal(fooName))
770+
771+
// Remove the annotation from the secret and ensure the object is deleted from the descendant
772+
UpdateObjectWithAnnotations(ctx, "secrets", fooName, "foo-sec", map[string]string{})
773+
Eventually(HasObject(ctx, "secrets", barName, "foo-sec")).Should(BeFalse())
774+
})
727775
})
728776

729777
func modifyRole(ctx context.Context, nsName, roleName string) {

0 commit comments

Comments
 (0)