diff --git a/cli/cmd/init_install_config.go b/cli/cmd/init_install_config.go index 0be2637f..bff14f71 100644 --- a/cli/cmd/init_install_config.go +++ b/cli/cmd/init_install_config.go @@ -464,24 +464,25 @@ func (c *InitInstallConfigCmd) updateConfigFromOpts(config *files.RootConfig, va // ACME configuration if c.Opts.ACMEEnabled { - if config.Codesphere.CertIssuer.Acme == nil { - config.Codesphere.CertIssuer.Acme = &files.ACMEConfig{} + certIssuer := config.Codesphere.EnsureCertIssuer() + if certIssuer.Acme == nil { + certIssuer.Acme = &files.ACMEConfig{} } - config.Codesphere.CertIssuer.Type = files.CertIssuerTypeACME - config.Codesphere.CertIssuer.Acme.Enabled = true + certIssuer.Type = files.CertIssuerTypeACME + certIssuer.Acme.Enabled = true if c.Opts.ACMEIssuerName != "" { - config.Codesphere.CertIssuer.Acme.Name = c.Opts.ACMEIssuerName + certIssuer.Acme.Name = c.Opts.ACMEIssuerName } if c.Opts.ACMEEmail != "" { - config.Codesphere.CertIssuer.Acme.Email = c.Opts.ACMEEmail + certIssuer.Acme.Email = c.Opts.ACMEEmail } if c.Opts.ACMEServer != "" { - config.Codesphere.CertIssuer.Acme.Server = c.Opts.ACMEServer + certIssuer.Acme.Server = c.Opts.ACMEServer } if c.Opts.ACMEEABKeyID != "" { - config.Codesphere.CertIssuer.Acme.EABKeyID = c.Opts.ACMEEABKeyID + certIssuer.Acme.EABKeyID = c.Opts.ACMEEABKeyID } if c.Opts.ACMEEABMacKey != "" { vault.SetSecret(files.SecretEntry{Name: files.SecretAcmeEabMacKey, Fields: &files.SecretFields{Password: c.Opts.ACMEEABMacKey}}) @@ -489,7 +490,7 @@ func (c *InitInstallConfigCmd) updateConfigFromOpts(config *files.RootConfig, va // Configure DNS-01 solver if c.Opts.ACMEDNS01Provider != "" { - config.Codesphere.CertIssuer.Acme.Solver.DNS01 = &files.ACMEDNS01Solver{ + certIssuer.Acme.Solver.DNS01 = &files.ACMEDNS01Solver{ Provider: c.Opts.ACMEDNS01Provider, } } diff --git a/cli/cmd/update_install_config.go b/cli/cmd/update_install_config.go index 347ba0ae..c36224ed 100644 --- a/cli/cmd/update_install_config.go +++ b/cli/cmd/update_install_config.go @@ -287,43 +287,44 @@ func (c *UpdateInstallConfigCmd) applyACMEUpdates(config *files.RootConfig, vaul } acmeChanged := false - if config.Codesphere.CertIssuer.Acme == nil { - config.Codesphere.CertIssuer.Acme = &files.ACMEConfig{} + certIssuer := config.Codesphere.EnsureCertIssuer() + if certIssuer.Acme == nil { + certIssuer.Acme = &files.ACMEConfig{} } - if config.Codesphere.CertIssuer.Type != files.CertIssuerTypeACME { + if certIssuer.Type != files.CertIssuerTypeACME { log.Printf("Setting cert issuer type to ACME\n") - config.Codesphere.CertIssuer.Type = files.CertIssuerTypeACME + certIssuer.Type = files.CertIssuerTypeACME acmeChanged = true } - if !config.Codesphere.CertIssuer.Acme.Enabled { + if !certIssuer.Acme.Enabled { log.Printf("Enabling ACME certificate issuer\n") - config.Codesphere.CertIssuer.Acme.Enabled = true + certIssuer.Acme.Enabled = true acmeChanged = true } - if c.Opts.ACMEIssuerName != "" && config.Codesphere.CertIssuer.Acme.Name != c.Opts.ACMEIssuerName { - log.Printf("Updating ACME issuer name: %s -> %s\n", config.Codesphere.CertIssuer.Acme.Name, c.Opts.ACMEIssuerName) - config.Codesphere.CertIssuer.Acme.Name = c.Opts.ACMEIssuerName + if c.Opts.ACMEIssuerName != "" && certIssuer.Acme.Name != c.Opts.ACMEIssuerName { + log.Printf("Updating ACME issuer name: %s -> %s\n", certIssuer.Acme.Name, c.Opts.ACMEIssuerName) + certIssuer.Acme.Name = c.Opts.ACMEIssuerName acmeChanged = true } - if c.Opts.ACMEEmail != "" && config.Codesphere.CertIssuer.Acme.Email != c.Opts.ACMEEmail { - log.Printf("Updating ACME email: %s -> %s\n", config.Codesphere.CertIssuer.Acme.Email, c.Opts.ACMEEmail) - config.Codesphere.CertIssuer.Acme.Email = c.Opts.ACMEEmail + if c.Opts.ACMEEmail != "" && certIssuer.Acme.Email != c.Opts.ACMEEmail { + log.Printf("Updating ACME email: %s -> %s\n", certIssuer.Acme.Email, c.Opts.ACMEEmail) + certIssuer.Acme.Email = c.Opts.ACMEEmail acmeChanged = true } - if c.Opts.ACMEServer != "" && config.Codesphere.CertIssuer.Acme.Server != c.Opts.ACMEServer { - log.Printf("Updating ACME server: %s -> %s\n", config.Codesphere.CertIssuer.Acme.Server, c.Opts.ACMEServer) - config.Codesphere.CertIssuer.Acme.Server = c.Opts.ACMEServer + if c.Opts.ACMEServer != "" && certIssuer.Acme.Server != c.Opts.ACMEServer { + log.Printf("Updating ACME server: %s -> %s\n", certIssuer.Acme.Server, c.Opts.ACMEServer) + certIssuer.Acme.Server = c.Opts.ACMEServer acmeChanged = true } - if c.Opts.ACMEEABKeyID != "" && config.Codesphere.CertIssuer.Acme.EABKeyID != c.Opts.ACMEEABKeyID { - log.Printf("Updating ACME EAB key ID: %s -> %s\n", config.Codesphere.CertIssuer.Acme.EABKeyID, c.Opts.ACMEEABKeyID) - config.Codesphere.CertIssuer.Acme.EABKeyID = c.Opts.ACMEEABKeyID + if c.Opts.ACMEEABKeyID != "" && certIssuer.Acme.EABKeyID != c.Opts.ACMEEABKeyID { + log.Printf("Updating ACME EAB key ID: %s -> %s\n", certIssuer.Acme.EABKeyID, c.Opts.ACMEEABKeyID) + certIssuer.Acme.EABKeyID = c.Opts.ACMEEABKeyID acmeChanged = true } @@ -341,13 +342,13 @@ func (c *UpdateInstallConfigCmd) applyACMEUpdates(config *files.RootConfig, vaul // Update DNS-01 solver configuration if c.Opts.ACMEDNS01Provider != "" { - if config.Codesphere.CertIssuer.Acme.Solver.DNS01 == nil { - config.Codesphere.CertIssuer.Acme.Solver.DNS01 = &files.ACMEDNS01Solver{} + if certIssuer.Acme.Solver.DNS01 == nil { + certIssuer.Acme.Solver.DNS01 = &files.ACMEDNS01Solver{} } - if config.Codesphere.CertIssuer.Acme.Solver.DNS01.Provider != c.Opts.ACMEDNS01Provider { + if certIssuer.Acme.Solver.DNS01.Provider != c.Opts.ACMEDNS01Provider { log.Printf("Updating ACME DNS-01 provider: %s -> %s\n", - config.Codesphere.CertIssuer.Acme.Solver.DNS01.Provider, c.Opts.ACMEDNS01Provider) - config.Codesphere.CertIssuer.Acme.Solver.DNS01.Provider = c.Opts.ACMEDNS01Provider + certIssuer.Acme.Solver.DNS01.Provider, c.Opts.ACMEDNS01Provider) + certIssuer.Acme.Solver.DNS01.Provider = c.Opts.ACMEDNS01Provider acmeChanged = true } } diff --git a/internal/bootstrap/gcp/install_config.go b/internal/bootstrap/gcp/install_config.go index 83ebdf0a..f3338a50 100644 --- a/internal/bootstrap/gcp/install_config.go +++ b/internal/bootstrap/gcp/install_config.go @@ -250,7 +250,7 @@ func (b *GCPBootstrapper) UpdateInstallConfig() error { acmeConfig.EABKeyID = keyID b.icg.GetVault().SetSecret(files.SecretEntry{Name: files.SecretAcmeEabMacKey, Fields: &files.SecretFields{Password: b64MacKey}}) } - b.Env.InstallConfig.Codesphere.CertIssuer = files.CertIssuerConfig{ + b.Env.InstallConfig.Codesphere.CertIssuer = &files.CertIssuerConfig{ Type: "acme", Acme: acmeConfig, } diff --git a/internal/bootstrap/gcp/install_config_test.go b/internal/bootstrap/gcp/install_config_test.go index 6ccd50d0..c7391dd7 100644 --- a/internal/bootstrap/gcp/install_config_test.go +++ b/internal/bootstrap/gcp/install_config_test.go @@ -774,6 +774,7 @@ var _ = Describe("Installconfig & Secrets", func() { err := bs.UpdateInstallConfig() Expect(err).NotTo(HaveOccurred()) + Expect(bs.Env.InstallConfig.Codesphere.CertIssuer).NotTo(BeNil()) Expect(bs.Env.InstallConfig.Codesphere.CertIssuer.Acme.Server).To(Equal("https://dv.acme-v02.api.pki.goog/directory")) Expect(bs.Env.InstallConfig.Codesphere.CertIssuer.Acme.EABKeyID).To(Equal("fake-eab-key-id")) Expect(vault.GetSecret(files.SecretAcmeEabMacKey).Fields.Password).To(Equal("fake-eab-mac-key")) @@ -804,6 +805,7 @@ var _ = Describe("Installconfig & Secrets", func() { err := bs.UpdateInstallConfig() Expect(err).NotTo(HaveOccurred()) + Expect(bs.Env.InstallConfig.Codesphere.CertIssuer).NotTo(BeNil()) Expect(bs.Env.InstallConfig.Codesphere.CertIssuer.Acme.Server).To(Equal("https://acme-staging-v02.api.letsencrypt.org/directory")) }) }) diff --git a/internal/bootstrap/local/local.go b/internal/bootstrap/local/local.go index 8be7f17b..74bcf648 100644 --- a/internal/bootstrap/local/local.go +++ b/internal/bootstrap/local/local.go @@ -628,8 +628,10 @@ func (b *LocalBootstrapper) UpdateInstallConfig() (err error) { b.Env.InstallConfig.Cluster.PublicGateway.ServiceType = "LoadBalancer" // TODO: certificates - b.Env.InstallConfig.Codesphere.CertIssuer = files.CertIssuerConfig{ - Type: "self-signed", + if b.Env.InstallConfig.Codesphere.CertIssuer == nil { + b.Env.InstallConfig.Codesphere.CertIssuer = &files.CertIssuerConfig{ + Type: "self-signed", + } } b.Env.InstallConfig.Codesphere.Domain = b.Env.BaseDomain diff --git a/internal/installer/config_generator_collector.go b/internal/installer/config_generator_collector.go index 4920890b..095ab86c 100644 --- a/internal/installer/config_generator_collector.go +++ b/internal/installer/config_generator_collector.go @@ -215,46 +215,48 @@ func (g *InstallConfig) collectMetalLBConfig(prompter *Prompter) { func (g *InstallConfig) collectACMEConfig(prompter *Prompter) { log.Println("\n=== ACME Certificate Configuration (Optional) ===") + certIssuer := g.Config.Codesphere.EnsureCertIssuer() + // Initialize ACME config if it doesn't exist - if g.Config.Codesphere.CertIssuer.Acme == nil { - g.Config.Codesphere.CertIssuer.Acme = &files.ACMEConfig{} + if certIssuer.Acme == nil { + certIssuer.Acme = &files.ACMEConfig{} } - g.Config.Codesphere.CertIssuer.Acme.Enabled = prompter.Bool("Enable ACME certificate issuer (e.g., Let's Encrypt)", g.Config.Codesphere.CertIssuer.Acme.Enabled) + certIssuer.Acme.Enabled = prompter.Bool("Enable ACME certificate issuer (e.g., Let's Encrypt)", certIssuer.Acme.Enabled) // Early exit if ACME is disabled - if !g.Config.Codesphere.CertIssuer.Acme.Enabled { - g.Config.Codesphere.CertIssuer.Acme = nil - g.Config.Codesphere.CertIssuer.Type = files.CertIssuerTypeSelfSigned + if !certIssuer.Acme.Enabled { + certIssuer.Acme = nil + certIssuer.Type = files.CertIssuerTypeSelfSigned return } - g.Config.Codesphere.CertIssuer.Type = files.CertIssuerTypeACME + certIssuer.Type = files.CertIssuerTypeACME - defaultIssuerName := g.Config.Codesphere.CertIssuer.Acme.Name + defaultIssuerName := certIssuer.Acme.Name if defaultIssuerName == "" { defaultIssuerName = "acme-issuer" } - g.Config.Codesphere.CertIssuer.Acme.Name = g.collectString(prompter, "ACME issuer name", defaultIssuerName) + certIssuer.Acme.Name = g.collectString(prompter, "ACME issuer name", defaultIssuerName) - defaultEmail := g.Config.Codesphere.CertIssuer.Acme.Email + defaultEmail := certIssuer.Acme.Email if defaultEmail == "" { defaultEmail = "admin@example.com" } - g.Config.Codesphere.CertIssuer.Acme.Email = g.collectString(prompter, "Email address for ACME account registration", defaultEmail) + certIssuer.Acme.Email = g.collectString(prompter, "Email address for ACME account registration", defaultEmail) - defaultServer := g.Config.Codesphere.CertIssuer.Acme.Server + defaultServer := certIssuer.Acme.Server if defaultServer == "" { defaultServer = "https://acme-v02.api.letsencrypt.org/directory" } - g.Config.Codesphere.CertIssuer.Acme.Server = g.collectString(prompter, "ACME server URL", defaultServer) + certIssuer.Acme.Server = g.collectString(prompter, "ACME server URL", defaultServer) // External Account Binding (EAB) log.Println("\n--- External Account Binding (Optional) ---") - hasEAB := prompter.Bool("Configure External Account Binding (required by some ACME CAs)", g.Config.Codesphere.CertIssuer.Acme.EABKeyID != "") + hasEAB := prompter.Bool("Configure External Account Binding (required by some ACME CAs)", certIssuer.Acme.EABKeyID != "") - g.Config.Codesphere.CertIssuer.Acme.EABKeyID = "" + certIssuer.Acme.EABKeyID = "" if hasEAB { - g.Config.Codesphere.CertIssuer.Acme.EABKeyID = g.collectString(prompter, "EAB Key ID", g.Config.Codesphere.CertIssuer.Acme.EABKeyID) + certIssuer.Acme.EABKeyID = g.collectString(prompter, "EAB Key ID", certIssuer.Acme.EABKeyID) existingEabKey := "" if g.Vault != nil { if s := g.Vault.GetSecret(files.SecretAcmeEabMacKey); s != nil && s.Fields != nil { @@ -272,21 +274,21 @@ func (g *InstallConfig) collectACMEConfig(prompter *Prompter) { // DNS-01 Challenge Configuration log.Println("\n--- DNS-01 Challenge Configuration (Optional) ---") - if g.Config.Codesphere.CertIssuer.Acme.Solver.DNS01 == nil { - g.Config.Codesphere.CertIssuer.Acme.Solver.DNS01 = &files.ACMEDNS01Solver{} + if certIssuer.Acme.Solver.DNS01 == nil { + certIssuer.Acme.Solver.DNS01 = &files.ACMEDNS01Solver{} } - useDNS01 := prompter.Bool("Configure DNS-01 challenge solver", g.Config.Codesphere.CertIssuer.Acme.Solver.DNS01.Provider != "") + useDNS01 := prompter.Bool("Configure DNS-01 challenge solver", certIssuer.Acme.Solver.DNS01.Provider != "") if !useDNS01 { - g.Config.Codesphere.CertIssuer.Acme.Solver.DNS01 = nil + certIssuer.Acme.Solver.DNS01 = nil return } providerOptions := []string{"route53", "cloudflare", "azure", "gcp", "other"} - defaultProvider := g.Config.Codesphere.CertIssuer.Acme.Solver.DNS01.Provider + defaultProvider := certIssuer.Acme.Solver.DNS01.Provider if defaultProvider == "" { defaultProvider = "cloudflare" } - g.Config.Codesphere.CertIssuer.Acme.Solver.DNS01.Provider = g.collectChoice(prompter, "DNS provider", providerOptions, defaultProvider) + certIssuer.Acme.Solver.DNS01.Provider = g.collectChoice(prompter, "DNS provider", providerOptions, defaultProvider) log.Println("Note: Additional DNS provider configuration will need to be added to the vault file.") log.Println("Provider config and secrets should be added manually after generation.") } diff --git a/internal/installer/files/config_yaml.go b/internal/installer/files/config_yaml.go index 51f041f6..7be3e6ec 100644 --- a/internal/installer/files/config_yaml.go +++ b/internal/installer/files/config_yaml.go @@ -299,7 +299,7 @@ type CodesphereConfig struct { Domain string `yaml:"domain"` WorkspaceHostingBaseDomain string `yaml:"workspaceHostingBaseDomain"` PublicIP string `yaml:"publicIp"` - CertIssuer CertIssuerConfig `yaml:"certIssuer"` + CertIssuer *CertIssuerConfig `yaml:"certIssuer,omitempty"` CustomDomains CustomDomainsConfig `yaml:"customDomains"` DNSServers []string `yaml:"dnsServers"` Internal []string `yaml:"internal"` @@ -646,6 +646,13 @@ func NewRootConfig() RootConfig { } } +func (c *CodesphereConfig) EnsureCertIssuer() *CertIssuerConfig { + if c.CertIssuer == nil { + c.CertIssuer = &CertIssuerConfig{} + } + return c.CertIssuer +} + func (c *RootConfig) ExtractBomRefs() []string { var bomRefs []string for _, imageConfig := range c.Codesphere.DeployConfig.Images { @@ -671,7 +678,7 @@ func Capitalize(s string) string { // configuration from codesphere.certIssuer.acme.solver, matching the documented // config.yaml structure. func (c *RootConfig) buildACMEOverride() { - if c.Codesphere.CertIssuer.Acme == nil || c.Codesphere.CertIssuer.Acme.Solver.DNS01 == nil { + if c.Codesphere.CertIssuer == nil || c.Codesphere.CertIssuer.Acme == nil || c.Codesphere.CertIssuer.Acme.Solver.DNS01 == nil { return } @@ -717,7 +724,7 @@ func (c *RootConfig) buildACMEOverride() { // extractACMESolverFromOverride populates the ACMEConfig.Solver from // cluster.certificates.override.issuers.acme.dnsSolver after unmarshaling. func (c *RootConfig) extractACMESolverFromOverride() { - if c.Codesphere.CertIssuer.Acme == nil { + if c.Codesphere.CertIssuer == nil || c.Codesphere.CertIssuer.Acme == nil { return } diff --git a/internal/installer/files/config_yaml_test.go b/internal/installer/files/config_yaml_test.go index 5c764f7e..dbbfde9f 100644 --- a/internal/installer/files/config_yaml_test.go +++ b/internal/installer/files/config_yaml_test.go @@ -277,7 +277,7 @@ codesphere: // Verifies the marshaled YAML matches the structure documented at: // https://docs.codesphere.com/private-cloud/cluster-ingress-ca-options It("should marshal config.yaml to the expected ACME structure", func() { - rootConfig.Codesphere.CertIssuer = files.CertIssuerConfig{ + rootConfig.Codesphere.CertIssuer = &files.CertIssuerConfig{ Type: files.CertIssuerTypeACME, Acme: &files.ACMEConfig{ Enabled: true, @@ -373,6 +373,7 @@ cluster: err := parsed.Unmarshal([]byte(acmeYaml)) Expect(err).NotTo(HaveOccurred()) + Expect(parsed.Codesphere.CertIssuer).NotTo(BeNil()) Expect(parsed.Codesphere.CertIssuer.Type).To(Equal(files.CertIssuerTypeACME)) Expect(parsed.Codesphere.CertIssuer.Acme).NotTo(BeNil()) Expect(parsed.Codesphere.CertIssuer.Acme.Server).To(Equal("https://acme-v02.api.letsencrypt.org/directory"))