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 variablepull/22540/head
parent
b82871fdb2
commit
d83a843679
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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'
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue