Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,13 @@ Organizers may override the ordered regions before creating lab
scopes; the shared preflight must pass for that subscription and region. Every participant
gets a dedicated two-node AKS cluster and a separate private K3s VM. The organizer
needs a **Linux runner**, PowerShell 7, current Az.Accounts/Az.Resources/Az.Network,
Azure CLI, a Bicep compiler usable by Az deployments, Bash, curl, tar, jq, awk,
sha256sum and standard Linux utilities. The explicit
Azure CLI, a Bicep compiler usable by Az deployments, Bash, curl, tar, awk,
sha256sum and standard Linux utilities. The PowerShell hook downloads pinned
**jq 1.8.2** for Linux x64/ARM64 into its private `$HOME/.local/bin`, checks its
SHA-256 against the pinned release hash and verifies the executable before ARM
provisioning. It requires neither a system jq installation nor sudo/package-manager
access. The private tool directory is placed first on PATH for every Bash phase.
The explicit
downloaded `resources/install-console-tools.sh` installs the versioned client toolchain
and Bastion extension before any bootstrap phase. It does not authenticate.
Outbound access to Azure, GitHub, Kubernetes/Helm/K3s release endpoints and
Expand All @@ -34,6 +39,11 @@ only the required files from `microsoft/MicroHack`, using the immutable commit
and per-file SHA-256 hashes in `bootstrap-source.json`. No Git installation or
GitHub credential is required.

The jq bootstrap lives in the automation folder, so it also supplies the jq
prerequisite of the already-pinned Bash installer without modifying downloaded
sources or requiring a new `bootstrap-source.json` pin. A failed jq download,
hash or executable check stops before Azure provisioning and cleans up scratch.

Successful provisioning prepares **platform state through challenge 05**.
Participants still complete **challenge 01's workstation setup** before connecting:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,43 @@ function Invoke-AdaptiveBash {
}
}

function Install-AdaptiveJq {
param([Parameter(Mandatory)][string]$BinDirectory)
if (-not $IsLinux) { throw 'The Console jq bootstrap requires Linux.' }
$version = '1.8.2'
switch ([Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture.ToString()) {
'X64' {
$asset = 'jq-linux-amd64'
$expectedHash = 'b1c22172dd303f3be49e935aa56aa48a8b7a46e0bc838b4997d3bb451495870f'
}
'Arm64' {
$asset = 'jq-linux-arm64'
$expectedHash = '8b85c817833814ddca00a144c33705546355afccf0cf39b188f3cdb48b852309'
}
default { throw "Unsupported jq bootstrap architecture: $([Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)." }
}
New-Item -ItemType Directory -Path $BinDirectory -Force -ErrorAction Stop | Out-Null
$download = Join-Path $BinDirectory ".jq-$([guid]::NewGuid().ToString('N'))"
$target = Join-Path $BinDirectory 'jq'
try {
Invoke-WebRequest -Uri "https://github.com/jqlang/jq/releases/download/jq-$version/$asset" `
-OutFile $download -TimeoutSec 60 -MaximumRetryCount 3 -RetryIntervalSec 2 -ErrorAction Stop
if ((Get-FileHash -LiteralPath $download -Algorithm SHA256 -ErrorAction Stop).Hash -ne $expectedHash) {
throw "SHA-256 verification failed for $asset (jq $version). The downloaded file will not run."
}
& chmod 700 $download
if ($LASTEXITCODE -ne 0) { throw 'Unable to make the private jq binary executable.' }
$installedVersion = & $download --version
if ($LASTEXITCODE -ne 0 -or $installedVersion -cne "jq-$version") {
throw "The verified jq $version binary failed its version check."
}
Move-Item -LiteralPath $download -Destination $target -Force -ErrorAction Stop
Write-Host "Installed verified jq $version in the private Console tool directory."
} finally {
if (Test-Path -LiteralPath $download) { Remove-Item -LiteralPath $download -Force -ErrorAction Stop }
}
}

function Get-AdaptiveBootstrapFiles {
@(
'resources/install-console-tools.sh', 'resources/bootstrap-console.sh',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ try {
if ($LASTEXITCODE -ne 0) { throw 'Unable to secure isolated Console HOME.' }
New-Item -ItemType Directory -Path $workingRoot | Out-Null
Receive-AdaptiveBootstrap -Manifest $sourceManifest -Destination $workingRoot
Install-AdaptiveJq -BinDirectory (Join-Path $isolatedHome '.local/bin')

Update-MhhToken | Out-Null
Write-Host 'Validating participant Azure contexts and shared region metadata...'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "https://raw.githubusercontent.com/microsoft/MicroHack/refs/heads/main/lab-defaults-schema.json",
"deploymentType": "resourcegroup",
"labsPerSubscription": 5,
"preferredLocation": "swedencentral,spaincentral",
"preferredLocation": "spaincentral,swedencentral",
"estimatedDailyCostsUsd": 35.0,
"estimatedSharedDeploymentDailyCostsUsd": 0.0
}
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,56 @@ Describe 'Shared hook and hosted policy verification' {
}
}

Describe 'Private jq bootstrap' {
BeforeEach {
$script:jqBin = Join-Path $TestDrive 'bin'
$script:jqFault = ''
$script:jqAsset = if ([Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture -eq 'Arm64') { 'jq-linux-arm64' } else { 'jq-linux-amd64' }
$script:jqHash = if ($script:jqAsset -eq 'jq-linux-arm64') {
'8b85c817833814ddca00a144c33705546355afccf0cf39b188f3cdb48b852309'
} else {
'b1c22172dd303f3be49e935aa56aa48a8b7a46e0bc838b4997d3bb451495870f'
}
Mock Invoke-WebRequest {
$Uri | Should -Be "https://github.com/jqlang/jq/releases/download/jq-1.8.2/$script:jqAsset"
$MaximumRetryCount | Should -Be 3
$RetryIntervalSec | Should -Be 2
$timeout = if ($null -ne $TimeoutSec) { $TimeoutSec } else { $ConnectionTimeoutSeconds }
$timeout | Should -Be 60
$version = if ($script:jqFault -eq 'version') { 'wrong-version' } else { 'jq-1.8.2' }
Set-Content -LiteralPath $OutFile -Encoding utf8 -Value @('#!/bin/sh', "echo $version")
if ($script:jqFault -eq 'download') { throw 'Simulated jq download failure' }
}
Mock Get-FileHash {
$Algorithm | Should -Be 'SHA256'
@{ Hash = if ($script:jqFault -eq 'hash') { '0' * 64 } else { $script:jqHash } }
}
}
AfterEach {
@(Get-ChildItem -LiteralPath $script:jqBin -Force | Where-Object Name -like '.jq-*') | Should -HaveCount 0
}
It 'installs the verified executable into the private directory' {
Install-AdaptiveJq -BinDirectory $script:jqBin
$target = Join-Path $script:jqBin 'jq'
& $target --version | Should -Be 'jq-1.8.2'
[IO.File]::GetUnixFileMode($target) |
Should -Be ([IO.UnixFileMode]::UserRead -bor [IO.UnixFileMode]::UserWrite -bor [IO.UnixFileMode]::UserExecute)
Should -Invoke Invoke-WebRequest -Times 1 -Exactly
}
It 'rejects <Fault> failure and keeps an existing executable untouched' -ForEach @(
@{ Fault = 'download'; Error = '*Simulated jq download failure*' }
@{ Fault = 'hash'; Error = '*SHA-256 verification failed*' }
@{ Fault = 'version'; Error = '*failed its version check*' }
) {
New-Item -ItemType Directory -Path $script:jqBin -Force | Out-Null
$target = Join-Path $script:jqBin 'jq'
Set-Content -LiteralPath $target -Value 'Existing executable'
$script:jqFault = $Fault
{ Install-AdaptiveJq -BinDirectory $script:jqBin } | Should -Throw $Error
(Get-Content -LiteralPath $target -Raw).Trim() | Should -Be 'Existing executable'
}
}

Describe 'Participant hook orchestration' {
BeforeEach {
$script:calls = [Collections.Generic.List[string]]::new()
Expand All @@ -211,6 +261,7 @@ Describe 'Participant hook orchestration' {
$script:failedDownload = $false
$script:tamperedDownload = $false
$script:failedPhase = ''
$script:jqInstalled = $false
$script:location = 'westeurope'
$script:groupTags = @{ adaptiveAppsReady = 'true' }
$script:fallbackLocation = 'northeurope'
Expand Down Expand Up @@ -241,6 +292,10 @@ Describe 'Participant hook orchestration' {
if ($script:tamperedDownload) { Set-Content -LiteralPath $OutFile -Value 'Unexpected content' }
}
Mock Update-MhhToken { $script:calls.Add('refresh') }
Mock Install-AdaptiveJq {
$BinDirectory | Should -Be (Join-Path $script:observedHome '.local/bin')
$script:jqInstalled = $true
}
Mock Get-AzContext { @{ Subscription = @{ Id = $subscription } } }
Mock Get-AzResourceGroup {
@{ Location = $script:location; ResourceId = "/subscriptions/$subscription/resourceGroups/lab-one"; Tags = $script:groupTags }
Expand All @@ -264,6 +319,7 @@ Describe 'Participant hook orchestration' {
}
Mock New-MhhStablePassword { 'TestOnlyStablePassword123' }
Mock New-AzResourceGroupDeployment {
$script:jqInstalled | Should -BeTrue
$Mode | Should -Be 'Incremental'
$TemplateParameterObject | Should -BeNullOrEmpty
$TemplateParameterFile | Should -Not -BeNullOrEmpty
Expand Down Expand Up @@ -337,6 +393,7 @@ Describe 'Participant hook orchestration' {
}
}
Mock bash {
$script:jqInstalled | Should -BeTrue
Test-Path -LiteralPath $script:observedParameterFile | Should -BeFalse
$script:calls[-1] | Should -Be 'refresh'
$script:calls.Add("bash $args")
Expand Down Expand Up @@ -385,6 +442,16 @@ Describe 'Participant hook orchestration' {
$result.HackboxCredential.name | Should -Contain 'Adaptive Apps Connect'
($result | ConvertTo-Json -Depth 5) | Should -Not -Match 'TestOnlyStablePassword|client-key-data|token:'
Should -Invoke Invoke-WebRequest -Times 20 -Exactly
Should -Invoke Install-AdaptiveJq -Times 1 -Exactly
}
It 'stops before Azure provisioning and Bash when jq bootstrap fails' {
Mock Install-AdaptiveJq { throw 'Simulated jq installation failure' }
{ . (Join-Path $automation 'deploy-lab.ps1') -DeploymentType resourcegroup -SubscriptionId $subscription -ResourceGroupName lab-one -AllowedEntraUserIds @('user') } |
Should -Throw '*Simulated jq installation failure*'
Should -Invoke Update-MhhToken -Times 0
Should -Invoke New-AzResourceGroupDeployment -Times 0
Should -Invoke Invoke-MhhDeploymentWithRegionFallback -Times 0
Should -Invoke bash -Times 0
}
It 'does not mark ready or emit credentials when Bash fails and still removes its HOME' {
$script:failedPhase = 'recipes'
Expand Down
Loading