Skip to content

Commit adde9f0

Browse files
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 347c5a3 commit adde9f0

File tree

14 files changed

+255
-23
lines changed

14 files changed

+255
-23
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

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.

internal/hierarchyconfig/validator.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -246,10 +246,10 @@ 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.GetMode() == api.Propagate || t.GetMode() == api.Allow {
253253
conflicts = append(conflicts, v.getConflictingObjectsOfType(t.GetGVK(), newParent, ns)...)
254254
}
255255
}

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 `Allow` 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.GetMode() == api.Propagate || ts.GetMode() == api.Allow {
398398
numSrc := 0
399399
nms := r.Forest.GetNamespaceNames()
400400
for _, nm := range nms {

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.Allow:
41+
action = "Allow"
4042
default:
4143
action = "Ignoring"
4244
}

internal/kubectl/configset.go

+6-6
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.Allow),
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" +
@@ -48,8 +48,8 @@ var setResourceCmd = &cobra.Command{
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.Allow && !force {
52+
fmt.Printf("Switching directly from 'Ignore' to 'Propagate' mode or 'Allow' 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 Allow")
82+
setResourceCmd.Flags().BoolP("force", "f", false, "Allow the synchronization mode to be changed directly from Ignore to Propagate or Allow despite the dangers of doing so")
8383
return setResourceCmd
8484
}

internal/objects/reconciler.go

+18-6
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.Allow:
169169
return mode
170170
case "":
171171
log.Info("Sync mode is unset; using default 'Propagate'")
@@ -388,11 +388,13 @@ func (r *Reconciler) shouldSyncAsPropagated(log logr.Logger, inst *unstructured.
388388
}
389389

390390
// If there's a conflicting source in the ancestors (excluding itself) and the
391-
// the type has 'Propagate' mode, the object will be overwritten.
391+
// the type has 'Propagate' mode or 'Allow' mode, the object will be overwritten.
392392
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
393+
if mode == api.Propagate || mode == api.Allow {
394+
if srcInst != nil {
395+
log.Info("Conflicting object found in ancestors namespace; will overwrite this object", "conflictingAncestor", srcInst.GetNamespace())
396+
return true, srcInst
397+
}
396398
}
397399

398400
return false, nil
@@ -463,7 +465,7 @@ func (r *Reconciler) syncPropagated(inst, srcInst *unstructured.Unstructured) (s
463465
func (r *Reconciler) syncSource(log logr.Logger, src *unstructured.Unstructured) {
464466
// Update or create a copy of the source object in the forest. We now store
465467
// 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
468+
// or not, because HNCConfig webhook will also check the non-'Propagate' or 'Allow' modes
467469
// source objects in the forest to see if a mode change is allowed.
468470
ns := r.Forest.Get(src.GetNamespace())
469471

@@ -726,6 +728,16 @@ func (r *Reconciler) shouldPropagateSource(log logr.Logger, inst *unstructured.U
726728
case r.Mode == api.Remove:
727729
return false
728730

731+
case r.Mode == api.Allow:
732+
if exists, err := selectors.SelectorExists(inst, nsLabels); err != nil {
733+
log.Error(err, "Cannot propagate")
734+
r.EventRecorder.Event(inst, "Warning", api.EventCannotParseSelector, err.Error())
735+
return false
736+
} else if !exists {
737+
return false
738+
}
739+
return true
740+
729741
// Object with nonempty finalizer list is not propagated
730742
case hasFinalizers(inst):
731743
return false

internal/objects/reconciler_test.go

+50
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,32 @@ 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 'Allow'", func() {
753+
AddToHNCConfig(ctx, "", "secrets", api.Allow)
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+
Eventually(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+
// Give the reconciler some time to remove the object if it's going to.
774+
time.Sleep(500 * time.Millisecond)
775+
Eventually(HasObject(ctx, "secrets", barName, "foo-sec")).Should(BeFalse())
776+
})
727777
})
728778

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

internal/objects/validator.go

+32-5
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,10 @@ func (v *Validator) Handle(ctx context.Context, req admission.Request) admission
7979
if !config.IsManagedNamespace(req.Namespace) {
8080
return webhooks.Allow("unmanaged namespace " + req.Namespace)
8181
}
82-
// Allow changes to the types that are not in propagate mode. This is to dynamically enable/disable
82+
// Allow changes to the types that are not in Propagate or Allow mode. This is to dynamically enable/disable
8383
// object webhooks based on the types configured in hncconfig. Since the current admission rules only
8484
// apply to propagated objects, we can disable object webhooks on all other non-propagate-mode types.
85-
if !v.isPropagateType(req.Kind) {
85+
if !v.isPropagateType(req.Kind) && !v.isAllowType(req.Kind) {
8686
return webhooks.Allow("Non-propagate-mode types")
8787
}
8888
// Finally, let the HNC SA do whatever it wants.
@@ -114,6 +114,14 @@ func (v *Validator) isPropagateType(gvk metav1.GroupVersionKind) bool {
114114
return ts != nil && ts.GetMode() == api.Propagate
115115
}
116116

117+
func (v *Validator) isAllowType(gvk metav1.GroupVersionKind) bool {
118+
v.Forest.Lock()
119+
defer v.Forest.Unlock()
120+
121+
ts := v.Forest.GetTypeSyncerFromGroupKind(schema.GroupKind{Group: gvk.Group, Kind: gvk.Kind})
122+
return ts != nil && ts.GetMode() == api.Allow
123+
}
124+
117125
// handle implements the non-webhook-y businesss logic of this validator, allowing it to be more
118126
// easily unit tested (ie without constructing an admission.Request, setting up user infos, etc).
119127
func (v *Validator) handle(ctx context.Context, req *request) admission.Response {
@@ -142,6 +150,9 @@ func (v *Validator) handle(ctx context.Context, req *request) admission.Response
142150
if err := validateNoneSelectorChange(inst, oldInst); err != nil {
143151
return webhooks.DenyBadRequest(err)
144152
}
153+
if err := validateAllSelectorChange(inst, oldInst); err != nil {
154+
return webhooks.DenyBadRequest(err)
155+
}
145156
if msg := validateSelectorUniqueness(inst, oldInst); msg != "" {
146157
return webhooks.DenyBadRequest(errors.New(msg))
147158
}
@@ -169,15 +180,16 @@ func validateSelectorAnnot(inst *unstructured.Unstructured) string {
169180
}
170181
msg := "invalid HNC exceptions annotation: %v, should be one of the following: " +
171182
api.AnnotationSelector + "; " + api.AnnotationTreeSelector + "; " +
172-
api.AnnotationNoneSelector
183+
api.AnnotationNoneSelector + "; " + api.AnnotationAllSelector
173184
// If this annotation is part of HNC metagroup, we check if the prefix value is valid
174185
if segs[0] != api.AnnotationPropagatePrefix {
175186
return fmt.Sprintf(msg, key)
176187
}
177188
// check if the suffix is valid by checking the whole annotation key
178189
if key != api.AnnotationSelector &&
179190
key != api.AnnotationTreeSelector &&
180-
key != api.AnnotationNoneSelector {
191+
key != api.AnnotationNoneSelector &&
192+
key != api.AnnotationAllSelector {
181193
return fmt.Sprintf(msg, key)
182194
}
183195
}
@@ -188,12 +200,14 @@ func validateSelectorUniqueness(inst, oldInst *unstructured.Unstructured) string
188200
sel := selectors.GetSelectorAnnotation(inst)
189201
treeSel := selectors.GetTreeSelectorAnnotation(inst)
190202
noneSel := selectors.GetNoneSelectorAnnotation(inst)
203+
allSel := selectors.GetAllSelectorAnnotation(inst)
191204

192205
oldSel := selectors.GetSelectorAnnotation(oldInst)
193206
oldTreeSel := selectors.GetTreeSelectorAnnotation(oldInst)
194207
oldNoneSel := selectors.GetNoneSelectorAnnotation(oldInst)
208+
oldAllSel := selectors.GetAllSelectorAnnotation(oldInst)
195209

196-
isSelectorChange := oldSel != sel || oldTreeSel != treeSel || oldNoneSel != noneSel
210+
isSelectorChange := oldSel != sel || oldTreeSel != treeSel || oldNoneSel != noneSel || oldAllSel != allSel
197211
if !isSelectorChange {
198212
return ""
199213
}
@@ -207,6 +221,9 @@ func validateSelectorUniqueness(inst, oldInst *unstructured.Unstructured) string
207221
if noneSel != "" {
208222
found = append(found, api.AnnotationNoneSelector)
209223
}
224+
if allSel != "" {
225+
found = append(found, api.AnnotationAllSelector)
226+
}
210227
if len(found) <= 1 {
211228
return ""
212229
}
@@ -244,6 +261,16 @@ func validateNoneSelectorChange(inst, oldInst *unstructured.Unstructured) error
244261
return err
245262
}
246263

264+
func validateAllSelectorChange(inst, oldInst *unstructured.Unstructured) error {
265+
oldSelectorStr := selectors.GetAllSelectorAnnotation(oldInst)
266+
newSelectorStr := selectors.GetAllSelectorAnnotation(inst)
267+
if newSelectorStr == "" || oldSelectorStr == newSelectorStr {
268+
return nil
269+
}
270+
_, err := selectors.GetAllSelector(inst)
271+
return err
272+
}
273+
247274
func (v *Validator) handleInherited(ctx context.Context, req *request, newSource, oldSource string) admission.Response {
248275
op := req.op
249276
inst := req.obj

0 commit comments

Comments
 (0)