Skip to content

Commit 18f86cf

Browse files
committed
Fix flaky control-plane join certificate upload
Two changes to fix upload-certs timing out during control-plane join: 1. Pin kubernetesVersion in the kubeadm config template by querying kubeadm version -o short from the VM. Without it, every kubeadm subcommand fetches the latest version from dl.k8s.io. 2. Pass --config to upload-certs with a temp config that includes both the certificateKey and kubernetesVersion. Since --config and --certificate-key are mutually exclusive, the key goes in the config file. This prevents the remote version lookup that eats into kubeadm's 10s internal timeout, causing "rate: Wait(n=1) would exceed context deadline". Assisted-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a57955c commit 18f86cf

3 files changed

Lines changed: 36 additions & 7 deletions

File tree

internal/cluster/init.go

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"context"
99
_ "embed"
1010
"fmt"
11+
"strings"
1112
"text/template"
1213
"time"
1314

@@ -25,13 +26,15 @@ var calicoManifest []byte
2526
var kubeadmConfigTemplate = template.Must(template.New("kubeadm-config").Parse(kubeadmConfigTmpl))
2627

2728
type kubeadmConfigParams struct {
28-
AdvertiseAddress string
29+
AdvertiseAddress string
30+
KubernetesVersion string
2931
}
3032

3133
// InitOptions holds options for cluster initialization
3234
type InitOptions struct {
33-
NodeName string
34-
Timeout time.Duration
35+
NodeName string
36+
KubernetesVersion string
37+
Timeout time.Duration
3538
}
3639

3740
// Init initializes a Kubernetes cluster on the control plane node
@@ -60,14 +63,23 @@ func (c *Cluster) Init(ctx context.Context, opts InitOptions) error {
6063
// Create SSH client
6164
sshClient := ssh.NewClientForNode(c.name, nodeName, c.logger)
6265

66+
// Query the exact kubeadm version from the VM so the config pins it
67+
// and prevents kubeadm from doing a remote version lookup.
68+
kubeVersion := opts.KubernetesVersion
69+
if kubeVersion == "" {
70+
if out, err := sshClient.Exec(ctx, "kubeadm version -o short"); err == nil {
71+
kubeVersion = strings.TrimSpace(out)
72+
}
73+
}
74+
6375
// Create kubeadm config in container
6476
c.logger.Info("Creating kubeadm config file...")
6577
clusterLabel := c.name
6678
if clusterLabel == "" {
6779
clusterLabel = config.DefaultNetworkName
6880
}
6981
containerName := fmt.Sprintf("k8s-%s-%s", clusterLabel, nodeName)
70-
if err := c.createKubeadmConfig(ctx, containerName, clusterIP); err != nil {
82+
if err := c.createKubeadmConfig(ctx, containerName, clusterIP, kubeVersion); err != nil {
7183
return fmt.Errorf("failed to create kubeadm config: %w", err)
7284
}
7385

@@ -188,10 +200,11 @@ func (c *Cluster) waitForCalicoCNI(ctx context.Context, sshClient *ssh.Client) e
188200
}
189201

190202
// createKubeadmConfig creates the kubeadm config file in the container
191-
func (c *Cluster) createKubeadmConfig(ctx context.Context, containerName string, advertiseAddress string) error {
203+
func (c *Cluster) createKubeadmConfig(ctx context.Context, containerName string, advertiseAddress string, kubernetesVersion string) error {
192204
var buf bytes.Buffer
193205
if err := kubeadmConfigTemplate.Execute(&buf, kubeadmConfigParams{
194-
AdvertiseAddress: advertiseAddress,
206+
AdvertiseAddress: advertiseAddress,
207+
KubernetesVersion: kubernetesVersion,
195208
}); err != nil {
196209
return fmt.Errorf("failed to render kubeadm config: %w", err)
197210
}

internal/cluster/join.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,22 @@ func (c *Cluster) generateJoinCommand(ctx context.Context, cpSSHClient *ssh.Clie
146146
return "", fmt.Errorf("certificate key is empty")
147147
}
148148

149-
if _, err := cpSSHClient.Exec(ctx, fmt.Sprintf("sudo kubeadm init phase upload-certs --upload-certs --certificate-key %s", certificateKey)); err != nil {
149+
// Write a temp config that includes certificateKey and
150+
// kubernetesVersion, then pass --config to upload-certs.
151+
// --config and --certificate-key are mutually exclusive, so the
152+
// key must be in the config file. Setting kubernetesVersion
153+
// prevents kubeadm from fetching it from dl.k8s.io — that
154+
// remote call eats into the 10s internal timeout and causes
155+
// "rate: Wait(n=1) would exceed context deadline".
156+
writeCmd := fmt.Sprintf(
157+
`sudo cp /etc/kubernetes/kubeadm-config.yaml /tmp/upload-certs-config.yaml && `+
158+
`sudo sed -i '/^kind: InitConfiguration/a certificateKey: %s' /tmp/upload-certs-config.yaml`,
159+
certificateKey)
160+
if _, err := cpSSHClient.Exec(ctx, writeCmd); err != nil {
161+
return "", fmt.Errorf("failed to write upload-certs config: %w", err)
162+
}
163+
164+
if _, err := cpSSHClient.Exec(ctx, "sudo kubeadm init phase upload-certs --upload-certs --config /tmp/upload-certs-config.yaml"); err != nil {
150165
return "", fmt.Errorf("failed to upload certificates: %w", err)
151166
}
152167

internal/cluster/kubeadm-config.yaml.tmpl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ nodeRegistration:
88
---
99
apiVersion: kubeadm.k8s.io/v1beta3
1010
kind: ClusterConfiguration
11+
kubernetesVersion: "{{.KubernetesVersion}}"
1112
controlPlaneEndpoint: "{{.AdvertiseAddress}}:6443"
1213
apiServer:
1314
certSANs:

0 commit comments

Comments
 (0)