From 839e1aea651cf2a24c4334993bf7f0dbcb0b8a8e Mon Sep 17 00:00:00 2001 From: Mark Yen Date: Mon, 19 Aug 2024 11:42:48 -0700 Subject: [PATCH] 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://'); }