diff --git a/cli/cmd/bootstrap_gcp.go b/cli/cmd/bootstrap_gcp.go index 301b461a..a7cf22d6 100644 --- a/cli/cmd/bootstrap_gcp.go +++ b/cli/cmd/bootstrap_gcp.go @@ -38,7 +38,7 @@ func (c *BootstrapGcpCmd) RunE(_ *cobra.Command, args []string) error { return nil } -func AddBootstrapGcpCmd(root *cobra.Command, opts *GlobalOptions) { +func AddBootstrapGcpCmd(parent *cobra.Command, opts *GlobalOptions) { bootstrapGcpCmd := BootstrapGcpCmd{ cmd: &cobra.Command{ Use: "bootstrap-gcp", @@ -53,6 +53,7 @@ func AddBootstrapGcpCmd(root *cobra.Command, opts *GlobalOptions) { Env: env.NewEnv(), CodesphereEnv: &gcp.CodesphereEnvironment{}, } + bootstrapGcpCmd.cmd.RunE = bootstrapGcpCmd.RunE flags := bootstrapGcpCmd.cmd.Flags() flags.StringVar(&bootstrapGcpCmd.CodesphereEnv.ProjectName, "project-name", "", "Unique GCP Project Name (required)") @@ -82,8 +83,8 @@ func AddBootstrapGcpCmd(root *cobra.Command, opts *GlobalOptions) { util.MarkFlagRequired(bootstrapGcpCmd.cmd, "billing-account") util.MarkFlagRequired(bootstrapGcpCmd.cmd, "base-domain") - bootstrapGcpCmd.cmd.RunE = bootstrapGcpCmd.RunE - root.AddCommand(bootstrapGcpCmd.cmd) + parent.AddCommand(bootstrapGcpCmd.cmd) + AddBootstrapGcpPostconfigCmd(bootstrapGcpCmd.cmd, opts) } func (c *BootstrapGcpCmd) BootstrapGcp() error { @@ -92,9 +93,8 @@ func (c *BootstrapGcpCmd) BootstrapGcp() error { icg := installer.NewInstallConfigManager() gcpClient := gcp.NewGCPClient(ctx, stlog, os.Getenv("GOOGLE_APPLICATION_CREDENTIALS")) fw := util.NewFilesystemWriter() - nm := node.NewNode(fw, c.CodesphereEnv.SSHPrivateKeyPath, c.SSHQuiet) - bs, err := gcp.NewGCPBootstrapper(ctx, c.Env, stlog, c.CodesphereEnv, icg, gcpClient, nm, fw) + bs, err := gcp.NewGCPBootstrapper(ctx, c.Env, stlog, c.CodesphereEnv, icg, gcpClient, fw, node.NewSSHNodeClient(c.SSHQuiet)) if err != nil { return err } @@ -103,21 +103,34 @@ func (c *BootstrapGcpCmd) BootstrapGcp() error { err = bs.Bootstrap() envBytes, err2 := json.MarshalIndent(bs.Env, "", " ") + envString := string(envBytes) if err2 != nil { envString = "" } + if err != nil { - if bs.Env.Jumpbox != nil && bs.Env.Jumpbox.GetExternalIP() != "" { + if bs.Env.Jumpbox.GetExternalIP() != "" { log.Printf("To debug on the jumpbox host:\nssh-add $SSH_KEY_PATH; ssh -o StrictHostKeyChecking=no -o ForwardAgent=yes -o SendEnv=OMS_PORTAL_API_KEY root@%s", bs.Env.Jumpbox.GetExternalIP()) } return fmt.Errorf("failed to bootstrap GCP: %w, env: %s", err, envString) } + workdir := env.NewEnv().GetOmsWorkdir() + err = fw.MkdirAll(workdir, 0755) + if err != nil { + return fmt.Errorf("failed to create workdir: %w", err) + } + infraFilePath := gcp.GetInfraFilePath() + err = fw.WriteFile(infraFilePath, envBytes, 0644) + if err != nil { + return fmt.Errorf("failed to write gcp bootstrap env file: %w", err) + } + log.Println("\nšŸŽ‰šŸŽ‰šŸŽ‰ GCP infrastructure bootstrapped successfully!") log.Println(envString) + log.Printf("Infrastructure details written to %s", infraFilePath) log.Printf("Start the Codesphere installation using OMS from the jumpbox host:\nssh-add $SSH_KEY_PATH; ssh -o StrictHostKeyChecking=no -o ForwardAgent=yes -o SendEnv=OMS_PORTAL_API_KEY root@%s", bs.Env.Jumpbox.GetExternalIP()) - log.Printf("When the installation is done, run the k0s configuration script generated at the k0s-1 host %s /root/configure-k0s.sh.", bs.Env.ControlPlaneNodes[0].GetInternalIP()) - return err + return nil } diff --git a/cli/cmd/bootstrap_gcp_postconfig.go b/cli/cmd/bootstrap_gcp_postconfig.go new file mode 100644 index 00000000..8e0072d9 --- /dev/null +++ b/cli/cmd/bootstrap_gcp_postconfig.go @@ -0,0 +1,77 @@ +// Copyright (c) Codesphere Inc. +// SPDX-License-Identifier: Apache-2.0 + +package cmd + +import ( + "encoding/json" + "fmt" + "log" + + "github.com/codesphere-cloud/cs-go/pkg/io" + "github.com/codesphere-cloud/oms/internal/bootstrap/gcp" + "github.com/codesphere-cloud/oms/internal/installer" + "github.com/codesphere-cloud/oms/internal/util" + "github.com/spf13/cobra" +) + +type BootstrapGcpPostconfigCmd struct { + cmd *cobra.Command + + Opts *BootstrapGcpPostconfigOpts + CodesphereEnv gcp.CodesphereEnvironment +} + +type BootstrapGcpPostconfigOpts struct { + *GlobalOptions + InstallConfigPath string + PrivateKeyPath string +} + +func (c *BootstrapGcpPostconfigCmd) RunE(_ *cobra.Command, args []string) error { + log.Printf("running post-configuration steps...") + + icg := installer.NewInstallConfigManager() + + fw := util.NewFilesystemWriter() + + envFileContent, err := fw.ReadFile(gcp.GetInfraFilePath()) + if err != nil { + return fmt.Errorf("failed to read gcp infra file: %w", err) + } + + err = json.Unmarshal(envFileContent, &c.CodesphereEnv) + if err != nil { + return fmt.Errorf("failed to unmarshal gcp infra file: %w", err) + } + + err = icg.LoadInstallConfigFromFile(c.Opts.InstallConfigPath) + if err != nil { + return fmt.Errorf("failed to load config file: %w", err) + } + + return fmt.Errorf("not implemented: run config script on k0s-1 node to install GCP CCM") +} + +func AddBootstrapGcpPostconfigCmd(bootstrapGcp *cobra.Command, opts *GlobalOptions) { + postconfig := BootstrapGcpPostconfigCmd{ + cmd: &cobra.Command{ + Use: "postconfig", + Short: "Run post-configuration steps for GCP bootstrapping", + Long: io.Long(`After bootstrapping GCP infrastructure, this command runs additional configuration steps + to finalize the setup for the Codesphere cluster on GCP: + + * Install Google Cloud Controller Manager for ingress management.`), + }, + Opts: &BootstrapGcpPostconfigOpts{ + GlobalOptions: opts, + }, + } + + flags := postconfig.cmd.Flags() + flags.StringVar(&postconfig.Opts.InstallConfigPath, "install-config-path", "config.yaml", "Path to the installation configuration file") + flags.StringVar(&postconfig.Opts.PrivateKeyPath, "private-key-path", "", "Path to the GCP service account private key file (optional)") + + bootstrapGcp.AddCommand(postconfig.cmd) + postconfig.cmd.RunE = postconfig.RunE +} diff --git a/docs/oms-cli_beta_bootstrap-gcp.md b/docs/oms-cli_beta_bootstrap-gcp.md index ecf06c0f..b3fdc52f 100644 --- a/docs/oms-cli_beta_bootstrap-gcp.md +++ b/docs/oms-cli_beta_bootstrap-gcp.md @@ -45,4 +45,5 @@ oms-cli beta bootstrap-gcp [flags] ### SEE ALSO * [oms-cli beta](oms-cli_beta.md) - Commands for early testing +* [oms-cli beta bootstrap-gcp postconfig](oms-cli_beta_bootstrap-gcp_postconfig.md) - Run post-configuration steps for GCP bootstrapping diff --git a/docs/oms-cli_beta_bootstrap-gcp_postconfig.md b/docs/oms-cli_beta_bootstrap-gcp_postconfig.md new file mode 100644 index 00000000..d2af9c9e --- /dev/null +++ b/docs/oms-cli_beta_bootstrap-gcp_postconfig.md @@ -0,0 +1,27 @@ +## oms-cli beta bootstrap-gcp postconfig + +Run post-configuration steps for GCP bootstrapping + +### Synopsis + +After bootstrapping GCP infrastructure, this command runs additional configuration steps +to finalize the setup for the Codesphere cluster on GCP: + +* Install Google Cloud Controller Manager for ingress management. + +``` +oms-cli beta bootstrap-gcp postconfig [flags] +``` + +### Options + +``` + -h, --help help for postconfig + --install-config-path string Path to the installation configuration file (default "config.yaml") + --private-key-path string Path to the GCP service account private key file (optional) +``` + +### SEE ALSO + +* [oms-cli beta bootstrap-gcp](oms-cli_beta_bootstrap-gcp.md) - Bootstrap GCP infrastructure for Codesphere + diff --git a/internal/bootstrap/gcp/gcp.go b/internal/bootstrap/gcp/gcp.go index 431ecba0..20742ba6 100644 --- a/internal/bootstrap/gcp/gcp.go +++ b/internal/bootstrap/gcp/gcp.go @@ -54,34 +54,33 @@ var vmDefs = []VMDef{ } type GCPBootstrapper struct { - ctx context.Context - stlog *bootstrap.StepLogger - fw util.FileIO - icg installer.InstallConfigManager - NodeManager node.NodeManager - GCPClient GCPClientManager + ctx context.Context + stlog *bootstrap.StepLogger + fw util.FileIO + icg installer.InstallConfigManager + GCPClient GCPClientManager // Environment Env *CodesphereEnvironment - // SSH options - sshQuiet bool + // SSH command runner + CommandRunner node.NodeClient } type CodesphereEnvironment struct { - ProjectID string `json:"project_id"` - ProjectName string `json:"project_name"` - DNSProjectID string `json:"dns_project_id"` - Jumpbox node.NodeManager `json:"jumpbox"` - PostgreSQLNode node.NodeManager `json:"postgresql_node"` - ControlPlaneNodes []node.NodeManager `json:"control_plane_nodes"` - CephNodes []node.NodeManager `json:"ceph_nodes"` - ContainerRegistryURL string `json:"-"` - ExistingConfigUsed bool `json:"-"` - InstallCodesphereVersion string `json:"install_codesphere_version"` - Preemptible bool `json:"preemptible"` - WriteConfig bool `json:"-"` - GatewayIP string `json:"gateway_ip"` - PublicGatewayIP string `json:"public_gateway_ip"` - RegistryType RegistryType `json:"registry_type"` + ProjectID string `json:"project_id"` + ProjectName string `json:"project_name"` + DNSProjectID string `json:"dns_project_id"` + Jumpbox *node.Node `json:"jumpbox"` + PostgreSQLNode *node.Node `json:"postgres_node"` + ControlPlaneNodes []*node.Node `json:"control_plane_nodes"` + CephNodes []*node.Node `json:"ceph_nodes"` + ContainerRegistryURL string `json:"-"` + ExistingConfigUsed bool `json:"-"` + InstallCodesphereVersion string `json:"install_codesphere_version"` + Preemptible bool `json:"preemptible"` + WriteConfig bool `json:"-"` + GatewayIP string `json:"gateway_ip"` + PublicGatewayIP string `json:"public_gateway_ip"` + RegistryType RegistryType `json:"registry_type"` // Config InstallConfigPath string `json:"-"` @@ -106,19 +105,31 @@ type CodesphereEnvironment struct { DNSZoneName string `json:"dns_zone_name"` } -func NewGCPBootstrapper(ctx context.Context, env env.Env, stlog *bootstrap.StepLogger, CodesphereEnv *CodesphereEnvironment, icg installer.InstallConfigManager, gcpClient GCPClientManager, nm node.NodeManager, fw util.FileIO) (*GCPBootstrapper, error) { +func NewGCPBootstrapper( + ctx context.Context, + env env.Env, + stlog *bootstrap.StepLogger, + CodesphereEnv *CodesphereEnvironment, + icg installer.InstallConfigManager, + gcpClient GCPClientManager, + fw util.FileIO, + sshRunner node.NodeClient) (*GCPBootstrapper, error) { return &GCPBootstrapper{ - ctx: ctx, - stlog: stlog, - fw: fw, - icg: icg, - GCPClient: gcpClient, - NodeManager: nm, - Env: CodesphereEnv, - sshQuiet: true, + ctx: ctx, + stlog: stlog, + fw: fw, + icg: icg, + GCPClient: gcpClient, + Env: CodesphereEnv, + CommandRunner: sshRunner, }, nil } +func GetInfraFilePath() string { + workdir := env.NewEnv().GetOmsWorkdir() + return fmt.Sprintf("%s/gcp-infra.json", workdir) +} + func (b *GCPBootstrapper) Bootstrap() error { err := b.stlog.Step("Ensure install config", b.EnsureInstallConfig) if err != nil { @@ -659,18 +670,20 @@ func (b *GCPBootstrapper) EnsureComputeInstances() error { } // Create nodes from results (in main goroutine, not in spawned goroutines) + b.Env.Jumpbox = &node.Node{ + NodeClient: b.CommandRunner, + } for result := range resultCh { switch result.vmType { case "jumpbox": - b.NodeManager.UpdateNode(result.name, result.externalIP, result.internalIP) - b.Env.Jumpbox = b.NodeManager + b.Env.Jumpbox.UpdateNode(result.name, result.externalIP, result.internalIP) case "postgres": - b.Env.PostgreSQLNode = b.NodeManager.CreateSubNode(result.name, result.externalIP, result.internalIP) + b.Env.PostgreSQLNode = b.Env.Jumpbox.CreateSubNode(result.name, result.externalIP, result.internalIP) case "ceph": - node := b.NodeManager.CreateSubNode(result.name, result.externalIP, result.internalIP) + node := b.Env.Jumpbox.CreateSubNode(result.name, result.externalIP, result.internalIP) b.Env.CephNodes = append(b.Env.CephNodes, node) case "k0s": - node := b.NodeManager.CreateSubNode(result.name, result.externalIP, result.internalIP) + node := b.Env.Jumpbox.CreateSubNode(result.name, result.externalIP, result.internalIP) b.Env.ControlPlaneNodes = append(b.Env.ControlPlaneNodes, node) } } @@ -734,7 +747,7 @@ func (b *GCPBootstrapper) EnsureExternalIP(name string) (string, error) { } func (b *GCPBootstrapper) EnsureRootLoginEnabled() error { - allNodes := []node.NodeManager{ + allNodes := []*node.Node{ b.Env.Jumpbox, } allNodes = append(allNodes, b.Env.ControlPlaneNodes...) @@ -753,8 +766,8 @@ func (b *GCPBootstrapper) EnsureRootLoginEnabled() error { return nil } -func (b *GCPBootstrapper) ensureRootLoginEnabledInNode(node node.NodeManager) error { - err := node.WaitForSSH(30 * time.Second) +func (b *GCPBootstrapper) ensureRootLoginEnabledInNode(node *node.Node) error { + err := node.NodeClient.WaitReady(node, 30*time.Second) if err != nil { return fmt.Errorf("timed out waiting for SSH service to start on %s: %w", node.GetName(), err) } @@ -829,7 +842,7 @@ func (b *GCPBootstrapper) EnsureLocalContainerRegistry() error { // Figure out if registry is already running b.stlog.Logf("Checking if local container registry is already running on postgres node") checkCommand := `test "$(podman ps --filter 'name=registry' --format '{{.Names}}' | wc -l)" -eq "1"` - err := b.Env.PostgreSQLNode.RunSSHCommand("root", checkCommand, b.sshQuiet) + err := b.Env.PostgreSQLNode.RunSSHCommand("root", checkCommand) if err == nil && b.Env.InstallConfig.Registry != nil && b.Env.InstallConfig.Registry.Server == localRegistryServer && b.Env.InstallConfig.Registry.Username != "" && b.Env.InstallConfig.Registry.Password != "" { b.stlog.Logf("Local container registry already running on postgres node") @@ -863,7 +876,7 @@ func (b *GCPBootstrapper) EnsureLocalContainerRegistry() error { } for _, cmd := range commands { b.stlog.Logf("Running command on postgres node: %s", util.Truncate(cmd, 12)) - err := b.Env.PostgreSQLNode.RunSSHCommand("root", cmd, b.sshQuiet) + err := b.Env.PostgreSQLNode.RunSSHCommand("root", cmd) if err != nil { return fmt.Errorf("failed to run command on postgres node: %w", err) } @@ -872,15 +885,15 @@ func (b *GCPBootstrapper) EnsureLocalContainerRegistry() error { allNodes := append(b.Env.ControlPlaneNodes, b.Env.CephNodes...) for _, node := range allNodes { b.stlog.Logf("Configuring node '%s' to trust local registry certificate", node.GetName()) - err := b.Env.PostgreSQLNode.RunSSHCommand("root", "scp -o StrictHostKeyChecking=no /root/registry.crt root@"+node.GetInternalIP()+":/usr/local/share/ca-certificates/registry.crt", b.sshQuiet) + err := b.Env.PostgreSQLNode.RunSSHCommand("root", "scp -o StrictHostKeyChecking=no /root/registry.crt root@"+node.GetInternalIP()+":/usr/local/share/ca-certificates/registry.crt") if err != nil { return fmt.Errorf("failed to copy registry certificate to node %s: %w", node.GetInternalIP(), err) } - err = node.RunSSHCommand("root", "update-ca-certificates", b.sshQuiet) + err = node.RunSSHCommand("root", "update-ca-certificates") if err != nil { return fmt.Errorf("failed to update CA certificates on node %s: %w", node.GetInternalIP(), err) } - err = node.RunSSHCommand("root", "systemctl restart docker.service || true", true) // docker is probably not yet installed + err = node.RunSSHCommand("root", "systemctl restart docker.service || true") // docker is probably not yet installed if err != nil { return fmt.Errorf("failed to restart docker service on node %s: %w", node.GetInternalIP(), err) } @@ -1128,12 +1141,12 @@ func (b *GCPBootstrapper) UpdateInstallConfig() error { return fmt.Errorf("failed to write vault file: %w", err) } - err := b.Env.Jumpbox.CopyFile(b.Env.InstallConfigPath, "/etc/codesphere/config.yaml") + err := b.Env.Jumpbox.NodeClient.CopyFile(b.Env.Jumpbox, b.Env.InstallConfigPath, "/etc/codesphere/config.yaml") if err != nil { return fmt.Errorf("failed to copy install config to jumpbox: %w", err) } - err = b.Env.Jumpbox.CopyFile(b.Env.SecretsFilePath, b.Env.SecretsDir+"/prod.vault.yaml") + err = b.Env.Jumpbox.NodeClient.CopyFile(b.Env.Jumpbox, b.Env.SecretsFilePath, b.Env.SecretsDir+"/prod.vault.yaml") if err != nil { return fmt.Errorf("failed to copy secrets file to jumpbox: %w", err) } @@ -1141,12 +1154,12 @@ func (b *GCPBootstrapper) UpdateInstallConfig() error { } func (b *GCPBootstrapper) EnsureAgeKey() error { - hasKey := b.Env.Jumpbox.HasFile(b.Env.SecretsDir + "/age_key.txt") + hasKey := b.Env.Jumpbox.NodeClient.HasFile(b.Env.Jumpbox, b.Env.SecretsDir+"/age_key.txt") if hasKey { return nil } - err := b.Env.Jumpbox.RunSSHCommand("root", fmt.Sprintf("mkdir -p %s; age-keygen -o %s/age_key.txt", b.Env.SecretsDir, b.Env.SecretsDir), b.sshQuiet) + err := b.Env.Jumpbox.RunSSHCommand("root", fmt.Sprintf("mkdir -p %s; age-keygen -o %s/age_key.txt", b.Env.SecretsDir, b.Env.SecretsDir)) if err != nil { return fmt.Errorf("failed to generate age key on jumpbox: %w", err) } @@ -1155,12 +1168,12 @@ func (b *GCPBootstrapper) EnsureAgeKey() error { } func (b *GCPBootstrapper) EncryptVault() error { - err := b.Env.Jumpbox.RunSSHCommand("root", "cp "+b.Env.SecretsDir+"/prod.vault.yaml{,.bak}", b.sshQuiet) + err := b.Env.Jumpbox.RunSSHCommand("root", "cp "+b.Env.SecretsDir+"/prod.vault.yaml{,.bak}") if err != nil { return fmt.Errorf("failed backup vault on jumpbox: %w", err) } - err = b.Env.Jumpbox.RunSSHCommand("root", "sops --encrypt --in-place --age $(age-keygen -y "+b.Env.SecretsDir+"/age_key.txt) "+b.Env.SecretsDir+"/prod.vault.yaml", b.sshQuiet) + err = b.Env.Jumpbox.RunSSHCommand("root", "sops --encrypt --in-place --age $(age-keygen -y "+b.Env.SecretsDir+"/age_key.txt) "+b.Env.SecretsDir+"/prod.vault.yaml") if err != nil { return fmt.Errorf("failed to encrypt vault on jumpbox: %w", err) } @@ -1216,12 +1229,12 @@ func (b *GCPBootstrapper) EnsureDNSRecords() error { } func (b *GCPBootstrapper) InstallCodesphere() error { - err := b.Env.Jumpbox.RunSSHCommand("root", "oms-cli download package "+b.Env.InstallCodesphereVersion, b.sshQuiet) + err := b.Env.Jumpbox.RunSSHCommand("root", "oms-cli download package "+b.Env.InstallCodesphereVersion) if err != nil { return fmt.Errorf("failed to download Codesphere package from jumpbox: %w", err) } - err = b.Env.Jumpbox.RunSSHCommand("root", "oms-cli install codesphere -c /etc/codesphere/config.yaml -k "+b.Env.SecretsDir+"/age_key.txt -p "+b.Env.InstallCodesphereVersion+".tar.gz", b.sshQuiet) + err = b.Env.Jumpbox.RunSSHCommand("root", "oms-cli install codesphere -c /etc/codesphere/config.yaml -k "+b.Env.SecretsDir+"/age_key.txt -p "+b.Env.InstallCodesphereVersion+".tar.gz") if err != nil { return fmt.Errorf("failed to install Codesphere from jumpbox: %w", err) } @@ -1317,11 +1330,11 @@ systemctl restart k0scontroller if err != nil { return fmt.Errorf("failed to write configure-k0s.sh: %w", err) } - err = b.Env.ControlPlaneNodes[0].CopyFile("configure-k0s.sh", "/root/configure-k0s.sh") + err = b.Env.ControlPlaneNodes[0].NodeClient.CopyFile(b.Env.ControlPlaneNodes[0], "configure-k0s.sh", "/root/configure-k0s.sh") if err != nil { return fmt.Errorf("failed to copy configure-k0s.sh to control plane node: %w", err) } - err = b.Env.ControlPlaneNodes[0].RunSSHCommand("root", "chmod +x /root/configure-k0s.sh", b.sshQuiet) + err = b.Env.ControlPlaneNodes[0].RunSSHCommand("root", "chmod +x /root/configure-k0s.sh") if err != nil { return fmt.Errorf("failed to make configure-k0s.sh executable on control plane node: %w", err) } diff --git a/internal/bootstrap/gcp/gcp_test.go b/internal/bootstrap/gcp/gcp_test.go index 0a2149e9..e27ad419 100644 --- a/internal/bootstrap/gcp/gcp_test.go +++ b/internal/bootstrap/gcp/gcp_test.go @@ -7,7 +7,6 @@ import ( "context" "fmt" "strings" - "time" "os" @@ -30,2535 +29,2021 @@ import ( func protoString(s string) *string { return &s } -var _ = Describe("NewGCPBootstrapper", func() { - It("creates a valid GCPBootstrapper", func() { - env := env.NewEnv() - Expect(env).NotTo(BeNil()) +func jumpbboxMatcher(node *node.Node) bool { + return node.GetName() == "jumpbox" +} - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{} - stlog := bootstrap.NewStepLogger(false) - - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) - - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) - Expect(bs).NotTo(BeNil()) - }) -}) - -var _ = Describe("Bootstrap", func() { +var _ = Describe("GCP Bootstrapper", func() { var ( - e env.Env - ctx context.Context - csEnv *gcp.CodesphereEnvironment - icg *installer.MockInstallConfigManager - gc *gcp.MockGCPClientManager - fw *util.MockFileIO - nm *node.MockNodeManager - bs *gcp.GCPBootstrapper + nodeClient *node.MockNodeClient + csEnv *gcp.CodesphereEnvironment + ctx context.Context + e env.Env ) BeforeEach(func() { - e = env.NewEnv() + nodeClient = node.NewMockNodeClient(GinkgoT()) ctx = context.Background() + e = env.NewEnv() + csEnv = &gcp.CodesphereEnvironment{ InstallConfigPath: "fake-config-file", SecretsFilePath: "fake-secret", ProjectName: "test-project", + SecretsDir: "/etc/codesphere/secrets", BillingAccount: "test-billing-account", Region: "us-central1", Zone: "us-central1-a", + DatacenterID: 1, BaseDomain: "example.com", DNSProjectID: "dns-project", DNSZoneName: "test-zone", - } - stlog := bootstrap.NewStepLogger(false) - - icg = installer.NewMockInstallConfigManager(GinkgoT()) - gc = gcp.NewMockGCPClientManager(GinkgoT()) - fw = util.NewMockFileIO(GinkgoT()) - nm = node.NewMockNodeManager(GinkgoT()) - - var err error - bs, err = gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) - }) - - It("runs bootstrap successfully", func() { - bs.Env.RegistryType = gcp.RegistryTypeArtifactRegistry - bs.Env.WriteConfig = true - bs.Env.SecretsDir = "/secrets" - - // 1. EnsureInstallConfig - fw.EXPECT().Exists("fake-config-file").Return(false) - icg.EXPECT().ApplyProfile("dev").Return(nil) - // Returning a real install config to avoid nil pointer dereferences later - icg.EXPECT().GetInstallConfig().RunAndReturn(func() *files.RootConfig { - realIcm := installer.NewInstallConfigManager() - _ = realIcm.ApplyProfile("dev") - return realIcm.GetInstallConfig() - }) - - // 2. EnsureSecrets - fw.EXPECT().Exists("fake-secret").Return(false) - icg.EXPECT().GetVault().Return(&files.InstallVault{}) - - // 3. EnsureProject - gc.EXPECT().GetProjectByName(mock.Anything, "test-project").Return(nil, fmt.Errorf("project not found: test-project")) - gc.EXPECT().CreateProjectID("test-project").Return("test-project-id") - gc.EXPECT().CreateProject(mock.Anything, mock.Anything, "test-project").Return(mock.Anything, nil) - - // 4. EnsureBilling - gc.EXPECT().GetBillingInfo("test-project-id").Return(&cloudbilling.ProjectBillingInfo{BillingEnabled: false}, nil) - gc.EXPECT().EnableBilling("test-project-id", "test-billing-account").Return(nil) - - // 5. EnsureAPIsEnabled - gc.EXPECT().EnableAPIs("test-project-id", mock.Anything).Return(nil) - - // 6. EnsureArtifactRegistry - gc.EXPECT().GetArtifactRegistry("test-project-id", "us-central1", "codesphere-registry").Return(nil, fmt.Errorf("not found")) - gc.EXPECT().CreateArtifactRegistry("test-project-id", "us-central1", "codesphere-registry").Return(&artifactregistrypb.Repository{Name: "codesphere-registry"}, nil) - - // 7. EnsureServiceAccounts - gc.EXPECT().CreateServiceAccount("test-project-id", "cloud-controller", "cloud-controller").Return("cloud-controller@p.iam.gserviceaccount.com", false, nil) - gc.EXPECT().CreateServiceAccount("test-project-id", "artifact-registry-writer", "artifact-registry-writer").Return("writer@p.iam.gserviceaccount.com", true, nil) - gc.EXPECT().CreateServiceAccountKey("test-project-id", "writer@p.iam.gserviceaccount.com").Return("fake-key", nil) - - // 8. EnsureIAMRoles - gc.EXPECT().AssignIAMRole("test-project-id", "artifact-registry-writer", "roles/artifactregistry.writer").Return(nil) - gc.EXPECT().AssignIAMRole("test-project-id", "cloud-controller", "roles/compute.admin").Return(nil) - - // 9. EnsureVPC - gc.EXPECT().CreateVPC("test-project-id", "us-central1", "test-project-id-vpc", "test-project-id-us-central1-subnet", "test-project-id-router", "test-project-id-nat-gateway").Return(nil) - - // 10. EnsureFirewallRules (5 times) - gc.EXPECT().CreateFirewallRule("test-project-id", mock.Anything).Return(nil).Times(5) - - // 11. EnsureComputeInstances - gc.EXPECT().CreateInstance("test-project-id", "us-central1-a", mock.Anything).Return(nil).Times(9) - // GetInstance calls to retrieve IPs - ipResp := &computepb.Instance{ - NetworkInterfaces: []*computepb.NetworkInterface{ - { - NetworkIP: protoString("10.0.0.1"), - AccessConfigs: []*computepb.AccessConfig{ - {NatIP: protoString("1.2.3.4")}, - }, + ProjectID: "pid", + InstallConfig: &files.RootConfig{ + Registry: &files.RegistryConfig{}, + Postgres: files.PostgresConfig{ + Primary: &files.PostgresPrimaryConfig{}, }, + Cluster: files.ClusterConfig{}, }, - } - gc.EXPECT().GetInstance("test-project-id", "us-central1-a", mock.Anything).Return(ipResp, nil).Times(9) - fw.EXPECT().ReadFile(mock.Anything).Return([]byte("fake-key"), nil).Times(9) - // UpdateNode is called once for the jumpbox to set its name and IPs - nm.EXPECT().UpdateNode("jumpbox", "1.2.3.4", "10.0.0.1") - // CreateSubNode is called 8 times for the other nodes - nm.EXPECT().CreateSubNode("postgres", "1.2.3.4", "10.0.0.1").Return(nm) - nm.EXPECT().CreateSubNode("ceph-1", "1.2.3.4", "10.0.0.1").Return(nm) - nm.EXPECT().CreateSubNode("ceph-2", "1.2.3.4", "10.0.0.1").Return(nm) - nm.EXPECT().CreateSubNode("ceph-3", "1.2.3.4", "10.0.0.1").Return(nm) - nm.EXPECT().CreateSubNode("ceph-4", "1.2.3.4", "10.0.0.1").Return(nm) - nm.EXPECT().CreateSubNode("k0s-1", "1.2.3.4", "10.0.0.1").Return(nm) - nm.EXPECT().CreateSubNode("k0s-2", "1.2.3.4", "10.0.0.1").Return(nm) - nm.EXPECT().CreateSubNode("k0s-3", "1.2.3.4", "10.0.0.1").Return(nm) - - nm.EXPECT().GetName().Return("mocknode").Maybe() - nm.EXPECT().GetInternalIP().Return("10.0.0.1").Maybe() - nm.EXPECT().GetExternalIP().Return("1.2.3.4").Maybe() - - // 12. EnsureGatewayIPAddresses - gc.EXPECT().GetAddress("test-project-id", "us-central1", "gateway").Return(nil, fmt.Errorf("not found")) - gc.EXPECT().CreateAddress("test-project-id", "us-central1", mock.MatchedBy(func(addr *computepb.Address) bool { return *addr.Name == "gateway" })).Return("1.1.1.1", nil) - gc.EXPECT().GetAddress("test-project-id", "us-central1", "gateway").Return(nil, fmt.Errorf("not found")) - gc.EXPECT().GetAddress("test-project-id", "us-central1", "public-gateway").Return(nil, fmt.Errorf("not found")) - gc.EXPECT().CreateAddress("test-project-id", "us-central1", mock.MatchedBy(func(addr *computepb.Address) bool { return *addr.Name == "public-gateway" })).Return("2.2.2.2", nil) - gc.EXPECT().GetAddress("test-project-id", "us-central1", "public-gateway").Return(&computepb.Address{Address: protoString("2.2.2.2")}, nil) - - // 13. EnsureRootLoginEnabled - nm.EXPECT().WaitForSSH(30 * time.Second).Return(nil).Times(9) - nm.EXPECT().HasRootLoginEnabled().Return(false).Times(9) - nm.EXPECT().EnableRootLogin().Return(nil).Times(9) - - // 14. EnsureJumpboxConfigured - nm.EXPECT().HasAcceptEnvConfigured().Return(false) - nm.EXPECT().ConfigureAcceptEnv().Return(nil) - nm.EXPECT().HasCommand("oms-cli").Return(false) - nm.EXPECT().InstallOms().Return(nil) - - // 15. EnsureInotifyWatches - nm.EXPECT().HasInotifyWatchesConfigured().Return(false) - nm.EXPECT().ConfigureInotifyWatches().Return(nil) - nm.EXPECT().HasMemoryMapConfigured().Return(false) - nm.EXPECT().ConfigureMemoryMap().Return(nil) - - // 16. UpdateInstallConfig - icg.EXPECT().GenerateSecrets().Return(nil) - icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) - icg.EXPECT().WriteVault("fake-secret", true).Return(nil) - nm.EXPECT().CopyFile("fake-config-file", "/etc/codesphere/config.yaml").Return(nil) - nm.EXPECT().CopyFile("fake-secret", "/secrets/prod.vault.yaml").Return(nil) - - // 17. EnsureAgeKey - nm.EXPECT().HasFile("/secrets/age_key.txt").Return(false) - nm.EXPECT().RunSSHCommand("root", "mkdir -p /secrets; age-keygen -o /secrets/age_key.txt", true).Return(nil) - - // 18. EncryptVault - nm.EXPECT().RunSSHCommand("root", "cp /secrets/prod.vault.yaml{,.bak}", true).Return(nil) - nm.EXPECT().RunSSHCommand("root", mock.MatchedBy(func(cmd string) bool { - return strings.Contains(cmd, "sops --encrypt") - }), true).Return(nil) - - // 19. EnsureDNSRecords - gc.EXPECT().EnsureDNSManagedZone("dns-project", "test-zone", "example.com.", mock.Anything).Return(nil) - gc.EXPECT().EnsureDNSRecordSets("dns-project", "test-zone", mock.MatchedBy(func(records []*dns.ResourceRecordSet) bool { - return len(records) == 4 - })).Return(nil) - - // 20. GenerateK0sConfigScript - fw.EXPECT().WriteFile("configure-k0s.sh", mock.Anything, os.FileMode(0755)).Return(nil) - nm.EXPECT().CopyFile("configure-k0s.sh", "/root/configure-k0s.sh").Return(nil) - nm.EXPECT().RunSSHCommand("root", "chmod +x /root/configure-k0s.sh", true).Return(nil) - - err := bs.Bootstrap() - Expect(err).NotTo(HaveOccurred()) - Expect(bs.Env).NotTo(BeNil()) - Expect(bs.Env.ProjectID).To(HavePrefix("test-project-")) - - // Verify nodes are properly set in the environment - Expect(bs.Env.Jumpbox).NotTo(BeNil(), "Jumpbox should be created") - Expect(bs.Env.PostgreSQLNode).NotTo(BeNil(), "PostgreSQL node should be created") - Expect(bs.Env.CephNodes).To(HaveLen(4), "Should have 4 Ceph nodes") - Expect(bs.Env.ControlPlaneNodes).To(HaveLen(3), "Should have 3 K0s control plane nodes") - - // Verify mock returns expected values - Expect(bs.Env.Jumpbox.GetName()).To(Equal("mocknode")) - Expect(bs.Env.Jumpbox.GetExternalIP()).To(Equal("1.2.3.4")) - Expect(bs.Env.Jumpbox.GetInternalIP()).To(Equal("10.0.0.1")) - - Expect(bs.Env.PostgreSQLNode.GetName()).To(Equal("mocknode")) - Expect(bs.Env.PostgreSQLNode.GetExternalIP()).To(Equal("1.2.3.4")) - Expect(bs.Env.PostgreSQLNode.GetInternalIP()).To(Equal("10.0.0.1")) - - for _, cephNode := range bs.Env.CephNodes { - Expect(cephNode.GetName()).To(Equal("mocknode")) - Expect(cephNode.GetExternalIP()).To(Equal("1.2.3.4")) - Expect(cephNode.GetInternalIP()).To(Equal("10.0.0.1")) - } - - for _, cpNode := range bs.Env.ControlPlaneNodes { - Expect(cpNode.GetName()).To(Equal("mocknode")) - Expect(cpNode.GetExternalIP()).To(Equal("1.2.3.4")) - Expect(cpNode.GetInternalIP()).To(Equal("10.0.0.1")) + Jumpbox: fakeNode("jumpbox", nodeClient), + PostgreSQLNode: fakeNode("postgres", nodeClient), + ControlPlaneNodes: []*node.Node{fakeNode("k0s-1", nodeClient), fakeNode("k0s-2", nodeClient), fakeNode("k0s-3", nodeClient)}, + CephNodes: []*node.Node{fakeNode("ceph-1", nodeClient), fakeNode("ceph-2", nodeClient), fakeNode("ceph-3", nodeClient), fakeNode("ceph-4", nodeClient)}, } }) -}) - -var _ = Describe("EnsureInstallConfig", func() { - Describe("Valid EnsureInstallConfig", func() { - It("uses existing when config file exists", func() { - env := env.NewEnv() - Expect(env).NotTo(BeNil()) - - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - InstallConfigPath: "existing-config-file", - } + Describe("NewGCPBootstrapper", func() { + It("creates a valid GCPBootstrapper", func() { + csEnv = &gcp.CodesphereEnvironment{} stlog := bootstrap.NewStepLogger(false) icg := installer.NewMockInstallConfigManager(GinkgoT()) gc := gcp.NewMockGCPClientManager(GinkgoT()) fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) - - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) - - fw.EXPECT().Exists("existing-config-file").Return(true) - icg.EXPECT().LoadInstallConfigFromFile("existing-config-file").Return(nil) - icg.EXPECT().GetInstallConfig().Return(&files.RootConfig{}) - err = bs.EnsureInstallConfig() + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) Expect(err).NotTo(HaveOccurred()) + Expect(bs).NotTo(BeNil()) }) + }) - It("creates install config when missing", func() { - env := env.NewEnv() - Expect(env).NotTo(BeNil()) + Describe("Bootstrap", func() { + var ( + icg *installer.MockInstallConfigManager + gc *gcp.MockGCPClientManager + fw *util.MockFileIO + bs *gcp.GCPBootstrapper + ) - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - InstallConfigPath: "nonexistent-config-file", - } + BeforeEach(func() { stlog := bootstrap.NewStepLogger(false) - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) - - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) - - fw.EXPECT().Exists("nonexistent-config-file").Return(false) - icg.EXPECT().ApplyProfile("dev").Return(nil) - icg.EXPECT().GetInstallConfig().Return(&files.RootConfig{}) - - err = bs.EnsureInstallConfig() - Expect(err).NotTo(HaveOccurred()) - Expect(bs.Env.InstallConfigPath).To(Equal("nonexistent-config-file")) - Expect(bs.Env.InstallConfig).NotTo(BeNil()) - }) - }) + icg = installer.NewMockInstallConfigManager(GinkgoT()) + gc = gcp.NewMockGCPClientManager(GinkgoT()) + fw = util.NewMockFileIO(GinkgoT()) - Describe("Invalid cases", func() { - It("returns error when config file exists but fails to load", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - InstallConfigPath: "existing-bad-config", + csEnv = &gcp.CodesphereEnvironment{ + InstallConfigPath: "fake-config-file", + SecretsFilePath: "fake-secret", + SecretsDir: "/etc/codesphere/secrets", + ProjectName: "test-project", + BillingAccount: "test-billing-account", + Region: "us-central1", + Zone: "us-central1-a", + BaseDomain: "example.com", + DNSProjectID: "dns-project", + DNSZoneName: "test-zone", + InstallConfig: &files.RootConfig{ + Registry: &files.RegistryConfig{}, + }, } - stlog := bootstrap.NewStepLogger(false) - - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) - - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) + var err error + bs, err = gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) Expect(err).NotTo(HaveOccurred()) - - fw.EXPECT().Exists("existing-bad-config").Return(true) - icg.EXPECT().LoadInstallConfigFromFile("existing-bad-config").Return(fmt.Errorf("bad format")) - - err = bs.EnsureInstallConfig() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to load config file")) - Expect(err.Error()).To(ContainSubstring("bad format")) }) - It("returns error when config file missing and applying profile fails", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - InstallConfigPath: "missing-config", - } - stlog := bootstrap.NewStepLogger(false) + It("runs bootstrap successfully", func() { + bs.Env.RegistryType = gcp.RegistryTypeArtifactRegistry + bs.Env.WriteConfig = true - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) + // 1. EnsureInstallConfig + fw.EXPECT().Exists("fake-config-file").Return(false) + icg.EXPECT().ApplyProfile("dev").Return(nil) + // Returning a real install config to avoid nil pointer dereferences later + icg.EXPECT().GetInstallConfig().RunAndReturn(func() *files.RootConfig { + realIcm := installer.NewInstallConfigManager() + _ = realIcm.ApplyProfile("dev") + return realIcm.GetInstallConfig() + }) + + // 2. EnsureSecrets + fw.EXPECT().Exists("fake-secret").Return(false) + icg.EXPECT().GetVault().Return(&files.InstallVault{}) - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) + // 3. EnsureProject + gc.EXPECT().GetProjectByName(mock.Anything, "test-project").Return(nil, fmt.Errorf("project not found: test-project")) + gc.EXPECT().CreateProjectID("test-project").Return("test-project-id") + gc.EXPECT().CreateProject(mock.Anything, mock.Anything, "test-project").Return(mock.Anything, nil) - fw.EXPECT().Exists("missing-config").Return(false) - icg.EXPECT().ApplyProfile("dev").Return(fmt.Errorf("profile error")) + // 4. EnsureBilling + gc.EXPECT().GetBillingInfo("test-project-id").Return(&cloudbilling.ProjectBillingInfo{BillingEnabled: false}, nil) + gc.EXPECT().EnableBilling("test-project-id", "test-billing-account").Return(nil) - err = bs.EnsureInstallConfig() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to apply profile")) - Expect(err.Error()).To(ContainSubstring("profile error")) - }) - }) -}) + // 5. EnsureAPIsEnabled + gc.EXPECT().EnableAPIs("test-project-id", mock.Anything).Return(nil) -var _ = Describe("EnsureSecrets", func() { - Describe("Valid EnsureSecrets", func() { - It("loads existing secrets file", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - SecretsFilePath: "existing-secrets", - } - stlog := bootstrap.NewStepLogger(false) + // 6. EnsureArtifactRegistry + gc.EXPECT().GetArtifactRegistry("test-project-id", "us-central1", "codesphere-registry").Return(nil, fmt.Errorf("not found")) + gc.EXPECT().CreateArtifactRegistry("test-project-id", "us-central1", "codesphere-registry").Return(&artifactregistrypb.Repository{Name: "codesphere-registry"}, nil) - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) + // 7. EnsureServiceAccounts + gc.EXPECT().CreateServiceAccount("test-project-id", "cloud-controller", "cloud-controller").Return("cloud-controller@p.iam.gserviceaccount.com", false, nil) + gc.EXPECT().CreateServiceAccount("test-project-id", "artifact-registry-writer", "artifact-registry-writer").Return("writer@p.iam.gserviceaccount.com", true, nil) + gc.EXPECT().CreateServiceAccountKey("test-project-id", "writer@p.iam.gserviceaccount.com").Return("fake-key", nil) - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) + // 8. EnsureIAMRoles + gc.EXPECT().AssignIAMRole("test-project-id", "artifact-registry-writer", "roles/artifactregistry.writer").Return(nil) + gc.EXPECT().AssignIAMRole("test-project-id", "cloud-controller", "roles/compute.admin").Return(nil) - fw.EXPECT().Exists("existing-secrets").Return(true) - icg.EXPECT().LoadVaultFromFile("existing-secrets").Return(nil) - icg.EXPECT().MergeVaultIntoConfig().Return(nil) - icg.EXPECT().GetVault().Return(&files.InstallVault{}) + // 9. EnsureVPC + gc.EXPECT().CreateVPC("test-project-id", "us-central1", "test-project-id-vpc", "test-project-id-us-central1-subnet", "test-project-id-router", "test-project-id-nat-gateway").Return(nil) - err = bs.EnsureSecrets() - Expect(err).NotTo(HaveOccurred()) - }) + // 10. EnsureFirewallRules (5 times) + gc.EXPECT().CreateFirewallRule("test-project-id", mock.Anything).Return(nil).Times(5) - It("skips when secrets file missing", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - SecretsFilePath: "missing-secrets", + // 11. EnsureComputeInstances + gc.EXPECT().CreateInstance("test-project-id", "us-central1-a", mock.Anything).Return(nil).Times(9) + // GetInstance calls to retrieve IPs + ipResp := &computepb.Instance{ + NetworkInterfaces: []*computepb.NetworkInterface{ + { + NetworkIP: protoString("10.0.0.1"), + AccessConfigs: []*computepb.AccessConfig{ + {NatIP: protoString("1.2.3.4")}, + }, + }, + }, } - stlog := bootstrap.NewStepLogger(false) - - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) + gc.EXPECT().GetInstance("test-project-id", "us-central1-a", mock.Anything).Return(ipResp, nil).Times(9) + fw.EXPECT().ReadFile(mock.Anything).Return([]byte("fake-key"), nil).Times(9) - fw.EXPECT().Exists("missing-secrets").Return(false) - icg.EXPECT().GetVault().Return(&files.InstallVault{}) + // 12. EnsureGatewayIPAddresses + gc.EXPECT().GetAddress("test-project-id", "us-central1", "gateway").Return(nil, fmt.Errorf("not found")) + gc.EXPECT().CreateAddress("test-project-id", "us-central1", mock.MatchedBy(func(addr *computepb.Address) bool { return *addr.Name == "gateway" })).Return("1.1.1.1", nil) + gc.EXPECT().GetAddress("test-project-id", "us-central1", "gateway").Return(nil, fmt.Errorf("not found")) + gc.EXPECT().GetAddress("test-project-id", "us-central1", "public-gateway").Return(nil, fmt.Errorf("not found")) + gc.EXPECT().CreateAddress("test-project-id", "us-central1", mock.MatchedBy(func(addr *computepb.Address) bool { return *addr.Name == "public-gateway" })).Return("2.2.2.2", nil) + gc.EXPECT().GetAddress("test-project-id", "us-central1", "public-gateway").Return(&computepb.Address{Address: protoString("2.2.2.2")}, nil) - err = bs.EnsureSecrets() - Expect(err).NotTo(HaveOccurred()) - }) - }) + // 16. UpdateInstallConfig + icg.EXPECT().GenerateSecrets().Return(nil) + 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) + + // Enable Root Login + nodeClient.EXPECT().WaitReady(mock.Anything, mock.Anything).Return(nil).Return(nil) + nodeClient.EXPECT().RunCommand(mock.Anything, mock.Anything, mock.Anything).Return(nil) + + // 17. EnsureAgeKey + nodeClient.EXPECT().RunCommand(mock.MatchedBy(jumpbboxMatcher), "root", "mkdir -p /etc/codesphere/secrets; age-keygen -o /etc/codesphere/secrets/age_key.txt").Return(nil) + nodeClient.EXPECT().HasFile(mock.MatchedBy(jumpbboxMatcher), "/etc/codesphere/secrets/age_key.txt").Return(false) + + // 18. EncryptVault + nodeClient.EXPECT().RunCommand(mock.MatchedBy(jumpbboxMatcher), "root", "cp /etc/codesphere/secrets/prod.vault.yaml{,.bak}").Return(nil) + nodeClient.EXPECT().RunCommand(mock.MatchedBy(jumpbboxMatcher), "root", mock.MatchedBy(func(cmd string) bool { + return strings.Contains(cmd, "sops --encrypt") + })).Return(nil) - Describe("Invalid cases", func() { - It("returns error when secrets file load fails", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - SecretsFilePath: "bad-secrets", - } - stlog := bootstrap.NewStepLogger(false) + // 19. EnsureDNSRecords + gc.EXPECT().EnsureDNSManagedZone("dns-project", "test-zone", "example.com.", mock.Anything).Return(nil) + gc.EXPECT().EnsureDNSRecordSets("dns-project", "test-zone", mock.MatchedBy(func(records []*dns.ResourceRecordSet) bool { + return len(records) == 4 + })).Return(nil) - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) + // 20. GenerateK0sConfigScript + fw.EXPECT().WriteFile("configure-k0s.sh", mock.Anything, os.FileMode(0755)).Return(nil) + nodeClient.EXPECT().CopyFile(mock.Anything, "configure-k0s.sh", "/root/configure-k0s.sh").Return(nil) + nodeClient.EXPECT().RunCommand(mock.Anything, "root", "chmod +x /root/configure-k0s.sh").Return(nil) - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) + err := bs.Bootstrap() Expect(err).NotTo(HaveOccurred()) + Expect(bs.Env).NotTo(BeNil()) + Expect(bs.Env.ProjectID).To(HavePrefix("test-project-")) - fw.EXPECT().Exists("bad-secrets").Return(true) - icg.EXPECT().LoadVaultFromFile("bad-secrets").Return(fmt.Errorf("load error")) - - err = bs.EnsureSecrets() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to load vault file")) - Expect(err.Error()).To(ContainSubstring("load error")) - }) - - It("returns error when merge fails", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - SecretsFilePath: "merr-secrets", - } - stlog := bootstrap.NewStepLogger(false) + // Verify nodes are properly set in the environment + Expect(bs.Env.Jumpbox).NotTo(BeNil(), "Jumpbox should be created") + Expect(bs.Env.PostgreSQLNode).NotTo(BeNil(), "PostgreSQL node should be created") + Expect(bs.Env.CephNodes).To(HaveLen(4), "Should have 4 Ceph nodes") + Expect(bs.Env.ControlPlaneNodes).To(HaveLen(3), "Should have 3 K0s control plane nodes") - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) + // Verify mock returns expected values + Expect(bs.Env.Jumpbox.GetName()).To(Equal("jumpbox")) + Expect(bs.Env.Jumpbox.GetExternalIP()).To(Equal("1.2.3.4")) + Expect(bs.Env.Jumpbox.GetInternalIP()).To(Equal("10.0.0.1")) - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) + Expect(bs.Env.PostgreSQLNode.GetName()).To(Equal("postgres")) + Expect(bs.Env.PostgreSQLNode.GetExternalIP()).To(Equal("1.2.3.4")) + Expect(bs.Env.PostgreSQLNode.GetInternalIP()).To(Equal("10.0.0.1")) - fw.EXPECT().Exists("merr-secrets").Return(true) - icg.EXPECT().LoadVaultFromFile("merr-secrets").Return(nil) - icg.EXPECT().MergeVaultIntoConfig().Return(fmt.Errorf("merge error")) + for _, cephNode := range bs.Env.CephNodes { + Expect(cephNode.GetName()).To(MatchRegexp("ceph-\\d+")) + Expect(cephNode.GetExternalIP()).To(Equal("1.2.3.4")) + Expect(cephNode.GetInternalIP()).To(Equal("10.0.0.1")) + } - err = bs.EnsureSecrets() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to merge vault into config")) - Expect(err.Error()).To(ContainSubstring("merge error")) + for _, cpNode := range bs.Env.ControlPlaneNodes { + Expect(cpNode.GetName()).To(MatchRegexp("k0s-\\d+")) + Expect(cpNode.GetExternalIP()).To(Equal("1.2.3.4")) + Expect(cpNode.GetInternalIP()).To(Equal("10.0.0.1")) + } }) }) -}) - -var _ = Describe("EnsureProject", func() { - Describe("Valid EnsureProject", func() { - It("uses existing project", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - ProjectName: "existing-proj", - FolderID: "123", - } - stlog := bootstrap.NewStepLogger(false) - - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) - - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) - - gc.EXPECT().GetProjectByName("123", "existing-proj").Return(&resourcemanagerpb.Project{ProjectId: "existing-id", Name: "existing-proj"}, nil) - err = bs.EnsureProject() - Expect(err).NotTo(HaveOccurred()) - Expect(bs.Env.ProjectID).To(Equal("existing-id")) + Describe("EnsureInstallConfig", func() { + Describe("Valid EnsureInstallConfig", func() { + It("uses existing when config file exists", func() { + csEnv = &gcp.CodesphereEnvironment{ + InstallConfigPath: "existing-config-file", + } + stlog := bootstrap.NewStepLogger(false) + + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) + + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) + + fw.EXPECT().Exists("existing-config-file").Return(true) + icg.EXPECT().LoadInstallConfigFromFile("existing-config-file").Return(nil) + icg.EXPECT().GetInstallConfig().Return(&files.RootConfig{}) + + err = bs.EnsureInstallConfig() + Expect(err).NotTo(HaveOccurred()) + }) + + It("creates install config when missing", func() { + csEnv = &gcp.CodesphereEnvironment{ + InstallConfigPath: "nonexistent-config-file", + } + stlog := bootstrap.NewStepLogger(false) + + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) + + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) + + fw.EXPECT().Exists("nonexistent-config-file").Return(false) + icg.EXPECT().ApplyProfile("dev").Return(nil) + icg.EXPECT().GetInstallConfig().Return(&files.RootConfig{}) + + err = bs.EnsureInstallConfig() + Expect(err).NotTo(HaveOccurred()) + Expect(bs.Env.InstallConfigPath).To(Equal("nonexistent-config-file")) + Expect(bs.Env.InstallConfig).NotTo(BeNil()) + }) + }) + + Describe("Invalid cases", func() { + It("returns error when config file exists but fails to load", func() { + csEnv = &gcp.CodesphereEnvironment{ + InstallConfigPath: "existing-bad-config", + } + stlog := bootstrap.NewStepLogger(false) + + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) + + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) + + fw.EXPECT().Exists("existing-bad-config").Return(true) + icg.EXPECT().LoadInstallConfigFromFile("existing-bad-config").Return(fmt.Errorf("bad format")) + + err = bs.EnsureInstallConfig() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to load config file")) + Expect(err.Error()).To(ContainSubstring("bad format")) + }) + + It("returns error when config file missing and applying profile fails", func() { + csEnv = &gcp.CodesphereEnvironment{ + InstallConfigPath: "missing-config", + } + stlog := bootstrap.NewStepLogger(false) + + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) + + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) + + fw.EXPECT().Exists("missing-config").Return(false) + icg.EXPECT().ApplyProfile("dev").Return(fmt.Errorf("profile error")) + + err = bs.EnsureInstallConfig() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to apply profile")) + Expect(err.Error()).To(ContainSubstring("profile error")) + }) }) + }) - It("creates project when missing", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - ProjectName: "new-proj", - } - stlog := bootstrap.NewStepLogger(false) - - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) - - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) - - gc.EXPECT().GetProjectByName("", "new-proj").Return(nil, fmt.Errorf("project not found: new-proj")) - gc.EXPECT().CreateProjectID("new-proj").Return("new-proj-id") - gc.EXPECT().CreateProject("", "new-proj-id", "new-proj").Return("", nil) - - err = bs.EnsureProject() - Expect(err).NotTo(HaveOccurred()) - Expect(bs.Env.ProjectID).To(Equal("new-proj-id")) + Describe("EnsureSecrets", func() { + Describe("Valid EnsureSecrets", func() { + It("loads existing secrets file", func() { + csEnv = &gcp.CodesphereEnvironment{ + SecretsFilePath: "existing-secrets", + } + stlog := bootstrap.NewStepLogger(false) + + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) + + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) + + fw.EXPECT().Exists("existing-secrets").Return(true) + icg.EXPECT().LoadVaultFromFile("existing-secrets").Return(nil) + icg.EXPECT().MergeVaultIntoConfig().Return(nil) + icg.EXPECT().GetVault().Return(&files.InstallVault{}) + + err = bs.EnsureSecrets() + Expect(err).NotTo(HaveOccurred()) + }) + + It("skips when secrets file missing", func() { + csEnv = &gcp.CodesphereEnvironment{ + SecretsFilePath: "missing-secrets", + } + stlog := bootstrap.NewStepLogger(false) + + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) + + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) + + fw.EXPECT().Exists("missing-secrets").Return(false) + icg.EXPECT().GetVault().Return(&files.InstallVault{}) + + err = bs.EnsureSecrets() + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Describe("Invalid cases", func() { + It("returns error when secrets file load fails", func() { + csEnv = &gcp.CodesphereEnvironment{ + SecretsFilePath: "bad-secrets", + } + stlog := bootstrap.NewStepLogger(false) + + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) + + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) + + fw.EXPECT().Exists("bad-secrets").Return(true) + icg.EXPECT().LoadVaultFromFile("bad-secrets").Return(fmt.Errorf("load error")) + + err = bs.EnsureSecrets() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to load vault file")) + Expect(err.Error()).To(ContainSubstring("load error")) + }) + + It("returns error when merge fails", func() { + csEnv = &gcp.CodesphereEnvironment{ + SecretsFilePath: "merr-secrets", + } + stlog := bootstrap.NewStepLogger(false) + + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) + + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) + + fw.EXPECT().Exists("merr-secrets").Return(true) + icg.EXPECT().LoadVaultFromFile("merr-secrets").Return(nil) + icg.EXPECT().MergeVaultIntoConfig().Return(fmt.Errorf("merge error")) + + err = bs.EnsureSecrets() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to merge vault into config")) + Expect(err.Error()).To(ContainSubstring("merge error")) + }) }) }) - Describe("Invalid cases", func() { - It("returns error when GetProjectByName fails unexpectedly", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - ProjectName: "error-proj", - } - stlog := bootstrap.NewStepLogger(false) - - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) - - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) - - gc.EXPECT().GetProjectByName("", "error-proj").Return(nil, fmt.Errorf("api error")) - - err = bs.EnsureProject() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to get project")) - Expect(err.Error()).To(ContainSubstring("api error")) + Describe("EnsureProject", func() { + Describe("Valid EnsureProject", func() { + It("uses existing project", func() { + csEnv = &gcp.CodesphereEnvironment{ + ProjectName: "existing-proj", + FolderID: "123", + } + stlog := bootstrap.NewStepLogger(false) + + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) + + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) + + gc.EXPECT().GetProjectByName("123", "existing-proj").Return(&resourcemanagerpb.Project{ProjectId: "existing-id", Name: "existing-proj"}, nil) + + err = bs.EnsureProject() + Expect(err).NotTo(HaveOccurred()) + Expect(bs.Env.ProjectID).To(Equal("existing-id")) + }) + + It("creates project when missing", func() { + csEnv = &gcp.CodesphereEnvironment{ + ProjectName: "new-proj", + } + stlog := bootstrap.NewStepLogger(false) + + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) + + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) + + gc.EXPECT().GetProjectByName("", "new-proj").Return(nil, fmt.Errorf("project not found: new-proj")) + gc.EXPECT().CreateProjectID("new-proj").Return("new-proj-id") + gc.EXPECT().CreateProject("", "new-proj-id", "new-proj").Return("", nil) + + err = bs.EnsureProject() + Expect(err).NotTo(HaveOccurred()) + Expect(bs.Env.ProjectID).To(Equal("new-proj-id")) + }) + }) + + Describe("Invalid cases", func() { + It("returns error when GetProjectByName fails unexpectedly", func() { + csEnv = &gcp.CodesphereEnvironment{ + ProjectName: "error-proj", + } + stlog := bootstrap.NewStepLogger(false) + + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) + + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) + + gc.EXPECT().GetProjectByName("", "error-proj").Return(nil, fmt.Errorf("api error")) + + err = bs.EnsureProject() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to get project")) + Expect(err.Error()).To(ContainSubstring("api error")) + }) + + It("returns error when CreateProject fails", func() { + csEnv = &gcp.CodesphereEnvironment{ + ProjectName: "fail-create-proj", + } + stlog := bootstrap.NewStepLogger(false) + + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) + + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) + + gc.EXPECT().GetProjectByName("", "fail-create-proj").Return(nil, fmt.Errorf("project not found: fail-create-proj")) + gc.EXPECT().CreateProjectID("fail-create-proj").Return("fail-create-proj-id") + gc.EXPECT().CreateProject("", "fail-create-proj-id", "fail-create-proj").Return("", fmt.Errorf("create error")) + + err = bs.EnsureProject() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to create project")) + Expect(err.Error()).To(ContainSubstring("create error")) + }) }) + }) - It("returns error when CreateProject fails", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - ProjectName: "fail-create-proj", - } - stlog := bootstrap.NewStepLogger(false) - - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) - - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) - - gc.EXPECT().GetProjectByName("", "fail-create-proj").Return(nil, fmt.Errorf("project not found: fail-create-proj")) - gc.EXPECT().CreateProjectID("fail-create-proj").Return("fail-create-proj-id") - gc.EXPECT().CreateProject("", "fail-create-proj-id", "fail-create-proj").Return("", fmt.Errorf("create error")) - - err = bs.EnsureProject() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to create project")) - Expect(err.Error()).To(ContainSubstring("create error")) + Describe("EnsureBilling", func() { + Describe("Valid EnsureBilling", func() { + It("does nothing if billing already enabled correctly", func() { + csEnv = &gcp.CodesphereEnvironment{ + ProjectID: "pid", + BillingAccount: "billing-123", + } + stlog := bootstrap.NewStepLogger(false) + + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) + + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) + + bi := &cloudbilling.ProjectBillingInfo{ + BillingEnabled: true, + BillingAccountName: "billing-123", + } + gc.EXPECT().GetBillingInfo("pid").Return(bi, nil) + + err = bs.EnsureBilling() + Expect(err).NotTo(HaveOccurred()) + }) + + It("enables billing if not enabled", func() { + csEnv = &gcp.CodesphereEnvironment{ + ProjectID: "pid", + BillingAccount: "billing-123", + } + stlog := bootstrap.NewStepLogger(false) + + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) + + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) + + bi := &cloudbilling.ProjectBillingInfo{ + BillingEnabled: false, + } + gc.EXPECT().GetBillingInfo("pid").Return(bi, nil) + gc.EXPECT().EnableBilling("pid", "billing-123").Return(nil) + + err = bs.EnsureBilling() + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Describe("Invalid cases", func() { + It("fails when GetBillingInfo fails", func() { + csEnv = &gcp.CodesphereEnvironment{ + ProjectID: "pid", + } + stlog := bootstrap.NewStepLogger(false) + + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) + + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) + + gc.EXPECT().GetBillingInfo("pid").Return(nil, fmt.Errorf("billing info error")) + + err = bs.EnsureBilling() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to get billing info")) + Expect(err.Error()).To(ContainSubstring("billing info error")) + }) + + It("fails when EnableBilling fails", func() { + csEnv = &gcp.CodesphereEnvironment{ + ProjectID: "pid", + BillingAccount: "acc", + } + stlog := bootstrap.NewStepLogger(false) + + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) + + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) + + bi := &cloudbilling.ProjectBillingInfo{ + BillingEnabled: false, + } + gc.EXPECT().GetBillingInfo("pid").Return(bi, nil) + gc.EXPECT().EnableBilling("pid", "acc").Return(fmt.Errorf("enable error")) + + err = bs.EnsureBilling() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to enable billing")) + Expect(err.Error()).To(ContainSubstring("enable error")) + }) }) }) -}) -var _ = Describe("EnsureBilling", func() { - Describe("Valid EnsureBilling", func() { - It("does nothing if billing already enabled correctly", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - ProjectID: "pid", - BillingAccount: "billing-123", - } - stlog := bootstrap.NewStepLogger(false) + Describe("EnsureAPIsEnabled", func() { + Describe("Valid EnsureAPIsEnabled", func() { + It("enables default APIs", func() { + csEnv = &gcp.CodesphereEnvironment{ + ProjectID: "pid", + } + stlog := bootstrap.NewStepLogger(false) - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) - bi := &cloudbilling.ProjectBillingInfo{ - BillingEnabled: true, - BillingAccountName: "billing-123", - } - gc.EXPECT().GetBillingInfo("pid").Return(bi, nil) + gc.EXPECT().EnableAPIs("pid", []string{ + "compute.googleapis.com", + "serviceusage.googleapis.com", + "artifactregistry.googleapis.com", + "dns.googleapis.com", + }).Return(nil) - err = bs.EnsureBilling() - Expect(err).NotTo(HaveOccurred()) + err = bs.EnsureAPIsEnabled() + Expect(err).NotTo(HaveOccurred()) + }) }) - It("enables billing if not enabled", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - ProjectID: "pid", - BillingAccount: "billing-123", - } - stlog := bootstrap.NewStepLogger(false) + Describe("Invalid cases", func() { + It("fails when EnableAPIs fails", func() { + csEnv = &gcp.CodesphereEnvironment{ + ProjectID: "pid", + } + stlog := bootstrap.NewStepLogger(false) - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) - bi := &cloudbilling.ProjectBillingInfo{ - BillingEnabled: false, - } - gc.EXPECT().GetBillingInfo("pid").Return(bi, nil) - gc.EXPECT().EnableBilling("pid", "billing-123").Return(nil) + gc.EXPECT().EnableAPIs("pid", mock.Anything).Return(fmt.Errorf("api error")) - err = bs.EnsureBilling() - Expect(err).NotTo(HaveOccurred()) + err = bs.EnsureAPIsEnabled() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to enable APIs")) + Expect(err.Error()).To(ContainSubstring("api error")) + }) }) }) - Describe("Invalid cases", func() { - It("fails when GetBillingInfo fails", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - ProjectID: "pid", - } - stlog := bootstrap.NewStepLogger(false) + Describe("EnsureArtifactRegistry", func() { + Describe("Valid EnsureArtifactRegistry", func() { + It("uses existing registry if present", func() { + csEnv = &gcp.CodesphereEnvironment{ + ProjectID: "pid", + Region: "us-central1", + InstallConfig: &files.RootConfig{ + Registry: &files.RegistryConfig{}, + }, + } + stlog := bootstrap.NewStepLogger(false) - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) - gc.EXPECT().GetBillingInfo("pid").Return(nil, fmt.Errorf("billing info error")) + repo := &artifactregistrypb.Repository{Name: "projects/pid/locations/us-central1/repositories/codesphere-registry"} + gc.EXPECT().GetArtifactRegistry("pid", "us-central1", "codesphere-registry").Return(repo, nil) - err = bs.EnsureBilling() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to get billing info")) - Expect(err.Error()).To(ContainSubstring("billing info error")) - }) + err = bs.EnsureArtifactRegistry() + Expect(err).NotTo(HaveOccurred()) + }) - It("fails when EnableBilling fails", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - ProjectID: "pid", - BillingAccount: "acc", - } - stlog := bootstrap.NewStepLogger(false) + It("creates registry if missing", func() { + csEnv = &gcp.CodesphereEnvironment{ + ProjectID: "pid", + Region: "us-central1", + InstallConfig: &files.RootConfig{ + Registry: &files.RegistryConfig{}, + }, + } + stlog := bootstrap.NewStepLogger(false) - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) - bi := &cloudbilling.ProjectBillingInfo{ - BillingEnabled: false, - } - gc.EXPECT().GetBillingInfo("pid").Return(bi, nil) - gc.EXPECT().EnableBilling("pid", "acc").Return(fmt.Errorf("enable error")) + gc.EXPECT().GetArtifactRegistry("pid", "us-central1", "codesphere-registry").Return(nil, fmt.Errorf("not found")) + + createdRepo := &artifactregistrypb.Repository{Name: "projects/pid/locations/us-central1/repositories/codesphere-registry"} + gc.EXPECT().CreateArtifactRegistry("pid", "us-central1", "codesphere-registry").Return(createdRepo, nil) - err = bs.EnsureBilling() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to enable billing")) - Expect(err.Error()).To(ContainSubstring("enable error")) + err = bs.EnsureArtifactRegistry() + Expect(err).NotTo(HaveOccurred()) + }) }) - }) -}) -var _ = Describe("EnsureAPIsEnabled", func() { - Describe("Valid EnsureAPIsEnabled", func() { - It("enables default APIs", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - ProjectID: "pid", - } - stlog := bootstrap.NewStepLogger(false) + Describe("Invalid cases", func() { + It("fails when CreateArtifactRegistry fails", func() { + csEnv = &gcp.CodesphereEnvironment{ + ProjectID: "pid", + Region: "us-central1", + InstallConfig: &files.RootConfig{ + Registry: &files.RegistryConfig{}, + }, + } + stlog := bootstrap.NewStepLogger(false) - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) - gc.EXPECT().EnableAPIs("pid", []string{ - "compute.googleapis.com", - "serviceusage.googleapis.com", - "artifactregistry.googleapis.com", - "dns.googleapis.com", - }).Return(nil) + gc.EXPECT().GetArtifactRegistry("pid", "us-central1", "codesphere-registry").Return(nil, fmt.Errorf("not found")) + gc.EXPECT().CreateArtifactRegistry("pid", "us-central1", "codesphere-registry").Return(nil, fmt.Errorf("create error")) - err = bs.EnsureAPIsEnabled() - Expect(err).NotTo(HaveOccurred()) + err = bs.EnsureArtifactRegistry() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to create artifact registry")) + Expect(err.Error()).To(ContainSubstring("create error")) + }) }) }) - Describe("Invalid cases", func() { - It("fails when EnableAPIs fails", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - ProjectID: "pid", - } - stlog := bootstrap.NewStepLogger(false) - - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) - - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) - - gc.EXPECT().EnableAPIs("pid", mock.Anything).Return(fmt.Errorf("api error")) - - err = bs.EnsureAPIsEnabled() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to enable APIs")) - Expect(err.Error()).To(ContainSubstring("api error")) + Describe("EnsureLocalContainerRegistry", func() { + Describe("Valid EnsureLocalContainerRegistry", func() { + It("installs local registry", func() { + csEnv = &gcp.CodesphereEnvironment{ + ProjectID: "pid", + InstallConfig: &files.RootConfig{ + Registry: &files.RegistryConfig{}, + }, + } + stlog := bootstrap.NewStepLogger(false) + + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) + + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) + + // Setup mocked node + bs.Env.PostgreSQLNode = fakeNode("postgres", nodeClient) + + // Check if running - return error to simulate not running + nodeClient.EXPECT().RunCommand(bs.Env.PostgreSQLNode, "root", mock.MatchedBy(func(cmd string) bool { + return strings.Contains(cmd, "podman ps") + })).Return(fmt.Errorf("not running")) + + // Install commands (8 commands) + scp/update-ca/docker commands (3 per 4 nodes = 12) + nodeClient.EXPECT().RunCommand(mock.Anything, "root", mock.Anything).Return(nil).Times(8 + 12) + + bs.Env.ControlPlaneNodes = []*node.Node{fakeNode("k0s-1", nodeClient), fakeNode("k0s-2", nodeClient)} + bs.Env.CephNodes = []*node.Node{fakeNode("ceph-1", nodeClient), fakeNode("ceph-2", nodeClient)} + + err = bs.EnsureLocalContainerRegistry() + Expect(err).NotTo(HaveOccurred()) + Expect(bs.Env.InstallConfig.Registry.Username).To(Equal("custom-registry")) + }) + }) + + Describe("Invalid cases", func() { + var ( + ctx context.Context + icg *installer.MockInstallConfigManager + gc *gcp.MockGCPClientManager + fw *util.MockFileIO + bs *gcp.GCPBootstrapper + ) + + BeforeEach(func() { + ctx = context.Background() + csEnv = &gcp.CodesphereEnvironment{ + ProjectID: "pid", + InstallConfig: &files.RootConfig{ + Registry: &files.RegistryConfig{}, + }, + PostgreSQLNode: fakeNode("postgres", nodeClient), + ControlPlaneNodes: []*node.Node{fakeNode("k0s-1", nodeClient), fakeNode("k0s-2", nodeClient)}, + CephNodes: []*node.Node{fakeNode("ceph-1", nodeClient), fakeNode("ceph-2", nodeClient)}, + } + stlog := bootstrap.NewStepLogger(false) + + icg = installer.NewMockInstallConfigManager(GinkgoT()) + gc = gcp.NewMockGCPClientManager(GinkgoT()) + fw = util.NewMockFileIO(GinkgoT()) + + var err error + bs, err = gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) + }) + + It("fails when the 8th install command fails", func() { + // First check - registry not running + nodeClient.EXPECT().RunCommand(csEnv.PostgreSQLNode, "root", mock.MatchedBy(func(cmd string) bool { + return strings.Contains(cmd, "podman ps") + })).Return(fmt.Errorf("not running")) + + // First 7 install commands succeed + nodeClient.EXPECT().RunCommand(csEnv.PostgreSQLNode, "root", mock.Anything).Return(nil).Times(7) + + // 8th install command fails + nodeClient.EXPECT().RunCommand(csEnv.PostgreSQLNode, "root", mock.Anything).Return(fmt.Errorf("ssh error")).Once() + + err := bs.EnsureLocalContainerRegistry() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("ssh error")) + }) + + It("fails when the first scp command fails", func() { + // First check - registry not running + nodeClient.EXPECT().RunCommand(csEnv.PostgreSQLNode, "root", mock.MatchedBy(func(cmd string) bool { + return strings.Contains(cmd, "podman ps") + })).Return(fmt.Errorf("not running")) + + // All 8 install commands succeed + nodeClient.EXPECT().RunCommand(csEnv.PostgreSQLNode, "root", mock.Anything).Return(nil).Times(8) + + // First scp command fails + nodeClient.EXPECT().RunCommand(csEnv.PostgreSQLNode, "root", mock.MatchedBy(func(cmd string) bool { + return strings.HasPrefix(cmd, "scp ") + })).Return(fmt.Errorf("scp error")).Once() + + err := bs.EnsureLocalContainerRegistry() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to copy registry certificate")) + }) + + It("fails when update-ca-certificates fails", func() { + // Override node setup for this test + bs.Env.ControlPlaneNodes = []*node.Node{fakeNode("k0s-1", nodeClient)} + bs.Env.CephNodes = []*node.Node{} + + // First check - registry not running + nodeClient.EXPECT().RunCommand(csEnv.PostgreSQLNode, "root", mock.MatchedBy(func(cmd string) bool { + return strings.Contains(cmd, "podman ps") + })).Return(fmt.Errorf("not running")) + + // All 8 install commands succeed + nodeClient.EXPECT().RunCommand(csEnv.PostgreSQLNode, "root", mock.Anything).Return(nil).Times(8) + // scp succeeds + nodeClient.EXPECT().RunCommand(csEnv.PostgreSQLNode, "root", mock.MatchedBy(func(cmd string) bool { + return strings.HasPrefix(cmd, "scp ") + })).Return(nil).Once() + + // update-ca-certificates fails + nodeClient.EXPECT().RunCommand(bs.Env.ControlPlaneNodes[0], "root", "update-ca-certificates").Return(fmt.Errorf("ca update error")).Once() + + err := bs.EnsureLocalContainerRegistry() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to update CA certificates")) + }) + + It("fails when docker restart fails", func() { + // Override node setup for this test + bs.Env.ControlPlaneNodes = []*node.Node{fakeNode("k0s-1", nodeClient)} + bs.Env.CephNodes = []*node.Node{} + + // First check - registry not running + nodeClient.EXPECT().RunCommand(csEnv.PostgreSQLNode, "root", mock.MatchedBy(func(cmd string) bool { + return strings.Contains(cmd, "podman ps") + })).Return(fmt.Errorf("not running")) + + // All 8 install commands succeed + nodeClient.EXPECT().RunCommand(csEnv.PostgreSQLNode, "root", mock.Anything).Return(nil).Times(8) + + // scp succeeds + nodeClient.EXPECT().RunCommand(csEnv.PostgreSQLNode, "root", mock.MatchedBy(func(cmd string) bool { + return strings.HasPrefix(cmd, "scp ") + })).Return(nil).Once() + + // update-ca-certificates succeeds + nodeClient.EXPECT().RunCommand(bs.Env.ControlPlaneNodes[0], "root", "update-ca-certificates").Return(nil).Once() + + // docker restart fails + nodeClient.EXPECT().RunCommand(bs.Env.ControlPlaneNodes[0], "root", "systemctl restart docker.service || true").Return(fmt.Errorf("docker restart error")).Once() + + err := bs.EnsureLocalContainerRegistry() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to restart docker service")) + }) }) }) -}) -var _ = Describe("EnsureArtifactRegistry", func() { - Describe("Valid EnsureArtifactRegistry", func() { - It("uses existing registry if present", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - ProjectID: "pid", - Region: "us-central1", - InstallConfig: &files.RootConfig{ - Registry: &files.RegistryConfig{}, - }, - } - stlog := bootstrap.NewStepLogger(false) + Describe("EnsureServiceAccounts", func() { + Describe("Valid EnsureServiceAccounts", func() { + It("creates cloud-controller and skips writer if not artifact registry", func() { + csEnv = &gcp.CodesphereEnvironment{ + ProjectID: "pid", + RegistryType: gcp.RegistryTypeLocalContainer, + } + stlog := bootstrap.NewStepLogger(false) + + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) + + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) + + gc.EXPECT().CreateServiceAccount("pid", "cloud-controller", "cloud-controller").Return("email@sa", false, nil) + + err = bs.EnsureServiceAccounts() + Expect(err).NotTo(HaveOccurred()) + }) + + It("creates both accounts for artifact registry", func() { + csEnv = &gcp.CodesphereEnvironment{ + ProjectID: "pid", + RegistryType: gcp.RegistryTypeArtifactRegistry, + InstallConfig: &files.RootConfig{ + Registry: &files.RegistryConfig{}, + }, + } + stlog := bootstrap.NewStepLogger(false) - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) - repo := &artifactregistrypb.Repository{Name: "projects/pid/locations/us-central1/repositories/codesphere-registry"} - gc.EXPECT().GetArtifactRegistry("pid", "us-central1", "codesphere-registry").Return(repo, nil) + gc.EXPECT().CreateServiceAccount("pid", "cloud-controller", "cloud-controller").Return("email@sa", false, nil) + gc.EXPECT().CreateServiceAccount("pid", "artifact-registry-writer", "artifact-registry-writer").Return("writer@sa", true, nil) + gc.EXPECT().CreateServiceAccountKey("pid", "writer@sa").Return("key-content", nil) - err = bs.EnsureArtifactRegistry() - Expect(err).NotTo(HaveOccurred()) + err = bs.EnsureServiceAccounts() + Expect(err).NotTo(HaveOccurred()) + Expect(bs.Env.InstallConfig.Registry.Password).To(Equal("key-content")) + }) }) - It("creates registry if missing", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - ProjectID: "pid", - Region: "us-central1", - InstallConfig: &files.RootConfig{ - Registry: &files.RegistryConfig{}, - }, - } - stlog := bootstrap.NewStepLogger(false) - - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) + Describe("Invalid cases", func() { + It("fails when cloud-controller creation fails", func() { + csEnv = &gcp.CodesphereEnvironment{ + ProjectID: "pid", + } + stlog := bootstrap.NewStepLogger(false) - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) - gc.EXPECT().GetArtifactRegistry("pid", "us-central1", "codesphere-registry").Return(nil, fmt.Errorf("not found")) + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) - createdRepo := &artifactregistrypb.Repository{Name: "projects/pid/locations/us-central1/repositories/codesphere-registry"} - gc.EXPECT().CreateArtifactRegistry("pid", "us-central1", "codesphere-registry").Return(createdRepo, nil) + gc.EXPECT().CreateServiceAccount("pid", "cloud-controller", "cloud-controller").Return("", false, fmt.Errorf("create error")) - err = bs.EnsureArtifactRegistry() - Expect(err).NotTo(HaveOccurred()) + err = bs.EnsureServiceAccounts() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("create error")) + }) }) }) - Describe("Invalid cases", func() { - It("fails when CreateArtifactRegistry fails", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - ProjectID: "pid", - Region: "us-central1", - InstallConfig: &files.RootConfig{ - Registry: &files.RegistryConfig{}, - }, - } - stlog := bootstrap.NewStepLogger(false) - - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) - - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) - - gc.EXPECT().GetArtifactRegistry("pid", "us-central1", "codesphere-registry").Return(nil, fmt.Errorf("not found")) - gc.EXPECT().CreateArtifactRegistry("pid", "us-central1", "codesphere-registry").Return(nil, fmt.Errorf("create error")) - - err = bs.EnsureArtifactRegistry() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to create artifact registry")) - Expect(err.Error()).To(ContainSubstring("create error")) - }) - }) -}) - -var _ = Describe("EnsureLocalContainerRegistry", func() { - Describe("Valid EnsureLocalContainerRegistry", func() { - It("installs local registry", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - ProjectID: "pid", - InstallConfig: &files.RootConfig{ - Registry: &files.RegistryConfig{}, - }, - } - stlog := bootstrap.NewStepLogger(false) - - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) - - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) - - // Setup mocked node - bs.Env.PostgreSQLNode = nm - nm.EXPECT().GetInternalIP().Return("10.0.0.1").Maybe() - - // Check if running - return error to simulate not running - nm.EXPECT().RunSSHCommand("root", mock.MatchedBy(func(cmd string) bool { - return strings.Contains(cmd, "podman ps") - }), true).Return(fmt.Errorf("not running")) - - // Install commands (8 commands) + scp/update-ca/docker commands (3 per 4 nodes = 12) - nm.EXPECT().RunSSHCommand("root", mock.Anything, true).Return(nil).Times(8 + 12) - - bs.Env.ControlPlaneNodes = []node.NodeManager{nm, nm} - bs.Env.CephNodes = []node.NodeManager{nm, nm} - - nm.EXPECT().GetName().Return("mocknode").Maybe() - - err = bs.EnsureLocalContainerRegistry() - Expect(err).NotTo(HaveOccurred()) - Expect(bs.Env.InstallConfig.Registry.Username).To(Equal("custom-registry")) - }) - }) - - Describe("Invalid cases", func() { - var ( - e env.Env - ctx context.Context - csEnv *gcp.CodesphereEnvironment - icg *installer.MockInstallConfigManager - gc *gcp.MockGCPClientManager - fw *util.MockFileIO - nm *node.MockNodeManager - bs *gcp.GCPBootstrapper - ) - - BeforeEach(func() { - e = env.NewEnv() - ctx = context.Background() - csEnv = &gcp.CodesphereEnvironment{ - ProjectID: "pid", - InstallConfig: &files.RootConfig{ - Registry: &files.RegistryConfig{}, - }, - } - stlog := bootstrap.NewStepLogger(false) - - icg = installer.NewMockInstallConfigManager(GinkgoT()) - gc = gcp.NewMockGCPClientManager(GinkgoT()) - fw = util.NewMockFileIO(GinkgoT()) - nm = node.NewMockNodeManager(GinkgoT()) - - var err error - bs, err = gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) - - bs.Env.PostgreSQLNode = nm - bs.Env.ControlPlaneNodes = []node.NodeManager{nm, nm} - bs.Env.CephNodes = []node.NodeManager{nm, nm} - - nm.EXPECT().GetInternalIP().Return("10.0.0.1").Maybe() - }) - - It("fails when the 8th install command fails", func() { - // First check - registry not running - nm.EXPECT().RunSSHCommand("root", mock.MatchedBy(func(cmd string) bool { - return strings.Contains(cmd, "podman ps") - }), true).Return(fmt.Errorf("not running")) - - // First 7 install commands succeed - nm.EXPECT().RunSSHCommand("root", mock.Anything, true).Return(nil).Times(7) - - // 8th install command fails - nm.EXPECT().RunSSHCommand("root", mock.Anything, true).Return(fmt.Errorf("ssh error")).Once() - - err := bs.EnsureLocalContainerRegistry() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("ssh error")) - }) - - It("fails when the first scp command fails", func() { - // GetName is called in Logf - nm.EXPECT().GetName().Return("mocknode").Maybe() - - // First check - registry not running - nm.EXPECT().RunSSHCommand("root", mock.MatchedBy(func(cmd string) bool { - return strings.Contains(cmd, "podman ps") - }), true).Return(fmt.Errorf("not running")) - - // All 8 install commands succeed - nm.EXPECT().RunSSHCommand("root", mock.Anything, true).Return(nil).Times(8) - - // First scp command fails - nm.EXPECT().RunSSHCommand("root", mock.MatchedBy(func(cmd string) bool { - return strings.HasPrefix(cmd, "scp ") - }), true).Return(fmt.Errorf("scp error")).Once() - - err := bs.EnsureLocalContainerRegistry() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to copy registry certificate")) - }) - - It("fails when update-ca-certificates fails", func() { - // Override node setup for this test - node1 := node.NewMockNodeManager(GinkgoT()) - bs.Env.ControlPlaneNodes = []node.NodeManager{node1} - bs.Env.CephNodes = []node.NodeManager{} - - node1.EXPECT().GetInternalIP().Return("10.0.0.2").Maybe() - node1.EXPECT().GetName().Return("node1").Maybe() - - // First check - registry not running - nm.EXPECT().RunSSHCommand("root", mock.MatchedBy(func(cmd string) bool { - return strings.Contains(cmd, "podman ps") - }), true).Return(fmt.Errorf("not running")) - - // All 8 install commands succeed - nm.EXPECT().RunSSHCommand("root", mock.Anything, true).Return(nil).Times(8) - - // scp succeeds - nm.EXPECT().RunSSHCommand("root", mock.MatchedBy(func(cmd string) bool { - return strings.HasPrefix(cmd, "scp ") - }), true).Return(nil).Once() - - // update-ca-certificates fails - node1.EXPECT().RunSSHCommand("root", "update-ca-certificates", true).Return(fmt.Errorf("ca update error")).Once() - - err := bs.EnsureLocalContainerRegistry() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to update CA certificates")) - }) - - It("fails when docker restart fails", func() { - // Override node setup for this test - node1 := node.NewMockNodeManager(GinkgoT()) - bs.Env.ControlPlaneNodes = []node.NodeManager{node1} - bs.Env.CephNodes = []node.NodeManager{} - - node1.EXPECT().GetInternalIP().Return("10.0.0.2").Maybe() - node1.EXPECT().GetName().Return("node1").Maybe() - - // First check - registry not running - nm.EXPECT().RunSSHCommand("root", mock.MatchedBy(func(cmd string) bool { - return strings.Contains(cmd, "podman ps") - }), true).Return(fmt.Errorf("not running")) - - // All 8 install commands succeed - nm.EXPECT().RunSSHCommand("root", mock.Anything, true).Return(nil).Times(8) - - // scp succeeds - nm.EXPECT().RunSSHCommand("root", mock.MatchedBy(func(cmd string) bool { - return strings.HasPrefix(cmd, "scp ") - }), true).Return(nil).Once() - - // update-ca-certificates succeeds - node1.EXPECT().RunSSHCommand("root", "update-ca-certificates", true).Return(nil).Once() - - // docker restart fails - node1.EXPECT().RunSSHCommand("root", "systemctl restart docker.service || true", true).Return(fmt.Errorf("docker restart error")).Once() - - err := bs.EnsureLocalContainerRegistry() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to restart docker service")) - }) - }) -}) - -var _ = Describe("EnsureServiceAccounts", func() { - Describe("Valid EnsureServiceAccounts", func() { - It("creates cloud-controller and skips writer if not artifact registry", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - ProjectID: "pid", - RegistryType: gcp.RegistryTypeLocalContainer, - } - stlog := bootstrap.NewStepLogger(false) - - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) - - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) - - gc.EXPECT().CreateServiceAccount("pid", "cloud-controller", "cloud-controller").Return("email@sa", false, nil) - - err = bs.EnsureServiceAccounts() - Expect(err).NotTo(HaveOccurred()) - }) - - It("creates both accounts for artifact registry", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - ProjectID: "pid", - RegistryType: gcp.RegistryTypeArtifactRegistry, - InstallConfig: &files.RootConfig{ - Registry: &files.RegistryConfig{}, - }, - } - stlog := bootstrap.NewStepLogger(false) - - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) - - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) - - gc.EXPECT().CreateServiceAccount("pid", "cloud-controller", "cloud-controller").Return("email@sa", false, nil) - gc.EXPECT().CreateServiceAccount("pid", "artifact-registry-writer", "artifact-registry-writer").Return("writer@sa", true, nil) - gc.EXPECT().CreateServiceAccountKey("pid", "writer@sa").Return("key-content", nil) - - err = bs.EnsureServiceAccounts() - Expect(err).NotTo(HaveOccurred()) - Expect(bs.Env.InstallConfig.Registry.Password).To(Equal("key-content")) - }) - }) - - Describe("Invalid cases", func() { - It("fails when cloud-controller creation fails", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - ProjectID: "pid", - } - stlog := bootstrap.NewStepLogger(false) - - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) - - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) - - gc.EXPECT().CreateServiceAccount("pid", "cloud-controller", "cloud-controller").Return("", false, fmt.Errorf("create error")) - - err = bs.EnsureServiceAccounts() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("create error")) - }) - }) -}) - -var _ = Describe("EnsureIAMRoles", func() { - Describe("Valid EnsureIAMRoles", func() { - It("assigns roles correctly", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - ProjectID: "pid", - RegistryType: gcp.RegistryTypeArtifactRegistry, - } - stlog := bootstrap.NewStepLogger(false) - - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) - - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) - - gc.EXPECT().AssignIAMRole("pid", "cloud-controller", "roles/compute.admin").Return(nil) - gc.EXPECT().AssignIAMRole("pid", "artifact-registry-writer", "roles/artifactregistry.writer").Return(nil) - - err = bs.EnsureIAMRoles() - Expect(err).NotTo(HaveOccurred()) - }) - }) - - Describe("Invalid cases", func() { - It("fails when AssignIAMRole fails", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - ProjectID: "pid", - } - stlog := bootstrap.NewStepLogger(false) - - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) - - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) - - gc.EXPECT().AssignIAMRole("pid", "cloud-controller", "roles/compute.admin").Return(fmt.Errorf("iam error")) - - err = bs.EnsureIAMRoles() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("iam error")) - }) - }) -}) - -var _ = Describe("EnsureVPC", func() { - Describe("Valid EnsureVPC", func() { - It("creates VPC, subnet, router, and nat", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - ProjectID: "pid", - Region: "us-central1", - } - stlog := bootstrap.NewStepLogger(false) - - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) - - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) - - gc.EXPECT().CreateVPC("pid", "us-central1", "pid-vpc", "pid-us-central1-subnet", "pid-router", "pid-nat-gateway").Return(nil) - - err = bs.EnsureVPC() - Expect(err).NotTo(HaveOccurred()) - }) - }) - - Describe("Invalid cases", func() { - It("fails when CreateVPC fails", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - ProjectID: "pid", - Region: "us-central1", - } - stlog := bootstrap.NewStepLogger(false) - - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) - - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) - - gc.EXPECT().CreateVPC("pid", "us-central1", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(fmt.Errorf("vpc error")) - - err = bs.EnsureVPC() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to ensure VPC")) - Expect(err.Error()).To(ContainSubstring("vpc error")) - }) - }) -}) - -var _ = Describe("EnsureFirewallRules", func() { - Describe("Valid EnsureFirewallRules", func() { - It("creates required firewall rules", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - ProjectID: "pid", - } - stlog := bootstrap.NewStepLogger(false) - - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) - - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) - - // Expect 4 rules: allow-ssh-ext, allow-internal, allow-all-egress, allow-ingress-web, allow-ingress-postgres - // Wait, code showed 5 blocks? ssh, internal, egress, web, postgres. - gc.EXPECT().CreateFirewallRule("pid", mock.MatchedBy(func(r *computepb.Firewall) bool { - return *r.Name == "allow-ssh-ext" - })).Return(nil) - gc.EXPECT().CreateFirewallRule("pid", mock.MatchedBy(func(r *computepb.Firewall) bool { - return *r.Name == "allow-internal" - })).Return(nil) - gc.EXPECT().CreateFirewallRule("pid", mock.MatchedBy(func(r *computepb.Firewall) bool { - return *r.Name == "allow-all-egress" - })).Return(nil) - gc.EXPECT().CreateFirewallRule("pid", mock.MatchedBy(func(r *computepb.Firewall) bool { - return *r.Name == "allow-ingress-web" - })).Return(nil) - gc.EXPECT().CreateFirewallRule("pid", mock.MatchedBy(func(r *computepb.Firewall) bool { - return *r.Name == "allow-ingress-postgres" - })).Return(nil) - - err = bs.EnsureFirewallRules() - Expect(err).NotTo(HaveOccurred()) - }) - }) - - Describe("Invalid cases", func() { - It("fails when first firewall rule creation fails", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - ProjectID: "pid", - } - stlog := bootstrap.NewStepLogger(false) - - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) - - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) - - gc.EXPECT().CreateFirewallRule("pid", mock.Anything).Return(fmt.Errorf("firewall error")).Once() - - err = bs.EnsureFirewallRules() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to create jumpbox ssh firewall rule")) - }) - }) -}) - -var _ = Describe("EnsureComputeInstances", func() { - Describe("Valid EnsureComputeInstances", func() { - It("creates all instances", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - ProjectID: "pid", - Region: "us-central1", - Zone: "us-central1-a", - SSHPublicKeyPath: "key.pub", - } - stlog := bootstrap.NewStepLogger(false) - - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) - - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) - - // Mock ReadFile for SSH key (called 9 times in parallel) - fw.EXPECT().ReadFile("key.pub").Return([]byte("ssh-rsa AAA..."), nil).Times(9) - - // Mock CreateInstance (9 times) - gc.EXPECT().CreateInstance("pid", "us-central1-a", mock.Anything).Return(nil).Times(9) - - // Mock GetInstance (9 times) - ipResp := &computepb.Instance{ - NetworkInterfaces: []*computepb.NetworkInterface{ - { - NetworkIP: protoString("10.0.0.x"), - AccessConfigs: []*computepb.AccessConfig{ - {NatIP: protoString("1.2.3.x")}, - }, - }, - }, - } - gc.EXPECT().GetInstance("pid", "us-central1-a", mock.Anything).Return(ipResp, nil).Times(9) - - // Mock UpdateNode (called once for jumpbox to update the original NodeManager) - nm.EXPECT().UpdateNode(mock.Anything, mock.Anything, mock.Anything).Once() - - // Mock CreateSubNode (8 times for postgres, ceph, k0s - now in main goroutine after channel) - nm.EXPECT().CreateSubNode(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(func(name, extIP, intIP string) node.NodeManager { - m := node.NewMockNodeManager(GinkgoT()) - m.EXPECT().GetName().Return(name).Maybe() // Allow Name() calls for sorting - return m - }).Times(8) - - err = bs.EnsureComputeInstances() - Expect(err).NotTo(HaveOccurred()) - Expect(len(bs.Env.ControlPlaneNodes)).To(Equal(3)) - Expect(len(bs.Env.CephNodes)).To(Equal(4)) - Expect(bs.Env.PostgreSQLNode).NotTo(BeNil()) - Expect(bs.Env.Jumpbox).NotTo(BeNil()) // Jumpbox is now the NodeManager itself after UpdateNode - }) - }) - - Describe("Invalid cases", func() { - It("fails when SSH key read fails", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - ProjectID: "pid", - Region: "us-central1", - Zone: "us-central1-a", - SSHPublicKeyPath: "key.pub", - } - stlog := bootstrap.NewStepLogger(false) - - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) - - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) - - fw.EXPECT().ReadFile("key.pub").Return(nil, fmt.Errorf("read error")).Maybe() - - err = bs.EnsureComputeInstances() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("error ensuring compute instances")) - }) - - It("fails when CreateInstance fails", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - ProjectID: "pid", - Region: "us-central1", - Zone: "us-central1-a", - SSHPublicKeyPath: "key.pub", - } - stlog := bootstrap.NewStepLogger(false) - - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) - - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) - - fw.EXPECT().ReadFile("key.pub").Return([]byte("ssh-rsa AAA..."), nil).Maybe() - gc.EXPECT().CreateInstance("pid", "us-central1-a", mock.Anything).Return(fmt.Errorf("create error")).Maybe() - - err = bs.EnsureComputeInstances() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("error ensuring compute instances")) - }) - - It("fails when GetInstance fails", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - ProjectID: "pid", - Region: "us-central1", - Zone: "us-central1-a", - SSHPublicKeyPath: "key.pub", - } - stlog := bootstrap.NewStepLogger(false) - - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) - - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) - - fw.EXPECT().ReadFile("key.pub").Return([]byte("ssh-rsa AAA..."), nil).Maybe() - gc.EXPECT().CreateInstance("pid", "us-central1-a", mock.Anything).Return(nil).Maybe() - gc.EXPECT().GetInstance("pid", "us-central1-a", mock.Anything).Return(nil, fmt.Errorf("get error")).Maybe() - - err = bs.EnsureComputeInstances() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("error ensuring compute instances")) - }) - }) -}) - -var _ = Describe("EnsureGatewayIPAddresses", func() { - Describe("Valid EnsureGatewayIPAddresses", func() { - It("creates two addresses", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - ProjectID: "pid", - Region: "us-central1", - } - stlog := bootstrap.NewStepLogger(false) - - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) - - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) - - // Gateway - gc.EXPECT().GetAddress("pid", "us-central1", "gateway").Return(nil, fmt.Errorf("not found")) - gc.EXPECT().CreateAddress("pid", "us-central1", mock.MatchedBy(func(a *computepb.Address) bool { - return *a.Name == "gateway" - })).Return("1.1.1.1", nil) - - // Public Gateway - gc.EXPECT().GetAddress("pid", "us-central1", "public-gateway").Return(nil, fmt.Errorf("not found")) - gc.EXPECT().CreateAddress("pid", "us-central1", mock.MatchedBy(func(a *computepb.Address) bool { - return *a.Name == "public-gateway" - })).Return("2.2.2.2", nil) - - err = bs.EnsureGatewayIPAddresses() - Expect(err).NotTo(HaveOccurred()) - Expect(bs.Env.GatewayIP).To(Equal("1.1.1.1")) - Expect(bs.Env.PublicGatewayIP).To(Equal("2.2.2.2")) - }) - }) - - Describe("Invalid cases", func() { - It("fails when gateway IP creation fails", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - ProjectID: "pid", - Region: "us-central1", - } - stlog := bootstrap.NewStepLogger(false) - - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) - - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) - - gc.EXPECT().GetAddress("pid", "us-central1", "gateway").Return(nil, fmt.Errorf("not found")) - gc.EXPECT().CreateAddress("pid", "us-central1", mock.Anything).Return("", fmt.Errorf("create error")) - - err = bs.EnsureGatewayIPAddresses() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to ensure gateway IP")) - }) - - It("fails when public gateway IP creation fails", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - ProjectID: "pid", - Region: "us-central1", - } - stlog := bootstrap.NewStepLogger(false) - - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) - - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) - - gc.EXPECT().GetAddress("pid", "us-central1", "gateway").Return(nil, fmt.Errorf("not found")) - gc.EXPECT().CreateAddress("pid", "us-central1", mock.MatchedBy(func(a *computepb.Address) bool { - return *a.Name == "gateway" - })).Return("1.1.1.1", nil) - gc.EXPECT().GetAddress("pid", "us-central1", "public-gateway").Return(nil, fmt.Errorf("not found")) - gc.EXPECT().CreateAddress("pid", "us-central1", mock.MatchedBy(func(a *computepb.Address) bool { - return *a.Name == "public-gateway" - })).Return("", fmt.Errorf("create error")) - - err = bs.EnsureGatewayIPAddresses() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to ensure public gateway IP")) - }) - }) -}) - -var _ = Describe("EnsureRootLoginEnabled", func() { - Describe("Valid EnsureRootLoginEnabled", func() { - It("enables root login on all nodes", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{} - stlog := bootstrap.NewStepLogger(false) - - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) - - // Setup nodes - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) - - // Use the same mock for all for simplicity, but expect multiple calls - bs.Env.Jumpbox = nm - bs.Env.PostgreSQLNode = nm - bs.Env.ControlPlaneNodes = []node.NodeManager{nm} - bs.Env.CephNodes = []node.NodeManager{nm} - - // Total nodes: 1 (jumpbox) + 1 (pg) + 1 (cp) + 1 (ceph) = 4 - nm.EXPECT().GetName().Return("mock-node").Maybe() - nm.EXPECT().WaitForSSH(mock.Anything).Return(nil).Times(4) - nm.EXPECT().HasRootLoginEnabled().Return(false).Times(4) - nm.EXPECT().EnableRootLogin().Return(nil).Times(4) - - err = bs.EnsureRootLoginEnabled() - Expect(err).NotTo(HaveOccurred()) - }) - }) - - Describe("Invalid cases", func() { - It("fails when WaitForSSH times out", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{} - stlog := bootstrap.NewStepLogger(false) - - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) - - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) - - bs.Env.Jumpbox = nm - bs.Env.PostgreSQLNode = nm - bs.Env.ControlPlaneNodes = []node.NodeManager{} - bs.Env.CephNodes = []node.NodeManager{} - - nm.EXPECT().GetName().Return("mock-node").Maybe() - nm.EXPECT().WaitForSSH(mock.Anything).Return(fmt.Errorf("timeout")).Once() - - err = bs.EnsureRootLoginEnabled() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("timed out waiting for SSH service")) - }) - - It("fails when EnableRootLogin fails", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{} - stlog := bootstrap.NewStepLogger(false) - - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) - - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) - - bs.Env.Jumpbox = nm - bs.Env.PostgreSQLNode = nm - bs.Env.ControlPlaneNodes = []node.NodeManager{} - bs.Env.CephNodes = []node.NodeManager{} - - nm.EXPECT().GetName().Return("mock-node").Maybe() - nm.EXPECT().WaitForSSH(mock.Anything).Return(nil).Once() - nm.EXPECT().HasRootLoginEnabled().Return(false).Once() - nm.EXPECT().EnableRootLogin().Return(fmt.Errorf("enable error")).Times(3) - - err = bs.EnsureRootLoginEnabled() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to enable root login")) - }) - }) -}) - -var _ = Describe("EnsureJumpboxConfigured", func() { - Describe("Valid EnsureJumpboxConfigured", func() { - It("configures jumpbox", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{} - stlog := bootstrap.NewStepLogger(false) - - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) - - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) - - bs.Env.Jumpbox = nm - - nm.EXPECT().HasAcceptEnvConfigured().Return(false) - nm.EXPECT().ConfigureAcceptEnv().Return(nil) - nm.EXPECT().HasCommand("oms-cli").Return(false) - nm.EXPECT().InstallOms().Return(nil) - - err = bs.EnsureJumpboxConfigured() - Expect(err).NotTo(HaveOccurred()) - }) - }) - - Describe("Invalid cases", func() { - It("fails when ConfigureAcceptEnv fails", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{} - stlog := bootstrap.NewStepLogger(false) - - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) - - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) - - bs.Env.Jumpbox = nm - - nm.EXPECT().HasAcceptEnvConfigured().Return(false) - nm.EXPECT().ConfigureAcceptEnv().Return(fmt.Errorf("config error")) - - err = bs.EnsureJumpboxConfigured() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to configure AcceptEnv")) - }) - - It("fails when InstallOms fails", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{} - stlog := bootstrap.NewStepLogger(false) - - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) + Describe("EnsureIAMRoles", func() { + Describe("Valid EnsureIAMRoles", func() { + It("assigns roles correctly", func() { + csEnv = &gcp.CodesphereEnvironment{ + ProjectID: "pid", + RegistryType: gcp.RegistryTypeArtifactRegistry, + } + stlog := bootstrap.NewStepLogger(false) - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) - bs.Env.Jumpbox = nm + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) - nm.EXPECT().HasAcceptEnvConfigured().Return(true) - nm.EXPECT().HasCommand("oms-cli").Return(false) - nm.EXPECT().InstallOms().Return(fmt.Errorf("install error")) + gc.EXPECT().AssignIAMRole("pid", "cloud-controller", "roles/compute.admin").Return(nil) + gc.EXPECT().AssignIAMRole("pid", "artifact-registry-writer", "roles/artifactregistry.writer").Return(nil) - err = bs.EnsureJumpboxConfigured() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to install OMS")) + err = bs.EnsureIAMRoles() + Expect(err).NotTo(HaveOccurred()) + }) }) - }) -}) - -var _ = Describe("EnsureHostsConfigured", func() { - Describe("Valid EnsureHostsConfigured", func() { - It("configures hosts", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{} - stlog := bootstrap.NewStepLogger(false) - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) + Describe("Invalid cases", func() { + It("fails when AssignIAMRole fails", func() { + csEnv = &gcp.CodesphereEnvironment{ + ProjectID: "pid", + } + stlog := bootstrap.NewStepLogger(false) - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) - // Setup nodes - bs.Env.PostgreSQLNode = nm - bs.Env.ControlPlaneNodes = []node.NodeManager{nm} - bs.Env.CephNodes = []node.NodeManager{} // Empty to reduce calls + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) - // Total nodes: 1 (pg) + 1 (cp) = 2 - nm.EXPECT().GetName().Return("mock-node").Maybe() - nm.EXPECT().HasInotifyWatchesConfigured().Return(false).Times(2) - nm.EXPECT().ConfigureInotifyWatches().Return(nil).Times(2) - nm.EXPECT().HasMemoryMapConfigured().Return(false).Times(2) - nm.EXPECT().ConfigureMemoryMap().Return(nil).Times(2) + gc.EXPECT().AssignIAMRole("pid", "cloud-controller", "roles/compute.admin").Return(fmt.Errorf("iam error")) - err = bs.EnsureHostsConfigured() - Expect(err).NotTo(HaveOccurred()) + err = bs.EnsureIAMRoles() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("iam error")) + }) }) }) - Describe("Invalid cases", func() { - It("fails when ConfigureInotifyWatches fails", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{} - stlog := bootstrap.NewStepLogger(false) - - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) + Describe("EnsureVPC", func() { + Describe("Valid EnsureVPC", func() { + It("creates VPC, subnet, router, and nat", func() { + csEnv = &gcp.CodesphereEnvironment{ + ProjectID: "pid", + Region: "us-central1", + } + stlog := bootstrap.NewStepLogger(false) - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) - bs.Env.PostgreSQLNode = nm - bs.Env.ControlPlaneNodes = []node.NodeManager{} - bs.Env.CephNodes = []node.NodeManager{} + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) - nm.EXPECT().GetName().Return("mock-node").Maybe() - nm.EXPECT().HasInotifyWatchesConfigured().Return(false) - nm.EXPECT().ConfigureInotifyWatches().Return(fmt.Errorf("inotify error")) + gc.EXPECT().CreateVPC("pid", "us-central1", "pid-vpc", "pid-us-central1-subnet", "pid-router", "pid-nat-gateway").Return(nil) - err = bs.EnsureHostsConfigured() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to configure inotify watches")) + err = bs.EnsureVPC() + Expect(err).NotTo(HaveOccurred()) + }) }) - It("fails when ConfigureMemoryMap fails", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{} - stlog := bootstrap.NewStepLogger(false) + Describe("Invalid cases", func() { + It("fails when CreateVPC fails", func() { + csEnv = &gcp.CodesphereEnvironment{ + ProjectID: "pid", + Region: "us-central1", + } + stlog := bootstrap.NewStepLogger(false) - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) - bs.Env.PostgreSQLNode = nm - bs.Env.ControlPlaneNodes = []node.NodeManager{} - bs.Env.CephNodes = []node.NodeManager{} + gc.EXPECT().CreateVPC("pid", "us-central1", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(fmt.Errorf("vpc error")) - nm.EXPECT().GetName().Return("mock-node").Maybe() - nm.EXPECT().HasInotifyWatchesConfigured().Return(true) - nm.EXPECT().HasMemoryMapConfigured().Return(false) - nm.EXPECT().ConfigureMemoryMap().Return(fmt.Errorf("memory map error")) + err = bs.EnsureVPC() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to ensure VPC")) + Expect(err.Error()).To(ContainSubstring("vpc error")) + }) + }) + }) - err = bs.EnsureHostsConfigured() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to configure memory map")) + Describe("EnsureFirewallRules", func() { + Describe("Valid EnsureFirewallRules", func() { + It("creates required firewall rules", func() { + csEnv = &gcp.CodesphereEnvironment{ + ProjectID: "pid", + } + stlog := bootstrap.NewStepLogger(false) + + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) + + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) + + // Expect 4 rules: allow-ssh-ext, allow-internal, allow-all-egress, allow-ingress-web, allow-ingress-postgres + // Wait, code showed 5 blocks? ssh, internal, egress, web, postgres. + gc.EXPECT().CreateFirewallRule("pid", mock.MatchedBy(func(r *computepb.Firewall) bool { + return *r.Name == "allow-ssh-ext" + })).Return(nil) + gc.EXPECT().CreateFirewallRule("pid", mock.MatchedBy(func(r *computepb.Firewall) bool { + return *r.Name == "allow-internal" + })).Return(nil) + gc.EXPECT().CreateFirewallRule("pid", mock.MatchedBy(func(r *computepb.Firewall) bool { + return *r.Name == "allow-all-egress" + })).Return(nil) + gc.EXPECT().CreateFirewallRule("pid", mock.MatchedBy(func(r *computepb.Firewall) bool { + return *r.Name == "allow-ingress-web" + })).Return(nil) + gc.EXPECT().CreateFirewallRule("pid", mock.MatchedBy(func(r *computepb.Firewall) bool { + return *r.Name == "allow-ingress-postgres" + })).Return(nil) + + err = bs.EnsureFirewallRules() + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Describe("Invalid cases", func() { + It("fails when first firewall rule creation fails", func() { + csEnv = &gcp.CodesphereEnvironment{ + ProjectID: "pid", + } + stlog := bootstrap.NewStepLogger(false) + + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) + + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) + + gc.EXPECT().CreateFirewallRule("pid", mock.Anything).Return(fmt.Errorf("firewall error")).Once() + + err = bs.EnsureFirewallRules() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to create jumpbox ssh firewall rule")) + }) }) }) -}) -var _ = Describe("UpdateInstallConfig", func() { - Describe("Valid UpdateInstallConfig", func() { - It("updates config and writes files", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - SecretsDir: "/secrets", - InstallConfigPath: "config.yaml", - SecretsFilePath: "secrets.yaml", - DatacenterID: 1, - BaseDomain: "example.com", - GatewayIP: "1.1.1.1", - PublicGatewayIP: "2.2.2.2", - GithubAppClientID: "gh-id", - GithubAppClientSecret: "gh-secret", - InstallConfig: &files.RootConfig{ - Registry: &files.RegistryConfig{}, - Postgres: files.PostgresConfig{ - Primary: &files.PostgresPrimaryConfig{}, + Describe("EnsureComputeInstances", func() { + Describe("Valid EnsureComputeInstances", func() { + It("creates all instances", func() { + csEnv = &gcp.CodesphereEnvironment{ + ProjectID: "pid", + Region: "us-central1", + Zone: "us-central1-a", + SSHPublicKeyPath: "key.pub", + } + stlog := bootstrap.NewStepLogger(false) + + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) + + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) + + // Mock ReadFile for SSH key (called 9 times in parallel) + fw.EXPECT().ReadFile("key.pub").Return([]byte("ssh-rsa AAA..."), nil).Times(9) + + // Mock CreateInstance (9 times) + gc.EXPECT().CreateInstance("pid", "us-central1-a", mock.Anything).Return(nil).Times(9) + + // Mock GetInstance (9 times) + ipResp := &computepb.Instance{ + NetworkInterfaces: []*computepb.NetworkInterface{ + { + NetworkIP: protoString("10.0.0.x"), + AccessConfigs: []*computepb.AccessConfig{ + {NatIP: protoString("1.2.3.x")}, + }, + }, }, - Cluster: files.ClusterConfig{}, - }, - } - stlog := bootstrap.NewStepLogger(false) - - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) - - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) + } + gc.EXPECT().GetInstance("pid", "us-central1-a", mock.Anything).Return(ipResp, nil).Times(9) + + err = bs.EnsureComputeInstances() + Expect(err).NotTo(HaveOccurred()) + Expect(len(bs.Env.ControlPlaneNodes)).To(Equal(3)) + Expect(len(bs.Env.CephNodes)).To(Equal(4)) + Expect(bs.Env.PostgreSQLNode).NotTo(BeNil()) + Expect(bs.Env.Jumpbox).NotTo(BeNil()) + }) + }) + + Describe("Invalid cases", func() { + It("fails when SSH key read fails", func() { + + csEnv = &gcp.CodesphereEnvironment{ + ProjectID: "pid", + Region: "us-central1", + Zone: "us-central1-a", + SSHPublicKeyPath: "key.pub", + } + stlog := bootstrap.NewStepLogger(false) + + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) + + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) + + fw.EXPECT().ReadFile("key.pub").Return(nil, fmt.Errorf("read error")).Maybe() + + err = bs.EnsureComputeInstances() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("error ensuring compute instances")) + }) + + It("fails when CreateInstance fails", func() { + + csEnv = &gcp.CodesphereEnvironment{ + ProjectID: "pid", + Region: "us-central1", + Zone: "us-central1-a", + SSHPublicKeyPath: "key.pub", + } + stlog := bootstrap.NewStepLogger(false) + + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) + + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) + + fw.EXPECT().ReadFile("key.pub").Return([]byte("ssh-rsa AAA..."), nil).Maybe() + gc.EXPECT().CreateInstance("pid", "us-central1-a", mock.Anything).Return(fmt.Errorf("create error")).Maybe() + + err = bs.EnsureComputeInstances() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("error ensuring compute instances")) + }) + + It("fails when GetInstance fails", func() { + + csEnv = &gcp.CodesphereEnvironment{ + ProjectID: "pid", + Region: "us-central1", + Zone: "us-central1-a", + SSHPublicKeyPath: "key.pub", + } + stlog := bootstrap.NewStepLogger(false) + + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) + + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) + + fw.EXPECT().ReadFile("key.pub").Return([]byte("ssh-rsa AAA..."), nil).Maybe() + gc.EXPECT().CreateInstance("pid", "us-central1-a", mock.Anything).Return(nil).Maybe() + gc.EXPECT().GetInstance("pid", "us-central1-a", mock.Anything).Return(nil, fmt.Errorf("get error")).Maybe() + + err = bs.EnsureComputeInstances() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("error ensuring compute instances")) + }) + }) + }) - // Setup Nodes - bs.Env.Jumpbox = nm - bs.Env.PostgreSQLNode = nm - bs.Env.CephNodes = []node.NodeManager{nm, nm, nm, nm} - bs.Env.ControlPlaneNodes = []node.NodeManager{nm, nm, nm} + Describe("EnsureGatewayIPAddresses", func() { + Describe("Valid EnsureGatewayIPAddresses", func() { + It("creates two addresses", func() { + + csEnv = &gcp.CodesphereEnvironment{ + ProjectID: "pid", + Region: "us-central1", + } + stlog := bootstrap.NewStepLogger(false) + + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) + + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) + + // Gateway + gc.EXPECT().GetAddress("pid", "us-central1", "gateway").Return(nil, fmt.Errorf("not found")) + gc.EXPECT().CreateAddress("pid", "us-central1", mock.MatchedBy(func(a *computepb.Address) bool { + return *a.Name == "gateway" + })).Return("1.1.1.1", nil) + + // Public Gateway + gc.EXPECT().GetAddress("pid", "us-central1", "public-gateway").Return(nil, fmt.Errorf("not found")) + gc.EXPECT().CreateAddress("pid", "us-central1", mock.MatchedBy(func(a *computepb.Address) bool { + return *a.Name == "public-gateway" + })).Return("2.2.2.2", nil) + + err = bs.EnsureGatewayIPAddresses() + Expect(err).NotTo(HaveOccurred()) + Expect(bs.Env.GatewayIP).To(Equal("1.1.1.1")) + Expect(bs.Env.PublicGatewayIP).To(Equal("2.2.2.2")) + }) + }) + + Describe("Invalid cases", func() { + It("fails when gateway IP creation fails", func() { + + csEnv = &gcp.CodesphereEnvironment{ + ProjectID: "pid", + Region: "us-central1", + } + stlog := bootstrap.NewStepLogger(false) + + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) + + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) + + gc.EXPECT().GetAddress("pid", "us-central1", "gateway").Return(nil, fmt.Errorf("not found")) + gc.EXPECT().CreateAddress("pid", "us-central1", mock.Anything).Return("", fmt.Errorf("create error")) + + err = bs.EnsureGatewayIPAddresses() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to ensure gateway IP")) + }) + + It("fails when public gateway IP creation fails", func() { + + csEnv = &gcp.CodesphereEnvironment{ + ProjectID: "pid", + Region: "us-central1", + } + stlog := bootstrap.NewStepLogger(false) + + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) + + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) + + gc.EXPECT().GetAddress("pid", "us-central1", "gateway").Return(nil, fmt.Errorf("not found")) + gc.EXPECT().CreateAddress("pid", "us-central1", mock.MatchedBy(func(a *computepb.Address) bool { + return *a.Name == "gateway" + })).Return("1.1.1.1", nil) + gc.EXPECT().GetAddress("pid", "us-central1", "public-gateway").Return(nil, fmt.Errorf("not found")) + gc.EXPECT().CreateAddress("pid", "us-central1", mock.MatchedBy(func(a *computepb.Address) bool { + return *a.Name == "public-gateway" + })).Return("", fmt.Errorf("create error")) + + err = bs.EnsureGatewayIPAddresses() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to ensure public gateway IP")) + }) + }) + }) - nm.EXPECT().GetName().Return("mock-node").Maybe() - nm.EXPECT().GetInternalIP().Return("10.0.0.1").Maybe() - nm.EXPECT().GetExternalIP().Return("8.8.8.8").Maybe() // For PublicIP + Describe("EnsureRootLoginEnabled", func() { + Context("When WaitReady times out", func() { + It("fails", func() { + nodeClient.EXPECT().WaitReady(mock.Anything, mock.Anything).Return(fmt.Errorf("TIMEOUT!")) - // Expectations - icg.EXPECT().GenerateSecrets().Return(nil) - icg.EXPECT().WriteInstallConfig("config.yaml", true).Return(nil) - icg.EXPECT().WriteVault("secrets.yaml", true).Return(nil) + stlog := bootstrap.NewStepLogger(false) - nm.EXPECT().CopyFile("config.yaml", "/etc/codesphere/config.yaml").Return(nil) - nm.EXPECT().CopyFile("secrets.yaml", "/secrets/prod.vault.yaml").Return(nil) + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) - err = bs.UpdateInstallConfig() - Expect(err).NotTo(HaveOccurred()) + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) - Expect(bs.Env.InstallConfig.Datacenter.ID).To(Equal(1)) - Expect(bs.Env.InstallConfig.Codesphere.Domain).To(Equal("cs.example.com")) + err = bs.EnsureRootLoginEnabled() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("timed out waiting for SSH service")) + }) }) - }) + Context("When WaitReady succeeds", func() { + BeforeEach(func() { + nodeClient.EXPECT().WaitReady(mock.Anything, mock.Anything).Return(nil) + }) + Describe("Valid EnsureRootLoginEnabled", func() { + It("enables root login on all nodes", func() { + nodeClient.EXPECT().RunCommand(mock.Anything, "ubuntu", mock.Anything).Return(nil) + stlog := bootstrap.NewStepLogger(false) - Describe("Invalid cases", func() { - It("fails when GenerateSecrets fails", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - SecretsDir: "/secrets", - InstallConfigPath: "config.yaml", - SecretsFilePath: "secrets.yaml", - DatacenterID: 1, - BaseDomain: "example.com", - GatewayIP: "1.1.1.1", - PublicGatewayIP: "2.2.2.2", - GithubAppClientID: "gh-id", - GithubAppClientSecret: "gh-secret", - InstallConfig: &files.RootConfig{ - Registry: &files.RegistryConfig{}, - Postgres: files.PostgresConfig{ - Primary: &files.PostgresPrimaryConfig{}, - }, - Cluster: files.ClusterConfig{}, - }, - } - stlog := bootstrap.NewStepLogger(false) + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) + // Setup nodes + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) + err = bs.EnsureRootLoginEnabled() + Expect(err).NotTo(HaveOccurred()) + }) + }) - bs.Env.Jumpbox = nm - bs.Env.PostgreSQLNode = nm - bs.Env.CephNodes = []node.NodeManager{nm, nm, nm, nm} - bs.Env.ControlPlaneNodes = []node.NodeManager{nm, nm, nm} + It("fails when EnableRootLogin fails", func() { + nodeClient.EXPECT().RunCommand(mock.Anything, "ubuntu", mock.Anything).Return(fmt.Errorf("ouch")) + stlog := bootstrap.NewStepLogger(false) - nm.EXPECT().GetName().Return("mock-node").Maybe() - nm.EXPECT().GetInternalIP().Return("10.0.0.1").Maybe() - nm.EXPECT().GetExternalIP().Return("8.8.8.8").Maybe() + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) - icg.EXPECT().GenerateSecrets().Return(fmt.Errorf("generate error")) + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) - err = bs.UpdateInstallConfig() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to generate secrets")) + err = bs.EnsureRootLoginEnabled() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to enable root login")) + }) }) + }) - It("fails when WriteInstallConfig fails", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - SecretsDir: "/secrets", - InstallConfigPath: "config.yaml", - SecretsFilePath: "secrets.yaml", - DatacenterID: 1, - BaseDomain: "example.com", - GatewayIP: "1.1.1.1", - PublicGatewayIP: "2.2.2.2", - GithubAppClientID: "gh-id", - GithubAppClientSecret: "gh-secret", - InstallConfig: &files.RootConfig{ - Registry: &files.RegistryConfig{}, - Postgres: files.PostgresConfig{ - Primary: &files.PostgresPrimaryConfig{}, - }, - Cluster: files.ClusterConfig{}, - }, - } - stlog := bootstrap.NewStepLogger(false) + Describe("EnsureJumpboxConfigured", func() { + Describe("Valid EnsureJumpboxConfigured", func() { + It("configures jumpbox", func() { + // Setup jumpbox node requires some commands to run + nodeClient.EXPECT().RunCommand(mock.Anything, mock.Anything, mock.Anything).Return(nil) - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) + stlog := bootstrap.NewStepLogger(false) - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) - bs.Env.Jumpbox = nm - bs.Env.PostgreSQLNode = nm - bs.Env.CephNodes = []node.NodeManager{nm, nm, nm, nm} - bs.Env.ControlPlaneNodes = []node.NodeManager{nm, nm, nm} + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) - nm.EXPECT().GetName().Return("mock-node").Maybe() - nm.EXPECT().GetInternalIP().Return("10.0.0.1").Maybe() - nm.EXPECT().GetExternalIP().Return("8.8.8.8").Maybe() + err = bs.EnsureJumpboxConfigured() + Expect(err).NotTo(HaveOccurred()) + }) + }) - icg.EXPECT().GenerateSecrets().Return(nil) - icg.EXPECT().WriteInstallConfig("config.yaml", true).Return(fmt.Errorf("write error")) + Describe("Invalid cases", func() { + It("fails when ConfigureAcceptEnv fails", func() { + // Setup jumpbox node requires some commands to run + nodeClient.EXPECT().RunCommand(mock.Anything, "ubuntu", mock.Anything).Return(fmt.Errorf("ouch")).Twice() - err = bs.UpdateInstallConfig() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to write config file")) - }) + stlog := bootstrap.NewStepLogger(false) - It("fails when WriteVault fails", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - SecretsDir: "/secrets", - InstallConfigPath: "config.yaml", - SecretsFilePath: "secrets.yaml", - DatacenterID: 1, - BaseDomain: "example.com", - GatewayIP: "1.1.1.1", - PublicGatewayIP: "2.2.2.2", - GithubAppClientID: "gh-id", - GithubAppClientSecret: "gh-secret", - InstallConfig: &files.RootConfig{ - Registry: &files.RegistryConfig{}, - Postgres: files.PostgresConfig{ - Primary: &files.PostgresPrimaryConfig{}, - }, - Cluster: files.ClusterConfig{}, - }, - } - stlog := bootstrap.NewStepLogger(false) + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) + err = bs.EnsureJumpboxConfigured() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to configure AcceptEnv")) + }) - bs.Env.Jumpbox = nm - bs.Env.PostgreSQLNode = nm - bs.Env.CephNodes = []node.NodeManager{nm, nm, nm, nm} - bs.Env.ControlPlaneNodes = []node.NodeManager{nm, nm, nm} + It("fails when InstallOms fails", func() { + nodeClient.EXPECT().RunCommand(mock.Anything, "ubuntu", mock.Anything).Return(nil) + nodeClient.EXPECT().RunCommand(mock.Anything, "root", mock.Anything).Return(fmt.Errorf("outch")) + stlog := bootstrap.NewStepLogger(false) - nm.EXPECT().GetName().Return("mock-node").Maybe() - nm.EXPECT().GetInternalIP().Return("10.0.0.1").Maybe() - nm.EXPECT().GetExternalIP().Return("8.8.8.8").Maybe() + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) - icg.EXPECT().GenerateSecrets().Return(nil) - icg.EXPECT().WriteInstallConfig("config.yaml", true).Return(nil) - icg.EXPECT().WriteVault("secrets.yaml", true).Return(fmt.Errorf("vault write error")) + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) - err = bs.UpdateInstallConfig() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to write vault file")) + err = bs.EnsureJumpboxConfigured() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to install OMS")) + }) }) + }) - It("fails when CopyFile config fails", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - SecretsDir: "/secrets", - InstallConfigPath: "config.yaml", - SecretsFilePath: "secrets.yaml", - DatacenterID: 1, - BaseDomain: "example.com", - GatewayIP: "1.1.1.1", - PublicGatewayIP: "2.2.2.2", - GithubAppClientID: "gh-id", - GithubAppClientSecret: "gh-secret", - InstallConfig: &files.RootConfig{ - Registry: &files.RegistryConfig{}, - Postgres: files.PostgresConfig{ - Primary: &files.PostgresPrimaryConfig{}, - }, - Cluster: files.ClusterConfig{}, - }, - } - stlog := bootstrap.NewStepLogger(false) - - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) + Describe("EnsureHostsConfigured", func() { + Describe("Valid EnsureHostsConfigured", func() { + It("configures hosts", func() { + nodeClient.EXPECT().RunCommand(mock.Anything, "root", mock.Anything).Return(nil) + stlog := bootstrap.NewStepLogger(false) - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) - bs.Env.Jumpbox = nm - bs.Env.PostgreSQLNode = nm - bs.Env.CephNodes = []node.NodeManager{nm, nm, nm, nm} - bs.Env.ControlPlaneNodes = []node.NodeManager{nm, nm, nm} + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) - nm.EXPECT().GetName().Return("mock-node").Maybe() - nm.EXPECT().GetInternalIP().Return("10.0.0.1").Maybe() - nm.EXPECT().GetExternalIP().Return("8.8.8.8").Maybe() + err = bs.EnsureHostsConfigured() + Expect(err).NotTo(HaveOccurred()) + }) + }) - icg.EXPECT().GenerateSecrets().Return(nil) - icg.EXPECT().WriteInstallConfig("config.yaml", true).Return(nil) - icg.EXPECT().WriteVault("secrets.yaml", true).Return(nil) - nm.EXPECT().CopyFile("config.yaml", "/etc/codesphere/config.yaml").Return(fmt.Errorf("copy error")) + Describe("Invalid cases", func() { + It("fails when ConfigureInotifyWatches fails", func() { + nodeClient.EXPECT().RunCommand(mock.Anything, "root", mock.Anything).Return(fmt.Errorf("ouch")) + stlog := bootstrap.NewStepLogger(false) - err = bs.UpdateInstallConfig() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to copy install config to jumpbox")) - }) + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) - It("fails when CopyFile secrets fails", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - SecretsDir: "/secrets", - InstallConfigPath: "config.yaml", - SecretsFilePath: "secrets.yaml", - DatacenterID: 1, - BaseDomain: "example.com", - GatewayIP: "1.1.1.1", - PublicGatewayIP: "2.2.2.2", - GithubAppClientID: "gh-id", - GithubAppClientSecret: "gh-secret", - InstallConfig: &files.RootConfig{ - Registry: &files.RegistryConfig{}, - Postgres: files.PostgresConfig{ - Primary: &files.PostgresPrimaryConfig{}, - }, - Cluster: files.ClusterConfig{}, - }, - } - stlog := bootstrap.NewStepLogger(false) + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) + err = bs.EnsureHostsConfigured() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to configure inotify watches")) + }) - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) + It("fails when ConfigureMemoryMap fails", func() { + stlog := bootstrap.NewStepLogger(false) + mock.InOrder( + nodeClient.EXPECT().RunCommand(mock.Anything, "root", mock.Anything).Return(nil).Times(1), // for inotify + nodeClient.EXPECT().RunCommand(mock.Anything, "root", mock.Anything).Return(fmt.Errorf("ouch")).Times(2), // for memory map + ) - bs.Env.Jumpbox = nm - bs.Env.PostgreSQLNode = nm - bs.Env.CephNodes = []node.NodeManager{nm, nm, nm, nm} - bs.Env.ControlPlaneNodes = []node.NodeManager{nm, nm, nm} + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) - nm.EXPECT().GetName().Return("mock-node").Maybe() - nm.EXPECT().GetInternalIP().Return("10.0.0.1").Maybe() - nm.EXPECT().GetExternalIP().Return("8.8.8.8").Maybe() + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) - icg.EXPECT().GenerateSecrets().Return(nil) - icg.EXPECT().WriteInstallConfig("config.yaml", true).Return(nil) - icg.EXPECT().WriteVault("secrets.yaml", true).Return(nil) - nm.EXPECT().CopyFile("config.yaml", "/etc/codesphere/config.yaml").Return(nil) - nm.EXPECT().CopyFile("secrets.yaml", "/secrets/prod.vault.yaml").Return(fmt.Errorf("copy error")) - - err = bs.UpdateInstallConfig() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to copy secrets file to jumpbox")) + err = bs.EnsureHostsConfigured() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to configure memory map")) + }) }) }) -}) -var _ = Describe("EnsureAgeKey", func() { - Describe("Valid EnsureAgeKey", func() { - It("generates key if missing", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - SecretsDir: "/secrets", - } - stlog := bootstrap.NewStepLogger(false) + Describe("UpdateInstallConfig", func() { + Describe("Valid UpdateInstallConfig", func() { + It("updates config and writes files", func() { - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) + stlog := bootstrap.NewStepLogger(false) - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) - bs.Env.Jumpbox = nm + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) - nm.EXPECT().HasFile("/secrets/age_key.txt").Return(false) - nm.EXPECT().RunSSHCommand("root", mock.MatchedBy(func(cmd string) bool { - return strings.Contains(cmd, "age-keygen") - }), true).Return(nil) + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) - err = bs.EnsureAgeKey() - Expect(err).NotTo(HaveOccurred()) + // Expectations + 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.Datacenter.ID).To(Equal(1)) + Expect(bs.Env.InstallConfig.Codesphere.Domain).To(Equal("cs.example.com")) + }) }) - It("skips if key exists", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - SecretsDir: "/secrets", - } - stlog := bootstrap.NewStepLogger(false) + Describe("Invalid cases", func() { + It("fails when GenerateSecrets fails", func() { + stlog := bootstrap.NewStepLogger(false) - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) - bs.Env.Jumpbox = nm + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) - nm.EXPECT().HasFile("/secrets/age_key.txt").Return(true) - // No SSH command expected + icg.EXPECT().GenerateSecrets().Return(fmt.Errorf("generate error")) - err = bs.EnsureAgeKey() - Expect(err).NotTo(HaveOccurred()) - }) - }) + err = bs.UpdateInstallConfig() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to generate secrets")) + }) - Describe("Invalid cases", func() { - It("fails when age-keygen command fails", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - SecretsDir: "/secrets", - } - stlog := bootstrap.NewStepLogger(false) + It("fails when WriteInstallConfig fails", func() { + stlog := bootstrap.NewStepLogger(false) - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) - bs.Env.Jumpbox = nm + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) - nm.EXPECT().HasFile("/secrets/age_key.txt").Return(false) - nm.EXPECT().RunSSHCommand("root", mock.MatchedBy(func(cmd string) bool { - return strings.Contains(cmd, "age-keygen") - }), true).Return(fmt.Errorf("keygen error")) + icg.EXPECT().GenerateSecrets().Return(nil) + icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(fmt.Errorf("write error")) - err = bs.EnsureAgeKey() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to generate age key on jumpbox")) - }) - }) -}) + err = bs.UpdateInstallConfig() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to write config file")) + }) -var _ = Describe("EncryptVault", func() { - Describe("Valid EncryptVault", func() { - It("encrypts vault using sops", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - SecretsDir: "/secrets", - } - stlog := bootstrap.NewStepLogger(false) + It("fails when WriteVault fails", func() { + stlog := bootstrap.NewStepLogger(false) - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) - bs.Env.Jumpbox = nm + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) - nm.EXPECT().RunSSHCommand("root", mock.MatchedBy(func(cmd string) bool { - return strings.HasPrefix(cmd, "cp ") - }), true).Return(nil) + 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")) - nm.EXPECT().RunSSHCommand("root", mock.MatchedBy(func(cmd string) bool { - return strings.Contains(cmd, "sops --encrypt") - }), true).Return(nil) + err = bs.UpdateInstallConfig() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to write vault file")) + }) - err = bs.EncryptVault() - Expect(err).NotTo(HaveOccurred()) - }) - }) + It("fails when CopyFile config fails", func() { - Describe("Invalid cases", func() { - It("fails when backup vault command fails", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - SecretsDir: "/secrets", - } - stlog := bootstrap.NewStepLogger(false) + stlog := bootstrap.NewStepLogger(false) - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) - bs.Env.Jumpbox = nm + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) - nm.EXPECT().RunSSHCommand("root", mock.MatchedBy(func(cmd string) bool { - return strings.HasPrefix(cmd, "cp ") - }), true).Return(fmt.Errorf("backup error")) + icg.EXPECT().GenerateSecrets().Return(nil) + icg.EXPECT().WriteInstallConfig("fake-config-file", true).Return(nil) + icg.EXPECT().WriteVault("fake-secret", true).Return(nil) - err = bs.EncryptVault() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed backup vault on jumpbox")) - }) + nodeClient.EXPECT().CopyFile(mock.Anything, mock.Anything, mock.Anything).Return(fmt.Errorf("copy error")).Once() - It("fails when sops encrypt command fails", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - SecretsDir: "/secrets", - } - stlog := bootstrap.NewStepLogger(false) + err = bs.UpdateInstallConfig() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to copy install config to jumpbox")) + }) - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) + It("fails when CopyFile secrets fails", func() { + stlog := bootstrap.NewStepLogger(false) - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) - bs.Env.Jumpbox = nm + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) - nm.EXPECT().RunSSHCommand("root", mock.MatchedBy(func(cmd string) bool { - return strings.HasPrefix(cmd, "cp ") - }), true).Return(nil) + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) - nm.EXPECT().RunSSHCommand("root", mock.MatchedBy(func(cmd string) bool { - return strings.Contains(cmd, "sops --encrypt") - }), true).Return(fmt.Errorf("encrypt error")) + 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, "fake-config-file", mock.Anything).Return(nil).Once() + nodeClient.EXPECT().CopyFile(mock.Anything, "fake-secret", mock.Anything).Return(fmt.Errorf("copy error")).Once() - err = bs.EncryptVault() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to encrypt vault on jumpbox")) + err = bs.UpdateInstallConfig() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to copy secrets file to jumpbox")) + }) }) }) -}) - -var _ = Describe("EnsureDNSRecords", func() { - Describe("Valid EnsureDNSRecords", func() { - It("ensures DNS records", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - DNSProjectID: "dns-proj", - DNSZoneName: "zone", - BaseDomain: "example.com", - GatewayIP: "1.1.1.1", - PublicGatewayIP: "2.2.2.2", - } - stlog := bootstrap.NewStepLogger(false) - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) + Describe("EnsureAgeKey", func() { + Describe("Valid EnsureAgeKey", func() { + It("generates key if missing", func() { + stlog := bootstrap.NewStepLogger(false) - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) - gc.EXPECT().EnsureDNSManagedZone("dns-proj", "zone", "example.com.", mock.Anything).Return(nil) - gc.EXPECT().EnsureDNSRecordSets("dns-proj", "zone", mock.MatchedBy(func(records []*dns.ResourceRecordSet) bool { - // Expect 4 records: *.ws, *.cs, cs, ws - return len(records) == 4 - })).Return(nil) + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) - err = bs.EnsureDNSRecords() - Expect(err).NotTo(HaveOccurred()) - }) - }) + nodeClient.EXPECT().HasFile(mock.MatchedBy(jumpbboxMatcher), "/etc/codesphere/secrets/age_key.txt").Return(false) + nodeClient.EXPECT().RunCommand(mock.MatchedBy(jumpbboxMatcher), "root", "mkdir -p /etc/codesphere/secrets; age-keygen -o /etc/codesphere/secrets/age_key.txt").Return(nil) - Describe("Invalid cases", func() { - It("fails when EnsureDNSManagedZone fails", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - DNSProjectID: "dns-proj", - DNSZoneName: "zone", - BaseDomain: "example.com", - GatewayIP: "1.1.1.1", - PublicGatewayIP: "2.2.2.2", - } - stlog := bootstrap.NewStepLogger(false) + err = bs.EnsureAgeKey() + Expect(err).NotTo(HaveOccurred()) + }) - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) + It("skips if key exists", func() { + stlog := bootstrap.NewStepLogger(false) - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) - gc.EXPECT().EnsureDNSManagedZone("dns-proj", "zone", "example.com.", mock.Anything).Return(fmt.Errorf("zone error")) + nodeClient.EXPECT().HasFile(mock.MatchedBy(jumpbboxMatcher), "/etc/codesphere/secrets/age_key.txt").Return(true) + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) - err = bs.EnsureDNSRecords() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to ensure DNS managed zone")) + err = bs.EnsureAgeKey() + Expect(err).NotTo(HaveOccurred()) + }) }) - It("fails when EnsureDNSRecordSets fails", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - DNSProjectID: "dns-proj", - DNSZoneName: "zone", - BaseDomain: "example.com", - GatewayIP: "1.1.1.1", - PublicGatewayIP: "2.2.2.2", - } - stlog := bootstrap.NewStepLogger(false) + Describe("Invalid cases", func() { + It("fails when age-keygen command fails", func() { + stlog := bootstrap.NewStepLogger(false) - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) + nodeClient.EXPECT().HasFile(mock.MatchedBy(jumpbboxMatcher), "/etc/codesphere/secrets/age_key.txt").Return(false) + nodeClient.EXPECT().RunCommand(mock.MatchedBy(jumpbboxMatcher), "root", "mkdir -p /etc/codesphere/secrets; age-keygen -o /etc/codesphere/secrets/age_key.txt").Return(fmt.Errorf("ouch")) - gc.EXPECT().EnsureDNSManagedZone("dns-proj", "zone", "example.com.", mock.Anything).Return(nil) - gc.EXPECT().EnsureDNSRecordSets("dns-proj", "zone", mock.Anything).Return(fmt.Errorf("record error")) + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) - err = bs.EnsureDNSRecords() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to ensure DNS record sets")) + err = bs.EnsureAgeKey() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to generate age key on jumpbox")) + }) }) }) -}) -var _ = Describe("InstallCodesphere", func() { - Describe("Valid InstallCodesphere", func() { - It("downloads and installs codesphere", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - InstallCodesphereVersion: "v1.2.3", - SecretsDir: "/secrets", - } - stlog := bootstrap.NewStepLogger(false) + Describe("EncryptVault", func() { + Describe("Valid EncryptVault", func() { + It("encrypts vault using sops", func() { + stlog := bootstrap.NewStepLogger(false) - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) - bs.Env.Jumpbox = nm + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) - // Expect download package - nm.EXPECT().RunSSHCommand("root", "oms-cli download package v1.2.3", true).Return(nil) + nodeClient.EXPECT().RunCommand(bs.Env.Jumpbox, "root", mock.MatchedBy(func(cmd string) bool { + return strings.HasPrefix(cmd, "cp ") + })).Return(nil) - // Expect install codesphere - nm.EXPECT().RunSSHCommand("root", "oms-cli install codesphere -c /etc/codesphere/config.yaml -k /secrets/age_key.txt -p v1.2.3.tar.gz", true).Return(nil) + nodeClient.EXPECT().RunCommand(bs.Env.Jumpbox, "root", mock.MatchedBy(func(cmd string) bool { + return strings.Contains(cmd, "sops --encrypt") + })).Return(nil) - err = bs.InstallCodesphere() - Expect(err).NotTo(HaveOccurred()) + err = bs.EncryptVault() + Expect(err).NotTo(HaveOccurred()) + }) }) - }) - Describe("Invalid cases", func() { - It("fails when download package fails", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - InstallCodesphereVersion: "v1.2.3", - SecretsDir: "/secrets", - } - stlog := bootstrap.NewStepLogger(false) + Describe("Invalid cases", func() { + It("fails when backup vault command fails", func() { + stlog := bootstrap.NewStepLogger(false) - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) - bs.Env.Jumpbox = nm + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) - nm.EXPECT().RunSSHCommand("root", "oms-cli download package v1.2.3", true).Return(fmt.Errorf("download error")) + nodeClient.EXPECT().RunCommand(bs.Env.Jumpbox, "root", mock.MatchedBy(func(cmd string) bool { + return strings.HasPrefix(cmd, "cp ") + })).Return(fmt.Errorf("backup error")) - err = bs.InstallCodesphere() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to download Codesphere package from jumpbox")) - }) + err = bs.EncryptVault() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed backup vault on jumpbox")) + }) - It("fails when install codesphere fails", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - InstallCodesphereVersion: "v1.2.3", - SecretsDir: "/secrets", - } - stlog := bootstrap.NewStepLogger(false) + It("fails when sops encrypt command fails", func() { + stlog := bootstrap.NewStepLogger(false) - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) - bs.Env.Jumpbox = nm + nodeClient.EXPECT().RunCommand(bs.Env.Jumpbox, "root", mock.MatchedBy(func(cmd string) bool { + return strings.HasPrefix(cmd, "cp ") + })).Return(nil) - nm.EXPECT().RunSSHCommand("root", "oms-cli download package v1.2.3", true).Return(nil) - nm.EXPECT().RunSSHCommand("root", "oms-cli install codesphere -c /etc/codesphere/config.yaml -k /secrets/age_key.txt -p v1.2.3.tar.gz", true).Return(fmt.Errorf("install error")) + nodeClient.EXPECT().RunCommand(bs.Env.Jumpbox, "root", mock.MatchedBy(func(cmd string) bool { + return strings.Contains(cmd, "sops --encrypt") + })).Return(fmt.Errorf("encrypt error")) - err = bs.InstallCodesphere() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to install Codesphere from jumpbox")) + err = bs.EncryptVault() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to encrypt vault on jumpbox")) + }) }) }) -}) -var _ = Describe("GenerateK0sConfigScript", func() { - Describe("Valid GenerateK0sConfigScript", func() { - It("generates script", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - PublicGatewayIP: "2.2.2.2", - GatewayIP: "1.1.1.1", - } - stlog := bootstrap.NewStepLogger(false) + Describe("EnsureDNSRecords", func() { + Describe("Valid EnsureDNSRecords", func() { + It("ensures DNS records", func() { + + csEnv = &gcp.CodesphereEnvironment{ + DNSProjectID: "dns-proj", + DNSZoneName: "zone", + BaseDomain: "example.com", + GatewayIP: "1.1.1.1", + PublicGatewayIP: "2.2.2.2", + } + stlog := bootstrap.NewStepLogger(false) + + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) + + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) + + gc.EXPECT().EnsureDNSManagedZone("dns-proj", "zone", "example.com.", mock.Anything).Return(nil) + gc.EXPECT().EnsureDNSRecordSets("dns-proj", "zone", mock.MatchedBy(func(records []*dns.ResourceRecordSet) bool { + // Expect 4 records: *.ws, *.cs, cs, ws + return len(records) == 4 + })).Return(nil) + + err = bs.EnsureDNSRecords() + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Describe("Invalid cases", func() { + It("fails when EnsureDNSManagedZone fails", func() { + + csEnv = &gcp.CodesphereEnvironment{ + DNSProjectID: "dns-proj", + DNSZoneName: "zone", + BaseDomain: "example.com", + GatewayIP: "1.1.1.1", + PublicGatewayIP: "2.2.2.2", + } + stlog := bootstrap.NewStepLogger(false) + + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) + + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) + + gc.EXPECT().EnsureDNSManagedZone("dns-proj", "zone", "example.com.", mock.Anything).Return(fmt.Errorf("zone error")) + + err = bs.EnsureDNSRecords() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to ensure DNS managed zone")) + }) + + It("fails when EnsureDNSRecordSets fails", func() { + + csEnv = &gcp.CodesphereEnvironment{ + DNSProjectID: "dns-proj", + DNSZoneName: "zone", + BaseDomain: "example.com", + GatewayIP: "1.1.1.1", + PublicGatewayIP: "2.2.2.2", + } + stlog := bootstrap.NewStepLogger(false) + + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) + + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) + + gc.EXPECT().EnsureDNSManagedZone("dns-proj", "zone", "example.com.", mock.Anything).Return(nil) + gc.EXPECT().EnsureDNSRecordSets("dns-proj", "zone", mock.Anything).Return(fmt.Errorf("record error")) + + err = bs.EnsureDNSRecords() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to ensure DNS record sets")) + }) + }) + }) - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) + Describe("InstallCodesphere", func() { + BeforeEach(func() { + csEnv.InstallCodesphereVersion = "v1.2.3" + }) + Describe("Valid InstallCodesphere", func() { + It("downloads and installs codesphere", func() { + stlog := bootstrap.NewStepLogger(false) - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) - // Setup required nodes (indices 0, 1, 2 accessed) - bs.Env.ControlPlaneNodes = []node.NodeManager{nm, nm, nm} + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) - nm.EXPECT().GetInternalIP().Return("10.0.0.1").Maybe() + // Expect download package + nodeClient.EXPECT().RunCommand(mock.MatchedBy(jumpbboxMatcher), "root", "oms-cli download package v1.2.3").Return(nil) - fw.EXPECT().WriteFile("configure-k0s.sh", mock.Anything, os.FileMode(0755)).Return(nil) - nm.EXPECT().CopyFile("configure-k0s.sh", "/root/configure-k0s.sh").Return(nil) - nm.EXPECT().RunSSHCommand("root", "chmod +x /root/configure-k0s.sh", true).Return(nil) + // Expect install codesphere + nodeClient.EXPECT().RunCommand(mock.MatchedBy(jumpbboxMatcher), "root", "oms-cli install codesphere -c /etc/codesphere/config.yaml -k /etc/codesphere/secrets/age_key.txt -p v1.2.3.tar.gz").Return(nil) - err = bs.GenerateK0sConfigScript() - Expect(err).NotTo(HaveOccurred()) + err = bs.InstallCodesphere() + Expect(err).NotTo(HaveOccurred()) + }) }) - }) - Describe("Invalid cases", func() { - It("fails when WriteFile fails", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - PublicGatewayIP: "2.2.2.2", - GatewayIP: "1.1.1.1", - } - stlog := bootstrap.NewStepLogger(false) + Describe("Invalid cases", func() { + It("fails when download package fails", func() { + stlog := bootstrap.NewStepLogger(false) - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) - bs.Env.ControlPlaneNodes = []node.NodeManager{nm, nm, nm} + nodeClient.EXPECT().RunCommand(mock.MatchedBy(jumpbboxMatcher), "root", "oms-cli download package v1.2.3").Return(fmt.Errorf("download error")) - nm.EXPECT().GetInternalIP().Return("10.0.0.1").Maybe() + err = bs.InstallCodesphere() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to download Codesphere package from jumpbox")) + }) - fw.EXPECT().WriteFile("configure-k0s.sh", mock.Anything, os.FileMode(0755)).Return(fmt.Errorf("write error")) + It("fails when install codesphere fails", func() { + stlog := bootstrap.NewStepLogger(false) - err = bs.GenerateK0sConfigScript() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to write configure-k0s.sh")) - }) + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) - It("fails when CopyFile fails", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - PublicGatewayIP: "2.2.2.2", - GatewayIP: "1.1.1.1", - } - stlog := bootstrap.NewStepLogger(false) + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) + nodeClient.EXPECT().RunCommand(mock.MatchedBy(jumpbboxMatcher), "root", "oms-cli download package v1.2.3").Return(nil) + nodeClient.EXPECT().RunCommand(mock.MatchedBy(jumpbboxMatcher), "root", "oms-cli install codesphere -c /etc/codesphere/config.yaml -k /etc/codesphere/secrets/age_key.txt -p v1.2.3.tar.gz").Return(fmt.Errorf("install error")) - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) + err = bs.InstallCodesphere() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to install Codesphere from jumpbox")) + }) + }) + }) - bs.Env.ControlPlaneNodes = []node.NodeManager{nm, nm, nm} + Describe("GenerateK0sConfigScript", func() { + Describe("Valid GenerateK0sConfigScript", func() { + It("generates script", func() { + stlog := bootstrap.NewStepLogger(false) - nm.EXPECT().GetInternalIP().Return("10.0.0.1").Maybe() + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) - fw.EXPECT().WriteFile("configure-k0s.sh", mock.Anything, os.FileMode(0755)).Return(nil) - nm.EXPECT().CopyFile("configure-k0s.sh", "/root/configure-k0s.sh").Return(fmt.Errorf("copy error")) + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) + + fw.EXPECT().WriteFile("configure-k0s.sh", mock.Anything, os.FileMode(0755)).Return(nil) + nodeClient.EXPECT().CopyFile(bs.Env.ControlPlaneNodes[0], "configure-k0s.sh", "/root/configure-k0s.sh").Return(nil) + nodeClient.EXPECT().RunCommand(bs.Env.ControlPlaneNodes[0], "root", "chmod +x /root/configure-k0s.sh").Return(nil) - err = bs.GenerateK0sConfigScript() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to copy configure-k0s.sh to control plane node")) + err = bs.GenerateK0sConfigScript() + Expect(err).NotTo(HaveOccurred()) + }) }) - It("fails when RunSSHCommand chmod fails", func() { - env := env.NewEnv() - ctx := context.Background() - csEnv := &gcp.CodesphereEnvironment{ - PublicGatewayIP: "2.2.2.2", - GatewayIP: "1.1.1.1", - } - stlog := bootstrap.NewStepLogger(false) + Describe("Invalid cases", func() { + It("fails when WriteFile fails", func() { + stlog := bootstrap.NewStepLogger(false) - icg := installer.NewMockInstallConfigManager(GinkgoT()) - gc := gcp.NewMockGCPClientManager(GinkgoT()) - fw := util.NewMockFileIO(GinkgoT()) - nm := node.NewMockNodeManager(GinkgoT()) + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) - bs, err := gcp.NewGCPBootstrapper(ctx, env, stlog, csEnv, icg, gc, nm, fw) - Expect(err).NotTo(HaveOccurred()) + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) - bs.Env.ControlPlaneNodes = []node.NodeManager{nm, nm, nm} + fw.EXPECT().WriteFile("configure-k0s.sh", mock.Anything, os.FileMode(0755)).Return(fmt.Errorf("write error")) - nm.EXPECT().GetInternalIP().Return("10.0.0.1").Maybe() + err = bs.GenerateK0sConfigScript() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to write configure-k0s.sh")) + }) - fw.EXPECT().WriteFile("configure-k0s.sh", mock.Anything, os.FileMode(0755)).Return(nil) - nm.EXPECT().CopyFile("configure-k0s.sh", "/root/configure-k0s.sh").Return(nil) - nm.EXPECT().RunSSHCommand("root", "chmod +x /root/configure-k0s.sh", true).Return(fmt.Errorf("chmod error")) + It("fails when CopyFile fails", func() { + stlog := bootstrap.NewStepLogger(false) + + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) + + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) + + fw.EXPECT().WriteFile("configure-k0s.sh", mock.Anything, os.FileMode(0755)).Return(nil) + nodeClient.EXPECT().CopyFile(mock.Anything, "configure-k0s.sh", "/root/configure-k0s.sh").Return(fmt.Errorf("copy error")) + + err = bs.GenerateK0sConfigScript() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to copy configure-k0s.sh to control plane node")) + }) + + It("fails when RunSSHCommand chmod fails", func() { - err = bs.GenerateK0sConfigScript() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to make configure-k0s.sh executable")) + stlog := bootstrap.NewStepLogger(false) + + icg := installer.NewMockInstallConfigManager(GinkgoT()) + gc := gcp.NewMockGCPClientManager(GinkgoT()) + fw := util.NewMockFileIO(GinkgoT()) + + bs, err := gcp.NewGCPBootstrapper(ctx, e, stlog, csEnv, icg, gc, fw, nodeClient) + Expect(err).NotTo(HaveOccurred()) + + fw.EXPECT().WriteFile("configure-k0s.sh", mock.Anything, os.FileMode(0755)).Return(nil) + + nodeClient.EXPECT().CopyFile(bs.Env.ControlPlaneNodes[0], "configure-k0s.sh", "/root/configure-k0s.sh").Return(nil) + nodeClient.EXPECT().RunCommand(bs.Env.ControlPlaneNodes[0], "root", "chmod +x /root/configure-k0s.sh").Return(fmt.Errorf("chmod error")) + + err = bs.GenerateK0sConfigScript() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to make configure-k0s.sh executable")) + }) }) }) + }) + +func fakeNode(name string, commandRunner node.NodeClient) *node.Node { + return &node.Node{ + Name: name, + ExternalIP: "1.2.3.4", + InternalIP: "10.0.0.1", + + NodeClient: commandRunner, + } +} diff --git a/internal/installer/node/mocks.go b/internal/installer/node/mocks.go index 6b28e4fb..f88c2503 100644 --- a/internal/installer/node/mocks.go +++ b/internal/installer/node/mocks.go @@ -9,13 +9,13 @@ import ( "time" ) -// NewMockNodeManager creates a new instance of MockNodeManager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// NewMockNodeClient creates a new instance of MockNodeClient. 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 NewMockNodeManager(t interface { +func NewMockNodeClient(t interface { mock.TestingT Cleanup(func()) -}) *MockNodeManager { - mock := &MockNodeManager{} +}) *MockNodeClient { + mock := &MockNodeClient{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) @@ -23,245 +23,54 @@ func NewMockNodeManager(t interface { return mock } -// MockNodeManager is an autogenerated mock type for the NodeManager type -type MockNodeManager struct { +// MockNodeClient is an autogenerated mock type for the NodeClient type +type MockNodeClient struct { mock.Mock } -type MockNodeManager_Expecter struct { +type MockNodeClient_Expecter struct { mock *mock.Mock } -func (_m *MockNodeManager) EXPECT() *MockNodeManager_Expecter { - return &MockNodeManager_Expecter{mock: &_m.Mock} +func (_m *MockNodeClient) EXPECT() *MockNodeClient_Expecter { + return &MockNodeClient_Expecter{mock: &_m.Mock} } -// ConfigureAcceptEnv provides a mock function for the type MockNodeManager -func (_mock *MockNodeManager) ConfigureAcceptEnv() error { - ret := _mock.Called() - - if len(ret) == 0 { - panic("no return value specified for ConfigureAcceptEnv") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func() error); ok { - r0 = returnFunc() - } else { - r0 = ret.Error(0) - } - return r0 -} - -// MockNodeManager_ConfigureAcceptEnv_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ConfigureAcceptEnv' -type MockNodeManager_ConfigureAcceptEnv_Call struct { - *mock.Call -} - -// ConfigureAcceptEnv is a helper method to define mock.On call -func (_e *MockNodeManager_Expecter) ConfigureAcceptEnv() *MockNodeManager_ConfigureAcceptEnv_Call { - return &MockNodeManager_ConfigureAcceptEnv_Call{Call: _e.mock.On("ConfigureAcceptEnv")} -} - -func (_c *MockNodeManager_ConfigureAcceptEnv_Call) Run(run func()) *MockNodeManager_ConfigureAcceptEnv_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *MockNodeManager_ConfigureAcceptEnv_Call) Return(err error) *MockNodeManager_ConfigureAcceptEnv_Call { - _c.Call.Return(err) - return _c -} - -func (_c *MockNodeManager_ConfigureAcceptEnv_Call) RunAndReturn(run func() error) *MockNodeManager_ConfigureAcceptEnv_Call { - _c.Call.Return(run) - return _c -} - -// ConfigureInotifyWatches provides a mock function for the type MockNodeManager -func (_mock *MockNodeManager) ConfigureInotifyWatches() error { - ret := _mock.Called() - - if len(ret) == 0 { - panic("no return value specified for ConfigureInotifyWatches") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func() error); ok { - r0 = returnFunc() - } else { - r0 = ret.Error(0) - } - return r0 -} - -// MockNodeManager_ConfigureInotifyWatches_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ConfigureInotifyWatches' -type MockNodeManager_ConfigureInotifyWatches_Call struct { - *mock.Call -} - -// ConfigureInotifyWatches is a helper method to define mock.On call -func (_e *MockNodeManager_Expecter) ConfigureInotifyWatches() *MockNodeManager_ConfigureInotifyWatches_Call { - return &MockNodeManager_ConfigureInotifyWatches_Call{Call: _e.mock.On("ConfigureInotifyWatches")} -} - -func (_c *MockNodeManager_ConfigureInotifyWatches_Call) Run(run func()) *MockNodeManager_ConfigureInotifyWatches_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *MockNodeManager_ConfigureInotifyWatches_Call) Return(err error) *MockNodeManager_ConfigureInotifyWatches_Call { - _c.Call.Return(err) - return _c -} - -func (_c *MockNodeManager_ConfigureInotifyWatches_Call) RunAndReturn(run func() error) *MockNodeManager_ConfigureInotifyWatches_Call { - _c.Call.Return(run) - return _c -} - -// ConfigureMemoryMap provides a mock function for the type MockNodeManager -func (_mock *MockNodeManager) ConfigureMemoryMap() error { - ret := _mock.Called() - - if len(ret) == 0 { - panic("no return value specified for ConfigureMemoryMap") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func() error); ok { - r0 = returnFunc() - } else { - r0 = ret.Error(0) - } - return r0 -} - -// MockNodeManager_ConfigureMemoryMap_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ConfigureMemoryMap' -type MockNodeManager_ConfigureMemoryMap_Call struct { - *mock.Call -} - -// ConfigureMemoryMap is a helper method to define mock.On call -func (_e *MockNodeManager_Expecter) ConfigureMemoryMap() *MockNodeManager_ConfigureMemoryMap_Call { - return &MockNodeManager_ConfigureMemoryMap_Call{Call: _e.mock.On("ConfigureMemoryMap")} -} - -func (_c *MockNodeManager_ConfigureMemoryMap_Call) Run(run func()) *MockNodeManager_ConfigureMemoryMap_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *MockNodeManager_ConfigureMemoryMap_Call) Return(err error) *MockNodeManager_ConfigureMemoryMap_Call { - _c.Call.Return(err) - return _c -} - -func (_c *MockNodeManager_ConfigureMemoryMap_Call) RunAndReturn(run func() error) *MockNodeManager_ConfigureMemoryMap_Call { - _c.Call.Return(run) - return _c -} - -// CopyFile provides a mock function for the type MockNodeManager -func (_mock *MockNodeManager) CopyFile(src string, dst string) error { - ret := _mock.Called(src, dst) +// CopyFile provides a mock function for the type MockNodeClient +func (_mock *MockNodeClient) CopyFile(n *Node, src string, dst string) error { + ret := _mock.Called(n, src, dst) if len(ret) == 0 { panic("no return value specified for CopyFile") } var r0 error - if returnFunc, ok := ret.Get(0).(func(string, string) error); ok { - r0 = returnFunc(src, dst) + if returnFunc, ok := ret.Get(0).(func(*Node, string, string) error); ok { + r0 = returnFunc(n, src, dst) } else { r0 = ret.Error(0) } return r0 } -// MockNodeManager_CopyFile_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CopyFile' -type MockNodeManager_CopyFile_Call struct { +// MockNodeClient_CopyFile_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CopyFile' +type MockNodeClient_CopyFile_Call struct { *mock.Call } // CopyFile is a helper method to define mock.On call +// - n *Node // - src string // - dst string -func (_e *MockNodeManager_Expecter) CopyFile(src interface{}, dst interface{}) *MockNodeManager_CopyFile_Call { - return &MockNodeManager_CopyFile_Call{Call: _e.mock.On("CopyFile", src, dst)} +func (_e *MockNodeClient_Expecter) CopyFile(n interface{}, src interface{}, dst interface{}) *MockNodeClient_CopyFile_Call { + return &MockNodeClient_CopyFile_Call{Call: _e.mock.On("CopyFile", n, src, dst)} } -func (_c *MockNodeManager_CopyFile_Call) Run(run func(src string, dst string)) *MockNodeManager_CopyFile_Call { +func (_c *MockNodeClient_CopyFile_Call) Run(run func(n *Node, src string, dst string)) *MockNodeClient_CopyFile_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 string + var arg0 *Node 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 *MockNodeManager_CopyFile_Call) Return(err error) *MockNodeManager_CopyFile_Call { - _c.Call.Return(err) - return _c -} - -func (_c *MockNodeManager_CopyFile_Call) RunAndReturn(run func(src string, dst string) error) *MockNodeManager_CopyFile_Call { - _c.Call.Return(run) - return _c -} - -// CreateSubNode provides a mock function for the type MockNodeManager -func (_mock *MockNodeManager) CreateSubNode(name string, externalIP string, internalIP string) NodeManager { - ret := _mock.Called(name, externalIP, internalIP) - - if len(ret) == 0 { - panic("no return value specified for CreateSubNode") - } - - var r0 NodeManager - if returnFunc, ok := ret.Get(0).(func(string, string, string) NodeManager); ok { - r0 = returnFunc(name, externalIP, internalIP) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(NodeManager) - } - } - return r0 -} - -// MockNodeManager_CreateSubNode_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateSubNode' -type MockNodeManager_CreateSubNode_Call struct { - *mock.Call -} - -// CreateSubNode is a helper method to define mock.On call -// - name string -// - externalIP string -// - internalIP string -func (_e *MockNodeManager_Expecter) CreateSubNode(name interface{}, externalIP interface{}, internalIP interface{}) *MockNodeManager_CreateSubNode_Call { - return &MockNodeManager_CreateSubNode_Call{Call: _e.mock.On("CreateSubNode", name, externalIP, internalIP)} -} - -func (_c *MockNodeManager_CreateSubNode_Call) Run(run func(name string, externalIP string, internalIP string)) *MockNodeManager_CreateSubNode_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 string - if args[0] != nil { - arg0 = args[0].(string) + arg0 = args[0].(*Node) } var arg1 string if args[1] != nil { @@ -280,601 +89,108 @@ func (_c *MockNodeManager_CreateSubNode_Call) Run(run func(name string, external return _c } -func (_c *MockNodeManager_CreateSubNode_Call) Return(nodeManager NodeManager) *MockNodeManager_CreateSubNode_Call { - _c.Call.Return(nodeManager) - return _c -} - -func (_c *MockNodeManager_CreateSubNode_Call) RunAndReturn(run func(name string, externalIP string, internalIP string) NodeManager) *MockNodeManager_CreateSubNode_Call { - _c.Call.Return(run) - return _c -} - -// EnableRootLogin provides a mock function for the type MockNodeManager -func (_mock *MockNodeManager) EnableRootLogin() error { - ret := _mock.Called() - - if len(ret) == 0 { - panic("no return value specified for EnableRootLogin") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func() error); ok { - r0 = returnFunc() - } else { - r0 = ret.Error(0) - } - return r0 -} - -// MockNodeManager_EnableRootLogin_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EnableRootLogin' -type MockNodeManager_EnableRootLogin_Call struct { - *mock.Call -} - -// EnableRootLogin is a helper method to define mock.On call -func (_e *MockNodeManager_Expecter) EnableRootLogin() *MockNodeManager_EnableRootLogin_Call { - return &MockNodeManager_EnableRootLogin_Call{Call: _e.mock.On("EnableRootLogin")} -} - -func (_c *MockNodeManager_EnableRootLogin_Call) Run(run func()) *MockNodeManager_EnableRootLogin_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *MockNodeManager_EnableRootLogin_Call) Return(err error) *MockNodeManager_EnableRootLogin_Call { +func (_c *MockNodeClient_CopyFile_Call) Return(err error) *MockNodeClient_CopyFile_Call { _c.Call.Return(err) return _c } -func (_c *MockNodeManager_EnableRootLogin_Call) RunAndReturn(run func() error) *MockNodeManager_EnableRootLogin_Call { - _c.Call.Return(run) - return _c -} - -// GetExternalIP provides a mock function for the type MockNodeManager -func (_mock *MockNodeManager) GetExternalIP() string { - ret := _mock.Called() - - if len(ret) == 0 { - panic("no return value specified for GetExternalIP") - } - - var r0 string - if returnFunc, ok := ret.Get(0).(func() string); ok { - r0 = returnFunc() - } else { - r0 = ret.Get(0).(string) - } - return r0 -} - -// MockNodeManager_GetExternalIP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetExternalIP' -type MockNodeManager_GetExternalIP_Call struct { - *mock.Call -} - -// GetExternalIP is a helper method to define mock.On call -func (_e *MockNodeManager_Expecter) GetExternalIP() *MockNodeManager_GetExternalIP_Call { - return &MockNodeManager_GetExternalIP_Call{Call: _e.mock.On("GetExternalIP")} -} - -func (_c *MockNodeManager_GetExternalIP_Call) Run(run func()) *MockNodeManager_GetExternalIP_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *MockNodeManager_GetExternalIP_Call) Return(s string) *MockNodeManager_GetExternalIP_Call { - _c.Call.Return(s) - return _c -} - -func (_c *MockNodeManager_GetExternalIP_Call) RunAndReturn(run func() string) *MockNodeManager_GetExternalIP_Call { - _c.Call.Return(run) - return _c -} - -// GetInternalIP provides a mock function for the type MockNodeManager -func (_mock *MockNodeManager) GetInternalIP() string { - ret := _mock.Called() - - if len(ret) == 0 { - panic("no return value specified for GetInternalIP") - } - - var r0 string - if returnFunc, ok := ret.Get(0).(func() string); ok { - r0 = returnFunc() - } else { - r0 = ret.Get(0).(string) - } - return r0 -} - -// MockNodeManager_GetInternalIP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetInternalIP' -type MockNodeManager_GetInternalIP_Call struct { - *mock.Call -} - -// GetInternalIP is a helper method to define mock.On call -func (_e *MockNodeManager_Expecter) GetInternalIP() *MockNodeManager_GetInternalIP_Call { - return &MockNodeManager_GetInternalIP_Call{Call: _e.mock.On("GetInternalIP")} -} - -func (_c *MockNodeManager_GetInternalIP_Call) Run(run func()) *MockNodeManager_GetInternalIP_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *MockNodeManager_GetInternalIP_Call) Return(s string) *MockNodeManager_GetInternalIP_Call { - _c.Call.Return(s) - return _c -} - -func (_c *MockNodeManager_GetInternalIP_Call) RunAndReturn(run func() string) *MockNodeManager_GetInternalIP_Call { - _c.Call.Return(run) - return _c -} - -// GetName provides a mock function for the type MockNodeManager -func (_mock *MockNodeManager) GetName() string { - ret := _mock.Called() - - if len(ret) == 0 { - panic("no return value specified for GetName") - } - - var r0 string - if returnFunc, ok := ret.Get(0).(func() string); ok { - r0 = returnFunc() - } else { - r0 = ret.Get(0).(string) - } - return r0 -} - -// MockNodeManager_GetName_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetName' -type MockNodeManager_GetName_Call struct { - *mock.Call -} - -// GetName is a helper method to define mock.On call -func (_e *MockNodeManager_Expecter) GetName() *MockNodeManager_GetName_Call { - return &MockNodeManager_GetName_Call{Call: _e.mock.On("GetName")} -} - -func (_c *MockNodeManager_GetName_Call) Run(run func()) *MockNodeManager_GetName_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *MockNodeManager_GetName_Call) Return(s string) *MockNodeManager_GetName_Call { - _c.Call.Return(s) - return _c -} - -func (_c *MockNodeManager_GetName_Call) RunAndReturn(run func() string) *MockNodeManager_GetName_Call { - _c.Call.Return(run) - return _c -} - -// HasAcceptEnvConfigured provides a mock function for the type MockNodeManager -func (_mock *MockNodeManager) HasAcceptEnvConfigured() bool { - ret := _mock.Called() - - if len(ret) == 0 { - panic("no return value specified for HasAcceptEnvConfigured") - } - - var r0 bool - if returnFunc, ok := ret.Get(0).(func() bool); ok { - r0 = returnFunc() - } else { - r0 = ret.Get(0).(bool) - } - return r0 -} - -// MockNodeManager_HasAcceptEnvConfigured_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'HasAcceptEnvConfigured' -type MockNodeManager_HasAcceptEnvConfigured_Call struct { - *mock.Call -} - -// HasAcceptEnvConfigured is a helper method to define mock.On call -func (_e *MockNodeManager_Expecter) HasAcceptEnvConfigured() *MockNodeManager_HasAcceptEnvConfigured_Call { - return &MockNodeManager_HasAcceptEnvConfigured_Call{Call: _e.mock.On("HasAcceptEnvConfigured")} -} - -func (_c *MockNodeManager_HasAcceptEnvConfigured_Call) Run(run func()) *MockNodeManager_HasAcceptEnvConfigured_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *MockNodeManager_HasAcceptEnvConfigured_Call) Return(b bool) *MockNodeManager_HasAcceptEnvConfigured_Call { - _c.Call.Return(b) - return _c -} - -func (_c *MockNodeManager_HasAcceptEnvConfigured_Call) RunAndReturn(run func() bool) *MockNodeManager_HasAcceptEnvConfigured_Call { - _c.Call.Return(run) - return _c -} - -// HasCommand provides a mock function for the type MockNodeManager -func (_mock *MockNodeManager) HasCommand(command string) bool { - ret := _mock.Called(command) - - if len(ret) == 0 { - panic("no return value specified for HasCommand") - } - - var r0 bool - if returnFunc, ok := ret.Get(0).(func(string) bool); ok { - r0 = returnFunc(command) - } else { - r0 = ret.Get(0).(bool) - } - return r0 -} - -// MockNodeManager_HasCommand_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'HasCommand' -type MockNodeManager_HasCommand_Call struct { - *mock.Call -} - -// HasCommand is a helper method to define mock.On call -// - command string -func (_e *MockNodeManager_Expecter) HasCommand(command interface{}) *MockNodeManager_HasCommand_Call { - return &MockNodeManager_HasCommand_Call{Call: _e.mock.On("HasCommand", command)} -} - -func (_c *MockNodeManager_HasCommand_Call) Run(run func(command string)) *MockNodeManager_HasCommand_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 string - if args[0] != nil { - arg0 = args[0].(string) - } - run( - arg0, - ) - }) - return _c -} - -func (_c *MockNodeManager_HasCommand_Call) Return(b bool) *MockNodeManager_HasCommand_Call { - _c.Call.Return(b) - return _c -} - -func (_c *MockNodeManager_HasCommand_Call) RunAndReturn(run func(command string) bool) *MockNodeManager_HasCommand_Call { +func (_c *MockNodeClient_CopyFile_Call) RunAndReturn(run func(n *Node, src string, dst string) error) *MockNodeClient_CopyFile_Call { _c.Call.Return(run) return _c } -// HasFile provides a mock function for the type MockNodeManager -func (_mock *MockNodeManager) HasFile(filePath string) bool { - ret := _mock.Called(filePath) +// HasFile provides a mock function for the type MockNodeClient +func (_mock *MockNodeClient) HasFile(n *Node, filePath string) bool { + ret := _mock.Called(n, filePath) if len(ret) == 0 { panic("no return value specified for HasFile") } var r0 bool - if returnFunc, ok := ret.Get(0).(func(string) bool); ok { - r0 = returnFunc(filePath) + if returnFunc, ok := ret.Get(0).(func(*Node, string) bool); ok { + r0 = returnFunc(n, filePath) } else { r0 = ret.Get(0).(bool) } return r0 } -// MockNodeManager_HasFile_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'HasFile' -type MockNodeManager_HasFile_Call struct { +// MockNodeClient_HasFile_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'HasFile' +type MockNodeClient_HasFile_Call struct { *mock.Call } // HasFile is a helper method to define mock.On call +// - n *Node // - filePath string -func (_e *MockNodeManager_Expecter) HasFile(filePath interface{}) *MockNodeManager_HasFile_Call { - return &MockNodeManager_HasFile_Call{Call: _e.mock.On("HasFile", filePath)} +func (_e *MockNodeClient_Expecter) HasFile(n interface{}, filePath interface{}) *MockNodeClient_HasFile_Call { + return &MockNodeClient_HasFile_Call{Call: _e.mock.On("HasFile", n, filePath)} } -func (_c *MockNodeManager_HasFile_Call) Run(run func(filePath string)) *MockNodeManager_HasFile_Call { +func (_c *MockNodeClient_HasFile_Call) Run(run func(n *Node, filePath string)) *MockNodeClient_HasFile_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 string + var arg0 *Node if args[0] != nil { - arg0 = args[0].(string) + arg0 = args[0].(*Node) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) } run( arg0, + arg1, ) }) return _c } -func (_c *MockNodeManager_HasFile_Call) Return(b bool) *MockNodeManager_HasFile_Call { +func (_c *MockNodeClient_HasFile_Call) Return(b bool) *MockNodeClient_HasFile_Call { _c.Call.Return(b) return _c } -func (_c *MockNodeManager_HasFile_Call) RunAndReturn(run func(filePath string) bool) *MockNodeManager_HasFile_Call { +func (_c *MockNodeClient_HasFile_Call) RunAndReturn(run func(n *Node, filePath string) bool) *MockNodeClient_HasFile_Call { _c.Call.Return(run) return _c } -// HasInotifyWatchesConfigured provides a mock function for the type MockNodeManager -func (_mock *MockNodeManager) HasInotifyWatchesConfigured() bool { - ret := _mock.Called() +// RunCommand provides a mock function for the type MockNodeClient +func (_mock *MockNodeClient) RunCommand(n *Node, username string, command string) error { + ret := _mock.Called(n, username, command) if len(ret) == 0 { - panic("no return value specified for HasInotifyWatchesConfigured") - } - - var r0 bool - if returnFunc, ok := ret.Get(0).(func() bool); ok { - r0 = returnFunc() - } else { - r0 = ret.Get(0).(bool) - } - return r0 -} - -// MockNodeManager_HasInotifyWatchesConfigured_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'HasInotifyWatchesConfigured' -type MockNodeManager_HasInotifyWatchesConfigured_Call struct { - *mock.Call -} - -// HasInotifyWatchesConfigured is a helper method to define mock.On call -func (_e *MockNodeManager_Expecter) HasInotifyWatchesConfigured() *MockNodeManager_HasInotifyWatchesConfigured_Call { - return &MockNodeManager_HasInotifyWatchesConfigured_Call{Call: _e.mock.On("HasInotifyWatchesConfigured")} -} - -func (_c *MockNodeManager_HasInotifyWatchesConfigured_Call) Run(run func()) *MockNodeManager_HasInotifyWatchesConfigured_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *MockNodeManager_HasInotifyWatchesConfigured_Call) Return(b bool) *MockNodeManager_HasInotifyWatchesConfigured_Call { - _c.Call.Return(b) - return _c -} - -func (_c *MockNodeManager_HasInotifyWatchesConfigured_Call) RunAndReturn(run func() bool) *MockNodeManager_HasInotifyWatchesConfigured_Call { - _c.Call.Return(run) - return _c -} - -// HasMemoryMapConfigured provides a mock function for the type MockNodeManager -func (_mock *MockNodeManager) HasMemoryMapConfigured() bool { - ret := _mock.Called() - - if len(ret) == 0 { - panic("no return value specified for HasMemoryMapConfigured") - } - - var r0 bool - if returnFunc, ok := ret.Get(0).(func() bool); ok { - r0 = returnFunc() - } else { - r0 = ret.Get(0).(bool) - } - return r0 -} - -// MockNodeManager_HasMemoryMapConfigured_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'HasMemoryMapConfigured' -type MockNodeManager_HasMemoryMapConfigured_Call struct { - *mock.Call -} - -// HasMemoryMapConfigured is a helper method to define mock.On call -func (_e *MockNodeManager_Expecter) HasMemoryMapConfigured() *MockNodeManager_HasMemoryMapConfigured_Call { - return &MockNodeManager_HasMemoryMapConfigured_Call{Call: _e.mock.On("HasMemoryMapConfigured")} -} - -func (_c *MockNodeManager_HasMemoryMapConfigured_Call) Run(run func()) *MockNodeManager_HasMemoryMapConfigured_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *MockNodeManager_HasMemoryMapConfigured_Call) Return(b bool) *MockNodeManager_HasMemoryMapConfigured_Call { - _c.Call.Return(b) - return _c -} - -func (_c *MockNodeManager_HasMemoryMapConfigured_Call) RunAndReturn(run func() bool) *MockNodeManager_HasMemoryMapConfigured_Call { - _c.Call.Return(run) - return _c -} - -// HasRootLoginEnabled provides a mock function for the type MockNodeManager -func (_mock *MockNodeManager) HasRootLoginEnabled() bool { - ret := _mock.Called() - - if len(ret) == 0 { - panic("no return value specified for HasRootLoginEnabled") - } - - var r0 bool - if returnFunc, ok := ret.Get(0).(func() bool); ok { - r0 = returnFunc() - } else { - r0 = ret.Get(0).(bool) - } - return r0 -} - -// MockNodeManager_HasRootLoginEnabled_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'HasRootLoginEnabled' -type MockNodeManager_HasRootLoginEnabled_Call struct { - *mock.Call -} - -// HasRootLoginEnabled is a helper method to define mock.On call -func (_e *MockNodeManager_Expecter) HasRootLoginEnabled() *MockNodeManager_HasRootLoginEnabled_Call { - return &MockNodeManager_HasRootLoginEnabled_Call{Call: _e.mock.On("HasRootLoginEnabled")} -} - -func (_c *MockNodeManager_HasRootLoginEnabled_Call) Run(run func()) *MockNodeManager_HasRootLoginEnabled_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *MockNodeManager_HasRootLoginEnabled_Call) Return(b bool) *MockNodeManager_HasRootLoginEnabled_Call { - _c.Call.Return(b) - return _c -} - -func (_c *MockNodeManager_HasRootLoginEnabled_Call) RunAndReturn(run func() bool) *MockNodeManager_HasRootLoginEnabled_Call { - _c.Call.Return(run) - return _c -} - -// InstallOms provides a mock function for the type MockNodeManager -func (_mock *MockNodeManager) InstallOms() error { - ret := _mock.Called() - - if len(ret) == 0 { - panic("no return value specified for InstallOms") + panic("no return value specified for RunCommand") } var r0 error - if returnFunc, ok := ret.Get(0).(func() error); ok { - r0 = returnFunc() + if returnFunc, ok := ret.Get(0).(func(*Node, string, string) error); ok { + r0 = returnFunc(n, username, command) } else { r0 = ret.Error(0) } return r0 } -// MockNodeManager_InstallOms_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'InstallOms' -type MockNodeManager_InstallOms_Call struct { +// MockNodeClient_RunCommand_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RunCommand' +type MockNodeClient_RunCommand_Call struct { *mock.Call } -// InstallOms is a helper method to define mock.On call -func (_e *MockNodeManager_Expecter) InstallOms() *MockNodeManager_InstallOms_Call { - return &MockNodeManager_InstallOms_Call{Call: _e.mock.On("InstallOms")} -} - -func (_c *MockNodeManager_InstallOms_Call) Run(run func()) *MockNodeManager_InstallOms_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *MockNodeManager_InstallOms_Call) Return(err error) *MockNodeManager_InstallOms_Call { - _c.Call.Return(err) - return _c -} - -func (_c *MockNodeManager_InstallOms_Call) RunAndReturn(run func() error) *MockNodeManager_InstallOms_Call { - _c.Call.Return(run) - return _c -} - -// RunSSHCommand provides a mock function for the type MockNodeManager -func (_mock *MockNodeManager) RunSSHCommand(username string, command string, quiet bool) error { - ret := _mock.Called(username, command, quiet) - - if len(ret) == 0 { - panic("no return value specified for RunSSHCommand") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func(string, string, bool) error); ok { - r0 = returnFunc(username, command, quiet) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// MockNodeManager_RunSSHCommand_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RunSSHCommand' -type MockNodeManager_RunSSHCommand_Call struct { - *mock.Call -} - -// RunSSHCommand is a helper method to define mock.On call +// RunCommand is a helper method to define mock.On call +// - n *Node // - username string // - command string -// - quiet bool -func (_e *MockNodeManager_Expecter) RunSSHCommand(username interface{}, command interface{}, quiet interface{}) *MockNodeManager_RunSSHCommand_Call { - return &MockNodeManager_RunSSHCommand_Call{Call: _e.mock.On("RunSSHCommand", username, command, quiet)} -} - -func (_c *MockNodeManager_RunSSHCommand_Call) Run(run func(username string, command string, quiet bool)) *MockNodeManager_RunSSHCommand_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 bool - if args[2] != nil { - arg2 = args[2].(bool) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *MockNodeManager_RunSSHCommand_Call) Return(err error) *MockNodeManager_RunSSHCommand_Call { - _c.Call.Return(err) - return _c -} - -func (_c *MockNodeManager_RunSSHCommand_Call) RunAndReturn(run func(username string, command string, quiet bool) error) *MockNodeManager_RunSSHCommand_Call { - _c.Call.Return(run) - return _c -} - -// UpdateNode provides a mock function for the type MockNodeManager -func (_mock *MockNodeManager) UpdateNode(name string, externalIP string, internalIP string) { - _mock.Called(name, externalIP, internalIP) - return -} - -// MockNodeManager_UpdateNode_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateNode' -type MockNodeManager_UpdateNode_Call struct { - *mock.Call +func (_e *MockNodeClient_Expecter) RunCommand(n interface{}, username interface{}, command interface{}) *MockNodeClient_RunCommand_Call { + return &MockNodeClient_RunCommand_Call{Call: _e.mock.On("RunCommand", n, username, command)} } -// UpdateNode is a helper method to define mock.On call -// - name string -// - externalIP string -// - internalIP string -func (_e *MockNodeManager_Expecter) UpdateNode(name interface{}, externalIP interface{}, internalIP interface{}) *MockNodeManager_UpdateNode_Call { - return &MockNodeManager_UpdateNode_Call{Call: _e.mock.On("UpdateNode", name, externalIP, internalIP)} -} - -func (_c *MockNodeManager_UpdateNode_Call) Run(run func(name string, externalIP string, internalIP string)) *MockNodeManager_UpdateNode_Call { +func (_c *MockNodeClient_RunCommand_Call) Run(run func(n *Node, username string, command string)) *MockNodeClient_RunCommand_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 string + var arg0 *Node if args[0] != nil { - arg0 = args[0].(string) + arg0 = args[0].(*Node) } var arg1 string if args[1] != nil { @@ -893,63 +209,69 @@ func (_c *MockNodeManager_UpdateNode_Call) Run(run func(name string, externalIP return _c } -func (_c *MockNodeManager_UpdateNode_Call) Return() *MockNodeManager_UpdateNode_Call { - _c.Call.Return() +func (_c *MockNodeClient_RunCommand_Call) Return(err error) *MockNodeClient_RunCommand_Call { + _c.Call.Return(err) return _c } -func (_c *MockNodeManager_UpdateNode_Call) RunAndReturn(run func(name string, externalIP string, internalIP string)) *MockNodeManager_UpdateNode_Call { - _c.Run(run) +func (_c *MockNodeClient_RunCommand_Call) RunAndReturn(run func(n *Node, username string, command string) error) *MockNodeClient_RunCommand_Call { + _c.Call.Return(run) return _c } -// WaitForSSH provides a mock function for the type MockNodeManager -func (_mock *MockNodeManager) WaitForSSH(timeout time.Duration) error { - ret := _mock.Called(timeout) +// WaitReady provides a mock function for the type MockNodeClient +func (_mock *MockNodeClient) WaitReady(n *Node, timeout time.Duration) error { + ret := _mock.Called(n, timeout) if len(ret) == 0 { - panic("no return value specified for WaitForSSH") + panic("no return value specified for WaitReady") } var r0 error - if returnFunc, ok := ret.Get(0).(func(time.Duration) error); ok { - r0 = returnFunc(timeout) + if returnFunc, ok := ret.Get(0).(func(*Node, time.Duration) error); ok { + r0 = returnFunc(n, timeout) } else { r0 = ret.Error(0) } return r0 } -// MockNodeManager_WaitForSSH_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WaitForSSH' -type MockNodeManager_WaitForSSH_Call struct { +// MockNodeClient_WaitReady_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WaitReady' +type MockNodeClient_WaitReady_Call struct { *mock.Call } -// WaitForSSH is a helper method to define mock.On call +// WaitReady is a helper method to define mock.On call +// - n *Node // - timeout time.Duration -func (_e *MockNodeManager_Expecter) WaitForSSH(timeout interface{}) *MockNodeManager_WaitForSSH_Call { - return &MockNodeManager_WaitForSSH_Call{Call: _e.mock.On("WaitForSSH", timeout)} +func (_e *MockNodeClient_Expecter) WaitReady(n interface{}, timeout interface{}) *MockNodeClient_WaitReady_Call { + return &MockNodeClient_WaitReady_Call{Call: _e.mock.On("WaitReady", n, timeout)} } -func (_c *MockNodeManager_WaitForSSH_Call) Run(run func(timeout time.Duration)) *MockNodeManager_WaitForSSH_Call { +func (_c *MockNodeClient_WaitReady_Call) Run(run func(n *Node, timeout time.Duration)) *MockNodeClient_WaitReady_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 time.Duration + var arg0 *Node if args[0] != nil { - arg0 = args[0].(time.Duration) + arg0 = args[0].(*Node) + } + var arg1 time.Duration + if args[1] != nil { + arg1 = args[1].(time.Duration) } run( arg0, + arg1, ) }) return _c } -func (_c *MockNodeManager_WaitForSSH_Call) Return(err error) *MockNodeManager_WaitForSSH_Call { +func (_c *MockNodeClient_WaitReady_Call) Return(err error) *MockNodeClient_WaitReady_Call { _c.Call.Return(err) return _c } -func (_c *MockNodeManager_WaitForSSH_Call) RunAndReturn(run func(timeout time.Duration) error) *MockNodeManager_WaitForSSH_Call { +func (_c *MockNodeClient_WaitReady_Call) RunAndReturn(run func(n *Node, timeout time.Duration) error) *MockNodeClient_WaitReady_Call { _c.Call.Return(run) return _c } diff --git a/internal/installer/node/node.go b/internal/installer/node/node.go index 8223416f..3c80ed2b 100644 --- a/internal/installer/node/node.go +++ b/internal/installer/node/node.go @@ -20,35 +20,6 @@ import ( "golang.org/x/term" ) -type NodeManager interface { - // Node - CreateSubNode(name string, externalIP string, internalIP string) NodeManager - UpdateNode(name string, externalIP string, internalIP string) - GetExternalIP() string - GetInternalIP() string - GetName() string - // SSH - WaitForSSH(timeout time.Duration) error - RunSSHCommand(username string, command string, quiet bool) error - // OMS - HasCommand(command string) bool - InstallOms() error - // AcceptEnv - HasAcceptEnvConfigured() bool - ConfigureAcceptEnv() error - // Root Login - HasRootLoginEnabled() bool - EnableRootLogin() error - // Host Config - HasInotifyWatchesConfigured() bool - ConfigureInotifyWatches() error - HasMemoryMapConfigured() bool - ConfigureMemoryMap() error - // Files - HasFile(filePath string) bool - CopyFile(src string, dst string) error -} - type Node struct { FileIO util.FileIO `json:"-"` // If connecting via the Jumpbox @@ -60,31 +31,93 @@ type Node struct { InternalIP string `json:"internal_ip"` cachedSigner ssh.Signer `json:"-"` sshQuiet bool `json:"-"` + + NodeClient NodeClient `json:"-"` // SSH client cache: map[username]*ssh.Client clientCache map[string]*ssh.Client clientMu sync.Mutex } -const jumpboxUser = "ubuntu" +type NodeClient interface { + RunCommand(n *Node, username string, command string) error + CopyFile(n *Node, src string, dst string) error + WaitReady(n *Node, timeout time.Duration) error + HasFile(n *Node, filePath string) bool +} -// NewNode creates a new Node with the given FileIO and SSH key path -func NewNode(fileIO util.FileIO, keyPath string, sshQuiet bool) *Node { - return &Node{ - FileIO: fileIO, - keyPath: util.ExpandPath(keyPath), - clientCache: make(map[string]*ssh.Client), - sshQuiet: sshQuiet, +type SSHNodeClient struct { + Quiet bool +} + +func NewSSHNodeClient(quiet bool) *SSHNodeClient { + return &SSHNodeClient{ + Quiet: quiet, + } +} + +func (r *SSHNodeClient) RunCommand(n *Node, username string, command string) error { + var jumpboxIp string + var ip string + if n.Jumpbox != nil { + jumpboxIp = n.Jumpbox.ExternalIP + ip = n.InternalIP + } else { + jumpboxIp = "" + ip = n.ExternalIP + } + client, err := n.getOrCreateClient(jumpboxIp, ip, username) + if err != nil { + return fmt.Errorf("failed to get client: %w", err) + } + // Don't close the client - it's cached for reuse + + session, err := client.NewSession() + if err != nil { + // Connection might be stale, try to reconnect + n.invalidateClient(username) + client, err = n.getOrCreateClient(jumpboxIp, ip, username) + if err != nil { + return fmt.Errorf("failed to reconnect client: %w", err) + } + session, err = client.NewSession() + if err != nil { + return fmt.Errorf("failed to create session: %v", err) + } + } + defer util.IgnoreError(session.Close) + + _ = session.Setenv("OMS_PORTAL_API_KEY", os.Getenv("OMS_PORTAL_API_KEY")) + _ = agent.RequestAgentForwarding(session) // Best effort, ignore errors + + if !r.Quiet { + session.Stdout = os.Stdout + session.Stderr = os.Stderr + } + // Start the command + if err := session.Start(command); err != nil { + return fmt.Errorf("failed to start command: %v", err) + } + + if err := session.Wait(); err != nil { + // A non-zero exit status from the remote command is also considered an error + return fmt.Errorf("command failed: %w", err) } + return nil } +const jumpboxUser = "ubuntu" + +// NewNode creates a new Node with the given File // CreateSubNode creates a Node object representing a node behind a jumpbox -func (n *Node) CreateSubNode(name string, externalIP string, internalIP string) NodeManager { +func (n *Node) CreateSubNode(name string, externalIP string, internalIP string) *Node { return &Node{ // Inherited from jumpbox - FileIO: n.FileIO, - Jumpbox: n, - keyPath: util.ExpandPath(n.keyPath), - sshQuiet: n.sshQuiet, + FileIO: n.FileIO, + Jumpbox: n, + keyPath: util.ExpandPath(n.keyPath), + sshQuiet: n.sshQuiet, + NodeClient: n.NodeClient, + // Custom Name: name, ExternalIP: externalIP, @@ -115,19 +148,17 @@ func (n *Node) GetName() string { return n.Name } -// WaitForSSH tries to connect to the node via SSH until timeout. -// Once successful, the connection is cached for reuse. -func (n *Node) WaitForSSH(timeout time.Duration) error { +func (c *SSHNodeClient) WaitReady(node *Node, timeout time.Duration) error { start := time.Now() jumpboxIp := "" - nodeIp := n.ExternalIP - if n.Jumpbox != nil { - jumpboxIp = n.Jumpbox.ExternalIP - nodeIp = n.InternalIP + nodeIp := node.ExternalIP + if node.Jumpbox != nil { + jumpboxIp = node.Jumpbox.ExternalIP + nodeIp = node.InternalIP } for { // Try to get or create a cached client - _, err := n.getOrCreateClient(jumpboxIp, nodeIp, jumpboxUser) + _, err := node.getOrCreateClient(jumpboxIp, nodeIp, jumpboxUser) if err == nil { // Connection successful and cached return nil @@ -142,62 +173,14 @@ func (n *Node) WaitForSSH(timeout time.Duration) error { // RunSSHCommand connects to the node, executes a command and streams the output. // If quiet is true, command output is not printed to stdout/stderr. // The SSH client connection is cached and reused for subsequent commands. -func (n *Node) RunSSHCommand(username string, command string, quiet bool) error { - var jumpboxIp string - var ip string - if n.Jumpbox != nil { - jumpboxIp = n.Jumpbox.ExternalIP - ip = n.InternalIP - } else { - jumpboxIp = "" - ip = n.ExternalIP - } - - client, err := n.getOrCreateClient(jumpboxIp, ip, username) - if err != nil { - return fmt.Errorf("failed to get client: %w", err) - } - // Don't close the client - it's cached for reuse - - session, err := client.NewSession() - if err != nil { - // Connection might be stale, try to reconnect - n.invalidateClient(username) - client, err = n.getOrCreateClient(jumpboxIp, ip, username) - if err != nil { - return fmt.Errorf("failed to reconnect client: %w", err) - } - session, err = client.NewSession() - if err != nil { - return fmt.Errorf("failed to create session: %v", err) - } - } - defer util.IgnoreError(session.Close) - - _ = session.Setenv("OMS_PORTAL_API_KEY", os.Getenv("OMS_PORTAL_API_KEY")) - _ = agent.RequestAgentForwarding(session) // Best effort, ignore errors - - if !quiet { - session.Stdout = os.Stdout - session.Stderr = os.Stderr - } - // Start the command - if err := session.Start(command); err != nil { - return fmt.Errorf("failed to start command: %v", err) - } - - if err := session.Wait(); err != nil { - // A non-zero exit status from the remote command is also considered an error - return fmt.Errorf("command failed: %w", err) - } - - return nil +func (n *Node) RunSSHCommand(username string, command string) error { + return n.NodeClient.RunCommand(n, username, command) } // HasCommand checks if a command exists on the remote node via SSH func (n *Node) HasCommand(command string) bool { checkCommand := fmt.Sprintf("command -v %s >/dev/null 2>&1", command) - err := n.RunSSHCommand("root", checkCommand, n.sshQuiet) + err := n.RunSSHCommand("root", checkCommand) if err != nil { // If the command returns a non-zero exit status, it means the command is not found return false @@ -214,7 +197,7 @@ func (n *Node) InstallOms() error { "wget https://dl.filippo.io/age/latest?for=linux/amd64 -O age.tar.gz; tar -xvf age.tar.gz; sudo mv age/age* /usr/local/bin/", } for _, cmd := range remoteCommands { - err := n.RunSSHCommand("root", cmd, n.sshQuiet) + err := n.RunSSHCommand("root", cmd) if err != nil { return fmt.Errorf("failed to run remote command '%s': %w", cmd, err) } @@ -225,7 +208,7 @@ func (n *Node) InstallOms() error { // HasAcceptEnvConfigured checks if AcceptEnv is configured func (n *Node) HasAcceptEnvConfigured() bool { checkCommand := "sudo grep -E '^AcceptEnv OMS_PORTAL_API_KEY' /etc/ssh/sshd_config >/dev/null 2>&1" - err := n.RunSSHCommand("ubuntu", checkCommand, n.sshQuiet) + err := n.RunSSHCommand("ubuntu", checkCommand) if err != nil { // If the command returns a NON-zero exit status, it means AcceptEnv is not configured return false @@ -240,7 +223,7 @@ func (n *Node) ConfigureAcceptEnv() error { "sudo systemctl restart sshd", } for _, cmd := range cmds { - err := n.RunSSHCommand("ubuntu", cmd, n.sshQuiet) + err := n.RunSSHCommand("ubuntu", cmd) if err != nil { return fmt.Errorf("failed to run command '%s': %w", cmd, err) } @@ -251,13 +234,13 @@ func (n *Node) ConfigureAcceptEnv() error { // HasRootLoginEnabled checks if root login is enabled on the remote node via SSH func (n *Node) HasRootLoginEnabled() bool { checkCommandPermit := "sudo grep -E '^PermitRootLogin yes' /etc/ssh/sshd_config >/dev/null 2>&1" - err := n.RunSSHCommand("ubuntu", checkCommandPermit, n.sshQuiet) + err := n.RunSSHCommand("ubuntu", checkCommandPermit) if err != nil { // If the command returns a NON-zero exit status, it means root login is not permitted return false } checkCommandAuthorizedKeys := "sudo grep -E '^no-port-forwarding' /root/.ssh/authorized_keys >/dev/null 2>&1" - err = n.RunSSHCommand("ubuntu", checkCommandAuthorizedKeys, n.sshQuiet) + err = n.RunSSHCommand("ubuntu", checkCommandAuthorizedKeys) if err == nil { // If the command returns a ZERO exit status, it means root login is prevented return false @@ -273,7 +256,7 @@ func (n *Node) EnableRootLogin() error { "sudo systemctl restart sshd", } for _, cmd := range cmds { - err := n.RunSSHCommand("ubuntu", cmd, n.sshQuiet) + err := n.RunSSHCommand("ubuntu", cmd) if err != nil { return fmt.Errorf("failed to run command '%s': %w", cmd, err) } @@ -298,9 +281,9 @@ func (n *Node) ConfigureMemoryMap() error { } // HasFile checks if a file exists on the remote node via SSH -func (n *Node) HasFile(filePath string) bool { +func (c *SSHNodeClient) HasFile(n *Node, filePath string) bool { checkCommand := fmt.Sprintf("test -f '%s'", filePath) - err := n.RunSSHCommand("ubuntu", checkCommand, n.sshQuiet) + err := n.RunSSHCommand("ubuntu", checkCommand) if err != nil { // If the command returns a non-zero exit status, it means the file does not exist return false @@ -309,7 +292,7 @@ func (n *Node) HasFile(filePath string) bool { } // CopyFile copies a file from the local system to the remote node via SFTP -func (n *Node) CopyFile(src string, dst string) error { +func (c *SSHNodeClient) CopyFile(n *Node, src string, dst string) error { if n.Jumpbox == nil { err := n.ensureDirectoryExists("root", filepath.Dir(dst)) if err != nil { @@ -329,7 +312,7 @@ func (n *Node) CopyFile(src string, dst string) error { // hasSysctlLine checks if a specific line exists in /etc/sysctl.conf on the remote node via SSH func (n *Node) hasSysctlLine(line string) bool { checkCommand := fmt.Sprintf("sudo grep -E '^%s' /etc/sysctl.conf >/dev/null 2>&1", line) - err := n.RunSSHCommand("root", checkCommand, n.sshQuiet) + err := n.RunSSHCommand("root", checkCommand) if err != nil { // If the command returns a NON-zero exit status, it means the setting is not configured return false @@ -344,7 +327,7 @@ func (n *Node) configureSysctlLine(line string) error { "sudo sysctl -p", } for _, cmd := range cmds { - err := n.RunSSHCommand("root", cmd, n.sshQuiet) + err := n.RunSSHCommand("root", cmd) if err != nil { return fmt.Errorf("failed to run command '%s': %w", cmd, err) } @@ -467,7 +450,7 @@ func (n *Node) getSFTPClient(jumpboxIp string, ip string, username string) (*sft // ensureDirectoryExists creates the directory on the remote node via SSH if it does not exist. func (n *Node) ensureDirectoryExists(username string, dir string) error { cmd := fmt.Sprintf("mkdir -p '%s'", dir) - return n.RunSSHCommand(username, cmd, n.sshQuiet) + return n.RunSSHCommand(username, cmd) } // copyFile copies a file from the local system to the remote node via SFTP.