Skip to content

Commit ebd96f9

Browse files
committed
metal: split host creation from enrollment
This is needeed for bootstrapping the control plane, because it's a CRD so can't be registered until the control plane is running. It's also quite nice because we might want to review the contents of the host CRD, e.g. to verify the key out-of-band.
1 parent 82f11ac commit ebd96f9

File tree

5 files changed

+87
-63
lines changed

5 files changed

+87
-63
lines changed

cmd/kops/toolbox_enroll.go

+2
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,7 @@ func NewCmdToolboxEnroll(f commandutils.Factory, out io.Writer) *cobra.Command {
5151
cmd.Flags().StringVar(&options.SSHUser, "ssh-user", options.SSHUser, "user for ssh")
5252
cmd.Flags().IntVar(&options.SSHPort, "ssh-port", options.SSHPort, "port for ssh")
5353

54+
cmd.Flags().BoolVar(&options.BuildHost, "build-host", options.BuildHost, "only build the host resource, don't apply it or enroll the node")
55+
5456
return cmd
5557
}

docs/cli/kops_toolbox_enroll.md

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/commands/toolbox_enroll.go

+76-59
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ type ToolboxEnrollOptions struct {
6666
SSHUser string
6767
SSHPort int
6868

69+
// BuildHost is a flag to only build the host resource, don't apply it or enroll the node
70+
BuildHost bool
71+
6972
// PodCIDRs is the list of IP Address ranges to use for pods that run on this node
7073
PodCIDRs []string
7174
}
@@ -100,14 +103,6 @@ func RunToolboxEnroll(ctx context.Context, f commandutils.Factory, out io.Writer
100103
if err != nil {
101104
return err
102105
}
103-
fullInstanceGroup, err := configBuilder.GetFullInstanceGroup(ctx)
104-
if err != nil {
105-
return err
106-
}
107-
bootstrapData, err := configBuilder.GetBootstrapData(ctx)
108-
if err != nil {
109-
return err
110-
}
111106

112107
// Enroll the node over SSH.
113108
if options.Host != "" {
@@ -116,72 +111,109 @@ func RunToolboxEnroll(ctx context.Context, f commandutils.Factory, out io.Writer
116111
return err
117112
}
118113

119-
if err := enrollHost(ctx, fullInstanceGroup, options, bootstrapData, restConfig); err != nil {
114+
sudo := true
115+
if options.SSHUser == "root" {
116+
sudo = false
117+
}
118+
119+
sshTarget, err := NewSSHHost(ctx, options.Host, options.SSHPort, options.SSHUser, sudo)
120+
if err != nil {
120121
return err
121122
}
122-
}
123+
defer sshTarget.Close()
123124

124-
return nil
125-
}
125+
hostData, err := buildHostData(ctx, sshTarget, options)
126+
if err != nil {
127+
return err
128+
}
126129

127-
func enrollHost(ctx context.Context, ig *kops.InstanceGroup, options *ToolboxEnrollOptions, bootstrapData *BootstrapData, restConfig *rest.Config) error {
128-
scheme := runtime.NewScheme()
129-
if err := v1alpha2.AddToScheme(scheme); err != nil {
130-
return fmt.Errorf("building kubernetes scheme: %w", err)
131-
}
132-
kubeClient, err := client.New(restConfig, client.Options{
133-
Scheme: scheme,
134-
})
135-
if err != nil {
136-
return fmt.Errorf("building kubernetes client: %w", err)
137-
}
130+
if options.BuildHost {
131+
klog.Infof("building host data for %+v", hostData)
132+
b, err := yaml.Marshal(hostData)
133+
if err != nil {
134+
return fmt.Errorf("error marshalling host data: %w", err)
135+
}
136+
fmt.Fprintf(out, "%s\n", string(b))
137+
} else {
138+
fullInstanceGroup, err := configBuilder.GetFullInstanceGroup(ctx)
139+
if err != nil {
140+
return err
141+
}
142+
bootstrapData, err := configBuilder.GetBootstrapData(ctx)
143+
if err != nil {
144+
return err
145+
}
138146

139-
sudo := true
140-
if options.SSHUser == "root" {
141-
sudo = false
147+
if err := enrollHost(ctx, fullInstanceGroup, bootstrapData, restConfig, hostData, sshTarget); err != nil {
148+
return err
149+
}
150+
}
142151
}
143152

144-
sshTarget, err := NewSSHHost(ctx, options.Host, options.SSHPort, options.SSHUser, sudo)
145-
if err != nil {
146-
return err
147-
}
148-
defer sshTarget.Close()
153+
return nil
154+
}
149155

156+
// buildHostData builds an instance of the Host CRD, based on information in the options and by SSHing to the target host.
157+
func buildHostData(ctx context.Context, sshTarget *SSHHost, options *ToolboxEnrollOptions) (*v1alpha2.Host, error) {
150158
publicKeyPath := "/etc/kubernetes/kops/pki/machine/public.pem"
151159

152160
publicKeyBytes, err := sshTarget.readFile(ctx, publicKeyPath)
153161
if err != nil {
154162
if errors.Is(err, fs.ErrNotExist) {
155163
publicKeyBytes = nil
156164
} else {
157-
return fmt.Errorf("error reading public key %q: %w", publicKeyPath, err)
165+
return nil, fmt.Errorf("error reading public key %q: %w", publicKeyPath, err)
158166
}
159167
}
160168

169+
// Create the key if it doesn't exist
161170
publicKeyBytes = bytes.TrimSpace(publicKeyBytes)
162171
if len(publicKeyBytes) == 0 {
163-
if _, err := sshTarget.runScript(ctx, scriptCreateKey, ExecOptions{Sudo: sudo, Echo: true}); err != nil {
164-
return err
172+
if _, err := sshTarget.runScript(ctx, scriptCreateKey, ExecOptions{Echo: true}); err != nil {
173+
return nil, err
165174
}
166175

167176
b, err := sshTarget.readFile(ctx, publicKeyPath)
168177
if err != nil {
169-
return fmt.Errorf("error reading public key %q (after creation): %w", publicKeyPath, err)
178+
return nil, fmt.Errorf("error reading public key %q (after creation): %w", publicKeyPath, err)
170179
}
171180
publicKeyBytes = b
172181
}
173182
klog.Infof("public key is %s", string(publicKeyBytes))
174183

175184
hostname, err := sshTarget.getHostname(ctx)
176185
if err != nil {
177-
return err
186+
return nil, err
187+
}
188+
189+
host := &v1alpha2.Host{}
190+
host.SetGroupVersionKind(v1alpha2.SchemeGroupVersion.WithKind("Host"))
191+
host.Namespace = "kops-system"
192+
host.Name = hostname
193+
host.Spec.InstanceGroup = options.InstanceGroup
194+
host.Spec.PublicKey = string(publicKeyBytes)
195+
host.Spec.PodCIDRs = options.PodCIDRs
196+
197+
return host, nil
198+
}
199+
200+
func enrollHost(ctx context.Context, ig *kops.InstanceGroup, bootstrapData *BootstrapData, restConfig *rest.Config, hostData *v1alpha2.Host, sshTarget *SSHHost) error {
201+
scheme := runtime.NewScheme()
202+
if err := v1alpha2.AddToScheme(scheme); err != nil {
203+
return fmt.Errorf("building kubernetes scheme: %w", err)
204+
}
205+
kubeClient, err := client.New(restConfig, client.Options{
206+
Scheme: scheme,
207+
})
208+
if err != nil {
209+
return fmt.Errorf("building kubernetes client: %w", err)
178210
}
179211

180212
// We can't create the host resource in the API server for control-plane nodes,
181213
// because the API server (likely) isn't running yet.
182214
if !ig.IsControlPlane() {
183-
if err := createHostResourceInAPIServer(ctx, options, hostname, publicKeyBytes, kubeClient); err != nil {
184-
return err
215+
if err := kubeClient.Create(ctx, hostData); err != nil {
216+
return fmt.Errorf("failed to create host %s/%s: %w", hostData.Namespace, hostData.Name, err)
185217
}
186218
}
187219

@@ -192,28 +224,13 @@ func enrollHost(ctx context.Context, ig *kops.InstanceGroup, options *ToolboxEnr
192224
}
193225

194226
if len(bootstrapData.NodeupScript) != 0 {
195-
if _, err := sshTarget.runScript(ctx, string(bootstrapData.NodeupScript), ExecOptions{Sudo: sudo, Echo: true}); err != nil {
227+
if _, err := sshTarget.runScript(ctx, string(bootstrapData.NodeupScript), ExecOptions{Echo: true}); err != nil {
196228
return err
197229
}
198230
}
199231
return nil
200232
}
201233

202-
func createHostResourceInAPIServer(ctx context.Context, options *ToolboxEnrollOptions, nodeName string, publicKey []byte, client client.Client) error {
203-
host := &v1alpha2.Host{}
204-
host.Namespace = "kops-system"
205-
host.Name = nodeName
206-
host.Spec.InstanceGroup = options.InstanceGroup
207-
host.Spec.PublicKey = string(publicKey)
208-
host.Spec.PodCIDRs = options.PodCIDRs
209-
210-
if err := client.Create(ctx, host); err != nil {
211-
return fmt.Errorf("failed to create host %s/%s: %w", host.Namespace, host.Name, err)
212-
}
213-
214-
return nil
215-
}
216-
217234
const scriptCreateKey = `
218235
#!/bin/bash
219236
set -o errexit
@@ -314,7 +331,7 @@ func (s *SSHHost) runScript(ctx context.Context, script string, options ExecOpti
314331
p := vfs.NewSSHPath(s.sshClient, s.hostname, scriptPath, s.sudo)
315332

316333
defer func() {
317-
if _, err := s.runCommand(ctx, "rm -rf "+tempDir, ExecOptions{Sudo: s.sudo, Echo: false}); err != nil {
334+
if _, err := s.runCommand(ctx, "rm -rf "+tempDir, ExecOptions{Echo: false}); err != nil {
318335
klog.Warningf("error cleaning up temp directory %q: %v", tempDir, err)
319336
}
320337
}()
@@ -335,7 +352,6 @@ type CommandOutput struct {
335352

336353
// ExecOptions holds options for running a command remotely.
337354
type ExecOptions struct {
338-
Sudo bool
339355
Echo bool
340356
}
341357

@@ -352,10 +368,11 @@ func (s *SSHHost) runCommand(ctx context.Context, command string, options ExecOp
352368
session.Stderr = &output.Stderr
353369

354370
if options.Echo {
355-
session.Stdout = io.MultiWriter(os.Stdout, session.Stdout)
371+
// We send both to stderr, so we don't "corrupt" stdout
372+
session.Stdout = io.MultiWriter(os.Stderr, session.Stdout)
356373
session.Stderr = io.MultiWriter(os.Stderr, session.Stderr)
357374
}
358-
if options.Sudo {
375+
if s.sudo {
359376
command = "sudo " + command
360377
}
361378
if err := session.Run(command); err != nil {
@@ -367,7 +384,7 @@ func (s *SSHHost) runCommand(ctx context.Context, command string, options ExecOp
367384
// getHostname gets the hostname of the SSH target.
368385
// This is used as the node name when registering the node.
369386
func (s *SSHHost) getHostname(ctx context.Context) (string, error) {
370-
output, err := s.runCommand(ctx, "hostname", ExecOptions{Sudo: false, Echo: true})
387+
output, err := s.runCommand(ctx, "hostname", ExecOptions{Echo: true})
371388
if err != nil {
372389
return "", fmt.Errorf("failed to get hostname: %w", err)
373390
}

tests/e2e/scenarios/bare-metal/scenario-ipv6

+7-3
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,13 @@ for i in {1..60}; do
237237
sleep 10
238238
done
239239

240+
# Create CRD and namespace for host records
241+
kubectl create ns kops-system
242+
kubectl apply -f ${REPO_ROOT}/k8s/crds/kops.k8s.io_hosts.yaml
243+
244+
# Create the host record (we can't auto create for control plane nodes)
245+
${KOPS} toolbox enroll --cluster ${CLUSTER_NAME} --instance-group control-plane-main --host ${VM0_IP} --pod-cidr ${VM0_POD_CIDR} --v=2 --build-host | kubectl apply -f -
246+
240247
kubectl get nodes
241248
kubectl get pods -A
242249

@@ -247,9 +254,6 @@ sleep 10
247254
kubectl get nodes
248255
kubectl get pods -A
249256

250-
# For host records
251-
kubectl create ns kops-system
252-
kubectl apply -f ${REPO_ROOT}/k8s/crds/kops.k8s.io_hosts.yaml
253257

254258
# kops-controller extra permissions
255259
kubectl apply --server-side -f - <<EOF

upup/pkg/fi/loader/options_loader.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ func (l *OptionsLoader[T]) iterate(userConfig T, current T) (T, error) {
5555
reflectutils.JSONMergeStruct(next, current)
5656

5757
for _, t := range l.Builders {
58-
klog.V(2).Infof("executing builder %T", t)
58+
klog.V(4).Infof("executing builder %T", t)
5959

6060
err := t.BuildOptions(next)
6161
if err != nil {

0 commit comments

Comments
 (0)