From 528984890487a3350c4884481d191a698765efe1 Mon Sep 17 00:00:00 2001 From: Alice Frosi Date: Tue, 9 Jun 2026 11:08:46 +0000 Subject: [PATCH 1/2] Fix control-plane join failing with empty certificate key The upload-certs command was piped through `2>/dev/null | tail -1`, which suppressed errors and masked the exit code. When kubeadm failed, we got an empty string instead of an error. Remove the pipe and stderr suppression so failures propagate properly. Extract the certificate key by matching the 64-char hex string from the output instead of blindly taking the last line. Assisted-by: Claude Opus 4.6 (1M context) --- internal/cluster/join.go | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/internal/cluster/join.go b/internal/cluster/join.go index 5798b20..d90da0a 100644 --- a/internal/cluster/join.go +++ b/internal/cluster/join.go @@ -132,19 +132,40 @@ func (c *Cluster) Join(ctx context.Context, opts JoinOptions) error { return nil } +func isHex(s string) bool { + for _, c := range s { + switch { + case c >= '0' && c <= '9': + case c >= 'a' && c <= 'f': + case c >= 'A' && c <= 'F': + default: + return false + } + } + return true +} + // generateJoinCommand generates a fresh join command from the control plane func (c *Cluster) generateJoinCommand(ctx context.Context, cpSSHClient *ssh.Client, isControlPlane bool) (string, error) { if isControlPlane { - // For control-plane nodes, we need to upload certificates and get the certificate key + // For control-plane nodes, we need to upload certificates and get the certificate key. + // The command prints log lines to stderr and the 64-char hex key on stdout. + // We avoid piping (which masks the exit code) and extract the key ourselves. c.logger.Info("Uploading certificates for control-plane join...") - certKeyOutput, err := cpSSHClient.Exec(ctx, "sudo kubeadm init phase upload-certs --upload-certs 2>/dev/null | tail -1") + certKeyOutput, err := cpSSHClient.Exec(ctx, "sudo kubeadm init phase upload-certs --upload-certs") if err != nil { return "", fmt.Errorf("failed to upload certificates: %w", err) } - certificateKey := strings.TrimSpace(certKeyOutput) + var certificateKey string + for _, line := range strings.Split(certKeyOutput, "\n") { + line = strings.TrimSpace(line) + if len(line) == 64 && isHex(line) { + certificateKey = line + } + } if certificateKey == "" { - return "", fmt.Errorf("certificate key is empty") + return "", fmt.Errorf("certificate key not found in upload-certs output: %s", certKeyOutput) } c.logger.Infof("Certificate key: %s", certificateKey) From 26a5588cbdac1715194dafda55cf3fdd5b17e8b5 Mon Sep 17 00:00:00 2001 From: Alice Frosi Date: Tue, 9 Jun 2026 12:00:30 +0000 Subject: [PATCH 2/2] Fix upload-certs TLS error by passing kubeadm config Without --config, kubeadm upload-certs auto-discovers the API server and connects to the container's podman bridge IP instead of the VM cluster IP, causing a certificate validation error (x509: certificate is valid for 10.0.0.x, not 10.89.x.x). Pass --config /etc/kubernetes/kubeadm-config.yaml so kubeadm uses the correct controlPlaneEndpoint from the init config. Assisted-by: Claude Opus 4.6 (1M context) --- internal/cluster/join.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/cluster/join.go b/internal/cluster/join.go index d90da0a..5fc1471 100644 --- a/internal/cluster/join.go +++ b/internal/cluster/join.go @@ -152,7 +152,7 @@ func (c *Cluster) generateJoinCommand(ctx context.Context, cpSSHClient *ssh.Clie // The command prints log lines to stderr and the 64-char hex key on stdout. // We avoid piping (which masks the exit code) and extract the key ourselves. c.logger.Info("Uploading certificates for control-plane join...") - certKeyOutput, err := cpSSHClient.Exec(ctx, "sudo kubeadm init phase upload-certs --upload-certs") + certKeyOutput, err := cpSSHClient.Exec(ctx, "sudo kubeadm init phase upload-certs --upload-certs --config /etc/kubernetes/kubeadm-config.yaml") if err != nil { return "", fmt.Errorf("failed to upload certificates: %w", err) }