Skip to content
Open
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
24 changes: 18 additions & 6 deletions cli/cmd/beta_vault_secret.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type BetaVaultSecretOpts struct {
AgeKeyPath string
Namespace string
SecretName string
VaultType string
}

func (c *BetaVaultSecretCmd) RunE(_ *cobra.Command, _ []string) error {
Expand All @@ -47,16 +48,26 @@ func (c *BetaVaultSecretCmd) RunE(_ *cobra.Command, _ []string) error {

creator := vault.NewVaultSecretCreator(kubeClient)

return creator.CreateSecretFromFile(c.cmd.Context(), c.Opts.VaultFile, c.Opts.AgeKeyPath, c.Opts.Namespace, c.Opts.SecretName)
store, err := vault.NewFromString(c.Opts.VaultType, vault.Options{Path: c.Opts.VaultFile, AgeKey: c.Opts.AgeKeyPath})
if err != nil {
return fmt.Errorf("failed to load vault: %w", err)
}

err = creator.CreateSecretFromStore(c.cmd.Context(), store, c.Opts.Namespace, c.Opts.SecretName)
if err != nil {
return fmt.Errorf("failed to create secret: %w", err)
}

return nil
}

func AddBetaVaultSecretCmd(parentCmd *cobra.Command, opts *util.GlobalOptions) {
cmd := BetaVaultSecretCmd{
cmd: &cobra.Command{
Use: "vault-secret",
Short: "Create a Kubernetes secret from a SOPS-encrypted vault file",
Long: packageio.Long(`Create a Kubernetes secret from a SOPS-encrypted prod.vault.yaml file.
Reads the encrypted vault file, decrypts it using the age key, and creates a Kubernetes secret
Short: "Create a Kubernetes secret from a vault file",
Long: packageio.Long(`Create a Kubernetes secret from a prod.vault.yaml file.
Loads the selected vault type and creates a Kubernetes secret
with all the vault entries as key-value pairs in the target cluster.`),
Example: util.FormatExamples("vault-secret", []packageio.Example{
{Cmd: "--vault-file prod.vault.yaml --namespace default --secret-name vault-secrets", Desc: "Create secret using default age key location"},
Expand All @@ -66,8 +77,9 @@ func AddBetaVaultSecretCmd(parentCmd *cobra.Command, opts *util.GlobalOptions) {
Opts: BetaVaultSecretOpts{GlobalOptions: opts},
}

cmd.cmd.Flags().StringVar(&cmd.Opts.VaultFile, "vault-file", "", "Path to the SOPS-encrypted vault file (required)")
cmd.cmd.Flags().StringVar(&cmd.Opts.AgeKeyPath, "age-key", "", "Path to the age key file (optional, will use defaults if not provided)")
cmd.cmd.Flags().StringVar(&cmd.Opts.VaultFile, "vault-file", "", "Path to the vault file (required)")
cmd.cmd.Flags().StringVar(&cmd.Opts.AgeKeyPath, "age-key", "", "Path to the age key file (required for sops unless an age key environment variable is set)")
cmd.cmd.Flags().StringVar(&cmd.Opts.VaultType, "vault-type", "sops", "Vault storage type (sops or plain)")
cmd.cmd.Flags().StringVar(&cmd.Opts.Namespace, "namespace", "codesphere", "Kubernetes namespace where the secret will be created")
cmd.cmd.Flags().StringVar(&cmd.Opts.SecretName, "secret-name", "cs-vault", "Name of the Kubernetes secret to create")

Expand Down
6 changes: 5 additions & 1 deletion cli/cmd/bootstrap_gcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,11 @@ func AddBootstrapGcpCmd(parent *cobra.Command, opts *util.GlobalOptions) {
func (c *BootstrapGcpCmd) BootstrapGcp() error {
ctx := c.cmd.Context()
stlog := bootstrap.NewStepLogger(false)
icg := installer.NewInstallConfigManager()

icg, err := installer.NewInstallConfigManager("plain", "")
if err != nil {
return fmt.Errorf("failed to initialize conig manager: %w", err)
}
gcpClient := gcp.NewGCPClient(ctx, stlog, os.Getenv("GOOGLE_APPLICATION_CREDENTIALS"))
fw := intutil.NewFilesystemWriter()
portalClient := portal.NewPortalClient()
Expand Down
5 changes: 4 additions & 1 deletion cli/cmd/bootstrap_gcp_postconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ type BootstrapGcpPostconfigOpts struct {
func (c *BootstrapGcpPostconfigCmd) RunE(_ *cobra.Command, args []string) error {
log.Printf("running post-configuration steps...")

icg := installer.NewInstallConfigManager()
icg, err := installer.NewInstallConfigManager("plain", "")
if err != nil {
return fmt.Errorf("failed to initialize config manager: %w", err)
}
fw := intutil.NewFilesystemWriter()

infraFilePath := gcp.GetInfraFilePath()
Expand Down
6 changes: 5 additions & 1 deletion cli/cmd/bootstrap_local.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,11 @@ func (c *BootstrapLocalCmd) BootstrapLocal() error {
}

stlog := bootstrap.NewStepLogger(false)
icg := installer.NewInstallConfigManager()

icg, err := installer.NewInstallConfigManager("plain", "")
if err != nil {
return fmt.Errorf("failed to initialize config manager: %w", err)
}
fw := intutil.NewFilesystemWriter()
kubeClient, restConfig, err := c.GetKubeClient(ctx)
if err != nil {
Expand Down
16 changes: 16 additions & 0 deletions cli/cmd/codesphere/codesphere_suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) Codesphere Inc.
// SPDX-License-Identifier: Apache-2.0

package codesphere_test

import (
"testing"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

func TestCodesphere(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Codesphere Command Suite")
}
37 changes: 32 additions & 5 deletions cli/cmd/codesphere/install_codesphere.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ type InstallCodesphereOpts struct {
ConfigPath string
Vault string
PrivKey string
VaultType string
SkipSteps []string
CodesphereOnly bool
DirectConnection bool
Expand All @@ -55,6 +56,9 @@ type InstallCodesphereOpts struct {
}

func (c *InstallCodesphereCmd) RunE(cmd *cobra.Command, _ []string) error {
if err := validateInstallCodesphereVault(c.Opts); err != nil {
return err
}
ctx := cmd.Context()
effectiveOpts, cfg, cleanup, err := prepareInstallConfig(c.Opts, installer.NewConfig())
if err != nil {
Expand Down Expand Up @@ -116,14 +120,14 @@ func AddInstallCmd(install *cobra.Command, opts *util.GlobalOptions) {
},
}),
},
Opts: &InstallCodesphereOpts{GlobalOptions: opts},
Opts: &InstallCodesphereOpts{GlobalOptions: opts, VaultType: string(vault.TypeSOPS)},
Env: env.NewEnv(),
}
codesphere.cmd.PersistentFlags().StringVarP(&codesphere.Opts.Package, "package", "p", "", "Package file (e.g. codesphere-v1.2.3-installer-lite.tar.gz) to load binaries, installer etc. from")
codesphere.cmd.PersistentFlags().BoolVarP(&codesphere.Opts.Force, "force", "f", false, "Enforce package extraction")
codesphere.cmd.PersistentFlags().StringArrayVarP(&codesphere.Opts.Configs, "config", "c", nil, "Path to a Codesphere Private Cloud configuration file (yaml). Can be specified multiple times and merged in order")
codesphere.cmd.PersistentFlags().StringVar(&codesphere.Opts.Vault, "vault", "", "Path to the SOPS-encrypted prod.vault.yaml file used for config templating")
codesphere.cmd.PersistentFlags().StringVarP(&codesphere.Opts.PrivKey, "priv-key", "k", "", "Path to the private key to encrypt/decrypt secrets")
codesphere.cmd.PersistentFlags().StringVar(&codesphere.Opts.Vault, "vault", "", "Path to the prod.vault.yaml file used for config templating")
codesphere.cmd.PersistentFlags().StringVarP(&codesphere.Opts.PrivKey, "priv-key", "k", "", "Path to the age private key (required for sops unless an age key environment variable is set)")
codesphere.cmd.PersistentFlags().StringSliceVarP(&codesphere.Opts.SkipSteps, "skip-steps", "s", []string{}, "Steps to be skipped. E.g. copy-dependencies, extract-dependencies, load-container-images, ceph, postgres, kubernetes, docker, argocd")
codesphere.cmd.PersistentFlags().BoolVar(&codesphere.Opts.DirectConnection, "direct-connection", false, "Use direct connection for installation, requires having access to the cluster nodes from your machine")
codesphere.cmd.PersistentFlags().BoolVar(&codesphere.Opts.AutoApprove, "auto-approve", true, "Auto approve confirmation prompts with default values")
Expand All @@ -137,7 +141,6 @@ func AddInstallCmd(install *cobra.Command, opts *util.GlobalOptions) {

util.MarkPersistentFlagRequired(codesphere.cmd, "package")
util.MarkPersistentFlagRequired(codesphere.cmd, "config")
util.MarkPersistentFlagRequired(codesphere.cmd, "priv-key")

util.AddCmd(install, codesphere.cmd)

Expand All @@ -148,6 +151,21 @@ func AddInstallCmd(install *cobra.Command, opts *util.GlobalOptions) {
AddInstallCodespherePlatformCmd(codesphere.cmd, codesphere.Opts)
}

// validateInstallCodesphereVault enforces the current TypeScript installer
// contract without changing the selected type on the command options.
func validateInstallCodesphereVault(opts *InstallCodesphereOpts) error {
if opts.VaultType != string(vault.TypeSOPS) {
return fmt.Errorf("install codesphere requires vault type %q", vault.TypeSOPS)
}

err := vault.ValidateConfiguration(vault.TypeSOPS, opts.PrivKey)
if err != nil {
return fmt.Errorf("failed to validate install config: %w", err)
}

return nil
}

func sharedInstallCodesphereSteps() []string {
return []string{"copy-dependencies", "extract-dependencies"}
}
Expand All @@ -168,7 +186,16 @@ func prepareInstallConfig(opts *InstallCodesphereOpts, cm installer.ConfigManage
return nil, files.RootConfig{}, func() {}, fmt.Errorf("no config.yaml input provided: at least one config file is required")
}

store := vault.NewLazyVaultTemplatingSecretStore(opts.Vault, opts.PrivKey)
var store *vault.VaultTemplatingSecretStore

if opts.Vault != "" {
backend, err := vault.NewFromString(opts.VaultType, vault.Options{Path: opts.Vault, AgeKey: opts.PrivKey})
if err != nil {
return nil, files.RootConfig{}, func() {}, fmt.Errorf("failed to load vault: %w", err)
}

store = vault.NewLazyVaultTemplatingSecretStoreWithVault(backend)
}
cleanupFns := []func(){}
cleanup := func() {
for i := len(cleanupFns) - 1; i >= 0; i-- {
Expand Down
13 changes: 13 additions & 0 deletions cli/cmd/codesphere/install_codesphere_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,3 +232,16 @@ func installCodesphereSopsAndAgeAvailable() bool {
}
return true
}

var _ = Describe("install codesphere vault type", func() {
It("accepts sops", func() {
opts := &InstallCodesphereOpts{VaultType: string(vault.TypeSOPS), PrivKey: "age-key.txt"}
Expect(validateInstallCodesphereVault(opts)).To(Succeed())
})

It("rejects plain vaults at the command boundary", func() {
opts := &InstallCodesphereOpts{VaultType: string(vault.TypePlain), PrivKey: "age-key.txt"}
err := validateInstallCodesphereVault(opts)
Expect(err).To(MatchError(`install codesphere requires vault type "sops"`))
})
})
5 changes: 4 additions & 1 deletion cli/cmd/codesphere/install_codesphere_dependencies.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ type InstallCodesphereDepenciesCmd struct {
}

func (c *InstallCodesphereDepenciesCmd) RunE(_ *cobra.Command, _ []string) error {
if err := validateInstallCodesphereVault(c.Opts); err != nil {
return err
}
effectiveOpts, cfg, cleanup, err := prepareInstallConfig(c.Opts, installer.NewConfig())
if err != nil {
return err
Expand Down Expand Up @@ -81,7 +84,7 @@ func installCodesphereDepencies(opts *InstallCodesphereOpts, cfg files.RootConfi
func installArgoCDAndApps(opts *InstallCodesphereOpts, cfg files.RootConfig, pm installer.PackageManager, stlog *bootstrap.StepLogger) error {
var install *argocdinstaller.AppInstaller
if err := stlog.Substep("Load vault data", func() error {
installVault, restConfig, err := installer.VaultAndRESTConfig(opts.Vault, opts.PrivKey, cfg)
installVault, restConfig, err := installer.VaultAndRESTConfig(opts.Vault, opts.PrivKey, opts.VaultType, cfg)
if err != nil {
return err
}
Expand Down
3 changes: 3 additions & 0 deletions cli/cmd/codesphere/install_codesphere_infra.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ type InstallCodesphereInfraCmd struct {
}

func (c *InstallCodesphereInfraCmd) RunE(_ *cobra.Command, _ []string) error {
if err := validateInstallCodesphereVault(c.Opts); err != nil {
return err
}
effectiveOpts, _, cleanup, err := prepareInstallConfig(c.Opts, installer.NewConfig())
if err != nil {
return err
Expand Down
5 changes: 4 additions & 1 deletion cli/cmd/codesphere/install_codesphere_platform.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ type InstallCodespherePlatformCmd struct {
}

func (c *InstallCodespherePlatformCmd) RunE(cmd *cobra.Command, _ []string) error {
if err := validateInstallCodesphereVault(c.Opts); err != nil {
return err
}
effectiveOpts, cfg, cleanup, err := prepareInstallConfig(c.Opts, installer.NewConfig())
if err != nil {
return err
Expand All @@ -35,7 +38,7 @@ func (c *InstallCodespherePlatformCmd) RunE(cmd *cobra.Command, _ []string) erro
}

func installCodespherePlatform(ctx context.Context, opts *InstallCodesphereOpts, cfg files.RootConfig, env env.Env) error {
if err := installer.EnsureClusterAdminSecret(ctx, opts.Vault, opts.PrivKey, cfg); err != nil {
if err := installer.EnsureClusterAdminSecret(ctx, opts.Vault, opts.PrivKey, opts.VaultType, cfg); err != nil {
return fmt.Errorf("failed to set cluster admin email: %w", err)
}

Expand Down
3 changes: 3 additions & 0 deletions cli/cmd/codesphere/install_codesphere_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ var _ = Describe("InstallCodesphereCmd", func() {
GlobalOptions: globalOpts,
Package: "codesphere-v1.66.0-installer-lite.tar.gz",
Force: false,
VaultType: "sops",
PrivKey: "age-key.txt",
}
c = codesphere.InstallCodesphereCmd{
Opts: opts,
Expand Down Expand Up @@ -124,6 +126,7 @@ var _ = Describe("AddInstallCodesphereCmd", func() {
vaultFlag := codesphereCmd.PersistentFlags().Lookup("vault")
Expect(vaultFlag).NotTo(BeNil())
Expect(vaultFlag.DefValue).To(Equal(""))
Expect(codesphereCmd.PersistentFlags().Lookup("vault-type")).To(BeNil())

skipStepFlag := codesphereCmd.PersistentFlags().Lookup("skip-steps")
Expect(skipStepFlag).NotTo(BeNil())
Expand Down
22 changes: 10 additions & 12 deletions cli/cmd/init_install_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ type InitInstallConfigOpts struct {

ConfigFile string
VaultFile string
VaultType string
AgeKey string

Profile string
AnsibleInventoryFile string
Expand Down Expand Up @@ -100,7 +102,10 @@ type InitInstallConfigOpts struct {
}

func (c *InitInstallConfigCmd) RunE(_ *cobra.Command, args []string) error {
icg := installer.NewInstallConfigManager()
icg, err := installer.NewInstallConfigManager(c.Opts.VaultType, c.Opts.AgeKey)
if err != nil {
return fmt.Errorf("failed to initialize config manager: %w", err)
}

return c.InitInstallConfig(icg)
}
Expand Down Expand Up @@ -142,6 +147,8 @@ func AddInitInstallConfigCmd(init *cobra.Command, opts *util.GlobalOptions) {

c.cmd.Flags().StringVarP(&c.Opts.ConfigFile, "config", "c", "config.yaml", "Output file path for config.yaml")
c.cmd.Flags().StringVar(&c.Opts.VaultFile, "vault", "prod.vault.yaml", "Output file path for prod.vault.yaml")
c.cmd.Flags().StringVar(&c.Opts.VaultType, "vault-type", "sops", "Vault storage type (sops or plain)")
c.cmd.Flags().StringVar(&c.Opts.AgeKey, "age-key", "", "Path to the age private key (required for sops unless SOPS_AGE_KEY or SOPS_AGE_KEY_FILE is set)")

c.cmd.Flags().StringVar(&c.Opts.Profile, "profile", "", "Use a predefined configuration profile (dev, production, minimal)")
c.cmd.Flags().StringVar(&c.Opts.AnsibleInventoryFile, "ansible-inventory", "", "Path to Ansible inventory file to import host information from")
Expand Down Expand Up @@ -244,7 +251,7 @@ func (c *InitInstallConfigCmd) InitInstallConfig(icg installer.InstallConfigMana
return fmt.Errorf("failed to write config file: %w", err)
}

if err := icg.WriteUnencryptedVault(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)
}

Expand Down Expand Up @@ -280,16 +287,7 @@ func (c *InitInstallConfigCmd) printSuccessMessage(warningCount int) {
log.Println(strings.Repeat("=", 70))

log.Println("\nIMPORTANT: Keys and certificates have been generated and embedded in the vault file.")
log.Println(" Keep the vault file secure and encrypt it with SOPS before storing.")

log.Println("\nNext steps:")
log.Println("1. Review the generated config.yaml and prod.vault.yaml")
log.Println("2. Install SOPS and Age: brew install sops age")
log.Println("3. Generate an Age keypair: age-keygen -o age_key.txt")
log.Println("4. Encrypt the vault file:")
log.Printf(" age-keygen -y age_key.txt # Get public key\n")
log.Printf(" sops --encrypt --age <PUBLIC_KEY> --in-place %s\n", c.Opts.VaultFile)
log.Println("5. Run the Codesphere installer with these configuration files")
log.Println(" Keep the vault file and its decryption key secure.")
log.Println()
}

Expand Down
12 changes: 6 additions & 6 deletions cli/cmd/init_install_config_interactive_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
var _ = Describe("Interactive profile usage", func() {
Context("when using profile with interactive mode", func() {
It("should use profile values as defaults", func() {
icg := installer.NewInstallConfigManager()
icg := newPlainInstallConfigManager()

// Apply dev profile first (like the command does)
err := icg.ApplyProfile("dev")
Expand Down Expand Up @@ -65,7 +65,7 @@ var _ = Describe("Interactive profile usage", func() {
})

It("should allow non-interactive collection to use profile defaults", func() {
icg := installer.NewInstallConfigManager()
icg := newPlainInstallConfigManager()

// Apply dev profile
err := icg.ApplyProfile("dev")
Expand Down Expand Up @@ -108,7 +108,7 @@ var _ = Describe("Interactive profile usage", func() {
FileWriter: intutil.NewFilesystemWriter(),
}

icg := installer.NewInstallConfigManager()
icg := newPlainInstallConfigManager()
err = c.InitInstallConfig(icg)
Expect(err).NotTo(HaveOccurred())

Expand All @@ -131,7 +131,7 @@ var _ = Describe("Interactive profile usage", func() {

Context("when using production profile", func() {
It("should set production-specific defaults", func() {
icg := installer.NewInstallConfigManager()
icg := newPlainInstallConfigManager()

err := icg.ApplyProfile("production")
Expect(err).NotTo(HaveOccurred())
Expand All @@ -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().WriteUnencryptedVault("vault.yaml", false).Return(nil)
mockIcg.EXPECT().WriteVault("vault.yaml", false).Return(nil)

c := &InitInstallConfigCmd{
Opts: &InitInstallConfigOpts{
Expand Down Expand Up @@ -195,7 +195,7 @@ var _ = Describe("Interactive profile usage", func() {
FileWriter: intutil.NewFilesystemWriter(),
}

icg := installer.NewInstallConfigManager()
icg := newPlainInstallConfigManager()

err = c.InitInstallConfig(icg)
Expect(err).To(HaveOccurred())
Expand Down
Loading
Loading