ci: add functional tests for windows hyperv in azure (#22534)

* add extra tests for windows on azure

* fix login

* fix login

* fix login

* change to  pull_request_target for now

* refactor: change workflow trigger from pull_request_target to pull_request

* update checkout fix

* wait for ssh

* fix clean up

* Fix Windows E2E test output processing by wrapping it in a powershell command and simplify Azure stack name generation.

* Fix Windows E2E command quoting

* Fix minikube binary path for Windows E2E tests

* apt update background

* chore: Add `-tt` flag to ssh command in extra-tests workflow.

* ci: install kubernetes-cli via choco in extra-tests workflow.

* fix: Execute `choco install kubernetes-cli` remotely via SSH in the extra-tests workflow.

* use more gh actions

* update

* ci: add Bicep template file to Azure ARM deployment action in extra tests workflow

* ssh keep alive

* test: Ensure absolute path for linked kubectl binary and add error handling in functional test.

* Generate Gopogh reports on the Linux runner for Windows E2E tests, remove VM-side tool installation, and enhance artifact uploads with detailed summaries.

* ci: Copy `test/integration/testdata` to the Windows host for extra tests.

* add more comments

* fix: Ensure error is non-nil before warning about unexpected machine state.

* refactor: Rename workflow to 'Functional Extra' and job to 'windows-hyperv'.

* create shared composite github actions

* fix: add checkout to functional_test job

* fix local gh action

* fix: Adjust indentation of `uses` keywords for gopogh actions in functional_test.yml.

* fix name

* fix: Correct YAML formatting for the 'Install Gopogh' action in functional_extra.yml.

* remove extra gopogh environment variable
pull/22540/head
Medya Ghazizadeh 2026-01-24 23:59:44 -08:00 committed by GitHub
parent b82871fdb2
commit d83a843679
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 574 additions and 163 deletions

View File

@ -0,0 +1,199 @@
name: 'Generate Gopogh Report'
description: 'Generates HTML report, uploads artifact, and prints summary'
inputs:
json_report_path:
description: 'Path to input JSON report'
default: './report/testout.json'
report_dir:
description: 'Directory to upload as artifact'
default: './report'
report_title:
description: 'Title for the HTML report'
required: true
artifact_name_suffix:
description: 'Suffix for the artifact name (e.g. matrix name)'
required: true
time_elapsed:
description: 'Time elapsed string'
required: false
default: ''
check_strict:
description: 'If true, will fail the step on test failures/timeouts'
required: false
default: 'false'
test_outcome:
description: 'Outcome of the test step (success/failure) for timeout detection'
required: false
default: ''
test_text_path:
description: 'Path to text output to check for timeouts'
required: false
default: ''
runs:
using: "composite"
steps:
- name: Generate Gopogh HTML Report
shell: bash
env:
JSON_REPORT: ${{ inputs.json_report_path }}
REPORT_TITLE: ${{ inputs.report_title }}
TIME_ELAPSED: ${{ inputs.time_elapsed }}
TEST_OUTCOME: ${{ inputs.test_outcome }}
TEST_TEXT_PATH: ${{ inputs.test_text_path }}
run: |
# Generate HTML
# We assume gopogh is installed
OUT_DIR=$(dirname "$JSON_REPORT")
BASE_NAME=$(basename "$JSON_REPORT" .json)
HTML_REPORT="${OUT_DIR}/${BASE_NAME}.html"
SUMMARY_JSON="${OUT_DIR}/${BASE_NAME}_summary.json"
STAT=$(gopogh -in "$JSON_REPORT" -out_html "$HTML_REPORT" -out_summary "$SUMMARY_JSON" -name "$REPORT_TITLE ${GITHUB_REF}" -repo "${GITHUB_REPOSITORY}" -details "${GITHUB_SHA}") || true
# Check for timeouts
IS_TIMEOUT=0
if [[ "$TEST_OUTCOME" == "failure" && -n "$TEST_TEXT_PATH" && -f "$TEST_TEXT_PATH" ]]; then
if grep -q "panic: test timed out" "$TEST_TEXT_PATH"; then
IS_TIMEOUT=1
fi
fi
if [ "$IS_TIMEOUT" -eq 1 ]; then
RESULT_SHORT="⌛⌛⌛ Test Timed out ${TIME_ELAPSED} ⌛⌛⌛"
else
# Calculate Status
PassNum=$(echo $STAT | jq '.NumberOfPass')
FailNum=$(echo $STAT | jq '.NumberOfFail')
TestsNum=$(echo $STAT | jq '.NumberOfTests')
if [ "${FailNum}" -eq 0 ]; then
STATUS_ICON="✓"
else
STATUS_ICON="✗"
fi
if [ "${PassNum}" -eq 0 ]; then
STATUS_ICON="✗"
fi
# Result in one sentence
RESULT_SHORT="${STATUS_ICON} Completed with ${FailNum} / ${TestsNum} failures"
if [ -n "$TIME_ELAPSED" ]; then
RESULT_SHORT="${RESULT_SHORT} in ${TIME_ELAPSED}"
fi
fi
echo "RESULT_SHORT=${RESULT_SHORT}" >> $GITHUB_ENV
echo 'STAT<<EOF' >> $GITHUB_ENV
echo "${STAT}" >> $GITHUB_ENV
echo 'EOF' >> $GITHUB_ENV
- name: Set PR or Branch label for report filename
id: vars
shell: bash
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
echo "PR_OR_MASTER=PR${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
else
echo "PR_OR_MASTER=Master" >> $GITHUB_OUTPUT
fi
echo "COMMIT_SHA=${GITHUB_SHA:0:7}" >> $GITHUB_OUTPUT
RUN_ID_SHORT="$GITHUB_RUN_ID"
if [ ${#RUN_ID_SHORT} -gt 7 ]; then
RUN_ID_SHORT="${RUN_ID_SHORT: -7}"
fi
echo "RUN_ID_SHORT=${RUN_ID_SHORT}" >> $GITHUB_OUTPUT
- name: Upload Gopogh report
id: upload_gopogh
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
with:
name: functional-${{ inputs.artifact_name_suffix }}-${{ steps.vars.outputs.PR_OR_MASTER }}-sha-${{ steps.vars.outputs.COMMIT_SHA }}-run-${{ steps.vars.outputs.RUN_ID_SHORT}}
path: ${{ inputs.report_dir }}
- name: The End Result Summary
shell: bash
env:
NAME: ${{ inputs.artifact_name_suffix }}
PR_OR_MASTER: ${{ steps.vars.outputs.PR_OR_MASTER }}
COMMIT_SHA: ${{ steps.vars.outputs.COMMIT_SHA }}
ARTIFACT_ID: ${{ steps.upload_gopogh.outputs.artifact-id }}
TIME_ELAPSED: ${{ inputs.time_elapsed }}
CHECK_STRICT: ${{ inputs.check_strict }}
run: |
summary="$GITHUB_STEP_SUMMARY"
ARTIFACT_NAME="functional-${NAME}-${PR_OR_MASTER}-sha-${COMMIT_SHA}"
if [ -n "$ARTIFACT_ID" ]; then
URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts/$ARTIFACT_ID"
echo "Gopogh report artifact ($ARTIFACT_NAME): $URL"
echo "📥 [Download Gopogh Report]($URL)" >> "$summary"
else
echo "Could not determine artifact ID. Find artifact named: $ARTIFACT_NAME"
echo "Report artifact name: $ARTIFACT_NAME" | tee -a "$summary"
fi
echo "-------------------- RESULT SUMMARY --------------------"
echo "$RESULT_SHORT" | tee -a "$summary"
# Only print time if it exists
if [ -n "$TIME_ELAPSED" ]; then
echo "Time Elapsed: ${TIME_ELAPSED}" | tee -a "$summary"
fi
numFail=$(echo "$STAT" | jq -r '.NumberOfFail // 0')
numPass=$(echo "$STAT" | jq -r '.NumberOfPass // 0')
numSkip=$(echo "$STAT" | jq -r '.NumberOfSkip // 0')
# Print test counts
if [ -n "${numFail}" ] && [ "$numFail" != "null" ]; then echo "Failed: ${numFail}" | tee -a "$summary"; fi
if [ -n "${numPass}" ] && [ "$numPass" != "null" ]; then echo "Passed: ${numPass}" | tee -a "$summary"; fi
if [ -n "${numSkip}" ] && [ "$numSkip" != "null" ]; then echo "Skipped: ${numSkip}" | tee -a "$summary"; fi
# Print lists
print_test_names_by_status() {
local count="$1" header="$2" sym="$3" field="$4" to_summary="$5"
(( count > 0 )) || return 0
local line="------------------------ ${count} ${header} ------------------------"
if [ "$to_summary" = "yes" ]; then
echo "$line" | tee -a "$summary"
jq -r ".${field}[]? | \" ${sym} \(.)\"" <<<"$STAT" | tee -a "$summary"
else
echo "$line"
jq -r ".${field}[]? | \" ${sym} \(.)\"" <<<"$STAT"
fi
}
print_test_names_by_status "${numFail:-0}" "Failed" "✗" "FailedTests" yes
print_test_names_by_status "${numPass:-0}" "Passed" "✓" "PassedTests" no
print_test_names_by_status "${numSkip:-0}" "Skipped" "•" "SkippedTests" yes
echo $summary >> $GITHUB_STEP_SUMMARY
# Strict Check logic
if [ "$CHECK_STRICT" = "true" ]; then
# Allow overriding minimum expected passes
timeout_pattern="Test Timed out"
echo "---------------------------------------------------------"
if echo "$RESULT_SHORT" | grep -iq "$timeout_pattern"; then
echo "*** Detected test timeout ${TIME_ELAPSED} ⌛: '$timeout_pattern' ***"
exit 3
fi
# Any failures
if [ "${numFail:-0}" -gt 0 ]; then
echo "*** ${numFail} test(s) failed ***"
exit 2
fi
# Zero passes
if [ "${numPass:-0}" -eq 0 ]; then
echo "*** No tests passed ***"
exit 4
fi
echo "Exit criteria satisfied: ${numPass} passed, ${numFail} failed, ${numSkip} skipped."
fi

View File

@ -0,0 +1,21 @@
name: 'Install Gopogh'
description: 'Downloads and installs gopogh'
inputs:
version:
description: 'Gopogh version'
default: 'v0.29.0'
runs:
using: "composite"
steps:
- shell: bash
run: |
GOPOGH_VERSION=${{ inputs.version }}
GOOS=$(go env GOOS)
GOARCH=$(go env GOARCH)
URL="https://github.com/medyagh/gopogh/releases/download/${GOPOGH_VERSION}/gopogh-${GOOS}-${GOARCH}"
echo "Downloading ${URL}"
curl -fsSL "${URL}" -o gopogh
sudo install -m 0755 gopogh /usr/local/bin/gopogh
rm gopogh
command -v gopogh
gopogh -version || true

141
.github/workflows/functional_extra.yml vendored Normal file
View File

@ -0,0 +1,141 @@
name: Functional Extra
on:
pull_request:
types: [labeled]
jobs:
windows-hyperv:
if: contains(github.event.pull_request.labels.*.name, 'ok-to-extra-test')
runs-on: ubuntu-latest
env:
MINIKUBE_AZ_RESOURCE_GROUP: "SIG-CLUSTER-LIFECYCLE-MINIKUBE"
AZURE_DEFAULTS_LOCATION: "southcentralus"
MINIKUBE_AZ_SIG_NAME: "minikube"
MINIKUBE_AZ_IMAGE_NAME: "minikube-ci-windows-11"
MINIKUBE_AZ_IMAGE_VERSION: "1.0.0"
steps:
- name: Azure Login
id: azlogin
uses: azure/login@v2
with:
creds: '{"clientId":"${{ secrets.MINIKUBE_AZ_CLIENT_ID }}","clientSecret":"${{ secrets.MINIKUBE_AZ_PASSWORD }}","subscriptionId":"${{ secrets.MINIKUBE_AZ_SUBSCRIPTION_ID }}","tenantId":"${{ secrets.MINIKUBE_AZ_TENANT_ID }}"}'
- uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'
cache: true
- name: Build Windows Binaries
run: |
sudo apt-get update &
make e2e-windows-amd64.exe
make minikube-windows-amd64.exe
- name: Install Gopogh
uses: ./.github/actions/install-gopogh
- name: Create VM
id: create_vm
uses: azure/bicep-deploy@v2
with:
type: deploymentStack
operation: create
name: "${{ github.event.pull_request.number }}-${{ github.run_id }}-stack"
scope: resourceGroup
resource-group-name: ${{ env.MINIKUBE_AZ_RESOURCE_GROUP }}
subscription-id: ${{ secrets.MINIKUBE_AZ_SUBSCRIPTION_ID }}
template-file: ./hack/windows-ci-image/vm.bicep
parameters-file: ./hack/windows-ci-image/vm.bicepparam
parameters: '{"vmName": "m-${{ github.event.pull_request.number }}-${{ github.run_id }}"}'
action-on-unmanage-resources: delete
deny-settings-mode: none
- name: Wait for SSH
env:
SSHPASS: ${{ secrets.MINIKUBE_AZ_CI_WINDOWS_VM_PASSWORD }}
run: |
VM_NAME="m-${{ github.event.pull_request.number }}-${{ github.run_id }}"
HOST="${VM_NAME}.${AZURE_DEFAULTS_LOCATION}.cloudapp.azure.com"
echo "Waiting for SSH on $HOST..."
for i in {1..30}; do
if sshpass -e ssh -o StrictHostKeyChecking=no -o PubkeyAuthentication=no -o ConnectTimeout=5 "minikubeadmin@$HOST" "echo 'SSH Ready'"; then
echo "SSH is ready!"
exit 0
fi
echo "Waiting for SSH... ($i/30)"
sleep 10
done
echo "SSH failed to become ready."
exit 1
- name: Copy Binaries to VM
env:
SSHPASS: ${{ secrets.MINIKUBE_AZ_CI_WINDOWS_VM_PASSWORD }}
run: |
VM_NAME="m-${{ github.event.pull_request.number }}-${{ github.run_id }}"
HOST="${VM_NAME}.${AZURE_DEFAULTS_LOCATION}.cloudapp.azure.com"
USER="minikubeadmin"
# Copy binaries
sshpass -e scp -o StrictHostKeyChecking=no -o PubkeyAuthentication=no ./out/minikube-windows-amd64.exe "$USER@$HOST:C:/Users/$USER/"
sshpass -e scp -o StrictHostKeyChecking=no -o PubkeyAuthentication=no ./out/e2e-windows-amd64.exe "$USER@$HOST:C:/Users/$USER/"
sshpass -e scp -r -o StrictHostKeyChecking=no -o PubkeyAuthentication=no ./test/integration/testdata "$USER@$HOST:C:/Users/$USER/"
- name: Run Functional Test
continue-on-error: true
env:
SSHPASS: ${{ secrets.MINIKUBE_AZ_CI_WINDOWS_VM_PASSWORD }}
run: |
VM_NAME="m-${{ github.event.pull_request.number }}-${{ github.run_id }}"
HOST="${VM_NAME}.${AZURE_DEFAULTS_LOCATION}.cloudapp.azure.com"
USER="minikubeadmin"
CMD="
\$env:Path += ';C:\Users\minikubeadmin\go\bin';
gotestsum --jsonfile testout.json -f standard-verbose --raw-command -- \`
powershell -Command \"./e2e-windows-amd64.exe --minikube-start-args='--driver=hyperv' --binary=./minikube-windows-amd64.exe '-test.run' TestFunctional '-test.v' '-test.timeout=40m' | go tool test2json -t | Tee-Object -FilePath testout.txt\"
\$env:result=\$lastexitcode
if(\$env:result -eq 0){ echo 'minikube: SUCCESS' } else { echo 'minikube: FAIL' }
"
echo "$CMD" > run_tests.ps1
sshpass -e scp -o StrictHostKeyChecking=no -o PubkeyAuthentication=no run_tests.ps1 "$USER@$HOST:C:/Users/$USER/"
sshpass -e ssh -o StrictHostKeyChecking=no -o PubkeyAuthentication=no -o ServerAliveInterval=60 -o ServerAliveCountMax=9 "$USER@$HOST" "powershell -File C:/Users/$USER/run_tests.ps1"
- name: Copy results from inside the VM
if: always() && steps.azlogin.outcome == 'success'
env:
SSHPASS: ${{ secrets.MINIKUBE_AZ_CI_WINDOWS_VM_PASSWORD }}
run: |
VM_NAME="m-${{ github.event.pull_request.number }}-${{ github.run_id }}"
HOST="${VM_NAME}.${AZURE_DEFAULTS_LOCATION}.cloudapp.azure.com"
USER="minikubeadmin"
sshpass -e scp -o StrictHostKeyChecking=no -o PubkeyAuthentication=no "$USER@$HOST:C:/Users/$USER/testout.json" . || true
sshpass -e scp -o StrictHostKeyChecking=no -o PubkeyAuthentication=no "$USER@$HOST:C:/Users/$USER/testout.txt" . || true
# Create Report Directory
mkdir -p report
[ -f testout.json ] && mv testout.json report/
[ -f testout.txt ] && mv testout.txt report/
- name: Generate Gopogh HTML Report
uses: ./.github/actions/generate-report
if: always() && steps.azlogin.outcome == 'success'
with:
json_report_path: './report/testout.json'
report_dir: './report'
report_title: "Windows Functional Azure ${GITHUB_REF}"
artifact_name_suffix: "windows-hyperv"
check_strict: 'true'
- name: Cleanup VM
if: always() && steps.azlogin.outcome == 'success'
run: az rest --method DELETE --url "/subscriptions/${{ secrets.MINIKUBE_AZ_SUBSCRIPTION_ID }}/resourceGroups/${{ env.MINIKUBE_AZ_RESOURCE_GROUP }}/providers/Microsoft.Resources/deploymentStacks/${{ github.event.pull_request.number }}-${{ github.run_id }}-stack?api-version=2024-03-01&actionOnUnmanage=deleteAll" || true
- name: Verify Cleanup
if: always() && steps.azlogin.outcome == 'success'
uses: azure/cli@v2
with:
inlineScript: |
echo "Verifying cleanup..."
az resource list --resource-group "${{ env.MINIKUBE_AZ_RESOURCE_GROUP }}" --output table

View File

@ -143,23 +143,14 @@ jobs:
steps:
- id: info-block
uses: medyagh/info-block@main
- uses: actions/checkout@v4
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c
with:
go-version: ${{env.GO_VERSION}}
cache: true
- name: Install gopogh
shell: bash
run: |
GOPOGH_VERSION=v0.29.0
GOOS=$(go env GOOS)
GOARCH=$(go env GOARCH)
URL="https://github.com/medyagh/gopogh/releases/download/${GOPOGH_VERSION}/gopogh-${GOOS}-${GOARCH}"
echo "Downloading ${URL}"
curl -fsSL "${URL}" -o gopogh
sudo install -m 0755 gopogh /usr/local/bin/gopogh
rm gopogh
command -v gopogh
gopogh -version || true
uses: ./.github/actions/install-gopogh
- name: Set up cgroup v2 delegation (rootless)
if: ${{ matrix.rootless }}
run: |
@ -315,150 +306,19 @@ jobs:
TIME_ELAPSED="${min} min $sec seconds "
# make variables available for next step
echo "TIME_ELAPSED=${TIME_ELAPSED}" >> $GITHUB_ENV
- name: Generate Gopogh HTML Report
- name: Convert Test Output to JSON
if: always()
shell: bash
run: |
go tool test2json -t < ./report/testout.txt > ./report/testout.json || true
STAT=$(gopogh -in ./report/testout.json -out_html ./report/testout.html -out_summary ./report/testout_summary.json -name "${{ matrix.name }} ${GITHUB_REF}" -repo "${GITHUB_REPOSITORY}" -details "${GITHUB_SHA}") || true
# Check if the test step failed AND the log contains "timed out"
if [[ "${{ steps.run_test.outcome }}" == "failure" && $(grep -c "panic: test timed out" ./report/testout.txt) -gt 0 ]]; then
# If it was a timeout, set your custom message
RESULT_SHORT="⌛⌛⌛ Test Timed out ${TIME_ELAPSED} ⌛⌛⌛"
else
PassNum=$(echo $STAT | jq '.NumberOfPass')
FailNum=$(echo $STAT | jq '.NumberOfFail')
TestsNum=$(echo $STAT | jq '.NumberOfTests')
if [ "${FailNum}" -eq 0 ]; then
STATUS_ICON="✓"
else
STATUS_ICON="✗"
fi
if [ "${PassNum}" -eq 0 ]; then
STATUS_ICON="✗"
fi
# Result in one sentence
RESULT_SHORT="${STATUS_ICON} Completed with ${FailNum} / ${TestsNum} failures in ${TIME_ELAPSED}"
fi
echo "RESULT_SHORT=${RESULT_SHORT}" >> $GITHUB_ENV
echo "TIME_ELAPSED=${TIME_ELAPSED}" >> $GITHUB_ENV
echo 'STAT<<EOF' >> $GITHUB_ENV
echo "${STAT}" >> $GITHUB_ENV
echo 'EOF' >> $GITHUB_ENV
- name: Set PR or Branch label for report filename
id: vars
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
echo "PR_OR_MASTER=PR${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
else
echo "PR_OR_MASTER=Master" >> $GITHUB_OUTPUT
fi
echo "COMMIT_SHA=${GITHUB_SHA:0:7}" >> $GITHUB_OUTPUT
RUN_ID_SHORT="$GITHUB_RUN_ID"
if [ ${#RUN_ID_SHORT} -gt 7 ]; then
RUN_ID_SHORT="${RUN_ID_SHORT: -7}"
fi
echo "RUN_ID_SHORT=${RUN_ID_SHORT}" >> $GITHUB_OUTPUT
- name: Upload Gopogh report
id: upload_gopogh
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
run: go tool test2json -t < ./report/testout.txt > ./report/testout.json || true
- name: Generate Gopogh HTML Report
uses: ./.github/actions/generate-report
if: always()
with:
name: functional-${{ matrix.name }}-${{ steps.vars.outputs.PR_OR_MASTER }}-sha-${{ steps.vars.outputs.COMMIT_SHA }}-run-${{ steps.vars.outputs.RUN_ID_SHORT}}
path: ./report
- name: The End Result Summary ${{ matrix.name }}
shell: bash
run: |
summary="$GITHUB_STEP_SUMMARY"
Print_Gopogh_Artifact_Download_URL() {
ARTIFACT_NAME="functional-${{ matrix.name }}-${{ steps.vars.outputs.PR_OR_MASTER }}-sha-${{ steps.vars.outputs.COMMIT_SHA }}"
ARTIFACT_ID='${{ steps.upload_gopogh.outputs.artifact-id }}'
if [ -n "$ARTIFACT_ID" ]; then
URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts/$ARTIFACT_ID"
echo "Gopogh report artifact ($ARTIFACT_NAME): $URL"
echo "📥 [Download Gopogh Report]($URL)" >> "$summary"
else
echo "Could not determine artifact ID (action version may not expose it). Find artifact named: $ARTIFACT_NAME"
echo "Report artifact name: $ARTIFACT_NAME" | tee -a "$summary"
fi
}
Print_Gopogh_Artifact_Download_URL
echo "-------------------- RESULT SUMMARY --------------------"
echo "$RESULT_SHORT" | tee -a "$summary"
echo "Time Elapsed: ${TIME_ELAPSED}" | tee -a "$summary"
numFail=$(echo "$STAT" | jq -r '.NumberOfFail // 0')
numPass=$(echo "$STAT" | jq -r '.NumberOfPass // 0')
numSkip=$(echo "$STAT" | jq -r '.NumberOfSkip // 0')
# Print test counts only if they are non-zero
print_test_counts_only() {
if [ -n "${numFail}" ]; then
echo "Failed: ${numFail}" | tee -a "$summary"
fi
if [ -n "${numPass}" ]; then
echo "Passed: ${numPass}" | tee -a "$summary"
fi
if [ -n "${numSkip}" ]; then
echo "Skipped: ${numSkip}" | tee -a "$summary"
fi
}
print_test_counts_only
# Prints lits of test names grouped by result status
print_test_names_by_status() {
local count="$1" header="$2" sym="$3" field="$4" to_summary="$5"
(( count > 0 )) || return 0
local line="------------------------ ${count} ${header} ------------------------"
if [ "$to_summary" = "yes" ]; then
echo "$line" | tee -a "$summary"
jq -r ".${field}[]? | \" ${sym} \(.)\"" <<<"$STAT" | tee -a "$summary"
else
echo "$line"
jq -r ".${field}[]? | \" ${sym} \(.)\"" <<<"$STAT"
fi
}
print_test_names_by_status "${numFail:-0}" "Failed" "✗" "FailedTests" yes
print_test_names_by_status "${numPass:-0}" "Passed" "✓" "PassedTests" no
print_test_names_by_status "${numSkip:-0}" "Skipped" "•" "SkippedTests" yes
echo $summary >> $GITHUB_STEP_SUMMARY
decide_exit_code() {
# Allow overriding minimum expected passes for when some tests pass and others are timed out
local min_pass="${MIN_PASS_THRESHOLD:-45}"
local timeout_pattern="Test Timed out"
echo "---------------------------------------------------------"
# Timeout detection
if echo "$RESULT_SHORT" | grep -iq "$timeout_pattern"; then
echo "*** Detected test timeout ${TIME_ELAPSED} ⌛: '$timeout_pattern' ***"
exit 3
fi
# Any failures
if [ "${numFail:-0}" -gt 0 ]; then
echo "*** ${numFail} test(s) failed ***"
exit 2
fi
# Zero passes (likely setup issue)
if [ "${numPass:-0}" -eq 0 ]; then
echo "*** No tests passed ***"
exit 4
fi
# Insufficient passes safeguard
if [ "${numPass:-0}" -lt "$min_pass" ]; then
echo "*** Only ${numPass} passed (< required ${min_pass}) ***" | tee -a "$summary"
exit 5
fi
echo "Exit criteria satisfied: ${numPass} passed, ${numFail} failed, ${numSkip} skipped."
}
decide_exit_code
json_report_path: './report/testout.json'
report_dir: './report'
report_title: "${{ matrix.name }} ${GITHUB_REF}"
artifact_name_suffix: "${{ matrix.name }}"
time_elapsed: "${{ env.TIME_ELAPSED }}"
check_strict: 'true'
test_outcome: "${{ steps.run_test.outcome }}"
test_text_path: './report/testout.txt'

View File

@ -0,0 +1,165 @@
targetScope = 'resourceGroup'
// vm.bicepparam
param sigName string
param sigImageDefinitionName string
param sigImageVersion string
param vmName string
param vmSize string
var location string = resourceGroup().location
var namePrefix string = vmName
var nameSuffix string = uniqueString(resourceGroup().location)
var networkInterfaceName string = '${namePrefix}-nic-${nameSuffix}'
var networkSecurityGroupName string = '${namePrefix}-nsg-${nameSuffix}'
var publicIpName string = '${namePrefix}-pip-${nameSuffix}'
var subnetName string = '${namePrefix}-snet-${nameSuffix}'
var virtualMachineName string = '${vmName}-${nameSuffix}'
var virtualNetworkName string = '${namePrefix}-vnet-${nameSuffix}'
resource sig 'Microsoft.Compute/galleries@2024-03-03' existing = {
name: sigName
}
resource sigImageDefinition 'Microsoft.Compute/galleries/images@2022-08-03' existing = {
name: sigImageDefinitionName
parent: sig
}
resource virtualNetwork 'Microsoft.Network/virtualNetworks@2023-11-01' = {
name: virtualNetworkName
location: location
tags: { project: 'minikube' }
properties: {
addressSpace: {
addressPrefixes: [
'10.0.0.0/16'
]
}
subnets: [
{
name: subnetName
properties: {
addressPrefix: '10.0.1.0/24'
networkSecurityGroup: {
id: networkSecurityGroup.id
}
}
}
]
}
}
resource publicIp 'Microsoft.Network/publicIPAddresses@2023-11-01' = {
name: publicIpName
location: location
tags: { project: 'minikube' }
sku: { name: 'Standard' } // Basic: Cannot create more than 0 IPv4 Basic SKU public IP addresses for this subscription in this region.
properties: {
dnsSettings: {
domainNameLabel: vmName
}
publicIPAddressVersion: 'IPv4'
publicIPAllocationMethod: 'Static' // Dynamic: Standard sku publicIp /subscriptions/... must have AllocationMethod set to Static.
}
}
resource networkSecurityGroup 'Microsoft.Network/networkSecurityGroups@2023-11-01' = {
name: networkSecurityGroupName
location: location
tags: { project: 'minikube' }
properties: {
securityRules: [
{
name: 'AllowRDP'
properties: {
access: 'Allow'
destinationAddressPrefix: '*'
destinationPortRange: '3389'
direction: 'Inbound'
priority: 1000
protocol: 'Tcp'
sourceAddressPrefix: '*'
sourcePortRange: '*'
}
}
{
name: 'AllowSSH'
properties: {
access: 'Allow'
destinationAddressPrefix: '*'
destinationPortRange: '22'
direction: 'Inbound'
priority: 1001
protocol: 'Tcp'
sourceAddressPrefix: '*'
sourcePortRange: '*'
}
}
]
}
}
resource networkInterface 'Microsoft.Network/networkInterfaces@2023-11-01' = {
name: networkInterfaceName
location: location
tags: { project: 'minikube' }
properties: {
ipConfigurations: [
{
name: 'internal'
properties: {
subnet: {
id: '${virtualNetwork.id}/subnets/${subnetName}'
}
privateIPAllocationMethod: 'Dynamic'
publicIPAddress: {
id: publicIp.id
}
}
}
]
networkSecurityGroup: {
id: networkSecurityGroup.id
}
}
}
resource virtualMachine 'Microsoft.Compute/virtualMachines@2023-09-01' = {
name: virtualMachineName
location: location
tags: { project: 'minikube' }
properties: {
hardwareProfile: {
vmSize: vmSize
}
//osProfile: {} // Parameter OSProfile is not allowed with a specialized image.
storageProfile: {
imageReference: {
id: '${sig.id}/images/${sigImageDefinition.name}/versions/${sigImageVersion}'
}
osDisk: {
createOption: 'FromImage'
managedDisk: {
storageAccountType: 'StandardSSD_LRS'
}
deleteOption: 'Delete'
}
}
networkProfile: {
networkInterfaces: [
{
id: networkInterface.id
properties: {
primary: true
}
}
]
}
}
}
output vmId string = virtualMachine.id
output vmName string = virtualMachine.name
output vmHostname string = publicIp.properties.dnsSettings.fqdn
output publicIpAddress string = publicIp.properties.ipAddress

View File

@ -0,0 +1,10 @@
using './vm.bicep'
// Azure Shared Image Gallery
param sigName = readEnvironmentVariable('MINIKUBE_AZ_SIG_NAME')
param sigImageDefinitionName = readEnvironmentVariable('MINIKUBE_AZ_IMAGE_NAME') // same as image name by convention
param sigImageVersion = readEnvironmentVariable('MINIKUBE_AZ_IMAGE_VERSION') // or 'latest'
// Azure Virtual Machine
param vmName = 'vm-minikube-ci'
param vmSize = 'Standard_D16s_v3'

View File

@ -133,7 +133,7 @@ func recreateIfNeeded(api libmachine.API, cc *config.ClusterConfig, n *config.No
}
}
if serr != constants.ErrMachineMissing {
if serr != nil && serr != constants.ErrMachineMissing {
klog.Warningf("unexpected machine state, will restart: %v", serr)
}

View File

@ -734,7 +734,8 @@ func validateMinikubeKubectl(ctx context.Context, t *testing.T, profile string)
}
}
// validateMinikubeKubectlDirectCall validates that calling minikube's kubectl
// validateMinikubeKubectlDirectCall validates that calling the minikube binary linked as "kubectl" acts as a kubectl wrapper.
// This tests the feature where minikube behaves like kubectl when invoked via a binary named "kubectl".
func validateMinikubeKubectlDirectCall(ctx context.Context, t *testing.T, profile string) {
defer PostMortemLogs(t, profile)
dir := filepath.Dir(Target())
@ -742,8 +743,11 @@ func validateMinikubeKubectlDirectCall(ctx context.Context, t *testing.T, profil
if runtime.GOOS == "windows" {
newName += ".exe"
}
dstfn := filepath.Join(dir, newName)
err := os.Link(Target(), dstfn)
dstfn, err := filepath.Abs(filepath.Join(dir, newName))
if err != nil {
t.Fatalf("failed to get absolute path check kubectl binary: %v", err)
}
err = os.Link(Target(), dstfn)
if err != nil {
t.Fatalf("failed to link kubectl binary from %s to %s: %v", Target(), dstfn, err)
@ -1858,10 +1862,18 @@ func localEmptyCertPath() string {
return filepath.Join(localpath.MiniPath(), "/certs", fmt.Sprintf("%d_empty.pem", os.Getpid()))
}
// Copy extra file into minikube home folder for file sync test
// setupFileSync copies files to the Minikube home directory to verify that they are correctly synced to the VM.
// It tests two main sync mechanisms:
// 1. Generic file sync: Files placed in $MINIKUBE_HOME/files/<path> should be synced to /<path> in the VM.
// - sync.test -> /etc/test/nested/copy/... (verified by validateFileSync)
// - minikube_test2.pem -> /etc/ssl/certs/... (verified by validateCertSync)
//
// 2. Certificate sync: Files placed in $MINIKUBE_HOME/certs should be installed as system certificates in the VM.
// - minikube_test.pem -> /etc/ssl/certs/... (verified by validateCertSync)
func setupFileSync(_ context.Context, t *testing.T, _ string) {
p := localSyncTestPath()
t.Logf("local sync path: %s", p)
// This file is tested by validateFileSync to ensure generic file sync works
syncFile := filepath.Join(*testdataDir, "sync.test")
err := cp.Copy(syncFile, p)
if err != nil {
@ -1870,7 +1882,8 @@ func setupFileSync(_ context.Context, t *testing.T, _ string) {
testPem := filepath.Join(*testdataDir, "minikube_test.pem")
// Write to a temp file for an atomic write
// Write to a temp file for an atomic write to $MINIKUBE_HOME/certs
// This tests that certs in this dir are installed to /etc/ssl/certs and /usr/share/ca-certificates in the VM
tmpPem := localTestCertPath() + ".pem"
if err := cp.Copy(testPem, tmpPem); err != nil {
t.Fatalf("failed to copy %s: %v", testPem, err)
@ -1895,6 +1908,7 @@ func setupFileSync(_ context.Context, t *testing.T, _ string) {
}
testPem2 := filepath.Join(*testdataDir, "minikube_test2.pem")
// This tests that certs placed in $MINIKUBE_HOME/files/etc/ssl/certs are also processed correctly
tmpPem2 := localTestCertFilesPath() + ".pem"
if err := cp.Copy(testPem2, tmpPem2); err != nil {
t.Fatalf("failed to copy %s: %v", testPem2, err)
@ -1918,7 +1932,8 @@ func setupFileSync(_ context.Context, t *testing.T, _ string) {
t.Errorf("%s size=%d, want %d", localTestCertFilesPath(), got.Size(), want.Size())
}
// Create an empty file just to mess with people
// Create an empty file just to mess with people.
// This tests that empty files or garbage files in these directories don't crash the sync process.
if _, err := os.Create(localEmptyCertPath()); err != nil {
t.Fatalf("create failed: %v", err)
}