@@ -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-
218235const scriptCreateKey = `
219236#!/bin/bash
220237set -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.
338355type 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.
370387func (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 }
0 commit comments