From b3386019378f0f0064754888e04626c72a651d17 Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Fri, 29 May 2026 11:59:17 +0200 Subject: [PATCH 1/4] feat(gcp): enhance SSH key management for jumpbox and control plane nodes --- internal/bootstrap/gcp/gce.go | 6 +- internal/bootstrap/gcp/gcp.go | 97 ++++++++++++++++++++++++++-- internal/bootstrap/gcp/gcp_test.go | 100 ++++++++++++++++++++++++++--- 3 files changed, 183 insertions(+), 20 deletions(-) diff --git a/internal/bootstrap/gcp/gce.go b/internal/bootstrap/gcp/gce.go index a7b7fc66..82716a85 100644 --- a/internal/bootstrap/gcp/gce.go +++ b/internal/bootstrap/gcp/gce.go @@ -33,9 +33,9 @@ var vmDefs = []VMDef{ {"ceph-1", "e2-standard-8", []string{"ceph"}, []int64{10, 100}, false}, {"ceph-2", "e2-standard-8", []string{"ceph"}, []int64{10, 100}, false}, {"ceph-3", "e2-standard-8", []string{"ceph"}, []int64{10, 100}, false}, - {"k0s-1", "e2-standard-8", []string{"k0s"}, []int64{}, false}, - {"k0s-2", "e2-standard-8", []string{"k0s"}, []int64{}, false}, - {"k0s-3", "e2-standard-8", []string{"k0s"}, []int64{}, false}, + {"k0s-1", "e2-standard-8", []string{"k0s"}, []int64{100}, false}, + {"k0s-2", "e2-standard-8", []string{"k0s"}, []int64{100}, false}, + {"k0s-3", "e2-standard-8", []string{"k0s"}, []int64{100}, false}, } // validateVMProvisioningOptions checks that spot and preemptible options are not both set diff --git a/internal/bootstrap/gcp/gcp.go b/internal/bootstrap/gcp/gcp.go index f91e56ce..68572a5a 100644 --- a/internal/bootstrap/gcp/gcp.go +++ b/internal/bootstrap/gcp/gcp.go @@ -290,6 +290,11 @@ func (b *GCPBootstrapper) Bootstrap() error { return fmt.Errorf("failed to ensure hosts are configured: %w", err) } + err = b.stlog.Step("Ensure etcd disks mounted", b.EnsureEtcdDisksMounted) + if err != nil { + return fmt.Errorf("failed to ensure etcd disks are mounted: %w", err) + } + if b.Env.RegistryType == RegistryTypeLocalContainer { err = b.stlog.Step("Ensure local container registry", b.EnsureLocalContainerRegistry) if err != nil { @@ -332,8 +337,12 @@ func (b *GCPBootstrapper) Bootstrap() error { } if b.Env.InstallVersion != "" || b.Env.InstallLocal != "" { - err = b.stlog.Step("Install Codesphere", b.InstallCodesphere) + err = b.stlog.Step("Ensure control plane SSH key", b.EnsureControlPlaneSSHKey) if err != nil { + return fmt.Errorf("failed to ensure control plane SSH key: %w", err) + } + + if err = b.stlog.Step("Install Codesphere", b.InstallCodesphere); err != nil { return fmt.Errorf("failed to install Codesphere: %w", err) } @@ -797,6 +806,20 @@ func (b *GCPBootstrapper) EnsureJumpboxConfigured() error { } } + // Ensure SSH private key is present on the jumpbox so the Codesphere installer + // can SSH into worker nodes. Skip if no private key path is configured. + if b.Env.SSHPrivateKeyPath != "" && !b.Env.Jumpbox.NodeClient.HasFile(b.Env.Jumpbox, "/root/.ssh/id_rsa") { + if err := b.Env.Jumpbox.NodeClient.RunCommand(b.Env.Jumpbox, "root", "mkdir -p /root/.ssh && chmod 700 /root/.ssh"); err != nil { + return fmt.Errorf("failed to create .ssh directory on jumpbox: %w", err) + } + if err := b.Env.Jumpbox.NodeClient.CopyFile(b.Env.Jumpbox, b.Env.SSHPrivateKeyPath, "/root/.ssh/id_rsa"); err != nil { + return fmt.Errorf("failed to copy SSH key to jumpbox: %w", err) + } + if err := b.Env.Jumpbox.NodeClient.RunCommand(b.Env.Jumpbox, "root", "chmod 600 /root/.ssh/id_rsa"); err != nil { + return fmt.Errorf("failed to set SSH key permissions on jumpbox: %w", err) + } + } + hasOms := b.Env.Jumpbox.HasCommand("oms") if hasOms { return nil @@ -810,6 +833,28 @@ func (b *GCPBootstrapper) EnsureJumpboxConfigured() error { return nil } +// EnsureControlPlaneSSHKey copies the SSH private key to the first control-plane node so +// that configure-k0s.sh can SSH into the other worker nodes. Skip if no private key path +// is configured or there are no control-plane nodes. +func (b *GCPBootstrapper) EnsureControlPlaneSSHKey() error { + if len(b.Env.ControlPlaneNodes) == 0 || b.Env.SSHPrivateKeyPath == "" { + return nil + } + controlPlane := b.Env.ControlPlaneNodes[0] + if !controlPlane.NodeClient.HasFile(controlPlane, "/root/.ssh/id_rsa") { + if err := controlPlane.NodeClient.RunCommand(controlPlane, "root", "mkdir -p /root/.ssh && chmod 700 /root/.ssh"); err != nil { + return fmt.Errorf("failed to create .ssh directory on control plane: %w", err) + } + if err := controlPlane.NodeClient.CopyFile(controlPlane, b.Env.SSHPrivateKeyPath, "/root/.ssh/id_rsa"); err != nil { + return fmt.Errorf("failed to copy SSH key to control plane: %w", err) + } + if err := controlPlane.NodeClient.RunCommand(controlPlane, "root", "chmod 600 /root/.ssh/id_rsa"); err != nil { + return fmt.Errorf("failed to set SSH key permissions on control plane: %w", err) + } + } + return nil +} + func (b *GCPBootstrapper) EnsureHostsConfigured() error { allNodes := append(b.Env.ControlPlaneNodes, b.Env.PostgreSQLNode) allNodes = append(allNodes, b.Env.CephNodes...) @@ -832,6 +877,44 @@ func (b *GCPBootstrapper) EnsureHostsConfigured() error { return nil } +// EnsureEtcdDisksMounted formats and mounts the dedicated etcd disk (/dev/sdb) on each control +// plane node at /var/lib/k0s/etcd. The disk is persisted via /etc/fstab using its UUID. +// This must run before k0s is installed so etcd writes land on the dedicated PD-SSD. +func (b *GCPBootstrapper) EnsureEtcdDisksMounted() error { + for _, n := range b.Env.ControlPlaneNodes { + // Idempotency check: skip if already mounted. + if err := n.RunSSHCommand("root", "mountpoint -q /var/lib/k0s/etcd"); err == nil { + b.stlog.Logf("etcd disk already mounted on %s, skipping", n.GetName()) + continue + } + + // Format /dev/sdb with ext4 if it has no filesystem yet. + if err := n.RunSSHCommand("root", "blkid -s TYPE -o value /dev/sdb | grep -q ext4"); err != nil { + b.stlog.Logf("Formatting etcd disk on %s", n.GetName()) + if err := n.RunSSHCommand("root", "mkfs.ext4 -F /dev/sdb"); err != nil { + return fmt.Errorf("failed to format etcd disk on %s: %w", n.GetName(), err) + } + } + + // Create mount point directory. + if err := n.RunSSHCommand("root", "mkdir -p /var/lib/k0s/etcd"); err != nil { + return fmt.Errorf("failed to create etcd mount point on %s: %w", n.GetName(), err) + } + + // Register in /etc/fstab by UUID (survives reboots) and mount. + fstabAndMount := `DISK_UUID=$(blkid -s UUID -o value /dev/sdb) && ` + + `grep -qF "UUID=$DISK_UUID" /etc/fstab || ` + + `echo "UUID=$DISK_UUID /var/lib/k0s/etcd ext4 defaults,noatime 0 2" >> /etc/fstab && ` + + `mount /var/lib/k0s/etcd` + if err := n.RunSSHCommand("root", fstabAndMount); err != nil { + return fmt.Errorf("failed to mount etcd disk on %s: %w", n.GetName(), err) + } + + b.stlog.Logf("etcd disk mounted at /var/lib/k0s/etcd on %s", n.GetName()) + } + return nil +} + // EnsureLocalContainerRegistry installs a docker registry on the postgres node to speed up image loading time func (b *GCPBootstrapper) EnsureLocalContainerRegistry() error { localRegistryServer := b.Env.PostgreSQLNode.GetInternalIP() + ":5000" @@ -1031,10 +1114,10 @@ func (b *GCPBootstrapper) GenerateK0sConfigScript() error { cat < cloud.conf [Global] -project-id = "$PROJECT_ID" +project-id = "` + b.Env.ProjectID + `" EOF -cat <> cc-deployment.yaml +cat < cc-deployment.yaml apiVersion: apps/v1 kind: DaemonSet metadata: @@ -1085,7 +1168,7 @@ spec: EOF KUBECTL="/etc/codesphere/deps/kubernetes/files/k0s kubectl" -$KUBECTL create configmap cloud-config --from-file=cloud.conf -n kube-system +$KUBECTL create configmap cloud-config --from-file=cloud.conf -n kube-system --dry-run=client -o yaml | $KUBECTL apply -f - echo alias kubectl=\"$KUBECTL\" >> /root/.bashrc echo alias k=\"$KUBECTL\" >> /root/.bashrc @@ -1097,11 +1180,11 @@ $KUBECTL apply -f cc-deployment.yaml $KUBECTL patch svc public-gateway-controller -n codesphere -p '{"spec": {"loadBalancerIP": "'` + b.Env.PublicGatewayIP + `'"}}' $KUBECTL patch svc gateway-controller -n codesphere -p '{"spec": {"loadBalancerIP": "'` + b.Env.GatewayIP + `'"}}' -sed -i 's/k0scontroller/k0scontroller --enable-cloud-provider/g' /etc/systemd/system/k0scontroller.service +grep -qF -- --enable-cloud-provider /etc/systemd/system/k0scontroller.service || sed -i '/ExecStart=/s/$/ --enable-cloud-provider/' /etc/systemd/system/k0scontroller.service -ssh -o StrictHostKeyChecking=no root@` + b.Env.ControlPlaneNodes[1].GetInternalIP() + ` "sed -i 's/k0sworker/k0sworker --enable-cloud-provider/g' /etc/systemd/system/k0sworker.service; systemctl daemon-reload; systemctl restart k0sworker" +ssh -o StrictHostKeyChecking=no -o ConnectTimeout=60 root@` + b.Env.ControlPlaneNodes[1].GetInternalIP() + ` "grep -qF -- --enable-cloud-provider /etc/systemd/system/k0sworker.service || sed -i '/ExecStart=/s/\$/ --enable-cloud-provider/' /etc/systemd/system/k0sworker.service; systemctl daemon-reload; systemctl restart k0sworker" || true -ssh -o StrictHostKeyChecking=no root@` + b.Env.ControlPlaneNodes[2].GetInternalIP() + ` "sed -i 's/k0sworker/k0sworker --enable-cloud-provider/g' /etc/systemd/system/k0sworker.service; systemctl daemon-reload; systemctl restart k0sworker" +ssh -o StrictHostKeyChecking=no -o ConnectTimeout=60 root@` + b.Env.ControlPlaneNodes[2].GetInternalIP() + ` "grep -qF -- --enable-cloud-provider /etc/systemd/system/k0sworker.service || sed -i '/ExecStart=/s/\$/ --enable-cloud-provider/' /etc/systemd/system/k0sworker.service; systemctl daemon-reload; systemctl restart k0sworker" || true systemctl daemon-reload systemctl restart k0scontroller diff --git a/internal/bootstrap/gcp/gcp_test.go b/internal/bootstrap/gcp/gcp_test.go index 83f5d4ec..ca56b155 100644 --- a/internal/bootstrap/gcp/gcp_test.go +++ b/internal/bootstrap/gcp/gcp_test.go @@ -605,16 +605,16 @@ var _ = Describe("GCP Bootstrapper", func() { Expect(err).To(MatchError(ContainSubstring("prometheus remote write username and password must both be set when remote write URL is specified"))) }) }) - Context("When Prometheus remote write URL is set but only password is missing", func() { - BeforeEach(func() { - csEnv.PrometheusRemoteWriteURL = "https://prometheus.example.com/api/v1/write" - csEnv.PrometheusRemoteWriteUser = "prom-user" - }) - It("returns an error", func() { - err := bs.ValidateInput() - Expect(err).To(MatchError(ContainSubstring("prometheus remote write username and password must both be set when remote write URL is specified"))) - }) - }) + Context("When Prometheus remote write URL is set but only password is missing", func() { + BeforeEach(func() { + csEnv.PrometheusRemoteWriteURL = "https://prometheus.example.com/api/v1/write" + csEnv.PrometheusRemoteWriteUser = "prom-user" + }) + It("returns an error", func() { + err := bs.ValidateInput() + Expect(err).To(MatchError(ContainSubstring("prometheus remote write username and password must both be set when remote write URL is specified"))) + }) + }) Context("When Prometheus remote write credentials are set but URL is missing", func() { BeforeEach(func() { csEnv.PrometheusRemoteWriteUser = "prom-user" @@ -1095,6 +1095,29 @@ var _ = Describe("GCP Bootstrapper", func() { err := bs.EnsureJumpboxConfigured() Expect(err).NotTo(HaveOccurred()) }) + + Context("when SSHPrivateKeyPath is set and key is absent", func() { + It("copies SSH key to jumpbox", func() { + bs.Env.SSHPrivateKeyPath = "key" + nodeClient.EXPECT().HasFile(mock.MatchedBy(jumpboxMatcher), "/root/.ssh/id_rsa").Return(false) + nodeClient.EXPECT().RunCommand(mock.Anything, mock.Anything, mock.Anything).Return(nil) + nodeClient.EXPECT().CopyFile(mock.MatchedBy(jumpboxMatcher), "key", "/root/.ssh/id_rsa").Return(nil) + + err := bs.EnsureJumpboxConfigured() + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Context("when SSHPrivateKeyPath is set and key already exists", func() { + It("skips SSH key copy", func() { + bs.Env.SSHPrivateKeyPath = "key" + nodeClient.EXPECT().HasFile(mock.MatchedBy(jumpboxMatcher), "/root/.ssh/id_rsa").Return(true) + nodeClient.EXPECT().RunCommand(mock.Anything, mock.Anything, mock.Anything).Return(nil) + + err := bs.EnsureJumpboxConfigured() + Expect(err).NotTo(HaveOccurred()) + }) + }) }) Describe("Invalid cases", func() { @@ -1115,6 +1138,63 @@ var _ = Describe("GCP Bootstrapper", func() { Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("failed to install OMS")) }) + + Context("when SSHPrivateKeyPath is set", func() { + It("fails when CopyFile for SSH key fails", func() { + bs.Env.SSHPrivateKeyPath = "key" + nodeClient.EXPECT().RunCommand(mock.Anything, "ubuntu", mock.Anything).Return(nil) + nodeClient.EXPECT().HasFile(mock.MatchedBy(jumpboxMatcher), "/root/.ssh/id_rsa").Return(false) + nodeClient.EXPECT().RunCommand(mock.MatchedBy(jumpboxMatcher), "root", "mkdir -p /root/.ssh && chmod 700 /root/.ssh").Return(nil) + nodeClient.EXPECT().CopyFile(mock.MatchedBy(jumpboxMatcher), "key", "/root/.ssh/id_rsa").Return(fmt.Errorf("copy failed")) + + err := bs.EnsureJumpboxConfigured() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to copy SSH key to jumpbox")) + }) + }) + }) + }) + + Describe("EnsureControlPlaneSSHKey", func() { + Context("when SSHPrivateKeyPath is not set", func() { + It("does nothing", func() { + bs.Env.SSHPrivateKeyPath = "" + err := bs.EnsureControlPlaneSSHKey() + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Context("when SSHPrivateKeyPath is set", func() { + BeforeEach(func() { + csEnv.SSHPrivateKeyPath = "key" + }) + + It("copies SSH key when not present", func() { + nodeClient.EXPECT().HasFile(bs.Env.ControlPlaneNodes[0], "/root/.ssh/id_rsa").Return(false) + nodeClient.EXPECT().RunCommand(bs.Env.ControlPlaneNodes[0], "root", "mkdir -p /root/.ssh && chmod 700 /root/.ssh").Return(nil) + nodeClient.EXPECT().CopyFile(bs.Env.ControlPlaneNodes[0], "key", "/root/.ssh/id_rsa").Return(nil) + nodeClient.EXPECT().RunCommand(bs.Env.ControlPlaneNodes[0], "root", "chmod 600 /root/.ssh/id_rsa").Return(nil) + + err := bs.EnsureControlPlaneSSHKey() + Expect(err).NotTo(HaveOccurred()) + }) + + It("skips copy when key already exists", func() { + nodeClient.EXPECT().HasFile(bs.Env.ControlPlaneNodes[0], "/root/.ssh/id_rsa").Return(true) + + err := bs.EnsureControlPlaneSSHKey() + Expect(err).NotTo(HaveOccurred()) + }) + + It("fails when CopyFile fails", func() { + nodeClient.EXPECT().HasFile(bs.Env.ControlPlaneNodes[0], "/root/.ssh/id_rsa").Return(false) + nodeClient.EXPECT().RunCommand(bs.Env.ControlPlaneNodes[0], "root", "mkdir -p /root/.ssh && chmod 700 /root/.ssh").Return(nil) + nodeClient.EXPECT().CopyFile(bs.Env.ControlPlaneNodes[0], "key", "/root/.ssh/id_rsa").Return(fmt.Errorf("copy failed")) + + err := bs.EnsureControlPlaneSSHKey() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to copy SSH key to control plane")) + }) }) }) From 9ae531651030cd1ff73cf05b6959fbce30f25872 Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Fri, 29 May 2026 12:35:24 +0200 Subject: [PATCH 2/4] feat(gcp): remove SSH key management for control plane and jumpbox --- internal/bootstrap/gcp/gcp.go | 41 ----------------------------------- 1 file changed, 41 deletions(-) diff --git a/internal/bootstrap/gcp/gcp.go b/internal/bootstrap/gcp/gcp.go index 68572a5a..ee1187b9 100644 --- a/internal/bootstrap/gcp/gcp.go +++ b/internal/bootstrap/gcp/gcp.go @@ -337,11 +337,6 @@ func (b *GCPBootstrapper) Bootstrap() error { } if b.Env.InstallVersion != "" || b.Env.InstallLocal != "" { - err = b.stlog.Step("Ensure control plane SSH key", b.EnsureControlPlaneSSHKey) - if err != nil { - return fmt.Errorf("failed to ensure control plane SSH key: %w", err) - } - if err = b.stlog.Step("Install Codesphere", b.InstallCodesphere); err != nil { return fmt.Errorf("failed to install Codesphere: %w", err) } @@ -806,20 +801,6 @@ func (b *GCPBootstrapper) EnsureJumpboxConfigured() error { } } - // Ensure SSH private key is present on the jumpbox so the Codesphere installer - // can SSH into worker nodes. Skip if no private key path is configured. - if b.Env.SSHPrivateKeyPath != "" && !b.Env.Jumpbox.NodeClient.HasFile(b.Env.Jumpbox, "/root/.ssh/id_rsa") { - if err := b.Env.Jumpbox.NodeClient.RunCommand(b.Env.Jumpbox, "root", "mkdir -p /root/.ssh && chmod 700 /root/.ssh"); err != nil { - return fmt.Errorf("failed to create .ssh directory on jumpbox: %w", err) - } - if err := b.Env.Jumpbox.NodeClient.CopyFile(b.Env.Jumpbox, b.Env.SSHPrivateKeyPath, "/root/.ssh/id_rsa"); err != nil { - return fmt.Errorf("failed to copy SSH key to jumpbox: %w", err) - } - if err := b.Env.Jumpbox.NodeClient.RunCommand(b.Env.Jumpbox, "root", "chmod 600 /root/.ssh/id_rsa"); err != nil { - return fmt.Errorf("failed to set SSH key permissions on jumpbox: %w", err) - } - } - hasOms := b.Env.Jumpbox.HasCommand("oms") if hasOms { return nil @@ -833,28 +814,6 @@ func (b *GCPBootstrapper) EnsureJumpboxConfigured() error { return nil } -// EnsureControlPlaneSSHKey copies the SSH private key to the first control-plane node so -// that configure-k0s.sh can SSH into the other worker nodes. Skip if no private key path -// is configured or there are no control-plane nodes. -func (b *GCPBootstrapper) EnsureControlPlaneSSHKey() error { - if len(b.Env.ControlPlaneNodes) == 0 || b.Env.SSHPrivateKeyPath == "" { - return nil - } - controlPlane := b.Env.ControlPlaneNodes[0] - if !controlPlane.NodeClient.HasFile(controlPlane, "/root/.ssh/id_rsa") { - if err := controlPlane.NodeClient.RunCommand(controlPlane, "root", "mkdir -p /root/.ssh && chmod 700 /root/.ssh"); err != nil { - return fmt.Errorf("failed to create .ssh directory on control plane: %w", err) - } - if err := controlPlane.NodeClient.CopyFile(controlPlane, b.Env.SSHPrivateKeyPath, "/root/.ssh/id_rsa"); err != nil { - return fmt.Errorf("failed to copy SSH key to control plane: %w", err) - } - if err := controlPlane.NodeClient.RunCommand(controlPlane, "root", "chmod 600 /root/.ssh/id_rsa"); err != nil { - return fmt.Errorf("failed to set SSH key permissions on control plane: %w", err) - } - } - return nil -} - func (b *GCPBootstrapper) EnsureHostsConfigured() error { allNodes := append(b.Env.ControlPlaneNodes, b.Env.PostgreSQLNode) allNodes = append(allNodes, b.Env.CephNodes...) From 518b1417136df5402e972c335ba7018afe3248f3 Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Fri, 29 May 2026 13:12:23 +0200 Subject: [PATCH 3/4] fix: lint --- internal/bootstrap/gcp/gcp_test.go | 78 ------------------------------ 1 file changed, 78 deletions(-) diff --git a/internal/bootstrap/gcp/gcp_test.go b/internal/bootstrap/gcp/gcp_test.go index ca56b155..b74b648f 100644 --- a/internal/bootstrap/gcp/gcp_test.go +++ b/internal/bootstrap/gcp/gcp_test.go @@ -1096,28 +1096,6 @@ var _ = Describe("GCP Bootstrapper", func() { Expect(err).NotTo(HaveOccurred()) }) - Context("when SSHPrivateKeyPath is set and key is absent", func() { - It("copies SSH key to jumpbox", func() { - bs.Env.SSHPrivateKeyPath = "key" - nodeClient.EXPECT().HasFile(mock.MatchedBy(jumpboxMatcher), "/root/.ssh/id_rsa").Return(false) - nodeClient.EXPECT().RunCommand(mock.Anything, mock.Anything, mock.Anything).Return(nil) - nodeClient.EXPECT().CopyFile(mock.MatchedBy(jumpboxMatcher), "key", "/root/.ssh/id_rsa").Return(nil) - - err := bs.EnsureJumpboxConfigured() - Expect(err).NotTo(HaveOccurred()) - }) - }) - - Context("when SSHPrivateKeyPath is set and key already exists", func() { - It("skips SSH key copy", func() { - bs.Env.SSHPrivateKeyPath = "key" - nodeClient.EXPECT().HasFile(mock.MatchedBy(jumpboxMatcher), "/root/.ssh/id_rsa").Return(true) - nodeClient.EXPECT().RunCommand(mock.Anything, mock.Anything, mock.Anything).Return(nil) - - err := bs.EnsureJumpboxConfigured() - Expect(err).NotTo(HaveOccurred()) - }) - }) }) Describe("Invalid cases", func() { @@ -1139,62 +1117,6 @@ var _ = Describe("GCP Bootstrapper", func() { Expect(err.Error()).To(ContainSubstring("failed to install OMS")) }) - Context("when SSHPrivateKeyPath is set", func() { - It("fails when CopyFile for SSH key fails", func() { - bs.Env.SSHPrivateKeyPath = "key" - nodeClient.EXPECT().RunCommand(mock.Anything, "ubuntu", mock.Anything).Return(nil) - nodeClient.EXPECT().HasFile(mock.MatchedBy(jumpboxMatcher), "/root/.ssh/id_rsa").Return(false) - nodeClient.EXPECT().RunCommand(mock.MatchedBy(jumpboxMatcher), "root", "mkdir -p /root/.ssh && chmod 700 /root/.ssh").Return(nil) - nodeClient.EXPECT().CopyFile(mock.MatchedBy(jumpboxMatcher), "key", "/root/.ssh/id_rsa").Return(fmt.Errorf("copy failed")) - - err := bs.EnsureJumpboxConfigured() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to copy SSH key to jumpbox")) - }) - }) - }) - }) - - Describe("EnsureControlPlaneSSHKey", func() { - Context("when SSHPrivateKeyPath is not set", func() { - It("does nothing", func() { - bs.Env.SSHPrivateKeyPath = "" - err := bs.EnsureControlPlaneSSHKey() - Expect(err).NotTo(HaveOccurred()) - }) - }) - - Context("when SSHPrivateKeyPath is set", func() { - BeforeEach(func() { - csEnv.SSHPrivateKeyPath = "key" - }) - - It("copies SSH key when not present", func() { - nodeClient.EXPECT().HasFile(bs.Env.ControlPlaneNodes[0], "/root/.ssh/id_rsa").Return(false) - nodeClient.EXPECT().RunCommand(bs.Env.ControlPlaneNodes[0], "root", "mkdir -p /root/.ssh && chmod 700 /root/.ssh").Return(nil) - nodeClient.EXPECT().CopyFile(bs.Env.ControlPlaneNodes[0], "key", "/root/.ssh/id_rsa").Return(nil) - nodeClient.EXPECT().RunCommand(bs.Env.ControlPlaneNodes[0], "root", "chmod 600 /root/.ssh/id_rsa").Return(nil) - - err := bs.EnsureControlPlaneSSHKey() - Expect(err).NotTo(HaveOccurred()) - }) - - It("skips copy when key already exists", func() { - nodeClient.EXPECT().HasFile(bs.Env.ControlPlaneNodes[0], "/root/.ssh/id_rsa").Return(true) - - err := bs.EnsureControlPlaneSSHKey() - Expect(err).NotTo(HaveOccurred()) - }) - - It("fails when CopyFile fails", func() { - nodeClient.EXPECT().HasFile(bs.Env.ControlPlaneNodes[0], "/root/.ssh/id_rsa").Return(false) - nodeClient.EXPECT().RunCommand(bs.Env.ControlPlaneNodes[0], "root", "mkdir -p /root/.ssh && chmod 700 /root/.ssh").Return(nil) - nodeClient.EXPECT().CopyFile(bs.Env.ControlPlaneNodes[0], "key", "/root/.ssh/id_rsa").Return(fmt.Errorf("copy failed")) - - err := bs.EnsureControlPlaneSSHKey() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to copy SSH key to control plane")) - }) }) }) From 9bce8c6b6a5e3ecc2141c163835f6a23071109b0 Mon Sep 17 00:00:00 2001 From: OliverTrautvetter <66372584+OliverTrautvetter@users.noreply.github.com> Date: Fri, 29 May 2026 13:15:10 +0200 Subject: [PATCH 4/4] fix: remove unnecessary blank lines in GCP Bootstrapper tests --- internal/bootstrap/gcp/gcp_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/bootstrap/gcp/gcp_test.go b/internal/bootstrap/gcp/gcp_test.go index b74b648f..2f6947ac 100644 --- a/internal/bootstrap/gcp/gcp_test.go +++ b/internal/bootstrap/gcp/gcp_test.go @@ -1095,7 +1095,6 @@ var _ = Describe("GCP Bootstrapper", func() { err := bs.EnsureJumpboxConfigured() Expect(err).NotTo(HaveOccurred()) }) - }) Describe("Invalid cases", func() { @@ -1116,7 +1115,6 @@ var _ = Describe("GCP Bootstrapper", func() { Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("failed to install OMS")) }) - }) })