Skip to content

Commit 06c6279

Browse files
authored
Merge pull request kubernetes-retired#219 from mzeevi/hrqIssue-214
Subtree usage updated when namespace is removed from subtree
2 parents 347c5a3 + c3328bd commit 06c6279

File tree

8 files changed

+233
-74
lines changed

8 files changed

+233
-74
lines changed

internal/forest/namespacehrq.go

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"fmt"
55

66
v1 "k8s.io/api/core/v1"
7-
"k8s.io/apimachinery/pkg/api/resource"
87

98
"sigs.k8s.io/hierarchical-namespaces/internal/hrq/utils"
109
)
@@ -155,6 +154,9 @@ func (n *Namespace) canUseResources(u v1.ResourceList) error {
155154
// usages are different from ResourceQuota.Status.Used
156155
// - Called by the HRQ Namespace reconciler to remove `local` usages of a
157156
// namespace from the subtree usages of the previous ancestors of the namespace.
157+
// - Called by the SetParent to remove `local` usages of a namespace from
158+
// the subtree usages of the previous ancestors of the namespace and add the
159+
// usages to the new ancestors following a parent update
158160
func (n *Namespace) UseResources(newUsage v1.ResourceList) {
159161
oldUsage := n.quotas.used.local
160162
// We only consider limited usages.
@@ -257,14 +259,3 @@ func (n *Namespace) UpdateLimits(nm string, l v1.ResourceList) bool {
257259
n.quotas.limits[nm] = l
258260
return true
259261
}
260-
261-
// DeleteUsages removes the local usages of a namespace. The usages are also
262-
// removed from the subtree usages of the ancestors of the namespace. The caller
263-
// should enqueue the ancestor HRQs to update the usages.
264-
func (n *Namespace) DeleteUsages() {
265-
local := v1.ResourceList{}
266-
for r := range n.quotas.used.local {
267-
local[r] = resource.MustParse("0")
268-
}
269-
n.UseResources(local)
270-
}

internal/forest/namespacehrq_test.go

Lines changed: 56 additions & 57 deletions
Large diffs are not rendered by default.

internal/forest/namespacestructure.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"fmt"
55
"sort"
66
"strings"
7+
8+
v1 "k8s.io/api/core/v1"
79
)
810

911
// ChildNames returns a sorted list of names or nil if there are no children.
@@ -31,10 +33,15 @@ func (ns *Namespace) IsAncestor(other *Namespace) bool {
3133
return ns.parent.IsAncestor(other)
3234
}
3335

34-
// SetParent modifies the namespace's parent, including updating the list of children. It may result
35-
// in a cycle being created; this can be prevented by calling CanSetParent before, or seeing if it
36-
// happened by calling CycleNames afterwards.
36+
// SetParent modifies the namespace's parent, including updating the list of children and updating
37+
// the old and new subtree usage. It may result in a cycle being created; this can be prevented by
38+
// calling CanSetParent before, or seeing if it happened by calling CycleNames afterwards.
3739
func (ns *Namespace) SetParent(p *Namespace) {
40+
// Remove usage from old subtree.
41+
nsUsage := ns.quotas.used.local
42+
if nsUsage != nil {
43+
ns.UseResources(v1.ResourceList{})
44+
}
3845
// Remove old parent and cleans it up.
3946
if ns.parent != nil {
4047
delete(ns.parent.children, ns.name)
@@ -48,6 +55,10 @@ func (ns *Namespace) SetParent(p *Namespace) {
4855
if p != nil {
4956
p.children[ns.name] = ns
5057
}
58+
// Add usage to new subtree.
59+
if nsUsage != nil {
60+
ns.UseResources(nsUsage)
61+
}
5162
}
5263

5364
// CanSetParent returns the empty string if the assignment is currently legal, or a non-empty string

internal/hrq/hrqreconciler.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,35 @@ func isDeleted(inst *api.HierarchicalResourceQuota) bool {
156156
return inst.GetCreationTimestamp() == (metav1.Time{})
157157
}
158158

159+
// allSubtreeHRQs returns a slice of all HRQ objects in a subtree
160+
func (r *HierarchicalResourceQuotaReconciler) allSubtreeHRQs(ns *forest.Namespace) []api.HierarchicalResourceQuota {
161+
insts := []api.HierarchicalResourceQuota{}
162+
for _, nsnm := range ns.AncestryNames() {
163+
for _, hrqnm := range r.Forest.Get(nsnm).HRQNames() {
164+
inst := api.HierarchicalResourceQuota{}
165+
inst.ObjectMeta.Name = hrqnm
166+
inst.ObjectMeta.Namespace = nsnm
167+
168+
insts = append(insts, inst)
169+
}
170+
}
171+
return insts
172+
}
173+
174+
// OnChangeNamespace enqueues all HRQ objects in the subtree for later reconciliation.
175+
// This is needed so that the HRQ objects are enqueued for reconciliation when there is a
176+
// change in the tree hierarchy which affects the subtree usage of the HRQ objects.
177+
// This occurs in a goroutine so the caller doesn't block; since the
178+
// reconciler is never garbage-collected, this is safe.
179+
func (r *HierarchicalResourceQuotaReconciler) OnChangeNamespace(log logr.Logger, ns *forest.Namespace) {
180+
insts := r.allSubtreeHRQs(ns)
181+
go func() {
182+
for _, inst := range insts {
183+
r.trigger <- event.GenericEvent{Object: &inst}
184+
}
185+
}()
186+
}
187+
159188
// Enqueue enqueues a specific HierarchicalResourceQuota object to trigger the reconciliation of the
160189
// object for a given reason. This occurs in a goroutine so the caller doesn't block; since the
161190
// reconciler is never garbage-collected, this is safe.

internal/hrq/hrqreconciler_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,68 @@ var _ = Describe("HRQ reconciler tests", func() {
163163
Eventually(getRQSpec(ctx, barName)).Should(equalRL("secrets", "6", "pods", "3"))
164164
Eventually(getRQSpec(ctx, bazName)).Should(equalRL("secrets", "6", "pods", "3"))
165165
})
166+
167+
It("should update usages in status correctly after moving namespace out of subtree", func() {
168+
setHRQ(ctx, fooHRQName, fooName, "secrets", "6", "pods", "3")
169+
setHRQ(ctx, barHRQName, barName, "secrets", "100", "cpu", "50")
170+
setHRQ(ctx, bazHRQName, bazName, "pods", "1")
171+
// Simulate the K8s ResourceQuota controller to update usages.
172+
updateRQUsage(ctx, fooName, "secrets", "0", "pods", "0")
173+
updateRQUsage(ctx, barName, "secrets", "0", "cpu", "0", "pods", "0")
174+
updateRQUsage(ctx, bazName, "secrets", "0", "pods", "0")
175+
176+
// Increase pods count from 0 to 1 in baz and verify that the usage is
177+
// increased in foo's HRQ but not bar's (not an ancestor of baz)
178+
updateRQUsage(ctx, bazName, "pods", "1")
179+
Eventually(getHRQUsed(ctx, fooName, fooHRQName)).Should(equalRL("secrets", "0", "pods", "1"))
180+
Eventually(getHRQUsed(ctx, barName, barHRQName)).Should(equalRL("secrets", "0", "cpu", "0"))
181+
Eventually(getHRQUsed(ctx, bazName, bazHRQName)).Should(equalRL("pods", "1"))
182+
183+
// Make baz a full namespace by changing its parent to nil
184+
bazHier := GetHierarchy(ctx, bazName)
185+
bazHier.Spec.Parent = ""
186+
UpdateHierarchy(ctx, bazHier)
187+
188+
Eventually(HasChild(ctx, fooName, bazName)).ShouldNot(Equal(true))
189+
190+
// Ensure pods usage is decreased on foo after the change in hierarchy
191+
Eventually(getHRQUsed(ctx, fooName, fooHRQName)).Should(equalRL("secrets", "0", "pods", "0"))
192+
Eventually(getHRQUsed(ctx, barName, barHRQName)).Should(equalRL("secrets", "0", "cpu", "0"))
193+
Eventually(getHRQUsed(ctx, bazName, bazHRQName)).Should(equalRL("pods", "1"))
194+
})
195+
196+
It("should update usages in status correctly after moving full namespace with limits into hierarchy", func() {
197+
// Make bar a full namespace by changing its parent to nil
198+
barHier := GetHierarchy(ctx, barName)
199+
barHier.Spec.Parent = ""
200+
UpdateHierarchy(ctx, barHier)
201+
202+
Eventually(HasChild(ctx, fooName, barName)).ShouldNot(Equal(true))
203+
204+
setHRQ(ctx, fooHRQName, fooName, "secrets", "6", "pods", "3")
205+
setHRQ(ctx, barHRQName, barName, "secrets", "100", "cpu", "50")
206+
207+
// Simulate the K8s ResourceQuota controller to update usages.
208+
updateRQUsage(ctx, fooName, "secrets", "0", "pods", "0")
209+
updateRQUsage(ctx, barName, "secrets", "0", "cpu", "0", "pods", "0")
210+
211+
// Increase secrets count from 0 to 1 in bar and verify that the usage is
212+
// increased in bar's HRQ but not foo's (not an ancestor of baz)
213+
updateRQUsage(ctx, barName, "secrets", "1")
214+
Eventually(getHRQUsed(ctx, fooName, fooHRQName)).Should(equalRL("secrets", "0", "pods", "0"))
215+
Eventually(getHRQUsed(ctx, barName, barHRQName)).Should(equalRL("secrets", "1", "cpu", "0"))
216+
217+
// Make bar a full namespace by changing its parent to nil
218+
barHier = GetHierarchy(ctx, barName)
219+
barHier.Spec.Parent = fooName
220+
UpdateHierarchy(ctx, barHier)
221+
222+
Eventually(HasChild(ctx, fooName, barName)).Should(Equal(true))
223+
224+
// Ensure secrets usage is decreased on foo after the change in hierarchy
225+
Eventually(getHRQUsed(ctx, fooName, fooHRQName)).Should(equalRL("secrets", "1", "pods", "0"))
226+
Eventually(getHRQUsed(ctx, barName, barHRQName)).Should(equalRL("secrets", "1", "cpu", "0"))
227+
})
166228
})
167229

168230
func forestSetSubtreeUsages(ns string, args ...string) {

internal/hrq/rqreconciler.go

Lines changed: 1 addition & 2 deletions
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/setup/reconcilers.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ func CreateReconcilers(mgr ctrl.Manager, f *forest.Forest, opts Options) error {
8989
RQR: rqr,
9090
}
9191
rqr.HRQR = hrqr
92+
f.AddListener(hrqr)
9293

9394
if err := rqr.SetupWithManager(mgr); err != nil {
9495
return fmt.Errorf("cannot create resource quota reconciler: %s", err.Error())

test/e2e/hrq_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const (
2222
nsA = prefix+"a"
2323
nsB = prefix+"b"
2424
nsC = prefix+"c"
25+
nsD = prefix+"d"
2526

2627
propagationTime = 5
2728
resourceQuotaSingleton = "hrq.hnc.x-k8s.io"
@@ -138,6 +139,55 @@ var _ = PDescribe("Hierarchical Resource Quota", func() {
138139
FieldShouldContain("hrq", nsA, "a-hrq-persistentvolumeclaims", ".status.used", "persistentvolumeclaims:0")
139140
})
140141

142+
It("should update ancestor HRQ usages if a descendant namespace with limited resources is moved out of subtree", func() {
143+
// set up namespaces and subnamespaces
144+
CreateNamespace(nsA)
145+
CreateSubnamespace(nsB, nsA)
146+
CreateSubnamespace(nsC, nsB)
147+
CreateNamespace(nsD)
148+
149+
// set up an HRQ on the root namespaces and on the child namespace
150+
setHRQ("a-hrq-persistentvolumeclaims", nsA, "persistentvolumeclaims", "5")
151+
setHRQ("a-hrq-secrets", nsA, "secrets", "5")
152+
153+
setHRQ("a-hrq-persistentvolumeclaims", nsC, "persistentvolumeclaims", "3")
154+
setHRQ("a-hrq-secrets", nsC, "secrets", "3")
155+
156+
setHRQ("a-hrq-persistentvolumeclaims", nsD, "persistentvolumeclaims", "1")
157+
setHRQ("a-hrq-secrets", nsD, "secrets", "1")
158+
159+
// verify usage updates after consuming limited resources in the descendants
160+
Eventually(createPVC("b-pvc", nsB)).Should(Succeed())
161+
Eventually(createSecret("b-secret", nsB)).Should(Succeed())
162+
163+
FieldShouldContain("hrq", nsA, "a-hrq-persistentvolumeclaims", ".status.used", "persistentvolumeclaims:1")
164+
FieldShouldContain("hrq", nsA, "a-hrq-secrets", ".status.used", "secrets:1")
165+
166+
Eventually(createPVC("d-pvc", nsD)).Should(Succeed())
167+
Eventually(createSecret("d-secret", nsD)).Should(Succeed())
168+
169+
// make full namespace the child of a subnamespace
170+
MustRun("kubectl hns set", nsD, "--parent", nsC)
171+
172+
FieldShouldContain("hrq", nsA, "a-hrq-persistentvolumeclaims", ".status.used", "persistentvolumeclaims:2")
173+
FieldShouldContain("hrq", nsA, "a-hrq-secrets", ".status.used", "secrets:2")
174+
FieldShouldContain("hrq", nsC, "a-hrq-persistentvolumeclaims", ".status.used", "persistentvolumeclaims:1")
175+
FieldShouldContain("hrq", nsC, "a-hrq-secrets", ".status.used", "secrets:1")
176+
FieldShouldContain("hrq", nsD, "a-hrq-persistentvolumeclaims", ".status.used", "persistentvolumeclaims:1")
177+
FieldShouldContain("hrq", nsD, "a-hrq-secrets", ".status.used", "secrets:1")
178+
179+
// test moving out of subree the namespace with usages
180+
MustRun("kubectl hns set --root", nsD)
181+
182+
// verify the HRQ usages are updated
183+
FieldShouldContain("hrq", nsA, "a-hrq-persistentvolumeclaims", ".status.used", "persistentvolumeclaims:1")
184+
FieldShouldContain("hrq", nsA, "a-hrq-secrets", ".status.used", "secrets:1")
185+
FieldShouldContain("hrq", nsC, "a-hrq-persistentvolumeclaims", ".status.used", "persistentvolumeclaims:0")
186+
FieldShouldContain("hrq", nsC, "a-hrq-secrets", ".status.used", "secrets:0")
187+
FieldShouldContain("hrq", nsD, "a-hrq-persistentvolumeclaims", ".status.used", "persistentvolumeclaims:1")
188+
FieldShouldContain("hrq", nsD, "a-hrq-secrets", ".status.used", "secrets:1")
189+
})
190+
141191
It("should update descendant RQ limits or delete them if a namespace with HRQs is deleted", func() {
142192
// set up namespaces - 'a' as the parent of 'b' and 'c'.
143193
CreateNamespace(nsA)
@@ -306,6 +356,23 @@ spec:
306356
}
307357
}
308358

359+
func createSecret(nm, nsnm string) func() error {
360+
return func() error {
361+
secret := `# temp file created by hrq_test.go
362+
apiVersion: v1
363+
kind: Secret
364+
metadata:
365+
name: ` + nm + `
366+
namespace: ` + nsnm + `
367+
data:
368+
key: YmFyCg==`
369+
fn := writeTempFile(secret)
370+
GinkgoT().Log("Wrote " + fn + ":\n" + secret)
371+
defer removeFile(fn)
372+
return TryRun("kubectl apply -f", fn)
373+
}
374+
}
375+
309376
func writeTempFile(cxt string) string {
310377
f, err := ioutil.TempFile(os.TempDir(), "e2e-test-*.yaml")
311378
Expect(err).Should(BeNil())

0 commit comments

Comments
 (0)