From 19ea435a0b9849b92c18aaecd267569805f2e1f1 Mon Sep 17 00:00:00 2001 From: Tim Schrodi Date: Wed, 27 May 2026 22:23:02 +0200 Subject: [PATCH 1/2] feat(installer): support templated install configs from vault Render install config templates through a shared configtemplating package. Add vault-backed secret lookup with explicit vault path handling for install and bootstrap flows. Signed-off-by: Tim Schrodi --- cli/cmd/config.go | 26 +++ cli/cmd/config_template.go | 87 ++++++++++ cli/cmd/config_template_test.go | 114 +++++++++++++ cli/cmd/init_install_config_test.go | 26 ++- cli/cmd/install_codesphere.go | 43 +++++ cli/cmd/install_codesphere_test.go | 4 + cli/cmd/root.go | 1 + cli/cmd/update_install_config_test.go | 37 ++++- docs/README.md | 2 +- docs/examples/install-config-templating.yaml | 22 +++ docs/oms.md | 2 +- docs/oms_beta_bootstrap-gcp.md | 1 - docs/oms_beta_bootstrap-local.md | 1 - docs/oms_config.md | 19 +++ docs/oms_config_template.md | 35 ++++ docs/oms_install_codesphere.md | 2 +- internal/bootstrap/gcp/gcp_test.go | 13 ++ internal/bootstrap/gcp/install_config.go | 16 ++ internal/bootstrap/gcp/install_config_test.go | 15 ++ internal/bootstrap/local/local.go | 16 ++ internal/configtemplating/config_template.go | 81 +++++++++ internal/installer/config.go | 9 +- internal/installer/config_manager.go | 27 +-- internal/installer/config_template_test.go | 125 ++++++++++++++ .../vault_templating_secret_store.go | 156 ++++++++++++++++++ 25 files changed, 842 insertions(+), 38 deletions(-) create mode 100644 cli/cmd/config.go create mode 100644 cli/cmd/config_template.go create mode 100644 cli/cmd/config_template_test.go create mode 100644 docs/examples/install-config-templating.yaml create mode 100644 docs/oms_config.md create mode 100644 docs/oms_config_template.md create mode 100644 internal/configtemplating/config_template.go create mode 100644 internal/installer/config_template_test.go create mode 100644 internal/installer/vault_templating_secret_store.go diff --git a/cli/cmd/config.go b/cli/cmd/config.go new file mode 100644 index 00000000..0b48bd1f --- /dev/null +++ b/cli/cmd/config.go @@ -0,0 +1,26 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package cmd + +import ( + "github.com/codesphere-cloud/cs-go/pkg/io" + "github.com/spf13/cobra" +) + +type ConfigCmd struct { + cmd *cobra.Command +} + +func AddConfigCmd(rootCmd *cobra.Command, opts *GlobalOptions) { + config := ConfigCmd{ + cmd: &cobra.Command{ + Use: "config", + Short: "Work with OMS configuration files", + Long: io.Long(`Work with OMS configuration files.`), + }, + } + + AddConfigTemplateCmd(config.cmd, opts) + AddCmd(rootCmd, config.cmd) +} diff --git a/cli/cmd/config_template.go b/cli/cmd/config_template.go new file mode 100644 index 00000000..19962e28 --- /dev/null +++ b/cli/cmd/config_template.go @@ -0,0 +1,87 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package cmd + +import ( + "fmt" + "os" + + "github.com/codesphere-cloud/cs-go/pkg/io" + "github.com/codesphere-cloud/oms/internal/configtemplating" + "github.com/codesphere-cloud/oms/internal/installer" + "github.com/codesphere-cloud/oms/internal/util" + "github.com/spf13/cobra" +) + +type ConfigTemplateCmd struct { + cmd *cobra.Command + Opts ConfigTemplateOpts +} + +type ConfigTemplateOpts struct { + *GlobalOptions + Config string + Vault string + AgeKey string +} + +func (c *ConfigTemplateCmd) RunE(cmd *cobra.Command, _ []string) error { + rendered, err := c.Render() + if err != nil { + return err + } + + if _, err := fmt.Fprint(cmd.OutOrStdout(), string(rendered)); err != nil { + return fmt.Errorf("failed to write rendered config: %w", err) + } + + return nil +} + +func AddConfigTemplateCmd(parentCmd *cobra.Command, opts *GlobalOptions) { + templateCmd := &ConfigTemplateCmd{ + cmd: &cobra.Command{ + Use: "template", + Short: "Render a config.yaml template using secrets from a vault file", + Long: io.Long(`Render a config.yaml template using secrets from a prod.vault.yaml file. + +This command prints the rendered configuration to stdout so templating can be tested without running an installation.`), + Example: formatExamples("config template", []io.Example{ + { + Cmd: "--config config.yaml --vault prod.vault.yaml --age-key age_key.txt", + Desc: "Render config.yaml with secrets from prod.vault.yaml", + }, + }), + Args: cobra.ExactArgs(0), + }, + Opts: ConfigTemplateOpts{GlobalOptions: opts}, + } + + templateCmd.cmd.Flags().StringVarP(&templateCmd.Opts.Config, "config", "c", "", "Path to the config.yaml template to render (required)") + templateCmd.cmd.Flags().StringVarP(&templateCmd.Opts.Vault, "vault", "v", "", "Path to the SOPS-encrypted prod.vault.yaml file (required)") + templateCmd.cmd.Flags().StringVarP(&templateCmd.Opts.AgeKey, "age-key", "k", "", "Path to the age key file used to decrypt the vault (required)") + + util.MarkFlagRequired(templateCmd.cmd, "config") + util.MarkFlagRequired(templateCmd.cmd, "vault") + util.MarkFlagRequired(templateCmd.cmd, "age-key") + + AddCmd(parentCmd, templateCmd.cmd) + + templateCmd.cmd.RunE = templateCmd.RunE +} + +func (c *ConfigTemplateCmd) Render() ([]byte, error) { + data, err := os.ReadFile(c.Opts.Config) + if err != nil { + return nil, fmt.Errorf("failed to read config file %s: %w", c.Opts.Config, err) + } + + store := installer.NewLazyVaultTemplatingSecretStore(c.Opts.Vault, c.Opts.AgeKey) + rendered, err := configtemplating.RenderInstallConfigTemplate(data, store) + if err != nil { + return nil, fmt.Errorf("failed to render config template: %w", err) + } + + return rendered, nil +} diff --git a/cli/cmd/config_template_test.go b/cli/cmd/config_template_test.go new file mode 100644 index 00000000..8d5acedf --- /dev/null +++ b/cli/cmd/config_template_test.go @@ -0,0 +1,114 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package cmd_test + +import ( + "bytes" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/codesphere-cloud/oms/cli/cmd" + "github.com/codesphere-cloud/oms/internal/installer" + "github.com/codesphere-cloud/oms/internal/installer/files" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("ConfigTemplateCmd", func() { + It("renders config templates with secrets from a vault file", func() { + if !sopsAndAgeAvailable() { + Skip("sops and age-keygen not available") + } + + tempDir := GinkgoT().TempDir() + configPath := filepath.Join(tempDir, "config.yaml") + vaultPath := filepath.Join(tempDir, "prod.vault.yaml") + plaintextVaultPath := filepath.Join(tempDir, "prod.vault.plain.yaml") + ageKeyPath := filepath.Join(tempDir, "age_key.txt") + + Expect(os.WriteFile(configPath, []byte(`codesphere: + override: + global: + license: + key: '{{ secret "codesphereLicenseKey" }}' +postgres: + override: + auth: + username: '{{ secret "postgresAdmin" "fields.username" }}' + password: '{{ secret "postgresAdmin" "fields.password" }}' +`), 0644)).To(Succeed()) + Expect(exec.Command("age-keygen", "-o", ageKeyPath).Run()).To(Succeed()) + + vault := &files.InstallVault{ + Secrets: []files.SecretEntry{ + { + Name: "codesphereLicenseKey", + File: &files.SecretFile{Content: "license-secret"}, + }, + { + Name: "postgresAdmin", + Fields: &files.SecretFields{ + Username: "postgres", + Password: "admin-secret", + }, + }, + }, + } + vaultYaml, err := vault.Marshal() + Expect(err).NotTo(HaveOccurred()) + Expect(os.WriteFile(plaintextVaultPath, vaultYaml, 0600)).To(Succeed()) + recipient, err := exec.Command("age-keygen", "-y", ageKeyPath).Output() + Expect(err).NotTo(HaveOccurred()) + Expect(installer.EncryptFileWithSOPS(plaintextVaultPath, vaultPath, strings.TrimSpace(string(recipient)))).To(Succeed()) + + rootCmd := cmd.GetRootCmd() + var output bytes.Buffer + rootCmd.SetOut(&output) + rootCmd.SetErr(&output) + rootCmd.SetArgs([]string{ + "config", + "template", + "--config", + configPath, + "--vault", + vaultPath, + "--age-key", + ageKeyPath, + }) + + err = rootCmd.Execute() + + Expect(err).NotTo(HaveOccurred()) + Expect(output.String()).To(ContainSubstring("key: 'license-secret'")) + Expect(output.String()).To(ContainSubstring("username: 'postgres'")) + Expect(output.String()).To(ContainSubstring("password: 'admin-secret'")) + }) + + It("adds the template command with required flags", func() { + rootCmd := cmd.GetRootCmd() + + configCmd, _, err := rootCmd.Find([]string{"config", "template"}) + Expect(err).NotTo(HaveOccurred()) + Expect(configCmd).NotTo(BeNil()) + Expect(configCmd.Use).To(Equal("template")) + Expect(configCmd.Short).To(Equal("Render a config.yaml template using secrets from a vault file")) + + Expect(configCmd.Flags().Lookup("config")).NotTo(BeNil()) + Expect(configCmd.Flags().Lookup("vault")).NotTo(BeNil()) + Expect(configCmd.Flags().Lookup("age-key")).NotTo(BeNil()) + }) +}) + +func sopsAndAgeAvailable() bool { + if _, err := exec.LookPath("sops"); err != nil { + return false + } + if _, err := exec.LookPath("age-keygen"); err != nil { + return false + } + return true +} diff --git a/cli/cmd/init_install_config_test.go b/cli/cmd/init_install_config_test.go index 21bdad4a..a7a2545d 100644 --- a/cli/cmd/init_install_config_test.go +++ b/cli/cmd/init_install_config_test.go @@ -5,6 +5,9 @@ package cmd import ( "os" + "os/exec" + "path/filepath" + "strings" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -162,15 +165,34 @@ codesphere: Context("valid configuration", func() { It("validates successfully", func() { + if !sopsAndAgeAvailableForUpdateInstallConfig() { + Skip("sops and age-keygen not available") + } + _, err := configFile.WriteString(validConfig) Expect(err).NotTo(HaveOccurred()) err = configFile.Close() Expect(err).NotTo(HaveOccurred()) - _, err = vaultFile.WriteString(validVault) - Expect(err).NotTo(HaveOccurred()) err = vaultFile.Close() Expect(err).NotTo(HaveOccurred()) + tempDir := GinkgoT().TempDir() + ageKeyPath := filepath.Join(tempDir, "age_key.txt") + plaintextVaultPath := filepath.Join(tempDir, "prod.vault.plain.yaml") + Expect(os.WriteFile(plaintextVaultPath, []byte(validVault), 0600)).To(Succeed()) + Expect(exec.Command("age-keygen", "-o", ageKeyPath).Run()).To(Succeed()) + recipient, err := exec.Command("age-keygen", "-y", ageKeyPath).Output() + Expect(err).NotTo(HaveOccurred()) + Expect(installer.EncryptFileWithSOPS(plaintextVaultPath, vaultFile.Name(), strings.TrimSpace(string(recipient)))).To(Succeed()) + previousAgeKeyFile, hadPreviousAgeKeyFile := os.LookupEnv("SOPS_AGE_KEY_FILE") + Expect(os.Setenv("SOPS_AGE_KEY_FILE", ageKeyPath)).To(Succeed()) + DeferCleanup(func() { + if hadPreviousAgeKeyFile { + Expect(os.Setenv("SOPS_AGE_KEY_FILE", previousAgeKeyFile)).To(Succeed()) + return + } + Expect(os.Unsetenv("SOPS_AGE_KEY_FILE")).To(Succeed()) + }) c := &InitInstallConfigCmd{ Opts: &InitInstallConfigOpts{ diff --git a/cli/cmd/install_codesphere.go b/cli/cmd/install_codesphere.go index 036a7dd1..4b076c0d 100644 --- a/cli/cmd/install_codesphere.go +++ b/cli/cmd/install_codesphere.go @@ -16,8 +16,10 @@ import ( "strings" "github.com/codesphere-cloud/cs-go/pkg/io" + "github.com/codesphere-cloud/oms/internal/configtemplating" "github.com/codesphere-cloud/oms/internal/env" "github.com/codesphere-cloud/oms/internal/installer" + "github.com/codesphere-cloud/oms/internal/installer/files" "github.com/codesphere-cloud/oms/internal/system" "github.com/codesphere-cloud/oms/internal/util" "github.com/spf13/cobra" @@ -35,6 +37,7 @@ type InstallCodesphereOpts struct { Package string Force bool Config string + Vault string PrivKey string SkipSteps []string CodesphereOnly bool @@ -79,6 +82,7 @@ func AddInstallCodesphereCmd(install *cobra.Command, opts *GlobalOptions) { codesphere.cmd.Flags().StringVarP(&codesphere.Opts.Package, "package", "p", "", "Package file (e.g. codesphere-v1.2.3-installer.tar.gz) to load binaries, installer etc. from") codesphere.cmd.Flags().BoolVarP(&codesphere.Opts.Force, "force", "f", false, "Enforce package extraction") codesphere.cmd.Flags().StringVarP(&codesphere.Opts.Config, "config", "c", "", "Path to the Codesphere Private Cloud configuration file (yaml)") + codesphere.cmd.Flags().StringVar(&codesphere.Opts.Vault, "vault", "prod.vault.yaml", "Path to the SOPS-encrypted prod.vault.yaml file used for config templating") codesphere.cmd.Flags().StringVarP(&codesphere.Opts.PrivKey, "priv-key", "k", "", "Path to the private key to encrypt/decrypt secrets") codesphere.cmd.Flags().StringSliceVarP(&codesphere.Opts.SkipSteps, "skip-steps", "s", []string{}, "Steps to be skipped. E.g. copy-dependencies, extract-dependencies, load-container-images, ceph, kubernetes") codesphere.cmd.Flags().BoolVar(&codesphere.Opts.CodesphereOnly, "codesphere-only", false, "Install only Codesphere without dependencies") @@ -98,10 +102,27 @@ func (c *InstallCodesphereCmd) ExtractAndInstall(pm installer.PackageManager, cm return fmt.Errorf("codesphere installation is only supported on Linux amd64. Current platform: %s/%s", goos, goarch) } + originalConfig := c.Opts.Config + cleanup := func() {} + if c.Opts.Vault != "" { + store := installer.NewLazyVaultTemplatingSecretStore(c.Opts.Vault, c.Opts.PrivKey) + renderedConfig, renderCleanup, err := configtemplating.RenderConfigFileToTempIfNeeded(c.Opts.Config, store) + if err != nil { + return fmt.Errorf("failed to render config template: %w", err) + } + cleanup = renderCleanup + c.Opts.Config = renderedConfig + } + defer cleanup() + defer func() { + c.Opts.Config = originalConfig + }() + config, err := cm.ParseConfigYaml(c.Opts.Config) if err != nil { return fmt.Errorf("failed to extract config.yaml: %w", err) } + c.warnIfVaultDirDiffersFromSecretsDir(config) err = pm.Extract(c.Opts.Force) if err != nil { @@ -246,6 +267,28 @@ func (c *InstallCodesphereCmd) ExtractAndInstall(pm installer.PackageManager, cm return nil } +func (c *InstallCodesphereCmd) warnIfVaultDirDiffersFromSecretsDir(config files.RootConfig) { + if c.Opts.Vault == "" || config.Secrets.BaseDir == "" { + return + } + + vaultDir, err := filepath.Abs(filepath.Dir(c.Opts.Vault)) + if err != nil { + log.Printf("Warning: failed to resolve vault directory for %s: %v", c.Opts.Vault, err) + return + } + + secretsDir, err := filepath.Abs(config.Secrets.BaseDir) + if err != nil { + log.Printf("Warning: failed to resolve configured secrets baseDir %s: %v", config.Secrets.BaseDir, err) + return + } + + if vaultDir != secretsDir { + log.Printf("Warning: config secrets.baseDir (%s) does not match the directory of --vault (%s)", secretsDir, vaultDir) + } +} + func (c *InstallCodesphereCmd) ListPackageContents(pm installer.PackageManager) ([]string, error) { packageDir := pm.GetWorkDir() if !pm.FileIO().Exists(packageDir) { diff --git a/cli/cmd/install_codesphere_test.go b/cli/cmd/install_codesphere_test.go index 23457d3e..868d3cec 100644 --- a/cli/cmd/install_codesphere_test.go +++ b/cli/cmd/install_codesphere_test.go @@ -510,6 +510,10 @@ var _ = Describe("AddInstallCodesphereCmd", func() { Expect(privKeyFlag).NotTo(BeNil()) Expect(privKeyFlag.Shorthand).To(Equal("k")) + vaultFlag := codesphereCmd.Flags().Lookup("vault") + Expect(vaultFlag).NotTo(BeNil()) + Expect(vaultFlag.DefValue).To(Equal("prod.vault.yaml")) + skipStepFlag := codesphereCmd.Flags().Lookup("skip-steps") Expect(skipStepFlag).NotTo(BeNil()) Expect(skipStepFlag.Shorthand).To(Equal("s")) diff --git a/cli/cmd/root.go b/cli/cmd/root.go index 602a9773..416f50ec 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -74,6 +74,7 @@ func GetRootCmd() *cobra.Command { AddDownloadCmd(rootCmd, opts) AddInstallCmd(rootCmd, opts) AddInitCmd(rootCmd, opts) + AddConfigCmd(rootCmd, opts) AddBuildCmd(rootCmd, opts) AddLicensesCmd(rootCmd) diff --git a/cli/cmd/update_install_config_test.go b/cli/cmd/update_install_config_test.go index ac1f1ae4..764bc865 100644 --- a/cli/cmd/update_install_config_test.go +++ b/cli/cmd/update_install_config_test.go @@ -6,6 +6,8 @@ package cmd import ( "fmt" "os" + "os/exec" + "path/filepath" "strings" . "github.com/onsi/ginkgo/v2" @@ -36,6 +38,10 @@ var _ = Describe("UpdateInstallConfig", func() { ) BeforeEach(func() { + if !sopsAndAgeAvailableForUpdateInstallConfig() { + Skip("sops and age-keygen not available") + } + var err error configFile, err = os.CreateTemp("", "config-*.yaml") Expect(err).NotTo(HaveOccurred()) @@ -177,8 +183,23 @@ codesphere: err = os.WriteFile(configFile.Name(), []byte(initialConfig), 0644) Expect(err).NotTo(HaveOccurred()) - err = os.WriteFile(vaultFile.Name(), []byte(initialVault), 0644) + ageKeyPath := filepath.Join(GinkgoT().TempDir(), "age_key.txt") + plaintextVaultPath := filepath.Join(filepath.Dir(ageKeyPath), "prod.vault.plain.yaml") + err = os.WriteFile(plaintextVaultPath, []byte(initialVault), 0600) Expect(err).NotTo(HaveOccurred()) + Expect(exec.Command("age-keygen", "-o", ageKeyPath).Run()).To(Succeed()) + recipient, err := exec.Command("age-keygen", "-y", ageKeyPath).Output() + Expect(err).NotTo(HaveOccurred()) + Expect(installer.EncryptFileWithSOPS(plaintextVaultPath, vaultFile.Name(), strings.TrimSpace(string(recipient)))).To(Succeed()) + previousAgeKeyFile, hadPreviousAgeKeyFile := os.LookupEnv("SOPS_AGE_KEY_FILE") + Expect(os.Setenv("SOPS_AGE_KEY_FILE", ageKeyPath)).To(Succeed()) + DeferCleanup(func() { + if hadPreviousAgeKeyFile { + Expect(os.Setenv("SOPS_AGE_KEY_FILE", previousAgeKeyFile)).To(Succeed()) + return + } + Expect(os.Unsetenv("SOPS_AGE_KEY_FILE")).To(Succeed()) + }) opts = &UpdateInstallConfigOpts{ GlobalOptions: &GlobalOptions{}, @@ -349,9 +370,7 @@ codesphere: ) BeforeEach(func() { - var err error - initialVaultContent, err = os.ReadFile(vaultFile.Name()) - Expect(err).NotTo(HaveOccurred()) + initialVaultContent = []byte(initialVault) }) It("should preserve all vault entries during non-certificate update", func() { @@ -433,6 +452,16 @@ codesphere: }) }) +func sopsAndAgeAvailableForUpdateInstallConfig() bool { + if _, err := exec.LookPath("sops"); err != nil { + return false + } + if _, err := exec.LookPath("age-keygen"); err != nil { + return false + } + return true +} + var _ = Describe("SecretDependencyTracker", func() { var tracker *SecretDependencyTracker diff --git a/docs/README.md b/docs/README.md index c3a10ccc..4ee1bc56 100644 --- a/docs/README.md +++ b/docs/README.md @@ -19,6 +19,7 @@ like downloading new versions. * [oms beta](oms_beta.md) - Commands for early testing * [oms build](oms_build.md) - Build and push images to a registry +* [oms config](oms_config.md) - Work with OMS configuration files * [oms create](oms_create.md) - Create resources for Codesphere * [oms download](oms_download.md) - Download resources available through OMS * [oms init](oms_init.md) - Initialize configuration files @@ -30,4 +31,3 @@ like downloading new versions. * [oms smoketest](oms_smoketest.md) - Run smoke tests for Codesphere components * [oms update](oms_update.md) - Update OMS related resources * [oms version](oms_version.md) - Print version - diff --git a/docs/examples/install-config-templating.yaml b/docs/examples/install-config-templating.yaml new file mode 100644 index 00000000..b86b564a --- /dev/null +++ b/docs/examples/install-config-templating.yaml @@ -0,0 +1,22 @@ +# Example config.yaml fragment using secrets from prod.vault.yaml. +# +# Rendered by: +# oms install codesphere --config config.yaml --vault prod.vault.yaml --priv-key age_key.txt ... +# +# Secret values are read dynamically from prod.vault.yaml and are not stored in +# plaintext in config.yaml. + +secrets: + baseDir: ./secrets + +codesphere: + override: + global: + license: + key: '{{ secret "codesphereLicenseKey" }}' + +postgres: + override: + auth: + username: '{{ secret "postgresAdmin" "fields.username" }}' + password: '{{ secret "postgresAdmin" "fields.password" }}' diff --git a/docs/oms.md b/docs/oms.md index c3a10ccc..4ee1bc56 100644 --- a/docs/oms.md +++ b/docs/oms.md @@ -19,6 +19,7 @@ like downloading new versions. * [oms beta](oms_beta.md) - Commands for early testing * [oms build](oms_build.md) - Build and push images to a registry +* [oms config](oms_config.md) - Work with OMS configuration files * [oms create](oms_create.md) - Create resources for Codesphere * [oms download](oms_download.md) - Download resources available through OMS * [oms init](oms_init.md) - Initialize configuration files @@ -30,4 +31,3 @@ like downloading new versions. * [oms smoketest](oms_smoketest.md) - Run smoke tests for Codesphere components * [oms update](oms_update.md) - Update OMS related resources * [oms version](oms_version.md) - Print version - diff --git a/docs/oms_beta_bootstrap-gcp.md b/docs/oms_beta_bootstrap-gcp.md index a2e63899..18538f90 100644 --- a/docs/oms_beta_bootstrap-gcp.md +++ b/docs/oms_beta_bootstrap-gcp.md @@ -87,4 +87,3 @@ oms beta bootstrap-gcp [flags] * [oms beta bootstrap-gcp cleanup](oms_beta_bootstrap-gcp_cleanup.md) - Clean up GCP infrastructure created by bootstrap-gcp * [oms beta bootstrap-gcp postconfig](oms_beta_bootstrap-gcp_postconfig.md) - Run post-configuration steps for GCP bootstrapping * [oms beta bootstrap-gcp restart-vms](oms_beta_bootstrap-gcp_restart-vms.md) - Restart stopped or terminated GCP VMs - diff --git a/docs/oms_beta_bootstrap-local.md b/docs/oms_beta_bootstrap-local.md index 9703d33a..e3e393d2 100644 --- a/docs/oms_beta_bootstrap-local.md +++ b/docs/oms_beta_bootstrap-local.md @@ -37,4 +37,3 @@ oms beta bootstrap-local [flags] ### SEE ALSO * [oms beta](oms_beta.md) - Commands for early testing - diff --git a/docs/oms_config.md b/docs/oms_config.md new file mode 100644 index 00000000..e41a0170 --- /dev/null +++ b/docs/oms_config.md @@ -0,0 +1,19 @@ +## oms config + +Work with OMS configuration files + +### Synopsis + +Work with OMS configuration files. + +### Options + +``` + -h, --help help for config +``` + +### SEE ALSO + +* [oms](oms.md) - Codesphere Operations Management System (OMS) +* [oms config template](oms_config_template.md) - Render a config.yaml template using secrets from a vault file + diff --git a/docs/oms_config_template.md b/docs/oms_config_template.md new file mode 100644 index 00000000..696f7f32 --- /dev/null +++ b/docs/oms_config_template.md @@ -0,0 +1,35 @@ +## oms config template + +Render a config.yaml template using secrets from a vault file + +### Synopsis + +Render a config.yaml template using secrets from a prod.vault.yaml file. + +This command prints the rendered configuration to stdout so templating can be tested without running an installation. + +``` +oms config template [flags] +``` + +### Examples + +``` +# Render config.yaml with secrets from prod.vault.yaml +$ oms config template --config config.yaml --vault prod.vault.yaml --age-key age_key.txt + +``` + +### Options + +``` + -k, --age-key string Path to the age key file used to decrypt the vault (required) + -c, --config string Path to the config.yaml template to render (required) + -h, --help help for template + -v, --vault string Path to the SOPS-encrypted prod.vault.yaml file (required) +``` + +### SEE ALSO + +* [oms config](oms_config.md) - Work with OMS configuration files + diff --git a/docs/oms_install_codesphere.md b/docs/oms_install_codesphere.md index ead037d2..8d141d58 100644 --- a/docs/oms_install_codesphere.md +++ b/docs/oms_install_codesphere.md @@ -33,9 +33,9 @@ $ oms install codesphere -p codesphere-v1.2.3-installer-lite.tar.gz -k 0 { + field = selector[0] + } + + switch field { + case "", "content", "file.content": + if entry.File != nil { + return entry.File.Content, nil + } + if entry.Fields != nil { + return entry.Fields.Password, nil + } + case "name", "file.name": + if entry.File != nil { + return entry.File.Name, nil + } + case "password", "fields.password": + if entry.Fields != nil { + return entry.Fields.Password, nil + } + case "username", "fields.username": + if entry.Fields != nil { + return entry.Fields.Username, nil + } + default: + return "", fmt.Errorf("unsupported selector %q for secret %q", field, entry.Name) + } + + return "", fmt.Errorf("selector %q is not available on secret %q", field, entry.Name) +} + +func LoadVaultData(vaultPath, ageKeyPath string) (*files.InstallVault, error) { + data, err := os.ReadFile(vaultPath) + if err != nil { + return nil, fmt.Errorf("failed to read vault file %s: %w", vaultPath, err) + } + + encrypted, err := isSOPSEncryptedYAML(data) + if err != nil { + return nil, fmt.Errorf("failed to inspect vault file %s: %w", vaultPath, err) + } + if !encrypted { + return nil, fmt.Errorf("vault file %s is not SOPS-encrypted", vaultPath) + } + + decrypted, err := DecryptFileWithSOPS(vaultPath, ageKeyPath) + if err != nil { + return nil, fmt.Errorf("failed to decrypt vault.yaml: %w", err) + } + + vault, err := parseVaultData(decrypted) + if err != nil { + return nil, fmt.Errorf("failed to parse decrypted vault.yaml: %w", err) + } + + return vault, nil +} + +// isSOPSEncryptedYAML checks whether the YAML document contains SOPS metadata. +// SOPS-encrypted YAML files have a top-level "sops" mapping that stores +// encryption metadata such as age recipients, encrypted data keys, and MACs. +func isSOPSEncryptedYAML(data []byte) (bool, error) { + var doc yaml.Node + if err := yaml.Unmarshal(data, &doc); err != nil { + return false, err + } + if len(doc.Content) == 0 { + return false, nil + } + + root := doc.Content[0] + if root.Kind != yaml.MappingNode { + return false, nil + } + + for i := 0; i+1 < len(root.Content); i += 2 { + if root.Content[i].Value == "sops" && root.Content[i+1].Kind == yaml.MappingNode { + return true, nil + } + } + + return false, nil +} + +func parseVaultData(data []byte) (*files.InstallVault, error) { + vault := &files.InstallVault{} + if err := vault.Unmarshal(data); err != nil { + return nil, err + } + return vault, nil +} From 8e4055ffd40e71def12b14cd01bfb29d38c14520 Mon Sep 17 00:00:00 2001 From: schrodit <7979201+schrodit@users.noreply.github.com> Date: Thu, 28 May 2026 06:47:48 +0000 Subject: [PATCH 2/2] chore(docs): Auto-update docs and licenses Signed-off-by: schrodit <7979201+schrodit@users.noreply.github.com> --- docs/README.md | 1 + docs/examples/install-config-templating.yaml | 22 -------------------- docs/oms.md | 1 + docs/oms_beta_bootstrap-gcp.md | 1 + docs/oms_beta_bootstrap-local.md | 1 + docs/oms_install_codesphere.md | 1 + 6 files changed, 5 insertions(+), 22 deletions(-) delete mode 100644 docs/examples/install-config-templating.yaml diff --git a/docs/README.md b/docs/README.md index 4ee1bc56..787b54ff 100644 --- a/docs/README.md +++ b/docs/README.md @@ -31,3 +31,4 @@ like downloading new versions. * [oms smoketest](oms_smoketest.md) - Run smoke tests for Codesphere components * [oms update](oms_update.md) - Update OMS related resources * [oms version](oms_version.md) - Print version + diff --git a/docs/examples/install-config-templating.yaml b/docs/examples/install-config-templating.yaml deleted file mode 100644 index b86b564a..00000000 --- a/docs/examples/install-config-templating.yaml +++ /dev/null @@ -1,22 +0,0 @@ -# Example config.yaml fragment using secrets from prod.vault.yaml. -# -# Rendered by: -# oms install codesphere --config config.yaml --vault prod.vault.yaml --priv-key age_key.txt ... -# -# Secret values are read dynamically from prod.vault.yaml and are not stored in -# plaintext in config.yaml. - -secrets: - baseDir: ./secrets - -codesphere: - override: - global: - license: - key: '{{ secret "codesphereLicenseKey" }}' - -postgres: - override: - auth: - username: '{{ secret "postgresAdmin" "fields.username" }}' - password: '{{ secret "postgresAdmin" "fields.password" }}' diff --git a/docs/oms.md b/docs/oms.md index 4ee1bc56..787b54ff 100644 --- a/docs/oms.md +++ b/docs/oms.md @@ -31,3 +31,4 @@ like downloading new versions. * [oms smoketest](oms_smoketest.md) - Run smoke tests for Codesphere components * [oms update](oms_update.md) - Update OMS related resources * [oms version](oms_version.md) - Print version + diff --git a/docs/oms_beta_bootstrap-gcp.md b/docs/oms_beta_bootstrap-gcp.md index 18538f90..a2e63899 100644 --- a/docs/oms_beta_bootstrap-gcp.md +++ b/docs/oms_beta_bootstrap-gcp.md @@ -87,3 +87,4 @@ oms beta bootstrap-gcp [flags] * [oms beta bootstrap-gcp cleanup](oms_beta_bootstrap-gcp_cleanup.md) - Clean up GCP infrastructure created by bootstrap-gcp * [oms beta bootstrap-gcp postconfig](oms_beta_bootstrap-gcp_postconfig.md) - Run post-configuration steps for GCP bootstrapping * [oms beta bootstrap-gcp restart-vms](oms_beta_bootstrap-gcp_restart-vms.md) - Restart stopped or terminated GCP VMs + diff --git a/docs/oms_beta_bootstrap-local.md b/docs/oms_beta_bootstrap-local.md index e3e393d2..9703d33a 100644 --- a/docs/oms_beta_bootstrap-local.md +++ b/docs/oms_beta_bootstrap-local.md @@ -37,3 +37,4 @@ oms beta bootstrap-local [flags] ### SEE ALSO * [oms beta](oms_beta.md) - Commands for early testing + diff --git a/docs/oms_install_codesphere.md b/docs/oms_install_codesphere.md index 8d141d58..23f6dd86 100644 --- a/docs/oms_install_codesphere.md +++ b/docs/oms_install_codesphere.md @@ -39,3 +39,4 @@ $ oms install codesphere -p codesphere-v1.2.3-installer-lite.tar.gz -k