From 78ad14ebdadd6b8ae1bc1a333fa378b940515b9d Mon Sep 17 00:00:00 2001 From: Filipe Galo Date: Tue, 1 Sep 2026 01:03:40 +0100 Subject: [PATCH 1/2] fix: bootstrap Prometheus CRDs for Cilium monitors --- README.md | 1 + cluster-bootstrap-cli/cmd/bootstrap.go | 13 +- cluster-bootstrap-cli/cmd/bootstrap_test.go | 6 + cluster-bootstrap-cli/cmd/root.go | 6 +- cluster-bootstrap-cli/internal/helm/argocd.go | 192 +++++++++++++++++- .../internal/helm/component_test.go | 149 ++++++++++++++ docs/cli/index.md | 1 + docs/components/cilium.md | 2 + 8 files changed, 360 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index fdbc114..2c523f9 100644 --- a/README.md +++ b/README.md @@ -234,6 +234,7 @@ The `apps/` chart uses a **single dynamic template** that iterates over a `compo |------|---------|-------------| | `--base-dir` | `.` | Base directory for repo content (local file resolution) | | `-v, --verbose` | `false` | Enable verbose output | +| `--verbose-with-templates` | `false` | Write rendered Helm manifests to a file during bootstrap (implies verbose; output may contain sensitive values) | ## Development diff --git a/cluster-bootstrap-cli/cmd/bootstrap.go b/cluster-bootstrap-cli/cmd/bootstrap.go index e363f6a..2fc59d6 100644 --- a/cluster-bootstrap-cli/cmd/bootstrap.go +++ b/cluster-bootstrap-cli/cmd/bootstrap.go @@ -89,6 +89,15 @@ func runBootstrap(cmd *cobra.Command, args []string) error { } logger := NewLogger(verbose) + templateOutputPath := "" + if verboseWithTemplates { + outputDir, err := filepath.Abs(baseDir) + if err != nil { + return fmt.Errorf("failed to resolve template output directory: %w", err) + } + templateOutputPath = filepath.Join(outputDir, fmt.Sprintf("bootstrap-rendered-helm-manifests-%s-%d.yaml", env, time.Now().UnixNano())) + fmt.Printf(" Rendered Helm manifests will be written to: %s\n", templateOutputPath) + } // Detect if we're running from a subdirectory and adjust paths accordingly var argoCDAppPath string @@ -440,7 +449,7 @@ func runBootstrap(cmd *cobra.Command, args []string) error { helmTimer := startStage("Installing Cilium") helmStage := logger.Stage("Installing Cilium via Helm") stepf("Installing Cilium via Helm and waiting for it to become healthy...") - installed, installErr := helm.InstallCilium(ctx, kubeconfig, kubeContext, env, baseDir, verbose) + installed, installErr := helm.InstallCilium(ctx, kubeconfig, kubeContext, env, baseDir, verbose, verboseWithTemplates, templateOutputPath) if installErr != nil { report.AddStage(helmTimer.complete(false, installErr)) return installErr @@ -463,7 +472,7 @@ func runBootstrap(cmd *cobra.Command, args []string) error { helmTimer := startStage("Installing ArgoCD") helmStage := logger.Stage("Installing ArgoCD via Helm") stepf("Installing ArgoCD via Helm...") - installed, installErr := helm.InstallArgoCD(ctx, kubeconfig, kubeContext, env, baseDir, verbose) + installed, installErr := helm.InstallArgoCD(ctx, kubeconfig, kubeContext, env, baseDir, verbose, verboseWithTemplates, templateOutputPath) if installErr != nil { report.AddStage(helmTimer.complete(false, installErr)) return installErr diff --git a/cluster-bootstrap-cli/cmd/bootstrap_test.go b/cluster-bootstrap-cli/cmd/bootstrap_test.go index 5897b29..f6a260d 100644 --- a/cluster-bootstrap-cli/cmd/bootstrap_test.go +++ b/cluster-bootstrap-cli/cmd/bootstrap_test.go @@ -20,6 +20,12 @@ func TestCiliumFlagIsOptIn(t *testing.T) { assert.Equal(t, "false", flag.DefValue) } +func TestVerboseWithTemplatesFlagIsOptIn(t *testing.T) { + flag := rootCmd.PersistentFlags().Lookup("verbose-with-templates") + require.NotNil(t, flag) + assert.Equal(t, "false", flag.DefValue) +} + func TestStoreSopsAgeKeyFlagIsOptIn(t *testing.T) { flag := bootstrapCmd.Flags().Lookup("store-sops-age-key") require.NotNil(t, flag) diff --git a/cluster-bootstrap-cli/cmd/root.go b/cluster-bootstrap-cli/cmd/root.go index e832359..dda7036 100644 --- a/cluster-bootstrap-cli/cmd/root.go +++ b/cluster-bootstrap-cli/cmd/root.go @@ -9,8 +9,9 @@ import ( ) var ( - verbose bool - baseDir string + verbose bool + verboseWithTemplates bool + baseDir string ) var ( @@ -55,5 +56,6 @@ func errorf(format string, args ...interface{}) { func init() { rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "enable verbose output") + rootCmd.PersistentFlags().BoolVar(&verboseWithTemplates, "verbose-with-templates", false, "write rendered Helm manifests to a file during bootstrap (implies --verbose; may expose sensitive values)") rootCmd.PersistentFlags().StringVar(&baseDir, "base-dir", ".", "base directory for repo content") } diff --git a/cluster-bootstrap-cli/internal/helm/argocd.go b/cluster-bootstrap-cli/internal/helm/argocd.go index 7e00615..ce157e9 100644 --- a/cluster-bootstrap-cli/internal/helm/argocd.go +++ b/cluster-bootstrap-cli/internal/helm/argocd.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "path/filepath" + "sort" "strings" "time" @@ -15,9 +16,11 @@ import ( "helm.sh/helm/v3/pkg/chart/loader" "helm.sh/helm/v3/pkg/chartutil" "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/engine" "helm.sh/helm/v3/pkg/getter" "helm.sh/helm/v3/pkg/repo" "helm.sh/helm/v3/pkg/storage/driver" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/client-go/discovery" "k8s.io/client-go/discovery/cached/memory" @@ -43,16 +46,21 @@ type componentConfig struct { dependencyName string installWrapper bool envOverridesBase bool + hasValues bool wait bool timeout time.Duration } var ( - argoCDComponent = componentConfig{ + installComponentFn = installComponent + serviceMonitorCRDAvailableFn = serviceMonitorCRDAvailable + waitForServiceMonitorCRDFn = waitForServiceMonitorCRD + argoCDComponent = componentConfig{ name: "argocd", releaseName: argoCDRelease, namespace: argoCDNamespace, dependencyName: argoCDChartDep, + hasValues: true, wait: true, timeout: componentTimeout, } @@ -63,9 +71,18 @@ var ( dependencyName: ciliumChartDep, installWrapper: true, envOverridesBase: true, + hasValues: true, wait: true, timeout: componentTimeout, } + prometheusOperatorCRDsComponent = componentConfig{ + name: "prometheus-operator-crds", + releaseName: "prometheus-operator-crds", + namespace: "monitoring", + dependencyName: "prometheus-operator-crds", + wait: true, + timeout: componentTimeout, + } ) // chartDependency represents a single entry in Chart.yaml dependencies. @@ -114,17 +131,39 @@ func loadChartConfig(baseDir, componentName, dependencyName string) (name, versi // then runs helm upgrade --install with --wait. // Returns helpful error messages for common failure scenarios. // Returns a boolean indicating if it was installed (true) or upgraded (false). -func InstallArgoCD(ctx context.Context, kubeconfig, kubeContext, env, baseDir string, verbose bool) (bool, error) { - return installComponent(ctx, kubeconfig, kubeContext, env, baseDir, verbose, argoCDComponent) +func InstallArgoCD(ctx context.Context, kubeconfig, kubeContext, env, baseDir string, verbose, verboseWithTemplates bool, templateOutputPath string) (bool, error) { + return installComponent(ctx, kubeconfig, kubeContext, env, baseDir, verbose, verboseWithTemplates, templateOutputPath, argoCDComponent) } // InstallCilium installs or upgrades Cilium using the same Helm bootstrap path as ArgoCD. // Helm wait is always enabled, so a successful return is the health barrier before ArgoCD. -func InstallCilium(ctx context.Context, kubeconfig, kubeContext, env, baseDir string, verbose bool) (bool, error) { - return installComponent(ctx, kubeconfig, kubeContext, env, baseDir, verbose, ciliumComponent) +func InstallCilium(ctx context.Context, kubeconfig, kubeContext, env, baseDir string, verbose, verboseWithTemplates bool, templateOutputPath string) (bool, error) { + serviceMonitorsEnabled, err := ciliumServiceMonitorsEnabled(baseDir, env) + if err != nil { + return false, fmt.Errorf("failed to inspect Cilium monitoring configuration: %w", err) + } + if serviceMonitorsEnabled { + available, err := serviceMonitorCRDAvailableFn(kubeconfig, kubeContext) + if err != nil { + return false, fmt.Errorf("failed to check for Prometheus Operator ServiceMonitor CRD: %w", err) + } + if !available { + fmt.Println(" Cilium ServiceMonitors are enabled; installing Prometheus Operator CRDs before Cilium...") + if _, err := installComponentFn(ctx, kubeconfig, kubeContext, env, baseDir, verbose, verboseWithTemplates, templateOutputPath, prometheusOperatorCRDsComponent); err != nil { + return false, fmt.Errorf("failed to install Prometheus Operator CRDs required by Cilium: %w", err) + } + if err := waitForServiceMonitorCRDFn(ctx, kubeconfig, kubeContext); err != nil { + return false, err + } + } else if verbose { + fmt.Println(" Prometheus Operator ServiceMonitor CRD is already installed") + } + } + return installComponentFn(ctx, kubeconfig, kubeContext, env, baseDir, verbose, verboseWithTemplates, templateOutputPath, ciliumComponent) } -func installComponent(ctx context.Context, kubeconfig, kubeContext, env, baseDir string, verbose bool, component componentConfig) (bool, error) { +func installComponent(ctx context.Context, kubeconfig, kubeContext, env, baseDir string, verbose, verboseWithTemplates bool, templateOutputPath string, component componentConfig) (bool, error) { + verbose = verbose || verboseWithTemplates settings := cli.New() settings.SetNamespace(component.namespace) if kubeconfig != "" { @@ -169,6 +208,10 @@ func installComponent(ctx context.Context, kubeconfig, kubeContext, env, baseDir if verbose { fmt.Printf(" Chart: %s-%s\n", installChart.Metadata.Name, installChart.Metadata.Version) + if component.hasValues { + fmt.Printf(" Values: %s\n", filepath.Join(baseDir, "components", component.name, "values", "base.yaml")) + fmt.Printf(" Values override: %s\n", filepath.Join(baseDir, "components", component.name, "values", fmt.Sprintf("%s.yaml", env))) + } } // Check if release exists; if not, install; otherwise upgrade @@ -179,6 +222,15 @@ func installComponent(ctx context.Context, kubeconfig, kubeContext, env, baseDir if historyErr != nil { return false, fmt.Errorf("failed to check Helm release %s in namespace %s: %w", component.releaseName, component.namespace, historyErr) } + if verboseWithTemplates { + manifest, renderErr := renderComponentTemplates(installChart, vals, component, !releaseExists) + if renderErr != nil { + return false, fmt.Errorf("failed to render %s templates for verbose output: %w", component.name, renderErr) + } + if err := appendRenderedTemplates(templateOutputPath, component.name, manifest); err != nil { + return false, err + } + } if !releaseExists { install := action.NewInstall(actionConfig) @@ -231,6 +283,56 @@ func installComponent(ctx context.Context, kubeconfig, kubeContext, env, baseDir return false, nil } +func appendRenderedTemplates(path, componentName, manifest string) error { + if path == "" { + return fmt.Errorf("template output path is required when verbose templates are enabled") + } + file, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600) + if err != nil { + return fmt.Errorf("failed to open rendered template output %s: %w", path, err) + } + defer file.Close() + if _, err := fmt.Fprintf(file, "# BEGIN RENDERED %s HELM MANIFESTS\n%s# END RENDERED %s HELM MANIFESTS\n", strings.ToUpper(componentName), manifest, strings.ToUpper(componentName)); err != nil { + return fmt.Errorf("failed to write rendered templates to %s: %w", path, err) + } + return nil +} + +// renderComponentTemplates renders the same chart and merged values that will be +// passed to Helm. It intentionally does not contact or modify the cluster. +func renderComponentTemplates(componentChart *chart.Chart, vals map[string]interface{}, component componentConfig, isInstall bool) (string, error) { + release := chartutil.ReleaseOptions{ + Name: component.releaseName, + Namespace: component.namespace, + IsInstall: isInstall, + IsUpgrade: !isInstall, + } + renderValues, err := chartutil.ToRenderValues(componentChart, vals, release, chartutil.DefaultCapabilities) + if err != nil { + return "", fmt.Errorf("failed to prepare template values: %w", err) + } + rendered, err := engine.Render(componentChart, renderValues) + if err != nil { + return "", fmt.Errorf("failed to render chart: %w", err) + } + + files := make([]string, 0, len(rendered)) + for name := range rendered { + files = append(files, name) + } + sort.Strings(files) + + var manifest strings.Builder + for _, name := range files { + contents := strings.TrimSpace(rendered[name]) + if contents == "" || strings.HasSuffix(name, "NOTES.txt") { + continue + } + fmt.Fprintf(&manifest, "---\n# Source: %s\n%s\n", name, contents) + } + return manifest.String(), nil +} + func helmReleaseExists(historyErr error) (bool, error) { if historyErr == nil { return true, nil @@ -322,9 +424,87 @@ func loadComponentValues(baseDir, componentName, env string) (map[string]interfa } func loadValuesForComponent(baseDir, env string, component componentConfig) (map[string]interface{}, error) { + if !component.hasValues { + return map[string]interface{}{}, nil + } return loadComponentValuesWithOrder(baseDir, component.name, env, component.envOverridesBase) } +func ciliumServiceMonitorsEnabled(baseDir, env string) (bool, error) { + values, err := loadValuesForComponent(baseDir, env, ciliumComponent) + if err != nil { + return false, err + } + for _, path := range [][]string{ + {"cilium", "prometheus", "serviceMonitor", "enabled"}, + {"cilium", "operator", "prometheus", "serviceMonitor", "enabled"}, + {"cilium", "hubble", "metrics", "serviceMonitor", "enabled"}, + } { + if valueAtPathIsTrue(values, path) { + return true, nil + } + } + return false, nil +} + +func valueAtPathIsTrue(values map[string]interface{}, path []string) bool { + var current interface{} = values + for _, key := range path { + mapValue, ok := current.(map[string]interface{}) + if !ok { + return false + } + current = mapValue[key] + } + enabled, ok := current.(bool) + return ok && enabled +} + +func serviceMonitorCRDAvailable(kubeconfig, kubeContext string) (bool, error) { + discoveryClient, err := newRESTClientGetter(kubeconfig, kubeContext, "").ToDiscoveryClient() + if err != nil { + return false, err + } + resources, err := discoveryClient.ServerResourcesForGroupVersion("monitoring.coreos.com/v1") + if isServiceMonitorCRDNotFound(err) { + return false, nil + } + if err != nil { + return false, err + } + for _, resource := range resources.APIResources { + if resource.Kind == "ServiceMonitor" { + return true, nil + } + } + return false, nil +} + +func isServiceMonitorCRDNotFound(err error) bool { + return apierrors.IsNotFound(err) || errors.Is(err, memory.ErrCacheNotFound) +} + +func waitForServiceMonitorCRD(ctx context.Context, kubeconfig, kubeContext string) error { + deadline := time.NewTimer(componentTimeout) + defer deadline.Stop() + ticker := time.NewTicker(500 * time.Millisecond) + defer ticker.Stop() + + for { + available, err := serviceMonitorCRDAvailable(kubeconfig, kubeContext) + if err == nil && available { + return nil + } + select { + case <-ctx.Done(): + return fmt.Errorf("waiting for Prometheus Operator ServiceMonitor CRD canceled: %w", ctx.Err()) + case <-deadline.C: + return fmt.Errorf("timed out waiting for Prometheus Operator ServiceMonitor CRD after %s", componentTimeout) + case <-ticker.C: + } + } +} + func loadComponentValuesWithOrder(baseDir, componentName, env string, envOverridesBase bool) (map[string]interface{}, error) { baseFile := filepath.Join(baseDir, "components", componentName, "values", "base.yaml") envFile := filepath.Join(baseDir, "components", componentName, "values", fmt.Sprintf("%s.yaml", env)) diff --git a/cluster-bootstrap-cli/internal/helm/component_test.go b/cluster-bootstrap-cli/internal/helm/component_test.go index 8e81a6b..a340107 100644 --- a/cluster-bootstrap-cli/internal/helm/component_test.go +++ b/cluster-bootstrap-cli/internal/helm/component_test.go @@ -1,9 +1,11 @@ package helm import ( + "context" "errors" "os" "path/filepath" + "strings" "testing" "time" @@ -13,6 +15,7 @@ import ( "helm.sh/helm/v3/pkg/chartutil" "helm.sh/helm/v3/pkg/engine" "helm.sh/helm/v3/pkg/storage/driver" + "k8s.io/client-go/discovery/cached/memory" ) func TestLoadChartConfigForComponent(t *testing.T) { @@ -60,6 +63,123 @@ func TestLoadComponentValues(t *testing.T) { assert.Equal(t, false, hubble["enabled"]) } +func TestCiliumServiceMonitorsEnabled(t *testing.T) { + baseDir := t.TempDir() + valuesDir := filepath.Join(baseDir, "components", "cilium", "values") + require.NoError(t, os.MkdirAll(valuesDir, 0755)) + require.NoError(t, os.WriteFile(filepath.Join(valuesDir, "base.yaml"), []byte(`cilium: + prometheus: + serviceMonitor: + enabled: false + operator: + prometheus: + serviceMonitor: + enabled: false + hubble: + metrics: + serviceMonitor: + enabled: false +`), 0600)) + require.NoError(t, os.WriteFile(filepath.Join(valuesDir, "homelab.yaml"), []byte(`cilium: + hubble: + metrics: + serviceMonitor: + enabled: true +`), 0600)) + + enabled, err := ciliumServiceMonitorsEnabled(baseDir, "homelab") + require.NoError(t, err) + assert.True(t, enabled) + + disabled, err := ciliumServiceMonitorsEnabled(baseDir, "missing") + require.NoError(t, err) + assert.False(t, disabled) +} + +func TestPrometheusOperatorCRDsComponentDoesNotRequireValuesFiles(t *testing.T) { + assert.Equal(t, "prometheus-operator-crds", prometheusOperatorCRDsComponent.name) + assert.False(t, prometheusOperatorCRDsComponent.hasValues) + + values, err := loadValuesForComponent(t.TempDir(), "homelab", prometheusOperatorCRDsComponent) + require.NoError(t, err) + assert.Empty(t, values) +} + +func TestIsServiceMonitorCRDNotFound(t *testing.T) { + assert.True(t, isServiceMonitorCRDNotFound(memory.ErrCacheNotFound)) + assert.False(t, isServiceMonitorCRDNotFound(errors.New("permission denied"))) +} + +func TestInstallCiliumInstallsRequiredCRDsBeforeCilium(t *testing.T) { + baseDir := t.TempDir() + valuesDir := filepath.Join(baseDir, "components", "cilium", "values") + require.NoError(t, os.MkdirAll(valuesDir, 0755)) + require.NoError(t, os.WriteFile(filepath.Join(valuesDir, "base.yaml"), []byte(`cilium: + prometheus: + serviceMonitor: + enabled: true +`), 0600)) + + previousInstall := installComponentFn + previousAvailable := serviceMonitorCRDAvailableFn + previousWait := waitForServiceMonitorCRDFn + t.Cleanup(func() { + installComponentFn = previousInstall + serviceMonitorCRDAvailableFn = previousAvailable + waitForServiceMonitorCRDFn = previousWait + }) + + calls := []string{} + installComponentFn = func(_ context.Context, _, _, _, _ string, _, _ bool, _ string, component componentConfig) (bool, error) { + calls = append(calls, component.name) + return true, nil + } + serviceMonitorCRDAvailableFn = func(_, _ string) (bool, error) { return false, nil } + waitForServiceMonitorCRDFn = func(_ context.Context, _, _ string) error { + calls = append(calls, "wait-for-servicemonitor-crd") + return nil + } + + _, err := InstallCilium(context.Background(), "", "", "homelab", baseDir, false, false, "") + require.NoError(t, err) + assert.Equal(t, []string{"prometheus-operator-crds", "wait-for-servicemonitor-crd", "cilium"}, calls) +} + +func TestInstallCiliumSkipsCRDInstallWhenServiceMonitorCRDExists(t *testing.T) { + baseDir := t.TempDir() + valuesDir := filepath.Join(baseDir, "components", "cilium", "values") + require.NoError(t, os.MkdirAll(valuesDir, 0755)) + require.NoError(t, os.WriteFile(filepath.Join(valuesDir, "base.yaml"), []byte(`cilium: + prometheus: + serviceMonitor: + enabled: true +`), 0600)) + + previousInstall := installComponentFn + previousAvailable := serviceMonitorCRDAvailableFn + previousWait := waitForServiceMonitorCRDFn + t.Cleanup(func() { + installComponentFn = previousInstall + serviceMonitorCRDAvailableFn = previousAvailable + waitForServiceMonitorCRDFn = previousWait + }) + + calls := []string{} + installComponentFn = func(_ context.Context, _, _, _, _ string, _, _ bool, _ string, component componentConfig) (bool, error) { + calls = append(calls, component.name) + return true, nil + } + serviceMonitorCRDAvailableFn = func(_, _ string) (bool, error) { return true, nil } + waitForServiceMonitorCRDFn = func(_ context.Context, _, _ string) error { + calls = append(calls, "wait-for-servicemonitor-crd") + return nil + } + + _, err := InstallCilium(context.Background(), "", "", "homelab", baseDir, false, false, "") + require.NoError(t, err) + assert.Equal(t, []string{"cilium"}, calls) +} + func TestLoadValuesPreservesArgoCDBootstrapPrecedence(t *testing.T) { baseDir := t.TempDir() valuesDir := filepath.Join(baseDir, "components", "argocd", "values") @@ -201,3 +321,32 @@ dependencies: assert.Equal(t, repositoryRendered, bootstrapRendered) } + +func TestRenderComponentTemplatesIncludesOnlyEnabledResources(t *testing.T) { + componentDir := filepath.Join(t.TempDir(), "cilium") + require.NoError(t, os.MkdirAll(filepath.Join(componentDir, "templates"), 0755)) + require.NoError(t, os.WriteFile(filepath.Join(componentDir, "Chart.yaml"), []byte("apiVersion: v2\nname: cilium\nversion: 1.20.1\n"), 0600)) + require.NoError(t, os.WriteFile(filepath.Join(componentDir, "templates", "servicemonitor.yaml"), []byte(`{{- if .Values.prometheus.serviceMonitor.enabled }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: cilium-agent +{{- end }} +`), 0600)) + + componentChart, err := loader.Load(componentDir) + require.NoError(t, err) + + disabled, err := renderComponentTemplates(componentChart, map[string]interface{}{ + "prometheus": map[string]interface{}{"serviceMonitor": map[string]interface{}{"enabled": false}}, + }, ciliumComponent, true) + require.NoError(t, err) + assert.NotContains(t, disabled, "ServiceMonitor") + + enabled, err := renderComponentTemplates(componentChart, map[string]interface{}{ + "prometheus": map[string]interface{}{"serviceMonitor": map[string]interface{}{"enabled": true}}, + }, ciliumComponent, true) + require.NoError(t, err) + assert.True(t, strings.Contains(enabled, "kind: ServiceMonitor")) + assert.Contains(t, enabled, "name: cilium-agent") +} diff --git a/docs/cli/index.md b/docs/cli/index.md index 93d9eec..7665d80 100644 --- a/docs/cli/index.md +++ b/docs/cli/index.md @@ -75,6 +75,7 @@ These flags are available on all commands: |------|---------|-------------| | `--base-dir` | `.` | Base directory for repo content. Use when K8s manifests live in a subdirectory (e.g. `k8s/`). Affects local file resolution only (Chart.yaml, values, secrets files). | | `-v, --verbose` | `false` | Enable verbose output | +| `--verbose-with-templates` | `false` | Write rendered Helm manifests to a file during bootstrap (implies verbose; output may contain sensitive values) | ## Commands diff --git a/docs/components/cilium.md b/docs/components/cilium.md index 45f72f0..d7203b5 100644 --- a/docs/components/cilium.md +++ b/docs/components/cilium.md @@ -17,6 +17,8 @@ cluster-bootstrap-cli bootstrap dev --enable-cilium 5. The App of Apps creates an ArgoCD Application named `cilium`, pointing at `components/cilium/` in the configured repository and target revision. 6. ArgoCD renders the same chart version, release name, namespace, base values, and environment values used by bootstrap. +When the effective Cilium values enable any Prometheus `ServiceMonitor` (agent, operator, or Hubble), bootstrap first checks for the Prometheus Operator `ServiceMonitor` CRD. If it is absent, bootstrap installs the repository-pinned `prometheus-operator-crds` Helm component, waits for the API to become discoverable, and then installs Cilium. This preserves the usual Cilium-before-ArgoCD order while ensuring monitoring resources can be created. + This matching configuration lets ArgoCD take over the existing resources without a second release identity. Re-running bootstrap with the flag upgrades the same Helm release and reapplies the App of Apps configuration. Running without the flag does not install Cilium and leaves the default App of Apps manifest unchanged. Because Cilium provides cluster networking, its Application deliberately has no cascading resource finalizer. Automated sync, pruning, and self-healing remain enabled, but deleting the Application cannot cascade into removal of the CNI. From 9e7bb8f5a856fe38eaa0c8ed58d138bed15599e2 Mon Sep 17 00:00:00 2001 From: Filipe Galo Date: Tue, 1 Sep 2026 01:08:30 +0100 Subject: [PATCH 2/2] fix: handle rendered template output safely --- cluster-bootstrap-cli/internal/helm/argocd.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cluster-bootstrap-cli/internal/helm/argocd.go b/cluster-bootstrap-cli/internal/helm/argocd.go index ce157e9..c150cbf 100644 --- a/cluster-bootstrap-cli/internal/helm/argocd.go +++ b/cluster-bootstrap-cli/internal/helm/argocd.go @@ -287,14 +287,17 @@ func appendRenderedTemplates(path, componentName, manifest string) error { if path == "" { return fmt.Errorf("template output path is required when verbose templates are enabled") } - file, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600) + file, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600) // #nosec G304 -- bootstrap creates this absolute path beneath --base-dir. if err != nil { return fmt.Errorf("failed to open rendered template output %s: %w", path, err) } - defer file.Close() if _, err := fmt.Fprintf(file, "# BEGIN RENDERED %s HELM MANIFESTS\n%s# END RENDERED %s HELM MANIFESTS\n", strings.ToUpper(componentName), manifest, strings.ToUpper(componentName)); err != nil { + _ = file.Close() return fmt.Errorf("failed to write rendered templates to %s: %w", path, err) } + if err := file.Close(); err != nil { + return fmt.Errorf("failed to close rendered template output %s: %w", path, err) + } return nil }