From 83a182ffd6f5d2568ec2887c9912583c161320ba Mon Sep 17 00:00:00 2001 From: Mark Yen Date: Mon, 6 Jan 2025 16:27:34 -0800 Subject: [PATCH 1/3] integration: win32: cope when we fail to get kubeconfig If we couldn't find a correct path for kubeconfig (e.g. because the home directory is not writable), log an error instead of throwing, as the latter breaks startup. Signed-off-by: Mark Yen --- .../integrations/windowsIntegrationManager.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/pkg/rancher-desktop/integrations/windowsIntegrationManager.ts b/pkg/rancher-desktop/integrations/windowsIntegrationManager.ts index 74eb2d1ba6c..fb3faed5e15 100644 --- a/pkg/rancher-desktop/integrations/windowsIntegrationManager.ts +++ b/pkg/rancher-desktop/integrations/windowsIntegrationManager.ts @@ -177,7 +177,14 @@ export default class WindowsIntegrationManager implements IntegrationManager { return this.syncState.queued; } try { - const kubeconfigPath = await K3sHelper.findKubeConfigToUpdate('rancher-desktop'); + let kubeconfigPath: string | undefined; + + try { + kubeconfigPath = await K3sHelper.findKubeConfigToUpdate('rancher-desktop'); + } catch (ex) { + console.error(`Could not determine kubeconfig: ${ ex } - Kubernetes configuration will not be updated.`); + kubeconfigPath = undefined; + } await Promise.all([ this.syncHostSocketProxy(), @@ -208,7 +215,7 @@ export default class WindowsIntegrationManager implements IntegrationManager { } } - async syncDistro(distro: string, kubeconfigPath: string): Promise { + async syncDistro(distro: string, kubeconfigPath?: string): Promise { let state = this.settings.WSL?.integrations?.[distro] === true; console.debug(`Integration sync: ${ distro } -> ${ state }`); @@ -490,7 +497,12 @@ export default class WindowsIntegrationManager implements IntegrationManager { console.debug(`Verified kubeconfig in the following distro: ${ distro }`); } - protected async syncDistroKubeconfig(distro: string, kubeconfigPath: string, state: boolean) { + protected async syncDistroKubeconfig(distro: string, kubeconfigPath: string | undefined, state: boolean) { + if (!kubeconfigPath) { + console.debug(`Skipping syncing ${ distro } kubeconfig: no kubeconfig found`); + + return 'Error setting up integration'; + } try { console.debug(`Syncing ${ distro } kubeconfig`); await this.execCommand( From a755b415b158d060e33afa8e7cf7d4f30b6b0127 Mon Sep 17 00:00:00 2001 From: Mark Yen Date: Mon, 6 Jan 2025 17:53:42 -0800 Subject: [PATCH 2/3] integration: win32: Report errors as diagnostics This makes the Windows integration report errors as diagnostics. The messages aren't quite detailed enough yet, but it's enough to let the user know there is a problem. Signed-off-by: Mark Yen --- .../integrations/windowsIntegrationManager.ts | 141 ++++++++++++------ .../main/diagnostics/diagnostics.ts | 1 + .../main/diagnostics/integrationsWindows.ts | 43 ++++++ pkg/rancher-desktop/main/mainEvents.ts | 1 + 4 files changed, 144 insertions(+), 42 deletions(-) create mode 100644 pkg/rancher-desktop/main/diagnostics/integrationsWindows.ts diff --git a/pkg/rancher-desktop/integrations/windowsIntegrationManager.ts b/pkg/rancher-desktop/integrations/windowsIntegrationManager.ts index fb3faed5e15..2d913999afd 100644 --- a/pkg/rancher-desktop/integrations/windowsIntegrationManager.ts +++ b/pkg/rancher-desktop/integrations/windowsIntegrationManager.ts @@ -64,6 +64,16 @@ type SyncState = /** The `queued` promise will be resolved after the current sync +1 is complete. */ { state: SyncStateKey.QUEUED, active: ReturnType, queued: ReturnType }; +/** + * DiagnosticKey limits the `key` argument of the diagnostic events. + */ +type DiagnosticKey = + 'docker-plugins' | + 'docker-socket' | + 'kubeconfig' | + 'spin-cli' | + never; + /** * WindowsIntegrationManager manages various integrations on Windows, for both * the Win32 host, as well as for each (foreign) WSL distribution. @@ -181,8 +191,10 @@ export default class WindowsIntegrationManager implements IntegrationManager { try { kubeconfigPath = await K3sHelper.findKubeConfigToUpdate('rancher-desktop'); - } catch (ex) { - console.error(`Could not determine kubeconfig: ${ ex } - Kubernetes configuration will not be updated.`); + this.diagnostic({ key: 'kubeconfig' }); + } catch (error) { + console.error(`Could not determine kubeconfig: ${ error } - Kubernetes configuration will not be updated.`); + this.diagnostic({ key: 'kubeconfig', error }); kubeconfigPath = undefined; } @@ -235,6 +247,21 @@ export default class WindowsIntegrationManager implements IntegrationManager { } } + /** + * Helper function to trigger a diagnostic report. If a diagnostic should be + * cleared, call this with the error unset. + */ + protected diagnostic(input: {key: DiagnosticKey, distro?: string, error?: unknown}) { + const error = input.error instanceof Error ? input.error : input.error ? new Error(`${ input.error }`) : undefined; + + mainEvents.emit('diagnostics-event', { + id: 'integrations-windows', + key: input.key, + distro: input.distro, + error, + }); + } + #wslExe = ''; /** * The path to the wsl.exe executable. @@ -333,10 +360,15 @@ export default class WindowsIntegrationManager implements IntegrationManager { const reason = this.dockerSocketProxyReason; console.debug(`Syncing Win32 socket proxy: ${ reason ? `should not run (${ reason })` : 'should run' }`); - if (!reason) { - this.windowsSocketProxyProcess.start(); - } else { - await this.windowsSocketProxyProcess.stop(); + try { + if (!reason) { + this.windowsSocketProxyProcess.start(); + } else { + await this.windowsSocketProxyProcess.stop(); + } + this.diagnostic({ key: 'docker-socket' }); + } catch (error) { + this.diagnostic({ key: 'docker-socket', error }); } } @@ -361,7 +393,6 @@ export default class WindowsIntegrationManager implements IntegrationManager { * distribution is started or stopped, as desired. * @param distro The distribution to manage. * @param state Whether integration is enabled for the given distro. - * @note this function must not throw. */ protected async syncDistroSocketProxy(distro: string, state: boolean) { try { @@ -400,39 +431,49 @@ export default class WindowsIntegrationManager implements IntegrationManager { delete this.distroSocketProxyProcesses[distro]; } } + this.diagnostic({ key: 'docker-socket', distro }); } catch (error) { console.error(`Error syncing ${ distro } distro socket proxy: ${ error }`); + this.diagnostic({ + key: 'docker-socket', distro, error, + }); } } protected async syncHostDockerPluginConfig() { - const configPath = path.join(os.homedir(), '.docker', 'config.json'); - let config: { cliPluginsExtraDirs?: string[] } = {}; - try { - config = JSON.parse(await fs.promises.readFile(configPath, 'utf-8')); - } catch (ex) { - if (ex && typeof ex === 'object' && 'code' in ex && ex.code === 'ENOENT') { - // If the file does not exist, create it. - } else { - console.error(`Could not set up docker plugins:`, ex); + const configPath = path.join(os.homedir(), '.docker', 'config.json'); + let config: { cliPluginsExtraDirs?: string[] } = {}; - return; + try { + config = JSON.parse(await fs.promises.readFile(configPath, 'utf-8')); + } catch (error) { + if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') { + // If the file does not exist, create it. + } else { + console.error(`Could not set up docker plugins:`, error); + this.diagnostic({ key: 'docker-plugins', error }); + + return; + } } - } - // All of the docker plugins are in the `docker-cli-plugins` directory. - const binDir = path.join(paths.resources, process.platform, 'docker-cli-plugins'); + // All of the docker plugins are in the `docker-cli-plugins` directory. + const binDir = path.join(paths.resources, process.platform, 'docker-cli-plugins'); - if (config.cliPluginsExtraDirs?.includes(binDir)) { - // If it's already configured, no need to do so again. - return; - } + if (config.cliPluginsExtraDirs?.includes(binDir)) { + // If it's already configured, no need to do so again. + return; + } - config.cliPluginsExtraDirs ??= []; - config.cliPluginsExtraDirs.push(binDir); + config.cliPluginsExtraDirs ??= []; + config.cliPluginsExtraDirs.push(binDir); - await fs.promises.writeFile(configPath, JSON.stringify(config), 'utf-8'); + await fs.promises.writeFile(configPath, JSON.stringify(config), 'utf-8'); + this.diagnostic({ key: 'docker-plugins' }); + } catch (error) { + this.diagnostic({ key: 'docker-plugins', error }); + } } /** @@ -456,8 +497,12 @@ export default class WindowsIntegrationManager implements IntegrationManager { } await this.execCommand({ distro }, wslHelper, ...args); + this.diagnostic({ key: 'docker-plugins', distro }); } catch (error) { console.error(`Failed to set up ${ distro } docker plugins: ${ error }`.trim()); + this.diagnostic({ + key: 'docker-plugins', distro, error, + }); } } @@ -500,6 +545,7 @@ export default class WindowsIntegrationManager implements IntegrationManager { protected async syncDistroKubeconfig(distro: string, kubeconfigPath: string | undefined, state: boolean) { if (!kubeconfigPath) { console.debug(`Skipping syncing ${ distro } kubeconfig: no kubeconfig found`); + this.diagnostic({ key: 'kubeconfig', distro }); return 'Error setting up integration'; } @@ -518,6 +564,7 @@ export default class WindowsIntegrationManager implements IntegrationManager { 'kubeconfig', `--enable=${ state && this.settings.kubernetes?.enabled }`, ); + this.diagnostic({ key: 'kubeconfig', distro }); } catch (error: any) { if (typeof error?.stdout === 'string') { error.stdout = error.stdout.replace(/\0/g, ''); @@ -526,6 +573,9 @@ export default class WindowsIntegrationManager implements IntegrationManager { error.stderr = error.stderr.replace(/\0/g, ''); } console.error(`Could not set up kubeconfig integration for ${ distro }:`, error); + this.diagnostic({ + key: 'kubeconfig', distro, error, + }); return `Error setting up integration`; } @@ -533,21 +583,28 @@ export default class WindowsIntegrationManager implements IntegrationManager { } protected async syncDistroSpinCLI(distro: string, state: boolean) { - if (state && this.settings.experimental?.containerEngine?.webAssembly) { - const version = semver.parse(DEPENDENCY_VERSIONS.spinCLI); - const env = { - KUBE_PLUGIN_VERSION: DEPENDENCY_VERSIONS.spinKubePlugin, - SPIN_TEMPLATE_BRANCH: (version ? `v${ version.major }.${ version.minor }` : 'main'), - }; - const wslenv = Object.keys(env).join(':'); - - // wsl-exec is needed to correctly resolve DNS names - await this.execCommand({ - distro, - env: { - ...process.env, ...env, WSLENV: wslenv, - }, - }, await this.getLinuxToolPath(distro, executable('setup-spin'))); + try { + if (state && this.settings.experimental?.containerEngine?.webAssembly) { + const version = semver.parse(DEPENDENCY_VERSIONS.spinCLI); + const env = { + KUBE_PLUGIN_VERSION: DEPENDENCY_VERSIONS.spinKubePlugin, + SPIN_TEMPLATE_BRANCH: (version ? `v${ version.major }.${ version.minor }` : 'main'), + }; + const wslenv = Object.keys(env).join(':'); + + // wsl-exec is needed to correctly resolve DNS names + await this.execCommand({ + distro, + env: { + ...process.env, ...env, WSLENV: wslenv, + }, + }, await this.getLinuxToolPath(distro, executable('setup-spin'))); + } + this.diagnostic({ key: 'spin-cli', distro }); + } catch (error) { + this.diagnostic({ + key: 'spin-cli', distro, error, + }); } } diff --git a/pkg/rancher-desktop/main/diagnostics/diagnostics.ts b/pkg/rancher-desktop/main/diagnostics/diagnostics.ts index bc52303bae1..82b8e097f3a 100644 --- a/pkg/rancher-desktop/main/diagnostics/diagnostics.ts +++ b/pkg/rancher-desktop/main/diagnostics/diagnostics.ts @@ -48,6 +48,7 @@ export class DiagnosticsManager { const imports = (await Promise.all([ import('./connectedToInternet'), import('./dockerCliSymlinks'), + import('./integrationsWindows'), import('./kubeConfigSymlink'), import('./kubeContext'), import('./kubeVersionsAvailable'), diff --git a/pkg/rancher-desktop/main/diagnostics/integrationsWindows.ts b/pkg/rancher-desktop/main/diagnostics/integrationsWindows.ts new file mode 100644 index 00000000000..be5be1fdd7f --- /dev/null +++ b/pkg/rancher-desktop/main/diagnostics/integrationsWindows.ts @@ -0,0 +1,43 @@ +import { DiagnosticsCategory, DiagnosticsChecker, DiagnosticsCheckerResult, DiagnosticsCheckerSingleResult } from './types'; + +import mainEvents from '@pkg/main/mainEvents'; + +const cachedResults: Record = {}; + +const CheckWindowsIntegrations: DiagnosticsChecker = { + id: 'WINDOWS_INTEGRATIONS', + category: DiagnosticsCategory.ContainerEngine, + applicable() { + return Promise.resolve(process.platform === 'win32'); + }, + check(): Promise { + return Promise.resolve(Object.entries(cachedResults).map(([id, result]) => { + return ({ ...result, id }); + })); + }, +}; + +mainEvents.on('diagnostics-event', (payload) => { + if (payload.id !== 'integrations-windows') { + return; + } + const { distro, key, error } = payload; + const message = error?.message ?? error?.toString(); + + cachedResults[`${ distro || '
' }-${ key }`] = { + passed: false, + fixes: [], + ...(() => { + if (!error) { + return { passed: true, description: `${ distro }/${ key } passed` }; + } + if (distro) { + return { description: `Error managing distribution ${ distro }: ${ key }: ${ message }` }; + } + + return { description: `Error managing ${ key }: ${ message }` }; + })(), + }; +}); + +export default CheckWindowsIntegrations; diff --git a/pkg/rancher-desktop/main/mainEvents.ts b/pkg/rancher-desktop/main/mainEvents.ts index 1979f247114..c48f63ddb8a 100644 --- a/pkg/rancher-desktop/main/mainEvents.ts +++ b/pkg/rancher-desktop/main/mainEvents.ts @@ -166,6 +166,7 @@ interface MainEventNames { * 'diagnostics-event' event. */ type DiagnosticsEventPayload = + { id: 'integrations-windows', distro?: string, key: string, error?: Error } | { id: 'kube-versions-available', available: boolean } | { id: 'path-management', fileName: string; error: Error | undefined }; From 6e1d40ac4fcca38f70692510413231b45cf6774e Mon Sep 17 00:00:00 2001 From: Mark Yen Date: Tue, 7 Jan 2025 14:47:40 -0800 Subject: [PATCH 3/3] Diagnostics: Address review comments https://github.com/rancher-sandbox/rancher-desktop/pull/8042/files#r1906116013 Signed-off-by: Mark Yen --- pkg/rancher-desktop/main/diagnostics/integrationsWindows.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/rancher-desktop/main/diagnostics/integrationsWindows.ts b/pkg/rancher-desktop/main/diagnostics/integrationsWindows.ts index be5be1fdd7f..e8b5edfa919 100644 --- a/pkg/rancher-desktop/main/diagnostics/integrationsWindows.ts +++ b/pkg/rancher-desktop/main/diagnostics/integrationsWindows.ts @@ -11,9 +11,11 @@ const CheckWindowsIntegrations: DiagnosticsChecker = { return Promise.resolve(process.platform === 'win32'); }, check(): Promise { - return Promise.resolve(Object.entries(cachedResults).map(([id, result]) => { + const resultMapper = ([id, result]: [string, DiagnosticsCheckerResult]) => { return ({ ...result, id }); - })); + }; + + return Promise.resolve(Object.entries(cachedResults).map(resultMapper)); }, };