Skip to content

Commit 2c8194a

Browse files
authored
Multiple scoped HRQs with the same name (#24)
1 parent 93e54a4 commit 2c8194a

File tree

7 files changed

+274
-39
lines changed

7 files changed

+274
-39
lines changed

internal/hrq/hrqreconciler.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,11 @@ func (r *HierarchicalResourceQuotaReconciler) Reconcile(ctx context.Context, req
9696

9797
rqName := api.ResourceQuotaSingletonName
9898
if r.Forest.IsMarkedAsScopedHRQ(req.NamespacedName) {
99-
rqName = utils.ScopedRQName(inst.GetName())
99+
rqName, err = utils.ScopedRQName(inst.GetNamespace(), inst.GetName())
100+
if err != nil {
101+
log.Error(err, "Couldn't get the scoped RQ name")
102+
return ctrl.Result{}, err
103+
}
100104
}
101105

102106
// Enqueue ResourceQuota objects in the current namespace and its descendants
@@ -116,6 +120,8 @@ func (r *HierarchicalResourceQuotaReconciler) Reconcile(ctx context.Context, req
116120
// forest. The first return value is true if the HRQ object is updated; the
117121
// second return value is true if the forest is updated.
118122
func (r *HierarchicalResourceQuotaReconciler) syncWithForest(log logr.Logger, inst *api.HierarchicalResourceQuota) (bool, bool, error) {
123+
var err error
124+
119125
r.Forest.Lock()
120126
defer r.Forest.Unlock()
121127

@@ -126,10 +132,16 @@ func (r *HierarchicalResourceQuotaReconciler) syncWithForest(log logr.Logger, in
126132
if isScopedHRQ {
127133
log.Info("Marking HRQ as scoped", "name", inst.GetName(), "namespace", inst.GetNamespace())
128134
r.Forest.MarkScopedRQ(nn)
129-
rqName = utils.ScopedRQName(inst.GetName())
135+
rqName, err = utils.ScopedRQName(inst.GetNamespace(), inst.GetName())
136+
if err != nil {
137+
return false, false, err
138+
}
130139
} else if r.Forest.IsMarkedAsScopedHRQ(nn) {
131140
log.Info("Detect the Scoped HRQ because of the mark")
132-
rqName = utils.ScopedRQName(inst.GetName())
141+
rqName, err = utils.ScopedRQName(inst.GetNamespace(), inst.GetName())
142+
if err != nil {
143+
return false, false, err
144+
}
133145
} else {
134146
log.Info("HRQ is a singleton")
135147
}

internal/hrq/hrqreconciler_test.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,11 @@ var _ = Describe("HRQ reconciler tests", func() {
150150

151151
// Scoped HRQs don't affect the result of TestCheckHRQDrift.
152152
forestOverrideSubtreeUsages("hrq-selector", "cpu", "3")
153-
updateRQUsage(ctx, fooName, utils.ScopedRQName("hrq-selector"), "cpu", "5")
153+
154+
scopedRQ, err := utils.ScopedRQName(fooName, "hrq-selector")
155+
Expect(err).NotTo(HaveOccurred())
156+
updateRQUsage(ctx, fooName, scopedRQ, "cpu", "5")
157+
154158
drift, err = TestCheckHRQDrift()
155159
Expect(err).NotTo(HaveOccurred())
156160
Expect(drift).Should(BeFalse())

internal/hrq/rqreconciler.go

Lines changed: 78 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,19 @@ func (r *ResourceQuotaReconciler) Reconcile(ctx context.Context, req ctrl.Reques
8383
}
8484

8585
isSingleton := utils.IsSingletonRQ(inst)
86+
isLegacyScoped := utils.IsLegacyScopedRQ(inst)
87+
88+
if isLegacyScoped {
89+
// Ignore legacy scoped RQs
90+
// It will be deleted in the reconcile loop for the new RQ.
91+
return ctrl.Result{}, nil
92+
}
93+
94+
if !notFound && !isSingleton { // scoped RQ exists
95+
if err := r.deleteLegacyScopedRQ(ctx, log, inst); err != nil {
96+
return ctrl.Result{}, err
97+
}
98+
}
8699

87100
r.Forest.Lock()
88101
ns := r.Forest.Get(inst.ObjectMeta.Namespace)
@@ -100,43 +113,31 @@ func (r *ResourceQuotaReconciler) Reconcile(ctx context.Context, req ctrl.Reques
100113

101114
log.Info("Reconciling ResourceQuota", "name", fmt.Sprintf("%s/%s", inst.GetNamespace(), inst.GetName()), "limits", inst.Spec.Hard, "usages", inst.Status.Used, "updated", updated)
102115

116+
var hrq *api.HierarchicalResourceQuota
117+
103118
// Delete the obsolete singleton and early exit if the new limits are empty.
104119
if inst.Spec.Hard == nil {
105120
return ctrl.Result{}, r.deleteRQ(ctx, log, inst)
106121
} else if !isSingleton && notFound {
107-
hrq := &api.HierarchicalResourceQuota{}
108-
hrqName, err := utils.ScopedHRQNameFromHRQName(inst.Name)
122+
hrqnnm, err := r.findHRQNameForRQ(inst)
109123
if err != nil {
110-
return ctrl.Result{}, fmt.Errorf("while getting hrq name: %w", err)
124+
return ctrl.Result{}, fmt.Errorf("while finding hrq name for new RQ: %w", err)
111125
}
112126

113-
cursorNm := ns
114-
var found bool
115-
for {
116-
if cursorNm == nil {
117-
break
118-
}
119-
120-
hrqnnm := types.NamespacedName{Namespace: cursorNm.Name(), Name: hrqName}
121-
err := r.Get(ctx, hrqnnm, hrq)
122-
if err == nil {
123-
found = true
124-
break
125-
}
127+
hrq = &api.HierarchicalResourceQuota{}
128+
err = r.Get(ctx, hrqnnm, hrq)
129+
if err != nil {
126130
if apierrors.IsNotFound(err) {
127-
cursorNm = cursorNm.Parent()
128-
continue
131+
return ctrl.Result{}, fmt.Errorf("the parent hrq not found: %s", hrqnnm)
132+
} else {
133+
return ctrl.Result{}, fmt.Errorf("getting hrq %s: %w", hrqnnm, err)
129134
}
130-
131-
return ctrl.Result{}, fmt.Errorf("while getting hrq: %w", err)
132-
}
133-
if !found {
134-
return ctrl.Result{}, fmt.Errorf("the parent hrq not found: %s", hrqName)
135135
}
136136

137137
log.Info("Found the parent HRQ", "namespace", hrq.Namespace, "name", hrq.Name)
138138

139139
inst.Spec.ScopeSelector = hrq.Spec.ScopeSelector
140+
utils.SetLabelsAnnotationsForScopedRQ(inst, hrq.Namespace, hrq.Name)
140141
}
141142

142143
// We only need to write back to the apiserver if the spec has changed
@@ -229,6 +230,38 @@ func (r *ResourceQuotaReconciler) getAncestorHRQs(inst *v1.ResourceQuota) []type
229230
return names
230231
}
231232

233+
// findHRQNameForRQ finds HRQ name and namespace for a new RQ by searching through namespace and ancestor HRQs
234+
func (r *ResourceQuotaReconciler) findHRQNameForRQ(inst *v1.ResourceQuota) (types.NamespacedName, error) {
235+
hrqnnm, err := utils.ScopedHRQNameFromRQ(inst)
236+
if err == nil {
237+
return hrqnnm, nil
238+
}
239+
240+
// New RQs that don't have the labels yet, so we need to search through the forest
241+
r.Forest.Lock()
242+
defer r.Forest.Unlock()
243+
244+
ns := r.Forest.Get(inst.Namespace)
245+
246+
// Check current namespace and all ancestors
247+
namespaces := []string{inst.Namespace}
248+
namespaces = append(namespaces, ns.AncestryNames()...)
249+
250+
for _, nsnm := range namespaces {
251+
ancestorNS := r.Forest.Get(nsnm)
252+
for _, hrqName := range ancestorNS.HRQNames() {
253+
expectedRQName, err := utils.ScopedRQName(nsnm, hrqName)
254+
if err != nil {
255+
continue
256+
}
257+
if expectedRQName == inst.Name {
258+
return types.NamespacedName{Namespace: nsnm, Name: hrqName}, nil
259+
}
260+
}
261+
}
262+
return types.NamespacedName{}, fmt.Errorf("no matching HRQ found for RQ: %s", inst.Name)
263+
}
264+
232265
// deleteRQ deletes a resource quota on the apiserver and a quota in on-memory if it exists. Otherwise,
233266
// do nothing.
234267
func (r *ResourceQuotaReconciler) deleteRQ(ctx context.Context, log logr.Logger, inst *v1.ResourceQuota) error {
@@ -313,6 +346,28 @@ func (r *ResourceQuotaReconciler) syncResourceLimits(ns *forest.Namespace, inst
313346
return true
314347
}
315348

349+
// deleteLegacyScopedRQ deletes the legacy scoped RQ if it exists.
350+
func (r *ResourceQuotaReconciler) deleteLegacyScopedRQ(ctx context.Context, log logr.Logger, newRQ *v1.ResourceQuota) error {
351+
hrqnnm, err := utils.ScopedHRQNameFromRQ(newRQ)
352+
if err != nil {
353+
return fmt.Errorf("get HRQ name from RQ: %w", err)
354+
}
355+
356+
legacyRQName := utils.LegacyScopedRQName(hrqnnm.Name)
357+
358+
legacyRQ, err := r.getRQ(ctx, newRQ.Namespace, legacyRQName)
359+
if err != nil {
360+
if apierrors.IsNotFound(err) {
361+
return nil
362+
} else {
363+
return err
364+
}
365+
}
366+
367+
log.Info("Deleting legacy scoped RQ", "legacyRQ", legacyRQ.Name, "namespace", legacyRQ.Namespace)
368+
return r.deleteRQ(ctx, log, legacyRQ)
369+
}
370+
316371
// OnChangeNamespace enqueues the singleton in a specific namespace to trigger the reconciliation of
317372
// the singleton for a given reason . This occurs in a goroutine so the caller doesn't block; since
318373
// the reconciler is never garbage-collected, this is safe.

internal/hrq/rqreconciler_test.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ var _ = Describe("RQ reconciler tests", func() {
6868
setHRQ(ctx, barHRQName, barName, nil, "secrets", "100", "cpu", "50")
6969
setHRQ(ctx, bazHRQName, bazName, nil, "pods", "1")
7070
hrqWithSelectorName := "hrq-with-selector"
71-
rqName := fmt.Sprintf("%s-%s", api.ResourceQuotaSingletonName, hrqWithSelectorName)
71+
rqName, err := utils.ScopedRQName(fooName, hrqWithSelectorName)
72+
Expect(err).NotTo(HaveOccurred())
7273
setHRQ(ctx, hrqWithSelectorName, fooName, &highPrioritySelector, "cpu", "4", "pods", "2")
7374

7475
Eventually(getRQHard(ctx, fooName, api.ResourceQuotaSingletonName)).Should(equalRL("secrets", "6", "pods", "3"))
@@ -89,7 +90,8 @@ var _ = Describe("RQ reconciler tests", func() {
8990
setHRQ(ctx, bazHRQName, bazName, nil, "pods", "1")
9091

9192
hrqWithSelectorName := "hrq-with-selector"
92-
rqName := fmt.Sprintf("%s-%s", api.ResourceQuotaSingletonName, hrqWithSelectorName)
93+
rqName, err := utils.ScopedRQName(fooName, hrqWithSelectorName)
94+
Expect(err).NotTo(HaveOccurred())
9395
setHRQ(ctx, hrqWithSelectorName, fooName, &highPrioritySelector, "cpu", "6", "pods", "3")
9496
setHRQ(ctx, hrqWithSelectorName, barName, &highPrioritySelector, "cpu", "100", "pods", "50")
9597
setHRQ(ctx, hrqWithSelectorName, bazName, &highPrioritySelector, "pods", "1")

internal/hrq/utils/scopedhrq.go

Lines changed: 76 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
11
package utils
22

33
import (
4+
"crypto/md5"
5+
"encoding/hex"
46
"fmt"
57
"strings"
68

79
v1 "k8s.io/api/core/v1"
8-
10+
"k8s.io/apimachinery/pkg/types"
11+
"k8s.io/apimachinery/pkg/util/validation"
912
api "sigs.k8s.io/hierarchical-namespaces/api/v1alpha2"
13+
"sigs.k8s.io/hierarchical-namespaces/internal/metadata"
14+
)
15+
16+
const (
17+
hrqNameLabel = "hnc.x-k8s.io/hrq-name"
18+
hrqNamespaceLabel = "hnc.x-k8s.io/hrq-namespace"
1019
)
1120

1221
func IsSingletonRQ(rq *v1.ResourceQuota) bool {
@@ -17,15 +26,77 @@ func IsScopedRQ(rq *v1.ResourceQuota) bool {
1726
return !IsSingletonRQ(rq)
1827
}
1928

20-
func ScopedRQName(hrqName string) string {
29+
// IsHNCManagedRQ checks if an RQ has the HNC cleanup label
30+
func IsHNCManagedRQ(rq *v1.ResourceQuota) bool {
31+
if label, ok := metadata.GetLabel(rq, api.HRQLabelCleanup); ok {
32+
return label == "true"
33+
}
34+
return false
35+
}
36+
37+
// IsLegacyScopedRQ checks if an RQ is a legacy scoped RQ (old format without namespace)
38+
// and is managed by HNC
39+
func IsLegacyScopedRQ(rq *v1.ResourceQuota) bool {
40+
if IsSingletonRQ(rq) {
41+
return false
42+
}
43+
// Only consider RQs managed by HNC for migration
44+
if !IsHNCManagedRQ(rq) {
45+
return false
46+
}
47+
_, nsLabelFound := metadata.GetLabel(rq, hrqNamespaceLabel)
48+
_, nameLabelFound := metadata.GetLabel(rq, hrqNameLabel)
49+
return !nsLabelFound || !nameLabelFound
50+
}
51+
52+
// LegacyScopedRQName generates the legacy RQ name for backward compatibility
53+
func LegacyScopedRQName(hrqName string) string {
2154
return api.ResourceQuotaSingletonName + "-" + hrqName
2255
}
2356

24-
func ScopedHRQNameFromHRQName(rqName string) (string, error) {
57+
// HRQNameFromLegacyRQName extracts HRQ name from legacy RQ name
58+
func HRQNameFromLegacyRQName(rqName string) (string, error) {
59+
if rqName == api.ResourceQuotaSingletonName {
60+
return "", fmt.Errorf("invalid legacy RQ name: %s", rqName)
61+
}
62+
2563
hrqName := strings.TrimPrefix(rqName, api.ResourceQuotaSingletonName+"-")
26-
if hrqName == api.ResourceQuotaSingletonName {
27-
return "", fmt.Errorf("invalid ScopedHRQ name: %s", hrqName)
64+
if hrqName == rqName {
65+
return "", fmt.Errorf("not a legacy scoped RQ name: %s", rqName)
2866
}
2967

3068
return hrqName, nil
3169
}
70+
71+
func ScopedRQName(hrqNamespace string, hrqName string) (string, error) {
72+
hash := md5.Sum([]byte(fmt.Sprintf("%s/%s", hrqNamespace, hrqName)))
73+
hashStr := hex.EncodeToString(hash[:])
74+
75+
namespaceAndName := truncate(
76+
fmt.Sprintf("%s-%s", hrqNamespace, hrqName),
77+
uint(validation.DNS1123SubdomainMaxLength-len(hashStr)-len(api.ResourceQuotaSingletonName)-2),
78+
)
79+
80+
return fmt.Sprintf("%s-%s-%s", api.ResourceQuotaSingletonName, namespaceAndName, hashStr), nil
81+
}
82+
83+
func ScopedHRQNameFromRQ(rq *v1.ResourceQuota) (types.NamespacedName, error) {
84+
namespace, nsOK := metadata.GetLabel(rq, hrqNamespaceLabel)
85+
name, nameOK := metadata.GetLabel(rq, hrqNameLabel)
86+
if nsOK && nameOK {
87+
return types.NamespacedName{Namespace: namespace, Name: name}, nil
88+
}
89+
return types.NamespacedName{}, fmt.Errorf("no matching HRQ found for RQ: %s", rq.Name)
90+
}
91+
92+
func SetLabelsAnnotationsForScopedRQ(rq *v1.ResourceQuota, hrqNamespace string, hrqName string) {
93+
metadata.SetLabel(rq, hrqNamespaceLabel, hrqNamespace)
94+
metadata.SetLabel(rq, hrqNameLabel, hrqName)
95+
}
96+
97+
func truncate(s string, n uint) string {
98+
if uint(len(s)) <= n {
99+
return s
100+
}
101+
return s[:n]
102+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package utils
2+
3+
import "testing"
4+
5+
func TestScopedRQName(t *testing.T) {
6+
tests := []struct {
7+
name string
8+
hrqNamespace string
9+
hrqName string
10+
want string
11+
}{
12+
{
13+
name: "short",
14+
hrqNamespace: "default",
15+
hrqName: "test",
16+
want: "hrq.hnc.x-k8s.io-default-test-1b5cb9615ea99c0edaf5b1f157ce3997",
17+
},
18+
{
19+
name: "too_long",
20+
hrqNamespace: "default",
21+
hrqName: "12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890",
22+
want: "hrq.hnc.x-k8s.io-default-123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345-dc2d54f49ea75bc9da92c7272bc626d7", // 253 chars
23+
},
24+
}
25+
26+
for _, test := range tests {
27+
t.Run(test.name, func(t *testing.T) {
28+
got, err := ScopedRQName(test.hrqNamespace, test.hrqName)
29+
if err != nil {
30+
t.Errorf("ScopedRQName(%s, %s) = %v", test.hrqNamespace, test.hrqName, err)
31+
}
32+
if got != test.want {
33+
t.Errorf("ScopedRQName(%s, %s) = %s, want %s", test.hrqNamespace, test.hrqName, got, test.want)
34+
}
35+
})
36+
}
37+
}

0 commit comments

Comments
 (0)