From 3572da1be6d91ecc821fccbeb36f91ae4582b5bd Mon Sep 17 00:00:00 2001 From: Mark Yen Date: Thu, 8 Aug 2024 15:25:51 -0700 Subject: [PATCH 1/9] Remove dashboard & steve We will replace the bundled version of dashboard and steve with the normal version to explore how that will work. Signed-off-by: Mark Yen --- background.ts | 25 ---- build/signing-config-win.yaml | 1 - pkg/rancher-desktop/assets/dependencies.yaml | 2 - pkg/rancher-desktop/backend/steve.ts | 100 -------------- .../main/dashboardServer/index.ts | 122 ----------------- .../main/dashboardServer/proxyUtils.ts | 74 ---------- pkg/rancher-desktop/typings/electron-ipc.d.ts | 4 - scripts/dependencies/tools.ts | 126 ------------------ scripts/lib/dependencies.ts | 2 - scripts/postinstall.ts | 2 - scripts/rddepman.ts | 2 - scripts/unreleased-change-monitor.ts | 2 - 12 files changed, 462 deletions(-) delete mode 100644 pkg/rancher-desktop/backend/steve.ts delete mode 100644 pkg/rancher-desktop/main/dashboardServer/index.ts delete mode 100644 pkg/rancher-desktop/main/dashboardServer/proxyUtils.ts diff --git a/background.ts b/background.ts index 091bad2e2fa..001422af7c8 100644 --- a/background.ts +++ b/background.ts @@ -14,7 +14,6 @@ import K8sFactory from '@pkg/backend/factory'; import { getImageProcessor } from '@pkg/backend/images/imageFactory'; import { ImageProcessor } from '@pkg/backend/images/imageProcessor'; import * as K8s from '@pkg/backend/k8s'; -import { Steve } from '@pkg/backend/steve'; import { FatalCommandLineOptionError, LockedFieldError, updateFromCommandLine } from '@pkg/config/commandLineOptions'; import { Help } from '@pkg/config/help'; import * as settings from '@pkg/config/settings'; @@ -26,7 +25,6 @@ import { getPathManagerFor } from '@pkg/integrations/pathManagerImpl'; import { BackendState, CommandWorkerInterface, HttpCommandServer } from '@pkg/main/commandServer/httpCommandServer'; import SettingsValidator from '@pkg/main/commandServer/settingsValidator'; import { HttpCredentialHelperServer } from '@pkg/main/credentialServer/httpCredentialHelperServer'; -import { DashboardServer } from '@pkg/main/dashboardServer'; import { DeploymentProfileError, readDeploymentProfiles } from '@pkg/main/deploymentProfiles'; import { DiagnosticsManager, DiagnosticsResultCollection } from '@pkg/main/diagnostics/diagnostics'; import { ExtensionErrorCode, isExtensionError } from '@pkg/main/extensions'; @@ -53,7 +51,6 @@ import { RecursivePartial, RecursiveReadonly } from '@pkg/utils/typeUtils'; import { getVersion } from '@pkg/utils/version'; import getWSLVersion from '@pkg/utils/wslVersion'; import * as window from '@pkg/window'; -import { closeDashboard, openDashboard } from '@pkg/window/dashboard'; import { openPreferences, preferencesSetDirtyFlag } from '@pkg/window/preferences'; Electron.app.setPath('cache', paths.cache); @@ -205,8 +202,6 @@ Electron.app.whenReady().then(async() => { // Check for required OS versions and features await checkPrerequisites(); - DashboardServer.getInstance().init(); - await setupNetworking(); try { @@ -680,14 +675,6 @@ ipcMainProxy.on('images-namespaces-read', (event) => { } }); -ipcMainProxy.on('dashboard-open', () => { - openDashboard(); -}); - -ipcMainProxy.on('dashboard-close', () => { - closeDashboard(); -}); - ipcMainProxy.on('preferences-open', () => { openPreferences(); }); @@ -712,11 +699,6 @@ function writeSettings(arg: RecursivePartial { writeSettings(arg); - // dashboard requires kubernetes, so we want to close it if kubernetes is disabled - if (arg?.kubernetes?.enabled === false) { - closeDashboard(); - } - event.sender.sendToFrame(event.frameId, 'settings-update', cfg); }); @@ -1240,15 +1222,8 @@ function newK8sManager() { writeSettings({ kubernetes: { version: mgr.kubeBackend.version } }); } currentImageProcessor?.relayNamespaces(); - - if (enabledK8s) { - Steve.getInstance().start(); - } } - if (state === K8s.State.STOPPING) { - Steve.getInstance().stop(); - } if (pendingRestartContext !== undefined && !backendIsBusy()) { // If we restart immediately the QEMU process in the VM doesn't always respond to a shutdown messages setTimeout(doFullRestart, 2_000, pendingRestartContext); diff --git a/build/signing-config-win.yaml b/build/signing-config-win.yaml index 694667f3c25..8a54a6e464c 100644 --- a/build/signing-config-win.yaml +++ b/build/signing-config-win.yaml @@ -29,6 +29,5 @@ resources/resources/win32/internal: - host-resolver.exe - host-switch.exe - privileged-service.exe -- steve.exe - vtunnel.exe - wsl-helper.exe diff --git a/pkg/rancher-desktop/assets/dependencies.yaml b/pkg/rancher-desktop/assets/dependencies.yaml index 4c0137ce193..a1f07f8bb90 100644 --- a/pkg/rancher-desktop/assets/dependencies.yaml +++ b/pkg/rancher-desktop/assets/dependencies.yaml @@ -11,8 +11,6 @@ dockerBuildx: 0.16.2 dockerCompose: 2.29.1 golangci-lint: 1.60.1 trivy: 0.54.1 -steve: 0.1.0-beta9 -rancherDashboard: desktop-v2.7.0.beta.1 dockerProvidedCredentialHelpers: 0.8.2 ECRCredentialHelper: 0.8.0 hostResolver: 0.1.5 diff --git a/pkg/rancher-desktop/backend/steve.ts b/pkg/rancher-desktop/backend/steve.ts deleted file mode 100644 index 7892b70d740..00000000000 --- a/pkg/rancher-desktop/backend/steve.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { ChildProcess, spawn } from 'child_process'; -import os from 'os'; -import path from 'path'; - -import Logging from '@pkg/utils/logging'; -import paths from '@pkg/utils/paths'; - -const console = Logging.steve; - -/** - * @description Singleton that manages the lifecycle of the Steve API - */ -export class Steve { - private static instance: Steve; - private process!: ChildProcess; - - private isRunning: boolean; - - private constructor() { - this.isRunning = false; - } - - /** - * @description Checks for an existing instance of Steve. If one does not - * exist, instantiate a new one. - */ - public static getInstance(): Steve { - if (!Steve.instance) { - Steve.instance = new Steve(); - } - - return Steve.instance; - } - - /** - * @description Starts the Steve API if one is not already running. - */ - public start() { - const { pid } = this.process || { }; - - if (this.isRunning && pid) { - console.debug(`Steve is already running with pid: ${ pid }`); - - return; - } - - const osSpecificName = /^win/i.test(os.platform()) ? 'steve.exe' : 'steve'; - const stevePath = path.join(paths.resources, os.platform(), 'internal', osSpecificName); - - this.process = spawn( - stevePath, - [ - '--context', - 'rancher-desktop', - '--ui-path', - path.join(paths.resources, 'rancher-dashboard'), - '--offline', - 'true', - ], - ); - - const { stdout, stderr } = this.process; - - if (!stdout || !stderr) { - console.error('Unable to get child process...'); - - return; - } - - stdout.on('data', (data: any) => { - console.log(`stdout: ${ data }`); - }); - - stderr.on('data', (data: any) => { - console.error(`stderr: ${ data }`); - }); - - this.process.on('spawn', () => { - this.isRunning = true; - }); - - this.process.on('close', (code: any) => { - console.log(`child process exited with code ${ code }`); - this.isRunning = false; - }); - - console.debug(`Spawned child pid: ${ this.process.pid }`); - } - - /** - * Stops the Steve API. - */ - public stop() { - if (!this.isRunning) { - return; - } - - this.process.kill('SIGINT'); - } -} diff --git a/pkg/rancher-desktop/main/dashboardServer/index.ts b/pkg/rancher-desktop/main/dashboardServer/index.ts deleted file mode 100644 index 6c8255bdead..00000000000 --- a/pkg/rancher-desktop/main/dashboardServer/index.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { Server } from 'http'; -import net from 'net'; -import path from 'path'; - -import express from 'express'; -import { createProxyMiddleware, Options } from 'http-proxy-middleware'; - -import { proxyWsOpts, proxyOpts } from './proxyUtils'; - -import Logging from '@pkg/utils/logging'; -import paths from '@pkg/utils/paths'; - -const ProxyKeys = ['/k8s', '/pp', '/api', '/apis', '/v1', '/v3', '/v3-public', '/api-ui', '/meta', '/v1-*'] as const; - -type ProxyKeys = typeof ProxyKeys[number]; - -const console = Logging.dashboardServer; - -/** - * Singleton that manages the lifecycle of the Dashboard server. - */ -export class DashboardServer { - private static instance: DashboardServer; - - private dashboardServer = express(); - private dashboardApp: Server = new Server(); - private host = '127.0.0.1'; - private port = 6120; - private api = 'https://127.0.0.1:9443'; - - private proxies = (() => { - const proxy: Record = { - '/k8s': proxyWsOpts, // Straight to a remote cluster (/k8s/clusters//) - '/pp': proxyWsOpts, // For (epinio) standalone API - '/api': proxyWsOpts, // Management k8s API - '/apis': proxyWsOpts, // Management k8s API - '/v1': proxyWsOpts, // Management Steve API - '/v3': proxyWsOpts, // Rancher API - '/api-ui': proxyOpts, // Browser API UI - '/v3-public': proxyOpts, // Rancher Unauthed API - '/meta': proxyOpts, // Browser API UI - '/v1-*': proxyOpts, // SAML, KDM, etc - }; - const entries = Object.entries(proxy).map(([key, options]) => { - return [key, createProxyMiddleware({ ...options, target: this.api + key })] as const; - }); - - return Object.fromEntries(entries); - })(); - - /** - * Checks for an existing instance of Dashboard server. - * Instantiate a new one if it does not exist. - */ - public static getInstance(): DashboardServer { - DashboardServer.instance ??= new DashboardServer(); - - return DashboardServer.instance; - } - - /** - * Starts the Dashboard server if one is not already running. - */ - public init() { - if (this.dashboardApp.address()) { - console.log(`Dashboard Server is already listening on ${ this.host }:${ this.port }`); - - return; - } - - ProxyKeys.forEach((key) => { - this.dashboardServer.use(key, this.proxies[key]); - }); - - this.dashboardApp = this.dashboardServer - // handle static assets, e.g. image, icons, fonts, and index.html - .use( - express.static( - path.join(paths.resources, 'rancher-dashboard'), - )) - /** - * Handle all routes that we don't account for, return index.html and let - * Vue router take over. - */ - .get( - '*', - (_req, res) => { - res.sendFile( - path.resolve(paths.resources, 'rancher-dashboard', 'index.html'), - ); - }) - .listen(this.port, this.host) - .on('upgrade', (req, socket, head) => { - if (!(socket instanceof net.Socket)) { - console.log(`Invalid upgrade for ${ req.url }`); - - return; - } - - if (req.url?.startsWith('/v1')) { - return this.proxies['/v1'].upgrade(req, socket, head); - } else if (req.url?.startsWith('/v3')) { - return this.proxies['/v3'].upgrade(req, socket, head); - } else if (req.url?.startsWith('/k8s/')) { - return this.proxies['/k8s'].upgrade(req, socket, head); - } else { - console.log(`Unknown Web socket upgrade request for ${ req.url }`); - } - }); - } - - /** - * Stop the Dashboard server. - */ - public stop() { - if (!this.dashboardApp.address()) { - return; - } - - this.dashboardApp.close(); - } -} diff --git a/pkg/rancher-desktop/main/dashboardServer/proxyUtils.ts b/pkg/rancher-desktop/main/dashboardServer/proxyUtils.ts deleted file mode 100644 index 98336218343..00000000000 --- a/pkg/rancher-desktop/main/dashboardServer/proxyUtils.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { ClientRequest } from 'http'; -import { Socket } from 'net'; - -import { Options } from 'http-proxy-middleware'; - -import Logging from '@pkg/utils/logging'; - -import type { ErrorCallback, ProxyReqCallback, ProxyReqWsCallback } from 'http-proxy'; - -const console = Logging.dashboardServer; - -const onProxyReq: ProxyReqCallback = (clientReq, req) => { - const actualClientReq: ClientRequest | undefined = (clientReq as any)._currentRequest; - - if (!actualClientReq || !actualClientReq.headersSent) { - if (req.headers.host) { - clientReq.setHeader('x-api-host', req.headers.host); - } - clientReq.setHeader('x-forwarded-proto', 'https'); - } -}; - -const onProxyReqWs: ProxyReqWsCallback = (clientReq, req, socket, options) => { - const target = options?.target as Partial | undefined; - - if (!target?.href) { - console.error(`onProxyReqWs: No target href, aborting`); - req.destroy(new Error(`onProxyReqWs: no target href`)); - - return; - } - if (target.pathname && clientReq.path.startsWith(target.pathname)) { - // `options.prependPath` is required for non-websocket requests to be routed - // correctly; this means that we end up with the prepended path here, but - // that does not work in this case. Therefore we need to manually strip off - // the prepended path here before passing it to the backend. - clientReq.path = clientReq.path.substring(target.pathname.length); - } - req.headers.origin = target.href; - clientReq.setHeader('origin', target.href); - if (req.headers.host) { - clientReq.setHeader('x-api-host', req.headers.host); - } - clientReq.setHeader('x-forwarded-proto', 'https'); - - socket.on('error', err => console.error('Proxy WS Error:', err)); -}; - -const onError: ErrorCallback = (err, req, res) => { - console.error('Proxy Error:', err); - if (res instanceof Socket) { - res.destroy(err); - } else { - res.statusCode = 598; // (Informal) Network read timeout error - res.write(JSON.stringify(err)); - } -}; - -export const proxyOpts: Omit = { - followRedirects: true, - secure: false, - logger: console, - on: { - proxyReq: onProxyReq, - proxyReqWs: onProxyReqWs, - error: onError, - }, -}; - -export const proxyWsOpts: Omit = { - ...proxyOpts, - ws: false, - changeOrigin: true, -}; diff --git a/pkg/rancher-desktop/typings/electron-ipc.d.ts b/pkg/rancher-desktop/typings/electron-ipc.d.ts index 766b5e3f2b2..22bfc34d91f 100644 --- a/pkg/rancher-desktop/typings/electron-ipc.d.ts +++ b/pkg/rancher-desktop/typings/electron-ipc.d.ts @@ -76,9 +76,6 @@ export interface IpcMainEvents { 'show-logs': () => void; - 'dashboard-open': () => void; - 'dashboard-close': () => void; - 'diagnostics/run': () => void; /** Only for the preferences window */ @@ -216,7 +213,6 @@ export interface IpcRendererEvents { 'dialog/close': (...args: any) => void; 'dialog/error': (args: any) => void; 'dialog/info': (args: Record) => void; - 'dashboard-open': () => void; // #endregion // #region tab navigation diff --git a/scripts/dependencies/tools.ts b/scripts/dependencies/tools.ts index b6106995246..c2e31a55dd4 100644 --- a/scripts/dependencies/tools.ts +++ b/scripts/dependencies/tools.ts @@ -11,7 +11,6 @@ import { import { DownloadContext, Dependency, GitHubDependency, getPublishedReleaseTagNames, getPublishedVersions, } from 'scripts/lib/dependencies'; -import { simpleSpawn } from 'scripts/simple_process'; function exeName(context: DownloadContext, name: string) { const onWindows = context.platform === 'win32'; @@ -329,131 +328,6 @@ export class Trivy implements Dependency, GitHubDependency { } } -export class Steve implements Dependency, GitHubDependency { - name = 'steve'; - githubOwner = 'rancher-sandbox'; - githubRepo = 'rancher-desktop-steve'; - - async download(context: DownloadContext): Promise { - const steveURLBase = `https://github.com/${ this.githubOwner }/${ this.githubRepo }/releases/download/v${ context.versions.steve }`; - const arch = context.isM1 ? 'arm64' : 'amd64'; - const steveExecutable = `steve-${ context.goPlatform }-${ arch }`; - const steveURL = `${ steveURLBase }/${ steveExecutable }.tar.gz`; - const stevePath = path.join(context.internalDir, exeName(context, 'steve')); - const steveSHA = await findChecksum(`${ steveURL }.sha512sum`, `${ steveExecutable }.tar.gz`); - - await downloadTarGZ( - steveURL, - stevePath, - { - expectedChecksum: steveSHA, - checksumAlgorithm: 'sha512', - }); - } - - // Note that we set includePrerelease to true by default, which is different - // from the way other Dependency's work. There is a reason for this: - // as of the time of writing, all releases of steve are prerelease versions. - // If this changes, the default value of includePrelease should be changed to false. - async getAvailableVersions(includePrerelease = true): Promise { - return await getPublishedVersions(this.githubOwner, this.githubRepo, includePrerelease); - } - - versionToTagName(version: string): string { - return `v${ version }`; - } - - rcompareVersions(version1: string, version2: string): -1 | 0 | 1 { - return semver.rcompare(version1, version2); - } -} - -export class RancherDashboard implements Dependency, GitHubDependency { - name = 'rancherDashboard'; - githubOwner = 'rancher-sandbox'; - githubRepo = 'dashboard'; - versionRegex = /^desktop-v([0-9]+\.[0-9]+\.[0-9]+)\.([0-9a-zA-Z]+(\.[0-9a-zA-Z]+)+)$/; - - async download(context: DownloadContext): Promise { - const baseURL = `https://github.com/rancher-sandbox/dashboard/releases/download/${ context.versions.rancherDashboard }`; - const executableName = 'rancher-dashboard-desktop-embed'; - const url = `${ baseURL }/${ executableName }.tar.gz`; - const destPath = path.join(context.resourcesDir, 'rancher-dashboard.tgz'); - const expectedChecksum = await findChecksum(`${ url }.sha512sum`, `${ executableName }.tar.gz`); - const rancherDashboardDir = path.join(context.resourcesDir, 'rancher-dashboard'); - - if (fs.existsSync(rancherDashboardDir)) { - console.log(`${ rancherDashboardDir } already exists, not re-downloading.`); - - return; - } - - await download( - url, - destPath, - { - expectedChecksum, - checksumAlgorithm: 'sha512', - access: fs.constants.W_OK, - }); - - await fs.promises.mkdir(rancherDashboardDir, { recursive: true }); - - const args = ['tar', '-xf', destPath]; - - if (os.platform().startsWith('win')) { - // On Windows, force use the bundled bsdtar. - // We may find GNU tar on the path, which looks at the Windows-style path - // and considers C:\Temp to be a reference to a remote host named `C`. - const systemRoot = process.env.SystemRoot; - - if (!systemRoot) { - throw new Error('Could not find system root'); - } - args[0] = path.join(systemRoot, 'system32', 'tar.exe'); - } - - console.log('Extracting rancher dashboard...'); - await simpleSpawn(args[0], args.slice(1), { - cwd: rancherDashboardDir, - stdio: ['ignore', 'inherit', 'inherit'], - }); - - await fs.promises.rm(destPath, { recursive: true, maxRetries: 10 }); - } - - async getAvailableVersions(): Promise { - const versions = await getPublishedReleaseTagNames(this.githubOwner, this.githubRepo); - - // Versions that contain .plugins. exist solely for testing during - // plugins development. For more info please see - // https://github.com/rancher-sandbox/rancher-desktop/issues/3757 - return versions.filter(version => !version.includes('.plugins.')); - } - - versionToTagName(version: string): string { - return version; - } - - versionToSemver(version: string): string { - const match = this.versionRegex.exec(version); - - if (match === null) { - throw new Error(`${ this.name }: ${ version } does not match version regex ${ this.versionRegex }`); - } - const [, mainVersion, prereleaseVersion] = match; - - return `${ mainVersion }-${ prereleaseVersion }`; - } - - rcompareVersions(version1: string, version2: string): -1 | 0 | 1 { - const semver1 = this.versionToSemver(version1); - const semver2 = this.versionToSemver(version2); - - return semver.rcompare(semver1, semver2); - } -} - export class DockerProvidedCredHelpers implements Dependency, GitHubDependency { name = 'dockerProvidedCredentialHelpers'; githubOwner = 'docker'; diff --git a/scripts/lib/dependencies.ts b/scripts/lib/dependencies.ts index c5c43d7c9df..9dbe0c7ae30 100644 --- a/scripts/lib/dependencies.ts +++ b/scripts/lib/dependencies.ts @@ -43,9 +43,7 @@ export type DependencyVersions = { dockerCompose: string; 'golangci-lint': string; trivy: string; - steve: string; guestAgent: string; - rancherDashboard: string; dockerProvidedCredentialHelpers: string; ECRCredentialHelper: string; hostResolver: string; diff --git a/scripts/postinstall.ts b/scripts/postinstall.ts index 95a6c3fd107..8f06dbfcbf6 100644 --- a/scripts/postinstall.ts +++ b/scripts/postinstall.ts @@ -84,8 +84,6 @@ const vmDependencies = [ // Dependencies that are specific to hosts. const hostDependencies = [ - new tools.Steve(), - new tools.RancherDashboard(), new MobyOpenAPISpec(), ]; diff --git a/scripts/rddepman.ts b/scripts/rddepman.ts index a16280de5ae..ee812106593 100644 --- a/scripts/rddepman.ts +++ b/scripts/rddepman.ts @@ -34,8 +34,6 @@ const dependencies: Dependency[] = [ new tools.DockerProvidedCredHelpers(), new tools.GoLangCILint(), new tools.Trivy(), - new tools.Steve(), - new tools.RancherDashboard(), new tools.ECRCredHelper(), new Lima(), new LimaAndQemu(), diff --git a/scripts/unreleased-change-monitor.ts b/scripts/unreleased-change-monitor.ts index b18aa2ca85a..32d4338b64e 100644 --- a/scripts/unreleased-change-monitor.ts +++ b/scripts/unreleased-change-monitor.ts @@ -23,8 +23,6 @@ const dependencies: UnreleasedChangeMonitoringDependency[] = [ new LimaAndQemu(), new WSLDistro(), new tools.DockerCLI(), - new tools.Steve(), - new tools.RancherDashboard(), new AlpineLimaISO(), new HostResolverHost(), // we only need one of HostResolverHost and HostResolverPeer new HostSwitch(), From 8ab44abfb0ab6b97e5dcc918410ac956f5f61906 Mon Sep 17 00:00:00 2001 From: Mark Yen Date: Tue, 13 Aug 2024 13:40:07 -0700 Subject: [PATCH 2/9] Install Rancher manager This installs Rancher Manager (but doesn't go through the setup). This is not yet very useful. Signed-off-by: Mark Yen --- .gitignore | 1 + pkg/rancher-desktop/assets/dependencies.yaml | 1 + .../assets/scripts/cert-manager.yaml | 2 + pkg/rancher-desktop/backend/backendHelper.ts | 76 +++++++++++++++++-- pkg/rancher-desktop/backend/k3sHelper.ts | 14 ++-- pkg/rancher-desktop/backend/kube/lima.ts | 7 +- pkg/rancher-desktop/backend/kube/wsl.ts | 11 ++- pkg/rancher-desktop/utils/resources.ts | 2 +- scripts/dependencies/tools.ts | 34 +++++++++ scripts/lib/dependencies.ts | 1 + scripts/lib/download.ts | 46 +++++++++++ scripts/postinstall.ts | 1 + 12 files changed, 171 insertions(+), 25 deletions(-) diff --git a/.gitignore b/.gitignore index b9bf8f0d40d..78fe7f86fe7 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ /resources/linux/* !/resources/linux/rancher-desktop.desktop /resources/preload.js* +/resources/rancher-*.tgz /resources/rancher-dashboard/ /resources/rdx-proxy.tar /resources/spin-operator* diff --git a/pkg/rancher-desktop/assets/dependencies.yaml b/pkg/rancher-desktop/assets/dependencies.yaml index a1f07f8bb90..102762b9c1c 100644 --- a/pkg/rancher-desktop/assets/dependencies.yaml +++ b/pkg/rancher-desktop/assets/dependencies.yaml @@ -11,6 +11,7 @@ dockerBuildx: 0.16.2 dockerCompose: 2.29.1 golangci-lint: 1.60.1 trivy: 0.54.1 +rancher: 2.9.0 dockerProvidedCredentialHelpers: 0.8.2 ECRCredentialHelper: 0.8.0 hostResolver: 0.1.5 diff --git a/pkg/rancher-desktop/assets/scripts/cert-manager.yaml b/pkg/rancher-desktop/assets/scripts/cert-manager.yaml index f7ed2f7c7c7..c41aafb850a 100644 --- a/pkg/rancher-desktop/assets/scripts/cert-manager.yaml +++ b/pkg/rancher-desktop/assets/scripts/cert-manager.yaml @@ -15,3 +15,5 @@ spec: # Old versions of the helm-controller don't support createNamespace, so we # created the namespace ourselves. createNamespace: false + valuesContent: |- + enableCertificateOwnerRef: true diff --git a/pkg/rancher-desktop/backend/backendHelper.ts b/pkg/rancher-desktop/backend/backendHelper.ts index b4f1aeb1329..3c7fbfd7916 100644 --- a/pkg/rancher-desktop/backend/backendHelper.ts +++ b/pkg/rancher-desktop/backend/backendHelper.ts @@ -1,3 +1,4 @@ +import crypto from 'crypto'; import path from 'path'; import Electron from 'electron'; @@ -19,6 +20,8 @@ import Logging from '@pkg/utils/logging'; import paths from '@pkg/utils/paths'; import { jsonStringifyWithWhiteSpace } from '@pkg/utils/stringify'; import { showMessageBox } from '@pkg/window'; +import DEPENDENCY_VERSIONS from '@pkg/assets/dependencies.yaml'; +import { KubeClient } from '@pkg/backend/kube/client'; const CONTAINERD_CONFIG_TOML = '/etc/containerd/config.toml'; const DOCKER_DAEMON_JSON = '/etc/docker/daemon.json'; @@ -32,9 +35,11 @@ export const MANIFEST_CERT_MANAGER_CRDS = 'z110-cert-manager.crds'; export const MANIFEST_CERT_MANAGER = 'z115-cert-manager'; export const MANIFEST_SPIN_OPERATOR_CRDS = 'z120-spin-operator.crds'; export const MANIFEST_SPIN_OPERATOR = 'z125-spin-operator'; +export const MANIFEST_RANCHER = 'z130-rancher-manager'; const STATIC_DIR = '/var/lib/rancher/k3s/server/static/rancher-desktop'; const STATIC_CERT_MANAGER_CHART = `${ STATIC_DIR }/cert-manager.tgz`; +const STATIC_RANCHER_CHART = `${ STATIC_DIR }/rancher-manager.tgz` const STATIC_SPIN_OPERATOR_CHART = `${ STATIC_DIR }/spin-operator.tgz`; const console = Logging.kube; @@ -315,19 +320,65 @@ export default class BackendHelper { } } + static rancherPassword = crypto.randomBytes(32).toString('hex'); + /** - * Write k3s manifests to install cert-manager and spinkube operator + * Write k3s manifests to install cert-manager, rancher manager, and spinkube + * operator. + * @param spin Whether to enable spinkube. */ - static async configureSpinOperator(vmx: VMExecutor) { - await Promise.all([ + static async configureKubeResources(vmx: VMExecutor, spin = false) { + let promises = []; + const rancherManifest = [ + { + apiVersion: 'v1', + kind: 'Namespace', + metadata: { name: 'cattle-system' }, + }, + { + apiVersion: 'helm.cattle.io/v1', + kind: 'HelmChart', + metadata: { + name: 'rancher-manager', + namespace: 'cattle-system', + }, + spec: { + chart: "https://%{KUBERNETES_API}%/static/rancher-desktop/rancher-manager.tgz", + targetNamespace: 'cattle-system', + // Old versions of the helm-controller don't support createNamespace, so we + // created the namespace ourselves. + createNamespace: false, + valuesContent: yaml.stringify({ + bootstrapPassword: BackendHelper.rancherPassword, + replicas: 1, + hostname: 'localhost', + useBundledSystemChart: true, + certmanager: { + version: DEPENDENCY_VERSIONS.certManager, + }, + postDelete: { + enabled: false, + }, + extraEnv: [ + { name: 'CATTLE_FEATURES', value: 'multi-cluster-management=false' }, + ] + }), + }, + }, + ].map(obj => '---\n' + yaml.stringify(obj)).join(''); + promises.push( vmx.copyFileIn(path.join(paths.resources, 'cert-manager.crds.yaml'), this.manifestFilename(MANIFEST_CERT_MANAGER_CRDS)), vmx.copyFileIn(path.join(paths.resources, 'cert-manager.tgz'), STATIC_CERT_MANAGER_CHART), vmx.writeFile(this.manifestFilename(MANIFEST_CERT_MANAGER), CERT_MANAGER, 0o644), - - vmx.copyFileIn(path.join(paths.resources, 'spin-operator.crds.yaml'), this.manifestFilename(MANIFEST_SPIN_OPERATOR_CRDS)), - vmx.copyFileIn(path.join(paths.resources, 'spin-operator.tgz'), STATIC_SPIN_OPERATOR_CHART), - vmx.writeFile(this.manifestFilename(MANIFEST_SPIN_OPERATOR), SPIN_OPERATOR, 0o644), - ]); + vmx.copyFileIn(path.join(paths.resources, `rancher-${ DEPENDENCY_VERSIONS.rancher }.tgz`), STATIC_RANCHER_CHART)); + vmx.writeFile(this.manifestFilename(MANIFEST_RANCHER), rancherManifest, 0o644); + if (spin) { + promises.push( + vmx.copyFileIn(path.join(paths.resources, 'spin-operator.crds.yaml'), this.manifestFilename(MANIFEST_SPIN_OPERATOR_CRDS)), + vmx.copyFileIn(path.join(paths.resources, 'spin-operator.tgz'), STATIC_SPIN_OPERATOR_CHART), + vmx.writeFile(this.manifestFilename(MANIFEST_SPIN_OPERATOR), SPIN_OPERATOR, 0o644)); + } + await Promise.all(promises); } /** @@ -391,4 +442,13 @@ export default class BackendHelper { await BackendHelper.writeContainerdConfig(vmx, configureWASM); await BackendHelper.writeMobyConfig(vmx, configureWASM); } + + static async setupRancherManager(client: KubeClient) { + const pod = await client.getActivePod('cattle-system', 'rancher-manager'); + + if (!pod) { + // We can get here if shutdown was initiated before the pod was ready. + return; + } + } } diff --git a/pkg/rancher-desktop/backend/k3sHelper.ts b/pkg/rancher-desktop/backend/k3sHelper.ts index 97ed70c378e..f007ddcf6ed 100644 --- a/pkg/rancher-desktop/backend/k3sHelper.ts +++ b/pkg/rancher-desktop/backend/k3sHelper.ts @@ -38,6 +38,7 @@ import { showMessageBox } from '@pkg/window'; import type Electron from 'electron'; +const KubeContextName = 'rancher-desktop'; const console = Logging.k8s; /** @@ -905,7 +906,6 @@ export default class K3sHelper extends events.EventEmitter { * @param configReader A function that returns the kubeconfig from the K3s VM. */ async updateKubeconfig(configReader: () => Promise): Promise { - const contextName = 'rancher-desktop'; const workDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'rancher-desktop-kubeconfig-')); try { @@ -929,18 +929,18 @@ export default class K3sHelper extends events.EventEmitter { const clusterIndex = workConfig.clusters.findIndex(cluster => cluster.name === context.cluster); if (userIndex >= 0) { - workConfig.users[userIndex] = { ...workConfig.users[userIndex], name: contextName }; + workConfig.users[userIndex] = { ...workConfig.users[userIndex], name: KubeContextName }; } if (clusterIndex >= 0) { - workConfig.clusters[clusterIndex] = { ...workConfig.clusters[clusterIndex], name: contextName }; + workConfig.clusters[clusterIndex] = { ...workConfig.clusters[clusterIndex], name: KubeContextName }; } workConfig.contexts[contextIndex] = { - ...context, name: contextName, user: contextName, cluster: contextName, + ...context, name: KubeContextName, user: KubeContextName, cluster: KubeContextName, }; - workConfig.currentContext = contextName; + workConfig.currentContext = KubeContextName; } - const userPath = await K3sHelper.findKubeConfigToUpdate(contextName); + const userPath = await K3sHelper.findKubeConfigToUpdate(KubeContextName); const userConfig = new KubeConfig(); // @kubernetes/client-node throws when merging things that already exist @@ -970,7 +970,7 @@ export default class K3sHelper extends events.EventEmitter { merge(userConfig.contexts, workConfig.contexts); merge(userConfig.users, workConfig.users); merge(userConfig.clusters, workConfig.clusters); - userConfig.currentContext ||= contextName; + userConfig.currentContext ||= KubeContextName; // Use custom exportConfig() that supports the `proxy-url` cluster field. const userYAML = this.ensureContentsAreYAML(exportConfig(userConfig)); const writeStream = fs.createWriteStream(workPath, { mode: 0o600 }); diff --git a/pkg/rancher-desktop/backend/kube/lima.ts b/pkg/rancher-desktop/backend/kube/lima.ts index 719e721aa3d..401a1251cab 100644 --- a/pkg/rancher-desktop/backend/kube/lima.ts +++ b/pkg/rancher-desktop/backend/kube/lima.ts @@ -132,11 +132,11 @@ export default class LimaKubernetesBackend extends events.EventEmitter implement const promises: Promise[] = []; promises.push(this.writeServiceScript(config, desiredVersion, allowSudo)); + promises.push(BackendHelper.configureKubeResources(this.vm, + config.experimental?.containerEngine?.webAssembly?.enabled && + !!config.experimental?.kubernetes?.options?.spinkube)); if (config.experimental?.containerEngine?.webAssembly?.enabled) { promises.push(BackendHelper.configureRuntimeClasses(this.vm)); - if (config.experimental?.kubernetes?.options?.spinkube) { - promises.push(BackendHelper.configureSpinOperator(this.vm)); - } } await Promise.all(promises); }); @@ -253,6 +253,7 @@ export default class LimaKubernetesBackend extends events.EventEmitter implement await new Promise(resolve => setTimeout(resolve, 5000)); }); } + await BackendHelper.setupRancherManager(this.client); } async stop() { diff --git a/pkg/rancher-desktop/backend/kube/wsl.ts b/pkg/rancher-desktop/backend/kube/wsl.ts index 0aee4da4d45..a57bdda4df9 100644 --- a/pkg/rancher-desktop/backend/kube/wsl.ts +++ b/pkg/rancher-desktop/backend/kube/wsl.ts @@ -176,15 +176,14 @@ export default class WSLKubernetesBackend extends events.EventEmitter implements await this.vm.runInstallScript(INSTALL_K3S_SCRIPT, 'install-k3s', version.raw, await this.vm.wslify(path.join(paths.cache, 'k3s'))); + const promises: Promise[] = []; + promises.push(BackendHelper.configureKubeResources(this.vm, + config.experimental?.containerEngine?.webAssembly?.enabled && + !!config.experimental?.kubernetes?.options?.spinkube)); if (config.experimental?.containerEngine?.webAssembly?.enabled) { - const promises: Promise[] = []; - promises.push(BackendHelper.configureRuntimeClasses(this.vm)); - if (config.experimental?.kubernetes?.options?.spinkube) { - promises.push(BackendHelper.configureSpinOperator(this.vm)); - } - await Promise.all(promises); } + await Promise.all(promises); } async start(config: BackendSettings, activeVersion: semver.SemVer, kubeClient?: () => KubeClient): Promise { diff --git a/pkg/rancher-desktop/utils/resources.ts b/pkg/rancher-desktop/utils/resources.ts index 8e8ddfe9f59..9c6af330f4d 100644 --- a/pkg/rancher-desktop/utils/resources.ts +++ b/pkg/rancher-desktop/utils/resources.ts @@ -10,7 +10,7 @@ import paths from '@pkg/utils/paths'; * user-accessible `bin` directory. * Otherwise, it's an array containing the path to the executable. */ -const executableMap: Record = { +const executableMap = { docker: undefined, kubectl: undefined, nerdctl: undefined, diff --git a/scripts/dependencies/tools.ts b/scripts/dependencies/tools.ts index c2e31a55dd4..4d201ab4172 100644 --- a/scripts/dependencies/tools.ts +++ b/scripts/dependencies/tools.ts @@ -6,6 +6,7 @@ import semver from 'semver'; import { download, downloadZip, downloadTarGZ, getResource, DownloadOptions, ArchiveDownloadOptions, + locateHelmChart, } from '../lib/download'; import { @@ -328,6 +329,39 @@ export class Trivy implements Dependency, GitHubDependency { } } +export class RancherManager implements Dependency, GitHubDependency { + name = 'rancher'; + githubOwner = 'rancher'; + githubRepo = 'rancher'; + + async download(context: DownloadContext): Promise { + await this.downloadChart(context); + } + + protected async downloadChart(context: DownloadContext): Promise { + const destPath = path.join(context.resourcesDir, `rancher-${ context.versions.rancher }.tgz`); + const options = await locateHelmChart( + 'https://releases.rancher.com/server-charts/latest/', + 'rancher', + context.versions.rancher); + + await download(options.url.toString(), destPath, options); + } + + getAvailableVersions(includePrerelease = false): Promise { + return getPublishedVersions(this.githubOwner, this.githubRepo, includePrerelease); + } + + versionToTagName(version: string): string { + return `v${ version }`; + } + + rcompareVersions(version1: string, version2: string): -1 | 0 | 1 { + return semver.rcompare(version1, version2); + } + +} + export class DockerProvidedCredHelpers implements Dependency, GitHubDependency { name = 'dockerProvidedCredentialHelpers'; githubOwner = 'docker'; diff --git a/scripts/lib/dependencies.ts b/scripts/lib/dependencies.ts index 9dbe0c7ae30..8180d33e590 100644 --- a/scripts/lib/dependencies.ts +++ b/scripts/lib/dependencies.ts @@ -44,6 +44,7 @@ export type DependencyVersions = { 'golangci-lint': string; trivy: string; guestAgent: string; + rancher: string; dockerProvidedCredentialHelpers: string; ECRCredentialHelper: string; hostResolver: string; diff --git a/scripts/lib/download.ts b/scripts/lib/download.ts index e717447b009..050458f6b91 100644 --- a/scripts/lib/download.ts +++ b/scripts/lib/download.ts @@ -9,6 +9,9 @@ import os from 'os'; import path from 'path'; import stream from 'stream'; +import semver from 'semver'; +import yaml from 'yaml'; + import { simpleSpawn } from 'scripts/simple_process'; type ChecksumAlgorithm = 'sha1' | 'sha256' | 'sha512'; @@ -251,3 +254,46 @@ export async function downloadZip(url: string, destPath: string, options: Archiv return destPath; } + +type HelmChartEntry = { + apiVersion: 'v2'; + digest: string; + name: string; + urls: string[]; + version: string; +} + +type HelmChartLocation = Pick & { + url: URL +}; + +/** + * Inspect a helm repository and locate a give helm chart, plus checksums. + * @param url The helm repository URL, without "index.yaml". + * @returns Options needed to download the helm chart. + */ +export async function locateHelmChart(url: string, chart: string, version?: string): Promise { + const indexURL = new URL('index.yaml', url); + const indexResponse = await fetch(indexURL); + const indexBody = await indexResponse.text(); + const chartContents = yaml.parse(indexBody);; + const entries: HelmChartEntry[] = chartContents.entries[chart]; + const chartEntries = entries.filter(e => e.name === chart); + let entry: HelmChartEntry | undefined; + if (version) { + entry = chartEntries.find(e => semver.eq(e.version, version)); + if (!entry) { + throw new Error(`Could not find helm chart ${ chart } version ${ version }`); + } + } else { + entry = chartEntries.slice().sort((a, b) => semver.compare(a.version, b.version)).pop(); + if (!entry) { + throw new Error(`Could not find maximum version of helm chart ${ chart }`); + } + } + return { + checksumAlgorithm: 'sha256', + expectedChecksum: entry.digest, + url: new URL(entry.urls[0], indexURL) + }; +} diff --git a/scripts/postinstall.ts b/scripts/postinstall.ts index 8f06dbfcbf6..f03eb3199f1 100644 --- a/scripts/postinstall.ts +++ b/scripts/postinstall.ts @@ -85,6 +85,7 @@ const vmDependencies = [ // Dependencies that are specific to hosts. const hostDependencies = [ new MobyOpenAPISpec(), + new tools.RancherManager(), ]; async function downloadDependencies(items: DependencyWithContext[]): Promise { From d772964e1ba5766affda9e4486437f48565eb487 Mon Sep 17 00:00:00 2001 From: Mark Yen Date: Tue, 13 Aug 2024 17:57:43 -0700 Subject: [PATCH 3/9] Move install stuff to k3sHelper, minimum k3s 1.22 --- pkg/rancher-desktop/backend/backendHelper.ts | 80 ------------ pkg/rancher-desktop/backend/k3sHelper.ts | 123 ++++++++++++++++++- pkg/rancher-desktop/backend/kube/lima.ts | 7 +- pkg/rancher-desktop/backend/lima.ts | 2 - pkg/rancher-desktop/utils/fetch.ts | 2 +- 5 files changed, 128 insertions(+), 86 deletions(-) diff --git a/pkg/rancher-desktop/backend/backendHelper.ts b/pkg/rancher-desktop/backend/backendHelper.ts index 3c7fbfd7916..a4cdae4b1a6 100644 --- a/pkg/rancher-desktop/backend/backendHelper.ts +++ b/pkg/rancher-desktop/backend/backendHelper.ts @@ -1,4 +1,3 @@ -import crypto from 'crypto'; import path from 'path'; import Electron from 'electron'; @@ -6,10 +5,8 @@ import merge from 'lodash/merge'; import semver from 'semver'; import yaml from 'yaml'; -import CERT_MANAGER from '@pkg/assets/scripts/cert-manager.yaml'; import INSTALL_CONTAINERD_SHIMS_SCRIPT from '@pkg/assets/scripts/install-containerd-shims'; import CONTAINERD_CONFIG from '@pkg/assets/scripts/k3s-containerd-config.toml'; -import SPIN_OPERATOR from '@pkg/assets/scripts/spin-operator.yaml'; import { BackendSettings, VMExecutor } from '@pkg/backend/backend'; import { LockedFieldError } from '@pkg/config/commandLineOptions'; import { ContainerEngine, Settings } from '@pkg/config/settings'; @@ -20,8 +17,6 @@ import Logging from '@pkg/utils/logging'; import paths from '@pkg/utils/paths'; import { jsonStringifyWithWhiteSpace } from '@pkg/utils/stringify'; import { showMessageBox } from '@pkg/window'; -import DEPENDENCY_VERSIONS from '@pkg/assets/dependencies.yaml'; -import { KubeClient } from '@pkg/backend/kube/client'; const CONTAINERD_CONFIG_TOML = '/etc/containerd/config.toml'; const DOCKER_DAEMON_JSON = '/etc/docker/daemon.json'; @@ -37,11 +32,6 @@ export const MANIFEST_SPIN_OPERATOR_CRDS = 'z120-spin-operator.crds'; export const MANIFEST_SPIN_OPERATOR = 'z125-spin-operator'; export const MANIFEST_RANCHER = 'z130-rancher-manager'; -const STATIC_DIR = '/var/lib/rancher/k3s/server/static/rancher-desktop'; -const STATIC_CERT_MANAGER_CHART = `${ STATIC_DIR }/cert-manager.tgz`; -const STATIC_RANCHER_CHART = `${ STATIC_DIR }/rancher-manager.tgz` -const STATIC_SPIN_OPERATOR_CHART = `${ STATIC_DIR }/spin-operator.tgz`; - const console = Logging.kube; export default class BackendHelper { @@ -320,67 +310,6 @@ export default class BackendHelper { } } - static rancherPassword = crypto.randomBytes(32).toString('hex'); - - /** - * Write k3s manifests to install cert-manager, rancher manager, and spinkube - * operator. - * @param spin Whether to enable spinkube. - */ - static async configureKubeResources(vmx: VMExecutor, spin = false) { - let promises = []; - const rancherManifest = [ - { - apiVersion: 'v1', - kind: 'Namespace', - metadata: { name: 'cattle-system' }, - }, - { - apiVersion: 'helm.cattle.io/v1', - kind: 'HelmChart', - metadata: { - name: 'rancher-manager', - namespace: 'cattle-system', - }, - spec: { - chart: "https://%{KUBERNETES_API}%/static/rancher-desktop/rancher-manager.tgz", - targetNamespace: 'cattle-system', - // Old versions of the helm-controller don't support createNamespace, so we - // created the namespace ourselves. - createNamespace: false, - valuesContent: yaml.stringify({ - bootstrapPassword: BackendHelper.rancherPassword, - replicas: 1, - hostname: 'localhost', - useBundledSystemChart: true, - certmanager: { - version: DEPENDENCY_VERSIONS.certManager, - }, - postDelete: { - enabled: false, - }, - extraEnv: [ - { name: 'CATTLE_FEATURES', value: 'multi-cluster-management=false' }, - ] - }), - }, - }, - ].map(obj => '---\n' + yaml.stringify(obj)).join(''); - promises.push( - vmx.copyFileIn(path.join(paths.resources, 'cert-manager.crds.yaml'), this.manifestFilename(MANIFEST_CERT_MANAGER_CRDS)), - vmx.copyFileIn(path.join(paths.resources, 'cert-manager.tgz'), STATIC_CERT_MANAGER_CHART), - vmx.writeFile(this.manifestFilename(MANIFEST_CERT_MANAGER), CERT_MANAGER, 0o644), - vmx.copyFileIn(path.join(paths.resources, `rancher-${ DEPENDENCY_VERSIONS.rancher }.tgz`), STATIC_RANCHER_CHART)); - vmx.writeFile(this.manifestFilename(MANIFEST_RANCHER), rancherManifest, 0o644); - if (spin) { - promises.push( - vmx.copyFileIn(path.join(paths.resources, 'spin-operator.crds.yaml'), this.manifestFilename(MANIFEST_SPIN_OPERATOR_CRDS)), - vmx.copyFileIn(path.join(paths.resources, 'spin-operator.tgz'), STATIC_SPIN_OPERATOR_CHART), - vmx.writeFile(this.manifestFilename(MANIFEST_SPIN_OPERATOR), SPIN_OPERATOR, 0o644)); - } - await Promise.all(promises); - } - /** * Install containerd-wasm shims into /usr/local/containerd-shims (and symlinks into /usr/local/bin). */ @@ -442,13 +371,4 @@ export default class BackendHelper { await BackendHelper.writeContainerdConfig(vmx, configureWASM); await BackendHelper.writeMobyConfig(vmx, configureWASM); } - - static async setupRancherManager(client: KubeClient) { - const pod = await client.getActivePod('cattle-system', 'rancher-manager'); - - if (!pod) { - // We can get here if shutdown was initiated before the pod was ready. - return; - } - } } diff --git a/pkg/rancher-desktop/backend/k3sHelper.ts b/pkg/rancher-desktop/backend/k3sHelper.ts index f007ddcf6ed..d4ee6585b8e 100644 --- a/pkg/rancher-desktop/backend/k3sHelper.ts +++ b/pkg/rancher-desktop/backend/k3sHelper.ts @@ -1,10 +1,12 @@ import crypto from 'crypto'; import events from 'events'; import fs from 'fs'; +import https from 'https'; import os from 'os'; import path from 'path'; import stream from 'stream'; import tls from 'tls'; +import timers from 'timers/promises'; import util from 'util'; import { @@ -18,6 +20,8 @@ import yaml from 'yaml'; import { Architecture, VMExecutor } from './backend'; +import CERT_MANAGER from '@pkg/assets/scripts/cert-manager.yaml'; +import SPIN_OPERATOR from '@pkg/assets/scripts/spin-operator.yaml'; import * as K8s from '@pkg/backend/k8s'; import { KubeClient } from '@pkg/backend/kube/client'; import { loadFromString, exportConfig } from '@pkg/backend/kubeconfig'; @@ -35,10 +39,22 @@ import safeRename from '@pkg/utils/safeRename'; import { jsonStringifyWithWhiteSpace } from '@pkg/utils/stringify'; import { defined, RecursivePartial, RecursiveTypes } from '@pkg/utils/typeUtils'; import { showMessageBox } from '@pkg/window'; +import DEPENDENCY_VERSIONS from '@pkg/assets/dependencies.yaml'; import type Electron from 'electron'; const KubeContextName = 'rancher-desktop'; +const RancherPassword = 'password'; +const MANIFEST_DIR = '/var/lib/rancher/k3s/server/manifests'; +const MANIFEST_CERT_MANAGER_CRDS = 'z110-cert-manager.crds'; +const MANIFEST_CERT_MANAGER = 'z115-cert-manager'; +const MANIFEST_SPIN_OPERATOR_CRDS = 'z120-spin-operator.crds'; +const MANIFEST_SPIN_OPERATOR = 'z125-spin-operator'; +const MANIFEST_RANCHER_MANAGER = 'z130-rancher-manager'; +const STATIC_DIR = '/var/lib/rancher/k3s/server/static/rancher-desktop'; +const STATIC_CERT_MANAGER_CHART = `${ STATIC_DIR }/cert-manager.tgz`; +const STATIC_RANCHER_CHART = `${ STATIC_DIR }/rancher-manager.tgz` +const STATIC_SPIN_OPERATOR_CHART = `${ STATIC_DIR }/spin-operator.tgz`; const console = Logging.k8s; /** @@ -132,7 +148,7 @@ export default class K3sHelper extends events.EventEmitter { protected readonly releaseApiUrl = 'https://api.github.com/repos/k3s-io/k3s/releases?per_page=100'; protected readonly releaseApiAccept = 'application/vnd.github.v3+json'; protected readonly cachePath = path.join(paths.cache, 'k3s-versions.json'); - protected readonly minimumVersion = new semver.SemVer('1.21.0'); + protected readonly minimumVersion = new semver.SemVer('1.22.0'); protected versionFromChannel: Record = {}; constructor(arch: Architecture) { @@ -1201,6 +1217,111 @@ export default class K3sHelper extends events.EventEmitter { return results; } + + /** + * Write k3s manifests to install cert-manager, rancher manager, and spinkube + * operator. + * @param spin Whether to enable spinkube. + */ + static async configureKubeResources(vmx: VMExecutor, spin = false) { + function manifestFilename(name: string) { + return `${ MANIFEST_DIR }/${ name }.yaml`; + } + let promises = []; + const manifests: Record[] = [ + { + apiVersion: 'v1', + kind: 'Namespace', + metadata: { name: 'cattle-system' }, + }, + { + apiVersion: 'helm.cattle.io/v1', + kind: 'HelmChart', + metadata: { + name: 'rancher-manager', + namespace: 'cattle-system', + }, + spec: { + chart: "https://%{KUBERNETES_API}%/static/rancher-desktop/rancher-manager.tgz", + targetNamespace: 'cattle-system', + // Old versions of the helm-controller don't support createNamespace, so we + // created the namespace ourselves. + createNamespace: false, + valuesContent: yaml.stringify({ + bootstrapPassword: RancherPassword, + replicas: 1, + hostname: 'localhost', + useBundledSystemChart: true, + certmanager: { + version: DEPENDENCY_VERSIONS.certManager, + }, + postDelete: { + enabled: false, + }, + extraEnv: [ + { name: 'CATTLE_FEATURES', value: 'multi-cluster-management=false' }, + ] + }), + }, + }, + ] + promises.push( + vmx.copyFileIn(path.join(paths.resources, 'cert-manager.crds.yaml'), manifestFilename(MANIFEST_CERT_MANAGER_CRDS)), + vmx.copyFileIn(path.join(paths.resources, 'cert-manager.tgz'), STATIC_CERT_MANAGER_CHART), + vmx.writeFile(manifestFilename(MANIFEST_CERT_MANAGER), CERT_MANAGER, 0o644), + vmx.copyFileIn(path.join(paths.resources, `rancher-${ DEPENDENCY_VERSIONS.rancher }.tgz`), STATIC_RANCHER_CHART)); + vmx.writeFile(manifestFilename(MANIFEST_RANCHER_MANAGER), manifests.map(obj => '---\n' + yaml.stringify(obj)).join(''), 0o644); + if (spin) { + promises.push( + vmx.copyFileIn(path.join(paths.resources, 'spin-operator.crds.yaml'), manifestFilename(MANIFEST_SPIN_OPERATOR_CRDS)), + vmx.copyFileIn(path.join(paths.resources, 'spin-operator.tgz'), STATIC_SPIN_OPERATOR_CHART)); + vmx.writeFile(manifestFilename(MANIFEST_SPIN_OPERATOR), SPIN_OPERATOR, 0o644); + } + await Promise.all(promises); + } + + static async setupRancherManager(client: KubeClient) { + const pod = await client.getActivePod('cattle-system', 'rancher-manager'); + + if (!pod) { + // We can get here if shutdown was initiated before the pod was ready. + return; + } + + const timeout = AbortSignal.timeout(10 * 60 * 1_000); + while (!timeout.aborted) { + try { + const url = `https://localhost/dashboard/?setup=${ RancherPassword }`; + const agent = new https.Agent({ rejectUnauthorized: false }); + const resp = await fetch(url, { agent }); + + console.log(`${ url } => ${ resp.status }: ${ resp.statusText }`); + console.log(await resp.text()); + if (resp.ok) { + break; + } + } catch (ex) { + console.log(ex); + } + await timers.setTimeout(10_000); + } + + const setSetting = async(name: string, value: string) => { + const apiClient = client.k8sClient.makeApiClient(CustomObjectsApi); + const apiGroup = 'management.cattle.io'; + const apiVersion = 'v3'; + const settingsType = 'settings'; + + await apiClient.patchClusterCustomObject(apiGroup, apiVersion, settingsType, + name, {value}, undefined, undefined, undefined, { + headers: { 'Content-Type': 'application/merge-patch+json' }, + }); + }; + + await setSetting('first-login', 'false'); + await setSetting('eula-agreed', (new Date).toISOString()); + } + } interface V1HelmChart { diff --git a/pkg/rancher-desktop/backend/kube/lima.ts b/pkg/rancher-desktop/backend/kube/lima.ts index 401a1251cab..7d6f9d2b1ac 100644 --- a/pkg/rancher-desktop/backend/kube/lima.ts +++ b/pkg/rancher-desktop/backend/kube/lima.ts @@ -132,7 +132,7 @@ export default class LimaKubernetesBackend extends events.EventEmitter implement const promises: Promise[] = []; promises.push(this.writeServiceScript(config, desiredVersion, allowSudo)); - promises.push(BackendHelper.configureKubeResources(this.vm, + promises.push(K3sHelper.configureKubeResources(this.vm, config.experimental?.containerEngine?.webAssembly?.enabled && !!config.experimental?.kubernetes?.options?.spinkube)); if (config.experimental?.containerEngine?.webAssembly?.enabled) { @@ -253,7 +253,10 @@ export default class LimaKubernetesBackend extends events.EventEmitter implement await new Promise(resolve => setTimeout(resolve, 5000)); }); } - await BackendHelper.setupRancherManager(this.client); + await this.progressTracker.action('Finishing Kubernetes Startup', 100, + this.client?.getActivePod('kube-system', 'kube-dns')); + await this.progressTracker.action('Setting up Rancher Dashboard', 100, + K3sHelper.setupRancherManager(this.client)); } async stop() { diff --git a/pkg/rancher-desktop/backend/lima.ts b/pkg/rancher-desktop/backend/lima.ts index ed07c94ea20..3f27febf33f 100644 --- a/pkg/rancher-desktop/backend/lima.ts +++ b/pkg/rancher-desktop/backend/lima.ts @@ -1914,8 +1914,6 @@ export default class LimaBackend extends events.EventEmitter implements VMBacken if (kubernetesVersion) { await this.kubeBackend.start(config, kubernetesVersion); } - if (config.containerEngine.name === ContainerEngine.MOBY) { - } await this.setState(config.kubernetes.enabled ? State.STARTED : State.DISABLED); } catch (err) { diff --git a/pkg/rancher-desktop/utils/fetch.ts b/pkg/rancher-desktop/utils/fetch.ts index eb5c2b18883..ba3dfc7bfac 100644 --- a/pkg/rancher-desktop/utils/fetch.ts +++ b/pkg/rancher-desktop/utils/fetch.ts @@ -108,7 +108,6 @@ export default async function fetch(url: string, options?: RequestInit) { try { return await _fetch(url, { - ...options, agent: (parsedURL) => { // Find the correct agent, given user options and defaults. const isSecure = parsedURL.protocol.startsWith('https'); @@ -133,6 +132,7 @@ export default async function fetch(url: string, options?: RequestInit) { return result.agent; }, + ...options, }); } catch (ex) { // result.lastError may be set by createConnection from wrapCreateConnection. From 90b1bb846ca770692b8866e34bc4bbd0d2e2bdde Mon Sep 17 00:00:00 2001 From: Mark Yen Date: Wed, 14 Aug 2024 14:27:01 -0700 Subject: [PATCH 4/9] Trying to get login to work --- background.ts | 14 +++++ pkg/rancher-desktop/backend/backendHelper.ts | 15 ++---- pkg/rancher-desktop/backend/k3sHelper.ts | 19 +++++-- pkg/rancher-desktop/backend/kube/lima.ts | 5 +- pkg/rancher-desktop/backend/kube/wsl.ts | 5 +- pkg/rancher-desktop/main/networking/index.ts | 20 +++---- pkg/rancher-desktop/preload/dashboard.ts | 53 +++++++++++++++++++ pkg/rancher-desktop/preload/extensions.ts | 11 +++- pkg/rancher-desktop/preload/index.ts | 2 + pkg/rancher-desktop/typings/electron-ipc.d.ts | 8 +++ pkg/rancher-desktop/window/dashboard.ts | 53 +++++++++++-------- pkg/rancher-desktop/window/index.ts | 15 +++++- 12 files changed, 160 insertions(+), 60 deletions(-) create mode 100644 pkg/rancher-desktop/preload/dashboard.ts diff --git a/background.ts b/background.ts index 001422af7c8..fba9c03eb4a 100644 --- a/background.ts +++ b/background.ts @@ -52,6 +52,7 @@ import { getVersion } from '@pkg/utils/version'; import getWSLVersion from '@pkg/utils/wslVersion'; import * as window from '@pkg/window'; import { openPreferences, preferencesSetDirtyFlag } from '@pkg/window/preferences'; +import { closeDashboard, openDashboard } from '@pkg/window/dashboard'; Electron.app.setPath('cache', paths.cache); Electron.app.setAppLogsPath(paths.logs); @@ -675,6 +676,14 @@ ipcMainProxy.on('images-namespaces-read', (event) => { } }); +ipcMainProxy.on('dashboard-open', () => { + openDashboard(); +}); + +ipcMainProxy.on('dashboard-close', () => { + closeDashboard(); +}); + ipcMainProxy.on('preferences-open', () => { openPreferences(); }); @@ -699,6 +708,11 @@ function writeSettings(arg: RecursivePartial { writeSettings(arg); + // dashboard requires kubernetes, so we want to close it if kubernetes is disabled + if (arg?.kubernetes?.enabled === false) { + closeDashboard(); + } + event.sender.sendToFrame(event.frameId, 'settings-update', cfg); }); diff --git a/pkg/rancher-desktop/backend/backendHelper.ts b/pkg/rancher-desktop/backend/backendHelper.ts index a4cdae4b1a6..e130a3a2626 100644 --- a/pkg/rancher-desktop/backend/backendHelper.ts +++ b/pkg/rancher-desktop/backend/backendHelper.ts @@ -25,12 +25,7 @@ const MANIFEST_DIR = '/var/lib/rancher/k3s/server/manifests'; // Manifests are applied in sort order, so use a prefix to load them last, in the required sequence. // Names should start with `z` followed by a digit, so that `install-k3s` cleans them up on restart. -export const MANIFEST_RUNTIMES = 'z100-runtimes'; -export const MANIFEST_CERT_MANAGER_CRDS = 'z110-cert-manager.crds'; -export const MANIFEST_CERT_MANAGER = 'z115-cert-manager'; -export const MANIFEST_SPIN_OPERATOR_CRDS = 'z120-spin-operator.crds'; -export const MANIFEST_SPIN_OPERATOR = 'z125-spin-operator'; -export const MANIFEST_RANCHER = 'z130-rancher-manager'; +const MANIFEST_RUNTIMES = 'z100-runtimes'; const console = Logging.kube; @@ -281,10 +276,6 @@ export default class BackendHelper { return shims; } - private static manifestFilename(manifest: string): string { - return `${ MANIFEST_DIR }/${ manifest }.yaml`; - } - /** * Write a k3s manifest to define a runtime class for each installed containerd shim. */ @@ -306,7 +297,9 @@ export default class BackendHelper { if (runtimes.length > 0) { const manifest = runtimes.map(r => yaml.stringify(r)).join('---\n'); - await vmx.writeFile(this.manifestFilename(MANIFEST_RUNTIMES), manifest, 0o644); + await vmx.writeFile(`${ MANIFEST_DIR }/${ MANIFEST_RUNTIMES }.yaml`, + manifest, + 0o644); } } diff --git a/pkg/rancher-desktop/backend/k3sHelper.ts b/pkg/rancher-desktop/backend/k3sHelper.ts index d4ee6585b8e..9ec4e08188c 100644 --- a/pkg/rancher-desktop/backend/k3sHelper.ts +++ b/pkg/rancher-desktop/backend/k3sHelper.ts @@ -45,11 +45,13 @@ import type Electron from 'electron'; const KubeContextName = 'rancher-desktop'; const RancherPassword = 'password'; +// Manifests are applied in sort order, so use a prefix to load them last, in the required sequence. +// Names should start with `z` followed by a digit, so that `install-k3s` cleans them up on restart. const MANIFEST_DIR = '/var/lib/rancher/k3s/server/manifests'; const MANIFEST_CERT_MANAGER_CRDS = 'z110-cert-manager.crds'; const MANIFEST_CERT_MANAGER = 'z115-cert-manager'; const MANIFEST_SPIN_OPERATOR_CRDS = 'z120-spin-operator.crds'; -const MANIFEST_SPIN_OPERATOR = 'z125-spin-operator'; +export const MANIFEST_SPIN_OPERATOR = 'z125-spin-operator'; const MANIFEST_RANCHER_MANAGER = 'z130-rancher-manager'; const STATIC_DIR = '/var/lib/rancher/k3s/server/static/rancher-desktop'; const STATIC_CERT_MANAGER_CHART = `${ STATIC_DIR }/cert-manager.tgz`; @@ -1259,7 +1261,16 @@ export default class K3sHelper extends events.EventEmitter { enabled: false, }, extraEnv: [ - { name: 'CATTLE_FEATURES', value: 'multi-cluster-management=false' }, + { name: 'CATTLE_FEATURES', + value: [ + 'auth=false', + 'multi-cluster-management=false', + 'fleet=false', + 'harvester=false', + 'continuous-delivery=false', + 'rke1-ui=false', + 'rke2=false', + ].join(',') }, ] }), }, @@ -1270,13 +1281,13 @@ export default class K3sHelper extends events.EventEmitter { vmx.copyFileIn(path.join(paths.resources, 'cert-manager.tgz'), STATIC_CERT_MANAGER_CHART), vmx.writeFile(manifestFilename(MANIFEST_CERT_MANAGER), CERT_MANAGER, 0o644), vmx.copyFileIn(path.join(paths.resources, `rancher-${ DEPENDENCY_VERSIONS.rancher }.tgz`), STATIC_RANCHER_CHART)); - vmx.writeFile(manifestFilename(MANIFEST_RANCHER_MANAGER), manifests.map(obj => '---\n' + yaml.stringify(obj)).join(''), 0o644); + vmx.writeFile(manifestFilename(MANIFEST_RANCHER_MANAGER), manifests.map(m => yaml.stringify(m)).join('---\n'), 0o644); if (spin) { promises.push( vmx.copyFileIn(path.join(paths.resources, 'spin-operator.crds.yaml'), manifestFilename(MANIFEST_SPIN_OPERATOR_CRDS)), vmx.copyFileIn(path.join(paths.resources, 'spin-operator.tgz'), STATIC_SPIN_OPERATOR_CHART)); vmx.writeFile(manifestFilename(MANIFEST_SPIN_OPERATOR), SPIN_OPERATOR, 0o644); - } + } await Promise.all(promises); } diff --git a/pkg/rancher-desktop/backend/kube/lima.ts b/pkg/rancher-desktop/backend/kube/lima.ts index 7d6f9d2b1ac..a57d6e2927a 100644 --- a/pkg/rancher-desktop/backend/kube/lima.ts +++ b/pkg/rancher-desktop/backend/kube/lima.ts @@ -8,8 +8,8 @@ import util from 'util'; import semver from 'semver'; import { Architecture, BackendSettings, RestartReasons } from '../backend'; -import BackendHelper, { MANIFEST_CERT_MANAGER, MANIFEST_SPIN_OPERATOR } from '../backendHelper'; -import K3sHelper, { ExtraRequiresReasons, NoCachedK3sVersionsError, ShortVersion } from '../k3sHelper'; +import BackendHelper from '../backendHelper'; +import K3sHelper, { ExtraRequiresReasons, MANIFEST_SPIN_OPERATOR, NoCachedK3sVersionsError, ShortVersion } from '../k3sHelper'; import LimaBackend, { Action } from '../lima'; import INSTALL_K3S_SCRIPT from '@pkg/assets/scripts/install-k3s'; @@ -230,7 +230,6 @@ export default class LimaKubernetesBackend extends events.EventEmitter implement 'Removing spinkube operator', 50, Promise.all([ - this.k3sHelper.uninstallHelmChart(this.client, MANIFEST_CERT_MANAGER), this.k3sHelper.uninstallHelmChart(this.client, MANIFEST_SPIN_OPERATOR), ])); } diff --git a/pkg/rancher-desktop/backend/kube/wsl.ts b/pkg/rancher-desktop/backend/kube/wsl.ts index a57bdda4df9..08e5973d79c 100644 --- a/pkg/rancher-desktop/backend/kube/wsl.ts +++ b/pkg/rancher-desktop/backend/kube/wsl.ts @@ -6,12 +6,12 @@ import util from 'util'; import semver from 'semver'; import { KubeClient } from './client'; -import K3sHelper, { ExtraRequiresReasons, NoCachedK3sVersionsError, ShortVersion } from '../k3sHelper'; +import K3sHelper, { ExtraRequiresReasons, MANIFEST_SPIN_OPERATOR, NoCachedK3sVersionsError, ShortVersion } from '../k3sHelper'; import WSLBackend, { Action } from '../wsl'; import INSTALL_K3S_SCRIPT from '@pkg/assets/scripts/install-k3s'; import { BackendSettings, RestartReasons } from '@pkg/backend/backend'; -import BackendHelper, { MANIFEST_CERT_MANAGER, MANIFEST_SPIN_OPERATOR } from '@pkg/backend/backendHelper'; +import BackendHelper from '@pkg/backend/backendHelper'; import * as K8s from '@pkg/backend/k8s'; import { ContainerEngine } from '@pkg/config/settings'; import mainEvents from '@pkg/main/mainEvents'; @@ -263,7 +263,6 @@ export default class WSLKubernetesBackend extends events.EventEmitter implements 'Removing spinkube operator', 50, Promise.all([ - this.k3sHelper.uninstallHelmChart(this.client, MANIFEST_CERT_MANAGER), this.k3sHelper.uninstallHelmChart(this.client, MANIFEST_SPIN_OPERATOR), ])); } diff --git a/pkg/rancher-desktop/main/networking/index.ts b/pkg/rancher-desktop/main/networking/index.ts index a34ccebdcb5..d636dc0e116 100644 --- a/pkg/rancher-desktop/main/networking/index.ts +++ b/pkg/rancher-desktop/main/networking/index.ts @@ -14,7 +14,7 @@ import getWinCertificates from './win-ca'; import mainEvents from '@pkg/main/mainEvents'; import Logging from '@pkg/utils/logging'; -import { windowMapping } from '@pkg/window'; +import { getWindowName, windowMapping } from '@pkg/window'; const console = Logging.networking; @@ -43,20 +43,12 @@ export default async function setupNetworking() { // Set up certificate handling for system certificates on Windows and macOS Electron.app.on('certificate-error', async(event, webContents, url, error, certificate, callback) => { - const tlsPort = 9443; - const dashboardUrls = [ - `https://127.0.0.1:${ tlsPort }`, - `wss://127.0.0.1:${ tlsPort }`, - 'http://127.0.0.1:6120', - 'ws://127.0.0.1:6120', - ]; - - const pluginDevUrls = [ - `https://localhost:8888`, - `wss://localhost:8888`, - ]; + const windowName = getWindowName(webContents); + const pluginDevUrls = [`https://localhost:8888`, `wss://localhost:8888`]; + const dashboardUrls = ['https://localhost/', 'wss://localhost/']; if ( + windowName === 'main' && process.env.NODE_ENV === 'development' && process.env.RD_ENV_PLUGINS_DEV && pluginDevUrls.some(x => url.startsWith(x)) @@ -68,7 +60,7 @@ export default async function setupNetworking() { return; } - if (dashboardUrls.some(x => url.startsWith(x)) && 'dashboard' in windowMapping) { + if (windowName === 'dashboard' && dashboardUrls.some(u => url.startsWith(u))) { event.preventDefault(); // eslint-disable-next-line n/no-callback-literal callback(true); diff --git a/pkg/rancher-desktop/preload/dashboard.ts b/pkg/rancher-desktop/preload/dashboard.ts new file mode 100644 index 00000000000..1b57a1c070e --- /dev/null +++ b/pkg/rancher-desktop/preload/dashboard.ts @@ -0,0 +1,53 @@ +import { ipcRenderer } from '@pkg/utils/ipcRenderer'; + +export default function initDashboard(): void { + if (!document.location.href.startsWith('https://localhost/dashboard/')) { + return; + } + console.log('Will init dashboard!'); + async function onNavigate(event: Event) { + console.log(`${ event.type }! -> ${ location.href }`); + + const resp = await fetch('https://localhost/v3/users?me=true'); + + console.log(resp); + if (resp.status === 401) { + // Need to login + const token = await ipcRenderer.invoke('dashboard/get-csrf-token') ?? ''; + await fetch("https://localhost/v3-public/localProviders/local?action=login", { + headers: { + 'Accept': "application/json", + 'Content-Type': "application/json", + 'X-API-CSRF': token, + }, + body: JSON.stringify({ + description: 'Rancher Desktop session', + responseType: 'cookie', + username: 'admin', + password: 'password', + }), + method: "POST", + credentials: "include" + }); + } + + if (location.pathname === '/dashboard/auth/login') { + console.log('Logging in!'); + /** Helper to evalute a singel XPath expression */ + function $x(expr: string) { + return document.evaluate( + expr, + document, + null, + XPathResult.FIRST_ORDERED_NODE_TYPE + ).singleNodeValue as T; + } + $x('//*[@id="username"]/descendant-or-self:input').value = 'admin'; + $x('//*[@id="password"]/descendant-or-self:input').value = 'password'; + $x('//*[@id=submit]').click(); + } + } + window.addEventListener('hashchange', onNavigate); + window.addEventListener('pageshow', onNavigate); + window.addEventListener('popstate', onNavigate); +} diff --git a/pkg/rancher-desktop/preload/extensions.ts b/pkg/rancher-desktop/preload/extensions.ts index 4bd41065285..be9b1e4cfb1 100644 --- a/pkg/rancher-desktop/preload/extensions.ts +++ b/pkg/rancher-desktop/preload/extensions.ts @@ -63,7 +63,16 @@ interface RDXSpawnOptions extends v1.SpawnOptions { /** * The identifier for the extension (the name of the image). */ -const extensionId = location.protocol === 'app:' ? '' : decodeURIComponent(location.hostname.replace(/(..)/g, '%$1')); +const extensionId = (function(){ + switch (location.protocol) { + case 'app:': + return ''; + case 'x-rd-extension:': + return decodeURIComponent(location.hostname.replace(/(..)/g, '%$1')); + default: + return '' + } +})(); /** * The processes that are waiting to complete, keyed by the process ID. diff --git a/pkg/rancher-desktop/preload/index.ts b/pkg/rancher-desktop/preload/index.ts index 42e6485f903..b1956c67a47 100644 --- a/pkg/rancher-desktop/preload/index.ts +++ b/pkg/rancher-desktop/preload/index.ts @@ -1,7 +1,9 @@ +import initDashboard from './dashboard'; import initExtensions from './extensions'; function init() { initExtensions(); + initDashboard(); } try { diff --git a/pkg/rancher-desktop/typings/electron-ipc.d.ts b/pkg/rancher-desktop/typings/electron-ipc.d.ts index 22bfc34d91f..4a3c4c37d73 100644 --- a/pkg/rancher-desktop/typings/electron-ipc.d.ts +++ b/pkg/rancher-desktop/typings/electron-ipc.d.ts @@ -76,6 +76,9 @@ export interface IpcMainEvents { 'show-logs': () => void; + 'dashboard-open': () => void; + 'dashboard-close': () => void; + 'diagnostics/run': () => void; /** Only for the preferences window */ @@ -148,6 +151,10 @@ export interface IpcMainInvokeEvents { 'show-snapshots-confirm-dialog': (options: { window: Partial, format: SnapshotDialog }) => any; 'show-snapshots-blocking-dialog': (options: { window: Partial, format: SnapshotDialog }) => any; // #endregion + + // #region dashboard + 'dashboard/get-csrf-token': () => string | null; + // #endregion } /** @@ -213,6 +220,7 @@ export interface IpcRendererEvents { 'dialog/close': (...args: any) => void; 'dialog/error': (args: any) => void; 'dialog/info': (args: Record) => void; + 'dashboard-open': () => void; // #endregion // #region tab navigation diff --git a/pkg/rancher-desktop/window/dashboard.ts b/pkg/rancher-desktop/window/dashboard.ts index bdc0d6dcf5e..42031545378 100644 --- a/pkg/rancher-desktop/window/dashboard.ts +++ b/pkg/rancher-desktop/window/dashboard.ts @@ -1,34 +1,43 @@ -import { BrowserWindow } from 'electron'; - -import { windowMapping, restoreWindow } from '.'; - -const dashboardURL = 'http://127.0.0.1:6120/c/local/explorer'; - -const getDashboardWindow = () => ('dashboard' in windowMapping) ? BrowserWindow.fromId(windowMapping['dashboard']) : null; +import path from 'path'; +import { createWindow, getWindow } from '.'; +import paths from '@pkg/utils/paths'; +import { getIpcMainProxy } from '@pkg/main/ipcMain'; +import Logging from '@pkg/utils/logging'; + +const dashboardName = 'dashboard'; +const dashboardURL = 'https://localhost/dashboard/c/local/explorer'; +const console = Logging.dashboard; +const ipcMain = getIpcMainProxy(console); + +ipcMain.removeHandler('dashboard/get-csrf-token'); +ipcMain.handle('dashboard/get-csrf-token', async (event) => { + const webContents = event.sender; + const cookies = await webContents.session.cookies.get({ + url: webContents.getURL(), + name: 'CSRF', + }); + return cookies?.[0].value ?? null; +}) export function openDashboard() { - let window = getDashboardWindow(); - - if (restoreWindow(window)) { - return window; - } - - window = new BrowserWindow({ - title: 'Rancher Dashboard', - width: 800, + const window = createWindow('dashboard', dashboardURL, { + title: 'Rancher Dashboard', + width: 800, height: 600, - show: false, + show: false, + webPreferences: { + nodeIntegration: false, + contextIsolation: true, + preload: path.join(paths.resources, 'preload.js'), + sandbox: true, + }, }); - window.loadURL(dashboardURL); - - windowMapping['dashboard'] = window.id; - window.once('ready-to-show', () => { window?.show(); }); } export function closeDashboard() { - getDashboardWindow()?.close(); + getWindow(dashboardName)?.close(); } diff --git a/pkg/rancher-desktop/window/index.ts b/pkg/rancher-desktop/window/index.ts index 923579a3aef..7dbe0594873 100644 --- a/pkg/rancher-desktop/window/index.ts +++ b/pkg/rancher-desktop/window/index.ts @@ -52,6 +52,14 @@ export function getWindow(name: string): Electron.BrowserWindow | null { return (name in windowMapping) ? BrowserWindow.fromId(windowMapping[name]) : null; } +export function getWindowName(webContents: Electron.WebContents): string | null { + const window = Electron.BrowserWindow.fromWebContents(webContents); + const entries = Object.entries(windowMapping); + const [name, ] = entries.find(([, id]) => id === window?.id) ?? [null, ]; + + return name; +} + /** * Open a given window; if it is already open, focus it. * @param name The window identifier; this controls window re-use. @@ -65,9 +73,12 @@ export function createWindow(name: string, url: string, options: Electron.Browse return window; } - const isInternalURL = (url: string) => { + function isInternalURL(url: string) { + if (name === 'dashboard') { + return url.startsWith('https://localhost/'); + } return url.startsWith(`${ webRoot }/`) || url.startsWith('x-rd-extension://'); - }; + } window = new BrowserWindow(options); window.webContents.on('console-message', (event, level, message, line, sourceId) => { From 50a91632eef11dabf004a47f988986c7da11da56 Mon Sep 17 00:00:00 2001 From: Mark Yen Date: Wed, 14 Aug 2024 17:33:31 -0700 Subject: [PATCH 5/9] Dashboard: Fix auto-login; hide extensions We are (at least for now) disabling dashboard extensions to reduce the amount of stuff we have to worry about. We may end up re-enabling them before this is merged (after cleanup). --- pkg/rancher-desktop/backend/k3sHelper.ts | 6 +-- pkg/rancher-desktop/preload/dashboard.ts | 54 +++++++++++++----------- pkg/rancher-desktop/window/dashboard.ts | 31 +++++++++++--- 3 files changed, 57 insertions(+), 34 deletions(-) diff --git a/pkg/rancher-desktop/backend/k3sHelper.ts b/pkg/rancher-desktop/backend/k3sHelper.ts index 9ec4e08188c..a28d563c148 100644 --- a/pkg/rancher-desktop/backend/k3sHelper.ts +++ b/pkg/rancher-desktop/backend/k3sHelper.ts @@ -1263,13 +1263,13 @@ export default class K3sHelper extends events.EventEmitter { extraEnv: [ { name: 'CATTLE_FEATURES', value: [ - 'auth=false', - 'multi-cluster-management=false', + 'continuous-delivery=false', 'fleet=false', 'harvester=false', - 'continuous-delivery=false', + 'multi-cluster-management=false', 'rke1-ui=false', 'rke2=false', + 'uiextension=false', ].join(',') }, ] }), diff --git a/pkg/rancher-desktop/preload/dashboard.ts b/pkg/rancher-desktop/preload/dashboard.ts index 1b57a1c070e..b7c5707fad5 100644 --- a/pkg/rancher-desktop/preload/dashboard.ts +++ b/pkg/rancher-desktop/preload/dashboard.ts @@ -4,17 +4,16 @@ export default function initDashboard(): void { if (!document.location.href.startsWith('https://localhost/dashboard/')) { return; } - console.log('Will init dashboard!'); - async function onNavigate(event: Event) { - console.log(`${ event.type }! -> ${ location.href }`); - + // Navigation API is only available in Chrome-derived browsers like Electron. + // https://developer.mozilla.org/en-US/docs/Web/API/Navigation + (window as any).navigation.addEventListener('navigate', async function onNavigate() { const resp = await fetch('https://localhost/v3/users?me=true'); + let loginSuccessful = false; - console.log(resp); if (resp.status === 401) { - // Need to login const token = await ipcRenderer.invoke('dashboard/get-csrf-token') ?? ''; - await fetch("https://localhost/v3-public/localProviders/local?action=login", { + const loginURL = 'https://localhost/v3-public/localProviders/local?action=login'; + const resp = await fetch(loginURL, { headers: { 'Accept': "application/json", 'Content-Type': "application/json", @@ -29,25 +28,30 @@ export default function initDashboard(): void { method: "POST", credentials: "include" }); + loginSuccessful = resp.ok; } - if (location.pathname === '/dashboard/auth/login') { - console.log('Logging in!'); - /** Helper to evalute a singel XPath expression */ - function $x(expr: string) { - return document.evaluate( - expr, - document, - null, - XPathResult.FIRST_ORDERED_NODE_TYPE - ).singleNodeValue as T; - } - $x('//*[@id="username"]/descendant-or-self:input').value = 'admin'; - $x('//*[@id="password"]/descendant-or-self:input').value = 'password'; - $x('//*[@id=submit]').click(); + switch (location.pathname) { + case '/dashboard/auth/login': + // If we logged in, return to the page before the login form. + if (loginSuccessful) { + history.back(); + } + return; + case '/dashboard/home': + // Whenever we go to home, replace with cluster explorer. + location.pathname = '/dashboard/c/local/explorer'; + return; } - } - window.addEventListener('hashchange', onNavigate); - window.addEventListener('pageshow', onNavigate); - window.addEventListener('popstate', onNavigate); + }); + window.addEventListener('load', function() { + const stylesheet = new CSSStyleSheet(); + // Hide the extensions navigation button. + stylesheet.insertRule(` + .side-menu div:has(> a.option[href="/dashboard/c/local/uiplugins"]) { + display: none; + } + `); + document.adoptedStyleSheets.push(stylesheet); + }); } diff --git a/pkg/rancher-desktop/window/dashboard.ts b/pkg/rancher-desktop/window/dashboard.ts index 42031545378..4c54559e0f3 100644 --- a/pkg/rancher-desktop/window/dashboard.ts +++ b/pkg/rancher-desktop/window/dashboard.ts @@ -12,12 +12,31 @@ const ipcMain = getIpcMainProxy(console); ipcMain.removeHandler('dashboard/get-csrf-token'); ipcMain.handle('dashboard/get-csrf-token', async (event) => { const webContents = event.sender; - const cookies = await webContents.session.cookies.get({ - url: webContents.getURL(), - name: 'CSRF', - }); - return cookies?.[0].value ?? null; -}) + const url = new URL(webContents.getURL()); + const cookies = webContents.session.cookies; + + while (true) { + const existingCookies = await cookies.get({domain: url.hostname, name: 'CSRF'}); + if (existingCookies.length > 0) { + console.log(`Got existing cookie: ${ existingCookies[0].value }`); + return existingCookies[0].value; + } + + // Cookie does not exist yet; wait for a cookie with the correct name to be + // created, then try again (to match the hostname). + console.log('Waiting for cookie to show up'); + await new Promise((resolve) => { + function onCookieChange(_event: any, cookie: Electron.Cookie, _cause: any, removed: boolean) { + console.log(`Cookie change: ${ cookie.name } (${ removed })`); + if (!removed && cookie.name === 'CSRF') { + cookies.removeListener('changed', onCookieChange); + resolve(); + } + } + cookies.addListener('changed', onCookieChange); + }); + } +}); export function openDashboard() { const window = createWindow('dashboard', dashboardURL, { From 839e1aea651cf2a24c4334993bf7f0dbcb0b8a8e Mon Sep 17 00:00:00 2001 From: Mark Yen Date: Mon, 19 Aug 2024 11:42:48 -0700 Subject: [PATCH 6/9] Dashboard: Use envoy instead of ingress Since ingress doesn't currently work on Windows, use envoy to do SSL termination and do Kubernetes-level service port forwarding instead. This also means it will work without traefik. --- .../assets/scripts/rancher-manager-envoy.yaml | 120 ++++++++++++++++++ pkg/rancher-desktop/backend/k3sHelper.ts | 55 +++++++- pkg/rancher-desktop/backend/kube/wsl.ts | 6 +- pkg/rancher-desktop/main/mainEvents.ts | 6 + pkg/rancher-desktop/main/networking/index.ts | 8 +- pkg/rancher-desktop/preload/dashboard.ts | 9 +- pkg/rancher-desktop/preload/index.ts | 2 +- pkg/rancher-desktop/typings/electron-ipc.d.ts | 1 + pkg/rancher-desktop/window/dashboard.ts | 9 +- pkg/rancher-desktop/window/index.ts | 6 +- 10 files changed, 210 insertions(+), 12 deletions(-) create mode 100644 pkg/rancher-desktop/assets/scripts/rancher-manager-envoy.yaml diff --git a/pkg/rancher-desktop/assets/scripts/rancher-manager-envoy.yaml b/pkg/rancher-desktop/assets/scripts/rancher-manager-envoy.yaml new file mode 100644 index 00000000000..1bad6ce29e2 --- /dev/null +++ b/pkg/rancher-desktop/assets/scripts/rancher-manager-envoy.yaml @@ -0,0 +1,120 @@ +# yaml-language-server: $schema=https://github.com/jcchavezs/envoy-config-schema/releases/download/v1.21.0/v3_Bootstrap.json +--- +static_resources: + listeners: + - name: tls-termination + address: + socket_address: + protocol: TCP + address: 0.0.0.0 + port_value: 9443 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: rancher_manager + route_config: + name: route + virtual_hosts: + - name: app + domains: [ "*" ] + routes: + - match: { prefix: / } + route: + cluster: rancher-manager + host_rewrite_literal: localhost + append_x_forwarded_host: true + request_headers_to_add: + - header: { key: X-Forwarded-Proto, value: https } + append_action: OVERWRITE_IF_EXISTS_OR_ADD + - header: { key: X-Forwarded-Port, value: '443' } + append_action: OVERWRITE_IF_EXISTS_OR_ADD + - header: { key: X-Forwarded-For, value: '192.0.2.1' } + append_action: OVERWRITE_IF_EXISTS_OR_ADD + http_filters: + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + transport_socket: + name: envoy.transport_sockets.tls + typed_config: + "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext + common_tls_context: + tls_certificates: + # openssl req -x509 -newkey rsa:2048 -keyout key.pem -out crt.pem + # -days 36500 -nodes -subj '/CN=rancher-manager-https-termination' + # Per CA/B BR 6.1.5 RSA keys are a minimum of 2048 bits; and ECDSA + # keys must be ST P‐256, NIST P‐384 or NIST P‐521. + - certificate_chain: + inline_string: | + -----BEGIN CERTIFICATE----- + MIIDOzCCAiOgAwIBAgIUA4weh/2CMM0zwHuSIhkbaFEvqRMwDQYJKoZIhvcNAQEL + BQAwLDEqMCgGA1UEAwwhcmFuY2hlci1tYW5hZ2VyLWh0dHBzLXRlcm1pbmF0aW9u + MCAXDTI0MDgxNTIzNDI0OFoYDzIxMjQwNzIyMjM0MjQ4WjAsMSowKAYDVQQDDCFy + YW5jaGVyLW1hbmFnZXItaHR0cHMtdGVybWluYXRpb24wggEiMA0GCSqGSIb3DQEB + AQUAA4IBDwAwggEKAoIBAQDbpo3Nvrvi6Ev5MGX1ukYh3Tuu03MHtzimGZs/0U+r + LJoVLBkWd4fUNit1wfvYSOJEdb1WMeU/IS36AzmTs4vkRVpilcow5LLklrmn2XJf + M7uvzUzBCzz6VnP7D0ltcD2u3VDplQv/doqm6p0vKE6CpYiaSjGq5ks6DPXaJZKO + 2HAtDjuIYJq8Dg+BwnkHmFHD30vpl7+LmnZ7WTmJlg1cqSCHDLKeNrVbTD9ua6GD + 4ImK+kLQQXPsvMM1QZXIg7mWslBLD9ucQosTSzCN9aVFqNnd3Nx2Ir5G0tc6ZwKg + cDawJyc3fYUQocNhKlJPa+eQl5u0quzCRsqRTTNlCV/HAgMBAAGjUzBRMB0GA1Ud + DgQWBBRlRLHhQ1GwgWJHglLSaLiw7gaPyjAfBgNVHSMEGDAWgBRlRLHhQ1GwgWJH + glLSaLiw7gaPyjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAr + SPAIOVGSuVa8z+Va9+J6fG+TrCx2pR6HUlayDpWfZ6LZXN4lIQ1Nrfnt2amwxbRA + 95gxSnJyAXS2jLaLLuqR1fCKFE/xxH7TyVyShr3mUUyP7rt/iHlig9Io3lST9mbk + /4ovlHJEQcgn+5TEfwzDzq76arvaLqpMKQk7p0V2F/YCoEE0V6d9ZMmgfyTG3ayA + wh1oodQFKrA8vXyhbIUP+kM5KAxm0qxQaYNbZfXTkCw4CEGSVxDv8hY1S606QUdS + /YYG4HHEzdSVqeDsV1F6mD28TMZfnOpP6OFLxLhi2TOwwsWPRwoxwL0H+i7glWUS + 682jYqxqLq+/OKsX+6Ul + -----END CERTIFICATE----- + private_key: + inline_string: | + -----BEGIN PRIVATE KEY----- + MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDbpo3Nvrvi6Ev5 + MGX1ukYh3Tuu03MHtzimGZs/0U+rLJoVLBkWd4fUNit1wfvYSOJEdb1WMeU/IS36 + AzmTs4vkRVpilcow5LLklrmn2XJfM7uvzUzBCzz6VnP7D0ltcD2u3VDplQv/doqm + 6p0vKE6CpYiaSjGq5ks6DPXaJZKO2HAtDjuIYJq8Dg+BwnkHmFHD30vpl7+LmnZ7 + WTmJlg1cqSCHDLKeNrVbTD9ua6GD4ImK+kLQQXPsvMM1QZXIg7mWslBLD9ucQosT + SzCN9aVFqNnd3Nx2Ir5G0tc6ZwKgcDawJyc3fYUQocNhKlJPa+eQl5u0quzCRsqR + TTNlCV/HAgMBAAECggEAA038MC5AcWeBTRx3TD0jNPs5HKY9ws304jrcZRdnFXI0 + V0E0l2vw9TZjbQAgI97k2JbU5GkXw91h7bMCuMAoyKRqebU7N4UZU+sYm/ffiqMi + ncB++SCMKFAIqqxONIFNzEW0I++EILHN4DkDaGQ42ipXZcrb+HBCjXsIb+HE1LVR + yBXdxpQV3JWqJrYiM1iXch5tuW/03Re68wMq2nfpe854vFcd5UoV1w9kRdsEhlNT + BHi7uO0+LtoB0CY+WSMq2Dpbp5DTL4WfiVtvbT7W1rGAE/lDjcW/3n/ed8TvRrhd + /EkuTQKiIOR2QCrCEtVVmRisl78SW4/1bqrMq841QQKBgQDtzTquacD9OX2oWTO6 + p2EVgSYHVnOfQaM+bUlq2NbcVsvoN+8QCWgw9mR6OxxH7CJvDNQG5mELEku2SeJ4 + 8LYhIkFEAyY7QDa+lIysclqY6wtq1Vw40hMs+idTfm78ZkGgsUgr7luZxj2HYUhx + zsPE3XcgznWN5lVkheXu1v1p9wKBgQDsdbokoYP6zfsuddrW6qe2GsXFY3N/CvrX + zBWN4FIoRLUYbDuxA+91Cbac5JCt4o6AUphsSz+qxqj1gvvNjjFMzGe7S2xJjSqu + H3csLKwpje9HL55cO1llnb559kg9XAbwLdJd5bWdhfRLahIbST+me36Mcfqqggbz + H5hAPl7EsQKBgD8zjmcQgFRM1VLK8m6nUawvePX2SiCHh2VuElctbl19TBBZ3VW7 + yk9JDQdXcnrDDZvKIwf6bsxMfobiOCjAgQdpXUNAOwcAWAxq2sByXBXMUmqAblRD + sQkBKzaLod+/Ja4Zr/7NCNdj0rKKboCg3XMTEThM5v1hvExNMgE6bnudAoGAQCh5 + RzMj0ktNWf/UTvgAZWLCQpqHXfMmuKLBPmudHxv1XxkO4SrGMCVgjRVfRC7yp1LB + 1LBeKAIbGfJeTBnGuqXDh4gha5uH9xLGjQ/Z7rR6NgBvoWrhCLdSVVlDpJJxt31X + VO7c5k7QSB4Rp6GqSYu8fHL4pob9R75M2zGRGSECgYEAjYzGEmXo+f2ezI+GHYHB + F8wWQhREOONC10MJ2ADj4FoPgbMghdfbpkHDTC0FFQqi4gCOLpU7h4H8/PDOl9vL + yXe6fabXFZrFrTa9IYO1ImWa6lkOWY4hO7DcKqWQzHFll93+Cs0STAhdSfEad0Fe + Sibf5N6AjHN4gWm/gCnn2nw= + -----END PRIVATE KEY----- + clusters: + - name: rancher-manager + type: STRICT_DNS + load_assignment: + cluster_name: rancher-manager + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: rancher-manager.cattle-system.svc + port_value: 80 + health_checks: + - timeout: 1s + interval: 30s + unhealthy_threshold: 5 + healthy_threshold: 1 + http_health_check: + host: localhost + path: /healthz diff --git a/pkg/rancher-desktop/backend/k3sHelper.ts b/pkg/rancher-desktop/backend/k3sHelper.ts index a28d563c148..5ef3757116d 100644 --- a/pkg/rancher-desktop/backend/k3sHelper.ts +++ b/pkg/rancher-desktop/backend/k3sHelper.ts @@ -22,6 +22,7 @@ import { Architecture, VMExecutor } from './backend'; import CERT_MANAGER from '@pkg/assets/scripts/cert-manager.yaml'; import SPIN_OPERATOR from '@pkg/assets/scripts/spin-operator.yaml'; +import RANCHER_MANAGER_ENVOY_CONFIG from '@pkg/assets/scripts/rancher-manager-envoy.yaml'; import * as K8s from '@pkg/backend/k8s'; import { KubeClient } from '@pkg/backend/kube/client'; import { loadFromString, exportConfig } from '@pkg/backend/kubeconfig'; @@ -42,6 +43,7 @@ import { showMessageBox } from '@pkg/window'; import DEPENDENCY_VERSIONS from '@pkg/assets/dependencies.yaml'; import type Electron from 'electron'; +import mainEvents from '@pkg/main/mainEvents'; const KubeContextName = 'rancher-desktop'; const RancherPassword = 'password'; @@ -1260,6 +1262,9 @@ export default class K3sHelper extends events.EventEmitter { postDelete: { enabled: false, }, + ingress: { + enabled: false, + }, extraEnv: [ { name: 'CATTLE_FEATURES', value: [ @@ -1275,6 +1280,47 @@ export default class K3sHelper extends events.EventEmitter { }), }, }, + { + apiVersion: 'apps/v1', + kind: 'Deployment', + metadata: { + name: 'rancher-envoy', + namespace: 'cattle-system', + }, + spec: { + replicas: 1, + selector: { + matchLabels: { app: 'rancher-envoy' }, + }, + template: { + metadata: { labels: { app: 'rancher-envoy' } }, + spec: { + containers: [{ + name: 'envoy', + image: 'envoyproxy/envoy:distroless-v1.31-latest', + args: [ + '--config-yaml', RANCHER_MANAGER_ENVOY_CONFIG + ], + }], + } + }, + } + }, + { + apiVersion: 'v1', + kind: 'Service', + metadata: { + name: 'rancher-envoy', + namespace: 'cattle-system', + }, + spec: { + selector: { app: 'rancher-envoy' }, + ports: [{ + port: 443, + targetPort: 9443, + }], + } + }, ] promises.push( vmx.copyFileIn(path.join(paths.resources, 'cert-manager.crds.yaml'), manifestFilename(MANIFEST_CERT_MANAGER_CRDS)), @@ -1299,10 +1345,16 @@ export default class K3sHelper extends events.EventEmitter { return; } + const hostPort = await client.forwardPort('cattle-system', 'rancher-envoy', 9443, 0); + if (!hostPort) { + return; + } + mainEvents.emit('dashboard/port-changed', hostPort); + const timeout = AbortSignal.timeout(10 * 60 * 1_000); while (!timeout.aborted) { try { - const url = `https://localhost/dashboard/?setup=${ RancherPassword }`; + const url = `https://localhost:${ hostPort }/dashboard/?setup=${ RancherPassword }`; const agent = new https.Agent({ rejectUnauthorized: false }); const resp = await fetch(url, { agent }); @@ -1332,7 +1384,6 @@ export default class K3sHelper extends events.EventEmitter { await setSetting('first-login', 'false'); await setSetting('eula-agreed', (new Date).toISOString()); } - } interface V1HelmChart { diff --git a/pkg/rancher-desktop/backend/kube/wsl.ts b/pkg/rancher-desktop/backend/kube/wsl.ts index 08e5973d79c..aab180ec574 100644 --- a/pkg/rancher-desktop/backend/kube/wsl.ts +++ b/pkg/rancher-desktop/backend/kube/wsl.ts @@ -177,7 +177,7 @@ export default class WSLKubernetesBackend extends events.EventEmitter implements 'install-k3s', version.raw, await this.vm.wslify(path.join(paths.cache, 'k3s'))); const promises: Promise[] = []; - promises.push(BackendHelper.configureKubeResources(this.vm, + promises.push(K3sHelper.configureKubeResources(this.vm, config.experimental?.containerEngine?.webAssembly?.enabled && !!config.experimental?.kubernetes?.options?.spinkube)); if (config.experimental?.containerEngine?.webAssembly?.enabled) { @@ -282,6 +282,10 @@ export default class WSLKubernetesBackend extends events.EventEmitter implements 'Skipping node checks, flannel is disabled', 100, Promise.resolve({})); } + await this.progressTracker.action('Finishing Kubernetes Startup', 100, + this.client?.getActivePod('kube-system', 'kube-dns')); + await this.progressTracker.action('Setting up Rancher Dashboard', 100, + K3sHelper.setupRancherManager(this.client)); } async stop() { diff --git a/pkg/rancher-desktop/main/mainEvents.ts b/pkg/rancher-desktop/main/mainEvents.ts index f33c3742729..5d2fa843ed1 100644 --- a/pkg/rancher-desktop/main/mainEvents.ts +++ b/pkg/rancher-desktop/main/mainEvents.ts @@ -113,6 +113,12 @@ interface MainEventNames { */ 'extensions/ui/uninstall'(id: string): void; + /** + * Emitted when the dashboard port has changed. + * @param port The port that the dashboard is now on. + */ + 'dashboard/port-changed'(port: number): void; + /** * Emitted on application quit, used to shut down any integrations. This * requires feedback from the handler to know when all tasks are complete. diff --git a/pkg/rancher-desktop/main/networking/index.ts b/pkg/rancher-desktop/main/networking/index.ts index d636dc0e116..ba96cd8c00f 100644 --- a/pkg/rancher-desktop/main/networking/index.ts +++ b/pkg/rancher-desktop/main/networking/index.ts @@ -14,10 +14,14 @@ import getWinCertificates from './win-ca'; import mainEvents from '@pkg/main/mainEvents'; import Logging from '@pkg/utils/logging'; -import { getWindowName, windowMapping } from '@pkg/window'; +import { getWindowName } from '@pkg/window'; const console = Logging.networking; +let dashboardPort = 0; + +mainEvents.on('dashboard/port-changed', port => dashboardPort = port); + export default async function setupNetworking() { const agentOptions = { ...https.globalAgent.options }; @@ -45,7 +49,7 @@ export default async function setupNetworking() { Electron.app.on('certificate-error', async(event, webContents, url, error, certificate, callback) => { const windowName = getWindowName(webContents); const pluginDevUrls = [`https://localhost:8888`, `wss://localhost:8888`]; - const dashboardUrls = ['https://localhost/', 'wss://localhost/']; + const dashboardUrls = [`https://localhost:${ dashboardPort }/`, `wss://localhost:${ dashboardPort }/`]; if ( windowName === 'main' && diff --git a/pkg/rancher-desktop/preload/dashboard.ts b/pkg/rancher-desktop/preload/dashboard.ts index b7c5707fad5..54235d68b60 100644 --- a/pkg/rancher-desktop/preload/dashboard.ts +++ b/pkg/rancher-desktop/preload/dashboard.ts @@ -1,18 +1,19 @@ import { ipcRenderer } from '@pkg/utils/ipcRenderer'; -export default function initDashboard(): void { - if (!document.location.href.startsWith('https://localhost/dashboard/')) { +export default async function initDashboard(): Promise { + const dashboardPort = await ipcRenderer.invoke('dashboard/get-port'); + if (!document.location.href.startsWith(`https://localhost:${ dashboardPort }/dashboard/`)) { return; } // Navigation API is only available in Chrome-derived browsers like Electron. // https://developer.mozilla.org/en-US/docs/Web/API/Navigation (window as any).navigation.addEventListener('navigate', async function onNavigate() { - const resp = await fetch('https://localhost/v3/users?me=true'); + const resp = await fetch(`https://localhost:${ dashboardPort }/v3/users?me=true`); let loginSuccessful = false; if (resp.status === 401) { const token = await ipcRenderer.invoke('dashboard/get-csrf-token') ?? ''; - const loginURL = 'https://localhost/v3-public/localProviders/local?action=login'; + const loginURL = `https://localhost:${ dashboardPort }/v3-public/localProviders/local?action=login`; const resp = await fetch(loginURL, { headers: { 'Accept': "application/json", diff --git a/pkg/rancher-desktop/preload/index.ts b/pkg/rancher-desktop/preload/index.ts index b1956c67a47..9e79c227e45 100644 --- a/pkg/rancher-desktop/preload/index.ts +++ b/pkg/rancher-desktop/preload/index.ts @@ -3,7 +3,7 @@ import initExtensions from './extensions'; function init() { initExtensions(); - initDashboard(); + initDashboard().catch(ex => console.error(ex)); } try { diff --git a/pkg/rancher-desktop/typings/electron-ipc.d.ts b/pkg/rancher-desktop/typings/electron-ipc.d.ts index 4a3c4c37d73..2cd1d51da33 100644 --- a/pkg/rancher-desktop/typings/electron-ipc.d.ts +++ b/pkg/rancher-desktop/typings/electron-ipc.d.ts @@ -154,6 +154,7 @@ export interface IpcMainInvokeEvents { // #region dashboard 'dashboard/get-csrf-token': () => string | null; + 'dashboard/get-port': () => number; // #endregion } diff --git a/pkg/rancher-desktop/window/dashboard.ts b/pkg/rancher-desktop/window/dashboard.ts index 4c54559e0f3..f68a1ebda0f 100644 --- a/pkg/rancher-desktop/window/dashboard.ts +++ b/pkg/rancher-desktop/window/dashboard.ts @@ -3,12 +3,16 @@ import { createWindow, getWindow } from '.'; import paths from '@pkg/utils/paths'; import { getIpcMainProxy } from '@pkg/main/ipcMain'; import Logging from '@pkg/utils/logging'; +import mainEvents from '@pkg/main/mainEvents'; const dashboardName = 'dashboard'; -const dashboardURL = 'https://localhost/dashboard/c/local/explorer'; const console = Logging.dashboard; const ipcMain = getIpcMainProxy(console); +let dashboardPort = 0; + +mainEvents.on('dashboard/port-changed', port => dashboardPort = port); + ipcMain.removeHandler('dashboard/get-csrf-token'); ipcMain.handle('dashboard/get-csrf-token', async (event) => { const webContents = event.sender; @@ -37,8 +41,11 @@ ipcMain.handle('dashboard/get-csrf-token', async (event) => { }); } }); +ipcMain.removeHandler('dashboard/get-port'); +ipcMain.handle('dashboard/get-port', () => dashboardPort); export function openDashboard() { + const dashboardURL = `https://localhost:${ dashboardPort }/dashboard/c/local/explorer`; const window = createWindow('dashboard', dashboardURL, { title: 'Rancher Dashboard', width: 800, diff --git a/pkg/rancher-desktop/window/index.ts b/pkg/rancher-desktop/window/index.ts index 7dbe0594873..6169b51fa5b 100644 --- a/pkg/rancher-desktop/window/index.ts +++ b/pkg/rancher-desktop/window/index.ts @@ -14,6 +14,7 @@ import paths from '@pkg/utils/paths'; import { CommandOrControl, Shortcuts } from '@pkg/utils/shortcuts'; import { mainRoutes } from '@pkg/window/constants'; import { openPreferences } from '@pkg/window/preferences'; +import mainEvents from '@pkg/main/mainEvents'; const console = Logging[`window_${ process.type || 'unknown' }`]; @@ -60,6 +61,9 @@ export function getWindowName(webContents: Electron.WebContents): string | null return name; } +let dashboardPort = 0; +mainEvents.on('dashboard/port-changed', port => dashboardPort = port); + /** * Open a given window; if it is already open, focus it. * @param name The window identifier; this controls window re-use. @@ -75,7 +79,7 @@ export function createWindow(name: string, url: string, options: Electron.Browse function isInternalURL(url: string) { if (name === 'dashboard') { - return url.startsWith('https://localhost/'); + return url.startsWith(`https://localhost:${ dashboardPort }/`); } return url.startsWith(`${ webRoot }/`) || url.startsWith('x-rd-extension://'); } From 39fd2bf59fc8e8796aebb62884503c1cde7e6bc0 Mon Sep 17 00:00:00 2001 From: Mark Yen Date: Tue, 20 Aug 2024 18:04:22 -0700 Subject: [PATCH 7/9] Dashboard: Fix websockets We need to override the origin when talking to upstream, as the header normally contains the downstream port and gets rejected. Also add a timeout to dashboard setup so we can retry instead of hanging forever if it doesn't work. --- pkg/rancher-desktop/assets/scripts/rancher-manager-envoy.yaml | 4 ++++ pkg/rancher-desktop/backend/k3sHelper.ts | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pkg/rancher-desktop/assets/scripts/rancher-manager-envoy.yaml b/pkg/rancher-desktop/assets/scripts/rancher-manager-envoy.yaml index 1bad6ce29e2..ab38ad6210f 100644 --- a/pkg/rancher-desktop/assets/scripts/rancher-manager-envoy.yaml +++ b/pkg/rancher-desktop/assets/scripts/rancher-manager-envoy.yaml @@ -14,6 +14,8 @@ static_resources: typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager stat_prefix: rancher_manager + upgrade_configs: + - upgrade_type: websocket route_config: name: route virtual_hosts: @@ -32,6 +34,8 @@ static_resources: append_action: OVERWRITE_IF_EXISTS_OR_ADD - header: { key: X-Forwarded-For, value: '192.0.2.1' } append_action: OVERWRITE_IF_EXISTS_OR_ADD + - header: { key: Origin, value: 'https://localhost' } + append_action: OVERWRITE_IF_EXISTS_OR_ADD http_filters: - name: envoy.filters.http.router typed_config: diff --git a/pkg/rancher-desktop/backend/k3sHelper.ts b/pkg/rancher-desktop/backend/k3sHelper.ts index 5ef3757116d..1f437ab24d8 100644 --- a/pkg/rancher-desktop/backend/k3sHelper.ts +++ b/pkg/rancher-desktop/backend/k3sHelper.ts @@ -1356,7 +1356,7 @@ export default class K3sHelper extends events.EventEmitter { try { const url = `https://localhost:${ hostPort }/dashboard/?setup=${ RancherPassword }`; const agent = new https.Agent({ rejectUnauthorized: false }); - const resp = await fetch(url, { agent }); + const resp = await fetch(url, { agent, timeout: 5_000 }); console.log(`${ url } => ${ resp.status }: ${ resp.statusText }`); console.log(await resp.text()); @@ -1364,7 +1364,7 @@ export default class K3sHelper extends events.EventEmitter { break; } } catch (ex) { - console.log(ex); + console.log(`Retrying dashboard setup due to error: ${ ex }`); } await timers.setTimeout(10_000); } From 39fabbdac1ec69555f8445277bfdd03a0bdd1c26 Mon Sep 17 00:00:00 2001 From: Mark Yen Date: Tue, 20 Aug 2024 18:05:57 -0700 Subject: [PATCH 8/9] Dashboard: hide kubeconfig buttons They generate kubeconfig files that have no port, which does not work as we require port forwarding to reach the server. To avoid confusing users, hide the buttons. --- pkg/rancher-desktop/preload/dashboard.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/rancher-desktop/preload/dashboard.ts b/pkg/rancher-desktop/preload/dashboard.ts index 54235d68b60..be87481a101 100644 --- a/pkg/rancher-desktop/preload/dashboard.ts +++ b/pkg/rancher-desktop/preload/dashboard.ts @@ -53,6 +53,13 @@ export default async function initDashboard(): Promise { display: none; } `); + // Hide the download kubeconfig button; the config has the wrong port. + stylesheet.insertRule(` + .header-buttons button[data-testid="btn-download-kubeconfig"], + .header-buttons button[data-testid="btn-copy-kubeconfig"]{ + display: none; + } + `); document.adoptedStyleSheets.push(stylesheet); }); } From 26db38d22150948e5ba054740edd55315ec19ecc Mon Sep 17 00:00:00 2001 From: Mark Yen Date: Tue, 20 Aug 2024 18:06:58 -0700 Subject: [PATCH 9/9] Kube client: fix port forwarding - When we create an error, we need to throw in, not drop it on the floor. - If the host port is not specified, and an existing server is found, return that server's port instead of 0. --- pkg/rancher-desktop/backend/kube/client.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/rancher-desktop/backend/kube/client.ts b/pkg/rancher-desktop/backend/kube/client.ts index 1addad15613..d52d3ce3a1a 100644 --- a/pkg/rancher-desktop/backend/kube/client.ts +++ b/pkg/rancher-desktop/backend/kube/client.ts @@ -472,7 +472,7 @@ export class KubeClient extends events.EventEmitter { // check if server is still valid if (!this.servers.has(namespace, endpoint, k8sPort)) { - new Error('Server is no longer valid'); + throw new Error('Server is no longer valid'); } // forward the port @@ -525,15 +525,15 @@ export class KubeClient extends events.EventEmitter { let server = this.servers.get(namespace, endpoint, k8sPort); if (server) { - console.log(`Found existing server for ${ targetName }.`); + console.debug(`Found existing server for ${ targetName }.`); const currentHostPort = (server.address() as net.AddressInfo).port; - if (currentHostPort === hostPort) { - console.log(`Server listening on ${ hostPort }, which is what we want.`); + if (hostPort === 0 || currentHostPort === hostPort) { + console.debug(`Server listening on ${ hostPort }, which is what we want.`); - return hostPort; + return hostPort || currentHostPort; } else { - console.log(`Server listening on ${ currentHostPort }, but we want ${ hostPort }. Closing it.`); + console.debug(`Server listening on ${ currentHostPort }, but we want ${ hostPort }. Closing it.`); await this.closeServerAndConns(namespace, endpoint, k8sPort); } }