Skip to content

Commit 7520967

Browse files
feat(operator): expose HelmRelease generation knobs (interval, retry-interval, timeouts, max-history) (#2509)
## Why The `cozystack-operator` Package reconciler builds every `HelmRelease` it generates with a handful of hardcoded values. Three of those have caused recurring pain across the project: - **`Spec.Interval = 5m`** (line 220 before this PR) — too long for E2E install where dependent HRs reconcile out of order; previously addressed by a manual `flux reconcile --force` workaround in `hack/e2e-install-cozystack.bats`. - **`Install.Remediation{Retries: -1}` + `Upgrade.Remediation{Retries: -1}`** — semantically "retry forever, never remediate", but the retry-on-failure timing was tied to `Spec.Interval` (5m). Audit of the last 30 successful PR runs found that **every** run hit `kubectl wait hr/seaweedfs-system` timeouts at the 2-min mark, recovering only after the inline `flux reconcile --force` workaround. - **`Install.Timeout = 10m` / `Upgrade.Timeout = 10m`** — directly contradicts the per-Application timeout work in `pkg/config/config.go` + `pkg/registry/apps/application/rest.go` (commit `7b146cbe`). `packages/apps/tenant/templates/etcd.yaml` hand-rolls its HR YAML to bypass this for cert rotation; harbor has had multiple commits chasing the same problem. - **`Spec.MaxHistory`** unset → Helm default 5. Test clusters bounce HRs frequently and accumulate release-history Secrets. This PR exposes all four as operator flags + helm chart values, with defaults that preserve current behavior. ## What ### New operator flags | Flag | Default | HR field | |---|---|---| | `--helmrelease-interval` | `5m` | `Spec.Interval` | | `--helmrelease-retry-interval` | `30s` | `Spec.{Install,Upgrade}.Strategy.RetryInterval` | | `--helmrelease-install-timeout` | `10m` | `Spec.Install.Timeout` | | `--helmrelease-upgrade-timeout` | `10m` | `Spec.Upgrade.Timeout` | | `--helmrelease-max-history` | `5` | `Spec.MaxHistory` | ### Strategy switch `Install.Strategy.Name` and `Upgrade.Strategy.Name` are now set to `RetryOnFailure` (instead of leaving the default `RemediateOnFailure`), with `RetryInterval` set from the new flag. Functionally equivalent to the previous `Remediation{Retries:-1}` ("retry forever") since remediation never fired anyway, but **decouples retry-on-failure timing from `Spec.Interval`** so failed releases recover at 30s without polling healthy releases at the same fast cadence. ### Chart exposure `packages/core/installer/values.yaml` gets five new optional values (`cozystackOperator.helmRelease{Interval,RetryInterval,InstallTimeout,UpgradeTimeout,MaxHistory}`), each defaulting to empty so the operator's own default applies. The chart conditionally renders the corresponding flag only when the value is set. ## Production safety - All static defaults match the previous hardcoded values (5m / 10m / 10m / 5). No new code path runs until an admin explicitly sets a flag. - **One intentional behavior change: failed-release retry cadence** drops from ~5m (previously coupled to `Spec.Interval`) to 30s (`RetryInterval` default). This is the whole motivation for the strategy switch — the recurring `kubectl wait hr/seaweedfs-system` flakes documented above were caused by the previous coupling. Healthy-release reconcile cadence is unchanged at 5m in production; CI overrides this to 30s via `--set cozystackOperator.helmReleaseInterval=30s` in `hack/e2e-install-cozystack.bats` for faster convergence on E2E clusters. - `RetryOnFailure` is functionally equivalent to `RemediateOnFailure + Retries:-1` since neither ever triggers remediation. - Empty value in the chart → flag not rendered → operator falls back to its built-in default. (`helmReleaseMaxHistory: 0` — unlimited history per Helm semantics — is also honored; the chart guards on `ne (toString …) ""` rather than truthiness so int `0` is not silently dropped.) ## Verification - `go build ./...` clean - `go test ./internal/operator/...` passes (covers `Interval`, `MaxHistory`, `Install/Upgrade.Timeout`, `Strategy.Name`, `RetryInterval`, `ChartRef` propagation, plus a regression test for `MaxHistory=0`) - `helm template installer packages/core/installer` renders cleanly with default and overridden values; `--set cozystackOperator.helmReleaseMaxHistory=0` correctly emits `--helmrelease-max-history=0` ## Followups (deliberately not bundled) - **Per-component timeout overrides** on `ComponentInstall` (mirroring the `UpgradeCRDs` pattern from #2427). Charts like etcd that need a 30m timeout would set it on their `Component` rather than tuning a global flag. Touches the Package CR API + codegen — bigger lift, separate PR. - **`Remediation.Retries` as a flag.** No current pain; skip. - **`createOrUpdateHelmRelease` field preservation.** Currently preserves `Suspend` but not `Test`/`Rollback`/`Uninstall`. Would silently overwrite an SRE's manual edit. Minor cleanliness, separate PR. ## Test plan - [ ] CI passes E2E with `helmReleaseInterval=30s` set in install bats (existing T1 test path) - [ ] No regression on production install path (default values) - [ ] HR objects in a healthy cluster render with `Strategy.Name=RetryOnFailure` and `RetryInterval=30s` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Operator and Helm chart now accept CLI flags and Helm values to configure HelmRelease reconciliation intervals, retry behavior, install/upgrade timeouts, and history retention. * **Bug Fixes** * Operator now validates HelmRelease timing/history inputs on startup and will exit if values are invalid or non-positive. * **Tests** * Added unit and e2e coverage to verify HelmRelease configuration and a chart install path using a custom reconciliation interval. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2 parents 086e9bb + 7d292eb commit 7520967

7 files changed

Lines changed: 273 additions & 34 deletions

File tree

‎cmd/cozystack-operator/main.go‎

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,11 @@ func main() {
8383
var disableTelemetry bool
8484
var telemetryEndpoint string
8585
var telemetryInterval string
86+
var helmReleaseInterval string
87+
var helmReleaseRetryInterval string
88+
var helmReleaseInstallTimeout string
89+
var helmReleaseUpgradeTimeout string
90+
var helmReleaseMaxHistory int
8691
var cozyValuesSecretName string
8792
var cozyValuesSecretNamespace string
8893
var cozyValuesNamespaceSelector string
@@ -107,6 +112,28 @@ func main() {
107112
"Endpoint for sending telemetry data")
108113
flag.StringVar(&telemetryInterval, "telemetry-interval", "15m",
109114
"Interval between telemetry data collection (e.g. 15m, 1h)")
115+
flag.StringVar(&helmReleaseInterval, "helmrelease-interval", "5m",
116+
"Reconcile interval applied to HelmReleases created by the Package reconciler. "+
117+
"Lower values speed up dependency-blocked retries (e.g. during E2E install) at the cost of "+
118+
"controller load. Production default 5m matches existing behaviour.")
119+
flag.StringVar(&helmReleaseRetryInterval, "helmrelease-retry-interval", "30s",
120+
"Retry interval applied to Install.Strategy and Upgrade.Strategy of HelmReleases created "+
121+
"by the Package reconciler. With Strategy.Name=RetryOnFailure, this controls how long the "+
122+
"controller waits between failed install/upgrade attempts. Decoupled from --helmrelease-interval "+
123+
"(which is the healthy reconcile cadence) so failures recover fast without polling healthy "+
124+
"releases at the same fast cadence.")
125+
flag.StringVar(&helmReleaseInstallTimeout, "helmrelease-install-timeout", "10m",
126+
"Timeout for the Helm install action of HelmReleases created by the Package reconciler "+
127+
"(Spec.Install.Timeout). Bounds how long an individual Kubernetes operation (Job/hook/wait) "+
128+
"may take during install.")
129+
flag.StringVar(&helmReleaseUpgradeTimeout, "helmrelease-upgrade-timeout", "10m",
130+
"Timeout for the Helm upgrade action of HelmReleases created by the Package reconciler "+
131+
"(Spec.Upgrade.Timeout). Bounds how long an individual Kubernetes operation (Job/hook/wait) "+
132+
"may take during upgrade.")
133+
flag.IntVar(&helmReleaseMaxHistory, "helmrelease-max-history", 5,
134+
"Number of release revisions Helm keeps for HelmReleases created by the Package reconciler "+
135+
"(Spec.MaxHistory). 0 means unlimited; 5 matches Helm's default. Lower values reduce "+
136+
"per-release Secret accumulation in clusters that bounce HRs frequently (e.g. E2E sandboxes).")
110137
flag.StringVar(&platformSourceURL, "platform-source-url", "", "Platform source URL (oci:// or https://). If specified, generates OCIRepository or GitRepository resource.")
111138
flag.StringVar(&platformSourceName, "platform-source-name", "cozystack-platform", "Name for the generated platform source resource and PackageSource")
112139
flag.StringVar(&platformSourceRef, "platform-source-ref", "", "Reference specification as key=value pairs (e.g., 'branch=main' or 'digest=sha256:...,tag=v1.0'). For OCI: digest, semver, semverFilter, tag. For Git: branch, tag, semver, name, commit.")
@@ -122,6 +149,23 @@ func main() {
122149

123150
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
124151

152+
parseFlag := func(flagName, raw string) time.Duration {
153+
d, err := parsePositiveDuration(flagName, raw)
154+
if err != nil {
155+
setupLog.Error(err, "invalid duration flag")
156+
os.Exit(1)
157+
}
158+
return d
159+
}
160+
hrIntervalDuration := parseFlag("--helmrelease-interval", helmReleaseInterval)
161+
hrRetryIntervalDuration := parseFlag("--helmrelease-retry-interval", helmReleaseRetryInterval)
162+
hrInstallTimeoutDuration := parseFlag("--helmrelease-install-timeout", helmReleaseInstallTimeout)
163+
hrUpgradeTimeoutDuration := parseFlag("--helmrelease-upgrade-timeout", helmReleaseUpgradeTimeout)
164+
if helmReleaseMaxHistory < 0 {
165+
setupLog.Error(fmt.Errorf("--helmrelease-max-history must be >= 0"), "invalid value", "value", helmReleaseMaxHistory)
166+
os.Exit(1)
167+
}
168+
125169
config := ctrl.GetConfigOrDie()
126170

127171
// Create a direct client (without cache) for pre-start operations
@@ -258,8 +302,13 @@ func main() {
258302

259303
// Setup Package reconciler
260304
if err := (&operator.PackageReconciler{
261-
Client: mgr.GetClient(),
262-
Scheme: mgr.GetScheme(),
305+
Client: mgr.GetClient(),
306+
Scheme: mgr.GetScheme(),
307+
HelmReleaseInterval: hrIntervalDuration,
308+
HelmReleaseRetryInterval: hrRetryIntervalDuration,
309+
HelmReleaseInstallTimeout: hrInstallTimeoutDuration,
310+
HelmReleaseUpgradeTimeout: hrUpgradeTimeoutDuration,
311+
HelmReleaseMaxHistory: helmReleaseMaxHistory,
263312
}).SetupWithManager(mgr); err != nil {
264313
setupLog.Error(err, "unable to create controller", "controller", "Package")
265314
os.Exit(1)
@@ -390,6 +439,21 @@ func installPlatformSourceResource(ctx context.Context, k8sClient client.Client,
390439
return nil
391440
}
392441

442+
// parsePositiveDuration parses raw as a time.Duration and rejects malformed
443+
// or non-positive values. Flux HelmRelease fields (Interval, Timeout,
444+
// RetryInterval) require strictly positive durations, so a misconfigured
445+
// flag must fail fast at startup rather than propagating into every HR.
446+
func parsePositiveDuration(flagName, raw string) (time.Duration, error) {
447+
d, err := time.ParseDuration(raw)
448+
if err != nil {
449+
return 0, fmt.Errorf("invalid duration for %s=%q: %w", flagName, raw, err)
450+
}
451+
if d <= 0 {
452+
return 0, fmt.Errorf("%s must be > 0 (got %q)", flagName, raw)
453+
}
454+
return d, nil
455+
}
456+
393457
// parsePlatformSourceURL parses the source URL and returns the source type and repository URL.
394458
// Supports formats:
395459
// - oci://registry.example.com/repo

‎cmd/cozystack-operator/main_test.go‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package main
1919
import (
2020
"context"
2121
"testing"
22+
"time"
2223

2324
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
2425
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -572,3 +573,38 @@ func TestGenerateGitRepository_InvalidRef(t *testing.T) {
572573
t.Fatal("expected error for invalid Git ref key, got nil")
573574
}
574575
}
576+
577+
func TestParsePositiveDuration(t *testing.T) {
578+
tests := []struct {
579+
name string
580+
raw string
581+
want time.Duration
582+
wantErr bool
583+
}{
584+
{name: "valid seconds", raw: "30s", want: 30 * time.Second},
585+
{name: "valid minutes", raw: "5m", want: 5 * time.Minute},
586+
{name: "valid compound", raw: "1h30m", want: 90 * time.Minute},
587+
{name: "zero rejected", raw: "0s", wantErr: true},
588+
{name: "negative rejected", raw: "-5m", wantErr: true},
589+
{name: "malformed rejected", raw: "5x", wantErr: true},
590+
{name: "empty rejected", raw: "", wantErr: true},
591+
}
592+
593+
for _, tt := range tests {
594+
t.Run(tt.name, func(t *testing.T) {
595+
got, err := parsePositiveDuration("--test-flag", tt.raw)
596+
if tt.wantErr {
597+
if err == nil {
598+
t.Fatalf("expected error for %q, got nil", tt.raw)
599+
}
600+
return
601+
}
602+
if err != nil {
603+
t.Fatalf("unexpected error for %q: %v", tt.raw, err)
604+
}
605+
if got != tt.want {
606+
t.Errorf("got %v, want %v", got, tt.want)
607+
}
608+
})
609+
}
610+
}

‎hack/e2e-install-cozystack.bats‎

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
--install \
3838
--namespace cozy-system \
3939
--create-namespace \
40+
--set cozystackOperator.helmReleaseInterval=30s \
4041
--wait \
4142
--timeout 2m
4243

@@ -185,16 +186,7 @@ EOF
185186
timeout 60 sh -ec 'until kubectl get hr -n tenant-root etcd ingress monitoring seaweedfs tenant-root >/dev/null 2>&1; do sleep 1; done'
186187
kubectl wait hr/etcd hr/ingress hr/tenant-root hr/seaweedfs -n tenant-root --timeout=4m --for=condition=ready
187188

188-
# TODO: Workaround ingress unvailability issue
189-
if ! kubectl wait hr/monitoring -n tenant-root --timeout=2m --for=condition=ready; then
190-
flux reconcile hr monitoring -n tenant-root --force
191-
kubectl wait hr/monitoring -n tenant-root --timeout=2m --for=condition=ready
192-
fi
193-
194-
if ! kubectl wait hr/seaweedfs-system -n tenant-root --timeout=2m --for=condition=ready; then
195-
flux reconcile hr seaweedfs-system -n tenant-root --force
196-
kubectl wait hr/seaweedfs-system -n tenant-root --timeout=2m --for=condition=ready
197-
fi
189+
kubectl wait hr/monitoring hr/seaweedfs-system -n tenant-root --timeout=2m --for=condition=ready
198190

199191

200192
# Expose Cozystack services through ingress

‎internal/operator/package_reconciler.go‎

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"context"
2121
"fmt"
2222
"strings"
23+
"time"
2324

2425
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
2526
helmv2 "github.com/fluxcd/helm-controller/api/v2"
@@ -58,7 +59,43 @@ func parseCRDPolicy(install *cozyv1alpha1.ComponentInstall) helmv2.CRDsPolicy {
5859
// PackageReconciler reconciles Package resources
5960
type PackageReconciler struct {
6061
client.Client
61-
Scheme *runtime.Scheme
62+
Scheme *runtime.Scheme
63+
HelmReleaseInterval time.Duration
64+
HelmReleaseRetryInterval time.Duration
65+
HelmReleaseInstallTimeout time.Duration
66+
HelmReleaseUpgradeTimeout time.Duration
67+
HelmReleaseMaxHistory int
68+
}
69+
70+
// buildHelmReleaseSpec assembles the Spec applied to every generated
71+
// HelmRelease. RetryInterval drives recovery from failed install/upgrade
72+
// attempts; Interval polls healthy releases.
73+
func (r *PackageReconciler) buildHelmReleaseSpec(componentInstall *cozyv1alpha1.ComponentInstall, artifactName string) helmv2.HelmReleaseSpec {
74+
maxHistory := r.HelmReleaseMaxHistory
75+
return helmv2.HelmReleaseSpec{
76+
Interval: metav1.Duration{Duration: r.HelmReleaseInterval},
77+
MaxHistory: &maxHistory,
78+
ChartRef: &helmv2.CrossNamespaceSourceReference{
79+
Kind: "ExternalArtifact",
80+
Name: artifactName,
81+
Namespace: "cozy-system",
82+
},
83+
Install: &helmv2.Install{
84+
Timeout: &metav1.Duration{Duration: r.HelmReleaseInstallTimeout},
85+
Strategy: &helmv2.InstallStrategy{
86+
Name: string(helmv2.ActionStrategyRetryOnFailure),
87+
RetryInterval: &metav1.Duration{Duration: r.HelmReleaseRetryInterval},
88+
},
89+
},
90+
Upgrade: &helmv2.Upgrade{
91+
Timeout: &metav1.Duration{Duration: r.HelmReleaseUpgradeTimeout},
92+
Strategy: &helmv2.UpgradeStrategy{
93+
Name: string(helmv2.ActionStrategyRetryOnFailure),
94+
RetryInterval: &metav1.Duration{Duration: r.HelmReleaseRetryInterval},
95+
},
96+
CRDs: parseCRDPolicy(componentInstall),
97+
},
98+
}
6299
}
63100

64101
// +kubebuilder:rbac:groups=cozystack.io,resources=packages,verbs=get;list;watch;create;update;patch;delete
@@ -213,27 +250,7 @@ func (r *PackageReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
213250
Namespace: namespace,
214251
Labels: labels,
215252
},
216-
Spec: helmv2.HelmReleaseSpec{
217-
Interval: metav1.Duration{Duration: 5 * 60 * 1000000000}, // 5m
218-
ChartRef: &helmv2.CrossNamespaceSourceReference{
219-
Kind: "ExternalArtifact",
220-
Name: artifactName,
221-
Namespace: "cozy-system",
222-
},
223-
Install: &helmv2.Install{
224-
Timeout: &metav1.Duration{Duration: 10 * 60 * 1000000000}, // 10m
225-
Remediation: &helmv2.InstallRemediation{
226-
Retries: -1,
227-
},
228-
},
229-
Upgrade: &helmv2.Upgrade{
230-
Timeout: &metav1.Duration{Duration: 10 * 60 * 1000000000}, // 10m
231-
Remediation: &helmv2.UpgradeRemediation{
232-
Retries: -1,
233-
},
234-
CRDs: parseCRDPolicy(component.Install),
235-
},
236-
},
253+
Spec: r.buildHelmReleaseSpec(component.Install, artifactName),
237254
}
238255

239256
// Add valuesFrom for cozystack-values secret unless disabled by annotation on PackageSource

‎internal/operator/package_reconciler_test.go‎

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"os"
2222
"path/filepath"
2323
"testing"
24+
"time"
2425

2526
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
2627
helmv2 "github.com/fluxcd/helm-controller/api/v2"
@@ -71,6 +72,101 @@ func TestParseCRDPolicy(t *testing.T) {
7172
}
7273
}
7374

75+
func TestBuildHelmReleaseSpec(t *testing.T) {
76+
r := &PackageReconciler{
77+
HelmReleaseInterval: 42 * time.Second,
78+
HelmReleaseRetryInterval: 17 * time.Second,
79+
HelmReleaseInstallTimeout: 11 * time.Minute,
80+
HelmReleaseUpgradeTimeout: 13 * time.Minute,
81+
HelmReleaseMaxHistory: 7,
82+
}
83+
componentInstall := &cozyv1alpha1.ComponentInstall{UpgradeCRDs: "Skip"}
84+
85+
spec := r.buildHelmReleaseSpec(componentInstall, "ps-variant-component")
86+
87+
if spec.Interval.Duration != 42*time.Second {
88+
t.Errorf("Interval = %v, want 42s", spec.Interval.Duration)
89+
}
90+
if spec.MaxHistory == nil {
91+
t.Fatal("MaxHistory is nil, want pointer to 7")
92+
}
93+
if *spec.MaxHistory != 7 {
94+
t.Errorf("MaxHistory = %d, want 7", *spec.MaxHistory)
95+
}
96+
97+
if spec.ChartRef == nil {
98+
t.Fatal("ChartRef is nil")
99+
}
100+
if spec.ChartRef.Kind != "ExternalArtifact" {
101+
t.Errorf("ChartRef.Kind = %q, want ExternalArtifact", spec.ChartRef.Kind)
102+
}
103+
if spec.ChartRef.Name != "ps-variant-component" {
104+
t.Errorf("ChartRef.Name = %q, want ps-variant-component", spec.ChartRef.Name)
105+
}
106+
if spec.ChartRef.Namespace != "cozy-system" {
107+
t.Errorf("ChartRef.Namespace = %q, want cozy-system", spec.ChartRef.Namespace)
108+
}
109+
110+
if spec.Install == nil {
111+
t.Fatal("Install is nil")
112+
}
113+
if spec.Install.Timeout == nil || spec.Install.Timeout.Duration != 11*time.Minute {
114+
t.Errorf("Install.Timeout = %v, want 11m", spec.Install.Timeout)
115+
}
116+
if spec.Install.Strategy == nil {
117+
t.Fatal("Install.Strategy is nil")
118+
}
119+
if spec.Install.Strategy.Name != string(helmv2.ActionStrategyRetryOnFailure) {
120+
t.Errorf("Install.Strategy.Name = %q, want %q", spec.Install.Strategy.Name, helmv2.ActionStrategyRetryOnFailure)
121+
}
122+
if spec.Install.Strategy.RetryInterval == nil || spec.Install.Strategy.RetryInterval.Duration != 17*time.Second {
123+
t.Errorf("Install.Strategy.RetryInterval = %v, want 17s", spec.Install.Strategy.RetryInterval)
124+
}
125+
// Remediation must remain nil: helm-controller's XValidation rule rejects
126+
// Strategy.Name=RetryOnFailure with RetryInterval set alongside a
127+
// Remediation entry, so a future "for safety" re-introduction of
128+
// Remediation{Retries: -1} would silently break every HR.
129+
if spec.Install.Remediation != nil {
130+
t.Errorf("Install.Remediation = %+v, want nil", spec.Install.Remediation)
131+
}
132+
133+
if spec.Upgrade == nil {
134+
t.Fatal("Upgrade is nil")
135+
}
136+
if spec.Upgrade.Timeout == nil || spec.Upgrade.Timeout.Duration != 13*time.Minute {
137+
t.Errorf("Upgrade.Timeout = %v, want 13m", spec.Upgrade.Timeout)
138+
}
139+
if spec.Upgrade.Strategy == nil {
140+
t.Fatal("Upgrade.Strategy is nil")
141+
}
142+
if spec.Upgrade.Strategy.Name != string(helmv2.ActionStrategyRetryOnFailure) {
143+
t.Errorf("Upgrade.Strategy.Name = %q, want %q", spec.Upgrade.Strategy.Name, helmv2.ActionStrategyRetryOnFailure)
144+
}
145+
if spec.Upgrade.Strategy.RetryInterval == nil || spec.Upgrade.Strategy.RetryInterval.Duration != 17*time.Second {
146+
t.Errorf("Upgrade.Strategy.RetryInterval = %v, want 17s", spec.Upgrade.Strategy.RetryInterval)
147+
}
148+
if spec.Upgrade.Remediation != nil {
149+
t.Errorf("Upgrade.Remediation = %+v, want nil", spec.Upgrade.Remediation)
150+
}
151+
if spec.Upgrade.CRDs != helmv2.Skip {
152+
t.Errorf("Upgrade.CRDs = %q, want Skip", spec.Upgrade.CRDs)
153+
}
154+
}
155+
156+
// TestBuildHelmReleaseSpecZeroMaxHistory pins that MaxHistory=0 (unlimited
157+
// history per Helm semantics) survives the spec build — i.e. is set as a
158+
// non-nil pointer to 0 rather than dropped or replaced with a default.
159+
func TestBuildHelmReleaseSpecZeroMaxHistory(t *testing.T) {
160+
r := &PackageReconciler{HelmReleaseMaxHistory: 0}
161+
spec := r.buildHelmReleaseSpec(nil, "x")
162+
if spec.MaxHistory == nil {
163+
t.Fatal("MaxHistory is nil for HelmReleaseMaxHistory=0; want pointer to 0")
164+
}
165+
if *spec.MaxHistory != 0 {
166+
t.Errorf("MaxHistory = %d, want 0", *spec.MaxHistory)
167+
}
168+
}
169+
74170
// TestPackageSourceCRDHasUpgradeCRDsEnum guards the generated CRD schema: the
75171
// invalid-value case from the spec is enforced at the API server via a
76172
// kubebuilder enum marker, not in the reconciler. If someone drops the marker

‎packages/core/installer/templates/cozystack-operator.yaml‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,21 @@ spec:
5858
{{- if .Values.cozystackOperator.disableTelemetry }}
5959
- --disable-telemetry
6060
{{- end }}
61+
{{- if ne (toString .Values.cozystackOperator.helmReleaseInterval) "" }}
62+
- --helmrelease-interval={{ .Values.cozystackOperator.helmReleaseInterval }}
63+
{{- end }}
64+
{{- if ne (toString .Values.cozystackOperator.helmReleaseRetryInterval) "" }}
65+
- --helmrelease-retry-interval={{ .Values.cozystackOperator.helmReleaseRetryInterval }}
66+
{{- end }}
67+
{{- if ne (toString .Values.cozystackOperator.helmReleaseInstallTimeout) "" }}
68+
- --helmrelease-install-timeout={{ .Values.cozystackOperator.helmReleaseInstallTimeout }}
69+
{{- end }}
70+
{{- if ne (toString .Values.cozystackOperator.helmReleaseUpgradeTimeout) "" }}
71+
- --helmrelease-upgrade-timeout={{ .Values.cozystackOperator.helmReleaseUpgradeTimeout }}
72+
{{- end }}
73+
{{- if ne (toString .Values.cozystackOperator.helmReleaseMaxHistory) "" }}
74+
- --helmrelease-max-history={{ .Values.cozystackOperator.helmReleaseMaxHistory }}
75+
{{- end }}
6176
- --platform-source-name=cozystack-platform
6277
- --platform-source-url={{ .Values.cozystackOperator.platformSourceUrl }}
6378
{{- if .Values.cozystackOperator.platformSourceRef }}

0 commit comments

Comments
 (0)