From 256a2ab8e089f2ed65fa206bd6c20871feab03af Mon Sep 17 00:00:00 2001 From: Tim Schrodi Date: Wed, 26 Aug 2026 12:54:03 +0200 Subject: [PATCH 1/3] feat(argocd): install chart from bundle BOM Prepare the installer bundle before bootstrap so its BOM is available during Argo CD installation. Resolve the Argo CD OCI chart and version from the BOM, nest wrapper chart values appropriately, and retain the upstream chart fallback when no usable BOM entry exists. --- .../install_codesphere_dependencies.go | 1 + internal/bootstrap/local/local.go | 8 ++++- internal/installer/argocd/installer.go | 27 +++++++++++++- internal/installer/argocd/installer_test.go | 35 +++++++++++++++++++ 4 files changed, 69 insertions(+), 2 deletions(-) diff --git a/cli/cmd/codesphere/install_codesphere_dependencies.go b/cli/cmd/codesphere/install_codesphere_dependencies.go index 8abb936d5..dad40b1ef 100644 --- a/cli/cmd/codesphere/install_codesphere_dependencies.go +++ b/cli/cmd/codesphere/install_codesphere_dependencies.go @@ -145,6 +145,7 @@ func installArgoCDAndApps(opts *InstallCodesphereOpts, cfg files.RootConfig, pm FullInstall: true, ForceConflicts: opts.ArgoCDForceConflicts, RepoURL: opts.ArgoCDRepoURL, + BOM: bomConfig, ValueFiles: opts.ArgoCDValues, RESTConfig: restConfig, }) diff --git a/internal/bootstrap/local/local.go b/internal/bootstrap/local/local.go index be82d1d6d..67e2539e8 100644 --- a/internal/bootstrap/local/local.go +++ b/internal/bootstrap/local/local.go @@ -237,13 +237,19 @@ func (b *LocalBootstrapper) Bootstrap() error { } func (b *LocalBootstrapper) newArgoCDAndAppsInstall() (*argocd.AppInstaller, error) { + version := "9.5.21" + if b.installerBOM != nil { + version = "" + } + // renovate: datasource=helm depName=argo-cd registryUrl=https://argoproj.github.io/argo-helm argoCDInstall, err := argocd.NewInstaller(argocd.InstallerConfig{ - Version: "9.5.21", + Version: version, OciPassword: b.Env.RegistryPassword, OciRegistryURL: strings.TrimPrefix(b.Env.ArgoCDRegistryURL, "oci://"), FullInstall: true, ForceConflicts: true, + BOM: b.installerBOM, RESTConfig: b.restConfig, }) if err != nil { diff --git a/internal/installer/argocd/installer.go b/internal/installer/argocd/installer.go index be03780c2..ab3b501a0 100644 --- a/internal/installer/argocd/installer.go +++ b/internal/installer/argocd/installer.go @@ -11,6 +11,7 @@ import ( "github.com/Masterminds/semver/v3" "github.com/codesphere-cloud/oms/internal/installer" + "github.com/codesphere-cloud/oms/internal/installer/bom" k8s "github.com/codesphere-cloud/oms/internal/util" "helm.sh/helm/v4/pkg/chart/common/util" "helm.sh/helm/v4/pkg/cli/values" @@ -34,6 +35,7 @@ type InstallerConfig struct { FullInstall bool ForceConflicts bool RepoURL string + BOM *bom.Config ValueFiles []string RESTConfig *rest.Config } @@ -89,6 +91,17 @@ func NewInstaller(cfg InstallerConfig) (*Installer, error) { // Install is the top-level orchestrator. It delegates every Helm interaction // to the HelmClient interface, keeping this function short and testable. func (a *Installer) Install() error { + chartName := "argo-cd" + usingBOMChart := false + if a.BOM != nil && a.RepoURL == "" && a.Version == "" { + if chart, ok := a.BOM.GetChart("argocd"); ok { + chartName = "oci://" + chart.Name() + usingBOMChart = true + a.Version = chart.Tag() + log.Printf("Using ArgoCD chart %s:%s from BOM\n", chart.Name(), chart.Tag()) + } + } + if err := a.validateRepoURL(); err != nil { return err } @@ -111,9 +124,18 @@ func (a *Installer) Install() error { defaults := map[string]any{ "dex": map[string]any{"enabled": false}, } + if usingBOMChart { + // The Codesphere argocd chart is a wrapper around the upstream + // argo-cd chart. Helm passes dependency values through the dependency + // name, so upstream defaults must be nested under "argo-cd". The + // upstream chart installed directly expects the same values at root. + defaults = map[string]any{ + "argo-cd": defaults, + } + } vals = util.MergeTables(vals, defaults) - chartName, repoURL := a.resolveChartRef("argo-cd") + chartName, repoURL := a.resolveChartRef(chartName) cfg := installer.ChartConfig{ ReleaseName: "argocd", ChartName: chartName, @@ -217,6 +239,9 @@ func (a *Installer) validateRepoURL() error { } func (a *Installer) resolveChartRef(chartName string) (string, string) { + if strings.HasPrefix(chartName, "oci://") { + return chartName, "" + } repoURL := a.RepoURL if repoURL == "" { repoURL = DefaultRepoURL diff --git a/internal/installer/argocd/installer_test.go b/internal/installer/argocd/installer_test.go index 631f46823..dbc6df0ed 100644 --- a/internal/installer/argocd/installer_test.go +++ b/internal/installer/argocd/installer_test.go @@ -10,6 +10,7 @@ import ( "github.com/codesphere-cloud/oms/internal/installer" "github.com/codesphere-cloud/oms/internal/installer/argocd" + "github.com/codesphere-cloud/oms/internal/installer/bom" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/stretchr/testify/mock" @@ -173,6 +174,40 @@ var _ = Describe("Installer.Install", func() { }) }) + Context("BOM chart", func() { + It("uses the argocd OCI chart and version from the BOM", func() { + bomPath := filepath.Join(GinkgoT().TempDir(), "bom.json") + Expect(os.WriteFile(bomPath, []byte(`{"components":{"argocd":{"files":{"chart":{"ociRef":"ghcr.io/codesphere-cloud/charts/argocd:1.2.3"}}}}}`), 0o600)).To(Succeed()) + bomConfig, err := bom.Parse(bomPath) + Expect(err).NotTo(HaveOccurred()) + + helmMock.EXPECT().FindRelease("argocd", "argocd").Return(nil, nil) + helmMock.EXPECT().InstallChart(mock.Anything, mock.MatchedBy(func(cfg installer.ChartConfig) bool { + argoValues, ok := cfg.Values["argo-cd"].(map[string]interface{}) + if !ok { + return false + } + dex, ok := argoValues["dex"].(map[string]interface{}) + return cfg.ChartName == "oci://ghcr.io/codesphere-cloud/charts/argocd" && + cfg.RepoURL == "" && cfg.Version == "1.2.3" && + ok && dex["enabled"] == false && cfg.Values["dex"] == nil + }), mock.Anything).Return(nil) + + a = &argocd.Installer{InstallerConfig: argocd.InstallerConfig{BOM: bomConfig}, Helm: helmMock} + Expect(a.Install()).To(Succeed()) + }) + + It("falls back to the upstream chart when no BOM is provided", func() { + helmMock.EXPECT().FindRelease("argocd", "argocd").Return(nil, nil) + helmMock.EXPECT().InstallChart(mock.Anything, mock.MatchedBy(func(cfg installer.ChartConfig) bool { + return cfg.ChartName == "argo-cd" && cfg.RepoURL == argocd.DefaultRepoURL + }), mock.Anything).Return(nil) + + a = &argocd.Installer{Helm: helmMock} + Expect(a.Install()).To(Succeed()) + }) + }) + Context("values overrides", func() { BeforeEach(func() { helmMock.EXPECT().FindRelease("argocd", "argocd").Return(nil, nil) From c589994dc2e1b061dd8af37cc5701519933fec1d Mon Sep 17 00:00:00 2001 From: Tim Schrodi Date: Wed, 26 Aug 2026 14:03:56 +0200 Subject: [PATCH 2/3] feat(installer): support alternative OCI registries Add an opt-in --registry flag to local and GCP bootstrap flows and persist explicit overrides in config.yaml. Rewrite BOM image and chart references for the selected registry and propagate it to the pc-applications Helm values. --- cli/cmd/bootstrap_gcp.go | 1 + cli/cmd/bootstrap_local.go | 3 +- .../install_codesphere_dependencies.go | 14 ++++- internal/bootstrap/gcp/gcp.go | 5 +- internal/bootstrap/gcp/gcp_test.go | 10 +++- internal/bootstrap/local/local.go | 30 ++++++++-- internal/installer/argocd/install_and_apps.go | 7 +++ .../installer/argocd/install_and_apps_test.go | 37 ++++++++++++ internal/installer/bom/bom.go | 56 +++++++++++++++++++ internal/installer/bom/bom_test.go | 25 +++++++++ internal/installer/files/config_yaml.go | 2 +- 11 files changed, 177 insertions(+), 13 deletions(-) diff --git a/cli/cmd/bootstrap_gcp.go b/cli/cmd/bootstrap_gcp.go index 5e36f993b..d4b50db05 100644 --- a/cli/cmd/bootstrap_gcp.go +++ b/cli/cmd/bootstrap_gcp.go @@ -100,6 +100,7 @@ func AddBootstrapGcpCmd(parent *cobra.Command, opts *util.GlobalOptions) { flags.StringArrayVarP(&bootstrapGcpCmd.CodesphereEnv.InstallSkipSteps, "install-skip-steps", "s", []string{}, "Installation steps to skip during Codesphere installation (optional)") flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.RemoteOmsBinaryPath, "remote-oms-binary", "", "Path to a local Linux amd64 OMS binary to copy to and use on the jumpbox instead of downloading a release (optional)") flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.RegistryUser, "registry-user", "", "Custom Registry username (only for GitHub registry type) (optional)") + flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.ContainerRegistryURL, "registry", "", "Alternative container registry used for Codesphere images and charts") flags.StringVar(&bootstrapGcpCmd.InputRegistryType, "registry-type", "local-container", "Container registry type to use (options: local-container, artifact-registry) (default: local-container)") flags.StringArrayVar(&bootstrapGcpCmd.CodesphereEnv.InternalFlags, "internal-flags", gcp.DefaultInternalFlags, "Internal flags to enable in Codesphere installation (optional)") flags.StringArrayVar(&bootstrapGcpCmd.experiments, "experiments", []string{}, "Deprecated: use --internal-flags instead. Values are added to the internal flags.") diff --git a/cli/cmd/bootstrap_local.go b/cli/cmd/bootstrap_local.go index c9dcf8315..bcc23dd01 100644 --- a/cli/cmd/bootstrap_local.go +++ b/cli/cmd/bootstrap_local.go @@ -77,6 +77,7 @@ func AddBootstrapLocalCmd(parent *cobra.Command) { flags.StringVar(&bootstrapLocalCmd.CodesphereEnv.InstallLocal, "install-local", "", "Path to a local installer package (tar.gz or unpacked directory)") // Registry flags.StringVar(&bootstrapLocalCmd.CodesphereEnv.RegistryUser, "registry-user", "", "Custom Registry username") + flags.StringVar(&bootstrapLocalCmd.CodesphereEnv.ContainerRegistryURL, "registry", "", "Alternative container registry used for Codesphere images and charts") // Codesphere Environment flags.StringVar(&bootstrapLocalCmd.CodesphereEnv.BaseDomain, "base-domain", "cs.local", "Base domain for Codesphere") @@ -97,8 +98,6 @@ func AddBootstrapLocalCmd(parent *cobra.Command) { flags.StringVar(&bootstrapLocalCmd.CodesphereEnv.SecretsFilePath, "secrets-file", "", "Path to secrets file (default: /prod.vault.yaml)") flags.StringVar(&bootstrapLocalCmd.CodesphereEnv.CephDeviceFilter, "ceph-device-filter", "", "Regular expression selecting Ceph block devices by name") flags.StringVar(&bootstrapLocalCmd.CodesphereEnv.CephDevicePathFilter, "ceph-device-path-filter", "", "Regular expression selecting Ceph block devices by path") - // ArgoCD integration - flags.StringVar(&bootstrapLocalCmd.CodesphereEnv.ArgoCDRegistryURL, "registry-url", "oci://ghcr.io/codesphere-cloud/charts", "OCI registry URL used for the ArgoCD helm pull secret") bootstrapLocalCmd.cmd.RunE = bootstrapLocalCmd.RunE util.MarkFlagRequired(bootstrapLocalCmd.cmd, "registry-user") diff --git a/cli/cmd/codesphere/install_codesphere_dependencies.go b/cli/cmd/codesphere/install_codesphere_dependencies.go index dad40b1ef..04ea09329 100644 --- a/cli/cmd/codesphere/install_codesphere_dependencies.go +++ b/cli/cmd/codesphere/install_codesphere_dependencies.go @@ -8,6 +8,7 @@ import ( "fmt" "os" "runtime" + "strings" argov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" "github.com/codesphere-cloud/cs-go/pkg/io" @@ -117,6 +118,15 @@ func installArgoCDAndApps(opts *InstallCodesphereOpts, cfg files.RootConfig, pm if err != nil { return fmt.Errorf("failed to parse installer BOM: %w", err) } + configuredRegistryURL := "" + if cfg.Registry != nil { + configuredRegistryURL = strings.TrimSuffix(strings.TrimPrefix(cfg.Registry.Server, "oci://"), "/") + if configuredRegistryURL != "" && configuredRegistryURL != "ghcr.io" { + if err := bomConfig.UseRegistry(configuredRegistryURL); err != nil { + return fmt.Errorf("failed to configure installer BOM registry: %w", err) + } + } + } var install *argocdinstaller.AppInstaller @@ -133,8 +143,8 @@ func installArgoCDAndApps(opts *InstallCodesphereOpts, cfg files.RootConfig, pm return fmt.Errorf("registry password not found in vault (secret %q)", files.SecretRegistryPassword) } registryURL := opts.ArgoCDRegistryURL - if registryURL == "" && cfg.Registry != nil { - registryURL = cfg.Registry.Server + "/codesphere-cloud/charts" + if registryURL == "" && configuredRegistryURL != "" { + registryURL = configuredRegistryURL + "/codesphere-cloud/charts" } argoCDInstall, err := argocdinstaller.NewInstaller(argocdinstaller.InstallerConfig{ Version: opts.ArgoCDVersion, diff --git a/internal/bootstrap/gcp/gcp.go b/internal/bootstrap/gcp/gcp.go index 2902070ca..ebd472e6c 100644 --- a/internal/bootstrap/gcp/gcp.go +++ b/internal/bootstrap/gcp/gcp.go @@ -1029,7 +1029,10 @@ func (b *GCPBootstrapper) EnsureGitHubAccessConfigured() error { if b.Env.GitHubPAT == "" { return fmt.Errorf("GitHub PAT is not set") } - b.Env.InstallConfig.Registry.Server = "ghcr.io" + registryURL := strings.TrimSuffix(strings.TrimPrefix(b.Env.ContainerRegistryURL, "oci://"), "/") + if registryURL != "" { + b.Env.InstallConfig.Registry.Server = registryURL + } b.icg.GetVault().SetSecret(files.SecretEntry{Name: files.SecretRegistryUsername, Fields: &files.SecretFields{Password: b.Env.RegistryUser}}) b.icg.GetVault().SetSecret(files.SecretEntry{Name: files.SecretRegistryPassword, Fields: &files.SecretFields{Password: b.Env.GitHubPAT}}) b.Env.InstallConfig.Registry.ReplaceImagesInBom = false diff --git a/internal/bootstrap/gcp/gcp_test.go b/internal/bootstrap/gcp/gcp_test.go index 43edc3ccd..f2ae45ea0 100644 --- a/internal/bootstrap/gcp/gcp_test.go +++ b/internal/bootstrap/gcp/gcp_test.go @@ -991,13 +991,21 @@ var _ = Describe("GCP Bootstrapper", func() { err := bs.EnsureGitHubAccessConfigured() Expect(err).NotTo(HaveOccurred()) - Expect(bs.Env.InstallConfig.Registry.Server).To(Equal("ghcr.io")) + Expect(bs.Env.InstallConfig.Registry.Server).To(BeEmpty()) Expect(vault.GetSecret(files.SecretRegistryUsername).Fields.Password).To(Equal(csEnv.RegistryUser)) Expect(vault.GetSecret(files.SecretRegistryPassword).Fields.Password).To(Equal(csEnv.GitHubPAT)) Expect(bs.Env.InstallConfig.Registry.LoadContainerImages).To(BeFalse()) Expect(bs.Env.InstallConfig.Registry.ReplaceImagesInBom).To(BeFalse()) }) + It("uses the configured registry URL", func() { + csEnv.ContainerRegistryURL = "oci://registry.example.com/mirror/" + icg.EXPECT().GetVault().Return(&files.InstallVault{}) + + Expect(bs.EnsureGitHubAccessConfigured()).To(Succeed()) + Expect(bs.Env.InstallConfig.Registry.Server).To(Equal("registry.example.com/mirror")) + }) + Context("When GitHub PAT is missing", func() { BeforeEach(func() { csEnv.GitHubPAT = "" diff --git a/internal/bootstrap/local/local.go b/internal/bootstrap/local/local.go index 67e2539e8..b197e908c 100644 --- a/internal/bootstrap/local/local.go +++ b/internal/bootstrap/local/local.go @@ -85,8 +85,9 @@ type CodesphereEnvironment struct { InstallHash string `json:"install_hash"` InstallLocal string `json:"install_local"` // Registry - RegistryUser string `json:"-"` - RegistryPassword string `json:"-"` + RegistryUser string `json:"-"` + RegistryPassword string `json:"-"` + ContainerRegistryURL string `json:"container_registry_url,omitempty"` // Config InstallDir string `json:"-"` ExistingConfigUsed bool `json:"-"` @@ -99,8 +100,6 @@ type CodesphereEnvironment struct { ServiceCIDR string `json:"service_cidr"` CephDeviceFilter string `json:"-"` CephDevicePathFilter string `json:"-"` - // ArgoCD integration - ArgoCDRegistryURL string `json:"-"` } // NewLocalBootstrapper creates a bootstrapper for a local Codesphere cluster. @@ -241,12 +240,16 @@ func (b *LocalBootstrapper) newArgoCDAndAppsInstall() (*argocd.AppInstaller, err if b.installerBOM != nil { version = "" } + registryURL := "" + if b.Env.InstallConfig.Registry != nil && b.Env.InstallConfig.Registry.Server != "" { + registryURL = strings.TrimSuffix(b.Env.InstallConfig.Registry.Server, "/") + "/codesphere-cloud/charts" + } // renovate: datasource=helm depName=argo-cd registryUrl=https://argoproj.github.io/argo-helm argoCDInstall, err := argocd.NewInstaller(argocd.InstallerConfig{ Version: version, OciPassword: b.Env.RegistryPassword, - OciRegistryURL: strings.TrimPrefix(b.Env.ArgoCDRegistryURL, "oci://"), + OciRegistryURL: strings.TrimPrefix(registryURL, "oci://"), FullInstall: true, ForceConflicts: true, BOM: b.installerBOM, @@ -517,6 +520,22 @@ func (b *LocalBootstrapper) EnsureInstallConfig() error { } b.Env.InstallConfig = b.icg.GetInstallConfig() + configuredRegistry := strings.TrimSuffix(strings.TrimPrefix(b.Env.ContainerRegistryURL, "oci://"), "/") + if configuredRegistry != "" { + if b.Env.InstallConfig.Registry == nil { + b.Env.InstallConfig.Registry = &files.RegistryConfig{} + } + b.Env.InstallConfig.Registry.Server = configuredRegistry + } + effectiveRegistry := "" + if b.Env.InstallConfig.Registry != nil { + effectiveRegistry = strings.TrimSuffix(strings.TrimPrefix(b.Env.InstallConfig.Registry.Server, "oci://"), "/") + } + if b.installerBOM != nil && effectiveRegistry != "" && effectiveRegistry != "ghcr.io" { + if err := b.installerBOM.UseRegistry(effectiveRegistry); err != nil { + return fmt.Errorf("failed to configure installer BOM registry: %w", err) + } + } return nil } @@ -694,7 +713,6 @@ func (b *LocalBootstrapper) EnsureGitHubAccessConfigured() error { if b.Env.RegistryPassword == "" { return fmt.Errorf("registry password is not set") } - b.Env.InstallConfig.Registry.Server = "ghcr.io" b.icg.GetVault().SetSecret(files.SecretEntry{Name: files.SecretRegistryUsername, Fields: &files.SecretFields{Password: b.Env.RegistryUser}}) b.icg.GetVault().SetSecret(files.SecretEntry{Name: files.SecretRegistryPassword, Fields: &files.SecretFields{Password: b.Env.RegistryPassword}}) b.Env.InstallConfig.Registry.ReplaceImagesInBom = false diff --git a/internal/installer/argocd/install_and_apps.go b/internal/installer/argocd/install_and_apps.go index 6d11a524c..332693a67 100644 --- a/internal/installer/argocd/install_and_apps.go +++ b/internal/installer/argocd/install_and_apps.go @@ -128,6 +128,13 @@ func (i *AppInstaller) InstallPCApps(ctx context.Context, bomConfig *bom.Config) // Values derived from the install config form the base; an explicit pcApps block in // config.yaml wins over them, and the --pc-apps-values files win over both. values := util.DeepMergeMaps(installer.OpenFgaPcAppsValues(&i.cfg.Config, i.cfg.Vault), i.cfg.Config.PcApps) + if i.cfg.Config.Registry != nil && i.cfg.Config.Registry.Server != "" { + values = util.DeepMergeMaps(map[string]any{ + "global": map[string]any{ + "imageRegistry": i.cfg.Config.Registry.Server, + }, + }, values) + } pcApps, err := installer.NewPcAppsFromBom( i.cfg.KubeClient, diff --git a/internal/installer/argocd/install_and_apps_test.go b/internal/installer/argocd/install_and_apps_test.go index 7e6814c45..6e37bae12 100644 --- a/internal/installer/argocd/install_and_apps_test.go +++ b/internal/installer/argocd/install_and_apps_test.go @@ -4,18 +4,28 @@ package argocd_test import ( + "context" + "encoding/json" "os" "os/exec" "path/filepath" "strings" + argov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" "github.com/codesphere-cloud/oms/internal/installer" "github.com/codesphere-cloud/oms/internal/installer/argocd" + "github.com/codesphere-cloud/oms/internal/installer/bom" "github.com/codesphere-cloud/oms/internal/installer/files" "github.com/codesphere-cloud/oms/internal/installer/vault" "github.com/codesphere-cloud/oms/internal/installer/vault/sops" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" ) func sopsAndAgeAvailable() bool { @@ -42,6 +52,33 @@ var _ = Describe("AppInstaller", func() { Expect(install.InstallArgoCD()).To(Succeed()) Expect(argoCDInstall.called).To(BeTrue()) }) + + It("configures the pc-applications global image registry", func() { + scheme := runtime.NewScheme() + Expect(clientgoscheme.AddToScheme(scheme)).To(Succeed()) + Expect(argov1alpha1.AddToScheme(scheme)).To(Succeed()) + kubeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "argocd-codesphere-oci-read", Namespace: "argocd"}, + Data: map[string][]byte{"url": []byte("registry.example.com/mirror/codesphere-cloud/charts")}, + }).Build() + install := argocd.NewAppInstaller(argocd.AppInstallerConfig{ + Config: files.RootConfig{Registry: &files.RegistryConfig{Server: "registry.example.com/mirror"}}, + Vault: &files.InstallVault{}, + KubeClient: kubeClient, + }) + bomConfig := &bom.Config{Components: map[string]bom.ComponentConfig{ + "pc-applications": {Files: map[string]bom.FileRef{ + "chart": {OciRef: "oci://registry.example.com/mirror/codesphere-cloud/charts/pc-applications:1.2.3"}, + }}, + }} + + Expect(install.InstallPCApps(context.Background(), bomConfig)).To(Succeed()) + app := &argov1alpha1.Application{} + Expect(kubeClient.Get(context.Background(), client.ObjectKey{Name: "pc-applications", Namespace: "argocd"}, app)).To(Succeed()) + values := map[string]any{} + Expect(json.Unmarshal(app.Spec.Source.Helm.ValuesObject.Raw, &values)).To(Succeed()) + Expect(values).To(HaveKeyWithValue("global", map[string]any{"imageRegistry": "registry.example.com/mirror"})) + }) }) var _ = Describe("VaultAndRESTConfig", func() { diff --git a/internal/installer/bom/bom.go b/internal/installer/bom/bom.go index d631fc7a2..0b14f9ae9 100644 --- a/internal/installer/bom/bom.go +++ b/internal/installer/bom/bom.go @@ -96,6 +96,62 @@ func Parse(filePath string) (*Config, error) { return &cfg, nil } +// UseRegistry rewrites every image and OCI chart reference to registry while +// preserving its repository path, tag, or digest. +func (b *Config) UseRegistry(registry string) error { + registry = strings.TrimSuffix(strings.TrimPrefix(registry, "oci://"), "/") + if registry == "" { + return fmt.Errorf("registry must not be empty") + } + + rewrite := func(value string) (string, error) { + ociPrefix := "" + if strings.HasPrefix(value, "oci://") { + ociPrefix = "oci://" + } + ref, err := reference.ParseAnyReference(strings.TrimPrefix(value, "oci://")) + if err != nil { + return "", fmt.Errorf("invalid OCI reference %q: %w", value, err) + } + named, ok := ref.(reference.Named) + if !ok { + return "", fmt.Errorf("OCI reference %q has no repository name", value) + } + path := reference.Path(named) + suffix := "" + switch typed := ref.(type) { + case reference.Digested: + suffix = "@" + typed.Digest().String() + case reference.Tagged: + suffix = ":" + typed.Tag() + } + return ociPrefix + registry + "/" + path + suffix, nil + } + + for componentName, component := range b.Components { + for name, image := range component.ContainerImages { + rewritten, err := rewrite(image) + if err != nil { + return fmt.Errorf("component %q image %q: %w", componentName, name, err) + } + component.ContainerImages[name] = rewritten + } + for name, file := range component.Files { + if file.OciRef == "" { + continue + } + rewritten, err := rewrite(file.OciRef) + if err != nil { + return fmt.Errorf("component %q file %q: %w", componentName, name, err) + } + file.OciRef = rewritten + component.Files[name] = file + } + b.Components[componentName] = component + } + return nil +} + // GetPCApps returns the pc-applications chart version from the BOM by // parsing the tag out of the OCI image reference stored at // components["pc-applications"].files["chart"].ociRef. diff --git a/internal/installer/bom/bom_test.go b/internal/installer/bom/bom_test.go index b871ed737..02784cec5 100644 --- a/internal/installer/bom/bom_test.go +++ b/internal/installer/bom/bom_test.go @@ -264,4 +264,29 @@ var _ = Describe("Bom", func() { })) }) }) + + Describe("UseRegistry", func() { + It("rewrites images and OCI charts while preserving paths, tags, and digests", func() { + cfg := &bom.Config{Components: map[string]bom.ComponentConfig{ + "codesphere": { + ContainerImages: map[string]string{ + "api": "ghcr.io/codesphere-cloud/api:v1", + "worker": "ghcr.io/codesphere-cloud/worker@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + }, + Files: map[string]bom.FileRef{ + "chart": {OciRef: "oci://ghcr.io/codesphere-cloud/charts/codesphere:v1"}, + }, + }, + }} + + Expect(cfg.UseRegistry("oci://registry.example.com/mirror/")).To(Succeed()) + Expect(cfg.Components["codesphere"].ContainerImages["api"]).To(Equal("registry.example.com/mirror/codesphere-cloud/api:v1")) + Expect(cfg.Components["codesphere"].ContainerImages["worker"]).To(Equal("registry.example.com/mirror/codesphere-cloud/worker@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")) + Expect(cfg.Components["codesphere"].Files["chart"].OciRef).To(Equal("oci://registry.example.com/mirror/codesphere-cloud/charts/codesphere:v1")) + }) + + It("rejects an empty registry", func() { + Expect((&bom.Config{}).UseRegistry("")).To(MatchError("registry must not be empty")) + }) + }) }) diff --git a/internal/installer/files/config_yaml.go b/internal/installer/files/config_yaml.go index b0f3050f4..8851f7ae9 100644 --- a/internal/installer/files/config_yaml.go +++ b/internal/installer/files/config_yaml.go @@ -133,7 +133,7 @@ type SecretsConfig struct { } type RegistryConfig struct { - Server string `yaml:"server"` + Server string `yaml:"server,omitempty"` ReplaceImagesInBom bool `yaml:"replaceImagesInBom"` LoadContainerImages bool `yaml:"loadContainerImages"` } From 7c1eeb9c491902eee126b1aa8bf0c471a9209085 Mon Sep 17 00:00:00 2001 From: schrodit <7979201+schrodit@users.noreply.github.com> Date: Thu, 27 Aug 2026 12:50:19 +0000 Subject: [PATCH 3/3] chore(docs): Auto-update docs and licenses Signed-off-by: schrodit <7979201+schrodit@users.noreply.github.com> --- docs/oms_beta_bootstrap-gcp.md | 1 + docs/oms_beta_bootstrap-local.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/oms_beta_bootstrap-gcp.md b/docs/oms_beta_bootstrap-gcp.md index fd6e88e75..f600b67da 100644 --- a/docs/oms_beta_bootstrap-gcp.md +++ b/docs/oms_beta_bootstrap-gcp.md @@ -74,6 +74,7 @@ oms beta bootstrap-gcp [flags] --prometheus-remote-write-user string Prometheus remote write username (optional) --recover-config Recover previously generated install config from the jumpbox. This will overwrite the local config! (default: false) --region string GCP Region (default: europe-west4) (default "europe-west4") + --registry string Alternative container registry used for Codesphere images and charts --registry-type string Container registry type to use (options: local-container, artifact-registry) (default: local-container) (default "local-container") --registry-user string Custom Registry username (only for GitHub registry type) (optional) --remote-oms-binary string Path to a local Linux amd64 OMS binary to copy to and use on the jumpbox instead of downloading a release (optional) diff --git a/docs/oms_beta_bootstrap-local.md b/docs/oms_beta_bootstrap-local.md index 3dbb5d8c9..1669691e7 100644 --- a/docs/oms_beta_bootstrap-local.md +++ b/docs/oms_beta_bootstrap-local.md @@ -31,7 +31,7 @@ oms beta bootstrap-local [flags] --pod-cidr string Service CIDR of the Kubernetes cluster. If not specified, OMS will try to determine it. --preview-flags stringArray Preview flags to enable in Codesphere installation (optional) (default [openfga-authz,cluster-admin,secret-management,sub-path-mount,workspace-ssh]) --profile string Profile to apply to the install config like resources (supported: dev, minimal, prod) (default "dev") - --registry-url string OCI registry URL used for the ArgoCD helm pull secret (default "oci://ghcr.io/codesphere-cloud/charts") + --registry string Alternative container registry used for Codesphere images and charts --registry-user string Custom Registry username --secrets-file string Path to secrets file (default: /prod.vault.yaml) --service-cidr string Service CIDR of the Kubernetes cluster. If not specified, OMS will try to determine it.