From 7383df15cb12ac22ce2ee8057e4a7ace05dcadd9 Mon Sep 17 00:00:00 2001 From: Tim Schrodi Date: Wed, 26 Aug 2026 12:54:03 +0200 Subject: [PATCH 1/5] 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 3735ab17bd1abaf81903785b2f937b8322be9ed8 Mon Sep 17 00:00:00 2001 From: Tim Schrodi Date: Wed, 26 Aug 2026 14:03:56 +0200 Subject: [PATCH 2/5] 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 | 6 +- 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(+), 14 deletions(-) diff --git a/cli/cmd/bootstrap_gcp.go b/cli/cmd/bootstrap_gcp.go index 334158061..e0a0f4ac7 100644 --- a/cli/cmd/bootstrap_gcp.go +++ b/cli/cmd/bootstrap_gcp.go @@ -118,6 +118,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 9db6fedec..005904f75 100644 --- a/internal/bootstrap/gcp/gcp.go +++ b/internal/bootstrap/gcp/gcp.go @@ -1072,8 +1072,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 ea7224163..f8c8b2e68 100644 --- a/internal/bootstrap/gcp/gcp_test.go +++ b/internal/bootstrap/gcp/gcp_test.go @@ -988,13 +988,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 9d2547eba3ef3f106403c95d255c2943a94d45c2 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/5] 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 096b3081d..2375527a0 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 9551845c5..e7b16fdfa 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,virtual-machines]) --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. From 84c962ae4724a629e870ec5280553aa5dc3a62b5 Mon Sep 17 00:00:00 2001 From: Tim Schrodi Date: Wed, 9 Sep 2026 11:50:31 +0200 Subject: [PATCH 4/5] review --- cli/cmd/bootstrap_gcp.go | 16 +++-- .../install_codesphere_dependencies.go | 1 + docs/oms_beta_bootstrap-gcp.md | 4 +- internal/bootstrap/gcp/gcp.go | 50 ++++++++++----- internal/bootstrap/gcp/gcp_test.go | 51 ++++++++++++--- internal/bootstrap/gcp/install_config.go | 2 +- internal/bootstrap/local/local.go | 5 ++ .../installer/argocd/install_and_apps_test.go | 2 + internal/installer/argocd/installer.go | 1 + internal/installer/argocd/installer_test.go | 2 + internal/installer/bom/bom.go | 63 +++++++++++-------- 11 files changed, 140 insertions(+), 57 deletions(-) diff --git a/cli/cmd/bootstrap_gcp.go b/cli/cmd/bootstrap_gcp.go index e0a0f4ac7..21df5e9bf 100644 --- a/cli/cmd/bootstrap_gcp.go +++ b/cli/cmd/bootstrap_gcp.go @@ -117,7 +117,8 @@ func AddBootstrapGcpCmd(parent *cobra.Command, opts *util.GlobalOptions) { flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.InstallHash, "install-hash", "", "Codesphere package hash to install (default: none)") 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.RegistryUsername, "registry-user", "", "Username for direct registry access") + flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.RegistryPassword, "registry-password", "", "Password or token for direct access to an alternative registry") 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)") @@ -197,9 +198,14 @@ func (c *BootstrapGcpCmd) BootstrapGcp() error { c.CodesphereEnv.RegistryType = gcp.RegistryType(c.InputRegistryType) c.CodesphereEnv.OmsWorkdir = c.Env.GetOmsWorkdir() - if c.CodesphereEnv.GitHubPAT != "" { + if c.CodesphereEnv.ContainerRegistryURL != "" { + c.CodesphereEnv.RegistryType = gcp.RegistryTypeExternal + if c.CodesphereEnv.RegistryUsername == "" || c.CodesphereEnv.RegistryPassword == "" { + return fmt.Errorf("registry-user and registry-password must be set when using an alternative registry") + } + } else if c.CodesphereEnv.GitHubPAT != "" { c.CodesphereEnv.RegistryType = gcp.RegistryTypeGitHub - if c.CodesphereEnv.RegistryUser == "" { + if c.CodesphereEnv.RegistryUsername == "" { return fmt.Errorf("registry-user must be set when using GitHub registry type") } } @@ -239,8 +245,8 @@ func (c *BootstrapGcpCmd) BootstrapGcp() error { packageName := "-installer" installCmd := "oms install codesphere -c /etc/codesphere/config.yaml -k /etc/codesphere/secrets/age_key.txt --vault /etc/codesphere/secrets/prod.vault.yaml" - if gcp.RegistryType(bs.Env.RegistryType) == gcp.RegistryTypeGitHub { - log.Printf("You set a GitHub PAT for direct image access. Make sure to use a lite package, as VM root disk sizes are reduced.") + if gcp.RegistryType(bs.Env.RegistryType) == gcp.RegistryTypeGitHub || gcp.RegistryType(bs.Env.RegistryType) == gcp.RegistryTypeExternal { + log.Printf("You configured direct registry access. Make sure to use a lite package, as VM root disk sizes are reduced.") installCmd += " -s load-container-images" packageName += "-lite" diff --git a/cli/cmd/codesphere/install_codesphere_dependencies.go b/cli/cmd/codesphere/install_codesphere_dependencies.go index 04ea09329..ede25a676 100644 --- a/cli/cmd/codesphere/install_codesphere_dependencies.go +++ b/cli/cmd/codesphere/install_codesphere_dependencies.go @@ -118,6 +118,7 @@ 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://"), "/") diff --git a/docs/oms_beta_bootstrap-gcp.md b/docs/oms_beta_bootstrap-gcp.md index 2375527a0..8fb89df09 100644 --- a/docs/oms_beta_bootstrap-gcp.md +++ b/docs/oms_beta_bootstrap-gcp.md @@ -75,8 +75,9 @@ oms beta bootstrap-gcp [flags] --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-password string Password or token for direct access to an alternative registry --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) + --registry-user string Username for direct registry access --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) --root-disk-size int Instance root disk size in GB (default: 50) (default 50) --secrets-dir string Directory for secrets (default: /etc/codesphere/secrets) (default "/etc/codesphere/secrets") @@ -95,4 +96,3 @@ oms beta bootstrap-gcp [flags] * [oms beta bootstrap-gcp cleanup](oms_beta_bootstrap-gcp_cleanup.md) - Clean up GCP infrastructure created by bootstrap-gcp * [oms beta bootstrap-gcp postconfig](oms_beta_bootstrap-gcp_postconfig.md) - Run post-configuration steps for GCP bootstrapping * [oms beta bootstrap-gcp restart-vms](oms_beta_bootstrap-gcp_restart-vms.md) - Restart stopped or terminated GCP VMs - diff --git a/internal/bootstrap/gcp/gcp.go b/internal/bootstrap/gcp/gcp.go index 005904f75..a168c1688 100644 --- a/internal/bootstrap/gcp/gcp.go +++ b/internal/bootstrap/gcp/gcp.go @@ -48,6 +48,10 @@ const ( // bootstrapping only configures GitHub access and installs the lite // package. RegistryTypeGitHub RegistryType = "github" + + // RegistryTypeExternal pulls images directly from a user-provided registry + // with explicit credentials and installs the lite package. + RegistryTypeExternal RegistryType = "external" ) // CheckOMSManagedLabel checks if the given labels map indicates an OMS-managed project. @@ -161,7 +165,6 @@ type CodesphereEnvironment struct { GitHubAppName string `json:"-"` GitHubTeamOrg string `json:"github_team_org"` GitHubTeamSlug string `json:"github_team_slug"` - RegistryUser string `json:"-"` InternalFlags []string `json:"internal"` PreviewFlags []string `json:"preview"` FeatureFlags []string `json:"feature_flags"` @@ -355,8 +358,8 @@ func (b *GCPBootstrapper) Bootstrap() error { } } - if b.Env.RegistryType == RegistryTypeGitHub { - err = b.stlog.Step("Ensure GitHub access configured", b.EnsureGitHubAccessConfigured) + if b.Env.RegistryType == RegistryTypeGitHub || b.Env.RegistryType == RegistryTypeExternal { + err = b.stlog.Step("Ensure registry access configured", b.EnsureRegistryAccessConfigured) if err != nil { return fmt.Errorf("failed to update install config: %w", err) } @@ -551,7 +554,7 @@ func (b *GCPBootstrapper) validateInstallVersion() error { } requiredFilename := "installer.tar.gz" - if b.Env.RegistryType == RegistryTypeGitHub { + if b.Env.RegistryType == RegistryTypeGitHub || b.Env.RegistryType == RegistryTypeExternal { requiredFilename = "installer-lite.tar.gz" } @@ -1068,16 +1071,35 @@ func (b *GCPBootstrapper) EnsureLocalContainerRegistry() error { return nil } -func (b *GCPBootstrapper) EnsureGitHubAccessConfigured() error { - if b.Env.GitHubPAT == "" { - return fmt.Errorf("GitHub PAT is not set") - } - registryURL := strings.TrimSuffix(strings.TrimPrefix(b.Env.ContainerRegistryURL, "oci://"), "/") - if registryURL != "" { +// EnsureRegistryAccessConfigured stores credentials and configures direct access +// to either GitHub Container Registry or an explicitly selected external registry. +func (b *GCPBootstrapper) EnsureRegistryAccessConfigured() error { + registryPassword := b.Env.RegistryPassword + if b.Env.RegistryType == RegistryTypeGitHub { + if b.Env.GitHubPAT == "" { + return fmt.Errorf("GitHub PAT is not set") + } + + registryPassword = b.Env.GitHubPAT + } else { + registryURL := strings.TrimSuffix(strings.TrimPrefix(b.Env.ContainerRegistryURL, "oci://"), "/") + if registryURL == "" { + return fmt.Errorf("external registry URL is not set") + } + 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}}) + + if b.Env.RegistryUsername == "" { + return fmt.Errorf("registry username is not set") + } + + if registryPassword == "" { + return fmt.Errorf("registry password is not set") + } + + b.icg.GetVault().SetSecret(files.SecretEntry{Name: files.SecretRegistryUsername, Fields: &files.SecretFields{Password: b.Env.RegistryUsername}}) + b.icg.GetVault().SetSecret(files.SecretEntry{Name: files.SecretRegistryPassword, Fields: &files.SecretFields{Password: registryPassword}}) b.Env.InstallConfig.Registry.ReplaceImagesInBom = false b.Env.InstallConfig.Registry.LoadContainerImages = false @@ -1160,7 +1182,7 @@ func (b *GCPBootstrapper) codespherePackageFilename() string { } func (b *GCPBootstrapper) codespherePackageArchiveName() string { - if b.Env.RegistryType == RegistryTypeGitHub { + if b.Env.RegistryType == RegistryTypeGitHub || b.Env.RegistryType == RegistryTypeExternal { return "installer-lite.tar.gz" } @@ -1213,7 +1235,7 @@ func (b *GCPBootstrapper) generateSkipStepsArg() string { skipSteps := []string{"kubernetes"} skipSteps = util.AppendUnique(skipSteps, b.Env.InstallSkipSteps...) - if b.Env.RegistryType == RegistryTypeGitHub { + if b.Env.RegistryType == RegistryTypeGitHub || b.Env.RegistryType == RegistryTypeExternal { skipSteps = util.AppendUnique(skipSteps, "load-container-images") } diff --git a/internal/bootstrap/gcp/gcp_test.go b/internal/bootstrap/gcp/gcp_test.go index f8c8b2e68..4d2600726 100644 --- a/internal/bootstrap/gcp/gcp_test.go +++ b/internal/bootstrap/gcp/gcp_test.go @@ -977,30 +977,38 @@ var _ = Describe("GCP Bootstrapper", func() { }) }) - Describe("EnsureGitHubAccessConfigured", func() { + Describe("EnsureRegistryAccessConfigured", func() { BeforeEach(func() { csEnv.GitHubPAT = "fake-pat" - csEnv.RegistryUser = "custom-registry" + csEnv.RegistryUsername = "custom-registry" + csEnv.RegistryType = gcp.RegistryTypeGitHub }) It("sets configuration options in installconfig", func() { vault := &files.InstallVault{} icg.EXPECT().GetVault().Return(vault) - err := bs.EnsureGitHubAccessConfigured() + err := bs.EnsureRegistryAccessConfigured() Expect(err).NotTo(HaveOccurred()) Expect(bs.Env.InstallConfig.Registry.Server).To(BeEmpty()) - Expect(vault.GetSecret(files.SecretRegistryUsername).Fields.Password).To(Equal(csEnv.RegistryUser)) + Expect(vault.GetSecret(files.SecretRegistryUsername).Fields.Password).To(Equal(csEnv.RegistryUsername)) 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() { + It("uses explicit credentials for an external registry", func() { csEnv.ContainerRegistryURL = "oci://registry.example.com/mirror/" - icg.EXPECT().GetVault().Return(&files.InstallVault{}) + csEnv.RegistryType = gcp.RegistryTypeExternal + csEnv.RegistryPassword = "registry-password" + vault := &files.InstallVault{} + icg.EXPECT().GetVault().Return(vault) - Expect(bs.EnsureGitHubAccessConfigured()).To(Succeed()) + Expect(bs.EnsureRegistryAccessConfigured()).To(Succeed()) Expect(bs.Env.InstallConfig.Registry.Server).To(Equal("registry.example.com/mirror")) + Expect(vault.GetSecret(files.SecretRegistryUsername).Fields.Password).To(Equal(csEnv.RegistryUsername)) + Expect(vault.GetSecret(files.SecretRegistryPassword).Fields.Password).To(Equal(csEnv.RegistryPassword)) + Expect(bs.Env.InstallConfig.Registry.LoadContainerImages).To(BeFalse()) + Expect(bs.Env.InstallConfig.Registry.ReplaceImagesInBom).To(BeFalse()) }) Context("When GitHub PAT is missing", func() { @@ -1008,11 +1016,22 @@ var _ = Describe("GCP Bootstrapper", func() { csEnv.GitHubPAT = "" }) It("returns an error", func() { - err := bs.EnsureGitHubAccessConfigured() + err := bs.EnsureRegistryAccessConfigured() Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("GitHub PAT is not set")) }) }) + + Context("When an external registry password is missing", func() { + BeforeEach(func() { + csEnv.ContainerRegistryURL = "registry.example.com" + csEnv.RegistryType = gcp.RegistryTypeExternal + }) + + It("returns an error", func() { + Expect(bs.EnsureRegistryAccessConfigured()).To(MatchError("registry password is not set")) + }) + }) }) Describe("EnsureVPC", func() { @@ -1389,7 +1408,7 @@ var _ = Describe("GCP Bootstrapper", func() { Context("Direct GitHub access", func() { BeforeEach(func() { csEnv.GitHubPAT = "fake-pat" - csEnv.RegistryUser = "fake-user" + csEnv.RegistryUsername = "fake-user" csEnv.RegistryType = "github" }) It("downloads and installs lite package", func() { @@ -1405,6 +1424,20 @@ var _ = Describe("GCP Bootstrapper", func() { }) }) + Context("External registry access", func() { + BeforeEach(func() { + csEnv.RegistryType = gcp.RegistryTypeExternal + }) + + It("downloads and installs the lite package", func() { + nodeClient.EXPECT().RunCommand(mock.MatchedBy(jumpboxMatcher), "root", "oms download package -f installer-lite.tar.gz -H abc1234567890 v1.2.3").Return(nil) + nodeClient.EXPECT().RunCommand(mock.MatchedBy(jumpboxMatcher), "root", + "oms install codesphere -c /etc/codesphere/config.yaml -k /etc/codesphere/secrets/age_key.txt --vault /etc/codesphere/secrets/prod.vault.yaml -p v1.2.3-abc1234567890-installer-lite.tar.gz -s kubernetes,load-container-images").Return(nil) + + Expect(bs.InstallCodesphere()).To(Succeed()) + }) + }) + Context("without explicit hash", func() { BeforeEach(func() { // Simulate that ValidateInput has populated the hash diff --git a/internal/bootstrap/gcp/install_config.go b/internal/bootstrap/gcp/install_config.go index 75132dc15..5de84d895 100644 --- a/internal/bootstrap/gcp/install_config.go +++ b/internal/bootstrap/gcp/install_config.go @@ -124,7 +124,7 @@ func (b *GCPBootstrapper) UpdateInstallConfig() error { b.Env.InstallConfig.Datacenter.CountryCode = "DE" b.Env.InstallConfig.Secrets.BaseDir = b.Env.SecretsDir - if b.Env.RegistryType != RegistryTypeGitHub { + if b.Env.RegistryType != RegistryTypeGitHub && b.Env.RegistryType != RegistryTypeExternal { b.Env.InstallConfig.Registry.ReplaceImagesInBom = true b.Env.InstallConfig.Registry.LoadContainerImages = true } diff --git a/internal/bootstrap/local/local.go b/internal/bootstrap/local/local.go index b197e908c..e528c3c48 100644 --- a/internal/bootstrap/local/local.go +++ b/internal/bootstrap/local/local.go @@ -240,6 +240,7 @@ 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" @@ -520,17 +521,21 @@ 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) diff --git a/internal/installer/argocd/install_and_apps_test.go b/internal/installer/argocd/install_and_apps_test.go index 6e37bae12..512ad6e58 100644 --- a/internal/installer/argocd/install_and_apps_test.go +++ b/internal/installer/argocd/install_and_apps_test.go @@ -73,8 +73,10 @@ var _ = Describe("AppInstaller", func() { }} 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"})) diff --git a/internal/installer/argocd/installer.go b/internal/installer/argocd/installer.go index ab3b501a0..3535ad5a7 100644 --- a/internal/installer/argocd/installer.go +++ b/internal/installer/argocd/installer.go @@ -93,6 +93,7 @@ func NewInstaller(cfg InstallerConfig) (*Installer, error) { 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() diff --git a/internal/installer/argocd/installer_test.go b/internal/installer/argocd/installer_test.go index dbc6df0ed..527a930c5 100644 --- a/internal/installer/argocd/installer_test.go +++ b/internal/installer/argocd/installer_test.go @@ -187,7 +187,9 @@ var _ = Describe("Installer.Install", func() { 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 diff --git a/internal/installer/bom/bom.go b/internal/installer/bom/bom.go index 0b14f9ae9..a9ec753b0 100644 --- a/internal/installer/bom/bom.go +++ b/internal/installer/bom/bom.go @@ -104,54 +104,65 @@ func (b *Config) UseRegistry(registry string) error { 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) + rewritten, err := rewriteRegistry(image, registry) 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) + + rewritten, err := rewriteRegistry(file.OciRef, registry) 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 } +func rewriteRegistry(value, registry 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 +} + // 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. From a485b618fbe2e38d3f69a82faa48832d90cd4db3 Mon Sep 17 00:00:00 2001 From: schrodit <7979201+schrodit@users.noreply.github.com> Date: Wed, 9 Sep 2026 11:07:32 +0000 Subject: [PATCH 5/5] chore(docs): Auto-update docs and licenses Signed-off-by: schrodit <7979201+schrodit@users.noreply.github.com> --- docs/oms_beta_bootstrap-gcp.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/oms_beta_bootstrap-gcp.md b/docs/oms_beta_bootstrap-gcp.md index 8fb89df09..1e8a084d3 100644 --- a/docs/oms_beta_bootstrap-gcp.md +++ b/docs/oms_beta_bootstrap-gcp.md @@ -96,3 +96,4 @@ oms beta bootstrap-gcp [flags] * [oms beta bootstrap-gcp cleanup](oms_beta_bootstrap-gcp_cleanup.md) - Clean up GCP infrastructure created by bootstrap-gcp * [oms beta bootstrap-gcp postconfig](oms_beta_bootstrap-gcp_postconfig.md) - Run post-configuration steps for GCP bootstrapping * [oms beta bootstrap-gcp restart-vms](oms_beta_bootstrap-gcp_restart-vms.md) - Restart stopped or terminated GCP VMs +