Created
November 13, 2025 14:27
-
-
Save anjannath/617ac6acedd4382a8caeba8681943e9a to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #Requires -Version 7.0 | |
| <# | |
| .SYNOPSIS | |
| A PowerShell script to initialize and run a CRC (CodeReady Containers) VM using macadam on Windows. | |
| This script is a translation of the original bash script 'run-with-macadam.sh'. | |
| #> | |
| # --- Script Configuration --- | |
| $ErrorActionPreference = 'Stop' | |
| # --- Global Variables --- | |
| $PASS_DEVELOPER = $env:PASS_DEVELOPER -if $null { "P@ssd3v3loper" } | |
| $PASS_KUBEADMIN = $env:PASS_KUBEADMIN -if $null { "P@sskub3admin" } | |
| $CRC_BUNDLE_PATH = $env:CRC_BUNDLE_PATH -if $null { "$env:USERPROFILE\Downloads\crc_hyperv_4.19.3_amd64.crcbundle" } | |
| $VM_NAME = "crc-ng" | |
| $PRIV_KEY_PATH = "" | |
| $PUB_KEY_PATH = "" | |
| $USER_DATA_PATH = "" | |
| $DISK_IMAGE_PATH = "" | |
| $PULL_SECRET = "" | |
| $PUB_KEY = "" | |
| $GVPROXY_SOCKET_PATH = "" | |
| $SCRIPT_ROOT = Split-Path -Parent $MyInvocation.MyCommand.Path | |
| # --- Functions --- | |
| # Cleanup function to be executed on script exit | |
| function cleanup { | |
| Write-Host "--- Cleaning up ---" | |
| $filesToRemove = @("user-data", "crc.vhd", "bundle.tar", "kubeconfig", $PRIV_KEY_PATH, $PUB_KEY_PATH) | |
| foreach ($file in $filesToRemove) { | |
| if (Test-Path $file) { | |
| Remove-Item -Path $file -Force -ErrorAction SilentlyContinue | |
| } | |
| } | |
| # Suppress errors if macadam rm fails (e.g., VM doesn't exist) | |
| try { | |
| macadam.exe rm crc-ng --force 2>$null | |
| } catch {} | |
| } | |
| # Generic error handler | |
| function die { | |
| param([string]$message) | |
| Write-Error "Error: $message" | |
| exit 1 | |
| } | |
| # Generates a temporary SSH keypair for the duration of the script execution. | |
| function Generate-SshKeyPair { | |
| Write-Host "--- Generating temporary SSH keypair ---" | |
| $key_file = Join-Path -Path $SCRIPT_ROOT -ChildPath "id_rsa_temp" | |
| if (Test-Path $key_file) { Remove-Item $key_file } | |
| if (Test-Path "$key_file.pub") { Remove-Item "$key_file.pub" } | |
| try { | |
| ssh-keygen.exe -t rsa -b 4096 -f $key_file -N "" -C "crc-macadam-ci" | |
| } catch { | |
| die "Failed to generate SSH keypair." | |
| } | |
| $script:PRIV_KEY_PATH = $key_file | |
| $script:PUB_KEY_PATH = "$key_file.pub" | |
| Write-Host "Temporary SSH keypair generated." | |
| } | |
| # Ensures all required dependencies are available in the PATH | |
| function Ensure-Deps { | |
| Write-Host "--- Checking dependencies ---" | |
| # On Windows, we expect tar and curl (or equivalents) to be available in modern versions. | |
| # zstd support in Windows' tar.exe is required. | |
| $deps = @{ | |
| "tar.exe" = "zstd"; | |
| "curl.exe" = "curl" | |
| } | |
| foreach ($dep in $deps.GetEnumerator()) { | |
| if (-not (Get-Command $dep.Name -ErrorAction SilentlyContinue)) { | |
| Write-Host "Required dependency '$($dep.Name)' not found in PATH." | |
| if (Get-Command choco.exe -ErrorAction SilentlyContinue) { | |
| Write-Host "Attempting to install '$($dep.Value)' with Chocolatey..." | |
| try { | |
| choco.exe install $($dep.Value) -y --no-progress | |
| } catch { | |
| die "Failed to install '$($dep.Value)' with Chocolatey. Please install it manually and try again." | |
| } | |
| # Verify installation | |
| if (-not (Get-Command $dep.Name -ErrorAction SilentlyContinue)) { | |
| die "Chocolatey installation of '$($dep.Value)' complete, but '$($dep.Name)' is still not in PATH. Please check your environment." | |
| } | |
| Write-Host "'$($dep.Name)' installed successfully." | |
| } else { | |
| die "Required dependency '$($dep.Name)' not found in PATH, and Chocolatey is not available. Please install it manually and try again." | |
| } | |
| } | |
| } | |
| } | |
| # Loads secrets and other resources into variables. | |
| function Load-Resources { | |
| Write-Host "--- Loading resources ---" | |
| if (-not ($env:PULL_SECRET_PATH) -or -not (Test-Path $env:PULL_SECRET_PATH)) { | |
| die "Path to pull secret file must be set via PULL_SECRET_PATH, and the file must exist." | |
| } | |
| if (-not (Test-Path $PUB_KEY_PATH)) { | |
| die "Public key file not found at $PUB_KEY_PATH. This should have been generated." | |
| } | |
| $script:PULL_SECRET = Get-Content -Path $env:PULL_SECRET_PATH -Raw | |
| $script:PUB_KEY = Get-Content -Path $PUB_KEY_PATH -Raw | |
| } | |
| # Generates cloud-init user-data file for VM configuration | |
| function Gen-CloudInit { | |
| Write-Host "--- Generating cloud-init user-data ---" | |
| $userDataFile = Join-Path -Path $SCRIPT_ROOT -ChildPath "user-data" | |
| if (Test-Path $userDataFile) { Remove-Item $userDataFile } | |
| # PowerShell's here-string (@"..."@) is used for multi-line content. | |
| # Variables inside are automatically expanded. | |
| $userDataContent = @" | |
| #cloud-config | |
| runcmd: | |
| - systemctl enable --now kubelet | |
| write_files: | |
| - path: /home/core/.ssh/authorized_keys | |
| content: '$PUB_KEY' | |
| owner: core | |
| permissions: '0600' | |
| - path: /opt/crc/id_rsa.pub | |
| content: '$PUB_KEY' | |
| owner: root:root | |
| permissions: '0644' | |
| - path: /etc/sysconfig/crc-env | |
| content: | | |
| CRC_SELF_SUFFICIENT=1 | |
| CRC_NETWORK_MODE_USER=1 | |
| owner: root:root | |
| permissions: '0644' | |
| - path: /opt/crc/pull-secret | |
| content: | | |
| $PULL_SECRET | |
| permissions: '0644' | |
| - path: /opt/crc/pass_kubeadmin | |
| content: '$PASS_KUBEADMIN' | |
| permissions: '0644' | |
| - path: /opt/crc/pass_developer | |
| content: '$PASS_DEVELOPER' | |
| permissions: '0644' | |
| - path: /opt/crc/ocp-custom-domain.service.done | |
| permissions: '0644' | |
| "@ | |
| Set-Content -Path $userDataFile -Value $userDataContent | |
| $script:USER_DATA_PATH = $userDataFile | |
| Write-Host "cloud-init user-data file created." | |
| } | |
| # Extracts the VM disk image from the CRC bundle | |
| function Extract-DiskImage { | |
| Write-Host "--- Extracting VM image from CRC bundle ---" | |
| if (-not (Test-Path $CRC_BUNDLE_PATH)) { | |
| die "CRC bundle not found at $CRC_BUNDLE_PATH." | |
| } | |
| $bundle_name = Split-Path -Leaf $CRC_BUNDLE_PATH | |
| $disk_image_name = "crc.vhd" | |
| $archive_internal_path = "$($bundle_name -replace '\.crcbundle$')/$disk_image_name" | |
| $output_path = Join-Path -Path $SCRIPT_ROOT -ChildPath $disk_image_name | |
| # Using Windows' built-in tar to extract the zstd compressed file. | |
| try { | |
| tar.exe --zstd -xvf $CRC_BUNDLE_PATH -C $SCRIPT_ROOT $archive_internal_path | |
| Rename-Item -Path (Join-Path $SCRIPT_ROOT $archive_internal_path) -NewName $output_path | |
| Remove-Item -Path (Join-Path $SCRIPT_ROOT ($archive_internal_path | Split-Path -Parent)) -Recurse | |
| } catch { | |
| die "Failed to extract disk image from bundle. Make sure your tar.exe supports --zstd." | |
| } | |
| if (-not ((Get-Item $output_path).Length -gt 0)) { | |
| die "Extracted disk image is empty." | |
| } | |
| $script:DISK_IMAGE_PATH = $output_path | |
| Write-Host "VM image extracted to $DISK_IMAGE_PATH." | |
| } | |
| # Ensures macadam tool is available | |
| function Ensure-MacadamExists { | |
| Write-Host "--- Setting up macadam ---" | |
| if (Get-Command macadam.exe -ErrorAction SilentlyContinue) { | |
| Write-Host "macadam.exe is already available in PATH." | |
| return | |
| } | |
| $macadam_dir = "C:\opt\macadam" | |
| $macadam_path = Join-Path -Path $macadam_dir -ChildPath "macadam.exe" | |
| if (Test-Path $macadam_path) { | |
| $env:PATH += ";$macadam_dir" | |
| Write-Host "macadam.exe found in $macadam_dir and added to PATH." | |
| return | |
| } | |
| Write-Host "macadam.exe not found, downloading..." | |
| $version = "latest" | |
| $url = "https://github.com/crc-org/macadam/releases/download/${version}/macadam-windows-amd64.exe" | |
| Write-Host "Downloading from $url" | |
| if (-not (Test-Path $macadam_dir)) { | |
| New-Item -Path $macadam_dir -ItemType Directory | Out-Null | |
| } | |
| try { | |
| Invoke-WebRequest -Uri $url -OutFile $macadam_path | |
| } catch { | |
| die "Failed to download macadam.exe." | |
| } | |
| $env:PATH += ";$macadam_dir" | |
| Write-Host "macadam.exe downloaded to $macadam_dir and added to PATH." | |
| } | |
| # Starts the VM | |
| function Start-MacadamVM { | |
| Write-Host "--- Creating VM ---" | |
| $macadam_args = @( | |
| "init", | |
| "`"$DISK_IMAGE_PATH`"", | |
| "--disk-size", "31", | |
| "--memory", "11264", | |
| "--name", "crc-ng", | |
| "--username", "core", | |
| "--ssh-identity-path", "`"$PRIV_KEY_PATH`"", | |
| "--cpus", "6", | |
| "--cloud-init", "`"$USER_DATA_PATH`"", | |
| "--log-level", "debug" | |
| ) | |
| Invoke-Expression "macadam.exe $macadam_args" | |
| if (-not (macadam.exe start crc-ng --log-level debug)) { | |
| Write-Warning "Machine didn't come up in time" | |
| } | |
| } | |
| # Helper function to send HTTP requests over a Unix socket | |
| # Requires PowerShell 7+ and a version of Windows that supports Unix sockets (.NET Core feature) | |
| function Invoke-UnixSocketRequest { | |
| param( | |
| [string]$SocketPath, | |
| [string]$Method, | |
| [string]$Uri, | |
| [hashtable]$Headers, | |
| [string]$Body | |
| ) | |
| try { | |
| $endPoint = [System.Net.Sockets.UnixDomainSocketEndPoint]::new($SocketPath) | |
| $socket = [System.Net.Sockets.Socket]::new([System.Net.Sockets.AddressFamily]::Unix, [System.Net.Sockets.SocketType]::Stream, [System.Net.Sockets.ProtocolType]::Unspecified) | |
| $socket.Connect($endPoint) | |
| $requestLine = "$Method $Uri HTTP/1.1`r`n" | |
| $headerLines = $Headers.GetEnumerator() | ForEach-Object { "$($_.Name): $($_.Value)`r`n" } | |
| $requestString = "$requestLine$headerLines`r`n$Body" | |
| $requestBytes = [System.Text.Encoding]::UTF8.GetBytes($requestString) | |
| $socket.Send($requestBytes) | Out-Null | |
| # Simple response reading, might need to be more robust for large responses | |
| $buffer = New-Object byte[] 1024 | |
| $received = $socket.Receive($buffer) | |
| $responseText = [System.Text.Encoding]::UTF8.GetString($buffer, 0, $received) | |
| return $responseText | |
| } | |
| finally { | |
| if ($socket) { $socket.Dispose() } | |
| } | |
| } | |
| # Exposes a port from the VM to the host | |
| function Forward-PortGvproxy { | |
| Write-Host "--- Exposing port ---" | |
| $expose_req = '{"local":"127.0.0.1:6443","remote":"192.168.127.2:6443","protocol":"tcp"}' | |
| try { | |
| $headers = @{ | |
| 'Content-Type' = 'application/json' | |
| 'Host' = 'gvproxy' | |
| 'Content-Length' = $expose_req.Length | |
| } | |
| Invoke-UnixSocketRequest -SocketPath $GVPROXY_SOCKET_PATH -Method "POST" -Uri "/services/forwarder/expose" -Headers $headers -Body $expose_req | |
| } catch { | |
| die "Failed to forward API server port: $expose_req" | |
| } | |
| } | |
| # Add dns entries for API server and console | |
| function Add-ApiServerDns { | |
| Write-Host "--- Adding DNS entries for API Server ---" | |
| $gvproxy_proc = Get-Process gvproxy -ErrorAction SilentlyContinue | |
| if (-not $gvproxy_proc) { | |
| die "The gvproxy process is not running." | |
| } | |
| $commandLine = (Get-CimInstance Win32_Process -Filter "ProcessId = $($gvproxy_proc.Id)").CommandLine | |
| $match = [regex]::Match($commandLine, '-services unix:\/\/([^ ]*)') | |
| if (-not $match.Success) { | |
| die "Could not find the gvproxy socket path." | |
| } | |
| $script:GVPROXY_SOCKET_PATH = $match.Groups[1].Value | |
| $CRC_TESTING_ZONE = @' | |
| { | |
| "Name": "crc.testing.", | |
| "Records": [ | |
| {"Name": "host", "IP": "192.168.127.254"}, | |
| {"Name": "api", "IP": "192.168.127.2"}, | |
| {"Name": "api-int", "IP": "192.168.127.2"}, | |
| {"Name": "crc", "IP": "192.168.126.11"} | |
| ] | |
| } | |
| '@ | |
| $APPS_CRC_TESTING_ZONE = '{"Name": "apps-crc.testing.","DefaultIP": "192.168.127.2"}' | |
| $zones_json = @($CRC_TESTING_ZONE, $APPS_CRC_TESTING_ZONE) | |
| foreach ($zone_json in $zones_json) { | |
| try { | |
| $headers = @{ | |
| 'Content-Type' = 'application/json' | |
| 'Host' = 'gvproxy' | |
| 'Content-Length' = $zone_json.Length | |
| } | |
| Invoke-UnixSocketRequest -SocketPath $GVPROXY_SOCKET_PATH -Method "POST" -Uri "/services/dns/add" -Headers $headers -Body $zone_json | |
| } catch { | |
| die "Failed to add DNS zone: $zone_json" | |
| } | |
| } | |
| } | |
| # Waits for the VM to be ready and retrieves the kubeconfig | |
| function Get-Kubeconfig { | |
| Write-Host "--- Waiting for cluster and retrieving kubeconfig ---" | |
| Write-Host "Waiting 3mins for VM to start..." | |
| Start-Sleep -Seconds 180 | |
| $ssh_cmd = "macadam.exe ssh crc-ng" | |
| Write-Host "Waiting for SSH to be available..." | |
| while ($true) { | |
| try { | |
| Invoke-Expression "$ssh_cmd -- exit 0" | |
| break | |
| } catch { | |
| Start-Sleep -Seconds 5 | |
| Write-Host "Retrying SSH connection..." | |
| } | |
| } | |
| Write-Host "VM is running. Waiting for API server..." | |
| while ($true) { | |
| try { | |
| Invoke-Expression "$ssh_cmd -- 'sudo oc get node --kubeconfig /opt/crc/kubeconfig --context system:admin'" | |
| break | |
| } catch { | |
| Start-Sleep -Seconds 30 | |
| Write-Host "Waiting for certificate rotation and API server to be ready..." | |
| } | |
| } | |
| Write-Host "API server is up. Fetching kubeconfig." | |
| # scp.exe is expected to be in PATH on modern Windows | |
| $scp_cmd = "scp.exe -o UserKnownHostsFile=nul -o StrictHostKeyChecking=no -i $PRIV_KEY_PATH" | |
| Invoke-Expression "$scp_cmd core@127.0.0.1:/opt/kubeconfig ." | |
| oc.exe config set "clusters.api-${VM_NAME}-testing:6443.server" "https://127.0.0.1:6443" --kubeconfig .\kubeconfig | |
| oc.exe config set "clusters.crc.server" "https://127.0.0.1:6443" --kubeconfig .\kubeconfig | |
| Write-Host "kubeconfig retrieved and updated." | |
| } | |
| # Checks the status of the OpenShift cluster | |
| function Check-ClusterStatus { | |
| Write-Host "--- Checking cluster status ---" | |
| $env:KUBECONFIG = Join-Path -Path $SCRIPT_ROOT -ChildPath "kubeconfig" | |
| try { | |
| oc.exe adm wait-for-stable-cluster --minimum-stable-period=1m --timeout=10m | |
| } catch { | |
| die "Cluster didn't start successfully." | |
| } | |
| } | |
| # --- Main execution --- | |
| function main { | |
| try { | |
| Ensure-Deps | |
| Generate-SshKeyPair | |
| Load-Resources | |
| Gen-CloudInit | |
| Extract-DiskImage | |
| Ensure-MacadamExists | |
| Start-MacadamVM | |
| Add-ApiServerDns | |
| Forward-PortGvproxy | |
| Get-Kubeconfig | |
| if (-not (Check-ClusterStatus)) { | |
| die "Bundle failed to start correctly with macadam." | |
| } | |
| Write-Host "--- Bundle started successfully ---" | |
| } | |
| finally { | |
| cleanup | |
| } | |
| } | |
| main |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment