1- import childProcess from 'child_process' ;
21import { Console } from 'console' ;
32import crypto from 'crypto' ;
43import events from 'events' ;
54import fs from 'fs' ;
65import os from 'os' ;
76import path from 'path' ;
87import stream from 'stream' ;
8+ import tls from 'tls' ;
99import util from 'util' ;
1010
1111import fetch from 'node-fetch' ;
@@ -14,6 +14,7 @@ import XDGAppPaths from 'xdg-app-paths';
1414import { KubeConfig } from '@kubernetes/client-node' ;
1515import yaml from 'yaml' ;
1616
17+ import * as childProcess from '../utils/childProcess' ;
1718import Logging from '../utils/logging' ;
1819import resources from '../resources' ;
1920import DownloadProgressListener from '../utils/DownloadProgressListener' ;
@@ -356,6 +357,69 @@ export default class K3sHelper extends events.EventEmitter {
356357 }
357358 }
358359
360+ /**
361+ * Wait the K3s server to be ready after starting up.
362+ *
363+ * This will check that the proper TLS certificate is generated by K3s; this
364+ * is required as if the VM IP address changes, K3s will use a certificate
365+ * that is only valid for the old address for a short while. If we attempt to
366+ * communicate with the cluster at this point, things will fail.
367+ *
368+ * @param getHost A function to return the IP address that K3s will listen on
369+ * internally. This may be called multiple times, as the
370+ * address may not be ready yet.
371+ * @param port The port that K3s will listen on.
372+ */
373+ async waitForServerReady ( getHost : ( ) => Promise < string | undefined > , port = 6443 ) : Promise < void > {
374+ let host : string | undefined ;
375+
376+ console . log ( `Waiting for K3s server to be ready on port ${ port } ...` ) ;
377+ while ( true ) {
378+ try {
379+ host = await getHost ( ) ;
380+
381+ if ( typeof host === 'undefined' ) {
382+ await util . promisify ( setTimeout ) ( 500 ) ;
383+ continue ;
384+ }
385+
386+ await new Promise < void > ( ( resolve , reject ) => {
387+ const socket = tls . connect (
388+ {
389+ host, port, rejectUnauthorized : false
390+ } ,
391+ ( ) => {
392+ const { subjectaltname } = socket . getPeerCertificate ( ) ;
393+ const names = subjectaltname . split ( ',' ) . map ( s => s . trim ( ) ) ;
394+ const acceptable = [ `IP Address:${ host } ` , `DNS:${ host } ` ] ;
395+
396+ if ( names . some ( name => acceptable . includes ( name ) ) ) {
397+ // We got a certificate with a SubjectAltName that includes the
398+ // host we're looking for.
399+ resolve ( ) ;
400+ }
401+ reject ( { code : 'ENOHOST' } ) ;
402+ } ) ;
403+
404+ socket . on ( 'error' , reject ) ;
405+ } ) ;
406+ break ;
407+ } catch ( error ) {
408+ switch ( error . code ) {
409+ case 'ENOHOST' :
410+ case 'ECONNREFUSED' :
411+ case 'ECONNRESET' :
412+ break ;
413+ default :
414+ // Unrecognized error; log but continue waiting.
415+ console . error ( error ) ;
416+ }
417+ await util . promisify ( setTimeout ) ( 1_000 ) ;
418+ }
419+ }
420+ console . log ( `The K3s server is ready on ${ host } :${ port } .` ) ;
421+ }
422+
359423 /**
360424 * Find the home directory, in a way that is compatible with the
361425 * @kubernetes /client-node package.
@@ -426,58 +490,23 @@ export default class K3sHelper extends events.EventEmitter {
426490 /**
427491 * Update the user's kubeconfig such that the K3s context is available and
428492 * set as the current context. This assumes that K3s is already running.
493+ *
494+ * @param configReader A function that returns the kubeconfig from the K3s VM.
429495 */
430- async updateKubeconfig ( spawnExecutable : string , ... spawnArgs : string [ ] ) : Promise < void > {
496+ async updateKubeconfig ( configReader : ( ) => Promise < string > ) : Promise < void > {
431497 const contextName = 'rancher-desktop' ;
432498 const workDir = await fs . promises . mkdtemp ( path . join ( os . tmpdir ( ) , 'rancher-desktop-kubeconfig-' ) ) ;
433499
434500 try {
435501 const workPath = path . join ( workDir , 'kubeconfig' ) ;
436- const workFile = await fs . promises . open ( workPath , 'w+' , 0o600 ) ;
437-
438- try {
439- const k3sOptions : childProcess . SpawnOptions = { stdio : [ 'ignore' , workFile . fd , 'inherit' ] } ;
440- const k3sChild = childProcess . spawn ( spawnExecutable , spawnArgs , k3sOptions ) ;
441-
442- console . log ( 'Fetching K3s kubeconfig...' ) ;
443- await new Promise < void > ( ( resolve , reject ) => {
444- k3sChild . on ( 'error' , reject ) ;
445- k3sChild . on ( 'exit' , ( status , signal ) => {
446- if ( status === 0 ) {
447- return resolve ( ) ;
448- }
449- const message = status ? `status ${ status } ` : `signal ${ signal } ` ;
450-
451- reject ( new Error ( `Error getting kubeconfig: exited with ${ message } ` ) ) ;
452- } ) ;
453- } ) ;
454- } finally {
455- await workFile . close ( ) ;
456- }
457-
458- // On Windows repeat until the kubeconfig file is readable
459- let delay = 0 ; // msec
460- const delayIncrement = 200 ;
461- const maxDelay = 10_000 ;
462-
463- while ( delay < maxDelay ) {
464- try {
465- await fs . promises . readFile ( workPath , { encoding : 'utf-8' } ) ;
466- break ;
467- } catch ( err ) {
468- console . log ( `Error reading ${ workPath } : ${ err } ` ) ;
469- console . log ( `Waiting for ${ delay / 1000.0 } sec` ) ;
470- delay += delayIncrement ;
471- await util . promisify ( setTimeout ) ( delay ) ;
472- }
473- }
474502
475503 // For some reason, using KubeConfig.loadFromFile presents permissions
476504 // errors; doing the same ourselves seems to work better. Since the file
477505 // comes from the WSL container, it must not contain any paths, so there
478- // is no need to fix it up.
506+ // is no need to fix it up. This also lets us use an external function to
507+ // read the kubeconfig.
479508 const workConfig = new KubeConfig ( ) ;
480- const workContents = await fs . promises . readFile ( workPath , { encoding : 'utf-8' } ) ;
509+ const workContents = await configReader ( ) ;
481510
482511 workConfig . loadFromString ( workContents ) ;
483512 // @kubernetes /client-node deosn't have an API to modify the configs...
@@ -534,20 +563,8 @@ export default class K3sHelper extends events.EventEmitter {
534563 // The config file we modified might not be the top level one.
535564 // Update the current context.
536565 console . log ( 'Setting default context...' ) ;
537- await new Promise < void > ( ( resolve , reject ) => {
538- const child = childProcess . spawn (
539- resources . executable ( 'kubectl' ) ,
540- [ 'config' , 'use-context' , contextName ] ,
541- { stdio : 'inherit' } ) ;
542-
543- child . on ( 'error' , reject ) ;
544- child . on ( 'exit' , ( status , signal ) => {
545- if ( status !== 0 || signal !== null ) {
546- reject ( new Error ( `kubectl set-context returned with ${ [ status , signal ] } ` ) ) ;
547- }
548- resolve ( ) ;
549- } ) ;
550- } ) ;
566+ await childProcess . spawnFile (
567+ resources . executable ( 'kubectl' ) , [ 'config' , 'use-context' , contextName ] ) ;
551568 } finally {
552569 await fs . promises . rmdir ( workDir , { recursive : true , maxRetries : 10 } ) ;
553570 }
0 commit comments