Skip to content

Commit 5759774

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 fd1da7b commit 5759774

File tree

4 files changed

+90
-67
lines changed

4 files changed

+90
-67
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
}

pkg/commands/toolbox_enroll.go

+80-63
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ type ToolboxEnrollOptions struct {
6767
SSHUser string
6868
SSHPort int
6969

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

113108
// Enroll the node over SSH.
114109
if options.Host != "" {
@@ -117,104 +112,126 @@ func RunToolboxEnroll(ctx context.Context, f commandutils.Factory, out io.Writer
117112
return err
118113
}
119114

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

125-
return nil
126-
}
126+
hostData, err := buildHostData(ctx, target, options)
127+
if err != nil {
128+
return err
129+
}
127130

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

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

145-
host, err := NewSSHHost(ctx, options.Host, options.SSHPort, options.SSHUser, sudo)
146-
if err != nil {
147-
return err
148-
}
149-
defer host.Close()
154+
return nil
155+
}
150156

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

153-
publicKeyBytes, err := host.readFile(ctx, publicKeyPath)
161+
publicKeyBytes, err := target.readFile(ctx, publicKeyPath)
154162
if err != nil {
155163
if errors.Is(err, fs.ErrNotExist) {
156164
publicKeyBytes = nil
157165
} else {
158-
return fmt.Errorf("error reading public key %q: %w", publicKeyPath, err)
166+
return nil, fmt.Errorf("error reading public key %q: %w", publicKeyPath, err)
159167
}
160168
}
161169

170+
// Create the key if it doesn't exist
162171
publicKeyBytes = bytes.TrimSpace(publicKeyBytes)
163172
if len(publicKeyBytes) == 0 {
164-
if _, err := host.runScript(ctx, scriptCreateKey, ExecOptions{Sudo: sudo, Echo: true}); err != nil {
165-
return err
173+
if _, err := target.runScript(ctx, scriptCreateKey, ExecOptions{Echo: true}); err != nil {
174+
return nil, err
166175
}
167176

168-
b, err := host.readFile(ctx, publicKeyPath)
177+
b, err := target.readFile(ctx, publicKeyPath)
169178
if err != nil {
170-
return fmt.Errorf("error reading public key %q (after creation): %w", publicKeyPath, err)
179+
return nil, fmt.Errorf("error reading public key %q (after creation): %w", publicKeyPath, err)
171180
}
172181
publicKeyBytes = b
173182
}
174183
klog.Infof("public key is %s", string(publicKeyBytes))
175184

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

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

189221
for k, v := range bootstrapData.NodeupScriptAdditionalFiles {
190-
if err := host.writeFile(ctx, k, bytes.NewReader(v)); err != nil {
222+
if err := target.writeFile(ctx, k, bytes.NewReader(v)); err != nil {
191223
return fmt.Errorf("writing file %q over SSH: %w", k, err)
192224
}
193225
}
194226

195227
if len(bootstrapData.NodeupScript) != 0 {
196-
if _, err := host.runScript(ctx, string(bootstrapData.NodeupScript), ExecOptions{Sudo: sudo, Echo: true}); err != nil {
228+
if _, err := target.runScript(ctx, string(bootstrapData.NodeupScript), ExecOptions{Echo: true}); err != nil {
197229
return err
198230
}
199231
}
200232
return nil
201233
}
202234

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

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

337354
// ExecOptions holds options for running a command remotely.
338355
type ExecOptions struct {
339-
Sudo bool
340356
Echo bool
341357
}
342358

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

355371
if options.Echo {
356-
session.Stdout = io.MultiWriter(os.Stdout, session.Stdout)
372+
// We send both to stderr, so we don't "corrupt" stdout
373+
session.Stdout = io.MultiWriter(os.Stderr, session.Stdout)
357374
session.Stderr = io.MultiWriter(os.Stderr, session.Stderr)
358375
}
359-
if options.Sudo {
376+
if s.sudo {
360377
command = "sudo " + command
361378
}
362379
if err := session.Run(command); err != nil {
@@ -368,7 +385,7 @@ func (s *SSHHost) runCommand(ctx context.Context, command string, options ExecOp
368385
// getHostname gets the hostname of the SSH target.
369386
// This is used as the node name when registering the node.
370387
func (s *SSHHost) getHostname(ctx context.Context) (string, error) {
371-
output, err := s.runCommand(ctx, "hostname", ExecOptions{Sudo: false, Echo: true})
388+
output, err := s.runCommand(ctx, "hostname", ExecOptions{Echo: true})
372389
if err != nil {
373390
return "", fmt.Errorf("failed to get hostname: %w", err)
374391
}

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)