From 4a4b05547570af177607df2a4451802d5661da16 Mon Sep 17 00:00:00 2001 From: Aslak Knutsen Date: Mon, 2 Feb 2026 13:41:24 +0100 Subject: [PATCH 1/3] refactor(helm)!: migrate to fs.FS as the sole resource loading interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace string-based ResourceDirectory with fs.FS throughout the codebase to provide a unified abstraction for loading Helm charts and profiles. Changes: - ReconcilerConfig.ResourceDirectory string → ResourceFS fs.FS - cmd/main.go wraps flag value with os.DirFS() at startup - All controllers use ResourceFS directly (no path construction with ResourceDirectory) - UpgradeOrInstallChart now takes (fs.FS, chartPath) instead of chartDir - Renamed getChartDir() → getChartPath() (returns relative path) - Added pkg/helm/fsloader.go with LoadChart() for loading charts from fs.FS This enables consumers to use embed.FS for bundled resources or os.DirFS for filesystem-based resources through a single consistent interface. BREAKING CHANGE: ReconcilerConfig.ResourceDirectory replaced with ResourceFS fs.FS. ChartManager.UpgradeOrInstallChart signature changed to accept fs.FS. Signed-off-by: Aslak Knutsen --- cmd/main.go | 4 +- controllers/istio/istio_controller.go | 2 +- controllers/istio/istio_controller_test.go | 3 +- controllers/istiocni/istiocni_controller.go | 9 +- .../istiocni/istiocni_controller_test.go | 3 +- .../istiorevision/istiorevision_controller.go | 8 +- .../istiorevision_controller_test.go | 3 +- .../istiorevisiontag_controller.go | 8 +- .../istiorevisiontag_controller_test.go | 3 +- .../webhook/webhook_controller_test.go | 3 +- controllers/ztunnel/ztunnel_controller.go | 9 +- .../ztunnel/ztunnel_controller_test.go | 7 +- pkg/config/config.go | 3 +- pkg/helm/chartmanager.go | 26 ++++-- pkg/helm/chartmanager_test.go | 44 +++++----- pkg/helm/fsloader.go | 74 +++++++++++++++++ pkg/helm/fsloader_test.go | 83 +++++++++++++++++++ pkg/istiovalues/profiles.go | 26 ++++-- pkg/istiovalues/profiles_test.go | 2 +- pkg/revision/dependency.go | 8 +- pkg/revision/dependency_test.go | 3 +- pkg/revision/values.go | 7 +- pkg/revision/values_test.go | 4 +- tests/integration/api/suite_test.go | 3 +- 24 files changed, 269 insertions(+), 76 deletions(-) create mode 100644 pkg/helm/fsloader.go create mode 100644 pkg/helm/fsloader_test.go diff --git a/cmd/main.go b/cmd/main.go index 55c3da4c5e..9d8a760438 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -47,6 +47,7 @@ func main() { var metricsAddr string var probeAddr string var configFile string + var resourceDirectory string var logAPIRequests bool var printVersion bool var leaderElectionEnabled bool @@ -55,7 +56,7 @@ func main() { flag.StringVar(&metricsAddr, "metrics-bind-address", ":8443", "The address the metric endpoint binds to.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") flag.StringVar(&configFile, "config-file", "/etc/sail-operator/config.properties", "Location of the config file, propagated by k8s downward APIs") - flag.StringVar(&reconcilerCfg.ResourceDirectory, "resource-directory", "/var/lib/sail-operator/resources", "Where to find resources (e.g. charts)") + flag.StringVar(&resourceDirectory, "resource-directory", "/var/lib/sail-operator/resources", "Where to find resources (e.g. charts)") flag.IntVar(&reconcilerCfg.MaxConcurrentReconciles, "max-concurrent-reconciles", 1, "MaxConcurrentReconciles is the maximum number of concurrent Reconciles which can be run.") flag.BoolVar(&logAPIRequests, "log-api-requests", false, "Whether to log each request sent to the Kubernetes API server") @@ -78,6 +79,7 @@ func main() { ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + reconcilerCfg.ResourceFS = os.DirFS(resourceDirectory) reconcilerCfg.OperatorNamespace = os.Getenv("POD_NAMESPACE") if reconcilerCfg.OperatorNamespace == "" { contents, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace") diff --git a/controllers/istio/istio_controller.go b/controllers/istio/istio_controller.go index 04ed538794..1e19d63f75 100644 --- a/controllers/istio/istio_controller.go +++ b/controllers/istio/istio_controller.go @@ -128,7 +128,7 @@ func (r *Reconciler) reconcileActiveRevision(ctx context.Context, istio *v1.Isti values, err := revision.ComputeValues( istio.Spec.Values, istio.Spec.Namespace, version, r.Config.Platform, r.Config.DefaultProfile, istio.Spec.Profile, - r.Config.ResourceDirectory, getActiveRevisionName(istio)) + r.Config.ResourceFS, getActiveRevisionName(istio)) if err != nil { return err } diff --git a/controllers/istio/istio_controller_test.go b/controllers/istio/istio_controller_test.go index b00cbaaffe..766e678da8 100644 --- a/controllers/istio/istio_controller_test.go +++ b/controllers/istio/istio_controller_test.go @@ -17,6 +17,7 @@ package istio import ( "context" "fmt" + "os" "runtime/debug" "strings" "testing" @@ -1091,7 +1092,7 @@ func noWrites(t *testing.T) interceptor.Funcs { func newReconcilerTestConfig(t *testing.T) config.ReconcilerConfig { return config.ReconcilerConfig{ - ResourceDirectory: t.TempDir(), + ResourceFS: os.DirFS(t.TempDir()), Platform: config.PlatformKubernetes, DefaultProfile: "", MaxConcurrentReconciles: 1, diff --git a/controllers/istiocni/istiocni_controller.go b/controllers/istiocni/istiocni_controller.go index 4a5b6e38e1..db9aaef78e 100644 --- a/controllers/istiocni/istiocni_controller.go +++ b/controllers/istiocni/istiocni_controller.go @@ -162,20 +162,21 @@ func (r *Reconciler) installHelmChart(ctx context.Context, cni *v1.IstioCNI) err // apply userValues on top of defaultValues from profiles mergedHelmValues, err := istiovalues.ApplyProfilesAndPlatform( - r.Config.ResourceDirectory, version, r.Config.Platform, r.Config.DefaultProfile, cni.Spec.Profile, helm.FromValues(userValues)) + r.Config.ResourceFS, version, r.Config.Platform, r.Config.DefaultProfile, cni.Spec.Profile, helm.FromValues(userValues)) if err != nil { return fmt.Errorf("failed to apply profile: %w", err) } - _, err = r.ChartManager.UpgradeOrInstallChart(ctx, r.getChartDir(version), mergedHelmValues, cni.Spec.Namespace, cniReleaseName, &ownerReference) + _, err = r.ChartManager.UpgradeOrInstallChart( + ctx, r.Config.ResourceFS, r.getChartPath(version), mergedHelmValues, cni.Spec.Namespace, cniReleaseName, &ownerReference) if err != nil { return fmt.Errorf("failed to install/update Helm chart %q: %w", cniChartName, err) } return nil } -func (r *Reconciler) getChartDir(version string) string { - return path.Join(r.Config.ResourceDirectory, version, "charts", cniChartName) +func (r *Reconciler) getChartPath(version string) string { + return path.Join(version, "charts", cniChartName) } func applyImageDigests(version string, values *v1.CNIValues, config config.OperatorConfig) *v1.CNIValues { diff --git a/controllers/istiocni/istiocni_controller_test.go b/controllers/istiocni/istiocni_controller_test.go index 97920c6473..ee851d4244 100644 --- a/controllers/istiocni/istiocni_controller_test.go +++ b/controllers/istiocni/istiocni_controller_test.go @@ -17,6 +17,7 @@ package istiocni import ( "context" "fmt" + "os" "testing" "github.com/google/go-cmp/cmp" @@ -705,7 +706,7 @@ func normalize(condition v1.IstioCNICondition) v1.IstioCNICondition { func newReconcilerTestConfig(t *testing.T) config.ReconcilerConfig { return config.ReconcilerConfig{ - ResourceDirectory: t.TempDir(), + ResourceFS: os.DirFS(t.TempDir()), Platform: config.PlatformKubernetes, DefaultProfile: "", MaxConcurrentReconciles: 1, diff --git a/controllers/istiorevision/istiorevision_controller.go b/controllers/istiorevision/istiorevision_controller.go index d98b865e48..c1f786e54e 100644 --- a/controllers/istiorevision/istiorevision_controller.go +++ b/controllers/istiorevision/istiorevision_controller.go @@ -176,13 +176,13 @@ func (r *Reconciler) installHelmCharts(ctx context.Context, rev *v1.IstioRevisio } values := helm.FromValues(rev.Spec.Values) - _, err := r.ChartManager.UpgradeOrInstallChart(ctx, r.getChartDir(rev, constants.IstiodChartName), + _, err := r.ChartManager.UpgradeOrInstallChart(ctx, r.Config.ResourceFS, r.getChartPath(rev, constants.IstiodChartName), values, rev.Spec.Namespace, getReleaseName(rev, constants.IstiodChartName), &ownerReference) if err != nil { return fmt.Errorf("failed to install/update Helm chart %q: %w", constants.IstiodChartName, err) } if rev.Name == v1.DefaultRevision { - _, err := r.ChartManager.UpgradeOrInstallChart(ctx, r.getChartDir(rev, constants.BaseChartName), + _, err := r.ChartManager.UpgradeOrInstallChart(ctx, r.Config.ResourceFS, r.getChartPath(rev, constants.BaseChartName), values, r.Config.OperatorNamespace, getReleaseName(rev, constants.BaseChartName), &ownerReference) if err != nil { return fmt.Errorf("failed to install/update Helm chart %q: %w", constants.BaseChartName, err) @@ -195,8 +195,8 @@ func getReleaseName(rev *v1.IstioRevision, chartName string) string { return fmt.Sprintf("%s-%s", rev.Name, chartName) } -func (r *Reconciler) getChartDir(rev *v1.IstioRevision, chartName string) string { - return path.Join(r.Config.ResourceDirectory, rev.Spec.Version, "charts", chartName) +func (r *Reconciler) getChartPath(rev *v1.IstioRevision, chartName string) string { + return path.Join(rev.Spec.Version, "charts", chartName) } func (r *Reconciler) uninstallHelmCharts(ctx context.Context, rev *v1.IstioRevision) error { diff --git a/controllers/istiorevision/istiorevision_controller_test.go b/controllers/istiorevision/istiorevision_controller_test.go index b39882e00d..598f6c4e05 100644 --- a/controllers/istiorevision/istiorevision_controller_test.go +++ b/controllers/istiorevision/istiorevision_controller_test.go @@ -17,6 +17,7 @@ package istiorevision import ( "context" "fmt" + "os" "strings" "testing" @@ -1053,7 +1054,7 @@ func TestIgnoreStatusChangePredicate(t *testing.T) { func newReconcilerTestConfig(t *testing.T) config.ReconcilerConfig { return config.ReconcilerConfig{ - ResourceDirectory: t.TempDir(), + ResourceFS: os.DirFS(t.TempDir()), Platform: config.PlatformKubernetes, DefaultProfile: "", MaxConcurrentReconciles: 1, diff --git a/controllers/istiorevisiontag/istiorevisiontag_controller.go b/controllers/istiorevisiontag/istiorevisiontag_controller.go index 7bf027c245..b03b70e30e 100644 --- a/controllers/istiorevisiontag/istiorevisiontag_controller.go +++ b/controllers/istiorevisiontag/istiorevisiontag_controller.go @@ -198,13 +198,13 @@ func (r *Reconciler) installHelmCharts(ctx context.Context, tag *v1.IstioRevisio return err } - _, err := r.ChartManager.UpgradeOrInstallChart(ctx, r.getChartDir(rev, revisionTagsChartName), + _, err := r.ChartManager.UpgradeOrInstallChart(ctx, r.Config.ResourceFS, r.getChartPath(rev, revisionTagsChartName), values, rev.Spec.Namespace, getReleaseName(tag, revisionTagsChartName), &ownerReference) if err != nil { return fmt.Errorf("failed to install/update Helm chart %q: %w", revisionTagsChartName, err) } if tag.Name == v1.DefaultRevision { - _, err := r.ChartManager.UpgradeOrInstallChart(ctx, r.getChartDir(rev, constants.BaseChartName), + _, err := r.ChartManager.UpgradeOrInstallChart(ctx, r.Config.ResourceFS, r.getChartPath(rev, constants.BaseChartName), values, r.Config.OperatorNamespace, getReleaseName(tag, constants.BaseChartName), &ownerReference) if err != nil { return fmt.Errorf("failed to install/update Helm chart %q: %w", constants.BaseChartName, err) @@ -217,8 +217,8 @@ func getReleaseName(tag *v1.IstioRevisionTag, chartName string) string { return fmt.Sprintf("%s-%s", tag.Name, chartName) } -func (r *Reconciler) getChartDir(tag *v1.IstioRevision, chartName string) string { - return path.Join(r.Config.ResourceDirectory, tag.Spec.Version, "charts", chartName) +func (r *Reconciler) getChartPath(rev *v1.IstioRevision, chartName string) string { + return path.Join(rev.Spec.Version, "charts", chartName) } func (r *Reconciler) uninstallHelmCharts(ctx context.Context, tag *v1.IstioRevisionTag) error { diff --git a/controllers/istiorevisiontag/istiorevisiontag_controller_test.go b/controllers/istiorevisiontag/istiorevisiontag_controller_test.go index 90c2889012..88026c7138 100644 --- a/controllers/istiorevisiontag/istiorevisiontag_controller_test.go +++ b/controllers/istiorevisiontag/istiorevisiontag_controller_test.go @@ -17,6 +17,7 @@ package istiorevisiontag import ( "context" "fmt" + "os" "strings" "testing" @@ -277,7 +278,7 @@ func TestDetermineInUseCondition(t *testing.T) { func newReconcilerTestConfig(t *testing.T) config.ReconcilerConfig { return config.ReconcilerConfig{ - ResourceDirectory: t.TempDir(), + ResourceFS: os.DirFS(t.TempDir()), Platform: config.PlatformKubernetes, DefaultProfile: "", MaxConcurrentReconciles: 1, diff --git a/controllers/webhook/webhook_controller_test.go b/controllers/webhook/webhook_controller_test.go index 966a1033a7..206f250e54 100644 --- a/controllers/webhook/webhook_controller_test.go +++ b/controllers/webhook/webhook_controller_test.go @@ -29,6 +29,7 @@ import ( "net" "net/http" "net/http/httptest" + "os" "testing" "time" @@ -615,7 +616,7 @@ func generateSelfSignedCert(dnsNames ...string) (certPEM []byte, keyPEM []byte, func newReconcilerTestConfig(t *testing.T) config.ReconcilerConfig { return config.ReconcilerConfig{ - ResourceDirectory: t.TempDir(), + ResourceFS: os.DirFS(t.TempDir()), Platform: config.PlatformKubernetes, DefaultProfile: "", MaxConcurrentReconciles: 1, diff --git a/controllers/ztunnel/ztunnel_controller.go b/controllers/ztunnel/ztunnel_controller.go index 284ad4b078..7108029829 100644 --- a/controllers/ztunnel/ztunnel_controller.go +++ b/controllers/ztunnel/ztunnel_controller.go @@ -152,7 +152,7 @@ func (r *Reconciler) installHelmChart(ctx context.Context, ztunnel *v1.ZTunnel) // apply userValues on top of defaultValues from profiles mergedHelmValues, err := istiovalues.ApplyProfilesAndPlatform( - r.Config.ResourceDirectory, version, r.Config.Platform, r.Config.DefaultProfile, defaultProfile, helm.FromValues(userValues)) + r.Config.ResourceFS, version, r.Config.Platform, r.Config.DefaultProfile, defaultProfile, helm.FromValues(userValues)) if err != nil { return fmt.Errorf("failed to apply profile: %w", err) } @@ -166,15 +166,16 @@ func (r *Reconciler) installHelmChart(ctx context.Context, ztunnel *v1.ZTunnel) return fmt.Errorf("failed to apply user overrides: %w", err) } - _, err = r.ChartManager.UpgradeOrInstallChart(ctx, r.getChartDir(version), finalHelmValues, ztunnel.Spec.Namespace, ztunnelChart, &ownerReference) + _, err = r.ChartManager.UpgradeOrInstallChart( + ctx, r.Config.ResourceFS, r.getChartPath(version), finalHelmValues, ztunnel.Spec.Namespace, ztunnelChart, &ownerReference) if err != nil { return fmt.Errorf("failed to install/update Helm chart %q: %w", ztunnelChart, err) } return nil } -func (r *Reconciler) getChartDir(version string) string { - return path.Join(r.Config.ResourceDirectory, version, "charts", ztunnelChart) +func (r *Reconciler) getChartPath(version string) string { + return path.Join(version, "charts", ztunnelChart) } func applyImageDigests(version string, values *v1.ZTunnelValues, config config.OperatorConfig) *v1.ZTunnelValues { diff --git a/controllers/ztunnel/ztunnel_controller_test.go b/controllers/ztunnel/ztunnel_controller_test.go index 8a519e05af..df879ac684 100644 --- a/controllers/ztunnel/ztunnel_controller_test.go +++ b/controllers/ztunnel/ztunnel_controller_test.go @@ -17,6 +17,7 @@ package ztunnel import ( "context" "fmt" + "os" "testing" "time" @@ -581,8 +582,8 @@ func normalize(condition v1.ZTunnelCondition) v1.ZTunnelCondition { func newReconcilerTestConfig(t *testing.T) config.ReconcilerConfig { return config.ReconcilerConfig{ - ResourceDirectory: t.TempDir(), - Platform: config.PlatformKubernetes, - DefaultProfile: "", + ResourceFS: os.DirFS(t.TempDir()), + Platform: config.PlatformKubernetes, + DefaultProfile: "", } } diff --git a/pkg/config/config.go b/pkg/config/config.go index 64bbe85d40..9c402b63c5 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -15,6 +15,7 @@ package config import ( + "io/fs" "strings" "github.com/magiconair/properties" @@ -34,7 +35,7 @@ type IstioImageConfig struct { } type ReconcilerConfig struct { - ResourceDirectory string + ResourceFS fs.FS Platform Platform DefaultProfile string OperatorNamespace string diff --git a/pkg/helm/chartmanager.go b/pkg/helm/chartmanager.go index caf05d44ad..703e310dbd 100644 --- a/pkg/helm/chartmanager.go +++ b/pkg/helm/chartmanager.go @@ -18,9 +18,10 @@ import ( "context" "errors" "fmt" + "io/fs" "helm.sh/helm/v3/pkg/action" - chartLoader "helm.sh/helm/v3/pkg/chart/loader" + "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/storage/driver" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -60,19 +61,28 @@ func (h *ChartManager) newActionConfig(ctx context.Context, namespace string) (* return actionConfig, err } -// UpgradeOrInstallChart upgrades a chart in cluster or installs it new if it does not already exist +// UpgradeOrInstallChart upgrades a chart in cluster or installs it new if it does not already exist. +// It loads the chart from an fs.FS (e.g., embed.FS or os.DirFS). func (h *ChartManager) UpgradeOrInstallChart( - ctx context.Context, chartDir string, values Values, + ctx context.Context, resourceFS fs.FS, chartPath string, values Values, namespace, releaseName string, ownerReference *metav1.OwnerReference, ) (*release.Release, error) { - log := logf.FromContext(ctx) - - cfg, err := h.newActionConfig(ctx, namespace) + loadedChart, err := LoadChart(resourceFS, chartPath) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to load chart from fs: %w", err) } - chart, err := chartLoader.Load(chartDir) + return h.upgradeOrInstallChart(ctx, loadedChart, values, namespace, releaseName, ownerReference) +} + +// upgradeOrInstallChart is the internal implementation that works with an already-loaded chart +func (h *ChartManager) upgradeOrInstallChart( + ctx context.Context, chart *chart.Chart, values Values, + namespace, releaseName string, ownerReference *metav1.OwnerReference, +) (*release.Release, error) { + log := logf.FromContext(ctx) + + cfg, err := h.newActionConfig(ctx, namespace) if err != nil { return nil, err } diff --git a/pkg/helm/chartmanager_test.go b/pkg/helm/chartmanager_test.go index 7db37d1800..e93dbd2cf3 100644 --- a/pkg/helm/chartmanager_test.go +++ b/pkg/helm/chartmanager_test.go @@ -17,7 +17,6 @@ package helm import ( "context" "os" - "path/filepath" "testing" "github.com/istio-ecosystem/sail-operator/pkg/test" @@ -36,8 +35,9 @@ import ( var ctx = context.TODO() var ( - relName = "my-release" - chartDir = filepath.Join("testdata", "chart") + relName = "my-release" + chartFS = os.DirFS("testdata") + chartPath = "chart" owner = metav1.OwnerReference{ APIVersion: "v1", @@ -59,50 +59,50 @@ var ( { name: "release exists", setup: func(g *WithT, cl client.Client, helm *ChartManager, ns string) { - install(g, helm, chartDir, ns, relName, owner) + install(g, helm, ns, relName, owner) }, }, { name: "release in failed state with previous revision", setup: func(g *WithT, cl client.Client, helm *ChartManager, ns string) { - install(g, helm, chartDir, ns, relName, owner) - upgrade(g, helm, chartDir, ns, relName, owner) + install(g, helm, ns, relName, owner) + upgrade(g, helm, ns, relName, owner) setReleaseStatus(g, helm, ns, relName, release.StatusFailed) }, }, { name: "release in failed state with no previous revision", setup: func(g *WithT, cl client.Client, helm *ChartManager, ns string) { - install(g, helm, chartDir, ns, relName, owner) + install(g, helm, ns, relName, owner) setReleaseStatus(g, helm, ns, relName, release.StatusFailed) }, }, { name: "release in pending-install state", setup: func(g *WithT, cl client.Client, helm *ChartManager, ns string) { - install(g, helm, chartDir, ns, relName, owner) + install(g, helm, ns, relName, owner) setReleaseStatus(g, helm, ns, relName, release.StatusPendingInstall) }, }, { name: "release in pending-upgrade state", setup: func(g *WithT, cl client.Client, helm *ChartManager, ns string) { - install(g, helm, chartDir, ns, relName, owner) - upgrade(g, helm, chartDir, ns, relName, owner) + install(g, helm, ns, relName, owner) + upgrade(g, helm, ns, relName, owner) setReleaseStatus(g, helm, ns, relName, release.StatusPendingUpgrade) }, }, { name: "release in uninstalling state", setup: func(g *WithT, cl client.Client, helm *ChartManager, ns string) { - install(g, helm, chartDir, ns, relName, owner) + install(g, helm, ns, relName, owner) setReleaseStatus(g, helm, ns, relName, release.StatusUninstalling) }, }, { name: "release in uninstalled state", setup: func(g *WithT, cl client.Client, helm *ChartManager, ns string) { - install(g, helm, chartDir, ns, relName, owner) + install(g, helm, ns, relName, owner) setReleaseStatus(g, helm, ns, relName, release.StatusUninstalled) }, wantErrOnInstall: true, @@ -111,7 +111,7 @@ var ( { name: "release in unknown state", setup: func(g *WithT, cl client.Client, helm *ChartManager, ns string) { - install(g, helm, chartDir, ns, relName, owner) + install(g, helm, ns, relName, owner) setReleaseStatus(g, helm, ns, relName, release.StatusUnknown) }, wantErrOnInstall: true, @@ -119,7 +119,7 @@ var ( { name: "release in superseded state", setup: func(g *WithT, cl client.Client, helm *ChartManager, ns string) { - install(g, helm, chartDir, ns, relName, owner) + install(g, helm, ns, relName, owner) setReleaseStatus(g, helm, ns, relName, release.StatusSuperseded) }, wantErrOnInstall: true, @@ -127,7 +127,7 @@ var ( { name: "release in pending-rollback state", setup: func(g *WithT, cl client.Client, helm *ChartManager, ns string) { - install(g, helm, chartDir, ns, relName, owner) + install(g, helm, ns, relName, owner) setReleaseStatus(g, helm, ns, relName, release.StatusPendingRollback) }, }, @@ -149,7 +149,7 @@ func TestUpgradeOrInstallChart(t *testing.T) { tc.setup(g, cl, helm, ns) } - rel, err := helm.UpgradeOrInstallChart(ctx, chartDir, Values{"value": "my-value"}, ns, relName, &owner) + rel, err := helm.UpgradeOrInstallChart(ctx, chartFS, chartPath, Values{"value": "my-value"}, ns, relName, &owner) if tc.wantErrOnInstall { g.Expect(err).To(HaveOccurred()) @@ -205,16 +205,16 @@ func createNamespace(cl client.Client, ns string) error { }) } -func install(g *WithT, helm *ChartManager, chartDir string, ns string, relName string, owner metav1.OwnerReference) { - upgradeOrInstall(g, helm, chartDir, ns, relName, owner) +func install(g *WithT, helm *ChartManager, ns string, relName string, owner metav1.OwnerReference) { + upgradeOrInstall(g, helm, ns, relName, owner) } -func upgrade(g *WithT, helm *ChartManager, chartDir string, ns string, relName string, owner metav1.OwnerReference) { - upgradeOrInstall(g, helm, chartDir, ns, relName, owner) +func upgrade(g *WithT, helm *ChartManager, ns string, relName string, owner metav1.OwnerReference) { + upgradeOrInstall(g, helm, ns, relName, owner) } -func upgradeOrInstall(g *WithT, helm *ChartManager, chartDir string, ns string, relName string, owner metav1.OwnerReference) { - _, err := helm.UpgradeOrInstallChart(ctx, chartDir, Values{"value": "other-value"}, ns, relName, &owner) +func upgradeOrInstall(g *WithT, helm *ChartManager, ns string, relName string, owner metav1.OwnerReference) { + _, err := helm.UpgradeOrInstallChart(ctx, chartFS, chartPath, Values{"value": "other-value"}, ns, relName, &owner) g.Expect(err).ToNot(HaveOccurred()) } diff --git a/pkg/helm/fsloader.go b/pkg/helm/fsloader.go new file mode 100644 index 0000000000..af95c94bb8 --- /dev/null +++ b/pkg/helm/fsloader.go @@ -0,0 +1,74 @@ +// Copyright Istio Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package helm + +import ( + "fmt" + "io/fs" + "strings" + + "helm.sh/helm/v3/pkg/chart" + chartLoader "helm.sh/helm/v3/pkg/chart/loader" +) + +// LoadChart loads a Helm chart from an fs.FS at the specified path. +// This allows loading charts from embed.FS, os.DirFS, or any other fs.FS implementation. +// +// The chartPath should be the path to the chart directory within the filesystem, +// e.g., "v1.28.2/charts/istiod". +func LoadChart(resourceFS fs.FS, chartPath string) (*chart.Chart, error) { + var files []*chartLoader.BufferedFile + + err := fs.WalkDir(resourceFS, chartPath, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + // Skip directories + if d.IsDir() { + return nil + } + + data, err := fs.ReadFile(resourceFS, path) + if err != nil { + return fmt.Errorf("failed to read file %s: %w", path, err) + } + + // Make path relative to chart root + // e.g., "v1.28.2/charts/istiod/Chart.yaml" -> "Chart.yaml" + relPath := strings.TrimPrefix(path, chartPath) + relPath = strings.TrimPrefix(relPath, "/") + + files = append(files, &chartLoader.BufferedFile{ + Name: relPath, + Data: data, + }) + return nil + }) + if err != nil { + return nil, fmt.Errorf("failed to walk chart directory %s: %w", chartPath, err) + } + + if len(files) == 0 { + return nil, fmt.Errorf("no files found in chart directory %s", chartPath) + } + + loadedChart, err := chartLoader.LoadFiles(files) + if err != nil { + return nil, fmt.Errorf("failed to load chart from files: %w", err) + } + + return loadedChart, nil +} diff --git a/pkg/helm/fsloader_test.go b/pkg/helm/fsloader_test.go new file mode 100644 index 0000000000..35d55785eb --- /dev/null +++ b/pkg/helm/fsloader_test.go @@ -0,0 +1,83 @@ +// Copyright Istio Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package helm + +import ( + "os" + "testing" + "testing/fstest" +) + +func TestLoadChart(t *testing.T) { + testFS := os.DirFS("testdata") + + t.Run("loads chart successfully", func(t *testing.T) { + chart, err := LoadChart(testFS, "chart") + if err != nil { + t.Fatalf("expected no error, got: %v", err) + } + if chart == nil { + t.Fatal("expected chart to be non-nil") + } + if chart.Name() != "test-chart" { + t.Errorf("expected chart name 'test-chart', got: %s", chart.Name()) + } + if chart.Metadata.Version != "0.1.0" { + t.Errorf("expected chart version '0.1.0', got: %s", chart.Metadata.Version) + } + }) + + t.Run("returns error for non-existent path", func(t *testing.T) { + _, err := LoadChart(testFS, "nonexistent") + if err == nil { + t.Fatal("expected error for non-existent path") + } + }) + + t.Run("returns error for empty directory", func(t *testing.T) { + emptyFS := fstest.MapFS{ + "empty/.gitkeep": &fstest.MapFile{}, // directory marker, but we skip it + } + // Create a truly empty directory by having only a subdirectory + emptyDirFS := fstest.MapFS{ + "emptydir/subdir/.gitkeep": &fstest.MapFile{}, + } + _, err := LoadChart(emptyDirFS, "emptydir/subdir") + if err == nil { + t.Fatal("expected error for empty directory") + } + _ = emptyFS // silence unused variable + }) + + t.Run("loads chart from nested path", func(t *testing.T) { + // Create a mock filesystem with nested chart structure + nestedFS := fstest.MapFS{ + "v1.28.0/charts/istiod/Chart.yaml": &fstest.MapFile{ + Data: []byte("apiVersion: v2\nname: istiod\nversion: 1.28.0\n"), + }, + "v1.28.0/charts/istiod/values.yaml": &fstest.MapFile{ + Data: []byte("# default values\n"), + }, + } + + chart, err := LoadChart(nestedFS, "v1.28.0/charts/istiod") + if err != nil { + t.Fatalf("expected no error, got: %v", err) + } + if chart.Name() != "istiod" { + t.Errorf("expected chart name 'istiod', got: %s", chart.Name()) + } + }) +} diff --git a/pkg/istiovalues/profiles.go b/pkg/istiovalues/profiles.go index dc8a9d6472..92dc11481b 100644 --- a/pkg/istiovalues/profiles.go +++ b/pkg/istiovalues/profiles.go @@ -16,7 +16,7 @@ package istiovalues import ( "fmt" - "os" + "io/fs" "path" "github.com/istio-ecosystem/sail-operator/pkg/config" @@ -28,11 +28,14 @@ import ( "istio.io/istio/pkg/util/sets" ) +// ApplyProfilesAndPlatform loads profiles from an fs.FS and applies them with platform settings. +// Works with embed.FS, os.DirFS, or any other fs.FS implementation. func ApplyProfilesAndPlatform( - resourceDir string, version string, platform config.Platform, defaultProfile, userProfile string, userValues helm.Values, + resourceFS fs.FS, version string, platform config.Platform, defaultProfile, userProfile string, userValues helm.Values, ) (helm.Values, error) { profile := resolve(defaultProfile, userProfile) - defaultValues, err := getValuesFromProfiles(path.Join(resourceDir, version, "profiles"), profile) + profilesPath := path.Join(version, "profiles") + defaultValues, err := getValuesFromProfiles(resourceFS, profilesPath, profile) if err != nil { return nil, fmt.Errorf("failed to get values from profile %q: %w", profile, err) } @@ -63,7 +66,7 @@ func resolve(defaultProfile, userProfile string) []string { } } -func getValuesFromProfiles(profilesDir string, profiles []string) (helm.Values, error) { +func getValuesFromProfiles(resourceFS fs.FS, profilesDir string, profiles []string) (helm.Values, error) { // start with an empty values map values := helm.Values{} @@ -84,7 +87,7 @@ func getValuesFromProfiles(profilesDir string, profiles []string) (helm.Values, return nil, reconciler.NewValidationError(fmt.Sprintf("invalid profile name %s", profile)) } - profileValues, err := getProfileValues(file) + profileValues, err := getProfileValues(resourceFS, file) if err != nil { return nil, err } @@ -94,16 +97,21 @@ func getValuesFromProfiles(profilesDir string, profiles []string) (helm.Values, return values, nil } -func getProfileValues(file string) (helm.Values, error) { - fileContents, err := os.ReadFile(file) +func getProfileValues(resourceFS fs.FS, file string) (helm.Values, error) { + fileContents, err := fs.ReadFile(resourceFS, file) if err != nil { return nil, fmt.Errorf("failed to read profile file %v: %w", file, err) } + return parseProfileYAML(fileContents, file) +} + +// parseProfileYAML parses the profile YAML content and extracts spec.values +func parseProfileYAML(fileContents []byte, filename string) (helm.Values, error) { var profile map[string]any - err = yaml.Unmarshal(fileContents, &profile) + err := yaml.Unmarshal(fileContents, &profile) if err != nil { - return nil, fmt.Errorf("failed to unmarshal profile YAML %s: %w", file, err) + return nil, fmt.Errorf("failed to unmarshal profile YAML %s: %w", filename, err) } val, found, err := unstructured.NestedFieldNoCopy(profile, "spec", "values") diff --git a/pkg/istiovalues/profiles_test.go b/pkg/istiovalues/profiles_test.go index 426c019eed..3f98273b38 100644 --- a/pkg/istiovalues/profiles_test.go +++ b/pkg/istiovalues/profiles_test.go @@ -105,7 +105,7 @@ spec: for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - actual, err := getValuesFromProfiles(profilesDir, tt.profiles) + actual, err := getValuesFromProfiles(os.DirFS(resourceDir), path.Join(version, "profiles"), tt.profiles) if (err != nil) != tt.expectErr { t.Errorf("applyProfile() error = %v, expectErr %v", err, tt.expectErr) } diff --git a/pkg/revision/dependency.go b/pkg/revision/dependency.go index d03b14b3f6..554faeabc7 100644 --- a/pkg/revision/dependency.go +++ b/pkg/revision/dependency.go @@ -15,18 +15,20 @@ package revision import ( + "io/fs" + v1 "github.com/istio-ecosystem/sail-operator/api/v1" "github.com/istio-ecosystem/sail-operator/pkg/config" ) -type computeValuesFunc func(*v1.Values, string, string, config.Platform, string, string, string, string) (*v1.Values, error) +type computeValuesFunc func(*v1.Values, string, string, config.Platform, string, string, fs.FS, string) (*v1.Values, error) var defaultComputeValues computeValuesFunc = ComputeValues // DependsOnIstioCNI returns true if CNI is enabled in the revision func DependsOnIstioCNI(rev *v1.IstioRevision, cfg config.ReconcilerConfig) bool { values, err := defaultComputeValues(rev.Spec.Values, rev.Spec.Namespace, rev.Spec.Version, - cfg.Platform, cfg.DefaultProfile, "", cfg.ResourceDirectory, rev.Name) + cfg.Platform, cfg.DefaultProfile, "", cfg.ResourceFS, rev.Name) if err != nil || values == nil { return false } @@ -48,7 +50,7 @@ func DependsOnIstioCNI(rev *v1.IstioRevision, cfg config.ReconcilerConfig) bool // DependsOnZTunnel returns true if the revision is configured for ambient mode and requires ZTunnel func DependsOnZTunnel(rev *v1.IstioRevision, cfg config.ReconcilerConfig) bool { values, err := defaultComputeValues(rev.Spec.Values, rev.Spec.Namespace, rev.Spec.Version, - cfg.Platform, cfg.DefaultProfile, "", cfg.ResourceDirectory, rev.Name) + cfg.Platform, cfg.DefaultProfile, "", cfg.ResourceFS, rev.Name) if err != nil || values == nil { return false } diff --git a/pkg/revision/dependency_test.go b/pkg/revision/dependency_test.go index 5653cc91f1..de31356fb4 100644 --- a/pkg/revision/dependency_test.go +++ b/pkg/revision/dependency_test.go @@ -15,6 +15,7 @@ package revision import ( + "io/fs" "testing" v1 "github.com/istio-ecosystem/sail-operator/api/v1" @@ -26,7 +27,7 @@ import ( // mockComputeValues returns the input values without any computation // this simulates what ComputeValues would do but without requiring actual files -func mockComputeValues(values *v1.Values, _, _ string, platform config.Platform, defaultProfile, userProfile, _, _ string) (*v1.Values, error) { +func mockComputeValues(values *v1.Values, _, _ string, platform config.Platform, defaultProfile, userProfile string, _ fs.FS, _ string) (*v1.Values, error) { if values == nil { values = &v1.Values{} } diff --git a/pkg/revision/values.go b/pkg/revision/values.go index 759e84a59c..805d13cee4 100644 --- a/pkg/revision/values.go +++ b/pkg/revision/values.go @@ -16,6 +16,7 @@ package revision import ( "fmt" + "io/fs" v1 "github.com/istio-ecosystem/sail-operator/api/v1" "github.com/istio-ecosystem/sail-operator/pkg/config" @@ -28,9 +29,11 @@ import ( // - applies vendor-specific default values // - applies the user-provided values on top of the default values from the default and user-selected profiles // - applies overrides that are not configurable by the user +// +// The resourceFS parameter accepts any fs.FS implementation (embed.FS, os.DirFS, etc.). func ComputeValues( userValues *v1.Values, namespace string, version string, - platform config.Platform, defaultProfile, userProfile string, resourceDir string, + platform config.Platform, defaultProfile, userProfile string, resourceFS fs.FS, activeRevisionName string, ) (*v1.Values, error) { // apply image digests from configuration, if not already set by user @@ -43,7 +46,7 @@ func ComputeValues( } // apply userValues on top of defaultValues from profiles - mergedHelmValues, err := istiovalues.ApplyProfilesAndPlatform(resourceDir, version, platform, defaultProfile, userProfile, helm.FromValues(userValues)) + mergedHelmValues, err := istiovalues.ApplyProfilesAndPlatform(resourceFS, version, platform, defaultProfile, userProfile, helm.FromValues(userValues)) if err != nil { return nil, fmt.Errorf("failed to apply profile: %w", err) } diff --git a/pkg/revision/values_test.go b/pkg/revision/values_test.go index f55d2edf45..ec3cda777a 100644 --- a/pkg/revision/values_test.go +++ b/pkg/revision/values_test.go @@ -68,7 +68,7 @@ spec: }, } - result, err := ComputeValues(values, namespace, version, config.PlatformOpenShift, "default", "my-profile", resourceDir, revisionName) + result, err := ComputeValues(values, namespace, version, config.PlatformOpenShift, "default", "my-profile", os.DirFS(resourceDir), revisionName) if err != nil { t.Errorf("Expected no error, but got an error: %v", err) } @@ -111,7 +111,7 @@ spec:`)), 0o644)) istiovalues.FipsEnabled = true values := &v1.Values{} result, err := ComputeValues(values, namespace, version, config.PlatformOpenShift, "default", "", - resourceDir, revisionName) + os.DirFS(resourceDir), revisionName) if err != nil { t.Errorf("Expected no error, but got an error: %v", err) } diff --git a/tests/integration/api/suite_test.go b/tests/integration/api/suite_test.go index 560cef425f..248758cde5 100644 --- a/tests/integration/api/suite_test.go +++ b/tests/integration/api/suite_test.go @@ -18,6 +18,7 @@ package integration import ( "context" + "os" "path" "testing" @@ -86,7 +87,7 @@ var _ = BeforeSuite(func() { Expect(k8sClient.Create(context.TODO(), operatorNs)).To(Succeed()) cfg := config.ReconcilerConfig{ - ResourceDirectory: path.Join(project.RootDir, "resources"), + ResourceFS: os.DirFS(path.Join(project.RootDir, "resources")), Platform: config.PlatformKubernetes, DefaultProfile: "", OperatorNamespace: operatorNs.Name, From d09c1bc6e1c285b0a276f6786f700aa51f2cb9ae Mon Sep 17 00:00:00 2001 From: Aslak Knutsen Date: Mon, 2 Feb 2026 13:45:55 +0100 Subject: [PATCH 2/3] feat(resources): add embedded fs.FS for library consumers Provide an embed.FS in the resources package so downstream consumers can bundle Helm charts and profiles directly in their binary instead of relying on filesystem paths. The Sail Operator itself does not import this package, keeping its binary size unchanged. This is intended for library consumers who want self-contained binaries with embedded resources. Usage: import "github.com/istio-ecosystem/sail-operator/resources" cfg := config.ReconcilerConfig{ResourceFS: resources.FS} Signed-off-by: Aslak Knutsen --- resources/resources.go | 72 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 resources/resources.go diff --git a/resources/resources.go b/resources/resources.go new file mode 100644 index 0000000000..40b045578b --- /dev/null +++ b/resources/resources.go @@ -0,0 +1,72 @@ +// Copyright Istio Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package resources provides embedded Istio Helm charts and profiles. +// +// This package embeds all version directories (v1.28.2, etc.) containing +// Helm charts and profiles. Importing this package will increase the binary +// size significantly (~10MB) as it includes all chart files. +// +// The Sail Operator itself does NOT import this package - it uses filesystem +// paths via os.DirFS instead, keeping the operator binary small. +// +// This package is intended for consumers who want to embed the charts +// directly in their binary. +// +// Usage: +// +// import "github.com/istio-ecosystem/sail-operator/resources" +// +// cfg := config.ReconcilerConfig{ +// ResourceFS: resources.FS, +// } +// +// The embedded paths are relative to this directory, e.g.: +// - v1.28.2/charts/istiod/Chart.yaml +// - v1.28.2/profiles/default.yaml +package resources + +import ( + "embed" + "io/fs" +) + +// FS contains the embedded resources directory with all Helm charts and profiles. +// Paths are relative to this directory (e.g., "v1.28.2/charts/istiod"). +// +//go:embed all:v* +var FS embed.FS + +// SubFS creates a sub-filesystem rooted at the specified directory. +// This is useful for stripping prefixes from embedded filesystems. +// +// Example: +// +// // If you have your own embed with a prefix: +// //go:embed my-resources +// var rawFS embed.FS +// fs := resources.SubFS(rawFS, "my-resources") +func SubFS(fsys fs.FS, dir string) (fs.FS, error) { + return fs.Sub(fsys, dir) +} + +// MustSubFS is like SubFS but panics on error. +// Use this when the directory is known to exist. +func MustSubFS(fsys fs.FS, dir string) fs.FS { + sub, err := fs.Sub(fsys, dir) + if err != nil { + panic("failed to create sub-filesystem for " + dir + ": " + err.Error()) + } + return sub +} From 8d0f7330416a800fc82562f2737d1feb0fa7f2fe Mon Sep 17 00:00:00 2001 From: Aslak Knutsen Date: Mon, 2 Feb 2026 21:22:15 +0100 Subject: [PATCH 3/3] feat(resources): use embedded resources by default Add embedded fs.FS from the resources package and use it as the default resource source. The operator now embeds all Helm charts and profiles directly in the binary, eliminating the need for external resource files. Changes: - Change --resource-directory default from /var/lib/sail-operator/resources to "" - When --resource-directory is empty (default), use embedded resources.FS - When --resource-directory is specified, use os.DirFS for filesystem access - Removed --resource-directory from Makefile - Removed --resource-directory from Dockerfile This increases binary size by ~10MB but simplifies deployment by removing the dependency on external resource files mounted into the container. Signed-off-by: Aslak Knutsen --- Dockerfile | 1 - Makefile.core.mk | 2 +- cmd/main.go | 11 +++++++++-- resources/resources.go | 3 --- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index d7fde71f8a..af59dd0867 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,6 @@ ARG TARGETOS TARGETARCH COPY --from=packager /output / ADD out/${TARGETOS:-linux}_${TARGETARCH:-amd64}/sail-operator /sail-operator -ADD resources /var/lib/sail-operator/resources USER 65532:65532 WORKDIR / diff --git a/Makefile.core.mk b/Makefile.core.mk index e0507f79a3..123bb6ea5b 100644 --- a/Makefile.core.mk +++ b/Makefile.core.mk @@ -251,7 +251,7 @@ build: build-$(TARGET_ARCH) ## Build the sail-operator binary. .PHONY: run run: gen ## Run a controller from your host. - POD_NAMESPACE=${NAMESPACE} go run ./cmd/main.go --config-file=./hack/config.properties --resource-directory=./resources + POD_NAMESPACE=${NAMESPACE} go run ./cmd/main.go --config-file=./hack/config.properties # docker build -t ${IMAGE} --build-arg GIT_TAG=${GIT_TAG} --build-arg GIT_REVISION=${GIT_REVISION} --build-arg GIT_STATUS=${GIT_STATUS} . .PHONY: docker-build diff --git a/cmd/main.go b/cmd/main.go index 9d8a760438..3514b19c3f 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -32,6 +32,7 @@ import ( "github.com/istio-ecosystem/sail-operator/pkg/helm" "github.com/istio-ecosystem/sail-operator/pkg/scheme" "github.com/istio-ecosystem/sail-operator/pkg/version" + "github.com/istio-ecosystem/sail-operator/resources" _ "k8s.io/client-go/plugin/pkg/client/auth" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/healthz" @@ -56,7 +57,7 @@ func main() { flag.StringVar(&metricsAddr, "metrics-bind-address", ":8443", "The address the metric endpoint binds to.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") flag.StringVar(&configFile, "config-file", "/etc/sail-operator/config.properties", "Location of the config file, propagated by k8s downward APIs") - flag.StringVar(&resourceDirectory, "resource-directory", "/var/lib/sail-operator/resources", "Where to find resources (e.g. charts)") + flag.StringVar(&resourceDirectory, "resource-directory", "", "Where to find resources (e.g. charts). If empty, uses embedded resources.") flag.IntVar(&reconcilerCfg.MaxConcurrentReconciles, "max-concurrent-reconciles", 1, "MaxConcurrentReconciles is the maximum number of concurrent Reconciles which can be run.") flag.BoolVar(&logAPIRequests, "log-api-requests", false, "Whether to log each request sent to the Kubernetes API server") @@ -79,7 +80,13 @@ func main() { ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) - reconcilerCfg.ResourceFS = os.DirFS(resourceDirectory) + if resourceDirectory != "" { + setupLog.Info("using filesystem resources", "directory", resourceDirectory) + reconcilerCfg.ResourceFS = os.DirFS(resourceDirectory) + } else { + setupLog.Info("using embedded resources") + reconcilerCfg.ResourceFS = resources.FS + } reconcilerCfg.OperatorNamespace = os.Getenv("POD_NAMESPACE") if reconcilerCfg.OperatorNamespace == "" { contents, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace") diff --git a/resources/resources.go b/resources/resources.go index 40b045578b..081c6ea65d 100644 --- a/resources/resources.go +++ b/resources/resources.go @@ -18,9 +18,6 @@ // Helm charts and profiles. Importing this package will increase the binary // size significantly (~10MB) as it includes all chart files. // -// The Sail Operator itself does NOT import this package - it uses filesystem -// paths via os.DirFS instead, keeping the operator binary small. -// // This package is intended for consumers who want to embed the charts // directly in their binary. //