Skip to content

Instantly share code, notes, and snippets.

@anjannath
Created November 13, 2025 14:27
Show Gist options
  • Select an option

  • Save anjannath/617ac6acedd4382a8caeba8681943e9a to your computer and use it in GitHub Desktop.

Select an option

Save anjannath/617ac6acedd4382a8caeba8681943e9a to your computer and use it in GitHub Desktop.
#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