Skip to content

Commit 5a784ea

Browse files
feat: add safe release name option to avoid random suffix in Helm release names
1 parent df72b9b commit 5a784ea

5 files changed

Lines changed: 136 additions & 113 deletions

File tree

internal/composition/composition.go

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,18 @@ const (
6565

6666
var _ controller.ExternalClient = (*handler)(nil)
6767

68-
func NewHandler(cfg *rest.Config, pig archive.Getter, event event.APIRecorder, pluralizer pluralizer.PluralizerInterface, chartInspectorUrl string, saName string, saNamespace string) controller.ExternalClient {
68+
type HandlerOptions struct {
69+
Kubeconfig *rest.Config
70+
PackageInfoGetter archive.Getter
71+
EventRecorder event.APIRecorder
72+
Pluralizer pluralizer.PluralizerInterface
73+
ChartInspectorUrl string
74+
SaName string
75+
SaNamespace string
76+
SafeReleaseName bool
77+
}
78+
79+
func NewHandler(opts *HandlerOptions) controller.ExternalClient {
6980
val, ok := os.LookupEnv(helmRegistryConfigPathEnvVar)
7081
if ok {
7182
helmRegistryConfigPath = val
@@ -74,13 +85,14 @@ func NewHandler(cfg *rest.Config, pig archive.Getter, event event.APIRecorder, p
7485
helmRegistryConfigFile = filepath.Join(helmRegistryConfigPath, registry.CredentialsFileBasename)
7586

7687
return &handler{
77-
kubeconfig: cfg,
78-
pluralizer: pluralizer,
79-
packageInfoGetter: pig,
80-
eventRecorder: event,
81-
chartInspectorUrl: chartInspectorUrl,
82-
saName: saName,
83-
saNamespace: saNamespace,
88+
kubeconfig: opts.Kubeconfig,
89+
pluralizer: opts.Pluralizer,
90+
packageInfoGetter: opts.PackageInfoGetter,
91+
eventRecorder: opts.EventRecorder,
92+
chartInspectorUrl: opts.ChartInspectorUrl,
93+
saName: opts.SaName,
94+
saNamespace: opts.SaNamespace,
95+
safeReleaseName: opts.SafeReleaseName,
8496
}
8597
}
8698

@@ -93,6 +105,8 @@ type handler struct {
93105
chartInspectorUrl string
94106
saName string
95107
saNamespace string
108+
// Feature flag to disable random suffix in Helm release names. This is highly discouraged as it can lead to release name collisions, but it can be useful for certain complex charts that have issues with long release names.
109+
safeReleaseName bool
96110
}
97111

98112
func (h *handler) Observe(ctx context.Context, mg *unstructured.Unstructured) (controller.ExternalObservation, error) {
@@ -116,7 +130,7 @@ func (h *handler) Observe(ctx context.Context, mg *unstructured.Unstructured) (c
116130
DynamicClient: dyn,
117131
}
118132

119-
compositionMeta.SetReleaseName(mg, compositionMeta.CalculateReleaseName(mg))
133+
compositionMeta.SetReleaseName(mg, compositionMeta.CalculateReleaseName(mg, h.safeReleaseName))
120134
if _, p := compositionMeta.GetGracefullyPausedTime(mg); p && compositionMeta.IsGracefullyPaused(mg) {
121135
log.Debug("Composition is gracefully paused, skipping observe.")
122136
h.eventRecorder.Event(mg, event.Normal(reasonReconciliationGracefullyPaused, "Observe", "Reconciliation is paused via the gracefully paused annotation."))
@@ -354,7 +368,7 @@ func (h *handler) Create(ctx context.Context, mg *unstructured.Unstructured) err
354368
return nil
355369
}
356370

357-
compositionMeta.SetReleaseName(mg, compositionMeta.CalculateReleaseName(mg))
371+
compositionMeta.SetReleaseName(mg, compositionMeta.CalculateReleaseName(mg, h.safeReleaseName))
358372
mg, err = tools.Update(ctx, mg, updateOpts)
359373
if err != nil {
360374
return fmt.Errorf("updating cr with values: %w", err)

internal/composition/composition_test.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -189,10 +189,18 @@ func TestController(t *testing.T) {
189189
return ctx
190190
}
191191

192-
handler = NewHandler(cfg.Client().RESTConfig(), pig, *event.NewAPIRecorder(rec), pluralizer, chartInspectorMockURL, "test-sa", altNamespace)
193-
194-
// handler = NewHandler(cfg.Client().RESTConfig(), log, pig, *event.NewAPIRecorder(rec), pluralizer, chartInspectorUrl, "test-sa", altNamespace)
195-
192+
// handler = NewHandler(cfg.Client().RESTConfig(), pig, *event.NewAPIRecorder(rec), pluralizer, chartInspectorMockURL, "test-sa", altNamespace)
193+
194+
handler = NewHandler(&HandlerOptions{
195+
Kubeconfig: cfg.Client().RESTConfig(),
196+
PackageInfoGetter: pig,
197+
EventRecorder: *event.NewAPIRecorder(rec),
198+
Pluralizer: pluralizer,
199+
ChartInspectorUrl: chartInspectorMockURL,
200+
SaName: "test-sa",
201+
SaNamespace: altNamespace,
202+
SafeReleaseName: true,
203+
})
196204
resli, err := decoder.DecodeAllFiles(ctx, os.DirFS(filepath.Join(testdataPath, "compositiondefinitions")), "*.yaml")
197205
if err != nil {
198206
t.Log("Error decoding CRDs: ", err)

internal/meta/meta.go

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,20 @@ const (
3333
AnnotationKeyReconciliationGracefullyPausedTime = "krateo.io/gracefully-paused-time"
3434
)
3535

36-
func CalculateReleaseName(o runtime.Object) string {
36+
// CalculateReleaseName generates a release name for the Helm chart based on the name and UID of the resource.
37+
// If safeReleaseName is false, it will not append a random suffix to the release name, which can lead to collisions but is necessary for certain complex charts that have issues with long release names.
38+
func CalculateReleaseName(o runtime.Object, safeReleaseName bool) string {
3739
obj := o.(metav1.Object)
38-
uid := obj.GetUID()
39-
if uid == "" {
40-
// Generate random string if UID is not set
41-
return fmt.Sprintf("%s-%s", obj.GetName(), rand.SafeEncodeString(rand.String(8)))
40+
if safeReleaseName {
41+
uid := obj.GetUID()
42+
if uid == "" {
43+
// Generate random string if UID is not set
44+
return fmt.Sprintf("%s-%s", obj.GetName(), rand.SafeEncodeString(rand.String(8)))
45+
}
46+
hashstr := rand.SafeEncodeString(string(obj.GetUID())[:8])
47+
return fmt.Sprintf("%s-%s", obj.GetName(), hashstr)
4248
}
43-
hashstr := rand.SafeEncodeString(string(obj.GetUID())[:8])
44-
return fmt.Sprintf("%s-%s", obj.GetName(), hashstr)
49+
return obj.GetName()
4550
}
4651

4752
func GetReleaseName(o metav1.Object) string {
@@ -55,6 +60,7 @@ func SetReleaseName(o metav1.Object, name string) {
5560
if mglabels == nil {
5661
mglabels = make(map[string]string)
5762
}
63+
5864
if _, ok := mglabels[ReleaseNameLabel]; !ok {
5965
mglabels[ReleaseNameLabel] = name
6066
}

internal/meta/meta_test.go

Lines changed: 68 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package meta
22

33
import (
4-
"fmt"
54
"strings"
65
"testing"
76
"time"
@@ -406,102 +405,80 @@ func TestGetGracefullyPausedTime(t *testing.T) {
406405
})
407406
}
408407
}
409-
func TestCalculateReleaseName_Deterministic(t *testing.T) {
410-
obj := unstructured.Unstructured{}
411-
obj.SetName("my-resource")
412-
obj.SetGroupVersionKind(schema.GroupVersionKind{
413-
Group: "test.group",
414-
Version: "v1",
415-
Kind: "MyKind",
416-
})
417-
obj.SetUID(types.UID("5a47edd9-c710-4b4b-b5ea-b6cdf9fc1f58"))
418-
419-
r1 := CalculateReleaseName(&obj)
420-
r2 := CalculateReleaseName(&obj)
421-
fmt.Println(r1)
422-
fmt.Println(r2)
423-
424-
if r1 == "" {
425-
t.Fatalf("CalculateReleaseName returned empty string")
426-
}
427-
if r1 != r2 {
428-
t.Fatalf("CalculateReleaseName not deterministic: %q vs %q", r1, r2)
429-
}
430-
if !strings.HasPrefix(r1, "my-resource-") {
431-
t.Fatalf("CalculateReleaseName result %q does not have expected prefix %q", r1, "my-resource-")
432-
}
433-
}
434-
435-
func TestCalculateReleaseName_DifferentGVKProducesDifferentHash(t *testing.T) {
436-
name := "same-name"
437-
438-
obj1 := unstructured.Unstructured{}
439-
obj1.SetName(name)
440-
obj1.SetGroupVersionKind(schema.GroupVersionKind{
441-
Group: "group.one",
442-
Version: "v1",
443-
Kind: "KindA",
444-
})
445-
obj1.SetUID(types.UID("5a47edd9-c710-4b4b-b5ea-b6cdf9fc1f58"))
446-
447-
obj2 := unstructured.Unstructured{}
448-
obj2.SetName(name)
449-
obj2.SetGroupVersionKind(schema.GroupVersionKind{
450-
Group: "group.two",
451-
Version: "v1",
452-
Kind: "KindA",
453-
})
454-
obj2.SetUID(types.UID("b3d6f4e2-1c2d-4e5f-9a6b-7c8d9e0f1a2b"))
455-
456-
r1 := CalculateReleaseName(&obj1)
457-
r2 := CalculateReleaseName(&obj2)
458-
459-
if r1 == r2 {
460-
t.Fatalf("Expected different release names for different GVKs but got same: %q", r1)
408+
func TestCalculateReleaseName(t *testing.T) {
409+
tests := []struct {
410+
name string
411+
objName string
412+
uid string
413+
safeReleaseName bool
414+
expectedPrefix string
415+
isDeterministic bool
416+
}{
417+
{
418+
name: "Deterministic with UID and safeReleaseName",
419+
objName: "my-resource",
420+
uid: "5a47edd9-c710-4b4b-b5ea-b6cdf9fc1f58",
421+
safeReleaseName: true,
422+
expectedPrefix: "my-resource-",
423+
isDeterministic: true,
424+
},
425+
{
426+
name: "Deterministic without safeReleaseName",
427+
objName: "simple-name",
428+
uid: "5a47edd9-c710-4b4b-b5ea-b6cdf9fc1f58",
429+
safeReleaseName: false,
430+
expectedPrefix: "simple-name",
431+
isDeterministic: true,
432+
},
433+
{
434+
name: "Non-deterministic when UID is missing and safeReleaseName is true",
435+
objName: "no-uid-resource",
436+
uid: "",
437+
safeReleaseName: true,
438+
expectedPrefix: "no-uid-resource-",
439+
isDeterministic: false,
440+
},
461441
}
462-
}
463-
464-
func TestCalculateReleaseName_NameIncludedAndUniqueForDifferentNames(t *testing.T) {
465-
gvk := schema.GroupVersionKind{Group: "example.io", Version: "v1", Kind: "Example"}
466-
objA := unstructured.Unstructured{}
467-
objA.SetName("alpha")
468-
objA.SetGroupVersionKind(gvk)
469-
objA.SetUID(types.UID("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"))
470442

471-
objB := unstructured.Unstructured{}
472-
objB.SetName("beta")
473-
objB.SetGroupVersionKind(gvk)
474-
objB.SetUID(types.UID("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"))
475-
476-
ra := CalculateReleaseName(&objA)
477-
rb := CalculateReleaseName(&objB)
443+
for _, tt := range tests {
444+
t.Run(tt.name, func(t *testing.T) {
445+
obj := &unstructured.Unstructured{}
446+
obj.SetName(tt.objName)
447+
if tt.uid != "" {
448+
obj.SetUID(types.UID(tt.uid))
449+
}
478450

479-
if !strings.HasPrefix(ra, "alpha-") {
480-
t.Fatalf("Release name %q does not start with expected prefix %q", ra, "alpha-")
481-
}
482-
if !strings.HasPrefix(rb, "beta-") {
483-
t.Fatalf("Release name %q does not start with expected prefix %q", rb, "beta-")
484-
}
485-
if ra == rb {
486-
t.Fatalf("Expected different release names for different resource names but got same: %q", ra)
487-
}
488-
}
451+
// First execution
452+
r1 := CalculateReleaseName(obj, tt.safeReleaseName)
453+
// Second execution to check determinism
454+
r2 := CalculateReleaseName(obj, tt.safeReleaseName)
489455

490-
func TestCalculateReleaseName_UIDNotFound(t *testing.T) {
491-
obj := unstructured.Unstructured{}
492-
obj.SetName("no-uid-resource")
493-
obj.SetGroupVersionKind(schema.GroupVersionKind{
494-
Group: "test.group",
495-
Version: "v1",
496-
Kind: "NoUIDKind",
497-
})
498-
// Not setting UID
456+
// 1. Basic validation
457+
if r1 == "" {
458+
t.Fatal("CalculateReleaseName returned an empty string")
459+
}
499460

500-
releaseName := CalculateReleaseName(&obj)
461+
// 2. Prefix validation
462+
if !strings.HasPrefix(r1, tt.expectedPrefix) {
463+
t.Errorf("Expected prefix %q, got %q", tt.expectedPrefix, r1)
464+
}
501465

502-
fmt.Println(releaseName)
466+
// 3. Determinism validation
467+
if tt.isDeterministic {
468+
if r1 != r2 {
469+
t.Errorf("Result should be deterministic but changed: %q vs %q", r1, r2)
470+
}
471+
} else {
472+
// For non-deterministic cases, we expect them to be different
473+
if r1 == r2 {
474+
t.Errorf("Result should be random but was identical: %q", r1)
475+
}
476+
}
503477

504-
if !strings.HasPrefix(releaseName, "no-uid-resource-") {
505-
t.Fatalf("CalculateReleaseName result %q does not have expected prefix %q", releaseName, "no-uid-resource-")
478+
// 4. Special case: safeReleaseName = false should return EXACTLY the name
479+
if !tt.safeReleaseName && r1 != tt.objName {
480+
t.Errorf("Expected exact name %q, got %q", tt.objName, r1)
481+
}
482+
})
506483
}
507484
}

main.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ func main() {
7575
env.Int("COMPOSITION_CONTROLLER_MAX_ERROR_RETRIES", 5), "How many times to retry the processing of a resource when an error occurs before giving up and dropping the resource.")
7676
metricsServerPort := flag.Int("metrics-server-port",
7777
env.Int("COMPOSITION_CONTROLLER_METRICS_SERVER_PORT", 0), "The address to bind the metrics server to. If empty, metrics server is disabled.")
78+
safeReleaseName := flag.Bool("safe-release-name",
79+
env.Bool("COMPOSITION_CONTROLLER_SAFE_RELEASE_NAME", true), "If disabled the randmom suffix is not appended in the Helm release name. This can be useful for avoid having problems with complex helm charts. The use of this option is highly discouraged, as it can lead to release name collisions.")
7880

7981
flag.Usage = func() {
8082
fmt.Fprintln(flag.CommandLine.Output(), "Flags:")
@@ -155,6 +157,7 @@ func main() {
155157
WithValues("maxErrorRetryInterval", *maxErrorRetryInterval).
156158
WithValues("maxErrorRetry", *maxErrorRetry).
157159
WithValues("metricsServerPort", *metricsServerPort).
160+
WithValues("safeReleaseName", *safeReleaseName).
158161
Info("Starting composition dynamic controller.")
159162

160163
// Create a label requirement for the composition version
@@ -165,7 +168,22 @@ func main() {
165168
}
166169
labelselector := labels.NewSelector().Add(*labelreq)
167170

168-
handler := composition.NewHandler(cfg, pig, *event.NewAPIRecorder(rec), *pluralizer, *urlChartInspector, *saName, *saNamespace)
171+
apiRecorder := event.NewAPIRecorder(rec)
172+
if apiRecorder == nil {
173+
log.Error(fmt.Errorf("creating API recorder"), "API recorder is nil, events will not be recorded.")
174+
os.Exit(1)
175+
}
176+
177+
handler := composition.NewHandler(&composition.HandlerOptions{
178+
Kubeconfig: cfg,
179+
Pluralizer: pluralizer,
180+
PackageInfoGetter: pig,
181+
EventRecorder: *apiRecorder,
182+
ChartInspectorUrl: *urlChartInspector,
183+
SaName: *saName,
184+
SaNamespace: *saNamespace,
185+
SafeReleaseName: *safeReleaseName,
186+
})
169187

170188
opts := []builder.FuncOption{
171189
builder.WithLogger(log),

0 commit comments

Comments
 (0)