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/background.ts b/background.ts index 091bad2e2fa..fba9c03eb4a 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,8 +51,8 @@ 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'; +import { closeDashboard, openDashboard } from '@pkg/window/dashboard'; Electron.app.setPath('cache', paths.cache); Electron.app.setAppLogsPath(paths.logs); @@ -205,8 +203,6 @@ Electron.app.whenReady().then(async() => { // Check for required OS versions and features await checkPrerequisites(); - DashboardServer.getInstance().init(); - await setupNetworking(); try { @@ -1240,15 +1236,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..102762b9c1c 100644 --- a/pkg/rancher-desktop/assets/dependencies.yaml +++ b/pkg/rancher-desktop/assets/dependencies.yaml @@ -11,8 +11,7 @@ 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 +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/assets/scripts/rancher-manager-envoy.yaml b/pkg/rancher-desktop/assets/scripts/rancher-manager-envoy.yaml new file mode 100644 index 00000000000..ab38ad6210f --- /dev/null +++ b/pkg/rancher-desktop/assets/scripts/rancher-manager-envoy.yaml @@ -0,0 +1,124 @@ +# 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 + upgrade_configs: + - upgrade_type: websocket + 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 + - header: { key: Origin, value: 'https://localhost' } + 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/backendHelper.ts b/pkg/rancher-desktop/backend/backendHelper.ts index b4f1aeb1329..e130a3a2626 100644 --- a/pkg/rancher-desktop/backend/backendHelper.ts +++ b/pkg/rancher-desktop/backend/backendHelper.ts @@ -5,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'; @@ -27,15 +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'; - -const STATIC_DIR = '/var/lib/rancher/k3s/server/static/rancher-desktop'; -const STATIC_CERT_MANAGER_CHART = `${ STATIC_DIR }/cert-manager.tgz`; -const STATIC_SPIN_OPERATOR_CHART = `${ STATIC_DIR }/spin-operator.tgz`; +const MANIFEST_RUNTIMES = 'z100-runtimes'; const console = Logging.kube; @@ -286,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. */ @@ -311,25 +297,12 @@ 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); } } - /** - * Write k3s manifests to install cert-manager and spinkube operator - */ - static async configureSpinOperator(vmx: VMExecutor) { - await Promise.all([ - 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), - ]); - } - /** * Install containerd-wasm shims into /usr/local/containerd-shims (and symlinks into /usr/local/bin). */ diff --git a/pkg/rancher-desktop/backend/k3sHelper.ts b/pkg/rancher-desktop/backend/k3sHelper.ts index 97ed70c378e..1f437ab24d8 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,9 @@ 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 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'; @@ -35,9 +40,25 @@ 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'; - +import mainEvents from '@pkg/main/mainEvents'; + +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'; +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`; +const STATIC_RANCHER_CHART = `${ STATIC_DIR }/rancher-manager.tgz` +const STATIC_SPIN_OPERATOR_CHART = `${ STATIC_DIR }/spin-operator.tgz`; const console = Logging.k8s; /** @@ -131,7 +152,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) { @@ -905,7 +926,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 +949,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 +990,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 }); @@ -1201,6 +1221,169 @@ 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, + }, + ingress: { + enabled: false, + }, + extraEnv: [ + { name: 'CATTLE_FEATURES', + value: [ + 'continuous-delivery=false', + 'fleet=false', + 'harvester=false', + 'multi-cluster-management=false', + 'rke1-ui=false', + 'rke2=false', + 'uiextension=false', + ].join(',') }, + ] + }), + }, + }, + { + 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)), + 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(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); + } + + 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 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:${ hostPort }/dashboard/?setup=${ RancherPassword }`; + const agent = new https.Agent({ rejectUnauthorized: false }); + const resp = await fetch(url, { agent, timeout: 5_000 }); + + console.log(`${ url } => ${ resp.status }: ${ resp.statusText }`); + console.log(await resp.text()); + if (resp.ok) { + break; + } + } catch (ex) { + console.log(`Retrying dashboard setup due to error: ${ 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/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); } } diff --git a/pkg/rancher-desktop/backend/kube/lima.ts b/pkg/rancher-desktop/backend/kube/lima.ts index 719e721aa3d..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'; @@ -132,11 +132,11 @@ export default class LimaKubernetesBackend extends events.EventEmitter implement const promises: Promise[] = []; promises.push(this.writeServiceScript(config, desiredVersion, allowSudo)); + promises.push(K3sHelper.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); }); @@ -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), ])); } @@ -253,6 +252,10 @@ export default class LimaKubernetesBackend extends events.EventEmitter implement await new Promise(resolve => setTimeout(resolve, 5000)); }); } + 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/kube/wsl.ts b/pkg/rancher-desktop/backend/kube/wsl.ts index 0aee4da4d45..aab180ec574 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'; @@ -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(K3sHelper.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 { @@ -264,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), ])); } @@ -284,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/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/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/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 a34ccebdcb5..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 { 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 }; @@ -43,20 +47,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:${ dashboardPort }/`, `wss://localhost:${ dashboardPort }/`]; if ( + windowName === 'main' && process.env.NODE_ENV === 'development' && process.env.RD_ENV_PLUGINS_DEV && pluginDevUrls.some(x => url.startsWith(x)) @@ -68,7 +64,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..be87481a101 --- /dev/null +++ b/pkg/rancher-desktop/preload/dashboard.ts @@ -0,0 +1,65 @@ +import { ipcRenderer } from '@pkg/utils/ipcRenderer'; + +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:${ 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:${ dashboardPort }/v3-public/localProviders/local?action=login`; + const resp = await fetch(loginURL, { + 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" + }); + loginSuccessful = resp.ok; + } + + 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('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; + } + `); + // 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); + }); +} 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..9e79c227e45 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().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 766b5e3f2b2..2cd1d51da33 100644 --- a/pkg/rancher-desktop/typings/electron-ipc.d.ts +++ b/pkg/rancher-desktop/typings/electron-ipc.d.ts @@ -151,6 +151,11 @@ 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; + 'dashboard/get-port': () => number; + // #endregion } /** 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. 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/pkg/rancher-desktop/window/dashboard.ts b/pkg/rancher-desktop/window/dashboard.ts index bdc0d6dcf5e..f68a1ebda0f 100644 --- a/pkg/rancher-desktop/window/dashboard.ts +++ b/pkg/rancher-desktop/window/dashboard.ts @@ -1,34 +1,69 @@ -import { BrowserWindow } from 'electron'; +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'; +import mainEvents from '@pkg/main/mainEvents'; -import { windowMapping, restoreWindow } from '.'; +const dashboardName = 'dashboard'; +const console = Logging.dashboard; +const ipcMain = getIpcMainProxy(console); -const dashboardURL = 'http://127.0.0.1:6120/c/local/explorer'; +let dashboardPort = 0; -const getDashboardWindow = () => ('dashboard' in windowMapping) ? BrowserWindow.fromId(windowMapping['dashboard']) : null; +mainEvents.on('dashboard/port-changed', port => dashboardPort = port); -export function openDashboard() { - let window = getDashboardWindow(); +ipcMain.removeHandler('dashboard/get-csrf-token'); +ipcMain.handle('dashboard/get-csrf-token', async (event) => { + const webContents = event.sender; + 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; + } - if (restoreWindow(window)) { - return window; + // 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); + }); } +}); +ipcMain.removeHandler('dashboard/get-port'); +ipcMain.handle('dashboard/get-port', () => dashboardPort); - window = new BrowserWindow({ - title: 'Rancher Dashboard', - width: 800, +export function openDashboard() { + const dashboardURL = `https://localhost:${ dashboardPort }/dashboard/c/local/explorer`; + 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..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' }`]; @@ -52,6 +53,17 @@ 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; +} + +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. @@ -65,9 +77,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:${ dashboardPort }/`); + } return url.startsWith(`${ webRoot }/`) || url.startsWith('x-rd-extension://'); - }; + } window = new BrowserWindow(options); window.webContents.on('console-message', (event, level, message, line, sourceId) => { diff --git a/scripts/dependencies/tools.ts b/scripts/dependencies/tools.ts index b6106995246..4d201ab4172 100644 --- a/scripts/dependencies/tools.ts +++ b/scripts/dependencies/tools.ts @@ -6,12 +6,12 @@ import semver from 'semver'; import { download, downloadZip, downloadTarGZ, getResource, DownloadOptions, ArchiveDownloadOptions, + locateHelmChart, } from '../lib/download'; 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,129 +329,37 @@ export class Trivy implements Dependency, GitHubDependency { } } -export class Steve implements Dependency, GitHubDependency { - name = 'steve'; - githubOwner = 'rancher-sandbox'; - githubRepo = 'rancher-desktop-steve'; +export class RancherManager implements Dependency, GitHubDependency { + name = 'rancher'; + githubOwner = 'rancher'; + githubRepo = 'rancher'; 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); + await this.downloadChart(context); } -} - -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'], - }); + 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 fs.promises.rm(destPath, { recursive: true, maxRetries: 10 }); + await download(options.url.toString(), destPath, options); } - 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.')); + getAvailableVersions(includePrerelease = false): Promise { + return getPublishedVersions(this.githubOwner, this.githubRepo, includePrerelease); } 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 }`; + return `v${ version }`; } rcompareVersions(version1: string, version2: string): -1 | 0 | 1 { - const semver1 = this.versionToSemver(version1); - const semver2 = this.versionToSemver(version2); - - return semver.rcompare(semver1, semver2); + return semver.rcompare(version1, version2); } + } export class DockerProvidedCredHelpers implements Dependency, GitHubDependency { diff --git a/scripts/lib/dependencies.ts b/scripts/lib/dependencies.ts index c5c43d7c9df..8180d33e590 100644 --- a/scripts/lib/dependencies.ts +++ b/scripts/lib/dependencies.ts @@ -43,9 +43,8 @@ export type DependencyVersions = { dockerCompose: string; 'golangci-lint': string; trivy: string; - steve: string; guestAgent: string; - rancherDashboard: 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 95a6c3fd107..f03eb3199f1 100644 --- a/scripts/postinstall.ts +++ b/scripts/postinstall.ts @@ -84,9 +84,8 @@ const vmDependencies = [ // Dependencies that are specific to hosts. const hostDependencies = [ - new tools.Steve(), - new tools.RancherDashboard(), new MobyOpenAPISpec(), + new tools.RancherManager(), ]; async function downloadDependencies(items: DependencyWithContext[]): Promise { 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(),