Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ License URL: https://gitea.com/gitea/go-sdk/src/tag/gitea/v0.25.1/gitea/LICENSE
Module: dario.cat/mergo
Version: v1.0.2
License: BSD-3-Clause
License URL: https://github.com/imdario/mergo/blob/v1.0.2/LICENSE
License URL: Unknown
Comment thread
schrodit marked this conversation as resolved.

----------
Module: filippo.io/age
Expand Down
26 changes: 24 additions & 2 deletions cli/cmd/init_install_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ package cmd

import (
"os"
"os/exec"
"path/filepath"
"strings"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
Expand Down Expand Up @@ -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{
Expand Down
43 changes: 43 additions & 0 deletions cli/cmd/install_codesphere.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -35,6 +37,7 @@ type InstallCodesphereOpts struct {
Package string
Force bool
Config string
Vault string
PrivKey string
SkipSteps []string
CodesphereOnly bool
Expand Down Expand Up @@ -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")
Expand All @@ -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.RenderConfigFileToTemp(c.Opts.Config, store)
if err != nil {
return fmt.Errorf("failed to render config template: %w", err)
}
cleanup = renderCleanup
Comment thread
schrodit marked this conversation as resolved.
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 {
Expand Down Expand Up @@ -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) {
Expand Down
4 changes: 4 additions & 0 deletions cli/cmd/install_codesphere_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down
1 change: 1 addition & 0 deletions cli/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ func GetRootCmd() *cobra.Command {
AddDownloadCmd(rootCmd, opts)
AddInstallCmd(rootCmd, opts)
AddInitCmd(rootCmd, opts)
AddTemplateCmd(rootCmd, opts)
AddBuildCmd(rootCmd, opts)
AddLicensesCmd(rootCmd)

Expand Down
26 changes: 26 additions & 0 deletions cli/cmd/template.go
Original file line number Diff line number Diff line change
@@ -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 TemplateCmd struct {
cmd *cobra.Command
}

func AddTemplateCmd(rootCmd *cobra.Command, opts *GlobalOptions) {
template := TemplateCmd{
cmd: &cobra.Command{
Use: "template",
Short: "Render OMS configuration templates",
Long: io.Long(`Render OMS configuration templates.`),
},
}

AddTemplateConfigCmd(template.cmd, opts)
AddCmd(rootCmd, template.cmd)
}
105 changes: 105 additions & 0 deletions cli/cmd/template_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// 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 TemplateConfigCmd struct {
cmd *cobra.Command
Opts TemplateConfigOpts
}

type TemplateConfigOpts struct {
*GlobalOptions
Config string
Vault string
AgeKey string
}

func (c *TemplateConfigCmd) RunE(cmd *cobra.Command, _ []string) error {
rendered, err := c.Render()
if err != nil {
return err
}

if _, err := fmt.Fprintln(cmd.ErrOrStderr(), "Warning: the rendered config is printed to stdout in plaintext and may contain secrets from the vault."); err != nil {
return fmt.Errorf("failed to write warning message: %w", err)
}

if _, err := fmt.Fprint(cmd.OutOrStdout(), string(rendered)); err != nil {
return fmt.Errorf("failed to write rendered config: %w", err)
}

return nil
}

func AddTemplateConfigCmd(parentCmd *cobra.Command, opts *GlobalOptions) {
configCmd := &TemplateConfigCmd{
cmd: &cobra.Command{
Use: "config",
Short: "Render a config.yaml template using secrets from a vault file",
Comment thread
schrodit marked this conversation as resolved.
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.

Template syntax in config.yaml:

# Inject a secret value (defaults to the "content"/"password" field)
someKey: "{{ secret "mySecret" }}"

# Select a specific field
username: "{{ secret "mySecret" "fields.username" }}"
password: "{{ secret "mySecret" "fields.password" }}"

# Inject a file secret's content
caCert: "{{ secret "caCert" "file.content" }}"

Secret names and selectors must match entries in the prod.vault.yaml file.`),
Example: formatExamples("template config", []io.Example{
Comment thread
schrodit marked this conversation as resolved.
{
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: TemplateConfigOpts{GlobalOptions: opts},
}

configCmd.cmd.Flags().StringVarP(&configCmd.Opts.Config, "config", "c", "", "Path to the config.yaml template to render (required)")
configCmd.cmd.Flags().StringVarP(&configCmd.Opts.Vault, "vault", "v", "", "Path to the SOPS-encrypted prod.vault.yaml file (required)")
configCmd.cmd.Flags().StringVarP(&configCmd.Opts.AgeKey, "age-key", "k", "", "Path to the age key file used to decrypt the vault (required)")

util.MarkFlagRequired(configCmd.cmd, "config")
util.MarkFlagRequired(configCmd.cmd, "vault")
util.MarkFlagRequired(configCmd.cmd, "age-key")

AddCmd(parentCmd, configCmd.cmd)

configCmd.cmd.RunE = configCmd.RunE
}

func (c *TemplateConfigCmd) 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
}
Loading
Loading