diff --git a/pkg/rancher-desktop/backend/k3sHelper.ts b/pkg/rancher-desktop/backend/k3sHelper.ts index 0b05f503e30..5c9d3b0a6f2 100644 --- a/pkg/rancher-desktop/backend/k3sHelper.ts +++ b/pkg/rancher-desktop/backend/k3sHelper.ts @@ -522,7 +522,12 @@ export default class K3sHelper extends events.EventEmitter { return; } - await this.updateCache(); + try { + await this.updateCache(); + } catch (ex) { + console.log(`Ignoring failure to get initial versions list: ${ ex }`); + // At this point this.versions is still empty. + } })(); } this.versionFromChannel = {}; @@ -567,6 +572,8 @@ export default class K3sHelper extends events.EventEmitter { /** * The versions that are available to install. + * @note The list will be empty if the machine is offline and we have no + * cached versions. */ get availableVersions(): Promise { return (async() => { diff --git a/pkg/rancher-desktop/backend/kube/lima.ts b/pkg/rancher-desktop/backend/kube/lima.ts index 78cb3cdf645..4f6d7ef8df6 100644 --- a/pkg/rancher-desktop/backend/kube/lima.ts +++ b/pkg/rancher-desktop/backend/kube/lima.ts @@ -112,7 +112,7 @@ export default class LimaKubernetesBackend extends events.EventEmitter implement const result = await showMessageBox(options, true); if (result.response !== 0) { - return [undefined, false]; + return [undefined, true]; } } console.log(`Going with alternative version ${ newVersion.raw }`); @@ -324,20 +324,24 @@ export default class LimaKubernetesBackend extends events.EventEmitter implement protected get desiredVersion(): Promise { return (async() => { let availableVersions: SemanticVersionEntry[]; + let available = true; try { availableVersions = await this.k3sHelper.availableVersions; + + return await BackendHelper.getDesiredVersion( + this.cfg as BackendSettings, + availableVersions, + this.vm.noModalDialogs, + this.vm.writeSetting.bind(this.vm)); } catch (ex) { console.error(`Could not get desired version: ${ ex }`); + available = false; return undefined; + } finally { + mainEvents.emit('diagnostics-event', { id: 'kube-versions-available', available }); } - - return await BackendHelper.getDesiredVersion( - this.cfg as BackendSettings, - availableVersions, - this.vm.noModalDialogs, - this.vm.writeSetting.bind(this.vm)); })(); } diff --git a/pkg/rancher-desktop/backend/kube/wsl.ts b/pkg/rancher-desktop/backend/kube/wsl.ts index 231d87d3127..716fa17bbfd 100644 --- a/pkg/rancher-desktop/backend/kube/wsl.ts +++ b/pkg/rancher-desktop/backend/kube/wsl.ts @@ -77,20 +77,24 @@ export default class WSLKubernetesBackend extends events.EventEmitter implements protected get desiredVersion(): Promise { return (async() => { let availableVersions: SemanticVersionEntry[]; + let available = true; try { availableVersions = await this.k3sHelper.availableVersions; + + return await BackendHelper.getDesiredVersion( + this.cfg as BackendSettings, + availableVersions, + this.vm.noModalDialogs, + this.vm.writeSetting.bind(this.vm)); } catch (ex) { console.error(`Could not get desired version: ${ ex }`); + available = false; return undefined; + } finally { + mainEvents.emit('diagnostics-event', { id: 'kube-versions-available', available }); } - - return await BackendHelper.getDesiredVersion( - this.cfg as BackendSettings, - availableVersions, - this.vm.noModalDialogs, - this.vm.writeSetting.bind(this.vm)); })(); } diff --git a/pkg/rancher-desktop/backend/lima.ts b/pkg/rancher-desktop/backend/lima.ts index 7cc8471f904..ffb468fb892 100644 --- a/pkg/rancher-desktop/backend/lima.ts +++ b/pkg/rancher-desktop/backend/lima.ts @@ -1813,14 +1813,23 @@ export default class LimaBackend extends events.EventEmitter implements VMBacken // Start the VM; if it's already running, this does nothing. await this.startVM(); + // Clear the diagnostic about not having Kubernetes versions + mainEvents.emit('diagnostics-event', { id: 'kube-versions-available', available: true }); + if (config.kubernetes.enabled) { [kubernetesVersion, isDowngrade] = await this.kubeBackend.download(config); - if (typeof (kubernetesVersion) === 'undefined') { - // The desired version was unavailable, and the user declined a downgrade. - await this.setState(State.ERROR); + if (kubernetesVersion === undefined) { + if (isDowngrade) { + // The desired version was unavailable, and the user declined a downgrade. + await this.setState(State.ERROR); - return; + return; + } + // The desired version was unavailable, and we couldn't find a fallback. + // Notify the user, and turn off Kubernetes. + mainEvents.emit('diagnostics-event', { id: 'kube-versions-available', available: false }); + this.writeSetting({ kubernetes: { enabled: false } }); } } diff --git a/pkg/rancher-desktop/backend/wsl.ts b/pkg/rancher-desktop/backend/wsl.ts index 890d7aa5b3c..8e0177fb241 100644 --- a/pkg/rancher-desktop/backend/wsl.ts +++ b/pkg/rancher-desktop/backend/wsl.ts @@ -1189,6 +1189,7 @@ export default class WSLBackend extends events.EventEmitter implements VMBackend const config = this.cfg = _.defaultsDeep(clone(config_), { containerEngine: { name: ContainerEngine.NONE } }); let kubernetesVersion: semver.SemVer | undefined; + let isDowngrade = false; await this.setState(State.STARTING); this.currentAction = Action.STARTING; @@ -1203,16 +1204,25 @@ export default class WSLBackend extends events.EventEmitter implements VMBackend if (config.kubernetes.enabled) { prepActions.push((async() => { - [kubernetesVersion] = await this.kubeBackend.download(config); + [kubernetesVersion, isDowngrade] = await this.kubeBackend.download(config); })()); } + // Clear the diagnostic about not having Kubernetes versions + mainEvents.emit('diagnostics-event', { id: 'kube-versions-available', available: true }); + await this.progressTracker.action('Preparing to start', 0, Promise.all(prepActions)); - if (config.kubernetes.enabled && typeof (kubernetesVersion) === 'undefined') { - // The desired version was unavailable, and the user declined a downgrade. - this.setState(State.ERROR); + if (config.kubernetes.enabled && kubernetesVersion === undefined) { + if (isDowngrade) { + // The desired version was unavailable, and the user declined a downgrade. + this.setState(State.ERROR); - return; + return; + } + // The desired version was unavailable, and we couldn't find a fallback. + // Notify the user, and turn off Kubernetes. + mainEvents.emit('diagnostics-event', { id: 'kube-versions-available', available: false }); + this.writeSetting({ kubernetes: { enabled: false } }); } if (this.currentAction !== Action.STARTING) { // User aborted before we finished diff --git a/pkg/rancher-desktop/integrations/pathManagerImpl.ts b/pkg/rancher-desktop/integrations/pathManagerImpl.ts index 6e1dce43f29..4db37e95417 100644 --- a/pkg/rancher-desktop/integrations/pathManagerImpl.ts +++ b/pkg/rancher-desktop/integrations/pathManagerImpl.ts @@ -60,9 +60,13 @@ export class RcFilePathManager implements PathManager { protected async manageLinesInFile(fileName: string, filePath: string, lines: string[], desiredPresent: boolean) { try { await manageLinesInFile(filePath, lines, desiredPresent); - mainEvents.emit('diagnostics-event', 'path-management', { fileName, error: undefined }); + mainEvents.emit('diagnostics-event', { + id: 'path-management', fileName, error: undefined, + }); } catch (error: any) { - mainEvents.emit('diagnostics-event', 'path-management', { fileName, error }); + mainEvents.emit('diagnostics-event', { + id: 'path-management', fileName, error, + }); throw error; } } @@ -96,10 +100,14 @@ export class RcFilePathManager implements PathManager { } catch (error: any) { if (error.code === 'ENOENT') { // If the file does not exist, it is not an error. - mainEvents.emit('diagnostics-event', 'path-management', { fileName, error: undefined }); + mainEvents.emit('diagnostics-event', { + id: 'path-management', fileName, error: undefined, + }); continue; } - mainEvents.emit('diagnostics-event', 'path-management', { fileName, error }); + mainEvents.emit('diagnostics-event', { + id: 'path-management', fileName, error, + }); throw error; } await this.manageLinesInFile(fileName, filePath, [pathLine], !linesAdded); diff --git a/pkg/rancher-desktop/main/diagnostics/diagnostics.ts b/pkg/rancher-desktop/main/diagnostics/diagnostics.ts index d492f733130..bc52303bae1 100644 --- a/pkg/rancher-desktop/main/diagnostics/diagnostics.ts +++ b/pkg/rancher-desktop/main/diagnostics/diagnostics.ts @@ -50,6 +50,7 @@ export class DiagnosticsManager { import('./dockerCliSymlinks'), import('./kubeConfigSymlink'), import('./kubeContext'), + import('./kubeVersionsAvailable'), import('./limaDarwin'), import('./mockForScreenshots'), import('./pathManagement'), diff --git a/pkg/rancher-desktop/main/diagnostics/kubeVersionsAvailable.ts b/pkg/rancher-desktop/main/diagnostics/kubeVersionsAvailable.ts new file mode 100644 index 00000000000..a698d3cc6cb --- /dev/null +++ b/pkg/rancher-desktop/main/diagnostics/kubeVersionsAvailable.ts @@ -0,0 +1,44 @@ +import mainEvents from '../mainEvents'; +import { DiagnosticsCategory, DiagnosticsChecker, DiagnosticsCheckerResult } from './types'; + +let kubeVersionsAvailable = true; + +mainEvents.on('diagnostics-event', (payload) => { + if (payload.id !== 'kube-versions-available') { + return; + } + kubeVersionsAvailable = payload.available; + mainEvents.invoke('diagnostics-trigger', instance.id); +}); + +/** + * KubeVersionsAvailable is a diagnostic that will be emitted when all of the + * following are met: + * - Kubernetes was configured to be enabled + * - The selected Kubernetes version is unavailable (e.g. user is offline) + * Once the diagnostic is triggered, it stays on until the backend is restarted. + */ +class KubeVersionsAvailable implements DiagnosticsChecker { + readonly id = 'KUBE_VERSIONS_AVAILABLE'; + readonly category = DiagnosticsCategory.Kubernetes; + applicable(): Promise { + return Promise.resolve(true); + } + + check(): Promise { + const description = [ + 'There are no issues with Kubernetes versions', + 'Kubernetes has been disabled due to issues with fetching Kubernetes versions', + ][kubeVersionsAvailable ? 0 : 1]; + + return Promise.resolve({ + passed: kubeVersionsAvailable, + description, + fixes: [{ description: 'Check your network connection to update.k3s.io' }], + }); + } +} + +const instance = new KubeVersionsAvailable(); + +export default instance; diff --git a/pkg/rancher-desktop/main/diagnostics/pathManagement.ts b/pkg/rancher-desktop/main/diagnostics/pathManagement.ts index a2ca762807c..37766999cbe 100644 --- a/pkg/rancher-desktop/main/diagnostics/pathManagement.ts +++ b/pkg/rancher-desktop/main/diagnostics/pathManagement.ts @@ -25,12 +25,11 @@ const CheckPathManagement: DiagnosticsChecker = { }, }; -mainEvents.on('diagnostics-event', (id, state) => { - if (id !== 'path-management') { +mainEvents.on('diagnostics-event', (payload) => { + if (payload.id !== 'path-management') { return; } - const typedState: { fileName: string, error: Error | undefined } = state; - const { fileName, error } = typedState; + const { fileName, error } = payload; cachedResults[fileName] = { description: error?.message ?? error?.toString() ?? `Unknown error managing ${ fileName }`, diff --git a/pkg/rancher-desktop/main/mainEvents.ts b/pkg/rancher-desktop/main/mainEvents.ts index b1dcb96ec16..dcb7c1a0666 100644 --- a/pkg/rancher-desktop/main/mainEvents.ts +++ b/pkg/rancher-desktop/main/mainEvents.ts @@ -111,7 +111,7 @@ interface MainEventNames { * @param id The diagnostic identifier. * @param state The new state for the diagnostic. */ - 'diagnostics-event'(id: K, state: DiagnosticsEventPayload[K]): void; + 'diagnostics-event'(payload: DiagnosticsEventPayload): void; /** * Emitted when an extension is uninstalled via the extension manager. @@ -154,9 +154,9 @@ interface MainEventNames { * DiagnosticsEventPayload defines the data that will be passed on a * 'diagnostics-event' event. */ -type DiagnosticsEventPayload = { - 'path-management': { fileName: string; error: Error | undefined }; -}; +type DiagnosticsEventPayload = + { id: 'kube-versions-available', available: boolean } | + { id: 'path-management', fileName: string; error: Error | undefined }; /** * Helper type definition to check if the given event name is a handler (i.e.