@@ -132,19 +132,40 @@ func (c *Cluster) Join(ctx context.Context, opts JoinOptions) error {
132132 return nil
133133}
134134
135+ func isHex (s string ) bool {
136+ for _ , c := range s {
137+ switch {
138+ case c >= '0' && c <= '9' :
139+ case c >= 'a' && c <= 'f' :
140+ case c >= 'A' && c <= 'F' :
141+ default :
142+ return false
143+ }
144+ }
145+ return true
146+ }
147+
135148// generateJoinCommand generates a fresh join command from the control plane
136149func (c * Cluster ) generateJoinCommand (ctx context.Context , cpSSHClient * ssh.Client , isControlPlane bool ) (string , error ) {
137150 if isControlPlane {
138- // For control-plane nodes, we need to upload certificates and get the certificate key
151+ // For control-plane nodes, we need to upload certificates and get the certificate key.
152+ // The command prints log lines to stderr and the 64-char hex key on stdout.
153+ // We avoid piping (which masks the exit code) and extract the key ourselves.
139154 c .logger .Info ("Uploading certificates for control-plane join..." )
140- certKeyOutput , err := cpSSHClient .Exec (ctx , "sudo kubeadm init phase upload-certs --upload-certs 2>/dev/null | tail -1 " )
155+ certKeyOutput , err := cpSSHClient .Exec (ctx , "sudo kubeadm init phase upload-certs --upload-certs --config /etc/kubernetes/kubeadm-config.yaml " )
141156 if err != nil {
142157 return "" , fmt .Errorf ("failed to upload certificates: %w" , err )
143158 }
144159
145- certificateKey := strings .TrimSpace (certKeyOutput )
160+ var certificateKey string
161+ for _ , line := range strings .Split (certKeyOutput , "\n " ) {
162+ line = strings .TrimSpace (line )
163+ if len (line ) == 64 && isHex (line ) {
164+ certificateKey = line
165+ }
166+ }
146167 if certificateKey == "" {
147- return "" , fmt .Errorf ("certificate key is empty" )
168+ return "" , fmt .Errorf ("certificate key not found in upload-certs output: %s" , certKeyOutput )
148169 }
149170
150171 c .logger .Infof ("Certificate key: %s" , certificateKey )
0 commit comments