From a866e1473a08ef19faf9a364471c91b11ff18173 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 d582f540f..f788c9c7e 100644 --- a/cli/cmd/codesphere/install_codesphere_dependencies.go +++ b/cli/cmd/codesphere/install_codesphere_dependencies.go @@ -146,6 +146,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 6c9ba446f..cd7659241 100644 --- a/internal/bootstrap/local/local.go +++ b/internal/bootstrap/local/local.go @@ -239,13 +239,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 a1acc9ed8a8065ffcfaed0d8a446a2e9751607f9 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_test.go | 10 +++- internal/bootstrap/gcp/registry.go | 7 ++- 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, 179 insertions(+), 13 deletions(-) diff --git a/cli/cmd/bootstrap_gcp.go b/cli/cmd/bootstrap_gcp.go index d59d940b8..134afc0d3 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", "github", "Container registry type to use (options: local-container, artifact-registry, github) (default: github)") 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 04c3a4be1..07aef7ac0 100644 --- a/cli/cmd/bootstrap_local.go +++ b/cli/cmd/bootstrap_local.go @@ -80,6 +80,7 @@ func AddBootstrapLocalCmd(parent *cobra.Command, opts *util.GlobalOptions) { 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") @@ -100,8 +101,6 @@ func AddBootstrapLocalCmd(parent *cobra.Command, opts *util.GlobalOptions) { 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 f788c9c7e..98b80c743 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" @@ -118,6 +119,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 @@ -134,8 +144,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_test.go b/internal/bootstrap/gcp/gcp_test.go index b37564107..56b742744 100644 --- a/internal/bootstrap/gcp/gcp_test.go +++ b/internal/bootstrap/gcp/gcp_test.go @@ -1055,13 +1055,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/gcp/registry.go b/internal/bootstrap/gcp/registry.go index 4ccb67b1f..f8d737b93 100644 --- a/internal/bootstrap/gcp/registry.go +++ b/internal/bootstrap/gcp/registry.go @@ -188,7 +188,12 @@ func (b *GCPBootstrapper) EnsureGitHubAccessConfigured() error { } registry := b.Env.InstallConfig.EnsureRegistry() - registry.Server = "ghcr.io" + registryURL := strings.TrimSuffix(strings.TrimPrefix(b.Env.ContainerRegistryURL, "oci://"), "/") + if registryURL != "" { + registry.Server = registryURL + } else { + registry.Server = "ghcr.io" + } registry.ReplaceImagesInBom = false registry.LoadContainerImages = false diff --git a/internal/bootstrap/local/local.go b/internal/bootstrap/local/local.go index cd7659241..9f5834a7a 100644 --- a/internal/bootstrap/local/local.go +++ b/internal/bootstrap/local/local.go @@ -86,8 +86,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:"-"` @@ -100,8 +101,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. @@ -243,12 +242,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, @@ -519,6 +522,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 } @@ -696,7 +715,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 ade9cb36c..e39c00b94 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 afbad2fa2d2be75baedb121532f7e0c4de0cebcb 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 | 2 +- docs/oms_beta_bootstrap-local.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/oms_beta_bootstrap-gcp.md b/docs/oms_beta_bootstrap-gcp.md index 072855440..e024f7710 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, github) (default: github) (default "github") --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) @@ -100,4 +101,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/docs/oms_beta_bootstrap-local.md b/docs/oms_beta_bootstrap-local.md index be2f3aeec..7774f02d7 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 977a0aba4d1ff6e27e8647d6a6d25cdd084513db 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 | 18 +++++- .../install_codesphere_dependencies.go | 1 + docs/oms_beta_bootstrap-gcp.md | 3 +- internal/bootstrap/gcp/gcp.go | 7 +-- internal/bootstrap/gcp/gcp_test.go | 59 +++++++++++++---- internal/bootstrap/gcp/install_config.go | 2 +- internal/bootstrap/gcp/registry.go | 54 +++++++++++----- 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 +++++++++++-------- 12 files changed, 153 insertions(+), 64 deletions(-) diff --git a/cli/cmd/bootstrap_gcp.go b/cli/cmd/bootstrap_gcp.go index 134afc0d3..5e19b3b95 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", "github", "Container registry type to use (options: local-container, artifact-registry, github) (default: github)") flags.StringArrayVar(&bootstrapGcpCmd.CodesphereEnv.InternalFlags, "internal-flags", gcp.DefaultInternalFlags, "Internal flags to enable in Codesphere installation (optional)") @@ -197,6 +198,17 @@ func (c *BootstrapGcpCmd) BootstrapGcp() error { c.CodesphereEnv.RegistryType = gcp.RegistryType(c.InputRegistryType) c.CodesphereEnv.OmsWorkdir = c.Env.GetOmsWorkdir() + 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.RegistryUsername == "" { + return fmt.Errorf("registry-user must be set when using GitHub registry type") + } + } if c.cmd.Flags().Changed("experiments") { if c.cmd.Flags().Changed("internal-flags") { @@ -232,8 +244,8 @@ func (c *BootstrapGcpCmd) BootstrapGcp() error { 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("Images are pulled directly from GHCR, so container images are not loaded from the package.") + 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" } diff --git a/cli/cmd/codesphere/install_codesphere_dependencies.go b/cli/cmd/codesphere/install_codesphere_dependencies.go index 98b80c743..f960e5246 100644 --- a/cli/cmd/codesphere/install_codesphere_dependencies.go +++ b/cli/cmd/codesphere/install_codesphere_dependencies.go @@ -119,6 +119,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 e024f7710..91bbb3869 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, github) (default: github) (default "github") - --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") diff --git a/internal/bootstrap/gcp/gcp.go b/internal/bootstrap/gcp/gcp.go index 5e8b12e40..ffa377288 100644 --- a/internal/bootstrap/gcp/gcp.go +++ b/internal/bootstrap/gcp/gcp.go @@ -163,7 +163,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"` @@ -369,8 +368,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) } @@ -1084,7 +1083,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 56b742744..c4a98588a 100644 --- a/internal/bootstrap/gcp/gcp_test.go +++ b/internal/bootstrap/gcp/gcp_test.go @@ -171,7 +171,7 @@ var _ = Describe("GCP Bootstrapper", func() { BeforeEach(func() { csEnv.RegistryType = gcp.RegistryTypeGitHub csEnv.GitHubPAT = "fake-pat" - csEnv.RegistryUser = "fake-registry-user" + csEnv.RegistryUsername = "fake-registry-user" }) It("accepts full GitHub credentials", func() { @@ -185,7 +185,7 @@ var _ = Describe("GCP Bootstrapper", func() { }) It("rejects a missing registry user", func() { - csEnv.RegistryUser = "" + csEnv.RegistryUsername = "" Expect(bs.ValidateInput()).To(MatchError(ContainSubstring("registry-user must be set"))) }) @@ -433,7 +433,7 @@ var _ = Describe("GCP Bootstrapper", func() { BeforeEach(func() { csEnv.RegistryType = gcp.RegistryTypeGitHub csEnv.GitHubPAT = "fake-pat" - csEnv.RegistryUser = "fake-registry-user" + csEnv.RegistryUsername = "fake-registry-user" }) Context("when GitHub arguments are partially set", func() { @@ -1044,30 +1044,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(bs.Env.InstallConfig.Registry.Server).To(Equal("ghcr.io")) + 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() { @@ -1075,11 +1083,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() { @@ -1564,7 +1583,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() { @@ -1580,6 +1599,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 d8b4662a7..28d9dad56 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/gcp/registry.go b/internal/bootstrap/gcp/registry.go index f8d737b93..0c5d69a1d 100644 --- a/internal/bootstrap/gcp/registry.go +++ b/internal/bootstrap/gcp/registry.go @@ -18,11 +18,13 @@ type RegistryType string // The registry types supported by the GCP bootstrapper. RegistryTypeLocalContainer runs a // local registry, RegistryTypeArtifactRegistry uses a GCP Artifact Registry -// repository, and RegistryTypeGitHub pulls straight from ghcr.io. +// repository, RegistryTypeGitHub pulls straight from ghcr.io, and RegistryTypeExternal pulls +// from a user-provided registry with explicit credentials. const ( RegistryTypeLocalContainer RegistryType = "local-container" RegistryTypeArtifactRegistry RegistryType = "artifact-registry" RegistryTypeGitHub RegistryType = "github" + RegistryTypeExternal RegistryType = "external" ) // validateGitHubParams checks if the GitHub credentials are fully specified if GitHub registry is selected @@ -55,13 +57,19 @@ func (b *GCPBootstrapper) validateRegistryParams() error { return fmt.Errorf("github-pat must be set when using GitHub registry type") } - if b.Env.RegistryUser == "" { + if b.Env.RegistryUsername == "" { return fmt.Errorf("registry-user must be set when using GitHub registry type") } + return nil + case RegistryTypeExternal: + if b.Env.ContainerRegistryURL == "" || b.Env.RegistryUsername == "" || b.Env.RegistryPassword == "" { + return fmt.Errorf("registry, registry-user and registry-password must be set when using an external registry") + } + return nil default: - return fmt.Errorf("unsupported registry type %q (supported: local-container, artifact-registry, github)", b.Env.RegistryType) + return fmt.Errorf("unsupported registry type %q (supported: local-container, artifact-registry, github, external)", b.Env.RegistryType) } } @@ -180,25 +188,39 @@ func (b *GCPBootstrapper) EnsureLocalContainerRegistry() error { return nil } -// EnsureGitHubAccessConfigured points the install config at ghcr.io and stores the GitHub -// credentials in the vault. The cluster pulls images from GHCR directly -func (b *GCPBootstrapper) EnsureGitHubAccessConfigured() error { - if b.Env.GitHubPAT == "" { - return fmt.Errorf("GitHub PAT is not set") - } - +// EnsureRegistryAccessConfigured stores credentials and configures direct access to either +// GitHub Container Registry or an explicitly selected external registry. +func (b *GCPBootstrapper) EnsureRegistryAccessConfigured() error { registry := b.Env.InstallConfig.EnsureRegistry() - registryURL := strings.TrimSuffix(strings.TrimPrefix(b.Env.ContainerRegistryURL, "oci://"), "/") - if registryURL != "" { - registry.Server = registryURL - } else { + 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 registry.Server = "ghcr.io" + } else { + registryURL := strings.TrimSuffix(strings.TrimPrefix(b.Env.ContainerRegistryURL, "oci://"), "/") + if registryURL == "" { + return fmt.Errorf("external registry URL is not set") + } + + registry.Server = registryURL } + + if b.Env.RegistryUsername == "" { + return fmt.Errorf("registry username is not set") + } + if registryPassword == "" { + return fmt.Errorf("registry password is not set") + } + registry.ReplaceImagesInBom = false registry.LoadContainerImages = false - 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.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}}) return nil } diff --git a/internal/bootstrap/local/local.go b/internal/bootstrap/local/local.go index 9f5834a7a..486e0e836 100644 --- a/internal/bootstrap/local/local.go +++ b/internal/bootstrap/local/local.go @@ -242,6 +242,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" @@ -522,17 +523,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 e8e1cf259b1da13fcf24690eb36f2bed43d6794e 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 91bbb3869..62c5fb1ea 100644 --- a/docs/oms_beta_bootstrap-gcp.md +++ b/docs/oms_beta_bootstrap-gcp.md @@ -102,3 +102,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 +