diff --git a/.github/workflows/test-microk8s-ubuntu.yml b/.github/workflows/test-microk8s-ubuntu.yml new file mode 100644 index 00000000..2027f850 --- /dev/null +++ b/.github/workflows/test-microk8s-ubuntu.yml @@ -0,0 +1,52 @@ +name: test-microk8s-ubuntu + +on: + pull_request: + branches: [master] + push: + branches: [master] + +jobs: + test-linux: + runs-on: ubuntu-latest + + steps: + - name: Check out Git repository + uses: actions/checkout@v3 + + - name: Get all script files that have changed + id: changed-files + uses: tj-actions/changed-files@v37 + with: + files: | + assets/scripts/**.sh + .github/workflows/test-microk8s-ubuntu.yml + files_ignore: assets/scripts/configure-minikube-linux.sh + since_last_remote_commit: true + + - name: Configure MicroK8s Cluster + if: ${{ steps.changed-files.outputs.any_modified == 'true' }} + run: | + ASSETS_PATH="$GITHUB_WORKSPACE/assets" + CONFIGS_PATH="/home/runner/opt/config" + FORCE_DB_REFRESH=true + ENGINE_PATH="/home/runner/opt/etherealengine" + CLUSTER_ID=test + OPS_PATH="/home/runner/opt/ethereal-engine-ops" + PASSWORD= # Github actions run in password-less sudo mode. + ENABLE_RIPPLE_STACK=false + + mkdir -p "$CONFIGS_PATH" + curl https://raw.githubusercontent.com/EtherealEngine/ethereal-engine-ops/master/configs/local.microk8s.template.values.yaml -o "$CONFIGS_PATH/test-engine.values.yaml" + sed -i "s,^\([[:space:]]*hostUploadFolder:\).*,\1 '/home/runner/opt/etherealengine/packages/server/upload'," "$CONFIGS_PATH/test-engine.values.yaml" + + bash "$ASSETS_PATH/scripts/configure-microk8s-linux.sh" \ + -a "$ASSETS_PATH" \ + -c "$CONFIGS_PATH" \ + -d "$FORCE_DB_REFRESH" \ + -f "$ENGINE_PATH" \ + -i "$CLUSTER_ID" \ + -o "$OPS_PATH" \ + -p "$PASSWORD" \ + -r "$ENABLE_RIPPLE_STACK" + shell: bash diff --git a/README.md b/README.md index 066f4796..f11298a5 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ https://user-images.githubusercontent.com/10975502/168554732-86a202b6-053c-4588- ## Getting Started -You can find complete getting started guide [here](https://etherealengine.github.io/etherealengine-docs/docs/tutorials/ethereal_control_center/getting_started). +You can find complete getting started guide [here](https://etherealengine.github.io/etherealengine-docs/docs/host/devops_deployment/tutorials/ethereal_control_center/getting_started/). Additionally, there are Control Center video tutorials which you can find [here](./TUTORIALS.md). Although some of them might be outdated. @@ -94,7 +94,16 @@ cd "C:\Users\%USERNAME%\Downloads" "Docker Desktop Installer.exe" install --accept-license --installation-dir=C:\Docker ``` -### 4. Reporting an Issue +### 4. Git status unavailable on Windows + +On Windows, if you are unable to see Git status after cluster configuration and getting following error in logs: + +```bash +git configs - "fatal: detected dubious ownership in repository at \nTo add an exception for this directory, call:\n\n\tgit config --global --add safe.directory \n" +``` +This is due to the app not having permissions to read the Git config file. To fix this, run Ethereal Engine Control Center as administrator. + +### 5. Reporting an Issue If you face an issue please report it to [Issues](https://github.com/canonical/microk8s/issues) or reach out to us on [Discord](https://discord.gg/xrf). Also please share following log files: diff --git a/assets/scripts/check-agones-redis.sh b/assets/scripts/check-agones-redis.sh index 0290738c..dfbd1b71 100644 --- a/assets/scripts/check-agones-redis.sh +++ b/assets/scripts/check-agones-redis.sh @@ -17,7 +17,7 @@ if helm status agones >/dev/null; then else echo "agones is not deployed" - helm install -f "$OPS_FOLDER/configs/agones-default-values.yaml" agones agones/agones --version "1.31.0" + helm install -f "$OPS_FOLDER/configs/agones-default-values.yaml" agones agones/agones sleep 20 fi diff --git a/assets/scripts/check-engine-deployment.sh b/assets/scripts/check-engine-deployment.sh index b7f36abd..b55c416f 100644 --- a/assets/scripts/check-engine-deployment.sh +++ b/assets/scripts/check-engine-deployment.sh @@ -20,6 +20,8 @@ TAG=$7 cd "$ENGINE_FOLDER" || exit +RE_INIT=false + export MYSQL_HOST=localhost export MYSQL_PORT=3304 DB_STATUS=$(npx cross-env ts-node --swc scripts/check-db-exists-only.ts) @@ -38,11 +40,19 @@ if [[ -d $PROJECTS_PATH ]]; then else echo "ethereal engine projects does not exists at $PROJECTS_PATH" + RE_INIT=true +fi + +if [[ $RE_INIT == true || $FORCE_DB_REFRESH == 'true' ]]; then export MYSQL_HOST=localhost - export MYSQL_PORT=3306 + export MYSQL_PORT=3305 + + # Resetting test db and doing dev reinit to ensure project folders are seeded. + docker container stop etherealengine_test_db + docker container rm etherealengine_test_db + docker container prune --force npm run dev-docker npm run dev-reinit - npx ts-node --swc scripts/install-projects.js fi export MYSQL_HOST=localhost @@ -134,11 +144,11 @@ if [[ $ENGINE_INSTALLED == true ]] && [[ $DB_EXISTS == false || $FORCE_DB_REFRES echo "Waiting for API pod to be ready. API ready count: $apiCount" done - helm upgrade --reuse-values -f "$OPS_FOLDER/configs/db-refresh-false.values.yaml" --set taskserver.image.tag="$TAG",api.image.tag="$TAG",instanceserver.image.tag="$TAG",testbot.image.tag="$TAG",client.image.tag="$TAG",testbot.image.tag="$TAG" local etherealengine/etherealengine + helm upgrade --reuse-values -f "$CONFIGS_FOLDER/$CLUSTER_ID-engine.values.yaml" -f "$OPS_FOLDER/configs/db-refresh-false.values.yaml" --set taskserver.image.tag="$TAG",api.image.tag="$TAG",instanceserver.image.tag="$TAG",testbot.image.tag="$TAG",client.image.tag="$TAG",testbot.image.tag="$TAG" local etherealengine/etherealengine elif [[ $ENGINE_INSTALLED == true ]] && [[ $DB_EXISTS == true ]]; then echo "Updating Ethereal Engine deployment without populating database" - helm upgrade --reuse-values --set taskserver.image.tag="$TAG",api.image.tag="$TAG",instanceserver.image.tag="$TAG",testbot.image.tag="$TAG",client.image.tag="$TAG",testbot.image.tag="$TAG" local etherealengine/etherealengine + helm upgrade --reuse-values -f "$CONFIGS_FOLDER/$CLUSTER_ID-engine.values.yaml" --set taskserver.image.tag="$TAG",api.image.tag="$TAG",instanceserver.image.tag="$TAG",testbot.image.tag="$TAG",client.image.tag="$TAG",testbot.image.tag="$TAG" local etherealengine/etherealengine elif [[ $ENGINE_INSTALLED == false ]] && [[ $DB_EXISTS == false || $FORCE_DB_REFRESH == 'true' ]]; then echo "Installing Ethereal Engine deployment with populating database" @@ -160,7 +170,7 @@ elif [[ $ENGINE_INSTALLED == false ]] && [[ $DB_EXISTS == false || $FORCE_DB_REF fi echo "Waiting for API pod to be ready. API ready count: $apiCount" done - + sleep 5 helm upgrade --reuse-values -f "$OPS_FOLDER/configs/db-refresh-false.values.yaml" --set taskserver.image.tag="$TAG",api.image.tag="$TAG",instanceserver.image.tag="$TAG",testbot.image.tag="$TAG",client.image.tag="$TAG",testbot.image.tag="$TAG" local etherealengine/etherealengine diff --git a/assets/scripts/check-git-windows.ps1 b/assets/scripts/check-git-windows.ps1 new file mode 100644 index 00000000..46d1af42 --- /dev/null +++ b/assets/scripts/check-git-windows.ps1 @@ -0,0 +1,49 @@ + +#========== +# Ref: https://stackoverflow.com/a/73285729 +#========== + +#======================== +# Verify Git installation +#======================== + +$IS_GIT_INSTALLED = $false; + +$gitVersion = Invoke-Expression "& git --version" + +if ($gitVersion) { + Write-Host "git for windows is installed"; + $IS_GIT_INSTALLED = $true; +} +else { + Write-Host "git for windows is not installed"; + $IS_GIT_INSTALLED = $false; +} + +#============ +# Install Git +#============ + +if ($IS_GIT_INSTALLED -eq $false) { + $exePath = "$env:TEMP\git.exe" + + # Reference: https://copdips.com/2019/12/Using-Powershell-to-retrieve-latest-package-url-from-github-releases.html + $url = 'https://github.com/git-for-windows/git/releases/latest' + $request = [System.Net.WebRequest]::Create($url) + $response = $request.GetResponse() + $realTagUrl = $response.ResponseUri.OriginalString + $version = $realTagUrl.split('/')[-1].Trim('v') + $downloadUrl = $realTagUrl.Replace('tag', 'download') + '/Git-' + $version.Replace('.windows', '') + '-64-bit.exe' + + # Download git installer + Write-Host "downloading git for windows using url: $downloadUrl" + Invoke-WebRequest -Uri $downloadUrl -UseBasicParsing -OutFile $exePath + + Start-Process powershell -PassThru -WindowStyle hidden -Wait -verb runas -ArgumentList " -file $PSScriptRoot\setup-git-windows.ps1 '$exePath'" + + $gitVersion = Invoke-Expression "& git --version" +} + +Write-Host "git for windows version is $gitVersion"; + +exit 0; \ No newline at end of file diff --git a/assets/scripts/check-mok.sh b/assets/scripts/check-mok.sh new file mode 100644 index 00000000..420b02de --- /dev/null +++ b/assets/scripts/check-mok.sh @@ -0,0 +1,50 @@ +#!/bin/bash + +set -e + +#=========== +# Parameters +#=========== + +while getopts o:p: flag; do + case "${flag}" in + p) PASSWORD=${OPTARG} ;; + *) + echo "Invalid argument passed" >&2 + exit 1 + ;; + esac +done + +if [[ -z $PASSWORD ]]; then + echo "Missing arguments" + exit 1 +fi + +#=========== +# Verify MOK +#=========== + +if mokutil --version 2>/dev/null | grep -q 'command not found'; then + echo "mokutil is not installed" + echo $(mokutil --version) + + echo "$PASSWORD" | sudo -S apt-get update -y + echo "$PASSWORD" | sudo -S apt-get install -y mokutil +else + echo "mokutil is installed" +fi + +if echo "$PASSWORD" | sudo -S mokutil --sb-state | grep -q 'SecureBoot enabled'; then + echo "Secureboot is enabled" + if echo "$PASSWORD" | sudo -S mokutil --list-enrolled | grep -q 'Secure Boot Module Signature key'; then + echo "mok is enrolled" + exit 0 + else + echo "mok is not enrolled" + #Exit code 2 indicates permission is needed + exit 2 + fi +else + echo "SecureBoot is disabled" +fi \ No newline at end of file diff --git a/assets/scripts/configure-microk8s-linux.sh b/assets/scripts/configure-microk8s-linux.sh index bc814acd..6f256ce7 100755 --- a/assets/scripts/configure-microk8s-linux.sh +++ b/assets/scripts/configure-microk8s-linux.sh @@ -32,7 +32,8 @@ while getopts a:c:d:f:i:o:p:r: flag; do esac done -if [[ -z $ASSETS_FOLDER || -z $CONFIGS_FOLDER || -z $FORCE_DB_REFRESH || -z $ENGINE_FOLDER || -z $CLUSTER_ID || -z $OPS_FOLDER || -z $PASSWORD || -z $ENABLE_RIPPLE_STACK ]]; then +if [[ -z $ASSETS_FOLDER || -z $CONFIGS_FOLDER || -z $FORCE_DB_REFRESH || -z $ENGINE_FOLDER || -z $CLUSTER_ID || -z $OPS_FOLDER || -z $ENABLE_RIPPLE_STACK ]]; then + # Allow empty passwords echo "Missing arguments" exit 1 fi diff --git a/assets/scripts/configure-microk8s-windows.ps1 b/assets/scripts/configure-microk8s-windows.ps1 index 7b0c70ff..ba8b0555 100644 --- a/assets/scripts/configure-microk8s-windows.ps1 +++ b/assets/scripts/configure-microk8s-windows.ps1 @@ -15,6 +15,7 @@ function checkExitCode() { exit $LastExitCode; } } + function cleanseString($inputSt) { $finalString = '' $inputString = $inputSt -join "`n" | Out-String @@ -86,7 +87,7 @@ $wslStatus = cleanseString(wsl --status); Write-Host "WSL Status: `n$wslStatus"; if ([string]::IsNullOrEmpty($wslStatus) -or $wslStatus -notlike '*: Ubuntu*') { - throw "Make sure WSL is installed and Ubuntu is selected as default distribution.`nhttps://etherealengine.github.io/etherealengine-docs/docs/devops_deployment/microk8s_windows/#install-windows-subsystem-for-linux-wsl"; + throw "Make sure WSL is installed and Ubuntu is selected as default distribution.`nhttps://etherealengine.github.io/etherealengine-docs/docs/host/devops_deployment/microk8s_windows/#install-windows-subsystem-for-linux-wsl"; exit 1; } @@ -96,7 +97,7 @@ $wslDockerVersion = cleanseString(wsl docker version); Write-Host "WSL Docker version: `n$wslDockerVersion"; if ($dockerVersion -notlike '*Server: Docker Desktop*' -or $wslDockerVersion -notlike '*Server: Docker Desktop*') { - throw "Make sure Docker Desktop is installed and Ubuntu WSL Integration is enabled.`nhttps://etherealengine.github.io/etherealengine-docs/docs/devops_deployment/microk8s_windows/#install-docker-desktop"; + throw "Make sure Docker Desktop is installed and Ubuntu WSL Integration is enabled.`nhttps://etherealengine.github.io/etherealengine-docs/docs/host/devops_deployment/microk8s_windows/#install-docker-desktop"; exit 1; } @@ -111,6 +112,12 @@ else { exit 1; } +#======================= +# Verify Git for Windows +#======================= + +& "$PSScriptRoot\check-git-windows.ps1"; + #========== # WSL Login #========== @@ -317,6 +324,38 @@ wsl bash -ic "`"$SCRIPTS_FOLDER/check-engine-repo.sh`" `"$ENGINE_FOLDER`" `"$OPS checkExitCode; +#========================= +# Ensure directory is safe +#========================= + +Write-Host "Checking if repositories are marked as safe directories" + +$distro = cleanseString(wsl bash -ic 'echo $WSL_DISTRO_NAME'); +$distro = $distro.ToString().Trim(); +$isEngineSafe = $true +$isOpsSafe = $true + +$engineStatusCommand = 'git -C "\\wsl.localhost\$distro$ENGINE_FOLDER" status'; +$opsStatusCommand = 'git -C "\\wsl.localhost\$distro$OPS_FOLDER" status'; + +$engineOutput = Invoke-Expression "& $engineStatusCommand 2>&1"; +$opsOutput = Invoke-Expression "& $opsStatusCommand 2>&1"; + +if ([String]$engineOutput -match "dubious ownership") { + $isEngineSafe = $false +} + +if ([String]$opsOutput -match "dubious ownership") { + $isOpsSafe = $false +} + +if (($isEngineSafe -eq $false) -or ($isOpsSafe -eq $false)) { + Write-Host "Marking repositories as safe directories" + Start-Process powershell -PassThru -Wait -verb runas -ArgumentList "-file $PSScriptRoot\set-git-safe-directory.ps1 -e '$ENGINE_FOLDER' -o '$OPS_FOLDER' -d '$distro' -es $isEngineSafe -os $isOpsSafe" +} + +Write-Host "Repositories are marked as safe directories" + #============================ # Ensure DB and Redis Running #============================ diff --git a/assets/scripts/configure-minikube-linux.sh b/assets/scripts/configure-minikube-linux.sh index b445ccf5..0c7d542f 100755 --- a/assets/scripts/configure-minikube-linux.sh +++ b/assets/scripts/configure-minikube-linux.sh @@ -166,6 +166,19 @@ bash "$SCRIPTS_FOLDER/check-mysql.sh" "$PASSWORD" "$ENGINE_FOLDER" checkExitCode +#======================= +# Verify VirtualBox dkms +#======================= + +if virtualbox-dkms --version >/dev/null; then + echo "virtualbox-dkms is installed" +else + echo "virtualbox-dkms is not installed" + + echo "$PASSWORD" | sudo -S apt update -y + echo "$PASSWORD" | sudo -S sudo apt-get install -y virtualbox-dkms +fi + #================== # Verify VirtualBox #================== diff --git a/assets/scripts/set-git-safe-directory.ps1 b/assets/scripts/set-git-safe-directory.ps1 new file mode 100644 index 00000000..dd8fa3e2 --- /dev/null +++ b/assets/scripts/set-git-safe-directory.ps1 @@ -0,0 +1,51 @@ + +#========================= +# Set directory as safe +#========================= + +$wslPath = '///wsl$/'; +$wslLocalPath = '///wsl.localhost/'; + +for ( $i = 0; $i -lt $args.count; $i += 2 ) { + if ($args[$i] -eq "-e") { + $ENGINE_FOLDER = $args[$i + 1].Trim("'") + } + elseif ($args[$i] -eq "-o") { + $OPS_FOLDER = $args[$i + 1].Trim("'") + } + elseif ($args[$i] -eq "-d") { + $DISTRO = $args[$i + 1].Trim("'") + } + elseif ($args[$i] -eq "-es") { + $IS_ENGINE_SAFE = $args[$i + 1] + } + elseif ($args[$i] -eq "-os") { + $IS_OPS_SAFE = $args[$i + 1] + } + else { + throw "Invalid argument passed" + exit 1 + } +} + +if ($IS_ENGINE_SAFE -eq $false) { + $localEnginePathCommand = "git config --global --add safe.directory '%(prefix)$wslPath$DISTRO$ENGINE_FOLDER'"; + $localhostEnginePathCommand = "git config --global --add safe.directory '%(prefix)$wslLocalPath$DISTRO$ENGINE_FOLDER'"; + + Write-Host "Running git command: $localhostEnginePathCommand"; + Invoke-Expression "& $localhostEnginePathCommand"; + Write-Host "Running git command: $localEnginePathCommand"; + Invoke-Expression "& $localEnginePathCommand"; +} + +if ($IS_OPS_SAFE -eq $false) { + $localOpsPathCommand = "git config --global --add safe.directory '%(prefix)$wslPath$DISTRO$OPS_FOLDER'"; + $localhostOpsPathCommand = "git config --global --add safe.directory '%(prefix)$wslLocalPath$DISTRO$OPS_FOLDER'"; + + Write-Host "Running git command: $localhostOpsPathCommand"; + Invoke-Expression "& $localhostOpsPathCommand"; + Write-Host "Running git command: $localOpsPathCommand"; + Invoke-Expression "& $localOpsPathCommand"; +} + +exit 0; \ No newline at end of file diff --git a/assets/scripts/setup-git-windows.ps1 b/assets/scripts/setup-git-windows.ps1 new file mode 100644 index 00000000..78f17dbe --- /dev/null +++ b/assets/scripts/setup-git-windows.ps1 @@ -0,0 +1,27 @@ + +#========== +# Setup Git +#========== + +$exePath = $args[0].Trim("'") + +Write-Host "installing git for windows"; + +# Execute git installer +Start-Process $exePath -ArgumentList '/VERYSILENT /NORESTART /NOCANCEL /SP- /CLOSEAPPLICATIONS /RESTARTAPPLICATIONS /COMPONENTS="icons,ext\reg\shellhere,assoc,assoc_sh"' -Wait + +# Optional: For bash.exe, add '$env:PROGRAMFILES\Git\bin' to PATH +[Environment]::SetEnvironmentVariable('Path', "$([Environment]::GetEnvironmentVariable('Path', 'Machine'));$env:PROGRAMFILES\Git\bin", 'Machine') + +# Make new environment variables available in the current PowerShell session: +foreach ($level in "Machine", "User") { + [Environment]::GetEnvironmentVariables($level).GetEnumerator() | ForEach-Object { + # For Path variables, append the new values, if they're not already in there + if ($_.Name -match 'Path$') { + $_.Value = ($((Get-Content "Env:$($_.Name)") + ";$($_.Value)") -split ';' | Select-Object -unique) -join ';' + } + $_ + } | Set-Content -Path { "Env:$($_.Name)" } +} + +exit 0; \ No newline at end of file diff --git a/release/app/package.json b/release/app/package.json index 9bc3b37c..14376e70 100644 --- a/release/app/package.json +++ b/release/app/package.json @@ -1,6 +1,6 @@ { "name": "etherealengine-control-center", - "version": "0.3.3", + "version": "0.3.8", "description": "A desktop app for managing Ethereal Engine cluster.", "license": "MIT", "author": { diff --git a/src/constants/Channels.ts b/src/constants/Channels.ts index eb53678d..1e4c0e6c 100644 --- a/src/constants/Channels.ts +++ b/src/constants/Channels.ts @@ -29,6 +29,7 @@ const Channels = { }, Engine: { StartFileServer: 'StartFileServer', + StopFileServer: 'StopFileServer', EnsureAdminAccess: 'EnsureAdminAccess', EnsureAdminAccessError: 'EnsureAdminAccessError', EnsureAdminAccessResponse: 'EnsureAdminAccessResponse' @@ -42,7 +43,8 @@ const Channels = { ConfigureK8Dashboard: 'ConfigureK8Dashboard', ConfigureK8DashboardError: 'ConfigureK8DashboardError', ConfigureK8DashboardResponse: 'ConfigureK8DashboardResponse', - ConfigureCluster: 'ConfigureCluster' + ConfigureCluster: 'ConfigureCluster', + PromptSetupMok: 'PromptSetupMok' }, Updates: { CheckUpdate: 'CheckUpdate', diff --git a/src/constants/Endpoints.ts b/src/constants/Endpoints.ts index 94245b59..f2421e68 100755 --- a/src/constants/Endpoints.ts +++ b/src/constants/Endpoints.ts @@ -11,6 +11,10 @@ const Endpoints = { DEFAULT_OPS_FOLDER: 'etherealengine-ops', Urls: { HOST: 'https://etherealengine.org', + CLIENT_HOST: 'https://local.etherealengine.org', + API_HOST: 'https://api-local.etherealengine.org', + INSTANCE_HOST: 'https://instanceserver-local.etherealengine.org', + FILE_HOST: 'https://localhost:8642', ADMIN_PORTAL: 'https://local.etherealengine.org/admin', LOGIN_PAGE: 'https://local.etherealengine.org/', LAUNCH_PAGE: (host: string) => `${host.startsWith('https') ? '' : 'https://'}${host}/location/apartment`, @@ -53,9 +57,11 @@ const Endpoints = { }, Docs: { INSTALL_WSL: - 'https://etherealengine.github.io/etherealengine-docs/docs/devops_deployment/microk8s_windows/#install-windows-subsystem-for-linux-wsl', + 'https://etherealengine.github.io/etherealengine-docs/docs/host/devops_deployment/microk8s_windows/#install-windows-subsystem-for-linux-wsl', INSTALL_DOCKER: - 'https://etherealengine.github.io/etherealengine-docs/docs/devops_deployment/microk8s_windows/#install-docker-desktop' + 'https://etherealengine.github.io/etherealengine-docs/docs/host/devops_deployment/microk8s_windows/#install-docker-desktop', + ACCEPT_INVALID_CERTS: + 'https://etherealengine.github.io/etherealengine-docs/docs/host/devops_deployment/microk8s_linux#accept-invalid-certs' } } diff --git a/src/constants/Storage.ts b/src/constants/Storage.ts index ceaf78f0..db9baad2 100755 --- a/src/constants/Storage.ts +++ b/src/constants/Storage.ts @@ -38,6 +38,7 @@ const Storage = { KUBECONFIG_CONTEXT: 'KUBECONFIG_CONTEXT', RELEASE_NAME: 'RELEASE_NAME', OPS_PATH: 'OPS_PATH', + SHOW_ALL_BRANCHES: 'SHOW_ALL_BRANCHES', PASSWORD_KEY: generateUUID() } diff --git a/src/main/Clusters/BaseCluster/BaseCluster.class.ts b/src/main/Clusters/BaseCluster/BaseCluster.class.ts index a74fb439..36ea5af1 100644 --- a/src/main/Clusters/BaseCluster/BaseCluster.class.ts +++ b/src/main/Clusters/BaseCluster/BaseCluster.class.ts @@ -18,6 +18,8 @@ import { getEnvFile } from '../../managers/PathManager' import { exec } from '../../managers/ShellManager' import Commands from './BaseCluster.commands' +const type = os.type() + class BaseCluster { // #region Status Check Methods @@ -40,100 +42,108 @@ class BaseCluster { systemApps: AppModel[], sysRequirements: SysRequirement[] ) => { - await Promise.all( - systemApps.map(async (app) => { - let status: AppModel = { - ...app - } + const batchSize = type === 'Windows_NT' ? Commands.STATUS_CHECK_BATCH_LIMIT : systemApps.length + + for (let batch = 0; batch < systemApps.length; batch = batch + batchSize) { + const currentBatch = systemApps.slice(batch, batch + batchSize) + await Promise.all( + currentBatch.map(async (app) => { + let status: AppModel = { + ...app + } - const type = os.type() - const currentOSReqs = sysRequirements.find((item) => item.os === type) + const currentOSReqs = sysRequirements.find((item) => item.os === type) - if (status.id === 'os') { - status = { - ...app, - detail: type, - status: currentOSReqs ? AppStatus.Configured : AppStatus.NotConfigured - } - } else if (status.id === 'cpu') { - const cpus = os.cpus() - status = { - ...app, - detail: `${cpus.length.toString()} core(s)`, - status: currentOSReqs - ? cpus.length < currentOSReqs.minCPU - ? AppStatus.NotConfigured - : AppStatus.Configured - : AppStatus.Pending - } - } else if (status.id === 'memory') { - let memory = os.totalmem() / (1024 * 1024) - status = { - ...app, - detail: `${memory.toString()} MB`, - status: currentOSReqs - ? memory < currentOSReqs.minMemory - ? AppStatus.NotConfigured - : AppStatus.Configured - : AppStatus.Pending + if (status.id === 'os') { + status = { + ...app, + detail: type, + status: currentOSReqs ? AppStatus.Configured : AppStatus.NotConfigured + } + } else if (status.id === 'cpu') { + const cpus = os.cpus() + status = { + ...app, + detail: `${cpus.length.toString()} core(s)`, + status: currentOSReqs + ? cpus.length < currentOSReqs.minCPU + ? AppStatus.NotConfigured + : AppStatus.Configured + : AppStatus.Pending + } + } else if (status.id === 'memory') { + let memory = os.totalmem() / (1024 * 1024) + status = { + ...app, + detail: `${memory.toString()} MB`, + status: currentOSReqs + ? memory < currentOSReqs.minMemory + ? AppStatus.NotConfigured + : AppStatus.Configured + : AppStatus.Pending + } + } else { + status = await Utilities.checkPrerequisite(app) } - } else { - status = await Utilities.checkPrerequisite(app) - } - window.webContents.send(Channels.Utilities.Log, cluster.id, { - category: status.name, - message: status.detail - } as LogModel) - window.webContents.send(Channels.Cluster.CheckSystemStatusResult, cluster.id, status) - }) - ) + window.webContents.send(Channels.Utilities.Log, cluster.id, { + category: status.name, + message: status.detail + } as LogModel) + window.webContents.send(Channels.Cluster.CheckSystemStatusResult, cluster.id, status) + }) + ) + } } private static _checkAppStatus = async (window: BrowserWindow, cluster: ClusterModel, apps: AppModel[]) => { let mandatoryConfigured = true + const batchSize = type === 'Windows_NT' ? Commands.STATUS_CHECK_BATCH_LIMIT : apps.length + + for (let batch = 0; batch < apps.length; batch = batch + batchSize) { + const currentBatch = apps.slice(batch, batch + batchSize) + await Promise.all( + currentBatch.map(async (app) => { + let status: AppModel = { + ...app + } - await Promise.all( - apps.map(async (app) => { - let status: AppModel = { - ...app - } + if (app.checkCommand) { + const response = await exec(app.checkCommand, app.isLinuxCommand) + const { stdout, stderr, error } = response - if (app.checkCommand) { - const response = await exec(app.checkCommand, app.isLinuxCommand) - const { stdout, stderr, error } = response + if (stdout) { + const message = typeof stdout === 'string' ? stdout.trim() : stdout.toString() - if (stdout) { - const message = typeof stdout === 'string' ? stdout.trim() : stdout.toString() + window.webContents.send(Channels.Utilities.Log, cluster.id, { + category: status.name, + message + } as LogModel) + } + if (stderr) { + const message = typeof stderr === 'string' ? stderr.trim() : stderr.toString() - window.webContents.send(Channels.Utilities.Log, cluster.id, { - category: status.name, - message - } as LogModel) - } - if (stderr) { - const message = typeof stderr === 'string' ? stderr.trim() : stderr.toString() + window.webContents.send(Channels.Utilities.Log, cluster.id, { + category: status.name, + message + } as LogModel) - window.webContents.send(Channels.Utilities.Log, cluster.id, { - category: status.name, - message - } as LogModel) + if (!app.isOptional) { + mandatoryConfigured = false + } + } - if (!app.isOptional) { - mandatoryConfigured = false + status = { + ...app, + detail: stderr ? stderr : stdout, + status: stderr || error ? AppStatus.NotConfigured : AppStatus.Configured } } - status = { - ...app, - detail: stderr ? stderr : stdout, - status: stderr || error ? AppStatus.NotConfigured : AppStatus.Configured - } - } - - window.webContents.send(Channels.Cluster.CheckAppStatusResult, cluster.id, status) - }) - ) + window.webContents.send(Channels.Cluster.CheckAppStatusResult, cluster.id, status) + }) + ) + } return mandatoryConfigured } @@ -144,55 +154,60 @@ class BaseCluster { engineApps: AppModel[], preRequisitesConfigured: boolean ) => { - await Promise.all( - engineApps.map(async (engineItem) => { - let status: AppModel = { - ...engineItem - } - - if (preRequisitesConfigured == false) { - status = { - ...engineItem, - detail: 'Ethereal Engine required apps not configured', - status: AppStatus.NotConfigured - } - } else if (engineItem.checkCommand) { - const response = await exec(engineItem.checkCommand, engineItem.isLinuxCommand) - const { stdout, stderr } = response - - if (stdout) { - window.webContents.send(Channels.Utilities.Log, cluster.id, { - category: engineItem.name, - message: typeof stdout === 'string' ? stdout.trim() : stdout - } as LogModel) - } - if (stderr) { - window.webContents.send(Channels.Utilities.Log, cluster.id, { - category: engineItem.name, - message: typeof stderr === 'string' ? stderr.trim() : stderr - } as LogModel) + const batchSize = type === 'Windows_NT' ? Commands.STATUS_CHECK_BATCH_LIMIT : engineApps.length + + for (let batch = 0; batch < engineApps.length; batch = batch + batchSize) { + const currentBatch = engineApps.slice(batch, batch + batchSize) + await Promise.all( + currentBatch.map(async (engineItem) => { + let status: AppModel = { + ...engineItem } - let detail: string | Buffer = `Ready Instances: ${stdout === '' || stdout === undefined ? 0 : stdout}` - let itemStatus = AppStatus.Configured + if (preRequisitesConfigured == false) { + status = { + ...engineItem, + detail: 'Ethereal Engine required apps not configured', + status: AppStatus.NotConfigured + } + } else if (engineItem.checkCommand) { + const response = await exec(engineItem.checkCommand, engineItem.isLinuxCommand) + const { stdout, stderr } = response + + if (stdout) { + window.webContents.send(Channels.Utilities.Log, cluster.id, { + category: engineItem.name, + message: typeof stdout === 'string' ? stdout.trim() : stdout + } as LogModel) + } + if (stderr) { + window.webContents.send(Channels.Utilities.Log, cluster.id, { + category: engineItem.name, + message: typeof stderr === 'string' ? stderr.trim() : stderr + } as LogModel) + } - if (stderr) { - detail = stderr - itemStatus = AppStatus.NotConfigured - } else if (!stdout || parseInt(stdout.toString()) < 1) { - itemStatus = AppStatus.NotConfigured - } + let detail: string | Buffer = `Ready Instances: ${stdout === '' || stdout === undefined ? 0 : stdout}` + let itemStatus = AppStatus.Configured + + if (stderr) { + detail = stderr + itemStatus = AppStatus.NotConfigured + } else if (!stdout || parseInt(stdout.toString()) < 1) { + itemStatus = AppStatus.NotConfigured + } - status = { - ...engineItem, - detail, - status: itemStatus + status = { + ...engineItem, + detail, + status: itemStatus + } } - } - window.webContents.send(Channels.Cluster.CheckEngineStatusResult, cluster.id, status) - }) - ) + window.webContents.send(Channels.Cluster.CheckEngineStatusResult, cluster.id, status) + }) + ) + } } // #endregion Status Check Methods diff --git a/src/main/Clusters/BaseCluster/BaseCluster.commands.ts b/src/main/Clusters/BaseCluster/BaseCluster.commands.ts index 567875de..30096042 100644 --- a/src/main/Clusters/BaseCluster/BaseCluster.commands.ts +++ b/src/main/Clusters/BaseCluster/BaseCluster.commands.ts @@ -1,7 +1,11 @@ const Commands = { IPFS_SECRET: "od -vN 32 -An -tx1 /dev/urandom | tr -d ' \n'", DOCKER_STATS: "docker system df --format='{{json .}}';", - DOCKER_PRUNE: 'docker system prune -a -f;' + DOCKER_PRUNE: 'docker system prune -a -f;', + DEPLOYMENT_PRUNE: 'helm uninstall agones local-redis local;', + MOK_SETUP: 'gnome-terminal --wait -- bash -c "sudo dpkg --configure -a; exit 0; exec bash"', + MOK_RESTART: `sudo systemctl reboot`, + STATUS_CHECK_BATCH_LIMIT: 2 } export default Commands diff --git a/src/main/Clusters/MicroK8s/MicroK8s.appstatus.ts b/src/main/Clusters/MicroK8s/MicroK8s.appstatus.ts index 7d1a34ca..805e0505 100644 --- a/src/main/Clusters/MicroK8s/MicroK8s.appstatus.ts +++ b/src/main/Clusters/MicroK8s/MicroK8s.appstatus.ts @@ -20,7 +20,6 @@ const microk8sDependantScript = (script: string, microk8sPrefix: string) => { return script } - export const MicroK8sAppsStatus = (sudoPassword?: string): AppModel[] => { let microk8sPrefix = '' @@ -32,12 +31,11 @@ export const MicroK8sAppsStatus = (sudoPassword?: string): AppModel[] => { microk8sPrefix = `echo '${sudoPassword}' | sudo -S ${microk8sPrefix}` } - return [ + const appStatus = [ getAppModel('node', 'Node', 'node --version;'), getAppModel('npm', 'npm', 'npm --version;'), getAppModel('python', 'Python', 'pip3 --version; python3 --version;'), getAppModel('make', 'Make', 'make --version;'), - getAppModel('git', 'Git', 'git --version;'), getAppModel('docker', 'Docker', 'docker --version;'), getAppModel('dockercompose', 'Docker Compose', 'docker-compose --version;'), getAppModel('mysql', 'MySql', 'docker top etherealengine_minikube_db;'), @@ -129,6 +127,19 @@ export const MicroK8sAppsStatus = (sudoPassword?: string): AppModel[] => { ), getAppModel('engine', 'Ethereal Engine', microk8sDependantScript(`helm status local;`, microk8sPrefix)) ] + + if (type === 'Windows_NT') { + appStatus.splice( + 4, + 0, + getAppModel('gitWindows', 'Git for Windows', 'git --version;', false), + getAppModel('gitWsl', 'Git for WSL', 'git --version;') + ) + } else { + appStatus.splice(4, 0, getAppModel('git', 'Git', 'git --version;')) + } + + return appStatus } export const MicroK8sRippleAppsStatus = (sudoPassword?: string): AppModel[] => { diff --git a/src/main/Clusters/MicroK8s/MicroK8s.class.ts b/src/main/Clusters/MicroK8s/MicroK8s.class.ts index 766d4d8e..fa14e01d 100644 --- a/src/main/Clusters/MicroK8s/MicroK8s.class.ts +++ b/src/main/Clusters/MicroK8s/MicroK8s.class.ts @@ -158,7 +158,7 @@ class MicroK8s { throw `Failed with error code ${code}.` } - await startFileServer(window, cluster) + await startFileServer(window, cluster, password) } catch (err) { log.error('Error in configureCluster MicroK8s.', err) window.webContents.send(Channels.Utilities.Log, cluster.id, { diff --git a/src/main/Clusters/Minikube/Minikube.class.ts b/src/main/Clusters/Minikube/Minikube.class.ts index 613997ba..5e2167f1 100644 --- a/src/main/Clusters/Minikube/Minikube.class.ts +++ b/src/main/Clusters/Minikube/Minikube.class.ts @@ -88,6 +88,22 @@ class Minikube { const scriptsFolder = scriptsPath() const assetsFolder = assetsPath() + + const checkMokScript = path.join(scriptsFolder, 'check-mok.sh') + log.info(`Executing script ${checkMokScript}`) + + const onCheckMokStd = (data: any) => { + window.webContents.send(Channels.Utilities.Log, cluster.id, { category, message: data } as LogModel) + } + const mokCode = await execStreamScriptFile(checkMokScript, [`-p "${password}"`], onCheckMokStd, onCheckMokStd) + + if (mokCode === 1) { + throw `Failed with error code ${mokCode}.` + } else if (mokCode === 2) { + window.webContents.send(Channels.Cluster.PromptSetupMok, cluster) + return + } + const configureScript = path.join(scriptsFolder, 'configure-minikube-linux.sh') log.info(`Executing script ${configureScript}`) @@ -113,7 +129,7 @@ class Minikube { throw `Failed with error code ${code}.` } - await startFileServer(window, cluster) + await startFileServer(window, cluster, password) } catch (err) { log.error('Error in configureCluster Minikube.', err) window.webContents.send(Channels.Utilities.Log, cluster.id, { diff --git a/src/main/Clusters/Minikube/Minikube.commands.ts b/src/main/Clusters/Minikube/Minikube.commands.ts index 2d1295e7..aef9a4d7 100644 --- a/src/main/Clusters/Minikube/Minikube.commands.ts +++ b/src/main/Clusters/Minikube/Minikube.commands.ts @@ -2,7 +2,9 @@ const Commands = { DOCKER_STATS: "eval $(minikube docker-env); docker system df --format='{{json .}}'; eval $(minikube docker-env -u);", DOCKER_PRUNE: 'eval $(minikube docker-env); docker system prune -a -f; eval $(minikube docker-env -u);', DASHBOARD: 'minikube dashboard --url', - MINIKUBE_REMOVE: 'minikube delete --all --purge=true' + MINIKUBE_REMOVE: 'minikube delete --all --purge=true', + VIRTUALBOX_REMOVE: 'apt-get remove -y --purge virtualbox', + VIRTUALBOX_DKMS_REMOVE: 'apt-get remove -y --purge virtualbox-dkms ' } export default Commands diff --git a/src/main/handlers/Engine/Engine.class.ts b/src/main/handlers/Engine/Engine.class.ts index 3b982509..ac4ea082 100755 --- a/src/main/handlers/Engine/Engine.class.ts +++ b/src/main/handlers/Engine/Engine.class.ts @@ -8,7 +8,7 @@ import Storage from '../../../constants/Storage' import { ClusterModel } from '../../../models/Cluster' import { LogModel } from '../../../models/Log' import { executeJS } from '../../managers/BrowserManager' -import { startFileServer } from '../../managers/FileServerManager' +import { startFileServer, stopFileServer } from '../../managers/FileServerManager' import { exec } from '../../managers/ShellManager' class Engine { @@ -40,7 +40,7 @@ class Engine { let retry = 0 do { - await delay(3000) + await delay(10000) const userRole = await executeJS( 'function getUserRole() { return document.getElementById("user-role").innerHTML } getUserRole()', @@ -120,9 +120,9 @@ class Engine { } } - static startFileServer = async (parentWindow: BrowserWindow, cluster: ClusterModel) => { + static startFileServer = async (parentWindow: BrowserWindow, cluster: ClusterModel, sudoPassword: string) => { try { - await startFileServer(parentWindow, cluster) + await startFileServer(parentWindow, cluster, sudoPassword) } catch (err) { parentWindow.webContents.send(Channels.Utilities.Log, cluster.id, { category: 'file server', @@ -131,6 +131,18 @@ class Engine { throw err } } + + static stopFileServer = async (parentWindow: BrowserWindow, cluster: ClusterModel, sudoPassword: string) => { + try { + await stopFileServer(sudoPassword) + } catch (err) { + parentWindow.webContents.send(Channels.Utilities.Log, cluster.id, { + category: 'stop file server', + message: JSON.stringify(err) + } as LogModel) + throw err + } + } } export default Engine diff --git a/src/main/handlers/Engine/Engine.handler.ts b/src/main/handlers/Engine/Engine.handler.ts index d3f0f9e0..50ff5a59 100755 --- a/src/main/handlers/Engine/Engine.handler.ts +++ b/src/main/handlers/Engine/Engine.handler.ts @@ -11,9 +11,18 @@ class EngineHandler implements IBaseHandler { ipcMain.handle(Channels.Engine.EnsureAdminAccess, async (_event: IpcMainInvokeEvent, cluster: ClusterModel) => { await Engine.ensureAdminAccess(window, cluster) }), - ipcMain.handle(Channels.Engine.StartFileServer, async (_event: IpcMainInvokeEvent, cluster: ClusterModel) => { - await Engine.startFileServer(window, cluster) - }) + ipcMain.handle( + Channels.Engine.StartFileServer, + async (_event: IpcMainInvokeEvent, cluster: ClusterModel, sudoPassword: string) => { + await Engine.startFileServer(window, cluster, sudoPassword) + } + ), + ipcMain.handle( + Channels.Engine.StopFileServer, + async (_event: IpcMainInvokeEvent, cluster: ClusterModel, sudoPassword: string) => { + await Engine.stopFileServer(window, cluster, sudoPassword) + } + ) } } diff --git a/src/main/handlers/Git/Git-helper.ts b/src/main/handlers/Git/Git-helper.ts new file mode 100644 index 00000000..bd354c35 --- /dev/null +++ b/src/main/handlers/Git/Git-helper.ts @@ -0,0 +1,19 @@ +import { exec } from '../../managers/ShellManager' + +export const pull = async (repoPath: string) => { + const command = `git -C ${repoPath} pull` + const { stdout } = await exec(command, true) + return stdout?.toString() +} + +export const checkout = async (repoPath: string, branch: string) => { + const command = `git -C ${repoPath} checkout ${branch}` + const { stdout } = await exec(command, true) + return stdout?.toString() +} + +export const checkoutBranch = async (repoPath: string, localBranch: string, branch: string) => { + const command = `git -C ${repoPath} checkout -b ${localBranch} ${branch}` + const { stdout } = await exec(command, true) + return stdout?.toString() +} diff --git a/src/main/handlers/Git/Git.class.ts b/src/main/handlers/Git/Git.class.ts index d54e65f4..a3abc183 100755 --- a/src/main/handlers/Git/Git.class.ts +++ b/src/main/handlers/Git/Git.class.ts @@ -6,6 +6,7 @@ import { ClusterModel } from '../../../models/Cluster' import { GitStatus } from '../../../models/GitStatus' import { LogModel } from '../../../models/Log' import { ensureWSLToWindowsPath } from '../../managers/PathManager' +import { checkout, checkoutBranch, pull } from './Git-helper' class Git { private static _getGit = async (repoPath: string) => { @@ -73,19 +74,19 @@ class Git { const localExists = all.includes(localBranch) if (localExists) { - await git.checkout(localBranch) + await checkout(repoPath, localBranch) } else { - await git.checkoutBranch(localBranch, branch) + await checkoutBranch(repoPath, localBranch, branch) } } else { - await git.checkout(branch) + await checkout(repoPath, branch) } return true } catch (err) { parentWindow.webContents.send(Channels.Utilities.Log, cluster.id, { category: 'git change branch', - message: JSON.stringify((err as GitResponseError).message) + message: JSON.stringify(err) } as LogModel) return false } @@ -93,15 +94,13 @@ class Git { static pullBranch = async (parentWindow: BrowserWindow, cluster: ClusterModel, repoPath: string) => { try { - const git = await Git._getGit(repoPath) - - await git.pull() + await pull(repoPath) return true } catch (err) { parentWindow.webContents.send(Channels.Utilities.Log, cluster.id, { category: 'git pull branch', - message: JSON.stringify((err as GitResponseError).message) + message: JSON.stringify(err) } as LogModel) return false } diff --git a/src/main/handlers/Workloads/Workloads.class.ts b/src/main/handlers/Workloads/Workloads.class.ts index d491bb7d..ab0a5de5 100644 --- a/src/main/handlers/Workloads/Workloads.class.ts +++ b/src/main/handlers/Workloads/Workloads.class.ts @@ -51,12 +51,13 @@ class Workloads { const configMap = await getConfigMap( k8DefaultClient, - `app.kubernetes.io/instance=${releaseName},app.kubernetes.io/component=client,app.kubernetes.io/name=etherealengine` + `app.kubernetes.io/instance=${releaseName},app.kubernetes.io/component=api,app.kubernetes.io/name=etherealengine` ) - let appHost = configMap.length > 0 && configMap[0].data && configMap[0].data['VITE_APP_HOST'] + let appHost = configMap.length > 0 && configMap[0].data && configMap[0].data['CLIENT_ADDRESS'] + if (!appHost) { - appHost = configMap.length > 0 && configMap[0].data && configMap[0].data['CLIENT_ADDRESS'] + appHost = configMap.length > 0 && configMap[0].data && configMap[0].data['APP_URL'] } if (!appHost) { diff --git a/src/main/managers/FileServerManager.ts b/src/main/managers/FileServerManager.ts index dee9e6d5..050318d7 100644 --- a/src/main/managers/FileServerManager.ts +++ b/src/main/managers/FileServerManager.ts @@ -1,15 +1,14 @@ import { app, BrowserWindow } from 'electron' import path from 'path' -import { kill } from 'ps-node' import Channels from '../../constants/Channels' import Storage from '../../constants/Storage' import { ClusterModel } from '../../models/Cluster' import { LogModel } from '../../models/Log' import { scriptsPath } from './PathManager' -import { execStreamScriptFile, getProcessList } from './ShellManager' +import { exec, execStreamScriptFile, getProcessList } from './ShellManager' -export const startFileServer = async (window: BrowserWindow, cluster: ClusterModel) => { +export const startFileServer = async (window: BrowserWindow, cluster: ClusterModel, sudoPassword: string) => { const existingServer = await getProcessList('http-server') if (existingServer.length > 0) { window.webContents.send(Channels.Utilities.Log, cluster.id, { @@ -25,9 +24,9 @@ export const startFileServer = async (window: BrowserWindow, cluster: ClusterMod e.preventDefault() const existingServers = await getProcessList('http-server') - existingServers.forEach((httpProcess) => { - kill(httpProcess.pid) - }) + for (const httpProcess of existingServers) { + await exec(`echo '${sudoPassword}' | sudo -S kill -9 ${httpProcess.pid}`) + } } catch {} app.quit() @@ -53,3 +52,14 @@ export const startFileServer = async (window: BrowserWindow, cluster: ClusterMod onFileServerStd ) } + +export const stopFileServer = async (sudoPassword: string) => { + const existingServers = await getProcessList('http-server') + if (existingServers.length > 0) { + for (const httpProcess of existingServers) { + await exec(`echo '${sudoPassword}' | sudo -S kill -9 ${httpProcess.pid}`) + } + } else { + throw 'No file server found.' + } +} diff --git a/src/renderer/App.css b/src/renderer/App.css index bec80899..d6d906dd 100644 --- a/src/renderer/App.css +++ b/src/renderer/App.css @@ -234,4 +234,11 @@ body { .font-14 { font-size: 14px !important; +} + +.textEllipse { + white-space: nowrap; + width: 100%; + overflow: hidden; + text-overflow: ellipsis; } \ No newline at end of file diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 3696eaea..1c0e411c 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -14,6 +14,8 @@ import HotBar from './common/HotBar' import NavView from './common/NavView' import { defaultAction } from './common/NotistackActions' import AuthenticationDialog from './dialogs/AuthenticationDialog' +import MokEnrollDialog from './dialogs/MokEnrollDialog' +import MokRestartDialog from './dialogs/MokRestartDialog' import AdminPage from './pages/AdminPage' import ConfigPage from './pages/ConfigPage' import IPFSPage from './pages/IPFSPage' @@ -33,6 +35,8 @@ const App = () => { const settingsState = useSettingsState() const { showAuthenticationDialog } = settingsState.value + const { mokEnrollCluster } = settingsState.value + const { mokRestartCluster } = settingsState.value const defaultMode = 'vaporwave' as ThemeMode const storedMode = localStorage.getItem(Storage.COLOR_MODE) as ThemeMode | undefined @@ -108,6 +112,10 @@ const App = () => { {showAuthenticationDialog && ( SettingsService.setAuthenticationDialog(false)} /> )} + {mokEnrollCluster && SettingsService.setMokEnrollCluster(undefined)} />} + {mokRestartCluster && ( + SettingsService.setMokRestartCluster(undefined)} /> + )} diff --git a/src/renderer/common/OptionsPanel.tsx b/src/renderer/common/OptionsPanel.tsx index d6287222..6ee3afe5 100644 --- a/src/renderer/common/OptionsPanel.tsx +++ b/src/renderer/common/OptionsPanel.tsx @@ -1,4 +1,5 @@ import Channels from 'constants/Channels' +import Endpoints from 'constants/Endpoints' import Storage from 'constants/Storage' import UIEnabled from 'constants/UIEnabled' import { AppStatus } from 'models/AppStatus' @@ -12,13 +13,15 @@ import SettingsDialog from 'renderer/dialogs/SettingsDialog' import { ConfigFileService, useConfigFileState } from 'renderer/services/ConfigFileService' import { DeploymentService, useDeploymentState } from 'renderer/services/DeploymentService' +import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos' import CachedOutlinedIcon from '@mui/icons-material/CachedOutlined' import DeleteIcon from '@mui/icons-material/Delete' +import LocalPoliceIcon from '@mui/icons-material/LocalPolice' import PowerSettingsNewOutlinedIcon from '@mui/icons-material/PowerSettingsNewOutlined' import RocketLaunchOutlinedIcon from '@mui/icons-material/RocketLaunchOutlined' import SettingsIcon from '@mui/icons-material/Settings' import LoadingButton from '@mui/lab/LoadingButton' -import { Box, CircularProgress, IconButton, Stack, Typography } from '@mui/material' +import { Avatar, Box, Button, CircularProgress, IconButton, Popover, Stack, Typography } from '@mui/material' import logoEngine from '../../../assets/icon.svg' import logoMicrok8s from '../../../assets/icons/microk8s.png' @@ -40,6 +43,7 @@ const OptionsPanel = () => { const allAppsConfigured = currentDeployment?.appStatus.every((app) => app.status === AppStatus.Configured) const allEngineConfigured = currentDeployment?.engineStatus.every((engine) => engine.status === AppStatus.Configured) const allConfigured = allAppsConfigured && allEngineConfigured + const [anchorEl, setAnchorEl] = useState(null) if (!selectedCluster) { return <> @@ -51,6 +55,8 @@ const OptionsPanel = () => { const onLaunch = async () => { try { + handlePopoverClose() + setLaunching(true) const clonedCluster = cloneCluster(selectedCluster) @@ -64,6 +70,10 @@ const OptionsPanel = () => { setLaunching(false) } + const handlePopoverClose = () => { + setAnchorEl(null) + } + const handleDelete = async () => { ConfigFileService.setSelectedClusterId('') await ConfigFileService.deleteConfig(selectedClusterId) @@ -156,11 +166,96 @@ const OptionsPanel = () => { Launching } - onClick={onLaunch} + onClick={(event) => + selectedCluster.type === ClusterType.MicroK8s || selectedCluster.type === ClusterType.Minikube + ? setAnchorEl(event.currentTarget) + : onLaunch() + } > Launch + + + + + + Please make sure to accept all certificates in the browser. + + + + + 1 + + Launch{' '} + + Engine + {' '} + in browser & accept certificate. + + + + 2 + Accept certificates for following as well: + + + + + + Api Server + + + + + + + + Instance Server + + + + + + + + File Server + + + + + Reference: + + accept-invalid-certs + + + + + + + + + + {showConfigDialog && setConfigDialog(false)} />} {showSettingsDialog && setSettingsDialog(false)} />} diff --git a/src/renderer/components/Config/PrereqsView.tsx b/src/renderer/components/Config/PrereqsView.tsx index 38be4e9e..ab7a7def 100644 --- a/src/renderer/components/Config/PrereqsView.tsx +++ b/src/renderer/components/Config/PrereqsView.tsx @@ -2,9 +2,11 @@ import Channels from 'constants/Channels' import Endpoints from 'constants/Endpoints' import { ipcRenderer } from 'electron' import log from 'electron-log' +import Commands from 'main/Clusters/BaseCluster/BaseCluster.commands' import { AppModel, AppStatus } from 'models/AppStatus' +import { OSType } from 'models/AppSysInfo' import { useEffect, useState } from 'react' -import { SettingsService } from 'renderer/services/SettingsService' +import { accessSettingsState, SettingsService } from 'renderer/services/SettingsService' import { Box, SxProps, Theme, Typography } from '@mui/material' @@ -27,27 +29,32 @@ const PrereqsView = ({ sx }: Props) => { setStatuses(initialStatuses) const checkedStatuses = [...initialStatuses] + const appSysInfo = accessSettingsState().value.appSysInfo + const batchSize = appSysInfo.osType === OSType.Windows ? Commands.STATUS_CHECK_BATCH_LIMIT : initialStatuses.length - await Promise.all( - initialStatuses.map(async (status) => { - // Display prerequisite with checked status - const checkedStatus = await SettingsService.checkPrerequisite(status) + for (let batch = 0; batch < initialStatuses.length; batch = batch + batchSize) { + const currentBatch = initialStatuses.slice(batch, batch + batchSize) + await Promise.all( + currentBatch.map(async (status) => { + // Display prerequisite with checked status + const checkedStatus = await SettingsService.checkPrerequisite(status) - // Add description for corrective actions to be displayed in dialog - if (checkedStatus.status !== AppStatus.Configured) { - processDescriptions(checkedStatus) - } + // Add description for corrective actions to be displayed in dialog + if (checkedStatus.status !== AppStatus.Configured) { + processDescriptions(checkedStatus) + } - const currentIndex = initialStatuses.findIndex((item) => item.id === status.id) - checkedStatuses[currentIndex] = checkedStatus + const currentIndex = initialStatuses.findIndex((item) => item.id === status.id) + checkedStatuses[currentIndex] = checkedStatus - setStatuses((prevState) => { - const newState = [...prevState] - newState[currentIndex] = checkedStatus - return newState + setStatuses((prevState) => { + const newState = [...prevState] + newState[currentIndex] = checkedStatus + return newState + }) }) - }) - ) + ) + } } const processDescriptions = async (status: AppModel) => { diff --git a/src/renderer/components/GitView.tsx b/src/renderer/components/GitView.tsx index a7ee0550..cc504106 100644 --- a/src/renderer/components/GitView.tsx +++ b/src/renderer/components/GitView.tsx @@ -1,4 +1,5 @@ import Channels from 'constants/Channels' +import Storage from 'constants/Storage' import { cloneCluster } from 'models/Cluster' import { useSnackbar } from 'notistack' import { useConfigFileState } from 'renderer/services/ConfigFileService' @@ -42,9 +43,17 @@ const GitView = ({ name, repoType, sx }: Props) => { let branches: string[] = [] const gitData = currentDeployment.gitStatus[repoType].data if (gitData) { - const allowedBranches = ['dev', '/dev', 'master', '/master'] + const showAllBranches = localStorage.getItem(Storage.SHOW_ALL_BRANCHES) ?? 'false' - branches = gitData.branches.filter((item) => allowedBranches.some((allowed) => item.endsWith(allowed))) + if (showAllBranches === 'true') { + branches = gitData.branches.map((item) => item) // Doing map here so that a clone is created rather than referencing original object + } else { + const allowedBranches = ['dev', '/dev', 'master', '/master'] + branches = gitData.branches.filter((item) => allowedBranches.some((allowed) => item.endsWith(allowed))) + } + + console.log(branches) + console.log(gitData.tags) branches.push(...gitData.tags) const current = gitData.current diff --git a/src/renderer/components/Setting/AdditionalConfigsView.tsx b/src/renderer/components/Setting/AdditionalConfigsView.tsx new file mode 100644 index 00000000..4c451c7d --- /dev/null +++ b/src/renderer/components/Setting/AdditionalConfigsView.tsx @@ -0,0 +1,33 @@ +import Storage from 'constants/Storage' + +import { Box, FormControlLabel, Switch, SxProps, Theme, Typography } from '@mui/material' + +import InfoTooltip from '../../common/InfoTooltip' + +interface Props { + localFlags: Record + onChange: (key: string, value: string) => void + sx?: SxProps +} + +const AdditionalConfigsView = ({ localFlags, onChange, sx }: Props) => { + return ( + + + {Storage.SHOW_ALL_BRANCHES.replaceAll('_', ' ')} + + + } + sx={{ marginTop: 1, marginLeft: 0 }} + control={} + value={localFlags[Storage.SHOW_ALL_BRANCHES] === 'true'} + onChange={(_event, checked) => onChange(Storage.SHOW_ALL_BRANCHES, checked ? 'true' : 'false')} + /> + + ) +} + +export default AdditionalConfigsView diff --git a/src/renderer/components/Setting/EngineView.tsx b/src/renderer/components/Setting/EngineView.tsx new file mode 100644 index 00000000..1015f3b2 --- /dev/null +++ b/src/renderer/components/Setting/EngineView.tsx @@ -0,0 +1,374 @@ +import Channels from 'constants/Channels' +import Endpoints from 'constants/Endpoints' +import Commands from 'main/Clusters/BaseCluster/BaseCluster.commands' +import { cloneCluster } from 'models/Cluster' +import { ShellResponse } from 'models/ShellResponse' +import { useSnackbar } from 'notistack' +import { useState } from 'react' +import { useConfigFileState } from 'renderer/services/ConfigFileService' +import { DeploymentService } from 'renderer/services/DeploymentService' +import { SettingsService } from 'renderer/services/SettingsService' + +import { LoadingButton } from '@mui/lab' +import { + Box, + CircularProgress, + FormControlLabel, + InputAdornment, + SxProps, + TextField, + Theme, + Typography +} from '@mui/material' + +import Storage from '../../../constants/Storage' +import InfoTooltip from '../../common/InfoTooltip' +import AlertDialog from '../../dialogs/AlertDialog' + +interface Props { + sx?: SxProps +} + +const EngineView = ({ sx }: Props) => { + const { enqueueSnackbar } = useSnackbar() + const [showDeploymentAlert, setDeploymentAlert] = useState(false) + const [showDatabaseAlert, setDatabaseAlert] = useState(false) + const [showFileServerAlert, setFileServerAlert] = useState(false) + const [showEnvAlert, setEnvAlert] = useState(false) + const [processingMakeAdmin, setProcessingMakeAdmin] = useState(false) + const [processingDeploymentPrune, setProcessingDeploymentPrune] = useState(false) + const [processingDatabaseClear, setProcessingDatabaseClear] = useState(false) + const [processingFileServerStop, setProcessingFileServerStop] = useState(false) + const [processingEnvReset, setProcessingEnvReset] = useState(false) + const [adminValue, setAdminValue] = useState('') + + const configFileState = useConfigFileState() + const { selectedCluster } = configFileState.value + + if (!selectedCluster) { + return <> + } + + const onMakeAdmin = async () => { + try { + setProcessingMakeAdmin(true) + + const clonedCluster = cloneCluster(selectedCluster) + const enginePath = clonedCluster.configs[Storage.ENGINE_PATH] + + const command = `export MYSQL_PORT=${Endpoints.MYSQL_PORT}; cd ${enginePath}; npm run make-user-admin -- --id=${adminValue}` + const output: ShellResponse = await window.electronAPI.invoke( + Channels.Shell.ExecuteCommand, + clonedCluster, + command + ) + + const stringError = output.stderr?.toString().trim() || '' + if (stringError.toLowerCase().includes('error') || stringError.toLowerCase().includes('does not exist')) { + throw stringError + } + } catch (err) { + enqueueSnackbar('Failed to make admin.', { variant: 'error' }) + } + + setProcessingMakeAdmin(false) + } + + const onPruneDeployment = async () => { + try { + setDeploymentAlert(false) + setProcessingDeploymentPrune(true) + + const clonedCluster = cloneCluster(selectedCluster) + + const command = Commands.DEPLOYMENT_PRUNE + const output: ShellResponse = await window.electronAPI.invoke( + Channels.Shell.ExecuteCommand, + clonedCluster, + command + ) + + const stringError = output.stderr?.toString().trim() || '' + if (stringError.toLowerCase().includes('error')) { + throw stringError + } + } catch (err) { + enqueueSnackbar('Failed to remove Ethereal Engine deployment.', { variant: 'error' }) + } + + setProcessingDeploymentPrune(false) + } + + const onClearDatabase = async () => { + try { + setDatabaseAlert(false) + setProcessingDatabaseClear(true) + + const clonedCluster = cloneCluster(selectedCluster) + const enginePath = clonedCluster.configs[Storage.ENGINE_PATH] + + const command = ` + docker container stop etherealengine_minikube_db; + docker container stop etherealengine_test_db; + docker container rm etherealengine_minikube_db; + docker container rm etherealengine_test_db; + docker container prune --force; + cd '${enginePath}'; + npm run dev-docker` + const output: ShellResponse = await window.electronAPI.invoke( + Channels.Shell.ExecuteCommand, + clonedCluster, + command + ) + + const stringError = output.stderr?.toString().trim() || '' + if (stringError.toLowerCase().includes('error')) { + throw stringError + } + } catch (err) { + enqueueSnackbar('Failed to clear database.', { variant: 'error' }) + } + + setProcessingDatabaseClear(false) + } + + const onStopFileServer = async () => { + const clonedCluster = cloneCluster(selectedCluster) + + try { + setFileServerAlert(false) + setProcessingFileServerStop(true) + + const password = await SettingsService.getDecryptedSudoPassword() + + await window.electronAPI.invoke(Channels.Engine.StopFileServer, clonedCluster, password) + + setProcessingFileServerStop(false) + + await DeploymentService.fetchDeploymentStatus(clonedCluster) + } catch (err) { + enqueueSnackbar(err?.message ? err.message : err, { + variant: 'error' + }) + setProcessingFileServerStop(false) + } + } + + const onResetEnv = async () => { + try { + setEnvAlert(false) + setProcessingEnvReset(true) + + const clonedCluster = cloneCluster(selectedCluster) + const enginePath = clonedCluster.configs[Storage.ENGINE_PATH] + const envPath = enginePath + '/' + Endpoints.Paths.ENGINE_ENV + + const password = await SettingsService.getDecryptedSudoPassword() + + const command = `echo '${password}' | sudo -S rm -- ${envPath}; cd ${enginePath}; cp .env.local.default .env.local` + const output: ShellResponse = await window.electronAPI.invoke( + Channels.Shell.ExecuteCommand, + clonedCluster, + command + ) + + const stringError = output.stderr?.toString().trim() || '' + if (stringError.toLowerCase().includes('error')) { + throw stringError + } + } catch (err) { + enqueueSnackbar('Failed to reset .env.local file.', { variant: 'error' }) + } + + setProcessingEnvReset(false) + } + + return ( + + + + + + Making + + } + onClick={() => onMakeAdmin()} + > + {processingMakeAdmin ? '' : 'Make'} + + + ) + }} + onChange={(event) => { + setAdminValue(event.target.value) + }} + /> + + + + + REMOVE ENGINE DEPLOYMENT + + + } + control={<>} + sx={{ marginTop: 2, marginLeft: 0 }} + /> + + + Pruning + + } + onClick={() => setDeploymentAlert(true)} + > + Prune + + + + + CLEAR DATABASE + + + } + control={<>} + sx={{ marginTop: 2, marginLeft: 0 }} + /> + + + Clearing + + } + onClick={() => setDatabaseAlert(true)} + > + Clear + + + + + STOP FILE SERVER + + + } + control={<>} + sx={{ marginTop: 2, marginLeft: 0 }} + /> + + + Stopping + + } + onClick={() => setFileServerAlert(true)} + > + Stop + + + + + RESET .ENV.LOCAL + + + } + control={<>} + sx={{ marginTop: 2, marginLeft: 0 }} + /> + + + Resetting + + } + onClick={() => setEnvAlert(true)} + > + Reset + + + + {showDeploymentAlert && ( + setDeploymentAlert(false)} + onOk={onPruneDeployment} + /> + )} + {showDatabaseAlert && ( + setDatabaseAlert(false)} + onOk={onClearDatabase} + /> + )} + {showFileServerAlert && ( + setFileServerAlert(false)} + onOk={onStopFileServer} + /> + )} + {showEnvAlert && ( + setEnvAlert(false)} + onOk={onResetEnv} + /> + )} + + ) +} + +export default EngineView diff --git a/src/renderer/components/Setting/MicroK8sView.tsx b/src/renderer/components/Setting/MicroK8sView.tsx index dbc7d294..74a9544a 100644 --- a/src/renderer/components/Setting/MicroK8sView.tsx +++ b/src/renderer/components/Setting/MicroK8sView.tsx @@ -1,4 +1,3 @@ -import { decryptPassword, delay } from 'common/UtilitiesManager' import Channels from 'constants/Channels' import Endpoints from 'constants/Endpoints' import Commands from 'main/Clusters/MicroK8s/MicroK8s.commands' @@ -8,6 +7,7 @@ import { ShellResponse } from 'models/ShellResponse' import { useSnackbar } from 'notistack' import { useState } from 'react' import { useConfigFileState } from 'renderer/services/ConfigFileService' +import { DeploymentService } from 'renderer/services/DeploymentService' import { accessSettingsState, SettingsService } from 'renderer/services/SettingsService' import { LoadingButton } from '@mui/lab' @@ -35,24 +35,13 @@ const MicroK8sView = ({ sx }: Props) => { } const onPruneMicroK8s = async () => { + const clonedCluster = cloneCluster(selectedCluster) + try { setAlert(false) setProcessingMicroK8sPrune(true) - const clonedCluster = cloneCluster(selectedCluster) - - let sudoPassword = accessSettingsState().value.sudoPassword - - if (!sudoPassword) { - SettingsService.setAuthenticationDialog(true) - - while (!sudoPassword) { - await delay(1000) - sudoPassword = accessSettingsState().value.sudoPassword - } - } - - const password = decryptPassword(sudoPassword) + const password = await SettingsService.getDecryptedSudoPassword() const command = `echo '${password}' | sudo -S ${Commands.MICROK8S_REMOVE}` const output: ShellResponse = await window.electronAPI.invoke( @@ -65,11 +54,14 @@ const MicroK8sView = ({ sx }: Props) => { if (stringError.toLowerCase().includes('error') || stringError.toLowerCase().includes('is not installed')) { throw stringError } + + setProcessingMicroK8sPrune(false) + + await DeploymentService.fetchDeploymentStatus(clonedCluster) } catch (err) { enqueueSnackbar('Failed to remove microK8s.', { variant: 'error' }) + setProcessingMicroK8sPrune(false) } - - setProcessingMicroK8sPrune(false) } const onOpenMicroK8sRegistry = async () => { @@ -158,7 +150,7 @@ const MicroK8sView = ({ sx }: Props) => { - + {showAlert && ( { const { enqueueSnackbar } = useSnackbar() - const [showAlert, setAlert] = useState(false) + const [showMinikubeAlert, setMinikubeAlert] = useState(false) + const [showVirtualboxAlert, setVirtualboxAlert] = useState(false) const [processingMinikubePrune, setProcessingMinikubePrune] = useState(false) + const [processingVirtualboxPrune, setProcessingVirtualboxPrune] = useState(false) const configFileState = useConfigFileState() const { selectedCluster } = configFileState.value @@ -32,24 +34,13 @@ const MinikubeView = ({ sx }: Props) => { } const onPruneMinikube = async () => { + const clonedCluster = cloneCluster(selectedCluster) + try { - setAlert(false) + setMinikubeAlert(false) setProcessingMinikubePrune(true) - const clonedCluster = cloneCluster(selectedCluster) - - let sudoPassword = accessSettingsState().value.sudoPassword - - if (!sudoPassword) { - SettingsService.setAuthenticationDialog(true) - - while (!sudoPassword) { - await delay(1000) - sudoPassword = accessSettingsState().value.sudoPassword - } - } - - const password = decryptPassword(sudoPassword) + const password = await SettingsService.getDecryptedSudoPassword() const command = `echo '${password}' | sudo -S ${Commands.MINIKUBE_REMOVE}` const output: ShellResponse = await window.electronAPI.invoke( @@ -62,11 +53,48 @@ const MinikubeView = ({ sx }: Props) => { if (stringError.toLowerCase().includes('error') || stringError.toLowerCase().includes('is not installed')) { throw stringError } + + setProcessingMinikubePrune(false) + + await DeploymentService.fetchDeploymentStatus(clonedCluster) } catch (err) { enqueueSnackbar('Failed to remove minikube.', { variant: 'error' }) + setProcessingMinikubePrune(false) } + } + + const onPruneVirtualbox = async () => { + const clonedCluster = cloneCluster(selectedCluster) + + try { + setVirtualboxAlert(false) + setProcessingVirtualboxPrune(true) + + const password = await SettingsService.getDecryptedSudoPassword() + + let command = `echo '${password}' | sudo -S ${Commands.VIRTUALBOX_REMOVE}` + let output: ShellResponse = await window.electronAPI.invoke(Channels.Shell.ExecuteCommand, clonedCluster, command) - setProcessingMinikubePrune(false) + let stringError = output.stderr?.toString().trim() || '' + if (stringError.toLowerCase().includes('error')) { + throw stringError + } + + command = `echo '${password}' | sudo -S ${Commands.VIRTUALBOX_DKMS_REMOVE}` + output = await window.electronAPI.invoke(Channels.Shell.ExecuteCommand, clonedCluster, command) + + stringError = output.stderr?.toString().trim() || '' + if (stringError.toLowerCase().includes('error')) { + throw stringError + } + + setProcessingVirtualboxPrune(false) + + await DeploymentService.fetchDeploymentStatus(clonedCluster) + } catch (err) { + enqueueSnackbar('Failed to remove virtualbox.', { variant: 'error' }) + setProcessingVirtualboxPrune(false) + } } return ( @@ -93,23 +121,61 @@ const MinikubeView = ({ sx }: Props) => { Pruning } - onClick={() => setAlert(true)} + onClick={() => setMinikubeAlert(true)} > Prune - + + + REMOVE VIRTUALBOX + + + } + control={<>} + sx={{ marginTop: 2, marginLeft: 0 }} + /> + + + Pruning + + } + onClick={() => setVirtualboxAlert(true)} + > + Prune + + + + - {showAlert && ( + {showMinikubeAlert && ( setAlert(false)} + onClose={() => setMinikubeAlert(false)} onOk={onPruneMinikube} /> )} + + {showVirtualboxAlert && ( + setVirtualboxAlert(false)} + onOk={onPruneVirtualbox} + /> + )} ) } diff --git a/src/renderer/components/StatusView.tsx b/src/renderer/components/StatusView.tsx index b93458e1..fd23cda0 100644 --- a/src/renderer/components/StatusView.tsx +++ b/src/renderer/components/StatusView.tsx @@ -7,7 +7,7 @@ import { ShellResponse } from 'models/ShellResponse' import { Fragment, useState } from 'react' import { accessConfigFileState } from 'renderer/services/ConfigFileService' import { DeploymentService } from 'renderer/services/DeploymentService' -import { accessSettingsState } from 'renderer/services/SettingsService' +import { accessSettingsState, SettingsService } from 'renderer/services/SettingsService' import CancelOutlinedIcon from '@mui/icons-material/CancelOutlined' import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline' @@ -190,7 +190,8 @@ const onFix = async (appStatus: AppModel, setFixing: React.Dispatch { - await window.electronAPI.invoke(Channels.Engine.StartFileServer, clonedCluster) + const password = await SettingsService.getDecryptedSudoPassword() + await window.electronAPI.invoke(Channels.Engine.StartFileServer, clonedCluster, password) // Delay to wait for fileserver to start await delay(4000) diff --git a/src/renderer/dialogs/MokEnrollDialog.tsx b/src/renderer/dialogs/MokEnrollDialog.tsx new file mode 100644 index 00000000..2d49ba02 --- /dev/null +++ b/src/renderer/dialogs/MokEnrollDialog.tsx @@ -0,0 +1,121 @@ +import Channels from 'constants/Channels' +import Commands from 'main/Clusters/BaseCluster/BaseCluster.commands' +import { cloneCluster } from 'models/Cluster' +import { ShellResponse } from 'models/ShellResponse' +import { enqueueSnackbar } from 'notistack' +import { SettingsService, useSettingsState } from 'renderer/services/SettingsService' + +import { Avatar, Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, Typography } from '@mui/material' + +import logoMinikube from '../../../assets/icons/minikube.png' + +interface Props { + onClose: () => void +} + +const MokEnrollDialog = ({ onClose }: Props) => { + const settingsState = useSettingsState() + const selectedCluster = settingsState.value.mokEnrollCluster + + const onSetupMok = async () => { + try { + const clonedCluster = cloneCluster(selectedCluster!) + + const output: ShellResponse = await window.electronAPI.invoke( + Channels.Shell.ExecuteCommand, + clonedCluster, + Commands.MOK_SETUP + ) + + const stringError = output.stderr?.toString().trim() || '' + if (stringError.toLowerCase().includes('error')) { + throw stringError + } + + onClose() + SettingsService.setMokRestartCluster(clonedCluster) + } catch (err) { + enqueueSnackbar('Failed to setup MOK.', { variant: 'error' }) + } + } + + return ( + + Machine Owner Key (MOK) enrollment + + + + + + + {selectedCluster?.name} + + + + + UEFI Secure Boot is enabled for this system. You need a Secure Boot Module Signature key enrolled for + Minikube configuration to proceed. Once you allow this app to enroll this Machine Owner Key, you will be + presented with a terminal. + + + + + + Please follow these steps in the terminal to create a Secure Boot Module Signature key for your system: + + + + + 1 + Enter sudo password + + + 2 + In the 'Secure Boot' screen, press 'Escape' key + + + 3 + Enter password for MOK management + + + 4 + Confirm password for MOK management + + + + + Do you want to enroll Machine Owner Key? + + + + + + + + + + ) +} + +export default MokEnrollDialog diff --git a/src/renderer/dialogs/MokRestartDialog.tsx b/src/renderer/dialogs/MokRestartDialog.tsx new file mode 100644 index 00000000..496dffdc --- /dev/null +++ b/src/renderer/dialogs/MokRestartDialog.tsx @@ -0,0 +1,132 @@ +import Channels from 'constants/Channels' +import Commands from 'main/Clusters/BaseCluster/BaseCluster.commands' +import { cloneCluster } from 'models/Cluster' +import { ShellResponse } from 'models/ShellResponse' +import { enqueueSnackbar } from 'notistack' +import { SettingsService, useSettingsState } from 'renderer/services/SettingsService' + +import { Avatar, Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, Typography } from '@mui/material' + +import logoMinikube from '../../../assets/icons/minikube.png' + +interface Props { + onClose: () => void +} + +const MokRestartDialog = ({ onClose }: Props) => { + const settingsState = useSettingsState() + const selectedCluster = settingsState.value.mokRestartCluster + + const onRestart = async () => { + try { + const clonedCluster = cloneCluster(selectedCluster!) + + const password = await SettingsService.getDecryptedSudoPassword() + + const command = Commands.MOK_RESTART.replaceAll('sudo', `echo "${password}" | sudo -S`) + + const output: ShellResponse = await window.electronAPI.invoke( + Channels.Shell.ExecuteCommand, + clonedCluster, + command + ) + + const stringError = output.stderr?.toString().trim() || '' + if (stringError.toLowerCase().includes('error')) { + throw stringError + } + } catch (err) { + enqueueSnackbar('Failed to restart.', { variant: 'error' }) + } + } + + return ( + + To continue system needs to be rebooted + + + + + + + {selectedCluster?.name} + + + + Secure Boot Module Signature key has been setup, you will need to restart this system to enroll it. After + restarting, you will be presented with a MOK manager. + + + + + Please follow these steps in the MOK manager to enroll a Secure Boot Module Signature key for your system: + + + + + 1 + Press any key on first screen + + + 2 + Select 'Enroll MOK' option + + + 3 + Select 'Continue' on 'Enroll MOK' screen + + + 4 + Select 'Yes' on 'Enroll the key(s)' screen + + + 5 + Enter MOK management password, which you used in the terminal + + + 6 + Select 'Reboot' option in the final screen + + + + + After rebooting you can run 'Configure' button in Ethereal Engine Control Center again + + + + + Do you want to reboot the system? + + + + + + + + + + ) +} + +export default MokRestartDialog diff --git a/src/renderer/dialogs/SettingsDialog.tsx b/src/renderer/dialogs/SettingsDialog.tsx index 84b33793..3449ed59 100644 --- a/src/renderer/dialogs/SettingsDialog.tsx +++ b/src/renderer/dialogs/SettingsDialog.tsx @@ -1,7 +1,9 @@ +import Storage from 'constants/Storage' import UIEnabled from 'constants/UIEnabled' import { ClusterModel, ClusterType } from 'models/Cluster' import { useSnackbar } from 'notistack' import { useState } from 'react' +import AdditionalConfigsView from 'renderer/components/Setting/AdditionalConfigsView' import MicroK8sView from 'renderer/components/Setting/MicroK8sView' import { ConfigFileService, useConfigFileState } from 'renderer/services/ConfigFileService' import { DeploymentService } from 'renderer/services/DeploymentService' @@ -26,6 +28,7 @@ import logo from '../../../assets/icon.svg' import ConfigsView from '../components/Config/ConfigsView' import VarsView from '../components/Config/VarsView' import BackupView from '../components/Setting/BackupView' +import EngineView from '../components/Setting/EngineView' import MinikubeView from '../components/Setting/MinikubeView' interface Props { @@ -44,6 +47,12 @@ const SettingsDialog = ({ onClose }: Props) => { const [tempConfigs, setTempConfigs] = useState({} as Record) const [tempVars, setTempVars] = useState({} as Record) + const showAllBranches = localStorage.getItem(Storage.SHOW_ALL_BRANCHES) as string | undefined + const defaultFlags = { + [Storage.SHOW_ALL_BRANCHES]: showAllBranches ? showAllBranches : 'false' + } as Record + const [localFlags, setLocalFlags] = useState(defaultFlags) + if (!selectedCluster) { enqueueSnackbar('Please select a cluster.', { variant: 'error' }) onClose() @@ -60,6 +69,12 @@ const SettingsDialog = ({ onClose }: Props) => { localVars[key] = key in tempVars ? tempVars[key] : selectedCluster.variables[key] } + const changeFlag = async (key: string, value: string) => { + const newFlags = { ...localFlags } + newFlags[key] = value + setLocalFlags(newFlags) + } + const changeConfig = async (key: string, value: string) => { const newConfigs = { ...tempConfigs } newConfigs[key] = value @@ -87,6 +102,8 @@ const SettingsDialog = ({ onClose }: Props) => { updatedCluster.variables[key] = tempVars[key] } + localStorage.setItem(Storage.SHOW_ALL_BRANCHES, localFlags[Storage.SHOW_ALL_BRANCHES]) + const saved = await ConfigFileService.insertOrUpdateConfig(updatedCluster) if (saved) { onClose() @@ -114,6 +131,9 @@ const SettingsDialog = ({ onClose }: Props) => { {UIEnabled[selectedCluster.type].settings.variables && } {selectedCluster.type === ClusterType.Minikube && } {selectedCluster.type === ClusterType.MicroK8s && } + {(selectedCluster.type === ClusterType.MicroK8s || selectedCluster.type === ClusterType.Minikube) && ( + + )} @@ -125,6 +145,7 @@ const SettingsDialog = ({ onClose }: Props) => { onChange={changeConfig} sx={{ display: 'flex', flexDirection: 'column', alignItems: 'start' }} /> + )} {UIEnabled[selectedCluster.type].settings.variables && ( @@ -146,6 +167,9 @@ const SettingsDialog = ({ onClose }: Props) => { )} + + + {