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
1 change: 1 addition & 0 deletions cli/cmd/bootstrap_gcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ func AddBootstrapGcpCmd(parent *cobra.Command, opts *GlobalOptions) {
flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.PrometheusRemoteWriteURL, "prometheus-remote-write-url", "", "Prometheus remote write URL (optional)")
flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.PrometheusRemoteWriteUser, "prometheus-remote-write-user", "", "Prometheus remote write username (optional)")
flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.PrometheusRemoteWritePassword, "prometheus-remote-write-password", "", "Prometheus remote write password stored in the generated vault (optional)")
flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.ClusterAdminEmail, "cluster-admin-email", "", "Email address of the initial cluster admin. Written to the install config and applied as the cluster-admin-email secret before the Codesphere platform is installed (optional)")

flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.InstallConfigPath, "install-config", "config.yaml", "Path to install config file (optional)")
flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.SecretsFilePath, "secrets-file", "prod.vault.yaml", "Path to secrets files (optional)")
Expand Down
7 changes: 4 additions & 3 deletions cli/cmd/install_codesphere.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ type InstallCodesphereOpts struct {
PCAppsValues []string
}

func (c *InstallCodesphereCmd) RunE(_ *cobra.Command, _ []string) error {
func (c *InstallCodesphereCmd) RunE(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()
effectiveOpts, cfg, cleanup, err := prepareInstallConfig(c.Opts, installer.NewConfig())
if err != nil {
return err
Expand All @@ -73,7 +74,7 @@ func (c *InstallCodesphereCmd) RunE(_ *cobra.Command, _ []string) error {
}

if c.Opts.CodesphereOnly {
return installCodespherePlatform(effectiveOpts, c.Env)
return installCodespherePlatform(ctx, effectiveOpts, cfg, c.Env)
}

if infraInstaller.HasExecutableSteps(cfg) {
Expand All @@ -92,7 +93,7 @@ func (c *InstallCodesphereCmd) RunE(_ *cobra.Command, _ []string) error {
return nil
}

return installCodespherePlatform(effectiveOpts, c.Env)
return installCodespherePlatform(ctx, effectiveOpts, cfg, c.Env)
}

func AddInstallCodesphereCmd(install *cobra.Command, opts *GlobalOptions) {
Expand Down
41 changes: 3 additions & 38 deletions cli/cmd/install_codesphere_dependencies.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ import (
"context"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"

"github.com/codesphere-cloud/cs-go/pkg/io"
"github.com/codesphere-cloud/oms/internal/bootstrap"
Expand All @@ -21,7 +19,6 @@ import (
"github.com/codesphere-cloud/oms/internal/system"
"github.com/spf13/cobra"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
)

Expand Down Expand Up @@ -119,47 +116,26 @@ type argoCDAndAppsInstall struct {
}

func (i *argoCDAndAppsInstall) loadVaultData() error {
vaultPath, err := i.resolveVaultPath()
vault, kubeConfig, err := installer.VaultAndRESTConfig(i.opts.Vault, i.opts.PrivKey, i.config)
if err != nil {
return err
}
i.vault = vault
i.kubeConfig = kubeConfig

i.vault, err = installer.LoadVaultData(vaultPath, i.opts.PrivKey)
if err != nil {
return fmt.Errorf("failed to load vault %s: %w", vaultPath, err)
}
if s := i.vault.GetSecret(files.SecretRegistryPassword); s != nil && s.Fields != nil {
i.ociPassword = s.Fields.Password
}
if i.ociPassword == "" {
return fmt.Errorf("registry password not found in vault (secret %q)", files.SecretRegistryPassword)
}
kubeConfigContent, err := kubeConfigContentFromVault(i.vault)
if err != nil {
return err
}

i.kubeConfig, err = clientcmd.RESTConfigFromKubeConfig([]byte(kubeConfigContent))
if err != nil {
return fmt.Errorf("failed to load kubernetes config from vault: %w", err)
}
i.kubeClient, err = ctrlclient.New(i.kubeConfig, ctrlclient.Options{})
if err != nil {
return fmt.Errorf("failed to create kubernetes client: %w", err)
}
return nil
}

func (i *argoCDAndAppsInstall) resolveVaultPath() (string, error) {
if strings.TrimSpace(i.opts.Vault) != "" {
return i.opts.Vault, nil
}
if strings.TrimSpace(i.config.Secrets.BaseDir) == "" {
return "", fmt.Errorf("vault path is not set and config.yaml secrets.baseDir is empty")
}
return filepath.Join(i.config.Secrets.BaseDir, "prod.vault.yaml"), nil
}

func (i *argoCDAndAppsInstall) installArgoCD() error {
i.ociRegistryURL = i.opts.ArgoCDRegistryURL
if i.ociRegistryURL == "" && i.config.Registry != nil {
Expand Down Expand Up @@ -217,17 +193,6 @@ func (i *argoCDAndAppsInstall) installPcApps() error {
return nil
}

func kubeConfigContentFromVault(vault *files.InstallVault) (string, error) {
if vault == nil {
return "", fmt.Errorf("vault is not loaded")
}
kubeConfig := vault.GetSecret(files.SecretKubeConfig)
if kubeConfig == nil || kubeConfig.File == nil || strings.TrimSpace(kubeConfig.File.Content) == "" {
return "", fmt.Errorf("kubeconfig not found in vault (secret %q)", files.SecretKubeConfig)
}
return kubeConfig.File.Content, nil
}

func AddInstallCodesphereDepenciesCmd(codesphere *cobra.Command, opts *InstallCodesphereOpts) {
deps := InstallCodesphereDepenciesCmd{
cmd: &cobra.Command{
Expand Down
15 changes: 10 additions & 5 deletions cli/cmd/install_codesphere_platform.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/codesphere-cloud/cs-go/pkg/io"
"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/spf13/cobra"
)
Expand All @@ -22,21 +23,25 @@ type InstallCodespherePlatformCmd struct {
Env env.Env
}

func (c *InstallCodespherePlatformCmd) RunE(_ *cobra.Command, _ []string) error {
effectiveOpts, _, cleanup, err := prepareInstallConfig(c.Opts, installer.NewConfig())
func (c *InstallCodespherePlatformCmd) RunE(cmd *cobra.Command, _ []string) error {
effectiveOpts, cfg, cleanup, err := prepareInstallConfig(c.Opts, installer.NewConfig())
if err != nil {
return err
}
defer cleanup()

return installCodespherePlatform(effectiveOpts, c.Env)
return installCodespherePlatform(cmd.Context(), effectiveOpts, cfg, c.Env)
}

func installCodespherePlatform(opts *InstallCodesphereOpts, env env.Env) error {
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 {
return fmt.Errorf("failed to set cluster admin email: %w", err)
}

workdir := env.GetOmsWorkdir()
pm := installer.NewPackage(workdir, opts.Package)
cm := installer.NewConfig()
im := system.NewImage(context.Background())
im := system.NewImage(ctx)

ci := &installer.CodesphereInstaller{
ConfigPath: opts.ConfigPath,
Expand Down
5 changes: 4 additions & 1 deletion cli/cmd/install_codesphere_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package cmd_test

import (
"context"
"os"
"runtime"

Expand Down Expand Up @@ -56,7 +57,9 @@ var _ = Describe("InstallCodesphereCmd", func() {
c.Opts.Configs = []string{tempConfigFile.Name()}
mockEnv.EXPECT().GetOmsWorkdir().Return("/test/workdir")

err = c.RunE(nil, []string{})
runCmd := &cobra.Command{}
runCmd.SetContext(context.Background())
err = c.RunE(runCmd, []string{})

Expect(err).To(HaveOccurred())
if runtime.GOOS != "linux" || runtime.GOARCH != "amd64" {
Expand Down
1 change: 1 addition & 0 deletions docs/oms_beta_bootstrap-gcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ oms beta bootstrap-gcp [flags]
--central-otel-password string Central OpenTelemetry password. Needed when sending spans to central collector (optional)
--central-otel-span-metrics Enable span metrics in Central OpenTelemetry export (default: false)
--central-otel-username string Central OpenTelemetry username. Needed when sending spans to central collector (optional)
--cluster-admin-email string Email address of the initial cluster admin. Written to the install config and applied as the cluster-admin-email secret before the Codesphere platform is installed (optional)
--create-test-user Create a test user with API token on the bootstrapped instance for smoke testing (default: false)
--custom-pg-ip string Custom PostgreSQL IP (optional)
--datacenter-id int Datacenter ID (default: 1) (default 1)
Expand Down
27 changes: 27 additions & 0 deletions internal/bootstrap/gcp/gcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (

"cloud.google.com/go/compute/apiv1/computepb"
"github.com/codesphere-cloud/oms/internal/bootstrap"
"github.com/codesphere-cloud/oms/internal/clusteradmin"
"github.com/codesphere-cloud/oms/internal/env"
"github.com/codesphere-cloud/oms/internal/github"
"github.com/codesphere-cloud/oms/internal/installer"
Expand Down Expand Up @@ -134,6 +135,7 @@ type CodesphereEnvironment struct {
PrometheusRemoteWriteUser string `json:"prometheus_remote_write_user,omitempty"`
PrometheusRemoteWritePassword string `json:"-"`
PrometheusRemoteWriteURL string `json:"prometheus_remote_write_url,omitempty"`
ClusterAdminEmail string `json:"cluster_admin_email,omitempty"`

// ACME Issuer
GoogleACMEIssuer bool `json:"google_acme_issuer,omitempty"`
Expand Down Expand Up @@ -438,9 +440,34 @@ func (b *GCPBootstrapper) ValidateInput() error {
return err
}

err = b.validateClusterAdminEmail()
if err != nil {
return err
}

return b.validateTelemetryExportParams()
}

func (b *GCPBootstrapper) validateClusterAdminEmail() error {
if b.Env.ClusterAdminEmail == "" {
return nil
}

// The email reaches the cluster via the install config, which is only
// updated when the config is written.
if !b.Env.WriteConfig {
return fmt.Errorf("cluster admin email requires write-config to be enabled")
}

email, err := clusteradmin.NormalizeEmail(b.Env.ClusterAdminEmail)
if err != nil {
return fmt.Errorf("invalid cluster admin email: %w", err)
}
b.Env.ClusterAdminEmail = email

return nil
}
Comment thread
nbrodnicke marked this conversation as resolved.

// validateInstallVersion checks if the specified install version exists and contains the required installer artifact
func (b *GCPBootstrapper) validateInstallVersion() error {
if b.Env.InstallLocal != "" {
Expand Down
31 changes: 31 additions & 0 deletions internal/bootstrap/gcp/gcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,37 @@ var _ = Describe("GCP Bootstrapper", func() {
})
})
})
Context("When a cluster admin email is set", func() {
BeforeEach(func() {
csEnv.ClusterAdminEmail = "Admin@Codesphere.com"
csEnv.WriteConfig = true
})
It("passes validation and normalizes the email", func() {
err := bs.ValidateInput()
Expect(err).NotTo(HaveOccurred())
Expect(bs.Env.ClusterAdminEmail).To(Equal("admin@codesphere.com"))
})

Context("when the email is invalid", func() {
BeforeEach(func() {
csEnv.ClusterAdminEmail = "not-an-email"
})
It("fails", func() {
err := bs.ValidateInput()
Expect(err).To(MatchError(MatchRegexp("invalid cluster admin email")))
})
})

Context("when write-config is disabled", func() {
BeforeEach(func() {
csEnv.WriteConfig = false
})
It("fails", func() {
err := bs.ValidateInput()
Expect(err).To(MatchError(MatchRegexp("cluster admin email requires write-config")))
})
})
})
Context("When a version and hash are specified", func() {
BeforeEach(func() {
mockPortalClient = portal.NewMockPortal(GinkgoT())
Expand Down
4 changes: 4 additions & 0 deletions internal/bootstrap/gcp/install_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,10 @@ func (b *GCPBootstrapper) UpdateInstallConfig() error {
b.Env.InstallConfig.Codesphere.Internal = b.Env.InternalFlags
b.Env.InstallConfig.Codesphere.Preview = util.StringSliceToBoolMap(b.Env.PreviewFlags)
b.Env.InstallConfig.Codesphere.Features = util.StringSliceToBoolMap(b.Env.FeatureFlags)
// Only set when the flag is provided so a recovered config keeps its value on re-runs.
if b.Env.ClusterAdminEmail != "" {
b.Env.InstallConfig.Codesphere.ClusterAdminEmail = b.Env.ClusterAdminEmail
}
b.applyExternalLokiConfig()
b.applyPrometheusRemoteWriteConfig()

Expand Down
34 changes: 34 additions & 0 deletions internal/bootstrap/gcp/install_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,40 @@ var _ = Describe("Installconfig & Secrets", func() {
Expect(bs.Env.InstallConfig.Codesphere.Features).To(Equal(util.StringSliceToBoolMap(csEnv.FeatureFlags)))
})
})
Context("When cluster admin email is set", func() {
BeforeEach(func() {
csEnv.ClusterAdminEmail = "admin@codesphere.com"
})
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)

nodeClient.EXPECT().CopyFile(mock.Anything, mock.Anything, mock.Anything).Return(nil).Twice()

err := bs.UpdateInstallConfig()
Expect(err).NotTo(HaveOccurred())

Expect(bs.Env.InstallConfig.Codesphere.ClusterAdminEmail).To(Equal("admin@codesphere.com"))
})
})
Context("When cluster admin email is not set", func() {
BeforeEach(func() {
csEnv.InstallConfig.Codesphere.ClusterAdminEmail = "existing@codesphere.com"
})
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)

nodeClient.EXPECT().CopyFile(mock.Anything, mock.Anything, mock.Anything).Return(nil).Twice()

err := bs.UpdateInstallConfig()
Expect(err).NotTo(HaveOccurred())

Expect(bs.Env.InstallConfig.Codesphere.ClusterAdminEmail).To(Equal("existing@codesphere.com"))
})
})
Context("When GitHub App name is not set ", func() {
BeforeEach(func() {
csEnv.GitHubAppName = ""
Expand Down
27 changes: 24 additions & 3 deletions internal/clusteradmin/clusteradmin.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ type Opts struct {
Email string
Namespace string
SecretName string
// CreateNamespace creates the target namespace if it does not exist yet
// instead of failing. Used during installation, where the secret may be
// written before the platform charts have created the namespace.
CreateNamespace bool
}

// AddClusterAdmin writes the given email to the cluster-admin-email secret in
Expand All @@ -40,7 +44,7 @@ type Opts struct {
// The email is stored under the EmailKey data key. Running the command again
// with a different email overwrites the previous value.
func AddClusterAdmin(ctx context.Context, clientset kubernetes.Interface, opts Opts) error {
email, err := normalizeEmail(opts.Email)
email, err := NormalizeEmail(opts.Email)
if err != nil {
return err
}
Expand All @@ -52,6 +56,12 @@ func AddClusterAdmin(ctx context.Context, clientset kubernetes.Interface, opts O
return fmt.Errorf("secret name must not be empty")
}

if opts.CreateNamespace {
if err := ensureNamespace(ctx, clientset, opts.Namespace); err != nil {
return err
}
}

secrets := clientset.CoreV1().Secrets(opts.Namespace)

existing, err := secrets.Get(ctx, opts.SecretName, metav1.GetOptions{})
Expand Down Expand Up @@ -92,8 +102,19 @@ func AddClusterAdmin(ctx context.Context, clientset kubernetes.Interface, opts O
return nil
}

// normalizeEmail validates and canonicalizes an email address.
func normalizeEmail(raw string) (string, error) {
// ensureNamespace creates the namespace if it does not exist yet.
func ensureNamespace(ctx context.Context, clientset kubernetes.Interface, namespace string) error {
ns := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{Name: namespace},
}
if _, err := clientset.CoreV1().Namespaces().Create(ctx, ns, metav1.CreateOptions{}); err != nil && !apierrors.IsAlreadyExists(err) {
return fmt.Errorf("creating namespace %s: %w", namespace, err)
}
return nil
}
Comment thread
nbrodnicke marked this conversation as resolved.

// NormalizeEmail validates and canonicalizes an email address.
func NormalizeEmail(raw string) (string, error) {
trimmed := strings.TrimSpace(raw)
if trimmed == "" {
return "", fmt.Errorf("email must not be empty")
Expand Down
Loading
Loading