From 749f1b7523e10a669dc29316be328281e1a3b098 Mon Sep 17 00:00:00 2001 From: Manuel Dewald Date: Thu, 30 Jul 2026 08:25:00 +0200 Subject: [PATCH 1/9] fix(cli): keep updated install vault encrypted --- cli/cmd/update_install_config.go | 51 +++++++++++++++---- cli/cmd/update_install_config_test.go | 19 +++---- docs/oms_update_install-config.md | 2 +- internal/installer/vault/vault_encryption.go | 40 +++++++++++++++ .../installer/vault/vault_encryption_test.go | 24 +++++++++ 5 files changed, 116 insertions(+), 20 deletions(-) diff --git a/cli/cmd/update_install_config.go b/cli/cmd/update_install_config.go index 347ba0ae..0a85c747 100644 --- a/cli/cmd/update_install_config.go +++ b/cli/cmd/update_install_config.go @@ -6,6 +6,7 @@ package cmd import ( "fmt" "log" + "path/filepath" "strings" csio "github.com/codesphere-cloud/cs-go/pkg/io" @@ -13,6 +14,7 @@ import ( "github.com/codesphere-cloud/oms/internal/installer" "github.com/codesphere-cloud/oms/internal/installer/files" "github.com/codesphere-cloud/oms/internal/installer/secrets" + installervault "github.com/codesphere-cloud/oms/internal/installer/vault" intutil "github.com/codesphere-cloud/oms/internal/util" "github.com/spf13/cobra" ) @@ -37,6 +39,7 @@ type UpdateInstallConfigOpts struct { PostgresReplicaIP string PostgresReplicaName string PostgresServerAddress string + PostgresServer string CephNodesSubnet string @@ -105,6 +108,7 @@ func AddUpdateInstallConfigCmd(update *cobra.Command, opts *util.GlobalOptions) c.cmd.Flags().StringVar(&c.Opts.PostgresReplicaIP, "postgres-replica-ip", "", "Replica PostgreSQL server IP") c.cmd.Flags().StringVar(&c.Opts.PostgresReplicaName, "postgres-replica-name", "", "Replica PostgreSQL server name") c.cmd.Flags().StringVar(&c.Opts.PostgresServerAddress, "postgres-server-address", "", "PostgreSQL server address (for external mode)") + c.cmd.Flags().StringVar(&c.Opts.PostgresServer, "postgres-server", "", "PostgreSQL primary hostname for install mode or server address for external mode") // Ceph update flags c.cmd.Flags().StringVar(&c.Opts.CephNodesSubnet, "ceph-nodes-subnet", "", "Ceph nodes subnet") @@ -180,7 +184,7 @@ func (c *UpdateInstallConfigCmd) UpdateInstallConfig(icg installer.InstallConfig return fmt.Errorf("failed to write config file: %w", err) } - if err := icg.WriteVault(c.Opts.VaultFile, c.Opts.WithComments); err != nil { + if err := c.writeEncryptedVault(icg); err != nil { return fmt.Errorf("failed to write vault file: %w", err) } @@ -199,16 +203,27 @@ func (c *UpdateInstallConfigCmd) applyUpdates(config *files.RootConfig, vault *f } func (c *UpdateInstallConfigCmd) applyPostgresUpdates(config *files.RootConfig, tracker *SecretDependencyTracker) { - if c.Opts.PostgresPrimaryIP != "" || c.Opts.PostgresPrimaryHostname != "" { + primaryHostname := c.Opts.PostgresPrimaryHostname + serverAddress := c.Opts.PostgresServerAddress + if c.Opts.PostgresServer != "" { + if config.Postgres.Mode == "install" && primaryHostname == "" { + primaryHostname = c.Opts.PostgresServer + } + if config.Postgres.Mode == "external" && serverAddress == "" { + serverAddress = c.Opts.PostgresServer + } + } + + if c.Opts.PostgresPrimaryIP != "" || primaryHostname != "" { if config.Postgres.Primary != nil { if c.Opts.PostgresPrimaryIP != "" && config.Postgres.Primary.IP != c.Opts.PostgresPrimaryIP { log.Printf("Updating PostgreSQL primary IP: %s -> %s\n", config.Postgres.Primary.IP, c.Opts.PostgresPrimaryIP) config.Postgres.Primary.IP = c.Opts.PostgresPrimaryIP tracker.MarkPostgresPrimaryCertNeedsRegen() } - if c.Opts.PostgresPrimaryHostname != "" && config.Postgres.Primary.Hostname != c.Opts.PostgresPrimaryHostname { - log.Printf("Updating PostgreSQL primary hostname: %s -> %s\n", config.Postgres.Primary.Hostname, c.Opts.PostgresPrimaryHostname) - config.Postgres.Primary.Hostname = c.Opts.PostgresPrimaryHostname + if primaryHostname != "" && config.Postgres.Primary.Hostname != primaryHostname { + log.Printf("Updating PostgreSQL primary hostname: %s -> %s\n", config.Postgres.Primary.Hostname, primaryHostname) + config.Postgres.Primary.Hostname = primaryHostname tracker.MarkPostgresPrimaryCertNeedsRegen() } } @@ -229,12 +244,29 @@ func (c *UpdateInstallConfigCmd) applyPostgresUpdates(config *files.RootConfig, } } - if c.Opts.PostgresServerAddress != "" && config.Postgres.ServerAddress != c.Opts.PostgresServerAddress { - log.Printf("Updating PostgreSQL server address: %s -> %s\n", config.Postgres.ServerAddress, c.Opts.PostgresServerAddress) - config.Postgres.ServerAddress = c.Opts.PostgresServerAddress + if serverAddress != "" && config.Postgres.ServerAddress != serverAddress { + log.Printf("Updating PostgreSQL server address: %s -> %s\n", config.Postgres.ServerAddress, serverAddress) + config.Postgres.ServerAddress = serverAddress } } +func (c *UpdateInstallConfigCmd) writeEncryptedVault(icg installer.InstallConfigManager) error { + recipient, _, err := installervault.ResolveAgeKey("", filepath.Dir(c.Opts.VaultFile)) + if err != nil { + return fmt.Errorf("failed to resolve age key: %w", err) + } + + vaultData, err := icg.GetVault().Marshal() + if err != nil { + return fmt.Errorf("failed to marshal vault.yaml: %w", err) + } + if c.Opts.WithComments { + vaultData = installer.AddVaultComments(vaultData) + } + + return installervault.EncryptDataWithSOPSAtomically(vaultData, c.Opts.VaultFile, recipient) +} + func (c *UpdateInstallConfigCmd) applyCephUpdates(config *files.RootConfig) { if c.Opts.CephNodesSubnet != "" && config.Ceph.NodesSubnet != c.Opts.CephNodesSubnet { log.Printf("Updating Ceph nodes subnet: %s -> %s\n", config.Ceph.NodesSubnet, c.Opts.CephNodesSubnet) @@ -444,8 +476,7 @@ func (c *UpdateInstallConfigCmd) printSuccessMessage(tracker *SecretDependencyTr } } - log.Println("\nIMPORTANT: The vault file has been updated with new secrets.") - log.Println(" Remember to re-encrypt it with SOPS before storing.") + log.Println("\nThe vault file has been updated and re-encrypted with SOPS.") log.Println() } diff --git a/cli/cmd/update_install_config_test.go b/cli/cmd/update_install_config_test.go index 4b7a221b..8d078373 100644 --- a/cli/cmd/update_install_config_test.go +++ b/cli/cmd/update_install_config_test.go @@ -226,7 +226,7 @@ codesphere: Context("when updating PostgreSQL configuration", func() { It("should update primary IP and hostname, and regenerate certificates", func() { opts.PostgresPrimaryIP = "10.10.0.4" - opts.PostgresPrimaryHostname = "new-postgres-primary" + opts.PostgresServer = "new-postgres-primary" icg := installer.NewInstallConfigManager() err := cmd.UpdateInstallConfig(icg) @@ -237,6 +237,13 @@ codesphere: Expect(config.Postgres.Primary.Hostname).To(Equal("new-postgres-primary")) Expect(icg.GetVault().GetSecret(files.SecretPostgresPrimaryServerKeyPem)).NotTo(BeNil()) Expect(config.Postgres.Primary.SSLConfig.ServerCertPem).NotTo(BeEmpty()) + + encrypted, err := vault.IsSOPSEncryptedFile(vaultFile.Name()) + Expect(err).NotTo(HaveOccurred()) + Expect(encrypted).To(BeTrue()) + updatedVault, err := vault.LoadVaultData(vaultFile.Name(), "") + Expect(err).NotTo(HaveOccurred()) + Expect(updatedVault.GetSecret(files.SecretPostgresPrimaryServerKeyPem)).NotTo(BeNil()) }) It("should update replica IP and name, and regenerate certificates", func() { @@ -393,10 +400,7 @@ codesphere: err = cmd.UpdateInstallConfig(icg) Expect(err).NotTo(HaveOccurred()) - updatedVaultContent, err := os.ReadFile(vaultFile.Name()) - Expect(err).NotTo(HaveOccurred()) - updatedVault := &files.InstallVault{} - err = updatedVault.Unmarshal(updatedVaultContent) + updatedVault, err := vault.LoadVaultData(vaultFile.Name(), "") Expect(err).NotTo(HaveOccurred()) // Verify all initial secrets are still present with the same values @@ -427,10 +431,7 @@ codesphere: err = cmd.UpdateInstallConfig(icg) Expect(err).NotTo(HaveOccurred()) - updatedVaultContent, err := os.ReadFile(vaultFile.Name()) - Expect(err).NotTo(HaveOccurred()) - updatedVault := &files.InstallVault{} - err = updatedVault.Unmarshal(updatedVaultContent) + updatedVault, err := vault.LoadVaultData(vaultFile.Name(), "") Expect(err).NotTo(HaveOccurred()) // Verify all initial secrets are still present with the same values diff --git a/docs/oms_update_install-config.md b/docs/oms_update_install-config.md index 327591e3..9cf04ce2 100644 --- a/docs/oms_update_install-config.md +++ b/docs/oms_update_install-config.md @@ -59,6 +59,7 @@ $ oms update install-config --k8s-api-server 10.0.0.10 --config config.yaml --va --postgres-primary-ip string Primary PostgreSQL server IP --postgres-replica-ip string Replica PostgreSQL server IP --postgres-replica-name string Replica PostgreSQL server name + --postgres-server string PostgreSQL primary hostname for install mode or server address for external mode --postgres-server-address string PostgreSQL server address (for external mode) --public-ip string Codesphere public IP address --vault string Path to existing prod.vault.yaml file (default "prod.vault.yaml") @@ -69,4 +70,3 @@ $ oms update install-config --k8s-api-server 10.0.0.10 --config config.yaml --va ### SEE ALSO * [oms update](oms_update.md) - Update OMS related resources - diff --git a/internal/installer/vault/vault_encryption.go b/internal/installer/vault/vault_encryption.go index bc64d864..e08dec16 100644 --- a/internal/installer/vault/vault_encryption.go +++ b/internal/installer/vault/vault_encryption.go @@ -4,6 +4,7 @@ package vault import ( + "bytes" "fmt" "io" "os" @@ -165,6 +166,45 @@ func EncryptFileWithSOPS(src, target, recipient string) error { return nil } +// EncryptDataWithSOPSAtomically encrypts data into a temporary file next to +// target and replaces target only after encryption succeeds. Plaintext is +// passed to SOPS through stdin and is never written to disk. +func EncryptDataWithSOPSAtomically(data []byte, target, recipient string) error { + mode := os.FileMode(0600) + if info, err := os.Stat(target); err == nil { + mode = info.Mode().Perm() + } else if !os.IsNotExist(err) { + return fmt.Errorf("failed to inspect encryption target: %w", err) + } + + encryptedFile, err := os.CreateTemp(filepath.Dir(target), "."+filepath.Base(target)+".encrypted-*") + if err != nil { + return fmt.Errorf("failed to create temporary encrypted file: %w", err) + } + encryptedPath := encryptedFile.Name() + if err := encryptedFile.Close(); err != nil { + _ = os.Remove(encryptedPath) + return fmt.Errorf("failed to close temporary encrypted file: %w", err) + } + defer func() { + _ = os.Remove(encryptedPath) + }() + + cmd := exec.Command("sops", "--encrypt", "--input-type", "yaml", "--age", recipient, "--output", encryptedPath, "/dev/stdin") + cmd.Stdin = bytes.NewReader(data) + if out, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("sops encrypt failed: %w: %s", err, out) + } + if err := os.Chmod(encryptedPath, mode); err != nil { + return fmt.Errorf("failed to set encrypted vault permissions: %w", err) + } + if err := os.Rename(encryptedPath, target); err != nil { + return fmt.Errorf("failed to replace encrypted vault: %w", err) + } + + return nil +} + // DecryptFileWithSOPS decrypts a SOPS-encrypted file and returns the plaintext bytes. // If keyPath is non-empty, SOPS_AGE_KEY_FILE is set for the sops process. func DecryptFileWithSOPS(src, keyPath string) ([]byte, error) { diff --git a/internal/installer/vault/vault_encryption_test.go b/internal/installer/vault/vault_encryption_test.go index ba392b56..7277c54b 100644 --- a/internal/installer/vault/vault_encryption_test.go +++ b/internal/installer/vault/vault_encryption_test.go @@ -4,9 +4,11 @@ package vault_test import ( + "bytes" "os" "os/exec" "path/filepath" + "testing" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -14,6 +16,28 @@ import ( "github.com/codesphere-cloud/oms/internal/installer/vault" ) +func TestEncryptDataWithSOPSAtomicallyPreservesTargetOnFailure(t *testing.T) { + tmpDir := t.TempDir() + targetPath := filepath.Join(tmpDir, "vault.yaml") + original := []byte("existing encrypted content") + + if err := os.WriteFile(targetPath, original, 0600); err != nil { + t.Fatal(err) + } + + if err := vault.EncryptDataWithSOPSAtomically([]byte("secrets: []\n"), targetPath, "invalid-recipient"); err == nil { + t.Fatal("expected encryption to fail") + } + + content, err := os.ReadFile(targetPath) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(content, original) { + t.Fatalf("target changed after failed encryption: got %q, want %q", content, original) + } +} + func sopsAndAgeAvailable() bool { if _, err := exec.LookPath("sops"); err != nil { return false From 68fcaa371d97684d56a53df5c7a4131cdc15823a Mon Sep 17 00:00:00 2001 From: NautiluX <2600004+NautiluX@users.noreply.github.com> Date: Thu, 30 Jul 2026 06:26:35 +0000 Subject: [PATCH 2/9] chore(docs): Auto-update docs and licenses Signed-off-by: NautiluX <2600004+NautiluX@users.noreply.github.com> --- docs/oms_update_install-config.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/oms_update_install-config.md b/docs/oms_update_install-config.md index 9cf04ce2..68290eed 100644 --- a/docs/oms_update_install-config.md +++ b/docs/oms_update_install-config.md @@ -70,3 +70,4 @@ $ oms update install-config --k8s-api-server 10.0.0.10 --config config.yaml --va ### SEE ALSO * [oms update](oms_update.md) - Update OMS related resources + From 697b734d9fd432772d42df9793a6ed3980a5b2ae Mon Sep 17 00:00:00 2001 From: Manuel Dewald Date: Thu, 30 Jul 2026 14:25:51 +0200 Subject: [PATCH 3/9] refactor(installer): centralize encrypted vault writes --- cli/cmd/init_install_config.go | 26 +++--- cli/cmd/update_install_config.go | 37 ++------ internal/installer/config_manager.go | 85 +++++++++++++++++-- internal/installer/config_manager_test.go | 25 ++++++ internal/installer/mocks.go | 57 +++++++++++++ internal/installer/vault/vault_encryption.go | 40 --------- .../installer/vault/vault_encryption_test.go | 24 ------ 7 files changed, 180 insertions(+), 114 deletions(-) diff --git a/cli/cmd/init_install_config.go b/cli/cmd/init_install_config.go index 0be2637f..d12e8236 100644 --- a/cli/cmd/init_install_config.go +++ b/cli/cmd/init_install_config.go @@ -353,12 +353,14 @@ func (c *InitInstallConfigCmd) updateConfigFromOpts(config *files.RootConfig, va config.Postgres.Mode = c.Opts.PostgresMode } - postgresPrimaryHostname := determinePostgresPrimaryHostname(config.Postgres.Mode, c.Opts) + postgresPrimaryHostname, postgresServerAddress := determinePostgresServerConfig( + config.Postgres.Mode, + c.Opts.PostgresServerAddress, + c.Opts.PostgresPrimaryHostname, + config.Postgres.ServerAddress, + ) if c.Opts.PostgresServerAddress != "" { - config.Postgres.ServerAddress = c.Opts.PostgresServerAddress - } - if c.Opts.PostgresServerAddress != "" && config.Postgres.Mode == "install" { - config.Postgres.ServerAddress = "" + config.Postgres.ServerAddress = postgresServerAddress } if postgresPrimaryHostname != "" || c.Opts.PostgresPrimaryIP != "" { @@ -564,14 +566,12 @@ func (c *InitInstallConfigCmd) updateConfigFromOpts(config *files.RootConfig, va return config } -func determinePostgresPrimaryHostname(postgresMode string, opts *InitInstallConfigOpts) string { - if postgresMode != "install" { - return opts.PostgresPrimaryHostname +func determinePostgresServerConfig(postgresMode, postgresServer, primaryHostname, serverAddress string) (string, string) { + if postgresServer == "" { + return primaryHostname, serverAddress } - - if opts.PostgresServerAddress == "" { - return opts.PostgresPrimaryHostname + if postgresMode == "install" { + return postgresServer, "" } - - return opts.PostgresServerAddress + return primaryHostname, postgresServer } diff --git a/cli/cmd/update_install_config.go b/cli/cmd/update_install_config.go index 0a85c747..9250b5c7 100644 --- a/cli/cmd/update_install_config.go +++ b/cli/cmd/update_install_config.go @@ -6,7 +6,6 @@ package cmd import ( "fmt" "log" - "path/filepath" "strings" csio "github.com/codesphere-cloud/cs-go/pkg/io" @@ -14,7 +13,6 @@ import ( "github.com/codesphere-cloud/oms/internal/installer" "github.com/codesphere-cloud/oms/internal/installer/files" "github.com/codesphere-cloud/oms/internal/installer/secrets" - installervault "github.com/codesphere-cloud/oms/internal/installer/vault" intutil "github.com/codesphere-cloud/oms/internal/util" "github.com/spf13/cobra" ) @@ -184,7 +182,7 @@ func (c *UpdateInstallConfigCmd) UpdateInstallConfig(icg installer.InstallConfig return fmt.Errorf("failed to write config file: %w", err) } - if err := c.writeEncryptedVault(icg); err != nil { + if err := icg.WriteEncryptedVault(c.Opts.VaultFile, c.Opts.WithComments); err != nil { return fmt.Errorf("failed to write vault file: %w", err) } @@ -203,16 +201,12 @@ func (c *UpdateInstallConfigCmd) applyUpdates(config *files.RootConfig, vault *f } func (c *UpdateInstallConfigCmd) applyPostgresUpdates(config *files.RootConfig, tracker *SecretDependencyTracker) { - primaryHostname := c.Opts.PostgresPrimaryHostname - serverAddress := c.Opts.PostgresServerAddress - if c.Opts.PostgresServer != "" { - if config.Postgres.Mode == "install" && primaryHostname == "" { - primaryHostname = c.Opts.PostgresServer - } - if config.Postgres.Mode == "external" && serverAddress == "" { - serverAddress = c.Opts.PostgresServer - } - } + primaryHostname, serverAddress := determinePostgresServerConfig( + config.Postgres.Mode, + c.Opts.PostgresServer, + c.Opts.PostgresPrimaryHostname, + c.Opts.PostgresServerAddress, + ) if c.Opts.PostgresPrimaryIP != "" || primaryHostname != "" { if config.Postgres.Primary != nil { @@ -250,23 +244,6 @@ func (c *UpdateInstallConfigCmd) applyPostgresUpdates(config *files.RootConfig, } } -func (c *UpdateInstallConfigCmd) writeEncryptedVault(icg installer.InstallConfigManager) error { - recipient, _, err := installervault.ResolveAgeKey("", filepath.Dir(c.Opts.VaultFile)) - if err != nil { - return fmt.Errorf("failed to resolve age key: %w", err) - } - - vaultData, err := icg.GetVault().Marshal() - if err != nil { - return fmt.Errorf("failed to marshal vault.yaml: %w", err) - } - if c.Opts.WithComments { - vaultData = installer.AddVaultComments(vaultData) - } - - return installervault.EncryptDataWithSOPSAtomically(vaultData, c.Opts.VaultFile, recipient) -} - func (c *UpdateInstallConfigCmd) applyCephUpdates(config *files.RootConfig) { if c.Opts.CephNodesSubnet != "" && config.Ceph.NodesSubnet != c.Opts.CephNodesSubnet { log.Printf("Updating Ceph nodes subnet: %s -> %s\n", config.Ceph.NodesSubnet, c.Opts.CephNodesSubnet) diff --git a/internal/installer/config_manager.go b/internal/installer/config_manager.go index 32223435..e7fc61d0 100644 --- a/internal/installer/config_manager.go +++ b/internal/installer/config_manager.go @@ -7,6 +7,7 @@ import ( "fmt" "net" "net/url" + "os" "path/filepath" "github.com/codesphere-cloud/oms/internal/configtemplating" @@ -42,6 +43,7 @@ type InstallConfigManager interface { GenerateSecrets() error WriteInstallConfig(configPath string, withComments bool) error WriteVault(vaultPath string, withComments bool) error + WriteEncryptedVault(vaultPath string, withComments bool) error } type InstallConfig struct { @@ -267,8 +269,81 @@ func (g *InstallConfig) WriteInstallConfig(configPath string, withComments bool) } func (g *InstallConfig) WriteVault(vaultPath string, withComments bool) error { + vaultYAML, err := g.marshalVault(vaultPath, withComments) + if err != nil { + return err + } + + if err := g.fileIO.CreateAndWrite(vaultPath, vaultYAML, "Secrets"); err != nil { + return err + } + + return nil +} + +func (g *InstallConfig) WriteEncryptedVault(vaultPath string, withComments bool) error { + vaultYAML, err := g.marshalVault(vaultPath, withComments) + if err != nil { + return err + } + + recipient, _, err := vault.ResolveAgeKey("", filepath.Dir(vaultPath)) + if err != nil { + return fmt.Errorf("failed to resolve age key: %w", err) + } + + plainFile, err := os.CreateTemp(filepath.Dir(vaultPath), "."+filepath.Base(vaultPath)+".plaintext-*") + if err != nil { + return fmt.Errorf("failed to create temporary plaintext vault: %w", err) + } + plainPath := plainFile.Name() + defer func() { + _ = os.Remove(plainPath) + }() + if _, err := plainFile.Write(vaultYAML); err != nil { + _ = plainFile.Close() + return fmt.Errorf("failed to write temporary plaintext vault: %w", err) + } + if err := plainFile.Close(); err != nil { + return fmt.Errorf("failed to close temporary plaintext vault: %w", err) + } + + encryptedFile, err := os.CreateTemp(filepath.Dir(vaultPath), "."+filepath.Base(vaultPath)+".encrypted-*") + if err != nil { + return fmt.Errorf("failed to create temporary encrypted vault: %w", err) + } + encryptedPath := encryptedFile.Name() + if err := encryptedFile.Close(); err != nil { + _ = os.Remove(encryptedPath) + return fmt.Errorf("failed to close temporary encrypted vault: %w", err) + } + defer func() { + _ = os.Remove(encryptedPath) + }() + + if err := vault.EncryptFileWithSOPS(plainPath, encryptedPath, recipient); err != nil { + return err + } + + mode := os.FileMode(0600) + if info, err := os.Stat(vaultPath); err == nil { + mode = info.Mode().Perm() + } else if !os.IsNotExist(err) { + return fmt.Errorf("failed to inspect encrypted vault target: %w", err) + } + if err := os.Chmod(encryptedPath, mode); err != nil { + return fmt.Errorf("failed to set encrypted vault permissions: %w", err) + } + if err := os.Rename(encryptedPath, vaultPath); err != nil { + return fmt.Errorf("failed to replace encrypted vault: %w", err) + } + + return nil +} + +func (g *InstallConfig) marshalVault(vaultPath string, withComments bool) ([]byte, error) { if g.Config == nil { - return fmt.Errorf("no configuration provided - config is nil") + return nil, fmt.Errorf("no configuration provided - config is nil") } if g.Vault == nil { g.Vault = &files.InstallVault{} @@ -276,18 +351,14 @@ func (g *InstallConfig) WriteVault(vaultPath string, withComments bool) error { vaultYAML, err := g.Vault.Marshal() if err != nil { - return fmt.Errorf("failed to marshal vault.yaml: %w", err) + return nil, fmt.Errorf("failed to marshal %s: %w", filepath.Base(vaultPath), err) } if withComments { vaultYAML = AddVaultComments(vaultYAML) } - if err := g.fileIO.CreateAndWrite(vaultPath, vaultYAML, "Secrets"); err != nil { - return err - } - - return nil + return vaultYAML, nil } func AddConfigComments(yamlData []byte) []byte { diff --git a/internal/installer/config_manager_test.go b/internal/installer/config_manager_test.go index 1fd403d4..a7b4c9b3 100644 --- a/internal/installer/config_manager_test.go +++ b/internal/installer/config_manager_test.go @@ -6,7 +6,9 @@ package installer_test import ( "bytes" "os" + "path/filepath" + "filippo.io/age" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -510,6 +512,29 @@ var _ = Describe("ConfigManager", func() { }) }) + Describe("WriteEncryptedVault", func() { + It("should preserve the existing vault when encryption fails", func() { + tmpDir := GinkgoT().TempDir() + vaultPath := filepath.Join(tmpDir, "prod.vault.yaml") + keyPath := filepath.Join(tmpDir, "age-key.txt") + original := []byte("existing encrypted content") + identity, err := age.GenerateX25519Identity() + Expect(err).NotTo(HaveOccurred()) + Expect(os.WriteFile(keyPath, []byte(identity.String()), 0600)).To(Succeed()) + Expect(os.WriteFile(vaultPath, original, 0600)).To(Succeed()) + GinkgoT().Setenv("SOPS_AGE_KEY_FILE", keyPath) + GinkgoT().Setenv("PATH", tmpDir) + + manager := installer.NewInstallConfigManager() + err = manager.WriteEncryptedVault(vaultPath, false) + Expect(err).To(HaveOccurred()) + + content, readErr := os.ReadFile(vaultPath) + Expect(readErr).NotTo(HaveOccurred()) + Expect(content).To(Equal(original)) + }) + }) + Describe("Integration Tests", func() { Context("full configuration lifecycle", func() { It("should apply profile, validate, and prepare for write", func() { diff --git a/internal/installer/mocks.go b/internal/installer/mocks.go index 3de55b40..d4f62c7f 100644 --- a/internal/installer/mocks.go +++ b/internal/installer/mocks.go @@ -696,6 +696,63 @@ func (_c *MockInstallConfigManager_ValidateVault_Call) RunAndReturn(run func() [ return _c } +// WriteEncryptedVault provides a mock function for the type MockInstallConfigManager +func (_mock *MockInstallConfigManager) WriteEncryptedVault(vaultPath string, withComments bool) error { + ret := _mock.Called(vaultPath, withComments) + + if len(ret) == 0 { + panic("no return value specified for WriteEncryptedVault") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(string, bool) error); ok { + r0 = returnFunc(vaultPath, withComments) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockInstallConfigManager_WriteEncryptedVault_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteEncryptedVault' +type MockInstallConfigManager_WriteEncryptedVault_Call struct { + *mock.Call +} + +// WriteEncryptedVault is a helper method to define mock.On call +// - vaultPath string +// - withComments bool +func (_e *MockInstallConfigManager_Expecter) WriteEncryptedVault(vaultPath any, withComments any) *MockInstallConfigManager_WriteEncryptedVault_Call { + return &MockInstallConfigManager_WriteEncryptedVault_Call{Call: _e.mock.On("WriteEncryptedVault", vaultPath, withComments)} +} + +func (_c *MockInstallConfigManager_WriteEncryptedVault_Call) Run(run func(vaultPath string, withComments bool)) *MockInstallConfigManager_WriteEncryptedVault_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + var arg1 bool + if args[1] != nil { + arg1 = args[1].(bool) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockInstallConfigManager_WriteEncryptedVault_Call) Return(err error) *MockInstallConfigManager_WriteEncryptedVault_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockInstallConfigManager_WriteEncryptedVault_Call) RunAndReturn(run func(vaultPath string, withComments bool) error) *MockInstallConfigManager_WriteEncryptedVault_Call { + _c.Call.Return(run) + return _c +} + // WriteInstallConfig provides a mock function for the type MockInstallConfigManager func (_mock *MockInstallConfigManager) WriteInstallConfig(configPath string, withComments bool) error { ret := _mock.Called(configPath, withComments) diff --git a/internal/installer/vault/vault_encryption.go b/internal/installer/vault/vault_encryption.go index e08dec16..bc64d864 100644 --- a/internal/installer/vault/vault_encryption.go +++ b/internal/installer/vault/vault_encryption.go @@ -4,7 +4,6 @@ package vault import ( - "bytes" "fmt" "io" "os" @@ -166,45 +165,6 @@ func EncryptFileWithSOPS(src, target, recipient string) error { return nil } -// EncryptDataWithSOPSAtomically encrypts data into a temporary file next to -// target and replaces target only after encryption succeeds. Plaintext is -// passed to SOPS through stdin and is never written to disk. -func EncryptDataWithSOPSAtomically(data []byte, target, recipient string) error { - mode := os.FileMode(0600) - if info, err := os.Stat(target); err == nil { - mode = info.Mode().Perm() - } else if !os.IsNotExist(err) { - return fmt.Errorf("failed to inspect encryption target: %w", err) - } - - encryptedFile, err := os.CreateTemp(filepath.Dir(target), "."+filepath.Base(target)+".encrypted-*") - if err != nil { - return fmt.Errorf("failed to create temporary encrypted file: %w", err) - } - encryptedPath := encryptedFile.Name() - if err := encryptedFile.Close(); err != nil { - _ = os.Remove(encryptedPath) - return fmt.Errorf("failed to close temporary encrypted file: %w", err) - } - defer func() { - _ = os.Remove(encryptedPath) - }() - - cmd := exec.Command("sops", "--encrypt", "--input-type", "yaml", "--age", recipient, "--output", encryptedPath, "/dev/stdin") - cmd.Stdin = bytes.NewReader(data) - if out, err := cmd.CombinedOutput(); err != nil { - return fmt.Errorf("sops encrypt failed: %w: %s", err, out) - } - if err := os.Chmod(encryptedPath, mode); err != nil { - return fmt.Errorf("failed to set encrypted vault permissions: %w", err) - } - if err := os.Rename(encryptedPath, target); err != nil { - return fmt.Errorf("failed to replace encrypted vault: %w", err) - } - - return nil -} - // DecryptFileWithSOPS decrypts a SOPS-encrypted file and returns the plaintext bytes. // If keyPath is non-empty, SOPS_AGE_KEY_FILE is set for the sops process. func DecryptFileWithSOPS(src, keyPath string) ([]byte, error) { diff --git a/internal/installer/vault/vault_encryption_test.go b/internal/installer/vault/vault_encryption_test.go index 7277c54b..ba392b56 100644 --- a/internal/installer/vault/vault_encryption_test.go +++ b/internal/installer/vault/vault_encryption_test.go @@ -4,11 +4,9 @@ package vault_test import ( - "bytes" "os" "os/exec" "path/filepath" - "testing" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -16,28 +14,6 @@ import ( "github.com/codesphere-cloud/oms/internal/installer/vault" ) -func TestEncryptDataWithSOPSAtomicallyPreservesTargetOnFailure(t *testing.T) { - tmpDir := t.TempDir() - targetPath := filepath.Join(tmpDir, "vault.yaml") - original := []byte("existing encrypted content") - - if err := os.WriteFile(targetPath, original, 0600); err != nil { - t.Fatal(err) - } - - if err := vault.EncryptDataWithSOPSAtomically([]byte("secrets: []\n"), targetPath, "invalid-recipient"); err == nil { - t.Fatal("expected encryption to fail") - } - - content, err := os.ReadFile(targetPath) - if err != nil { - t.Fatal(err) - } - if !bytes.Equal(content, original) { - t.Fatalf("target changed after failed encryption: got %q, want %q", content, original) - } -} - func sopsAndAgeAvailable() bool { if _, err := exec.LookPath("sops"); err != nil { return false From fda0b1c5a219e49db54a8d581bd0efc93bffa23e Mon Sep 17 00:00:00 2001 From: Manuel Dewald Date: Thu, 30 Jul 2026 15:19:49 +0200 Subject: [PATCH 4/9] refactor(installer): abstract encrypted vault file operations --- internal/installer/config_manager.go | 69 ++++++------ internal/installer/config_manager_test.go | 45 +++++--- internal/util/filewriter.go | 19 ++++ internal/util/mocks.go | 123 ++++++++++++++++++++++ 4 files changed, 208 insertions(+), 48 deletions(-) diff --git a/internal/installer/config_manager.go b/internal/installer/config_manager.go index e7fc61d0..ff6c9e67 100644 --- a/internal/installer/config_manager.go +++ b/internal/installer/config_manager.go @@ -7,7 +7,6 @@ import ( "fmt" "net" "net/url" - "os" "path/filepath" "github.com/codesphere-cloud/oms/internal/configtemplating" @@ -47,9 +46,11 @@ type InstallConfigManager interface { } type InstallConfig struct { - fileIO util.FileIO - Config *files.RootConfig - Vault *files.InstallVault + fileIO util.FileIO + vaultEncryptor func(src, target, recipient string) error + ageKeyResolver func(explicitKeyFile, fallbackDir string) (recipient, keyPath string, err error) + Config *files.RootConfig + Vault *files.InstallVault } // SetFileIO overrides the file I/O implementation (useful for testing). @@ -57,12 +58,24 @@ func (g *InstallConfig) SetFileIO(fio util.FileIO) { g.fileIO = fio } +// SetVaultEncryptor overrides vault encryption (useful for testing). +func (g *InstallConfig) SetVaultEncryptor(encryptor func(src, target, recipient string) error) { + g.vaultEncryptor = encryptor +} + +// SetAgeKeyResolver overrides age key resolution (useful for testing). +func (g *InstallConfig) SetAgeKeyResolver(resolver func(explicitKeyFile, fallbackDir string) (recipient, keyPath string, err error)) { + g.ageKeyResolver = resolver +} + func NewInstallConfigManager() InstallConfigManager { config := files.NewRootConfig() return &InstallConfig{ - fileIO: &util.FilesystemWriter{}, - Config: &config, - Vault: &files.InstallVault{}, + fileIO: &util.FilesystemWriter{}, + vaultEncryptor: vault.EncryptFileWithSOPS, + ageKeyResolver: vault.ResolveAgeKey, + Config: &config, + Vault: &files.InstallVault{}, } } @@ -287,54 +300,46 @@ func (g *InstallConfig) WriteEncryptedVault(vaultPath string, withComments bool) return err } - recipient, _, err := vault.ResolveAgeKey("", filepath.Dir(vaultPath)) + resolveAgeKey := g.ageKeyResolver + if resolveAgeKey == nil { + resolveAgeKey = vault.ResolveAgeKey + } + recipient, _, err := resolveAgeKey("", filepath.Dir(vaultPath)) if err != nil { return fmt.Errorf("failed to resolve age key: %w", err) } - plainFile, err := os.CreateTemp(filepath.Dir(vaultPath), "."+filepath.Base(vaultPath)+".plaintext-*") + plainPath, err := g.fileIO.CreateTemp(filepath.Dir(vaultPath), "."+filepath.Base(vaultPath)+".plaintext-*") if err != nil { return fmt.Errorf("failed to create temporary plaintext vault: %w", err) } - plainPath := plainFile.Name() defer func() { - _ = os.Remove(plainPath) + _ = g.fileIO.Remove(plainPath) }() - if _, err := plainFile.Write(vaultYAML); err != nil { - _ = plainFile.Close() + if err := g.fileIO.WriteFile(plainPath, vaultYAML, 0600); err != nil { return fmt.Errorf("failed to write temporary plaintext vault: %w", err) } - if err := plainFile.Close(); err != nil { - return fmt.Errorf("failed to close temporary plaintext vault: %w", err) - } - encryptedFile, err := os.CreateTemp(filepath.Dir(vaultPath), "."+filepath.Base(vaultPath)+".encrypted-*") + encryptedPath, err := g.fileIO.CreateTemp(filepath.Dir(vaultPath), "."+filepath.Base(vaultPath)+".encrypted-*") if err != nil { return fmt.Errorf("failed to create temporary encrypted vault: %w", err) } - encryptedPath := encryptedFile.Name() - if err := encryptedFile.Close(); err != nil { - _ = os.Remove(encryptedPath) - return fmt.Errorf("failed to close temporary encrypted vault: %w", err) - } defer func() { - _ = os.Remove(encryptedPath) + _ = g.fileIO.Remove(encryptedPath) }() - if err := vault.EncryptFileWithSOPS(plainPath, encryptedPath, recipient); err != nil { + encryptor := g.vaultEncryptor + if encryptor == nil { + encryptor = vault.EncryptFileWithSOPS + } + if err := encryptor(plainPath, encryptedPath, recipient); err != nil { return err } - mode := os.FileMode(0600) - if info, err := os.Stat(vaultPath); err == nil { - mode = info.Mode().Perm() - } else if !os.IsNotExist(err) { - return fmt.Errorf("failed to inspect encrypted vault target: %w", err) - } - if err := os.Chmod(encryptedPath, mode); err != nil { + if err := g.fileIO.Chmod(encryptedPath, 0600); err != nil { return fmt.Errorf("failed to set encrypted vault permissions: %w", err) } - if err := os.Rename(encryptedPath, vaultPath); err != nil { + if err := g.fileIO.Rename(encryptedPath, vaultPath); err != nil { return fmt.Errorf("failed to replace encrypted vault: %w", err) } diff --git a/internal/installer/config_manager_test.go b/internal/installer/config_manager_test.go index a7b4c9b3..98393121 100644 --- a/internal/installer/config_manager_test.go +++ b/internal/installer/config_manager_test.go @@ -5,10 +5,10 @@ package installer_test import ( "bytes" + "errors" "os" "path/filepath" - "filippo.io/age" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -48,6 +48,18 @@ func (m *MockFileIO) CreateAndWrite(filePath string, data []byte, fileType strin return nil } +func (m *MockFileIO) CreateTemp(dir, pattern string) (string, error) { + path := filepath.Join(dir, pattern+"mock") + m.files[path] = nil + return path, nil +} + +func (m *MockFileIO) Rename(oldPath, newPath string) error { + m.files[newPath] = m.files[oldPath] + delete(m.files, oldPath) + return nil +} + func (m *MockFileIO) Open(filename string) (*os.File, error) { if m.openError != nil { return nil, m.openError @@ -514,24 +526,25 @@ var _ = Describe("ConfigManager", func() { Describe("WriteEncryptedVault", func() { It("should preserve the existing vault when encryption fails", func() { - tmpDir := GinkgoT().TempDir() - vaultPath := filepath.Join(tmpDir, "prod.vault.yaml") - keyPath := filepath.Join(tmpDir, "age-key.txt") + vaultPath := "prod.vault.yaml" original := []byte("existing encrypted content") - identity, err := age.GenerateX25519Identity() - Expect(err).NotTo(HaveOccurred()) - Expect(os.WriteFile(keyPath, []byte(identity.String()), 0600)).To(Succeed()) - Expect(os.WriteFile(vaultPath, original, 0600)).To(Succeed()) - GinkgoT().Setenv("SOPS_AGE_KEY_FILE", keyPath) - GinkgoT().Setenv("PATH", tmpDir) + mockIO := NewMockFileIO() + mockIO.files[vaultPath] = original + manager := &installer.InstallConfig{ + Config: &files.RootConfig{}, + Vault: &files.InstallVault{}, + } + manager.SetFileIO(mockIO) + manager.SetAgeKeyResolver(func(_, _ string) (string, string, error) { + return "recipient", "", nil + }) + manager.SetVaultEncryptor(func(_, _, _ string) error { + return errors.New("encryption failed") + }) - manager := installer.NewInstallConfigManager() - err = manager.WriteEncryptedVault(vaultPath, false) + err := manager.WriteEncryptedVault(vaultPath, false) Expect(err).To(HaveOccurred()) - - content, readErr := os.ReadFile(vaultPath) - Expect(readErr).NotTo(HaveOccurred()) - Expect(content).To(Equal(original)) + Expect(mockIO.GetFileContent(vaultPath)).To(Equal(original)) }) }) diff --git a/internal/util/filewriter.go b/internal/util/filewriter.go index 9c9a948b..b9380483 100644 --- a/internal/util/filewriter.go +++ b/internal/util/filewriter.go @@ -22,6 +22,8 @@ type FileIO interface { ReadDir(dirname string) ([]os.DirEntry, error) ReadFile(filename string) ([]byte, error) CreateAndWrite(filePath string, data []byte, fileType string) error + CreateTemp(dir, pattern string) (string, error) + Rename(oldPath, newPath string) error Remove(path string) error Chmod(name string, mode os.FileMode) error } @@ -51,6 +53,23 @@ func (fs *FilesystemWriter) CreateAndWrite(filePath string, data []byte, fileTyp return nil } +func (fs *FilesystemWriter) CreateTemp(dir, pattern string) (string, error) { + file, err := os.CreateTemp(dir, pattern) + if err != nil { + return "", err + } + path := file.Name() + if err := file.Close(); err != nil { + _ = os.Remove(path) + return "", err + } + return path, nil +} + +func (fs *FilesystemWriter) Rename(oldPath, newPath string) error { + return os.Rename(oldPath, newPath) +} + func (fs *FilesystemWriter) Open(filename string) (*os.File, error) { return os.Open(filename) } diff --git a/internal/util/mocks.go b/internal/util/mocks.go index cd37286d..f307cc03 100644 --- a/internal/util/mocks.go +++ b/internal/util/mocks.go @@ -219,6 +219,72 @@ func (_c *MockFileIO_CreateAndWrite_Call) RunAndReturn(run func(filePath string, return _c } +// CreateTemp provides a mock function for the type MockFileIO +func (_mock *MockFileIO) CreateTemp(dir string, pattern string) (string, error) { + ret := _mock.Called(dir, pattern) + + if len(ret) == 0 { + panic("no return value specified for CreateTemp") + } + + var r0 string + var r1 error + if returnFunc, ok := ret.Get(0).(func(string, string) (string, error)); ok { + return returnFunc(dir, pattern) + } + if returnFunc, ok := ret.Get(0).(func(string, string) string); ok { + r0 = returnFunc(dir, pattern) + } else { + r0 = ret.Get(0).(string) + } + if returnFunc, ok := ret.Get(1).(func(string, string) error); ok { + r1 = returnFunc(dir, pattern) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockFileIO_CreateTemp_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateTemp' +type MockFileIO_CreateTemp_Call struct { + *mock.Call +} + +// CreateTemp is a helper method to define mock.On call +// - dir string +// - pattern string +func (_e *MockFileIO_Expecter) CreateTemp(dir any, pattern any) *MockFileIO_CreateTemp_Call { + return &MockFileIO_CreateTemp_Call{Call: _e.mock.On("CreateTemp", dir, pattern)} +} + +func (_c *MockFileIO_CreateTemp_Call) Run(run func(dir string, pattern string)) *MockFileIO_CreateTemp_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockFileIO_CreateTemp_Call) Return(s string, err error) *MockFileIO_CreateTemp_Call { + _c.Call.Return(s, err) + return _c +} + +func (_c *MockFileIO_CreateTemp_Call) RunAndReturn(run func(dir string, pattern string) (string, error)) *MockFileIO_CreateTemp_Call { + _c.Call.Return(run) + return _c +} + // Exists provides a mock function for the type MockFileIO func (_mock *MockFileIO) Exists(filename string) bool { ret := _mock.Called(filename) @@ -760,6 +826,63 @@ func (_c *MockFileIO_Remove_Call) RunAndReturn(run func(path string) error) *Moc return _c } +// Rename provides a mock function for the type MockFileIO +func (_mock *MockFileIO) Rename(oldPath string, newPath string) error { + ret := _mock.Called(oldPath, newPath) + + if len(ret) == 0 { + panic("no return value specified for Rename") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(string, string) error); ok { + r0 = returnFunc(oldPath, newPath) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockFileIO_Rename_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Rename' +type MockFileIO_Rename_Call struct { + *mock.Call +} + +// Rename is a helper method to define mock.On call +// - oldPath string +// - newPath string +func (_e *MockFileIO_Expecter) Rename(oldPath any, newPath any) *MockFileIO_Rename_Call { + return &MockFileIO_Rename_Call{Call: _e.mock.On("Rename", oldPath, newPath)} +} + +func (_c *MockFileIO_Rename_Call) Run(run func(oldPath string, newPath string)) *MockFileIO_Rename_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockFileIO_Rename_Call) Return(err error) *MockFileIO_Rename_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockFileIO_Rename_Call) RunAndReturn(run func(oldPath string, newPath string) error) *MockFileIO_Rename_Call { + _c.Call.Return(run) + return _c +} + // WriteFile provides a mock function for the type MockFileIO func (_mock *MockFileIO) WriteFile(filename string, data []byte, perm os.FileMode) error { ret := _mock.Called(filename, data, perm) From b4fe9dc61ec83444831b17a680acc7001790b00a Mon Sep 17 00:00:00 2001 From: Manuel Dewald Date: Thu, 30 Jul 2026 16:18:43 +0200 Subject: [PATCH 5/9] refactor(installer): encapsulate vault encryptor fallback --- internal/installer/config_manager.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/internal/installer/config_manager.go b/internal/installer/config_manager.go index ff6c9e67..fd125034 100644 --- a/internal/installer/config_manager.go +++ b/internal/installer/config_manager.go @@ -68,6 +68,13 @@ func (g *InstallConfig) SetAgeKeyResolver(resolver func(explicitKeyFile, fallbac g.ageKeyResolver = resolver } +func (g *InstallConfig) encryptVault(src, target, recipient string) error { + if g.vaultEncryptor != nil { + return g.vaultEncryptor(src, target, recipient) + } + return vault.EncryptFileWithSOPS(src, target, recipient) +} + func NewInstallConfigManager() InstallConfigManager { config := files.NewRootConfig() return &InstallConfig{ @@ -328,11 +335,7 @@ func (g *InstallConfig) WriteEncryptedVault(vaultPath string, withComments bool) _ = g.fileIO.Remove(encryptedPath) }() - encryptor := g.vaultEncryptor - if encryptor == nil { - encryptor = vault.EncryptFileWithSOPS - } - if err := encryptor(plainPath, encryptedPath, recipient); err != nil { + if err := g.encryptVault(plainPath, encryptedPath, recipient); err != nil { return err } From eff1def5e0bb70f641054f72e3046ee86bd959e9 Mon Sep 17 00:00:00 2001 From: Manuel Dewald Date: Thu, 30 Jul 2026 16:52:48 +0200 Subject: [PATCH 6/9] refactor(installer): model vault dependencies as interfaces --- internal/installer/config_manager.go | 49 +++++++++++++++++------ internal/installer/config_manager_test.go | 20 ++++++--- 2 files changed, 50 insertions(+), 19 deletions(-) diff --git a/internal/installer/config_manager.go b/internal/installer/config_manager.go index fd125034..9f077fd2 100644 --- a/internal/installer/config_manager.go +++ b/internal/installer/config_manager.go @@ -45,10 +45,30 @@ type InstallConfigManager interface { WriteEncryptedVault(vaultPath string, withComments bool) error } +type VaultEncryptor interface { + Encrypt(src, target, recipient string) error +} + +type AgeKeyResolver interface { + Resolve(explicitKeyFile, fallbackDir string) (recipient, keyPath string, err error) +} + +type sopsVaultEncryptor struct{} + +func (sopsVaultEncryptor) Encrypt(src, target, recipient string) error { + return vault.EncryptFileWithSOPS(src, target, recipient) +} + +type sopsAgeKeyResolver struct{} + +func (sopsAgeKeyResolver) Resolve(explicitKeyFile, fallbackDir string) (recipient, keyPath string, err error) { + return vault.ResolveAgeKey(explicitKeyFile, fallbackDir) +} + type InstallConfig struct { fileIO util.FileIO - vaultEncryptor func(src, target, recipient string) error - ageKeyResolver func(explicitKeyFile, fallbackDir string) (recipient, keyPath string, err error) + vaultEncryptor VaultEncryptor + ageKeyResolver AgeKeyResolver Config *files.RootConfig Vault *files.InstallVault } @@ -59,28 +79,35 @@ func (g *InstallConfig) SetFileIO(fio util.FileIO) { } // SetVaultEncryptor overrides vault encryption (useful for testing). -func (g *InstallConfig) SetVaultEncryptor(encryptor func(src, target, recipient string) error) { +func (g *InstallConfig) SetVaultEncryptor(encryptor VaultEncryptor) { g.vaultEncryptor = encryptor } // SetAgeKeyResolver overrides age key resolution (useful for testing). -func (g *InstallConfig) SetAgeKeyResolver(resolver func(explicitKeyFile, fallbackDir string) (recipient, keyPath string, err error)) { +func (g *InstallConfig) SetAgeKeyResolver(resolver AgeKeyResolver) { g.ageKeyResolver = resolver } func (g *InstallConfig) encryptVault(src, target, recipient string) error { if g.vaultEncryptor != nil { - return g.vaultEncryptor(src, target, recipient) + return g.vaultEncryptor.Encrypt(src, target, recipient) } - return vault.EncryptFileWithSOPS(src, target, recipient) + return sopsVaultEncryptor{}.Encrypt(src, target, recipient) +} + +func (g *InstallConfig) resolveAgeKey(explicitKeyFile, fallbackDir string) (recipient, keyPath string, err error) { + if g.ageKeyResolver != nil { + return g.ageKeyResolver.Resolve(explicitKeyFile, fallbackDir) + } + return sopsAgeKeyResolver{}.Resolve(explicitKeyFile, fallbackDir) } func NewInstallConfigManager() InstallConfigManager { config := files.NewRootConfig() return &InstallConfig{ fileIO: &util.FilesystemWriter{}, - vaultEncryptor: vault.EncryptFileWithSOPS, - ageKeyResolver: vault.ResolveAgeKey, + vaultEncryptor: sopsVaultEncryptor{}, + ageKeyResolver: sopsAgeKeyResolver{}, Config: &config, Vault: &files.InstallVault{}, } @@ -307,11 +334,7 @@ func (g *InstallConfig) WriteEncryptedVault(vaultPath string, withComments bool) return err } - resolveAgeKey := g.ageKeyResolver - if resolveAgeKey == nil { - resolveAgeKey = vault.ResolveAgeKey - } - recipient, _, err := resolveAgeKey("", filepath.Dir(vaultPath)) + recipient, _, err := g.resolveAgeKey("", filepath.Dir(vaultPath)) if err != nil { return fmt.Errorf("failed to resolve age key: %w", err) } diff --git a/internal/installer/config_manager_test.go b/internal/installer/config_manager_test.go index 98393121..d963c741 100644 --- a/internal/installer/config_manager_test.go +++ b/internal/installer/config_manager_test.go @@ -27,6 +27,18 @@ type MockFileIO struct { mkdirAllError error } +type failingVaultEncryptor struct{} + +func (failingVaultEncryptor) Encrypt(_, _, _ string) error { + return errors.New("encryption failed") +} + +type staticAgeKeyResolver struct{} + +func (staticAgeKeyResolver) Resolve(_, _ string) (string, string, error) { + return "recipient", "", nil +} + func NewMockFileIO() *MockFileIO { return &MockFileIO{ files: make(map[string][]byte), @@ -535,12 +547,8 @@ var _ = Describe("ConfigManager", func() { Vault: &files.InstallVault{}, } manager.SetFileIO(mockIO) - manager.SetAgeKeyResolver(func(_, _ string) (string, string, error) { - return "recipient", "", nil - }) - manager.SetVaultEncryptor(func(_, _, _ string) error { - return errors.New("encryption failed") - }) + manager.SetAgeKeyResolver(staticAgeKeyResolver{}) + manager.SetVaultEncryptor(failingVaultEncryptor{}) err := manager.WriteEncryptedVault(vaultPath, false) Expect(err).To(HaveOccurred()) From 5bc7ac596b1a57a6acdadc51aeee04982d2235ec Mon Sep 17 00:00:00 2001 From: Manuel Dewald Date: Wed, 5 Aug 2026 10:47:30 +0200 Subject: [PATCH 7/9] refactor(installer): align vault write semantics --- cli/cmd/init_install_config.go | 4 +- .../init_install_config_interactive_test.go | 2 +- cli/cmd/update_install_config.go | 12 ++- docs/oms_init_install-config.md | 2 +- docs/oms_update_install-config.md | 10 ++- internal/bootstrap/gcp/gcp_test.go | 2 +- internal/bootstrap/gcp/install_config.go | 2 +- internal/bootstrap/gcp/install_config_test.go | 82 +++++++++---------- internal/bootstrap/local/local.go | 2 +- internal/installer/config_manager.go | 50 ++++------- internal/installer/config_manager_test.go | 29 +++++-- internal/installer/mocks.go | 44 +++++----- internal/installer/vault/vault_encryption.go | 24 ++++++ 13 files changed, 145 insertions(+), 120 deletions(-) diff --git a/cli/cmd/init_install_config.go b/cli/cmd/init_install_config.go index d12e8236..4c1c7013 100644 --- a/cli/cmd/init_install_config.go +++ b/cli/cmd/init_install_config.go @@ -163,7 +163,7 @@ func AddInitInstallConfigCmd(init *cobra.Command, opts *util.GlobalOptions) { // Postgres c.cmd.Flags().StringVar(&c.Opts.PostgresMode, "postgres-mode", "", "PostgreSQL setup mode (install/external)") - c.cmd.Flags().StringVar(&c.Opts.PostgresServerAddress, "postgres-server", "", "PostgreSQL server hostname for install mode or address for external mode") + c.cmd.Flags().StringVar(&c.Opts.PostgresServerAddress, "postgres-server", "", "PostgreSQL server: primary hostname in install mode, connection address in external mode") c.cmd.Flags().StringVar(&c.Opts.PostgresPrimaryIP, "postgres-primary-ip", "", "Primary PostgreSQL server IP") // K8s @@ -244,7 +244,7 @@ func (c *InitInstallConfigCmd) InitInstallConfig(icg installer.InstallConfigMana return fmt.Errorf("failed to write config file: %w", err) } - if err := icg.WriteVault(c.Opts.VaultFile, c.Opts.WithComments); err != nil { + if err := icg.WriteUnencryptedVault(c.Opts.VaultFile, c.Opts.WithComments); err != nil { return fmt.Errorf("failed to write vault file: %w", err) } diff --git a/cli/cmd/init_install_config_interactive_test.go b/cli/cmd/init_install_config_interactive_test.go index 61b89bd4..1be47680 100644 --- a/cli/cmd/init_install_config_interactive_test.go +++ b/cli/cmd/init_install_config_interactive_test.go @@ -153,7 +153,7 @@ var _ = Describe("Interactive profile usage", func() { mockIcg.EXPECT().ValidateInstallConfig().Return([]string{"configuration validation failed"}) mockIcg.EXPECT().GenerateSecrets().Return(nil) mockIcg.EXPECT().WriteInstallConfig("config.yaml", false).Return(nil) - mockIcg.EXPECT().WriteVault("vault.yaml", false).Return(nil) + mockIcg.EXPECT().WriteUnencryptedVault("vault.yaml", false).Return(nil) c := &InitInstallConfigCmd{ Opts: &InitInstallConfigOpts{ diff --git a/cli/cmd/update_install_config.go b/cli/cmd/update_install_config.go index 9250b5c7..9aeb6855 100644 --- a/cli/cmd/update_install_config.go +++ b/cli/cmd/update_install_config.go @@ -87,6 +87,8 @@ func AddUpdateInstallConfigCmd(update *cobra.Command, opts *util.GlobalOptions) of the PostgreSQL server certificates that include that IP address.`), Example: util.FormatExamples("update install-config", []csio.Example{ {Cmd: "--postgres-primary-ip 10.10.0.4 --config config.yaml --vault prod.vault.yaml", Desc: "Update PostgreSQL primary IP and regenerate certificates"}, + {Cmd: "--postgres-server postgres-1 --config config.yaml --vault prod.vault.yaml", Desc: "Set the primary PostgreSQL hostname when mode is install"}, + {Cmd: "--postgres-server db.example.com:5432 --config config.yaml --vault prod.vault.yaml", Desc: "Set the PostgreSQL connection address when mode is external"}, {Cmd: "--domain new.example.com --config config.yaml --vault prod.vault.yaml", Desc: "Update Codesphere domain"}, {Cmd: "--k8s-api-server 10.0.0.10 --config config.yaml --vault prod.vault.yaml", Desc: "Update Kubernetes API server host"}, }), @@ -102,11 +104,13 @@ func AddUpdateInstallConfigCmd(update *cobra.Command, opts *util.GlobalOptions) // PostgreSQL update flags c.cmd.Flags().StringVar(&c.Opts.PostgresPrimaryIP, "postgres-primary-ip", "", "Primary PostgreSQL server IP") - c.cmd.Flags().StringVar(&c.Opts.PostgresPrimaryHostname, "postgres-primary-hostname", "", "Primary PostgreSQL server hostname") + c.cmd.Flags().StringVar(&c.Opts.PostgresPrimaryHostname, "postgres-primary-hostname", "", "Primary PostgreSQL server hostname (deprecated: use --postgres-server)") c.cmd.Flags().StringVar(&c.Opts.PostgresReplicaIP, "postgres-replica-ip", "", "Replica PostgreSQL server IP") c.cmd.Flags().StringVar(&c.Opts.PostgresReplicaName, "postgres-replica-name", "", "Replica PostgreSQL server name") - c.cmd.Flags().StringVar(&c.Opts.PostgresServerAddress, "postgres-server-address", "", "PostgreSQL server address (for external mode)") - c.cmd.Flags().StringVar(&c.Opts.PostgresServer, "postgres-server", "", "PostgreSQL primary hostname for install mode or server address for external mode") + c.cmd.Flags().StringVar(&c.Opts.PostgresServerAddress, "postgres-server-address", "", "External PostgreSQL connection address (deprecated: use --postgres-server)") + c.cmd.Flags().StringVar(&c.Opts.PostgresServer, "postgres-server", "", "PostgreSQL server: primary hostname in install mode, connection address in external mode") + _ = c.cmd.Flags().MarkDeprecated("postgres-primary-hostname", "use --postgres-server instead") + _ = c.cmd.Flags().MarkDeprecated("postgres-server-address", "use --postgres-server instead") // Ceph update flags c.cmd.Flags().StringVar(&c.Opts.CephNodesSubnet, "ceph-nodes-subnet", "", "Ceph nodes subnet") @@ -182,7 +186,7 @@ func (c *UpdateInstallConfigCmd) UpdateInstallConfig(icg installer.InstallConfig return fmt.Errorf("failed to write config file: %w", err) } - if err := icg.WriteEncryptedVault(c.Opts.VaultFile, c.Opts.WithComments); err != nil { + if err := icg.WriteVault(c.Opts.VaultFile, c.Opts.WithComments); err != nil { return fmt.Errorf("failed to write vault file: %w", err) } diff --git a/docs/oms_init_install-config.md b/docs/oms_init_install-config.md index 71a94595..870b87c7 100644 --- a/docs/oms_init_install-config.md +++ b/docs/oms_init_install-config.md @@ -77,7 +77,7 @@ $ oms init install-config --validate -c config.yaml --vault prod.vault.yaml --openbao-user string Username for OpenBao authentication (default "admin") --postgres-mode string PostgreSQL setup mode (install/external) --postgres-primary-ip string Primary PostgreSQL server IP - --postgres-server string PostgreSQL server hostname for install mode or address for external mode + --postgres-server string PostgreSQL server: primary hostname in install mode, connection address in external mode --profile string Use a predefined configuration profile (dev, production, minimal) --registry-server string Server for container registry --secrets-dir string Secrets base directory (default "/root/secrets") diff --git a/docs/oms_update_install-config.md b/docs/oms_update_install-config.md index 68290eed..54412d20 100644 --- a/docs/oms_update_install-config.md +++ b/docs/oms_update_install-config.md @@ -24,6 +24,12 @@ oms update install-config [flags] # Update PostgreSQL primary IP and regenerate certificates $ oms update install-config --postgres-primary-ip 10.10.0.4 --config config.yaml --vault prod.vault.yaml +# Set the primary PostgreSQL hostname when mode is install +$ oms update install-config --postgres-server postgres-1 --config config.yaml --vault prod.vault.yaml + +# Set the PostgreSQL connection address when mode is external +$ oms update install-config --postgres-server db.example.com:5432 --config config.yaml --vault prod.vault.yaml + # Update Codesphere domain $ oms update install-config --domain new.example.com --config config.yaml --vault prod.vault.yaml @@ -55,12 +61,10 @@ $ oms update install-config --k8s-api-server 10.0.0.10 --config config.yaml --va --k8s-api-server string Kubernetes API server host --k8s-pod-cidr string Kubernetes Pod CIDR --k8s-service-cidr string Kubernetes Service CIDR - --postgres-primary-hostname string Primary PostgreSQL server hostname --postgres-primary-ip string Primary PostgreSQL server IP --postgres-replica-ip string Replica PostgreSQL server IP --postgres-replica-name string Replica PostgreSQL server name - --postgres-server string PostgreSQL primary hostname for install mode or server address for external mode - --postgres-server-address string PostgreSQL server address (for external mode) + --postgres-server string PostgreSQL server: primary hostname in install mode, connection address in external mode --public-ip string Codesphere public IP address --vault string Path to existing prod.vault.yaml file (default "prod.vault.yaml") --with-comments Add helpful comments to the generated YAML files diff --git a/internal/bootstrap/gcp/gcp_test.go b/internal/bootstrap/gcp/gcp_test.go index d0d6ff15..49601814 100644 --- a/internal/bootstrap/gcp/gcp_test.go +++ b/internal/bootstrap/gcp/gcp_test.go @@ -235,7 +235,7 @@ var _ = Describe("GCP Bootstrapper", func() { icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) nodeClient.EXPECT().CopyFile(mock.Anything, "fake-config-file", "/etc/codesphere/config.yaml").Return(nil) nodeClient.EXPECT().CopyFile(mock.Anything, "fake-secret", "/etc/codesphere/secrets/prod.vault.yaml").Return(nil) - icg.EXPECT().WriteVault("fake-secret", true).Return(nil) + icg.EXPECT().WriteUnencryptedVault("fake-secret", true).Return(nil) // Enable Root Login nodeClient.EXPECT().WaitReady(mock.Anything, mock.Anything).Return(nil).Return(nil) diff --git a/internal/bootstrap/gcp/install_config.go b/internal/bootstrap/gcp/install_config.go index 83ebdf0a..84ac388c 100644 --- a/internal/bootstrap/gcp/install_config.go +++ b/internal/bootstrap/gcp/install_config.go @@ -414,7 +414,7 @@ func (b *GCPBootstrapper) UpdateInstallConfig() error { return fmt.Errorf("failed to write config file: %w", err) } - if err := b.icg.WriteVault(b.Env.SecretsFilePath, true); err != nil { + if err := b.icg.WriteUnencryptedVault(b.Env.SecretsFilePath, true); err != nil { return fmt.Errorf("failed to write vault file: %w", err) } diff --git a/internal/bootstrap/gcp/install_config_test.go b/internal/bootstrap/gcp/install_config_test.go index 6ccd50d0..1c328e50 100644 --- a/internal/bootstrap/gcp/install_config_test.go +++ b/internal/bootstrap/gcp/install_config_test.go @@ -324,7 +324,7 @@ var _ = Describe("Installconfig & Secrets", func() { icg.EXPECT().GenerateSecrets().Return(nil) icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) - icg.EXPECT().WriteVault("fake-secret", true).Return(nil) + icg.EXPECT().WriteUnencryptedVault("fake-secret", true).Return(nil) nodeClient.EXPECT().CopyFile(mock.Anything, mock.Anything, mock.Anything).Return(nil).Twice() @@ -372,7 +372,7 @@ var _ = Describe("Installconfig & Secrets", func() { icg.EXPECT().GenerateSecrets().Return(nil) icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) - icg.EXPECT().WriteVault("fake-secret", true).Return(nil) + icg.EXPECT().WriteUnencryptedVault("fake-secret", true).Return(nil) nodeClient.EXPECT().CopyFile(mock.Anything, mock.Anything, mock.Anything).Return(nil).Twice() @@ -388,7 +388,7 @@ var _ = Describe("Installconfig & Secrets", func() { It("uses those internal flags instead of defaults", func() { icg.EXPECT().GenerateSecrets().Return(nil) icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) - icg.EXPECT().WriteVault("fake-secret", true).Return(nil) + icg.EXPECT().WriteUnencryptedVault("fake-secret", true).Return(nil) nodeClient.EXPECT().CopyFile(mock.Anything, mock.Anything, mock.Anything).Return(nil).Twice() @@ -405,7 +405,7 @@ var _ = Describe("Installconfig & Secrets", func() { It("uses those preview flags instead of defaults", func() { icg.EXPECT().GenerateSecrets().Return(nil) icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) - icg.EXPECT().WriteVault("fake-secret", true).Return(nil) + icg.EXPECT().WriteUnencryptedVault("fake-secret", true).Return(nil) nodeClient.EXPECT().CopyFile(mock.Anything, mock.Anything, mock.Anything).Return(nil).Twice() @@ -422,7 +422,7 @@ var _ = Describe("Installconfig & Secrets", func() { It("uses those feature flags", func() { icg.EXPECT().GenerateSecrets().Return(nil) icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) - icg.EXPECT().WriteVault("fake-secret", true).Return(nil) + icg.EXPECT().WriteUnencryptedVault("fake-secret", true).Return(nil) nodeClient.EXPECT().CopyFile(mock.Anything, mock.Anything, mock.Anything).Return(nil).Twice() @@ -439,7 +439,7 @@ var _ = Describe("Installconfig & Secrets", func() { It("writes the email to the install config", func() { icg.EXPECT().GenerateSecrets().Return(nil) icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) - icg.EXPECT().WriteVault("fake-secret", true).Return(nil) + icg.EXPECT().WriteUnencryptedVault("fake-secret", true).Return(nil) nodeClient.EXPECT().CopyFile(mock.Anything, mock.Anything, mock.Anything).Return(nil).Twice() @@ -456,7 +456,7 @@ var _ = Describe("Installconfig & Secrets", func() { It("keeps the value of an existing config", func() { icg.EXPECT().GenerateSecrets().Return(nil) icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) - icg.EXPECT().WriteVault("fake-secret", true).Return(nil) + icg.EXPECT().WriteUnencryptedVault("fake-secret", true).Return(nil) nodeClient.EXPECT().CopyFile(mock.Anything, mock.Anything, mock.Anything).Return(nil).Twice() @@ -473,7 +473,7 @@ var _ = Describe("Installconfig & Secrets", func() { It("skips setting GitHub OAuth configuration", func() { icg.EXPECT().GenerateSecrets().Return(nil) icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) - icg.EXPECT().WriteVault("fake-secret", true).Return(nil) + icg.EXPECT().WriteUnencryptedVault("fake-secret", true).Return(nil) nodeClient.EXPECT().CopyFile(mock.Anything, mock.Anything, mock.Anything).Return(nil).Twice() @@ -492,7 +492,7 @@ var _ = Describe("Installconfig & Secrets", func() { It("sets GitLab OAuth configuration", func() { icg.EXPECT().GenerateSecrets().Return(nil) icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) - icg.EXPECT().WriteVault("fake-secret", true).Return(nil) + icg.EXPECT().WriteUnencryptedVault("fake-secret", true).Return(nil) nodeClient.EXPECT().CopyFile(mock.Anything, mock.Anything, mock.Anything).Return(nil).Twice() err := bs.UpdateInstallConfig() @@ -516,7 +516,7 @@ var _ = Describe("Installconfig & Secrets", func() { It("skips setting GitLab OAuth configuration", func() { icg.EXPECT().GenerateSecrets().Return(nil) icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) - icg.EXPECT().WriteVault("fake-secret", true).Return(nil) + icg.EXPECT().WriteUnencryptedVault("fake-secret", true).Return(nil) nodeClient.EXPECT().CopyFile(mock.Anything, mock.Anything, mock.Anything).Return(nil).Twice() err := bs.UpdateInstallConfig() @@ -534,7 +534,7 @@ var _ = Describe("Installconfig & Secrets", func() { It("sets Bitbucket OAuth configuration", func() { icg.EXPECT().GenerateSecrets().Return(nil) icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) - icg.EXPECT().WriteVault("fake-secret", true).Return(nil) + icg.EXPECT().WriteUnencryptedVault("fake-secret", true).Return(nil) nodeClient.EXPECT().CopyFile(mock.Anything, mock.Anything, mock.Anything).Return(nil).Twice() err := bs.UpdateInstallConfig() @@ -558,7 +558,7 @@ var _ = Describe("Installconfig & Secrets", func() { It("skips setting Bitbucket OAuth configuration", func() { icg.EXPECT().GenerateSecrets().Return(nil) icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) - icg.EXPECT().WriteVault("fake-secret", true).Return(nil) + icg.EXPECT().WriteUnencryptedVault("fake-secret", true).Return(nil) nodeClient.EXPECT().CopyFile(mock.Anything, mock.Anything, mock.Anything).Return(nil).Twice() err := bs.UpdateInstallConfig() @@ -576,7 +576,7 @@ var _ = Describe("Installconfig & Secrets", func() { It("sets Azure DevOps OAuth configuration", func() { icg.EXPECT().GenerateSecrets().Return(nil) icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) - icg.EXPECT().WriteVault("fake-secret", true).Return(nil) + icg.EXPECT().WriteUnencryptedVault("fake-secret", true).Return(nil) nodeClient.EXPECT().CopyFile(mock.Anything, mock.Anything, mock.Anything).Return(nil).Twice() err := bs.UpdateInstallConfig() @@ -601,7 +601,7 @@ var _ = Describe("Installconfig & Secrets", func() { It("skips setting Azure DevOps OAuth configuration", func() { icg.EXPECT().GenerateSecrets().Return(nil) icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) - icg.EXPECT().WriteVault("fake-secret", true).Return(nil) + icg.EXPECT().WriteUnencryptedVault("fake-secret", true).Return(nil) nodeClient.EXPECT().CopyFile(mock.Anything, mock.Anything, mock.Anything).Return(nil).Twice() err := bs.UpdateInstallConfig() @@ -621,7 +621,7 @@ var _ = Describe("Installconfig & Secrets", func() { It("sets OIDC OAuth configuration", func() { icg.EXPECT().GenerateSecrets().Return(nil) icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) - icg.EXPECT().WriteVault("fake-secret", true).Return(nil) + icg.EXPECT().WriteUnencryptedVault("fake-secret", true).Return(nil) nodeClient.EXPECT().CopyFile(mock.Anything, mock.Anything, mock.Anything).Return(nil).Twice() err := bs.UpdateInstallConfig() @@ -648,7 +648,7 @@ var _ = Describe("Installconfig & Secrets", func() { It("defaults OIDC provider name to OIDC", func() { icg.EXPECT().GenerateSecrets().Return(nil) icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) - icg.EXPECT().WriteVault("fake-secret", true).Return(nil) + icg.EXPECT().WriteUnencryptedVault("fake-secret", true).Return(nil) nodeClient.EXPECT().CopyFile(mock.Anything, mock.Anything, mock.Anything).Return(nil).Twice() err := bs.UpdateInstallConfig() @@ -662,7 +662,7 @@ var _ = Describe("Installconfig & Secrets", func() { It("skips setting OIDC OAuth configuration", func() { icg.EXPECT().GenerateSecrets().Return(nil) icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) - icg.EXPECT().WriteVault("fake-secret", true).Return(nil) + icg.EXPECT().WriteUnencryptedVault("fake-secret", true).Return(nil) nodeClient.EXPECT().CopyFile(mock.Anything, mock.Anything, mock.Anything).Return(nil).Twice() err := bs.UpdateInstallConfig() @@ -680,7 +680,7 @@ var _ = Describe("Installconfig & Secrets", func() { It("sets CentralOtel credentials in install config with Enabled true", func() { icg.EXPECT().GenerateSecrets().Return(nil) icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) - icg.EXPECT().WriteVault("fake-secret", true).Return(nil) + icg.EXPECT().WriteUnencryptedVault("fake-secret", true).Return(nil) nodeClient.EXPECT().CopyFile(mock.Anything, mock.Anything, mock.Anything).Return(nil).Twice() err := bs.UpdateInstallConfig() @@ -696,7 +696,7 @@ var _ = Describe("Installconfig & Secrets", func() { It("stores CentralOtel username and password in the vault", func() { icg.EXPECT().GenerateSecrets().Return(nil) icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) - icg.EXPECT().WriteVault("fake-secret", true).Return(nil) + icg.EXPECT().WriteUnencryptedVault("fake-secret", true).Return(nil) nodeClient.EXPECT().CopyFile(mock.Anything, mock.Anything, mock.Anything).Return(nil).Twice() err := bs.UpdateInstallConfig() @@ -718,7 +718,7 @@ var _ = Describe("Installconfig & Secrets", func() { It("skips setting CentralOtel credentials", func() { icg.EXPECT().GenerateSecrets().Return(nil) icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) - icg.EXPECT().WriteVault("fake-secret", true).Return(nil) + icg.EXPECT().WriteUnencryptedVault("fake-secret", true).Return(nil) nodeClient.EXPECT().CopyFile(mock.Anything, mock.Anything, mock.Anything).Return(nil).Twice() err := bs.UpdateInstallConfig() @@ -736,7 +736,7 @@ var _ = Describe("Installconfig & Secrets", func() { It("skips setting CentralOtel credentials", func() { icg.EXPECT().GenerateSecrets().Return(nil) icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) - icg.EXPECT().WriteVault("fake-secret", true).Return(nil) + icg.EXPECT().WriteUnencryptedVault("fake-secret", true).Return(nil) nodeClient.EXPECT().CopyFile(mock.Anything, mock.Anything, mock.Anything).Return(nil).Twice() err := bs.UpdateInstallConfig() @@ -750,7 +750,7 @@ var _ = Describe("Installconfig & Secrets", func() { It("leaves Monitoring.CentralOtel nil", func() { icg.EXPECT().GenerateSecrets().Return(nil) icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) - icg.EXPECT().WriteVault("fake-secret", true).Return(nil) + icg.EXPECT().WriteUnencryptedVault("fake-secret", true).Return(nil) nodeClient.EXPECT().CopyFile(mock.Anything, mock.Anything, mock.Anything).Return(nil).Twice() err := bs.UpdateInstallConfig() @@ -768,7 +768,7 @@ var _ = Describe("Installconfig & Secrets", func() { gc.EXPECT().CreatePublicCAExternalAccountKey(mock.Anything).Return("fake-eab-key-id", "fake-eab-mac-key", nil) icg.EXPECT().GenerateSecrets().Return(nil) icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) - icg.EXPECT().WriteVault("fake-secret", true).Return(nil) + icg.EXPECT().WriteUnencryptedVault("fake-secret", true).Return(nil) nodeClient.EXPECT().CopyFile(mock.Anything, mock.Anything, mock.Anything).Return(nil).Twice() err := bs.UpdateInstallConfig() @@ -799,7 +799,7 @@ var _ = Describe("Installconfig & Secrets", func() { It("uses the Let's Encrypt staging directory", func() { icg.EXPECT().GenerateSecrets().Return(nil) icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) - icg.EXPECT().WriteVault("fake-secret", true).Return(nil) + icg.EXPECT().WriteUnencryptedVault("fake-secret", true).Return(nil) nodeClient.EXPECT().CopyFile(mock.Anything, mock.Anything, mock.Anything).Return(nil).Twice() err := bs.UpdateInstallConfig() @@ -818,7 +818,7 @@ var _ = Describe("Installconfig & Secrets", func() { It("sets OpenBao config in install config", func() { icg.EXPECT().GenerateSecrets().Return(nil) icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) - icg.EXPECT().WriteVault("fake-secret", true).Return(nil) + icg.EXPECT().WriteUnencryptedVault("fake-secret", true).Return(nil) nodeClient.EXPECT().CopyFile(mock.Anything, mock.Anything, mock.Anything).Return(nil).Twice() @@ -840,7 +840,7 @@ var _ = Describe("Installconfig & Secrets", func() { It("sets Grafana Alloy Loki config", func() { icg.EXPECT().GenerateSecrets().Return(nil) icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) - icg.EXPECT().WriteVault("fake-secret", true).Return(nil) + icg.EXPECT().WriteUnencryptedVault("fake-secret", true).Return(nil) nodeClient.EXPECT().CopyFile(mock.Anything, mock.Anything, mock.Anything).Return(nil).Twice() @@ -856,7 +856,7 @@ var _ = Describe("Installconfig & Secrets", func() { It("stores External Loki credentials in the vault", func() { icg.EXPECT().GenerateSecrets().Return(nil) icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) - icg.EXPECT().WriteVault("fake-secret", true).Return(nil) + icg.EXPECT().WriteUnencryptedVault("fake-secret", true).Return(nil) nodeClient.EXPECT().CopyFile(mock.Anything, mock.Anything, mock.Anything).Return(nil).Twice() err := bs.UpdateInstallConfig() @@ -875,7 +875,7 @@ var _ = Describe("Installconfig & Secrets", func() { It("sets Grafana Alloy Loki config without a password secret", func() { icg.EXPECT().GenerateSecrets().Return(nil) icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) - icg.EXPECT().WriteVault("fake-secret", true).Return(nil) + icg.EXPECT().WriteUnencryptedVault("fake-secret", true).Return(nil) nodeClient.EXPECT().CopyFile(mock.Anything, mock.Anything, mock.Anything).Return(nil).Twice() @@ -898,7 +898,7 @@ var _ = Describe("Installconfig & Secrets", func() { It("sets Prometheus remote write config", func() { icg.EXPECT().GenerateSecrets().Return(nil) icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) - icg.EXPECT().WriteVault("fake-secret", true).Return(nil) + icg.EXPECT().WriteUnencryptedVault("fake-secret", true).Return(nil) nodeClient.EXPECT().CopyFile(mock.Anything, mock.Anything, mock.Anything).Return(nil).Twice() err := bs.UpdateInstallConfig() @@ -918,7 +918,7 @@ var _ = Describe("Installconfig & Secrets", func() { It("leaves Prometheus remote write nil", func() { icg.EXPECT().GenerateSecrets().Return(nil) icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) - icg.EXPECT().WriteVault("fake-secret", true).Return(nil) + icg.EXPECT().WriteUnencryptedVault("fake-secret", true).Return(nil) nodeClient.EXPECT().CopyFile(mock.Anything, mock.Anything, mock.Anything).Return(nil).Twice() err := bs.UpdateInstallConfig() @@ -937,7 +937,7 @@ var _ = Describe("Installconfig & Secrets", func() { It("sets TelemetryExport with RemoteExport true", func() { icg.EXPECT().GenerateSecrets().Return(nil) icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) - icg.EXPECT().WriteVault("fake-secret", true).Return(nil) + icg.EXPECT().WriteUnencryptedVault("fake-secret", true).Return(nil) nodeClient.EXPECT().CopyFile(mock.Anything, mock.Anything, mock.Anything).Return(nil).Twice() err := bs.UpdateInstallConfig() @@ -960,7 +960,7 @@ var _ = Describe("Installconfig & Secrets", func() { It("sets TelemetryExport with Traces true and RemoteExport false", func() { icg.EXPECT().GenerateSecrets().Return(nil) icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) - icg.EXPECT().WriteVault("fake-secret", true).Return(nil) + icg.EXPECT().WriteUnencryptedVault("fake-secret", true).Return(nil) nodeClient.EXPECT().CopyFile(mock.Anything, mock.Anything, mock.Anything).Return(nil).Twice() err := bs.UpdateInstallConfig() @@ -985,7 +985,7 @@ var _ = Describe("Installconfig & Secrets", func() { It("sets TelemetryExport with both RemoteExport and Traces true", func() { icg.EXPECT().GenerateSecrets().Return(nil) icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) - icg.EXPECT().WriteVault("fake-secret", true).Return(nil) + icg.EXPECT().WriteUnencryptedVault("fake-secret", true).Return(nil) nodeClient.EXPECT().CopyFile(mock.Anything, mock.Anything, mock.Anything).Return(nil).Twice() err := bs.UpdateInstallConfig() @@ -1004,7 +1004,7 @@ var _ = Describe("Installconfig & Secrets", func() { It("leaves TelemetryExport nil", func() { icg.EXPECT().GenerateSecrets().Return(nil) icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) - icg.EXPECT().WriteVault("fake-secret", true).Return(nil) + icg.EXPECT().WriteUnencryptedVault("fake-secret", true).Return(nil) nodeClient.EXPECT().CopyFile(mock.Anything, mock.Anything, mock.Anything).Return(nil).Twice() err := bs.UpdateInstallConfig() @@ -1033,10 +1033,10 @@ var _ = Describe("Installconfig & Secrets", func() { Expect(err.Error()).To(ContainSubstring("failed to write config file")) }) - It("fails when WriteVault fails", func() { + It("fails when WriteUnencryptedVault fails", func() { icg.EXPECT().GenerateSecrets().Return(nil) icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) - icg.EXPECT().WriteVault("fake-secret", true).Return(fmt.Errorf("vault write error")) + icg.EXPECT().WriteUnencryptedVault("fake-secret", true).Return(fmt.Errorf("vault write error")) err := bs.UpdateInstallConfig() Expect(err).To(HaveOccurred()) @@ -1046,7 +1046,7 @@ var _ = Describe("Installconfig & Secrets", func() { It("fails when CopyFile config fails", func() { icg.EXPECT().GenerateSecrets().Return(nil) icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) - icg.EXPECT().WriteVault("fake-secret", true).Return(nil) + icg.EXPECT().WriteUnencryptedVault("fake-secret", true).Return(nil) nodeClient.EXPECT().CopyFile(mock.Anything, mock.Anything, mock.Anything).Return(fmt.Errorf("copy error")).Once() @@ -1058,7 +1058,7 @@ var _ = Describe("Installconfig & Secrets", func() { It("fails when CopyFile secrets fails", func() { icg.EXPECT().GenerateSecrets().Return(nil) icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) - icg.EXPECT().WriteVault("fake-secret", true).Return(nil) + icg.EXPECT().WriteUnencryptedVault("fake-secret", true).Return(nil) nodeClient.EXPECT().CopyFile(mock.Anything, "fake-config-file", mock.Anything).Return(nil).Once() nodeClient.EXPECT().CopyFile(mock.Anything, "fake-secret", mock.Anything).Return(fmt.Errorf("copy error")).Once() @@ -1095,7 +1095,7 @@ var _ = Describe("Installconfig & Secrets", func() { origCert := csEnv.InstallConfig.Postgres.Primary.SSLConfig.ServerCertPem icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) - icg.EXPECT().WriteVault("fake-secret", true).Return(nil) + icg.EXPECT().WriteUnencryptedVault("fake-secret", true).Return(nil) nodeClient.EXPECT().CopyFile(mock.Anything, mock.Anything, mock.Anything).Return(nil).Twice() err := bs.UpdateInstallConfig() @@ -1127,7 +1127,7 @@ var _ = Describe("Installconfig & Secrets", func() { origKey := vault.GetSecret(files.SecretPostgresPrimaryServerKeyPem).File.Content icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) - icg.EXPECT().WriteVault("fake-secret", true).Return(nil) + icg.EXPECT().WriteUnencryptedVault("fake-secret", true).Return(nil) nodeClient.EXPECT().CopyFile(mock.Anything, mock.Anything, mock.Anything).Return(nil).Twice() err := bs.UpdateInstallConfig() @@ -1161,7 +1161,7 @@ var _ = Describe("Installconfig & Secrets", func() { It("generates new cert/key pair", func() { icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) - icg.EXPECT().WriteVault("fake-secret", true).Return(nil) + icg.EXPECT().WriteUnencryptedVault("fake-secret", true).Return(nil) nodeClient.EXPECT().CopyFile(mock.Anything, mock.Anything, mock.Anything).Return(nil).Twice() err := bs.UpdateInstallConfig() diff --git a/internal/bootstrap/local/local.go b/internal/bootstrap/local/local.go index 8be7f17b..d99d8327 100644 --- a/internal/bootstrap/local/local.go +++ b/internal/bootstrap/local/local.go @@ -665,7 +665,7 @@ func (b *LocalBootstrapper) UpdateInstallConfig() (err error) { return fmt.Errorf("failed to write config file: %w", err) } - if err := b.icg.WriteVault(b.Env.SecretsFilePath, true); err != nil { + if err := b.icg.WriteUnencryptedVault(b.Env.SecretsFilePath, true); err != nil { return fmt.Errorf("failed to write vault file: %w", err) } if err := vault.EncryptFileWithSOPS(b.Env.SecretsFilePath, filepath.Join(b.Env.InstallConfig.Secrets.BaseDir, "prod.vault.yaml"), b.ageRecipient); err != nil { diff --git a/internal/installer/config_manager.go b/internal/installer/config_manager.go index 9f077fd2..171e90d1 100644 --- a/internal/installer/config_manager.go +++ b/internal/installer/config_manager.go @@ -42,33 +42,13 @@ type InstallConfigManager interface { GenerateSecrets() error WriteInstallConfig(configPath string, withComments bool) error WriteVault(vaultPath string, withComments bool) error - WriteEncryptedVault(vaultPath string, withComments bool) error -} - -type VaultEncryptor interface { - Encrypt(src, target, recipient string) error -} - -type AgeKeyResolver interface { - Resolve(explicitKeyFile, fallbackDir string) (recipient, keyPath string, err error) -} - -type sopsVaultEncryptor struct{} - -func (sopsVaultEncryptor) Encrypt(src, target, recipient string) error { - return vault.EncryptFileWithSOPS(src, target, recipient) -} - -type sopsAgeKeyResolver struct{} - -func (sopsAgeKeyResolver) Resolve(explicitKeyFile, fallbackDir string) (recipient, keyPath string, err error) { - return vault.ResolveAgeKey(explicitKeyFile, fallbackDir) + WriteUnencryptedVault(vaultPath string, withComments bool) error } type InstallConfig struct { fileIO util.FileIO - vaultEncryptor VaultEncryptor - ageKeyResolver AgeKeyResolver + vaultEncryptor vault.Encryptor + ageKeyResolver vault.AgeKeyResolver Config *files.RootConfig Vault *files.InstallVault } @@ -79,35 +59,35 @@ func (g *InstallConfig) SetFileIO(fio util.FileIO) { } // SetVaultEncryptor overrides vault encryption (useful for testing). -func (g *InstallConfig) SetVaultEncryptor(encryptor VaultEncryptor) { +func (g *InstallConfig) SetVaultEncryptor(encryptor vault.Encryptor) { g.vaultEncryptor = encryptor } // SetAgeKeyResolver overrides age key resolution (useful for testing). -func (g *InstallConfig) SetAgeKeyResolver(resolver AgeKeyResolver) { +func (g *InstallConfig) SetAgeKeyResolver(resolver vault.AgeKeyResolver) { g.ageKeyResolver = resolver } func (g *InstallConfig) encryptVault(src, target, recipient string) error { - if g.vaultEncryptor != nil { - return g.vaultEncryptor.Encrypt(src, target, recipient) + if g.vaultEncryptor == nil { + return fmt.Errorf("vault encryptor is not configured") } - return sopsVaultEncryptor{}.Encrypt(src, target, recipient) + return g.vaultEncryptor.Encrypt(src, target, recipient) } func (g *InstallConfig) resolveAgeKey(explicitKeyFile, fallbackDir string) (recipient, keyPath string, err error) { - if g.ageKeyResolver != nil { - return g.ageKeyResolver.Resolve(explicitKeyFile, fallbackDir) + if g.ageKeyResolver == nil { + return "", "", fmt.Errorf("age key resolver is not configured") } - return sopsAgeKeyResolver{}.Resolve(explicitKeyFile, fallbackDir) + return g.ageKeyResolver.Resolve(explicitKeyFile, fallbackDir) } func NewInstallConfigManager() InstallConfigManager { config := files.NewRootConfig() return &InstallConfig{ fileIO: &util.FilesystemWriter{}, - vaultEncryptor: sopsVaultEncryptor{}, - ageKeyResolver: sopsAgeKeyResolver{}, + vaultEncryptor: vault.SOPSEncryptor{}, + ageKeyResolver: vault.DefaultAgeKeyResolver{}, Config: &config, Vault: &files.InstallVault{}, } @@ -315,7 +295,7 @@ func (g *InstallConfig) WriteInstallConfig(configPath string, withComments bool) return nil } -func (g *InstallConfig) WriteVault(vaultPath string, withComments bool) error { +func (g *InstallConfig) WriteUnencryptedVault(vaultPath string, withComments bool) error { vaultYAML, err := g.marshalVault(vaultPath, withComments) if err != nil { return err @@ -328,7 +308,7 @@ func (g *InstallConfig) WriteVault(vaultPath string, withComments bool) error { return nil } -func (g *InstallConfig) WriteEncryptedVault(vaultPath string, withComments bool) error { +func (g *InstallConfig) WriteVault(vaultPath string, withComments bool) error { vaultYAML, err := g.marshalVault(vaultPath, withComments) if err != nil { return err diff --git a/internal/installer/config_manager_test.go b/internal/installer/config_manager_test.go index d963c741..f363e0cc 100644 --- a/internal/installer/config_manager_test.go +++ b/internal/installer/config_manager_test.go @@ -536,7 +536,7 @@ var _ = Describe("ConfigManager", func() { }) }) - Describe("WriteEncryptedVault", func() { + Describe("WriteVault", func() { It("should preserve the existing vault when encryption fails", func() { vaultPath := "prod.vault.yaml" original := []byte("existing encrypted content") @@ -550,10 +550,23 @@ var _ = Describe("ConfigManager", func() { manager.SetAgeKeyResolver(staticAgeKeyResolver{}) manager.SetVaultEncryptor(failingVaultEncryptor{}) - err := manager.WriteEncryptedVault(vaultPath, false) + err := manager.WriteVault(vaultPath, false) Expect(err).To(HaveOccurred()) Expect(mockIO.GetFileContent(vaultPath)).To(Equal(original)) }) + + It("should fail when the encryptor is not configured", func() { + manager := &installer.InstallConfig{ + Config: &files.RootConfig{}, + Vault: &files.InstallVault{}, + } + manager.SetFileIO(NewMockFileIO()) + manager.SetAgeKeyResolver(staticAgeKeyResolver{}) + manager.SetVaultEncryptor(nil) + + err := manager.WriteVault("prod.vault.yaml", false) + Expect(err).To(MatchError("vault encryptor is not configured")) + }) }) Describe("Integration Tests", func() { @@ -583,7 +596,7 @@ var _ = Describe("ConfigManager", func() { }) Context("vault deduplication on re-write", func() { - It("should not produce duplicate vault entries when WriteVault is called after loading existing vault", func() { + It("should not produce duplicate vault entries when WriteUnencryptedVault is called after loading existing vault", func() { mockIO := NewMockFileIO() configManager.SetFileIO(mockIO) @@ -593,8 +606,8 @@ var _ = Describe("ConfigManager", func() { err = configManager.GenerateSecrets() Expect(err).ToNot(HaveOccurred()) - // First write via WriteVault - err = configManager.WriteVault("/tmp/vault.yaml", false) + // First write via WriteUnencryptedVault + err = configManager.WriteUnencryptedVault("/tmp/vault.yaml", false) Expect(err).ToNot(HaveOccurred()) firstVaultBytes := mockIO.GetFileContent("/tmp/vault.yaml") @@ -607,7 +620,7 @@ var _ = Describe("ConfigManager", func() { configManager.Vault = vault // Re-write vault (simulating a second run) - err = configManager.WriteVault("/tmp/vault.yaml", false) + err = configManager.WriteUnencryptedVault("/tmp/vault.yaml", false) Expect(err).ToNot(HaveOccurred()) secondVaultBytes := mockIO.GetFileContent("/tmp/vault.yaml") @@ -646,7 +659,7 @@ var _ = Describe("ConfigManager", func() { // Write config and vault err = configManager.WriteInstallConfig("/tmp/config.yaml", false) Expect(err).ToNot(HaveOccurred()) - err = configManager.WriteVault("/tmp/vault.yaml", false) + err = configManager.WriteUnencryptedVault("/tmp/vault.yaml", false) Expect(err).ToNot(HaveOccurred()) // --- Second run: simulate loading existing files --- @@ -686,7 +699,7 @@ var _ = Describe("ConfigManager", func() { Expect(err).ToNot(HaveOccurred(), "cert/key should match after load from vault") // Write vault again - err = configManager2.WriteVault("/tmp/vault2.yaml", false) + err = configManager2.WriteUnencryptedVault("/tmp/vault2.yaml", false) Expect(err).ToNot(HaveOccurred()) // Verify no duplicates in re-written vault diff --git a/internal/installer/mocks.go b/internal/installer/mocks.go index d4f62c7f..f60d172b 100644 --- a/internal/installer/mocks.go +++ b/internal/installer/mocks.go @@ -696,12 +696,12 @@ func (_c *MockInstallConfigManager_ValidateVault_Call) RunAndReturn(run func() [ return _c } -// WriteEncryptedVault provides a mock function for the type MockInstallConfigManager -func (_mock *MockInstallConfigManager) WriteEncryptedVault(vaultPath string, withComments bool) error { +// WriteVault provides a mock function for the type MockInstallConfigManager +func (_mock *MockInstallConfigManager) WriteVault(vaultPath string, withComments bool) error { ret := _mock.Called(vaultPath, withComments) if len(ret) == 0 { - panic("no return value specified for WriteEncryptedVault") + panic("no return value specified for WriteVault") } var r0 error @@ -713,19 +713,19 @@ func (_mock *MockInstallConfigManager) WriteEncryptedVault(vaultPath string, wit return r0 } -// MockInstallConfigManager_WriteEncryptedVault_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteEncryptedVault' -type MockInstallConfigManager_WriteEncryptedVault_Call struct { +// MockInstallConfigManager_WriteVault_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteVault' +type MockInstallConfigManager_WriteVault_Call struct { *mock.Call } -// WriteEncryptedVault is a helper method to define mock.On call +// WriteVault is a helper method to define mock.On call // - vaultPath string // - withComments bool -func (_e *MockInstallConfigManager_Expecter) WriteEncryptedVault(vaultPath any, withComments any) *MockInstallConfigManager_WriteEncryptedVault_Call { - return &MockInstallConfigManager_WriteEncryptedVault_Call{Call: _e.mock.On("WriteEncryptedVault", vaultPath, withComments)} +func (_e *MockInstallConfigManager_Expecter) WriteVault(vaultPath any, withComments any) *MockInstallConfigManager_WriteVault_Call { + return &MockInstallConfigManager_WriteVault_Call{Call: _e.mock.On("WriteVault", vaultPath, withComments)} } -func (_c *MockInstallConfigManager_WriteEncryptedVault_Call) Run(run func(vaultPath string, withComments bool)) *MockInstallConfigManager_WriteEncryptedVault_Call { +func (_c *MockInstallConfigManager_WriteVault_Call) Run(run func(vaultPath string, withComments bool)) *MockInstallConfigManager_WriteVault_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { @@ -743,12 +743,12 @@ func (_c *MockInstallConfigManager_WriteEncryptedVault_Call) Run(run func(vaultP return _c } -func (_c *MockInstallConfigManager_WriteEncryptedVault_Call) Return(err error) *MockInstallConfigManager_WriteEncryptedVault_Call { +func (_c *MockInstallConfigManager_WriteVault_Call) Return(err error) *MockInstallConfigManager_WriteVault_Call { _c.Call.Return(err) return _c } -func (_c *MockInstallConfigManager_WriteEncryptedVault_Call) RunAndReturn(run func(vaultPath string, withComments bool) error) *MockInstallConfigManager_WriteEncryptedVault_Call { +func (_c *MockInstallConfigManager_WriteVault_Call) RunAndReturn(run func(vaultPath string, withComments bool) error) *MockInstallConfigManager_WriteVault_Call { _c.Call.Return(run) return _c } @@ -810,12 +810,12 @@ func (_c *MockInstallConfigManager_WriteInstallConfig_Call) RunAndReturn(run fun return _c } -// WriteVault provides a mock function for the type MockInstallConfigManager -func (_mock *MockInstallConfigManager) WriteVault(vaultPath string, withComments bool) error { +// WriteUnencryptedVault provides a mock function for the type MockInstallConfigManager +func (_mock *MockInstallConfigManager) WriteUnencryptedVault(vaultPath string, withComments bool) error { ret := _mock.Called(vaultPath, withComments) if len(ret) == 0 { - panic("no return value specified for WriteVault") + panic("no return value specified for WriteUnencryptedVault") } var r0 error @@ -827,19 +827,19 @@ func (_mock *MockInstallConfigManager) WriteVault(vaultPath string, withComments return r0 } -// MockInstallConfigManager_WriteVault_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteVault' -type MockInstallConfigManager_WriteVault_Call struct { +// MockInstallConfigManager_WriteUnencryptedVault_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteUnencryptedVault' +type MockInstallConfigManager_WriteUnencryptedVault_Call struct { *mock.Call } -// WriteVault is a helper method to define mock.On call +// WriteUnencryptedVault is a helper method to define mock.On call // - vaultPath string // - withComments bool -func (_e *MockInstallConfigManager_Expecter) WriteVault(vaultPath any, withComments any) *MockInstallConfigManager_WriteVault_Call { - return &MockInstallConfigManager_WriteVault_Call{Call: _e.mock.On("WriteVault", vaultPath, withComments)} +func (_e *MockInstallConfigManager_Expecter) WriteUnencryptedVault(vaultPath any, withComments any) *MockInstallConfigManager_WriteUnencryptedVault_Call { + return &MockInstallConfigManager_WriteUnencryptedVault_Call{Call: _e.mock.On("WriteUnencryptedVault", vaultPath, withComments)} } -func (_c *MockInstallConfigManager_WriteVault_Call) Run(run func(vaultPath string, withComments bool)) *MockInstallConfigManager_WriteVault_Call { +func (_c *MockInstallConfigManager_WriteUnencryptedVault_Call) Run(run func(vaultPath string, withComments bool)) *MockInstallConfigManager_WriteUnencryptedVault_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { @@ -857,12 +857,12 @@ func (_c *MockInstallConfigManager_WriteVault_Call) Run(run func(vaultPath strin return _c } -func (_c *MockInstallConfigManager_WriteVault_Call) Return(err error) *MockInstallConfigManager_WriteVault_Call { +func (_c *MockInstallConfigManager_WriteUnencryptedVault_Call) Return(err error) *MockInstallConfigManager_WriteUnencryptedVault_Call { _c.Call.Return(err) return _c } -func (_c *MockInstallConfigManager_WriteVault_Call) RunAndReturn(run func(vaultPath string, withComments bool) error) *MockInstallConfigManager_WriteVault_Call { +func (_c *MockInstallConfigManager_WriteUnencryptedVault_Call) RunAndReturn(run func(vaultPath string, withComments bool) error) *MockInstallConfigManager_WriteUnencryptedVault_Call { _c.Call.Return(run) return _c } diff --git a/internal/installer/vault/vault_encryption.go b/internal/installer/vault/vault_encryption.go index bc64d864..071369fe 100644 --- a/internal/installer/vault/vault_encryption.go +++ b/internal/installer/vault/vault_encryption.go @@ -19,6 +19,30 @@ import ( var xdgConfigHome = "XDG_CONFIG_HOME" +// Encryptor encrypts a plaintext vault for an age recipient. +type Encryptor interface { + Encrypt(src, target, recipient string) error +} + +// AgeKeyResolver finds the age recipient used to encrypt a vault. +type AgeKeyResolver interface { + Resolve(explicitKeyFile, fallbackDir string) (recipient, keyPath string, err error) +} + +// SOPSEncryptor encrypts vaults using SOPS and age. +type SOPSEncryptor struct{} + +func (SOPSEncryptor) Encrypt(src, target, recipient string) error { + return EncryptFileWithSOPS(src, target, recipient) +} + +// DefaultAgeKeyResolver resolves age keys from the standard SOPS locations. +type DefaultAgeKeyResolver struct{} + +func (DefaultAgeKeyResolver) Resolve(explicitKeyFile, fallbackDir string) (recipient, keyPath string, err error) { + return ResolveAgeKey(explicitKeyFile, fallbackDir) +} + // ResolveAgeKey resolves an existing age key or generates a new one. // // When explicitKeyFile is non-empty it takes priority over everything else: the From 6b11cc30c024649bbd6dbf2b79c028fd3abbe22d Mon Sep 17 00:00:00 2001 From: Manuel Dewald Date: Wed, 5 Aug 2026 11:02:41 +0200 Subject: [PATCH 8/9] test(installer): use generated vault mocks --- internal/installer/config_manager_test.go | 38 ++-- internal/installer/vault/mocks.go | 198 +++++++++++++++++++ internal/installer/vault/vault_encryption.go | 4 + 3 files changed, 213 insertions(+), 27 deletions(-) create mode 100644 internal/installer/vault/mocks.go diff --git a/internal/installer/config_manager_test.go b/internal/installer/config_manager_test.go index f363e0cc..077db592 100644 --- a/internal/installer/config_manager_test.go +++ b/internal/installer/config_manager_test.go @@ -15,6 +15,7 @@ import ( "github.com/codesphere-cloud/oms/internal/installer" "github.com/codesphere-cloud/oms/internal/installer/files" "github.com/codesphere-cloud/oms/internal/installer/secrets" + "github.com/codesphere-cloud/oms/internal/installer/vault" ) type MockFileIO struct { @@ -27,18 +28,6 @@ type MockFileIO struct { mkdirAllError error } -type failingVaultEncryptor struct{} - -func (failingVaultEncryptor) Encrypt(_, _, _ string) error { - return errors.New("encryption failed") -} - -type staticAgeKeyResolver struct{} - -func (staticAgeKeyResolver) Resolve(_, _ string) (string, string, error) { - return "recipient", "", nil -} - func NewMockFileIO() *MockFileIO { return &MockFileIO{ files: make(map[string][]byte), @@ -546,27 +535,22 @@ var _ = Describe("ConfigManager", func() { Config: &files.RootConfig{}, Vault: &files.InstallVault{}, } + ageKeyResolver := vault.NewMockAgeKeyResolver(GinkgoT()) + ageKeyResolver.EXPECT().Resolve("", ".").Return("recipient", "", nil) + encryptor := vault.NewMockEncryptor(GinkgoT()) + encryptor.EXPECT().Encrypt( + ".prod.vault.yaml.plaintext-*mock", + ".prod.vault.yaml.encrypted-*mock", + "recipient", + ).Return(errors.New("encryption failed")) manager.SetFileIO(mockIO) - manager.SetAgeKeyResolver(staticAgeKeyResolver{}) - manager.SetVaultEncryptor(failingVaultEncryptor{}) + manager.SetAgeKeyResolver(ageKeyResolver) + manager.SetVaultEncryptor(encryptor) err := manager.WriteVault(vaultPath, false) Expect(err).To(HaveOccurred()) Expect(mockIO.GetFileContent(vaultPath)).To(Equal(original)) }) - - It("should fail when the encryptor is not configured", func() { - manager := &installer.InstallConfig{ - Config: &files.RootConfig{}, - Vault: &files.InstallVault{}, - } - manager.SetFileIO(NewMockFileIO()) - manager.SetAgeKeyResolver(staticAgeKeyResolver{}) - manager.SetVaultEncryptor(nil) - - err := manager.WriteVault("prod.vault.yaml", false) - Expect(err).To(MatchError("vault encryptor is not configured")) - }) }) Describe("Integration Tests", func() { diff --git a/internal/installer/vault/mocks.go b/internal/installer/vault/mocks.go new file mode 100644 index 00000000..223dbc4c --- /dev/null +++ b/internal/installer/vault/mocks.go @@ -0,0 +1,198 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package vault + +import ( + mock "github.com/stretchr/testify/mock" +) + +// NewMockAgeKeyResolver creates a new instance of MockAgeKeyResolver. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockAgeKeyResolver(t interface { + mock.TestingT + Cleanup(func()) +}) *MockAgeKeyResolver { + mock := &MockAgeKeyResolver{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// MockAgeKeyResolver is an autogenerated mock type for the AgeKeyResolver type +type MockAgeKeyResolver struct { + mock.Mock +} + +type MockAgeKeyResolver_Expecter struct { + mock *mock.Mock +} + +func (_m *MockAgeKeyResolver) EXPECT() *MockAgeKeyResolver_Expecter { + return &MockAgeKeyResolver_Expecter{mock: &_m.Mock} +} + +// Resolve provides a mock function for the type MockAgeKeyResolver +func (_mock *MockAgeKeyResolver) Resolve(explicitKeyFile string, fallbackDir string) (string, string, error) { + ret := _mock.Called(explicitKeyFile, fallbackDir) + + if len(ret) == 0 { + panic("no return value specified for Resolve") + } + + var r0 string + var r1 string + var r2 error + if returnFunc, ok := ret.Get(0).(func(string, string) (string, string, error)); ok { + return returnFunc(explicitKeyFile, fallbackDir) + } + if returnFunc, ok := ret.Get(0).(func(string, string) string); ok { + r0 = returnFunc(explicitKeyFile, fallbackDir) + } else { + r0 = ret.Get(0).(string) + } + if returnFunc, ok := ret.Get(1).(func(string, string) string); ok { + r1 = returnFunc(explicitKeyFile, fallbackDir) + } else { + r1 = ret.Get(1).(string) + } + if returnFunc, ok := ret.Get(2).(func(string, string) error); ok { + r2 = returnFunc(explicitKeyFile, fallbackDir) + } else { + r2 = ret.Error(2) + } + return r0, r1, r2 +} + +// MockAgeKeyResolver_Resolve_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Resolve' +type MockAgeKeyResolver_Resolve_Call struct { + *mock.Call +} + +// Resolve is a helper method to define mock.On call +// - explicitKeyFile string +// - fallbackDir string +func (_e *MockAgeKeyResolver_Expecter) Resolve(explicitKeyFile any, fallbackDir any) *MockAgeKeyResolver_Resolve_Call { + return &MockAgeKeyResolver_Resolve_Call{Call: _e.mock.On("Resolve", explicitKeyFile, fallbackDir)} +} + +func (_c *MockAgeKeyResolver_Resolve_Call) Run(run func(explicitKeyFile string, fallbackDir string)) *MockAgeKeyResolver_Resolve_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockAgeKeyResolver_Resolve_Call) Return(recipient string, keyPath string, err error) *MockAgeKeyResolver_Resolve_Call { + _c.Call.Return(recipient, keyPath, err) + return _c +} + +func (_c *MockAgeKeyResolver_Resolve_Call) RunAndReturn(run func(explicitKeyFile string, fallbackDir string) (string, string, error)) *MockAgeKeyResolver_Resolve_Call { + _c.Call.Return(run) + return _c +} + +// NewMockEncryptor creates a new instance of MockEncryptor. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockEncryptor(t interface { + mock.TestingT + Cleanup(func()) +}) *MockEncryptor { + mock := &MockEncryptor{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// MockEncryptor is an autogenerated mock type for the Encryptor type +type MockEncryptor struct { + mock.Mock +} + +type MockEncryptor_Expecter struct { + mock *mock.Mock +} + +func (_m *MockEncryptor) EXPECT() *MockEncryptor_Expecter { + return &MockEncryptor_Expecter{mock: &_m.Mock} +} + +// Encrypt provides a mock function for the type MockEncryptor +func (_mock *MockEncryptor) Encrypt(src string, target string, recipient string) error { + ret := _mock.Called(src, target, recipient) + + if len(ret) == 0 { + panic("no return value specified for Encrypt") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(string, string, string) error); ok { + r0 = returnFunc(src, target, recipient) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockEncryptor_Encrypt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Encrypt' +type MockEncryptor_Encrypt_Call struct { + *mock.Call +} + +// Encrypt is a helper method to define mock.On call +// - src string +// - target string +// - recipient string +func (_e *MockEncryptor_Expecter) Encrypt(src any, target any, recipient any) *MockEncryptor_Encrypt_Call { + return &MockEncryptor_Encrypt_Call{Call: _e.mock.On("Encrypt", src, target, recipient)} +} + +func (_c *MockEncryptor_Encrypt_Call) Run(run func(src string, target string, recipient string)) *MockEncryptor_Encrypt_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *MockEncryptor_Encrypt_Call) Return(err error) *MockEncryptor_Encrypt_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockEncryptor_Encrypt_Call) RunAndReturn(run func(src string, target string, recipient string) error) *MockEncryptor_Encrypt_Call { + _c.Call.Return(run) + return _c +} diff --git a/internal/installer/vault/vault_encryption.go b/internal/installer/vault/vault_encryption.go index 071369fe..b82be612 100644 --- a/internal/installer/vault/vault_encryption.go +++ b/internal/installer/vault/vault_encryption.go @@ -20,11 +20,15 @@ import ( var xdgConfigHome = "XDG_CONFIG_HOME" // Encryptor encrypts a plaintext vault for an age recipient. +// +//mockery:generate: true type Encryptor interface { Encrypt(src, target, recipient string) error } // AgeKeyResolver finds the age recipient used to encrypt a vault. +// +//mockery:generate: true type AgeKeyResolver interface { Resolve(explicitKeyFile, fallbackDir string) (recipient, keyPath string, err error) } From e047a32bdaa99049882777a1a35701e293a5527f Mon Sep 17 00:00:00 2001 From: NautiluX <2600004+NautiluX@users.noreply.github.com> Date: Wed, 5 Aug 2026 11:46:28 +0000 Subject: [PATCH 9/9] chore(docs): Auto-update docs and licenses Signed-off-by: NautiluX <2600004+NautiluX@users.noreply.github.com> --- internal/installer/mocks.go | 78 +++++++-------- internal/installer/vault/mocks.go | 158 +++++++++++++++--------------- 2 files changed, 118 insertions(+), 118 deletions(-) diff --git a/internal/installer/mocks.go b/internal/installer/mocks.go index f60d172b..34455727 100644 --- a/internal/installer/mocks.go +++ b/internal/installer/mocks.go @@ -696,36 +696,36 @@ func (_c *MockInstallConfigManager_ValidateVault_Call) RunAndReturn(run func() [ return _c } -// WriteVault provides a mock function for the type MockInstallConfigManager -func (_mock *MockInstallConfigManager) WriteVault(vaultPath string, withComments bool) error { - ret := _mock.Called(vaultPath, withComments) +// WriteInstallConfig provides a mock function for the type MockInstallConfigManager +func (_mock *MockInstallConfigManager) WriteInstallConfig(configPath string, withComments bool) error { + ret := _mock.Called(configPath, withComments) if len(ret) == 0 { - panic("no return value specified for WriteVault") + panic("no return value specified for WriteInstallConfig") } var r0 error if returnFunc, ok := ret.Get(0).(func(string, bool) error); ok { - r0 = returnFunc(vaultPath, withComments) + r0 = returnFunc(configPath, withComments) } else { r0 = ret.Error(0) } return r0 } -// MockInstallConfigManager_WriteVault_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteVault' -type MockInstallConfigManager_WriteVault_Call struct { +// MockInstallConfigManager_WriteInstallConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteInstallConfig' +type MockInstallConfigManager_WriteInstallConfig_Call struct { *mock.Call } -// WriteVault is a helper method to define mock.On call -// - vaultPath string +// WriteInstallConfig is a helper method to define mock.On call +// - configPath string // - withComments bool -func (_e *MockInstallConfigManager_Expecter) WriteVault(vaultPath any, withComments any) *MockInstallConfigManager_WriteVault_Call { - return &MockInstallConfigManager_WriteVault_Call{Call: _e.mock.On("WriteVault", vaultPath, withComments)} +func (_e *MockInstallConfigManager_Expecter) WriteInstallConfig(configPath any, withComments any) *MockInstallConfigManager_WriteInstallConfig_Call { + return &MockInstallConfigManager_WriteInstallConfig_Call{Call: _e.mock.On("WriteInstallConfig", configPath, withComments)} } -func (_c *MockInstallConfigManager_WriteVault_Call) Run(run func(vaultPath string, withComments bool)) *MockInstallConfigManager_WriteVault_Call { +func (_c *MockInstallConfigManager_WriteInstallConfig_Call) Run(run func(configPath string, withComments bool)) *MockInstallConfigManager_WriteInstallConfig_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { @@ -743,46 +743,46 @@ func (_c *MockInstallConfigManager_WriteVault_Call) Run(run func(vaultPath strin return _c } -func (_c *MockInstallConfigManager_WriteVault_Call) Return(err error) *MockInstallConfigManager_WriteVault_Call { +func (_c *MockInstallConfigManager_WriteInstallConfig_Call) Return(err error) *MockInstallConfigManager_WriteInstallConfig_Call { _c.Call.Return(err) return _c } -func (_c *MockInstallConfigManager_WriteVault_Call) RunAndReturn(run func(vaultPath string, withComments bool) error) *MockInstallConfigManager_WriteVault_Call { +func (_c *MockInstallConfigManager_WriteInstallConfig_Call) RunAndReturn(run func(configPath string, withComments bool) error) *MockInstallConfigManager_WriteInstallConfig_Call { _c.Call.Return(run) return _c } -// WriteInstallConfig provides a mock function for the type MockInstallConfigManager -func (_mock *MockInstallConfigManager) WriteInstallConfig(configPath string, withComments bool) error { - ret := _mock.Called(configPath, withComments) +// WriteUnencryptedVault provides a mock function for the type MockInstallConfigManager +func (_mock *MockInstallConfigManager) WriteUnencryptedVault(vaultPath string, withComments bool) error { + ret := _mock.Called(vaultPath, withComments) if len(ret) == 0 { - panic("no return value specified for WriteInstallConfig") + panic("no return value specified for WriteUnencryptedVault") } var r0 error if returnFunc, ok := ret.Get(0).(func(string, bool) error); ok { - r0 = returnFunc(configPath, withComments) + r0 = returnFunc(vaultPath, withComments) } else { r0 = ret.Error(0) } return r0 } -// MockInstallConfigManager_WriteInstallConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteInstallConfig' -type MockInstallConfigManager_WriteInstallConfig_Call struct { +// MockInstallConfigManager_WriteUnencryptedVault_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteUnencryptedVault' +type MockInstallConfigManager_WriteUnencryptedVault_Call struct { *mock.Call } -// WriteInstallConfig is a helper method to define mock.On call -// - configPath string +// WriteUnencryptedVault is a helper method to define mock.On call +// - vaultPath string // - withComments bool -func (_e *MockInstallConfigManager_Expecter) WriteInstallConfig(configPath any, withComments any) *MockInstallConfigManager_WriteInstallConfig_Call { - return &MockInstallConfigManager_WriteInstallConfig_Call{Call: _e.mock.On("WriteInstallConfig", configPath, withComments)} +func (_e *MockInstallConfigManager_Expecter) WriteUnencryptedVault(vaultPath any, withComments any) *MockInstallConfigManager_WriteUnencryptedVault_Call { + return &MockInstallConfigManager_WriteUnencryptedVault_Call{Call: _e.mock.On("WriteUnencryptedVault", vaultPath, withComments)} } -func (_c *MockInstallConfigManager_WriteInstallConfig_Call) Run(run func(configPath string, withComments bool)) *MockInstallConfigManager_WriteInstallConfig_Call { +func (_c *MockInstallConfigManager_WriteUnencryptedVault_Call) Run(run func(vaultPath string, withComments bool)) *MockInstallConfigManager_WriteUnencryptedVault_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { @@ -800,22 +800,22 @@ func (_c *MockInstallConfigManager_WriteInstallConfig_Call) Run(run func(configP return _c } -func (_c *MockInstallConfigManager_WriteInstallConfig_Call) Return(err error) *MockInstallConfigManager_WriteInstallConfig_Call { +func (_c *MockInstallConfigManager_WriteUnencryptedVault_Call) Return(err error) *MockInstallConfigManager_WriteUnencryptedVault_Call { _c.Call.Return(err) return _c } -func (_c *MockInstallConfigManager_WriteInstallConfig_Call) RunAndReturn(run func(configPath string, withComments bool) error) *MockInstallConfigManager_WriteInstallConfig_Call { +func (_c *MockInstallConfigManager_WriteUnencryptedVault_Call) RunAndReturn(run func(vaultPath string, withComments bool) error) *MockInstallConfigManager_WriteUnencryptedVault_Call { _c.Call.Return(run) return _c } -// WriteUnencryptedVault provides a mock function for the type MockInstallConfigManager -func (_mock *MockInstallConfigManager) WriteUnencryptedVault(vaultPath string, withComments bool) error { +// WriteVault provides a mock function for the type MockInstallConfigManager +func (_mock *MockInstallConfigManager) WriteVault(vaultPath string, withComments bool) error { ret := _mock.Called(vaultPath, withComments) if len(ret) == 0 { - panic("no return value specified for WriteUnencryptedVault") + panic("no return value specified for WriteVault") } var r0 error @@ -827,19 +827,19 @@ func (_mock *MockInstallConfigManager) WriteUnencryptedVault(vaultPath string, w return r0 } -// MockInstallConfigManager_WriteUnencryptedVault_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteUnencryptedVault' -type MockInstallConfigManager_WriteUnencryptedVault_Call struct { +// MockInstallConfigManager_WriteVault_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteVault' +type MockInstallConfigManager_WriteVault_Call struct { *mock.Call } -// WriteUnencryptedVault is a helper method to define mock.On call +// WriteVault is a helper method to define mock.On call // - vaultPath string // - withComments bool -func (_e *MockInstallConfigManager_Expecter) WriteUnencryptedVault(vaultPath any, withComments any) *MockInstallConfigManager_WriteUnencryptedVault_Call { - return &MockInstallConfigManager_WriteUnencryptedVault_Call{Call: _e.mock.On("WriteUnencryptedVault", vaultPath, withComments)} +func (_e *MockInstallConfigManager_Expecter) WriteVault(vaultPath any, withComments any) *MockInstallConfigManager_WriteVault_Call { + return &MockInstallConfigManager_WriteVault_Call{Call: _e.mock.On("WriteVault", vaultPath, withComments)} } -func (_c *MockInstallConfigManager_WriteUnencryptedVault_Call) Run(run func(vaultPath string, withComments bool)) *MockInstallConfigManager_WriteUnencryptedVault_Call { +func (_c *MockInstallConfigManager_WriteVault_Call) Run(run func(vaultPath string, withComments bool)) *MockInstallConfigManager_WriteVault_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { @@ -857,12 +857,12 @@ func (_c *MockInstallConfigManager_WriteUnencryptedVault_Call) Run(run func(vaul return _c } -func (_c *MockInstallConfigManager_WriteUnencryptedVault_Call) Return(err error) *MockInstallConfigManager_WriteUnencryptedVault_Call { +func (_c *MockInstallConfigManager_WriteVault_Call) Return(err error) *MockInstallConfigManager_WriteVault_Call { _c.Call.Return(err) return _c } -func (_c *MockInstallConfigManager_WriteUnencryptedVault_Call) RunAndReturn(run func(vaultPath string, withComments bool) error) *MockInstallConfigManager_WriteUnencryptedVault_Call { +func (_c *MockInstallConfigManager_WriteVault_Call) RunAndReturn(run func(vaultPath string, withComments bool) error) *MockInstallConfigManager_WriteVault_Call { _c.Call.Return(run) return _c } diff --git a/internal/installer/vault/mocks.go b/internal/installer/vault/mocks.go index 223dbc4c..75bd5c16 100644 --- a/internal/installer/vault/mocks.go +++ b/internal/installer/vault/mocks.go @@ -8,13 +8,13 @@ import ( mock "github.com/stretchr/testify/mock" ) -// NewMockAgeKeyResolver creates a new instance of MockAgeKeyResolver. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// NewMockEncryptor creates a new instance of MockEncryptor. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. -func NewMockAgeKeyResolver(t interface { +func NewMockEncryptor(t interface { mock.TestingT Cleanup(func()) -}) *MockAgeKeyResolver { - mock := &MockAgeKeyResolver{} +}) *MockEncryptor { + mock := &MockEncryptor{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) @@ -22,64 +22,50 @@ func NewMockAgeKeyResolver(t interface { return mock } -// MockAgeKeyResolver is an autogenerated mock type for the AgeKeyResolver type -type MockAgeKeyResolver struct { +// MockEncryptor is an autogenerated mock type for the Encryptor type +type MockEncryptor struct { mock.Mock } -type MockAgeKeyResolver_Expecter struct { +type MockEncryptor_Expecter struct { mock *mock.Mock } -func (_m *MockAgeKeyResolver) EXPECT() *MockAgeKeyResolver_Expecter { - return &MockAgeKeyResolver_Expecter{mock: &_m.Mock} +func (_m *MockEncryptor) EXPECT() *MockEncryptor_Expecter { + return &MockEncryptor_Expecter{mock: &_m.Mock} } -// Resolve provides a mock function for the type MockAgeKeyResolver -func (_mock *MockAgeKeyResolver) Resolve(explicitKeyFile string, fallbackDir string) (string, string, error) { - ret := _mock.Called(explicitKeyFile, fallbackDir) +// Encrypt provides a mock function for the type MockEncryptor +func (_mock *MockEncryptor) Encrypt(src string, target string, recipient string) error { + ret := _mock.Called(src, target, recipient) if len(ret) == 0 { - panic("no return value specified for Resolve") + panic("no return value specified for Encrypt") } - var r0 string - var r1 string - var r2 error - if returnFunc, ok := ret.Get(0).(func(string, string) (string, string, error)); ok { - return returnFunc(explicitKeyFile, fallbackDir) - } - if returnFunc, ok := ret.Get(0).(func(string, string) string); ok { - r0 = returnFunc(explicitKeyFile, fallbackDir) - } else { - r0 = ret.Get(0).(string) - } - if returnFunc, ok := ret.Get(1).(func(string, string) string); ok { - r1 = returnFunc(explicitKeyFile, fallbackDir) - } else { - r1 = ret.Get(1).(string) - } - if returnFunc, ok := ret.Get(2).(func(string, string) error); ok { - r2 = returnFunc(explicitKeyFile, fallbackDir) + var r0 error + if returnFunc, ok := ret.Get(0).(func(string, string, string) error); ok { + r0 = returnFunc(src, target, recipient) } else { - r2 = ret.Error(2) + r0 = ret.Error(0) } - return r0, r1, r2 + return r0 } -// MockAgeKeyResolver_Resolve_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Resolve' -type MockAgeKeyResolver_Resolve_Call struct { +// MockEncryptor_Encrypt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Encrypt' +type MockEncryptor_Encrypt_Call struct { *mock.Call } -// Resolve is a helper method to define mock.On call -// - explicitKeyFile string -// - fallbackDir string -func (_e *MockAgeKeyResolver_Expecter) Resolve(explicitKeyFile any, fallbackDir any) *MockAgeKeyResolver_Resolve_Call { - return &MockAgeKeyResolver_Resolve_Call{Call: _e.mock.On("Resolve", explicitKeyFile, fallbackDir)} +// Encrypt is a helper method to define mock.On call +// - src string +// - target string +// - recipient string +func (_e *MockEncryptor_Expecter) Encrypt(src any, target any, recipient any) *MockEncryptor_Encrypt_Call { + return &MockEncryptor_Encrypt_Call{Call: _e.mock.On("Encrypt", src, target, recipient)} } -func (_c *MockAgeKeyResolver_Resolve_Call) Run(run func(explicitKeyFile string, fallbackDir string)) *MockAgeKeyResolver_Resolve_Call { +func (_c *MockEncryptor_Encrypt_Call) Run(run func(src string, target string, recipient string)) *MockEncryptor_Encrypt_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { @@ -89,31 +75,36 @@ func (_c *MockAgeKeyResolver_Resolve_Call) Run(run func(explicitKeyFile string, if args[1] != nil { arg1 = args[1].(string) } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } run( arg0, arg1, + arg2, ) }) return _c } -func (_c *MockAgeKeyResolver_Resolve_Call) Return(recipient string, keyPath string, err error) *MockAgeKeyResolver_Resolve_Call { - _c.Call.Return(recipient, keyPath, err) +func (_c *MockEncryptor_Encrypt_Call) Return(err error) *MockEncryptor_Encrypt_Call { + _c.Call.Return(err) return _c } -func (_c *MockAgeKeyResolver_Resolve_Call) RunAndReturn(run func(explicitKeyFile string, fallbackDir string) (string, string, error)) *MockAgeKeyResolver_Resolve_Call { +func (_c *MockEncryptor_Encrypt_Call) RunAndReturn(run func(src string, target string, recipient string) error) *MockEncryptor_Encrypt_Call { _c.Call.Return(run) return _c } -// NewMockEncryptor creates a new instance of MockEncryptor. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// NewMockAgeKeyResolver creates a new instance of MockAgeKeyResolver. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. -func NewMockEncryptor(t interface { +func NewMockAgeKeyResolver(t interface { mock.TestingT Cleanup(func()) -}) *MockEncryptor { - mock := &MockEncryptor{} +}) *MockAgeKeyResolver { + mock := &MockAgeKeyResolver{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) @@ -121,50 +112,64 @@ func NewMockEncryptor(t interface { return mock } -// MockEncryptor is an autogenerated mock type for the Encryptor type -type MockEncryptor struct { +// MockAgeKeyResolver is an autogenerated mock type for the AgeKeyResolver type +type MockAgeKeyResolver struct { mock.Mock } -type MockEncryptor_Expecter struct { +type MockAgeKeyResolver_Expecter struct { mock *mock.Mock } -func (_m *MockEncryptor) EXPECT() *MockEncryptor_Expecter { - return &MockEncryptor_Expecter{mock: &_m.Mock} +func (_m *MockAgeKeyResolver) EXPECT() *MockAgeKeyResolver_Expecter { + return &MockAgeKeyResolver_Expecter{mock: &_m.Mock} } -// Encrypt provides a mock function for the type MockEncryptor -func (_mock *MockEncryptor) Encrypt(src string, target string, recipient string) error { - ret := _mock.Called(src, target, recipient) +// Resolve provides a mock function for the type MockAgeKeyResolver +func (_mock *MockAgeKeyResolver) Resolve(explicitKeyFile string, fallbackDir string) (string, string, error) { + ret := _mock.Called(explicitKeyFile, fallbackDir) if len(ret) == 0 { - panic("no return value specified for Encrypt") + panic("no return value specified for Resolve") } - var r0 error - if returnFunc, ok := ret.Get(0).(func(string, string, string) error); ok { - r0 = returnFunc(src, target, recipient) + var r0 string + var r1 string + var r2 error + if returnFunc, ok := ret.Get(0).(func(string, string) (string, string, error)); ok { + return returnFunc(explicitKeyFile, fallbackDir) + } + if returnFunc, ok := ret.Get(0).(func(string, string) string); ok { + r0 = returnFunc(explicitKeyFile, fallbackDir) } else { - r0 = ret.Error(0) + r0 = ret.Get(0).(string) } - return r0 + if returnFunc, ok := ret.Get(1).(func(string, string) string); ok { + r1 = returnFunc(explicitKeyFile, fallbackDir) + } else { + r1 = ret.Get(1).(string) + } + if returnFunc, ok := ret.Get(2).(func(string, string) error); ok { + r2 = returnFunc(explicitKeyFile, fallbackDir) + } else { + r2 = ret.Error(2) + } + return r0, r1, r2 } -// MockEncryptor_Encrypt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Encrypt' -type MockEncryptor_Encrypt_Call struct { +// MockAgeKeyResolver_Resolve_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Resolve' +type MockAgeKeyResolver_Resolve_Call struct { *mock.Call } -// Encrypt is a helper method to define mock.On call -// - src string -// - target string -// - recipient string -func (_e *MockEncryptor_Expecter) Encrypt(src any, target any, recipient any) *MockEncryptor_Encrypt_Call { - return &MockEncryptor_Encrypt_Call{Call: _e.mock.On("Encrypt", src, target, recipient)} +// Resolve is a helper method to define mock.On call +// - explicitKeyFile string +// - fallbackDir string +func (_e *MockAgeKeyResolver_Expecter) Resolve(explicitKeyFile any, fallbackDir any) *MockAgeKeyResolver_Resolve_Call { + return &MockAgeKeyResolver_Resolve_Call{Call: _e.mock.On("Resolve", explicitKeyFile, fallbackDir)} } -func (_c *MockEncryptor_Encrypt_Call) Run(run func(src string, target string, recipient string)) *MockEncryptor_Encrypt_Call { +func (_c *MockAgeKeyResolver_Resolve_Call) Run(run func(explicitKeyFile string, fallbackDir string)) *MockAgeKeyResolver_Resolve_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { @@ -174,25 +179,20 @@ func (_c *MockEncryptor_Encrypt_Call) Run(run func(src string, target string, re if args[1] != nil { arg1 = args[1].(string) } - var arg2 string - if args[2] != nil { - arg2 = args[2].(string) - } run( arg0, arg1, - arg2, ) }) return _c } -func (_c *MockEncryptor_Encrypt_Call) Return(err error) *MockEncryptor_Encrypt_Call { - _c.Call.Return(err) +func (_c *MockAgeKeyResolver_Resolve_Call) Return(recipient string, keyPath string, err error) *MockAgeKeyResolver_Resolve_Call { + _c.Call.Return(recipient, keyPath, err) return _c } -func (_c *MockEncryptor_Encrypt_Call) RunAndReturn(run func(src string, target string, recipient string) error) *MockEncryptor_Encrypt_Call { +func (_c *MockAgeKeyResolver_Resolve_Call) RunAndReturn(run func(explicitKeyFile string, fallbackDir string) (string, string, error)) *MockAgeKeyResolver_Resolve_Call { _c.Call.Return(run) return _c }