Skip to content

Commit ae7901d

Browse files
feat: add safe release name option to avoid random suffix in Helm rel… (#169)
* feat: add safe release name option to avoid random suffix in Helm release names * docs: add safe release name option (feature flag) to avoid random suffix in Helm release names * docs: update Helm release name logic to enhance multi-tenancy support and character limit handling * docs: clarify caution regarding disabling uniqueness for composition names in README * refactor: remove unused event reasons from composition constants
1 parent d6170ef commit ae7901d

6 files changed

Lines changed: 182 additions & 136 deletions

File tree

README.md

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -95,25 +95,57 @@ The **Composition Dynamic Controller (CDC)** is a specialized Kubernetes operato
9595

9696
## Helm Release Name Logic
9797

98+
The logic used by the **`composition-dynamic-controller`** to determine the Helm release name has evolved to better handle multi-tenancy and character limits.
99+
100+
---
101+
98102
### Prior Versions (<= 0.19.9)
99103

100-
For versions up to 0.19.9, the **`composition-dynamic-controller`** used the following logic to determine the **Helm release name** associated with a composition resource:
104+
In these versions, the release name was determined by:
105+
106+
1. The value of the **label** `krateo.io/release-name` (if set).
107+
2. The **Composition resource name** (as a fallback).
108+
109+
---
110+
111+
### Versions 0.20.0 to 0.20.2
112+
113+
Starting with v0.20.0, the logic shifted to ensure uniqueness across different namespaces:
101114

102-
1. If the **label** `krateo.io/release-name` is set on the composition resource, its value is used as the Helm release name.
103-
2. Otherwise, the **composition resource name** is used as the Helm release name.
115+
1. If the **annotation** `krateo.io/release-name` is set, its value is used.
116+
2. Otherwise, the name is generated as: `{composition.metadata.name}-{composition.metadata.uid[:8]}`.
117+
118+
> [!IMPORTANT]
119+
> Because the UID suffix adds 9 characters (hyphen + 8-char UID) and Helm limits release names to **53 characters**, the `metadata.name` of a Composition cannot exceed **44 characters**.
104120
105121
---
106122

107-
### Subsequent Versions (>= 0.20.0)
123+
### Versions 0.20.3+ (Configurable Logic)
124+
125+
Starting from **v0.20.3**, the environment variable `COMPOSITION_CONTROLLER_SAFE_RELEASE_NAME` allows you to toggle between modern and legacy naming conventions.
126+
127+
#### Default Behavior (`true`)
128+
129+
The controller appends the **UID suffix** to ensure uniqueness across namespaces. This is the recommended setting to prevent Helm naming collisions when identical Composition names exist in different namespaces.
108130

109-
Starting from version 0.20.0, the **`composition-dynamic-controller`** uses the following logic to determine the Helm release name associated with a composition resource:
131+
#### Legacy Behavior (`false`)
110132

111-
1. If the **annotation** `krateo.io/release-name` is set on the composition resource, its value is used as the Helm release name.
112-
2. Otherwise, the release name is composed as follows: **`{composition.metadata.name}-{composition.metadata.uid[:8]}`**.
133+
The controller reverts to the logic used prior to v0.20.0. The release name is determined by:
134+
135+
1. The value of the `krateo.io/release-name` **annotation** (if set).
136+
2. The **Composition resource name**.
137+
138+
> [!CAUTION]
139+
> **Disabling this option is highly discouraged.** While it provides backward compatibility for charts with strict character length limits, it removes the uniqueness guarantee. Creating Compositions with the same name in different namespaces will cause release name collisions and failed Helm operations. It's up to the administrator to enforce a policy between users of composition name uniqueness to mitigate the risk.
140+
141+
---
113142

114-
**N.B.:** From this version onward, the `metadata.name` field of the composition cannot exceed **44 characters**. This is because the UID suffix adds **9 characters** to the release name (the hyphen `-` plus the 8 characters of the UID), and Helm release names cannot exceed **53 characters** in total.
143+
### Comparison Summary (v0.20.3+)
115144

116-
This change was implemented to avoid conflicts when multiple resources belonging to the `composition.krateo.io` group with the same `metadata.name` are created in **different namespaces**.
145+
| `SAFE_RELEASE_NAME` | Source | UID Suffix | Collision Risk | Max Name Length |
146+
| --- | --- | --- | --- | --- |
147+
| **`true` (Default)** | Name + UID | Included | Negligible | 44 Characters |
148+
| **`false`** | Annotation/Name | Excluded | **High** | 53 Characters |
117149

118150
---
119151

@@ -248,4 +280,5 @@ These enviroment varibles can be changed in the Deployment of the composition-dy
248280
| COMPOSITION_CONTROLLER_MAX_ERROR_RETRY_INTERVAL | The maximum interval between retries when an error occurs. This should be less than the half of the poll interval. | 60s |
249281
| COMPOSITION_CONTROLLER_MIN_ERROR_RETRY_INTERVAL | The minimum interval between retries when an error occurs. This should be less than max-error-retry-interval. | 1s |
250282
| COMPOSITION_CONTROLLER_MAX_ERROR_RETRIES | The maximum number of retries when an error occurs. Set to 0 to disable retries. | 5 |
251-
| COMPOSITION_CONTROLLER_METRICS_SERVER_PORT | The port where the metrics server will be listening. If not set, the metrics server is disabled. | |
283+
| COMPOSITION_CONTROLLER_METRICS_SERVER_PORT | The port where the metrics server will be listening. If not set, the metrics server is disabled. | |
284+
| COMPOSITION_CONTROLLER_SAFE_RELEASE_NAME | 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. | true |

internal/composition/composition.go

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,9 @@ const (
5050
reasonReconciliationGracefullyPaused event.Reason = "ReconciliationGracefullyPaused"
5151

5252
// Event reasons
53-
reasonCreated = "CompositionCreated"
54-
reasonDeleted = "CompositionDeleted"
55-
reasonReady = "CompositionReady"
56-
reasonNotReady = "CompositionNotReady"
57-
reasonUpdated = "CompositionUpdated"
58-
reasonInstalled = "CompositionInstalled"
53+
reasonCreated = "CompositionCreated"
54+
reasonDeleted = "CompositionDeleted"
55+
reasonUpdated = "CompositionUpdated"
5956

6057
// Environment variables
6158
helmMaxHistoryEnvvar = "HELM_MAX_HISTORY"
@@ -67,23 +64,29 @@ const (
6764

6865
var _ controller.ExternalClient = (*handler)(nil)
6966

70-
func NewHandler(cfg *rest.Config,
71-
pig archive.Getter,
72-
event event.APIRecorder,
73-
pluralizer pluralizer.PluralizerInterface,
74-
mapper apimeta.RESTMapper,
75-
chartInspectorUrl string,
76-
saName string,
77-
saNamespace string) controller.ExternalClient {
67+
type HandlerOptions struct {
68+
Kubeconfig *rest.Config
69+
PackageInfoGetter archive.Getter
70+
EventRecorder event.APIRecorder
71+
Pluralizer pluralizer.PluralizerInterface
72+
ChartInspectorUrl string
73+
SaName string
74+
SaNamespace string
75+
SafeReleaseName bool
76+
Mapper apimeta.RESTMapper
77+
}
7878

79+
func NewHandler(opts *HandlerOptions) controller.ExternalClient {
7980
return &handler{
80-
kubeconfig: cfg,
81-
pluralizer: pluralizer,
82-
packageInfoGetter: pig,
83-
eventRecorder: event,
84-
chartInspectorUrl: chartInspectorUrl,
85-
saName: saName,
86-
saNamespace: saNamespace,
81+
kubeconfig: opts.Kubeconfig,
82+
pluralizer: opts.Pluralizer,
83+
packageInfoGetter: opts.PackageInfoGetter,
84+
eventRecorder: opts.EventRecorder,
85+
chartInspectorUrl: opts.ChartInspectorUrl,
86+
saName: opts.SaName,
87+
saNamespace: opts.SaNamespace,
88+
safeReleaseName: opts.SafeReleaseName,
89+
mapper: opts.Mapper,
8790
}
8891
}
8992

@@ -98,6 +101,8 @@ type handler struct {
98101
chartInspectorUrl string
99102
saName string
100103
saNamespace string
104+
// 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.
105+
safeReleaseName bool
101106
}
102107

103108
func (h *handler) Observe(ctx context.Context, mg *unstructured.Unstructured) (controller.ExternalObservation, error) {
@@ -122,8 +127,7 @@ func (h *handler) Observe(ctx context.Context, mg *unstructured.Unstructured) (c
122127
DynamicClient: dyn,
123128
}
124129

125-
compositionMeta.SetReleaseName(mg, compositionMeta.CalculateReleaseName(mg))
126-
releaseName = compositionMeta.GetReleaseName(mg)
130+
compositionMeta.SetReleaseName(mg, compositionMeta.CalculateReleaseName(mg, h.safeReleaseName))
127131
if _, p := compositionMeta.GetGracefullyPausedTime(mg); p && compositionMeta.IsGracefullyPaused(mg) {
128132
log.Debug("Composition is gracefully paused, skipping observe.")
129133
h.eventRecorder.Event(mg, event.Normal(reasonReconciliationGracefullyPaused, "Observe", "Reconciliation is paused via the gracefully paused annotation."))
@@ -369,7 +373,7 @@ func (h *handler) Create(ctx context.Context, mg *unstructured.Unstructured) err
369373
return nil
370374
}
371375

372-
compositionMeta.SetReleaseName(mg, compositionMeta.CalculateReleaseName(mg))
376+
compositionMeta.SetReleaseName(mg, compositionMeta.CalculateReleaseName(mg, h.safeReleaseName))
373377
releaseName := compositionMeta.GetReleaseName(mg)
374378
mg, err = tools.Update(ctx, mg, updateOpts)
375379
if err != nil {

internal/composition/composition_test.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -195,10 +195,18 @@ func TestController(t *testing.T) {
195195
t.Error("Creating REST mapper.", "error", err)
196196
return ctx
197197
}
198-
handler = NewHandler(cfg.Client().RESTConfig(), pig, *event.NewAPIRecorder(rec), pluralizer, mapper, chartInspectorMockURL, "test-sa", altNamespace)
199-
200-
// handler = NewHandler(cfg.Client().RESTConfig(), log, pig, *event.NewAPIRecorder(rec), pluralizer, chartInspectorUrl, "test-sa", altNamespace)
201198

199+
handler = NewHandler(&HandlerOptions{
200+
Kubeconfig: cfg.Client().RESTConfig(),
201+
PackageInfoGetter: pig,
202+
EventRecorder: *event.NewAPIRecorder(rec),
203+
Pluralizer: pluralizer,
204+
ChartInspectorUrl: chartInspectorMockURL,
205+
SaName: "test-sa",
206+
SaNamespace: altNamespace,
207+
SafeReleaseName: true,
208+
Mapper: mapper,
209+
})
202210
resli, err := decoder.DecodeAllFiles(ctx, os.DirFS(filepath.Join(testdataPath, "compositiondefinitions")), "*.yaml")
203211
if err != nil {
204212
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
}

0 commit comments

Comments
 (0)