diff --git a/aksarc_jumpstart/deployaksarc.ps1 b/aksarc_jumpstart/deployaksarc.ps1 index 9c897bff..120ffe99 100644 --- a/aksarc_jumpstart/deployaksarc.ps1 +++ b/aksarc_jumpstart/deployaksarc.ps1 @@ -25,6 +25,10 @@ param ( [string] $workingDir ) +# Initialize execution status tracking via shared helpers +. "$PSScriptRoot/status-reporting.ps1" +Initialize-ExecutionStatus -ScriptName 'deployaksarc.ps1' + if ([string]::IsNullOrEmpty($workingDir)) { $workingDir = "E:\AKSArc" } @@ -64,23 +68,18 @@ $scriptToExecute = [ordered] @{ foreach ($script in $scriptToExecute.GetEnumerator()) { $scriptUrl = $script.Key $scriptName = $script.Value - - $deploymentName = "executescript-$($vmName)-$($scriptName.Split(" ")[0].Replace('.ps1',''))" - $commandToExecute = "powershell.exe -ExecutionPolicy Unrestricted -File $scriptName" - Write-Host "Executing $commandToExecute from $scriptUrl on VM $vmName ..." - try { - az deployment group create --name $deploymentName --resource-group $GroupName --template-file ./configuration/executescript-template.json --parameters location=$Location vmName=$vmName scriptFileUri=$scriptUrl commandToExecute=$commandToExecute # --debug - if ($LASTEXITCODE -ne 0) { - Write-Host "Azure CLI command failed with exit code $LASTEXITCODE" - throw "Failed to execute script $scriptName on VM $vmName. Exit code: $LASTEXITCODE" - } - } - catch { - Write-Error "An error occurred during AKS Arc cluster deployment: $_" - Write-Error "Exception details: $($_.Exception.Message)" - Write-Error "Stack trace: $($_.ScriptStackTrace)" - throw - } + $scriptBaseName = $scriptName.Split(" ")[0] + Invoke-VmScriptDeployment ` + -StepName "ExecuteScript_$scriptBaseName" ` + -ResourceGroup $GroupName ` + -Location $Location ` + -VmName $vmName ` + -ScriptFileUri $scriptUrl ` + -InnerScript $scriptName } -Write-Host "Setup is ready for AKS Arc deployment" \ No newline at end of file +Write-Host "Setup is ready for AKS Arc deployment" + +# Final execution status - Success +$script:ExecutionStatus.Status = "Success" +Write-ExecutionStatus \ No newline at end of file diff --git a/aksarc_jumpstart/deployaksarc.sh b/aksarc_jumpstart/deployaksarc.sh index d515dc33..ade413d4 100755 --- a/aksarc_jumpstart/deployaksarc.sh +++ b/aksarc_jumpstart/deployaksarc.sh @@ -15,6 +15,11 @@ VM_NAME="jumpstartVM" SUBNET_NAME="jumpstartSubnet" WORKING_DIR="E:\\AKSArc" +# Source shared status reporting helpers (also handles ARM extension failure +# detection via post-deployment instance view inspection). +source "$(dirname "$0")/status-reporting.sh" +init_execution_status "deployaksarc.sh" + # Function to print usage usage() { echo "Usage: $0 [OPTIONS]" @@ -168,11 +173,8 @@ log " Subscription ID: $SUBSCRIPTION_ID" # Set the subscription context log "Setting Azure subscription context..." -az account set --subscription "$SUBSCRIPTION_ID" -if [[ $? -ne 0 ]]; then - echo "Error: Failed to set subscription context" - exit 1 -fi +az account set --subscription "$SUBSCRIPTION_ID" \ + || handle_error "SetSubscription" "Failed to set subscription context to '$SUBSCRIPTION_ID'" $? # Get git repository information log "Getting git repository information..." @@ -185,44 +187,27 @@ log "Script location: $SCRIPT_LOCATION" # Check for required template file EXEC_TEMPLATE="./configuration/executescript-template.json" if [[ ! -f "$EXEC_TEMPLATE" ]]; then - echo "Error: Execute script template not found: $EXEC_TEMPLATE" - echo "Please ensure all required ARM templates are present in the configuration directory" - exit 1 + handle_error "PreflightChecks" "Execute script template not found: $EXEC_TEMPLATE -- ensure all required ARM templates are present in the configuration directory" fi # Execute deployment scripts on VM in sequence log "Executing AKS Arc deployment scripts on VM..." -# Define script execution order and details +# Wrapper that combines script_name + script_params into the InnerScript form +# expected by invoke_vm_script_deployment. execute_script() { local script_name="$1" local script_params="$2" local script_url="${SCRIPT_LOCATION}/${script_name}" - local deployment_name="executescript-${VM_NAME}-${script_name%.*}" - local command_to_execute="powershell.exe -ExecutionPolicy Unrestricted -File ${script_name} ${script_params}" - - log "Executing ${script_name%.*} from $script_url on VM $VM_NAME..." - - az deployment group create \ - --name "$deployment_name" \ - --resource-group "$GROUP_NAME" \ - --template-file "$EXEC_TEMPLATE" \ - --parameters \ - location="$LOCATION" \ - vmName="$VM_NAME" \ - scriptFileUri="$script_url" \ - commandToExecute="$command_to_execute" - - if [[ $? -ne 0 ]]; then - echo "Error: Failed to execute script ${script_name%.*}" - echo "This may be due to:" - echo " - MOC not being properly installed" - echo " - Network connectivity issues" - echo " - Azure resource quota limitations" - echo " - Previous deployment steps not completed" - exit 1 - fi - + local inner_script="${script_name} ${script_params}" + invoke_vm_script_deployment \ + "ExecuteScript_${script_name%.*}" \ + "$GROUP_NAME" \ + "$LOCATION" \ + "$VM_NAME" \ + "$script_url" \ + "$inner_script" \ + "$EXEC_TEMPLATE" log "Successfully completed: ${script_name%.*}" } @@ -262,3 +247,7 @@ log "" log "4. Connect to the cluster using kubectl" log "" log "Setup is ready for AKS Arc workload deployment!" + +# Final execution status - Success +EXECUTION_STATUS="Success" +print_execution_status diff --git a/aksarc_jumpstart/jumpstart.ps1 b/aksarc_jumpstart/jumpstart.ps1 index 980fd958..9b1cab33 100644 --- a/aksarc_jumpstart/jumpstart.ps1 +++ b/aksarc_jumpstart/jumpstart.ps1 @@ -26,38 +26,55 @@ param ( $subscriptionId ) +. "$PSScriptRoot/status-reporting.ps1" +Initialize-ExecutionStatus -ScriptName 'jumpstart.ps1' + # Create Resource Group -az group create --name $GroupName --location $Location +az group create --name $GroupName --location $Location if ($LASTEXITCODE -ne 0) { - Write-Host "Azure CLI command failed with exit code $LASTEXITCODE" - exit $LASTEXITCODE + Invoke-StepFailure -StepName "CreateResourceGroup" -ExitCode $LASTEXITCODE ` + -ErrorText "Failed to create resource group '$GroupName' in location '$Location'. Azure CLI command failed with exit code $LASTEXITCODE" } +Add-CompletedStep -StepName "CreateResourceGroup" # Create Vnet and VM az deployment group create --resource-group $GroupName --template-file ./configuration/vnet-template.json --parameters vnetName=$vnetName location=$Location subnetName=$subnetName if ($LASTEXITCODE -ne 0) { - Write-Host "Azure CLI command failed with exit code $LASTEXITCODE" - exit $LASTEXITCODE + Invoke-StepFailure -StepName "CreateVirtualNetwork" -ExitCode $LASTEXITCODE ` + -ErrorText "Failed to create virtual network '$vnetName' and subnet '$subnetName'. Azure CLI command failed with exit code $LASTEXITCODE" } +Add-CompletedStep -StepName "CreateVirtualNetwork" az deployment group create --resource-group $GroupName --template-file ./configuration/vm-template.json --parameters adminUsername=$userName adminPassword=$password vmName=$vmName location=$Location vnetName=$vnetName vmSize="Standard_E16s_v4" subnetName=$subnetName if ($LASTEXITCODE -ne 0) { - Write-Host "Azure CLI command failed with exit code $LASTEXITCODE" - exit $LASTEXITCODE + Invoke-StepFailure -StepName "CreateVirtualMachine" -ExitCode $LASTEXITCODE ` + -ErrorText "Failed to create virtual machine '$vmName'. Azure CLI command failed with exit code $LASTEXITCODE" } +Add-CompletedStep -StepName "CreateVirtualMachine" # Assign Managed Identity and Contributor Role to VM az vm identity assign --resource-group $GroupName --name $vmName if ($LASTEXITCODE -ne 0) { - Write-Host "Azure CLI command failed with exit code $LASTEXITCODE" - exit $LASTEXITCODE + Invoke-StepFailure -StepName "AssignManagedIdentity" -ExitCode $LASTEXITCODE ` + -ErrorText "Failed to assign managed identity to VM '$vmName'. Azure CLI command failed with exit code $LASTEXITCODE" } +Add-CompletedStep -StepName "AssignManagedIdentity" $principalId = az vm show --resource-group $GroupName --name $vmName --query identity.principalId -o tsv az role assignment create --assignee $principalId --role Contributor --scope /subscriptions/$subscriptionId +if ($LASTEXITCODE -ne 0) { + Invoke-StepFailure -StepName "AssignContributorRole" -ExitCode $LASTEXITCODE ` + -ErrorText "Failed to assign Contributor role to VM identity. Azure CLI command failed with exit code $LASTEXITCODE" +} +Add-CompletedStep -StepName "AssignContributorRole" #az deployment group create --resource-group $GroupName --template-file a4s-template.json --parameters location=$Location vmName=$vmName arcResourceGroup=$GroupName subscriptionId=$subscriptionId tenantId=$tenantId # Enable Nested Virtualization az vm update --resource-group $GroupName --name $vmName --set additionalCapabilities.enableNestedVirtualization=true +if ($LASTEXITCODE -ne 0) { + Invoke-StepFailure -StepName "EnableNestedVirtualization" -ExitCode $LASTEXITCODE ` + -ErrorText "Failed to enable nested virtualization on VM '$vmName'. Azure CLI command failed with exit code $LASTEXITCODE" +} +Add-CompletedStep -StepName "EnableNestedVirtualization" $gitSource = (git config --get remote.origin.url).Replace("github.com", "raw.githubusercontent.com").Replace("aksArc.git", "aksArc") $branch = (git branch --show-current) @@ -74,18 +91,17 @@ $scriptToExecute = [ordered] @{ foreach ($script in $scriptToExecute.GetEnumerator()) { $scriptUrl = $script.Key $scriptName = $script.Value - $deploymentName = "executescript-$($vmName)-$($scriptName.Replace('.ps1',''))" - $commandToExecute = "powershell.exe -ExecutionPolicy Unrestricted -File $scriptName" - Write-Host "Executing $scriptName from $scriptUrl on VM $vmName ..." - try { - az deployment group create --name $deploymentName --resource-group $GroupName --template-file ./configuration/executescript-template.json --parameters location=$Location vmName=$vmName scriptFileUri=$scriptUrl commandToExecute=$commandToExecute - } - catch { - Write-Error "An error occurred during AKS Arc cluster deployment: $_" - Write-Error "Exception details: $($_.Exception.Message)" - Write-Error "Stack trace: $($_.ScriptStackTrace)" - throw - } + Invoke-VmScriptDeployment ` + -StepName "ExecuteScript_$scriptName" ` + -ResourceGroup $GroupName ` + -Location $Location ` + -VmName $vmName ` + -ScriptFileUri $scriptUrl ` + -InnerScript $scriptName } -Write-Host "Login to the VM using Bastion or RDP. Wait for MOC install to finish. Then continue with aksarc deployment by running the script deployaksarc.ps1." \ No newline at end of file +Write-Host "Login to the VM using Bastion or RDP. Wait for MOC install to finish. Then continue with aksarc deployment by running the script deployaksarc.ps1." + +# Final execution status - Success +$script:ExecutionStatus.Status = "Success" +Write-ExecutionStatus \ No newline at end of file diff --git a/aksarc_jumpstart/jumpstart.sh b/aksarc_jumpstart/jumpstart.sh index bb63f362..b916d782 100755 --- a/aksarc_jumpstart/jumpstart.sh +++ b/aksarc_jumpstart/jumpstart.sh @@ -15,6 +15,11 @@ SUBNET_NAME="jumpstartSubnet" # Valid locations VALID_LOCATIONS=("eastus" "australiaeast") +# Source shared status reporting helpers (also handles ARM extension failure +# detection via post-deployment instance view inspection). +source "$(dirname "$0")/status-reporting.sh" +init_execution_status "jumpstart.sh" + # Function to print usage usage() { echo "Usage: $0 [OPTIONS]" @@ -158,19 +163,14 @@ log " Subscription ID: $SUBSCRIPTION_ID" # Set the subscription context log "Setting Azure subscription context..." -az account set --subscription "$SUBSCRIPTION_ID" -if [[ $? -ne 0 ]]; then - echo "Error: Failed to set subscription context" - exit 1 -fi +az account set --subscription "$SUBSCRIPTION_ID" \ + || handle_error "SetSubscription" "Failed to set subscription context to '$SUBSCRIPTION_ID'" $? # Create Resource Group log "Creating resource group '$GROUP_NAME' in '$LOCATION'..." -az group create --name "$GROUP_NAME" --location "$LOCATION" -if [[ $? -ne 0 ]]; then - echo "Error: Failed to create resource group" - exit 1 -fi +az group create --name "$GROUP_NAME" --location "$LOCATION" \ + || handle_error "CreateResourceGroup" "Failed to create resource group '$GROUP_NAME' in location '$LOCATION'" $? +add_completed_step "CreateResourceGroup" # Check for required template files VNET_TEMPLATE="./configuration/vnet-template.json" @@ -178,21 +178,15 @@ VM_TEMPLATE="./configuration/vm-template.json" EXEC_TEMPLATE="./configuration/executescript-template.json" if [[ ! -f "$VNET_TEMPLATE" ]]; then - echo "Error: VNet template not found: $VNET_TEMPLATE" - echo "Please ensure all required ARM templates are present in the configuration directory" - exit 1 + handle_error "PreflightChecks" "VNet template not found: $VNET_TEMPLATE -- ensure all required ARM templates are present in the configuration directory" fi if [[ ! -f "$VM_TEMPLATE" ]]; then - echo "Error: VM template not found: $VM_TEMPLATE" - echo "Please ensure all required ARM templates are present in the configuration directory" - exit 1 + handle_error "PreflightChecks" "VM template not found: $VM_TEMPLATE -- ensure all required ARM templates are present in the configuration directory" fi if [[ ! -f "$EXEC_TEMPLATE" ]]; then - echo "Error: Execute script template not found: $EXEC_TEMPLATE" - echo "Please ensure all required ARM templates are present in the configuration directory" - exit 1 + handle_error "PreflightChecks" "Execute script template not found: $EXEC_TEMPLATE -- ensure all required ARM templates are present in the configuration directory" fi # Create Virtual Network and Subnet @@ -201,11 +195,9 @@ az deployment group create \ --name "vnet-deployment-$(date +%s)" \ --resource-group "$GROUP_NAME" \ --template-file "$VNET_TEMPLATE" \ - --parameters vnetName="$VNET_NAME" location="$LOCATION" subnetName="$SUBNET_NAME" -if [[ $? -ne 0 ]]; then - echo "Error: Failed to create virtual network" - exit 1 -fi + --parameters vnetName="$VNET_NAME" location="$LOCATION" subnetName="$SUBNET_NAME" \ + || handle_error "CreateVirtualNetwork" "Failed to create virtual network '$VNET_NAME' and subnet '$SUBNET_NAME'" $? +add_completed_step "CreateVirtualNetwork" # Create Virtual Machine log "Creating virtual machine..." @@ -220,47 +212,39 @@ az deployment group create \ location="$LOCATION" \ vnetName="$VNET_NAME" \ vmSize="Standard_E16s_v4" \ - subnetName="$SUBNET_NAME" -if [[ $? -ne 0 ]]; then - echo "Error: Failed to create virtual machine" - exit 1 -fi + subnetName="$SUBNET_NAME" \ + || handle_error "CreateVirtualMachine" "Failed to create virtual machine '$VM_NAME'" $? +add_completed_step "CreateVirtualMachine" # Assign Managed Identity and Contributor Role to VM log "Assigning managed identity to VM..." -az vm identity assign --resource-group "$GROUP_NAME" --name "$VM_NAME" -if [[ $? -ne 0 ]]; then - echo "Error: Failed to assign managed identity" - exit 1 -fi +az vm identity assign --resource-group "$GROUP_NAME" --name "$VM_NAME" \ + || handle_error "AssignManagedIdentity" "Failed to assign managed identity to VM '$VM_NAME'" $? +add_completed_step "AssignManagedIdentity" log "Getting VM principal ID..." -PRINCIPAL_ID=$(az vm show --resource-group "$GROUP_NAME" --name "$VM_NAME" --query identity.principalId -o tsv) +PRINCIPAL_ID=$(az vm show --resource-group "$GROUP_NAME" --name "$VM_NAME" --query identity.principalId -o tsv) \ + || handle_error "GetPrincipalId" "Failed to get VM principal ID for '$VM_NAME'" $? if [[ -z "$PRINCIPAL_ID" ]]; then - echo "Error: Failed to get VM principal ID" - exit 1 + handle_error "GetPrincipalId" "Got empty VM principal ID for '$VM_NAME'" fi log "Assigning Contributor role to VM identity..." az role assignment create \ --assignee "$PRINCIPAL_ID" \ --role Contributor \ - --scope "/subscriptions/$SUBSCRIPTION_ID" -if [[ $? -ne 0 ]]; then - echo "Error: Failed to assign Contributor role" - exit 1 -fi + --scope "/subscriptions/$SUBSCRIPTION_ID" \ + || handle_error "AssignContributorRole" "Failed to assign Contributor role to VM identity" $? +add_completed_step "AssignContributorRole" # Enable Nested Virtualization log "Enabling nested virtualization on VM..." az vm update \ --resource-group "$GROUP_NAME" \ --name "$VM_NAME" \ - --set "additionalCapabilities.enableNestedVirtualization=true" -if [[ $? -ne 0 ]]; then - echo "Error: Failed to enable nested virtualization" - exit 1 -fi + --set "additionalCapabilities.enableNestedVirtualization=true" \ + || handle_error "EnableNestedVirtualization" "Failed to enable nested virtualization on VM '$VM_NAME'" $? +add_completed_step "EnableNestedVirtualization" # Get git repository information log "Getting git repository information..." @@ -283,25 +267,14 @@ SCRIPTS_TO_EXECUTE=( log "Executing initialization scripts on VM..." for script_name in "${SCRIPTS_TO_EXECUTE[@]}"; do script_url="$SCRIPT_LOCATION/$script_name" - deployment_name="executescript-${VM_NAME}-${script_name%.*}" - command_to_execute="powershell.exe -ExecutionPolicy Unrestricted -File $script_name" - - log "Executing $script_name from $script_url on VM $VM_NAME..." - - az deployment group create \ - --name "$deployment_name" \ - --resource-group "$GROUP_NAME" \ - --template-file "$EXEC_TEMPLATE" \ - --parameters \ - location="$LOCATION" \ - vmName="$VM_NAME" \ - scriptFileUri="$script_url" \ - commandToExecute="$command_to_execute" - - if [[ $? -ne 0 ]]; then - echo "Error: Failed to execute script $script_name" - exit 1 - fi + invoke_vm_script_deployment \ + "ExecuteScript_${script_name}" \ + "$GROUP_NAME" \ + "$LOCATION" \ + "$VM_NAME" \ + "$script_url" \ + "$script_name" \ + "$EXEC_TEMPLATE" done log "Jump start deployment completed successfully!" @@ -316,3 +289,7 @@ log "VM Connection Details:" log " Resource Group: $GROUP_NAME" log " VM Name: $VM_NAME" log " Location: $LOCATION" + +# Final execution status - Success +EXECUTION_STATUS="Success" +print_execution_status diff --git a/aksarc_jumpstart/status-reporting.ps1 b/aksarc_jumpstart/status-reporting.ps1 new file mode 100644 index 00000000..a7ed98ee --- /dev/null +++ b/aksarc_jumpstart/status-reporting.ps1 @@ -0,0 +1,153 @@ +# Shared status reporting and ARM-deployment error surfacing helpers. +# +# Dot-source from a calling script: +# . "$PSScriptRoot/status-reporting.ps1" +# Initialize-ExecutionStatus -ScriptName 'jumpstart.ps1' +# +# After Initialize-ExecutionStatus, the caller has access to: +# Write-ExecutionStatus +# Invoke-StepFailure -StepName ... -ErrorText ... +# Invoke-VmScriptDeployment -StepName ... -ResourceGroup ... -VmName ... +# -ScriptFileUri ... -InnerScript ... +# +# These helpers update a script-scoped $script:ExecutionStatus hashtable. + +function Initialize-ExecutionStatus { + param( + [Parameter(Mandatory = $true)] [string] $ScriptName + ) + $script:ExecutionStatus = @{ + Status = "InProgress" + Script = $ScriptName + StartTime = Get-Date -Format "yyyy-MM-dd HH:mm:ss" + CompletedSteps = @() + FailedStep = $null + ErrorMessage = "" + ExitCode = 0 + } +} + +function Write-ExecutionStatus { + $script:ExecutionStatus.EndTime = Get-Date -Format "yyyy-MM-dd HH:mm:ss" + Write-Host "`n===== EXECUTION STATUS =====" + Write-Host "Status: $($script:ExecutionStatus.Status)" + if ($script:ExecutionStatus.Status -eq "Failure") { + Write-Host "Failed Step: $($script:ExecutionStatus.FailedStep)" + Write-Host "Error Message: $($script:ExecutionStatus.ErrorMessage)" + } + Write-Host "Exit Code: $($script:ExecutionStatus.ExitCode)" + Write-Host "Completed Steps: $($script:ExecutionStatus.CompletedSteps -join ', ')" + Write-Host "Start Time: $($script:ExecutionStatus.StartTime)" + Write-Host "End Time: $($script:ExecutionStatus.EndTime)" + Write-Host "============================" +} + +function Invoke-StepFailure { + param( + [Parameter(Mandatory = $true)] [string] $StepName, + [Parameter(Mandatory = $true)] [string] $ErrorText, + [Parameter()] [int] $ExitCode = 1 + ) + $script:ExecutionStatus.Status = "Failure" + $script:ExecutionStatus.FailedStep = $StepName + $script:ExecutionStatus.ErrorMessage = $ErrorText + $script:ExecutionStatus.ExitCode = $ExitCode + Write-ExecutionStatus + exit $ExitCode +} + +function Add-CompletedStep { + param([Parameter(Mandatory = $true)] [string] $StepName) + $script:ExecutionStatus.CompletedSteps += $StepName +} + +# Deploy executescript-template.json and treat ANY of these as failure: +# 1. az deployment group create returned non-zero +# 2. Extension instance view shows ProvisioningState != succeeded +# 3. (Soft) Extension StdErr substatus is non-empty -> warn but don't fail +# +# On failure, surfaces ProvisioningState message + StdOut + StdErr in the +# error text instead of just "az command failed with exit code N". +# +# `commandToExecute` is constructed identically to the pre-existing code path +# (`powershell.exe -ExecutionPolicy Unrestricted -File `) so this +# change does not alter what runs inside the VM -- it only adds a post-deploy +# inspection of the extension to catch failures the deployment exit code +# missed (the actual bug we are fixing). +function Invoke-VmScriptDeployment { + param( + [Parameter(Mandatory = $true)] [string] $StepName, + [Parameter(Mandatory = $true)] [string] $ResourceGroup, + [Parameter(Mandatory = $true)] [string] $Location, + [Parameter(Mandatory = $true)] [string] $VmName, + [Parameter(Mandatory = $true)] [string] $ScriptFileUri, + [Parameter(Mandatory = $true)] [string] $InnerScript, + [Parameter()] [string] $TemplateFile = "./configuration/executescript-template.json", + [Parameter()] [string] $DeploymentName + ) + + if ([string]::IsNullOrEmpty($DeploymentName)) { + $base = ($InnerScript -split ' ')[0] + $DeploymentName = "executescript-$VmName-$($base -replace '\.ps1$','')" + } + + $commandToExecute = "powershell.exe -ExecutionPolicy Unrestricted -File $InnerScript" + Write-Host "Executing $commandToExecute from $ScriptFileUri on VM $VmName ..." + + az deployment group create ` + --name $DeploymentName ` + --resource-group $ResourceGroup ` + --template-file $TemplateFile ` + --parameters location=$Location vmName=$VmName scriptFileUri=$ScriptFileUri commandToExecute=$commandToExecute + $deploymentExitCode = $LASTEXITCODE + + # Always inspect the extension instance view for ground truth. + $instanceViewJson = az vm extension show ` + --resource-group $ResourceGroup ` + --vm-name $VmName ` + --name CustomScriptExtension ` + --instance-view ` + --query instanceView ` + -o json 2>$null + $extLookupExitCode = $LASTEXITCODE + + $provisioningState = $null + $statusMessage = "" + $stdOut = "" + $stdErr = "" + if ($extLookupExitCode -eq 0 -and -not [string]::IsNullOrWhiteSpace($instanceViewJson)) { + try { + $iv = $instanceViewJson | ConvertFrom-Json -ErrorAction Stop + $provStatus = $iv.statuses | Where-Object { $_.code -like 'ProvisioningState/*' } | Select-Object -First 1 + if ($provStatus) { + $provisioningState = $provStatus.code + $statusMessage = $provStatus.message + } + $stdOutSub = $iv.substatuses | Where-Object { $_.code -like 'ComponentStatus/StdOut/*' } | Select-Object -First 1 + if ($stdOutSub) { $stdOut = $stdOutSub.message } + $stdErrSub = $iv.substatuses | Where-Object { $_.code -like 'ComponentStatus/StdErr/*' } | Select-Object -First 1 + if ($stdErrSub) { $stdErr = $stdErrSub.message } + } + catch { + Write-Host "WARNING: could not parse extension instance view: $_" + } + } + + $extensionFailed = ($null -ne $provisioningState) -and ($provisioningState -notlike '*succeeded') + $hasStdErr = -not [string]::IsNullOrWhiteSpace($stdErr) + + if ($deploymentExitCode -ne 0 -or $extensionFailed) { + $failExitCode = if ($deploymentExitCode -ne 0) { $deploymentExitCode } else { 1 } + $errorText = "Failed to execute script '$InnerScript' on VM '$VmName' (step '$StepName'). " + + "DeploymentExitCode=$deploymentExitCode; ProvisioningState=$provisioningState; " + + "StatusMessage=$statusMessage; StdErr=$stdErr; StdOut=$stdOut" + Invoke-StepFailure -StepName $StepName -ExitCode $failExitCode -ErrorText $errorText + } + + if ($hasStdErr) { + Write-Host "WARNING: step '$StepName' completed but extension StdErr was non-empty:" + Write-Host $stdErr + } + + Add-CompletedStep -StepName $StepName +} diff --git a/aksarc_jumpstart/status-reporting.sh b/aksarc_jumpstart/status-reporting.sh new file mode 100644 index 00000000..b0675691 --- /dev/null +++ b/aksarc_jumpstart/status-reporting.sh @@ -0,0 +1,156 @@ +#!/bin/bash +# Shared status reporting and ARM-deployment error surfacing helpers. +# +# Source from a calling script: +# source "$(dirname "$0")/status-reporting.sh" +# init_execution_status "jumpstart.sh" +# +# After init_execution_status, the caller has access to: +# print_execution_status +# handle_error [exit_code] +# add_completed_step +# invoke_vm_script_deployment + +# Required globals (callers can override START_TIME if needed). +EXECUTION_STATUS="InProgress" +SCRIPT_NAME="" +START_TIME="" +COMPLETED_STEPS=() +FAILED_STEP="" +ERROR_MESSAGE="" +EXIT_CODE=0 + +init_execution_status() { + SCRIPT_NAME="$1" + EXECUTION_STATUS="InProgress" + START_TIME=$(date +'%Y-%m-%d %H:%M:%S') + COMPLETED_STEPS=() + FAILED_STEP="" + ERROR_MESSAGE="" + EXIT_CODE=0 +} + +print_execution_status() { + local end_time=$(date +'%Y-%m-%d %H:%M:%S') + echo "" + echo "===== EXECUTION STATUS =====" + echo "Status: $EXECUTION_STATUS" + if [[ "$EXECUTION_STATUS" == "Failure" ]]; then + echo "Failed Step: $FAILED_STEP" + echo "Error Message: $ERROR_MESSAGE" + fi + echo "Exit Code: $EXIT_CODE" + echo "Completed Steps: ${COMPLETED_STEPS[*]}" + echo "Start Time: $START_TIME" + echo "End Time: $end_time" + echo "============================" +} + +handle_error() { + local step_name="$1" + local error_msg="$2" + local exit_code="${3:-1}" + + EXECUTION_STATUS="Failure" + FAILED_STEP="$step_name" + ERROR_MESSAGE="$error_msg" + EXIT_CODE="$exit_code" + + print_execution_status + exit "$exit_code" +} + +add_completed_step() { + COMPLETED_STEPS+=("$1") +} + +# Deploy executescript-template.json and treat ANY of these as failure: +# 1. az deployment group create returned non-zero +# 2. Extension instance view shows ProvisioningState != succeeded +# 3. (Soft) Extension StdErr substatus is non-empty -> warn but don't fail +# +# `commandToExecute` is constructed identically to the pre-existing code path +# so this change does not alter what runs inside the VM -- it only adds a +# post-deploy inspection of the extension to catch failures the deployment +# exit code missed (the actual bug we are fixing). +# +# Args: +# $1 step_name (e.g. "ExecuteScript_deploymoc.ps1") +# $2 resource_group +# $3 location +# $4 vm_name +# $5 script_uri (raw github URL) +# $6 inner_script (e.g. "deploymoc.ps1" or "deployappliance.ps1 -arg val") +# $7 template_file (optional, default ./configuration/executescript-template.json) +invoke_vm_script_deployment() { + local step_name="$1" + local resource_group="$2" + local location="$3" + local vm_name="$4" + local script_uri="$5" + local inner_script="$6" + local template_file="${7:-./configuration/executescript-template.json}" + + local script_basename="${inner_script%% *}" # first whitespace-delimited token + local deployment_name="executescript-${vm_name}-${script_basename%.*}" + local command_to_execute="powershell.exe -ExecutionPolicy Unrestricted -File ${inner_script}" + + echo "Executing ${command_to_execute} from ${script_uri} on VM ${vm_name}..." + + # Use `|| rc=$?` so the helper works correctly under `set -e` -- otherwise + # a non-zero exit from `az` would terminate the script before we can + # capture the exit code or inspect the extension instance view. + local deployment_exit_code=0 + az deployment group create \ + --name "$deployment_name" \ + --resource-group "$resource_group" \ + --template-file "$template_file" \ + --parameters \ + location="$location" \ + vmName="$vm_name" \ + scriptFileUri="$script_uri" \ + commandToExecute="$command_to_execute" || deployment_exit_code=$? + + # Always inspect the extension instance view for ground truth. + local instance_view="" + local ext_lookup_exit_code=0 + instance_view=$(az vm extension show \ + --resource-group "$resource_group" \ + --vm-name "$vm_name" \ + --name CustomScriptExtension \ + --instance-view \ + --query instanceView \ + -o json 2>/dev/null) || ext_lookup_exit_code=$? + + local provisioning_state="" + local status_message="" + local std_out="" + local std_err="" + if [[ $ext_lookup_exit_code -eq 0 && -n "$instance_view" ]] && command -v jq >/dev/null 2>&1; then + provisioning_state=$(echo "$instance_view" | jq -r '.statuses[]? | select(.code | startswith("ProvisioningState/")) | .code' | head -n1) + status_message=$(echo "$instance_view" | jq -r '.statuses[]? | select(.code | startswith("ProvisioningState/")) | .message' | head -n1) + std_out=$(echo "$instance_view" | jq -r '.substatuses[]? | select(.code | startswith("ComponentStatus/StdOut/")) | .message' | head -n1) + std_err=$(echo "$instance_view" | jq -r '.substatuses[]? | select(.code | startswith("ComponentStatus/StdErr/")) | .message' | head -n1) + fi + + local extension_failed=false + if [[ -n "$provisioning_state" && "$provisioning_state" != *succeeded ]]; then + extension_failed=true + fi + + if [[ $deployment_exit_code -ne 0 || "$extension_failed" == "true" ]]; then + local fail_exit_code=$deployment_exit_code + if [[ $fail_exit_code -eq 0 ]]; then + fail_exit_code=1 + fi + local error_text="Failed to execute script '${inner_script}' on VM '${vm_name}' (step '${step_name}'). DeploymentExitCode=${deployment_exit_code}; ProvisioningState=${provisioning_state}; StatusMessage=${status_message}; StdErr=${std_err}; StdOut=${std_out}" + handle_error "$step_name" "$error_text" "$fail_exit_code" + fi + + if [[ -n "$std_err" ]]; then + echo "WARNING: step '${step_name}' completed but extension StdErr was non-empty:" + echo "$std_err" + fi + + add_completed_step "$step_name" +} diff --git a/aksarc_jumpstart/tests/Test-StatusReporting.ps1 b/aksarc_jumpstart/tests/Test-StatusReporting.ps1 new file mode 100644 index 00000000..f0d48476 --- /dev/null +++ b/aksarc_jumpstart/tests/Test-StatusReporting.ps1 @@ -0,0 +1,169 @@ +# Test harness for status-reporting.ps1. +# +# Mocks the `az` command so each scenario can be exercised without a live +# Azure subscription. Run with: +# pwsh -NoProfile -File ./tests/Test-StatusReporting.ps1 +# or +# powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\tests\Test-StatusReporting.ps1 + +$ErrorActionPreference = 'Stop' + +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$helpersPath = Join-Path (Split-Path -Parent $here) 'status-reporting.ps1' + +# Each test runs in a fresh PowerShell instance to get a clean script-scoped +# state and a clean `az` function definition. This also means `exit N` from +# Invoke-StepFailure terminates only the child, not the test harness. +function Invoke-Scenario { + param( + [Parameter(Mandatory = $true)] [string] $Name, + [Parameter(Mandatory = $true)] [string] $AzMockBody, + [Parameter(Mandatory = $true)] [int] $ExpectedExitCode, + [Parameter()] [string] $ExpectedOutputContains + ) + + $script = @" +`$ErrorActionPreference = 'Stop' +function az { +$AzMockBody +} +. '$helpersPath' +Initialize-ExecutionStatus -ScriptName 'test' +Invoke-VmScriptDeployment `` + -StepName 'TestStep' `` + -ResourceGroup 'rg' `` + -Location 'eastus' `` + -VmName 'vm' `` + -ScriptFileUri 'https://example/script.ps1' `` + -InnerScript 'script.ps1' +# Reached only on success path. +`$script:ExecutionStatus.Status = 'Success' +Write-ExecutionStatus +"@ + + $tmp = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), [System.IO.Path]::GetRandomFileName() + '.ps1') + try { + Set-Content -Path $tmp -Value $script -Encoding UTF8 + $output = & pwsh -NoProfile -File $tmp 2>&1 | Out-String + $exit = $LASTEXITCODE + } + finally { + Remove-Item $tmp -Force -ErrorAction SilentlyContinue + } + + $passed = $true + $reasons = @() + if ($exit -ne $ExpectedExitCode) { + $passed = $false + $reasons += "exit code expected=$ExpectedExitCode actual=$exit" + } + if ($ExpectedOutputContains -and ($output -notmatch [regex]::Escape($ExpectedOutputContains))) { + $passed = $false + $reasons += "output missing expected substring '$ExpectedOutputContains'" + } + + if ($passed) { + Write-Host "PASS $Name" + } + else { + Write-Host "FAIL $Name" + foreach ($r in $reasons) { Write-Host " $r" } + Write-Host " ---- output ----" + foreach ($line in ($output -split "`r?`n")) { Write-Host " $line" } + $script:FailCount++ + } +} + +$script:FailCount = 0 + +# --- Scenario 1: deployment success + extension success + clean stderr ---- +$mock1 = @' + if ($args -contains 'deployment' -and $args -contains 'create') { + $global:LASTEXITCODE = 0 + return '{}' + } + if ($args -contains 'extension' -and $args -contains 'show') { + $global:LASTEXITCODE = 0 + return '{"statuses":[{"code":"ProvisioningState/succeeded","level":"Info","message":"Enable succeeded"}],"substatuses":[{"code":"ComponentStatus/StdOut/succeeded","message":"all good"},{"code":"ComponentStatus/StdErr/succeeded","message":""}]}' + } + $global:LASTEXITCODE = 0 +'@ +Invoke-Scenario -Name 'success: deploy ok + extension succeeded + empty stderr' ` + -AzMockBody $mock1 -ExpectedExitCode 0 -ExpectedOutputContains 'Status: Success' + +# --- Scenario 2: deployment success + extension failed (silent failure!) ---- +# This is the bug we are fixing: `az deployment group create` returned 0 but +# the script inside the VM actually failed. +$mock2 = @' + if ($args -contains 'deployment' -and $args -contains 'create') { + $global:LASTEXITCODE = 0 + return '{}' + } + if ($args -contains 'extension' -and $args -contains 'show') { + $global:LASTEXITCODE = 0 + return '{"statuses":[{"code":"ProvisioningState/failed","level":"Error","message":"Enable failed: VM script returned non-zero"}],"substatuses":[{"code":"ComponentStatus/StdOut/failed","message":"some progress output"},{"code":"ComponentStatus/StdErr/failed","message":"INNER_SCRIPT_ERROR: Install-Module failed"}]}' + } + $global:LASTEXITCODE = 0 +'@ +Invoke-Scenario -Name 'silent-failure: deploy ok + extension Failed -> caught' ` + -AzMockBody $mock2 -ExpectedExitCode 1 ` + -ExpectedOutputContains 'INNER_SCRIPT_ERROR: Install-Module failed' + +# --- Scenario 3: deployment failed (loud failure) ---- +$mock3 = @' + if ($args -contains 'deployment' -and $args -contains 'create') { + $global:LASTEXITCODE = 2 + Write-Error 'simulated deployment error' -ErrorAction Continue + return '' + } + if ($args -contains 'extension' -and $args -contains 'show') { + $global:LASTEXITCODE = 0 + return '{"statuses":[{"code":"ProvisioningState/failed","level":"Error","message":"timeout"}],"substatuses":[{"code":"ComponentStatus/StdErr/failed","message":"timed out waiting for VM"}]}' + } + $global:LASTEXITCODE = 0 +'@ +Invoke-Scenario -Name 'deploy-failure: deploy non-zero -> caught with extension detail' ` + -AzMockBody $mock3 -ExpectedExitCode 2 ` + -ExpectedOutputContains 'timed out waiting for VM' + +# --- Scenario 4: deployment success + extension success + non-empty stderr (warn) ---- +$mock4 = @' + if ($args -contains 'deployment' -and $args -contains 'create') { + $global:LASTEXITCODE = 0 + return '{}' + } + if ($args -contains 'extension' -and $args -contains 'show') { + $global:LASTEXITCODE = 0 + return '{"statuses":[{"code":"ProvisioningState/succeeded","level":"Info","message":"Enable succeeded"}],"substatuses":[{"code":"ComponentStatus/StdOut/succeeded","message":"ok"},{"code":"ComponentStatus/StdErr/succeeded","message":"WARNING: deprecated cmdlet"}]}' + } + $global:LASTEXITCODE = 0 +'@ +Invoke-Scenario -Name 'noisy-success: succeeded + non-empty stderr -> warn but pass' ` + -AzMockBody $mock4 -ExpectedExitCode 0 ` + -ExpectedOutputContains 'WARNING: deprecated cmdlet' + +# --- Scenario 5: extension show itself fails (network/auth issue) ---- +# Should NOT mask a successful deployment -- if deploy returned 0 and we cannot +# fetch the instance view, treat as success (log nothing; don't crash). +$mock5 = @' + if ($args -contains 'deployment' -and $args -contains 'create') { + $global:LASTEXITCODE = 0 + return '{}' + } + if ($args -contains 'extension' -and $args -contains 'show') { + $global:LASTEXITCODE = 1 + return '' + } + $global:LASTEXITCODE = 0 +'@ +Invoke-Scenario -Name 'extension-show-fails: deploy ok + view unfetchable -> pass' ` + -AzMockBody $mock5 -ExpectedExitCode 0 ` + -ExpectedOutputContains 'Status: Success' + +if ($script:FailCount -gt 0) { + Write-Host "`n$script:FailCount scenario(s) failed." + exit 1 +} + +Write-Host "`nAll scenarios passed." +exit 0 diff --git a/aksarc_jumpstart/tests/test-integration-set-e.sh b/aksarc_jumpstart/tests/test-integration-set-e.sh new file mode 100644 index 00000000..ab8a7367 --- /dev/null +++ b/aksarc_jumpstart/tests/test-integration-set-e.sh @@ -0,0 +1,136 @@ +#!/bin/bash +# Integration test that simulates jumpstart.sh end-to-end with set -e enabled +# and an `az` mock. Validates that: +# 1. ARM deployment failures cause the script to exit non-zero AND print the +# EXECUTION STATUS block (the original silent-failure bug) +# 2. CustomScriptExtension silent failures (deployment exit 0 but extension +# ProvisioningState=failed) are caught +# +# Run with: bash ./tests/test-integration-set-e.sh + +set -uo pipefail + +here="$(cd "$(dirname "$0")" && pwd)" +helpers="$(cd "$here/.." && pwd)/status-reporting.sh" + +fail_count=0 + +if ! command -v jq >/dev/null 2>&1; then + echo "SKIP: jq not installed" + exit 0 +fi + +# Simulate the relevant slice of jumpstart.sh under set -e: az group create +# fails -> we expect Status: Failure block + non-zero exit, NOT silent death. +test_arm_failure_under_set_e() { + local script + script=$(cat <&2 + return 1 + fi + return 0 +} +source "$helpers" +init_execution_status "test-integration" +az group create --name foo --location eastus \ + || handle_error "CreateResourceGroup" "Failed to create resource group 'foo'" \$? +add_completed_step "CreateResourceGroup" +EXECUTION_STATUS="Success" +print_execution_status +EOF +) + local output + output=$(bash -c "$script" 2>&1) + local rc=$? + local passed=true + local reasons=() + if [[ $rc -eq 0 ]]; then + passed=false + reasons+=("expected non-zero exit, got $rc") + fi + if ! echo "$output" | grep -qF "Status: Failure"; then + passed=false + reasons+=("EXECUTION STATUS block not printed (silent death!)") + fi + if ! echo "$output" | grep -qF "CreateResourceGroup"; then + passed=false + reasons+=("FailedStep not in output") + fi + if [[ "$passed" == "true" ]]; then + echo "PASS set -e: ARM failure prints status block and exits non-zero" + else + echo "FAIL set -e: ARM failure prints status block and exits non-zero" + for r in "${reasons[@]}"; do echo " $r"; done + echo " ---- output ----" + while IFS= read -r line; do echo " $line"; done <<<"$output" + fail_count=$((fail_count + 1)) + fi +} + +# Same test but for the silent-failure case: deployment returns 0 but the +# extension instance view shows ProvisioningState=failed. The wrapped helper +# must still trigger a Failure status under set -e. +test_silent_extension_failure_under_set_e() { + local script + script=$(cat <&1) + local rc=$? + local passed=true + local reasons=() + if [[ $rc -eq 0 ]]; then + passed=false + reasons+=("expected non-zero exit, got $rc (silent failure not caught)") + fi + if ! echo "$output" | grep -qF "INNER ERROR: missing module"; then + passed=false + reasons+=("extension StdErr not surfaced in output") + fi + if [[ "$passed" == "true" ]]; then + echo "PASS set -e: silent extension failure caught and surfaced" + else + echo "FAIL set -e: silent extension failure caught and surfaced" + for r in "${reasons[@]}"; do echo " $r"; done + echo " ---- output ----" + while IFS= read -r line; do echo " $line"; done <<<"$output" + fail_count=$((fail_count + 1)) + fi +} + +test_arm_failure_under_set_e +test_silent_extension_failure_under_set_e + +if [[ $fail_count -gt 0 ]]; then + echo + echo "$fail_count integration test(s) failed." + exit 1 +fi +echo +echo "All integration tests passed." +exit 0 diff --git a/aksarc_jumpstart/tests/test-status-reporting.sh b/aksarc_jumpstart/tests/test-status-reporting.sh new file mode 100644 index 00000000..421cc56e --- /dev/null +++ b/aksarc_jumpstart/tests/test-status-reporting.sh @@ -0,0 +1,177 @@ +#!/bin/bash +# Test harness for status-reporting.sh. +# +# Mocks the `az` command so each scenario can be exercised without a live +# Azure subscription. Run with: +# bash ./tests/test-status-reporting.sh + +set -uo pipefail + +here="$(cd "$(dirname "$0")" && pwd)" +helpers="$(cd "$here/.." && pwd)/status-reporting.sh" + +if ! command -v jq >/dev/null 2>&1; then + echo "SKIP: jq not installed (required for invoke_vm_script_deployment)" + exit 0 +fi + +fail_count=0 + +# Run a single scenario in a subshell so the `az` mock and global state are +# isolated. Captures combined stdout/stderr and exit code, then asserts. +run_scenario() { + local name="$1" + local az_mock_body="$2" + local expected_exit="$3" + local expected_substring="${4:-}" + + local script + script=$(cat </dev/null || true +source "$helpers" +init_execution_status "test" +invoke_vm_script_deployment \ + "TestStep" \ + "rg" \ + "eastus" \ + "vm" \ + "https://example/script.ps1" \ + "script.ps1" +EXECUTION_STATUS="Success" +print_execution_status +EOF +) + + local output + output=$(bash -c "$script" 2>&1) + local actual_exit=$? + + local passed=true + local reasons=() + if [[ $actual_exit -ne $expected_exit ]]; then + passed=false + reasons+=("exit code expected=$expected_exit actual=$actual_exit") + fi + if [[ -n "$expected_substring" ]] && ! echo "$output" | grep -qF -- "$expected_substring"; then + passed=false + reasons+=("output missing expected substring '$expected_substring'") + fi + + if [[ "$passed" == "true" ]]; then + echo "PASS $name" + else + echo "FAIL $name" + for r in "${reasons[@]}"; do echo " $r"; done + echo " ---- output ----" + while IFS= read -r line; do echo " $line"; done <<<"$output" + fail_count=$((fail_count + 1)) + fi +} + +# --- Scenario 1: deployment success + extension success + clean stderr ---- +mock1=' + local is_dep=0 is_ext=0 + for arg in "$@"; do + if [[ "$arg" == "deployment" ]]; then is_dep=1; fi + if [[ "$arg" == "extension" ]]; then is_ext=1; fi + done + if [[ "${is_dep:-0}" == "1" ]]; then + echo "{}"; return 0 + fi + if [[ "${is_ext:-0}" == "1" ]]; then + echo "{\"statuses\":[{\"code\":\"ProvisioningState/succeeded\",\"level\":\"Info\",\"message\":\"Enable succeeded\"}],\"substatuses\":[{\"code\":\"ComponentStatus/StdOut/succeeded\",\"message\":\"all good\"},{\"code\":\"ComponentStatus/StdErr/succeeded\",\"message\":\"\"}]}" + return 0 + fi + return 0 +' +run_scenario "success: deploy ok + extension succeeded + empty stderr" \ + "$mock1" 0 "Status: Success" + +# --- Scenario 2: deployment success + extension failed (silent failure!) ---- +mock2=' + local is_dep=0 is_ext=0 + for arg in "$@"; do + if [[ "$arg" == "deployment" ]]; then is_dep=1; fi + if [[ "$arg" == "extension" ]]; then is_ext=1; fi + done + if [[ "${is_dep:-0}" == "1" ]]; then + echo "{}"; return 0 + fi + if [[ "${is_ext:-0}" == "1" ]]; then + echo "{\"statuses\":[{\"code\":\"ProvisioningState/failed\",\"level\":\"Error\",\"message\":\"Enable failed: VM script returned non-zero\"}],\"substatuses\":[{\"code\":\"ComponentStatus/StdOut/failed\",\"message\":\"some progress output\"},{\"code\":\"ComponentStatus/StdErr/failed\",\"message\":\"INNER_SCRIPT_ERROR: Install-Module failed\"}]}" + return 0 + fi + return 0 +' +run_scenario "silent-failure: deploy ok + extension Failed -> caught" \ + "$mock2" 1 "INNER_SCRIPT_ERROR: Install-Module failed" + +# --- Scenario 3: deployment failed (loud failure) ---- +mock3=' + local is_dep=0 is_ext=0 + for arg in "$@"; do + if [[ "$arg" == "deployment" ]]; then is_dep=1; fi + if [[ "$arg" == "extension" ]]; then is_ext=1; fi + done + if [[ "${is_dep:-0}" == "1" ]]; then + echo "simulated deployment error" >&2; return 2 + fi + if [[ "${is_ext:-0}" == "1" ]]; then + echo "{\"statuses\":[{\"code\":\"ProvisioningState/failed\",\"level\":\"Error\",\"message\":\"timeout\"}],\"substatuses\":[{\"code\":\"ComponentStatus/StdErr/failed\",\"message\":\"timed out waiting for VM\"}]}" + return 0 + fi + return 0 +' +run_scenario "deploy-failure: deploy non-zero -> caught with extension detail" \ + "$mock3" 2 "timed out waiting for VM" + +# --- Scenario 4: deployment success + extension success + non-empty stderr (warn) ---- +mock4=' + local is_dep=0 is_ext=0 + for arg in "$@"; do + if [[ "$arg" == "deployment" ]]; then is_dep=1; fi + if [[ "$arg" == "extension" ]]; then is_ext=1; fi + done + if [[ "${is_dep:-0}" == "1" ]]; then + echo "{}"; return 0 + fi + if [[ "${is_ext:-0}" == "1" ]]; then + echo "{\"statuses\":[{\"code\":\"ProvisioningState/succeeded\",\"level\":\"Info\",\"message\":\"Enable succeeded\"}],\"substatuses\":[{\"code\":\"ComponentStatus/StdOut/succeeded\",\"message\":\"ok\"},{\"code\":\"ComponentStatus/StdErr/succeeded\",\"message\":\"WARNING: deprecated cmdlet\"}]}" + return 0 + fi + return 0 +' +run_scenario "noisy-success: succeeded + non-empty stderr -> warn but pass" \ + "$mock4" 0 "WARNING: deprecated cmdlet" + +# --- Scenario 5: extension show itself fails ---- +mock5=' + local is_dep=0 is_ext=0 + for arg in "$@"; do + if [[ "$arg" == "deployment" ]]; then is_dep=1; fi + if [[ "$arg" == "extension" ]]; then is_ext=1; fi + done + if [[ "${is_dep:-0}" == "1" ]]; then + echo "{}"; return 0 + fi + if [[ "${is_ext:-0}" == "1" ]]; then + return 1 + fi + return 0 +' +run_scenario "extension-show-fails: deploy ok + view unfetchable -> pass" \ + "$mock5" 0 "Status: Success" + +if [[ $fail_count -gt 0 ]]; then + echo + echo "$fail_count scenario(s) failed." + exit 1 +fi + +echo +echo "All scenarios passed." +exit 0