diff --git a/.pipelines/.vsts-vhd-builder-pr-windows.yaml b/.pipelines/.vsts-vhd-builder-pr-windows.yaml index 591414b307a..95917727941 100644 --- a/.pipelines/.vsts-vhd-builder-pr-windows.yaml +++ b/.pipelines/.vsts-vhd-builder-pr-windows.yaml @@ -72,9 +72,9 @@ stages: useOverrides: False enableBackfillCleanup: True buildVmSize: Standard_D16ds_v5 - # 2022 gen1 is the default, so build that for PRs + # Keep the default Gen1 image and cover both security modes on Gen2. build2022containerd: True - build2022containerdgen2: False + build2022containerdgen2: True # 2025 gen2 is the default, so build that for PRs build2025: False build2025gen2: True diff --git a/.pipelines/templates/.build-and-test-windows-vhd-template.yaml b/.pipelines/templates/.build-and-test-windows-vhd-template.yaml index 8a85f6a009a..81b3174c5ba 100644 --- a/.pipelines/templates/.build-and-test-windows-vhd-template.yaml +++ b/.pipelines/templates/.build-and-test-windows-vhd-template.yaml @@ -36,10 +36,6 @@ parameters: type: boolean displayName: Enable Trusted Launch on the SIG image definition default: False - - name: trustedLaunchSupported - type: boolean - displayName: Determines if the image is capable of supporting Trusted Launch - default: False - name: skipExtensionCheck displayName: Skip Extension Check type: boolean @@ -91,7 +87,7 @@ stages: hyperVGeneration: ${{ parameters.hyperVGeneration }} architecture: ${{ parameters.architecture }} enableTrustedLaunch: ${{ parameters.enableTrustedLaunch }} - trustedLaunchSupported: ${{ parameters.trustedLaunchSupported }} + trustedLaunchSupported: ${{ eq(parameters.hyperVGeneration, 'V2') }} buildVmSize: ${{ parameters.buildVmSize }} build: ${{ parameters.build }} vhddebug: ${{ parameters.vhddebug }} diff --git a/.pipelines/templates/.build-and-test-windows-vhds-template.yaml b/.pipelines/templates/.build-and-test-windows-vhds-template.yaml index c0bcb8e24e6..7dd37e9a771 100644 --- a/.pipelines/templates/.build-and-test-windows-vhds-template.yaml +++ b/.pipelines/templates/.build-and-test-windows-vhds-template.yaml @@ -174,9 +174,8 @@ stages: windowsSku: 2025-gen2-tl hyperVGeneration: V2 architecture: X86_64 - # TODO: replace enableTrustedLaunch with trustedLaunchSupported when / as able - by default we shouldn't need to build dedicated trusted launch images + # TODO: retire the dedicated image after Trusted Launch moves to the shared Gen2 image. enableTrustedLaunch: True - trustedLaunchSupported: True buildVmSize: ${{ parameters.buildVmSize }} build: ${{ parameters.build2025gen2tl }} vhddebug: ${{ parameters.vhddebug }} diff --git a/e2e/README.md b/e2e/README.md index 0e9404bf3a8..81f507d0d1b 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -108,6 +108,30 @@ alongside cluster preparation. Setup, validators, and cleanup batches share `scenario/concurrent.go`, which waits for every task without canceling peers on failure. Each caller decides whether an already-canceled context prevents its tasks from starting. +## Shared Windows Gen2 images + +The ordinary WS2022 and WS2025 Gen2 images are published as `TrustedLaunchSupported`. +Each has a separate scenario that enables Secure Boot and vTPM on the VMSS: + +| Image | Standard scenario | Trusted Launch scenario | +| --- | --- | --- | +| `windows-2022-containerd-gen2` | `Windows2022Gen2AzureNetwork` | `Windows2022Gen2_TrustedLaunch_SharedImage` | +| `windows-2025-gen2` | `Windows2025Gen2` | `Windows2025Gen2_TrustedLaunch_SharedImage` | + +Separate nodes are needed because their VM security profiles differ. These direct +image tests do not change AKS RP's OS eligibility checks. + +The builder reuses already-capable staging definitions without `startsAtVersion` +without modifying them. Explicit boundaries must be valid and at or below the +build version; new capability migrations still establish and verify a boundary. +The VHD-build gate validates the actual build's image, not historical versions. +Production publication and RP image-version admission remain separate checks. + +`Windows2025Gen2TrustedLaunch` continues to validate the dedicated +`windows-2025-gen2-tl` image during the shared-image rollout. +The shared-image scenarios run in both the Windows VHD-build gate, using that +build's image, and the general Windows E2E job, using the main-tagged test image. + ## VM size configuration Resource SKU catalogues are loaded once per location using the runner's single diff --git a/e2e/scenario/scenario_win.go b/e2e/scenario/scenario_win.go index 31fc797d187..431b4c78df3 100644 --- a/e2e/scenario/scenario_win.go +++ b/e2e/scenario/scenario_win.go @@ -140,6 +140,33 @@ var _ = Register(&Scenario{ }, }) +var _ = Register(&Scenario{ + Name: "Windows2022Gen2_TrustedLaunch_SharedImage", + Description: "Windows Server 2022 Gen2 Trusted Launch (Secure Boot + vTPM)", + Config: Config{ + Cluster: ClusterAzureNetwork, + VHD: config.VHDWindows2022ContainerdGen2, + VMConfigMutator: func(vmss *armcompute.VirtualMachineScaleSet) { + vmss.Properties = addTrustedLaunchToVMSS(vmss.Properties) + }, + BootstrapConfigMutator: EmptyBootstrapConfigMutator, + Validator: func(ctx context.Context, s *Scenario) error { + return errors.Join( + ValidateWindowsVersionFromWindowsSettings(ctx, s, "2022-containerd-gen2"), + ValidateWindowsProductName(ctx, s, "Windows Server 2022 Datacenter"), + ValidateWindowsDisplayVersion(ctx, s, "21H2"), + ValidateFileHasContent(ctx, s, "/k/kubeletstart.ps1", "--container-runtime=remote"), + ValidateKubeletArgs(ctx, s), + ValidateContainerdWindowsPriorityClass(ctx, s), + ValidateCiliumIsNotRunningWindows(ctx, s), + ValidateDotnetNotInstalledWindows(ctx, s), + ValidateWindowsSystemServicesRestartConfiguration(ctx, s), + ValidateCollectWindowsLogsScript(ctx, s), + ) + }, + }, +}) + var _ = Register(&Scenario{ Name: "Windows2022Gen2AzureOverlayNetworkDualStack", SkipReason: "Dual stack tests are not working yet", @@ -220,34 +247,45 @@ var _ = Register(&Scenario{ }, }) -var _ = Register(&Scenario{ - Name: "Windows2025Gen2TrustedLaunch", - Description: "Windows Server 2025 Gen2 Trusted Launch (Secure Boot + vTPM)", - Config: Config{ - Cluster: ClusterAzureNetwork, - VHD: config.VHDWindows2025Gen2TL, - VMConfigMutator: func(vmss *armcompute.VirtualMachineScaleSet) { - vmss.Properties = addTrustedLaunchToVMSS(vmss.Properties) - }, - BootstrapConfigMutatorWithError: func(_ context.Context, _ *Cluster, configuration *datamodel.NodeBootstrappingConfiguration) error { - return Windows2025BootstrapConfigMutator(configuration) - }, - Validator: func(ctx context.Context, s *Scenario) error { - return errors.Join( - ValidateWindowsVersionFromWindowsSettings(ctx, s, "2025-gen2-tl"), - ValidateWindowsProductName(ctx, s, "Windows Server 2025 Datacenter"), - ValidateWindowsDisplayVersion(ctx, s, "24H2"), - ValidateFileHasContent(ctx, s, "/k/kubeletstart.ps1", "--container-runtime=remote"), - ValidateKubeletArgs(ctx, s), - ValidateContainerdWindowsPriorityClass(ctx, s), - ValidateCiliumIsNotRunningWindows(ctx, s), - ValidateDotnetNotInstalledWindows(ctx, s), - ValidateWindowsSystemServicesRestartConfiguration(ctx, s), - ValidateCollectWindowsLogsScript(ctx, s), - ) - }, - }, -}) +func init() { + for _, tc := range []struct { + name string + vhd *config.Image + windowsSKU string + }{ + {"Windows2025Gen2TrustedLaunch", config.VHDWindows2025Gen2TL, "2025-gen2-tl"}, + {"Windows2025Gen2_TrustedLaunch_SharedImage", config.VHDWindows2025Gen2, "2025-gen2"}, + } { + Register(&Scenario{ + Name: tc.name, + Description: "Windows Server 2025 Gen2 Trusted Launch (Secure Boot + vTPM)", + Config: Config{ + Cluster: ClusterAzureNetwork, + VHD: tc.vhd, + VMConfigMutator: func(vmss *armcompute.VirtualMachineScaleSet) { + vmss.Properties = addTrustedLaunchToVMSS(vmss.Properties) + }, + BootstrapConfigMutatorWithError: func(_ context.Context, _ *Cluster, configuration *datamodel.NodeBootstrappingConfiguration) error { + return Windows2025BootstrapConfigMutator(configuration) + }, + Validator: func(ctx context.Context, s *Scenario) error { + return errors.Join( + ValidateWindowsVersionFromWindowsSettings(ctx, s, tc.windowsSKU), + ValidateWindowsProductName(ctx, s, "Windows Server 2025 Datacenter"), + ValidateWindowsDisplayVersion(ctx, s, "24H2"), + ValidateFileHasContent(ctx, s, "/k/kubeletstart.ps1", "--container-runtime=remote"), + ValidateKubeletArgs(ctx, s), + ValidateContainerdWindowsPriorityClass(ctx, s), + ValidateCiliumIsNotRunningWindows(ctx, s), + ValidateDotnetNotInstalledWindows(ctx, s), + ValidateWindowsSystemServicesRestartConfiguration(ctx, s), + ValidateCollectWindowsLogsScript(ctx, s), + ) + }, + }, + }) + } +} var _ = Register(&Scenario{ Name: "Windows2025Gen2_WindowsCiliumNetworking", diff --git a/spec/vhdbuilder/packer/ensure_sig_trusted_launch_supported_spec.sh b/spec/vhdbuilder/packer/ensure_sig_trusted_launch_supported_spec.sh new file mode 100644 index 00000000000..403f77f76da --- /dev/null +++ b/spec/vhdbuilder/packer/ensure_sig_trusted_launch_supported_spec.sh @@ -0,0 +1,291 @@ +#!/bin/bash +# shellcheck disable=SC2329 + +Describe 'ensure_sig_trusted_launch_supported' + Include './vhdbuilder/packer/produce-packer-settings-functions.sh' + + setup() { + TEST_DIR="${SHELLSPEC_WORKDIR}/trusted-launch.${SHELLSPEC_EXAMPLE_NO}" + mkdir -p "${TEST_DIR}" + AZ_CALLS="${TEST_DIR}/az-calls" + UPDATE_FEATURES="${TEST_DIR}/update-features" + : > "${AZ_CALLS}" + AZURE_RESOURCE_GROUP_NAME="test-rg" + SIG_GALLERY_NAME="test-gallery" + SIG_IMAGE_NAME="windows-2025-gen2" + SIG_IMAGE_VERSION="26100.1.261006" + DEFINITION_ID="/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Compute/galleries/test-gallery/images/windows-2025-gen2" + INITIAL_DEFINITION='{"id":"definition-id","name":"windows-2025-gen2","type":"Microsoft.Compute/galleries/images","location":"eastus","tags":{"preserve":"true"},"properties":{"hyperVGeneration":"V2","provisioningState":"Succeeded","features":[{"name":"DiskControllerTypes","value":"SCSI,NVMe"}]}}' + UPDATED_DEFINITION='{"properties":{"provisioningState":"Succeeded","features":[{"name":"SecurityType","value":"TrustedLaunchSupported","startsAtVersion":"26100.1.261006"}]}}' + FAIL_OPERATION="" + } + BeforeEach 'setup' + + az() { + printf '%s\n' "$*" >> "${AZ_CALLS}" + case "$1 $2 $3" in + 'resource show --ids') + if [ "${FAIL_OPERATION}" = "show" ]; then return 1; fi + if [ -f "${TEST_DIR}/created-definition" ]; then + cat "${TEST_DIR}/created-definition" + else + printf '%s\n' "${INITIAL_DEFINITION}" + fi + ;; + 'resource update --ids') + if [ "${FAIL_OPERATION}" = "update" ]; then return 1; fi + while [ "$#" -gt 0 ]; do + case "$1" in + properties.features=*) + printf '%s\n' "${1#properties.features=}" > "${UPDATE_FEATURES}" + break + ;; + esac + shift + done + printf '%s\n' "${UPDATED_DEFINITION}" + ;; + 'sig show --resource-group') + printf '%s\n' '{"provisioningState":"Succeeded"}' + ;; + 'sig image-definition show') + if [ ! -f "${TEST_DIR}/created-definition" ]; then return 1; fi + printf '{"id":"%s"}\n' "${DEFINITION_ID}" + ;; + 'sig image-definition create') + if [ "${FAIL_OPERATION}" = "create" ]; then return 23; fi + local security_type="Standard" + case "$*" in *SecurityType=TrustedLaunchSupported*) security_type="TrustedLaunchSupported" ;; esac + jq --arg security_type "${security_type}" ' + .properties.features += [{name: "SecurityType", value: $security_type}] + ' <<<"${INITIAL_DEFINITION}" > "${TEST_DIR}/created-definition" + ;; + *) return 1 ;; + esac + } + + It 'adds versioned capability while preserving the existing definition' + When call ensure_sig_trusted_launch_supported "${DEFINITION_ID}" + The status should be success + The output should include 'from version 26100.1.261006' + The contents of file "${AZ_CALLS}" should include "resource show --ids ${DEFINITION_ID} --api-version 2024-03-03" + The contents of file "${AZ_CALLS}" should include "resource update --ids ${DEFINITION_ID} --api-version 2024-03-03 --set properties.allowUpdateImage=true properties.features=" + The contents of file "${AZ_CALLS}" should include '--remove properties.provisioningState' + The contents of file "${AZ_CALLS}" should not include '--no-wait' + The contents of file "${AZ_CALLS}" should not include 'image-definition wait' + The value "$(grep -c '^resource show ' "${AZ_CALLS}")" should equal 1 + The value "$(jq -c '.' "${UPDATE_FEATURES}")" should equal '[{"name":"DiskControllerTypes","value":"SCSI,NVMe"},{"name":"SecurityType","value":"TrustedLaunchSupported","startsAtVersion":"26100.1.261006"}]' + End + + It 'creates a new shared Windows definition capable directly without a migration' + MODE="windowsVhdMode" + ARCHITECTURE="x64" + FEATURE_FLAGS="" + HYPERV_GENERATION="V2" + OS_TYPE="Windows" + AZURE_LOCATION="eastus" + ENABLE_TRUSTED_LAUNCH="False" + TRUSTED_LAUNCH_SUPPORTED="True" + + When call ensure_sig_vhd_exists + The status should be success + The output should include 'Creating image definition windows-2025-gen2' + The output should not include 'Updating image definition' + The stderr should equal '' + The contents of file "${AZ_CALLS}" should include 'DiskControllerTypes=SCSI,NVMe SecurityType=TrustedLaunchSupported' + The contents of file "${AZ_CALLS}" should not include 'resource update' + The contents of file "${AZ_CALLS}" should not include 'image-definition wait' + The value "$(grep -c '^sig image-definition create ' "${AZ_CALLS}")" should equal 1 + The value "$(grep -c '^sig image-definition show ' "${AZ_CALLS}")" should equal 1 + The file "${UPDATE_FEATURES}" should not be exist + The value "$(jq -c '.properties.features' "${TEST_DIR}/created-definition")" should equal '[{"name":"DiskControllerTypes","value":"SCSI,NVMe"},{"name":"SecurityType","value":"TrustedLaunchSupported"}]' + End + + It 'propagates a new shared Windows definition creation failure without migration' + MODE="windowsVhdMode" + ARCHITECTURE="x64" + FEATURE_FLAGS="" + HYPERV_GENERATION="V2" + OS_TYPE="Windows" + AZURE_LOCATION="eastus" + ENABLE_TRUSTED_LAUNCH="False" + TRUSTED_LAUNCH_SUPPORTED="True" + FAIL_OPERATION="create" + + When call ensure_sig_vhd_exists + The status should equal 23 + The output should include 'Creating image definition windows-2025-gen2' + The contents of file "${AZ_CALLS}" should not include 'resource update' + The contents of file "${AZ_CALLS}" should not include 'image-definition wait' + The file "${UPDATE_FEATURES}" should not be exist + End + + It 'migrates an existing Standard staging definition during build setup' + MODE="windowsVhdMode" + ENABLE_TRUSTED_LAUNCH="False" + TRUSTED_LAUNCH_SUPPORTED="True" + jq '.properties.features += [{name:"SecurityType",value:"Standard"}]' \ + <<<"${INITIAL_DEFINITION}" > "${TEST_DIR}/created-definition" + + When call ensure_sig_vhd_exists + The status should be success + The output should include 'Updating image definition' + The stderr should equal '' + The contents of file "${AZ_CALLS}" should not include 'image-definition create' + The contents of file "${AZ_CALLS}" should include '--set properties.allowUpdateImage=true properties.features=' + The value "$(grep -c '^resource show ' "${AZ_CALLS}")" should equal 1 + The value "$(jq -c '.' "${UPDATE_FEATURES}")" should equal '[{"name":"DiskControllerTypes","value":"SCSI,NVMe"},{"name":"SecurityType","value":"TrustedLaunchSupported","startsAtVersion":"26100.1.261006"}]' + End + + It 'does not move the boundary of an already-supported definition' + INITIAL_DEFINITION='{"properties":{"hyperVGeneration":"V2","features":[{"name":"SecurityType","value":"TrustedLaunchSupported","startsAtVersion":"26100.1.260901"}]}}' + + When call ensure_sig_trusted_launch_supported "${DEFINITION_ID}" + The status should be success + The output should equal '' + The contents of file "${AZ_CALLS}" should not include 'resource update' + The file "${UPDATE_FEATURES}" should not be exist + End + + Describe 'existing capability without a boundary' + Parameters + absent '{"name":"SecurityType","value":"TrustedLaunchSupported"}' + null '{"name":"SecurityType","value":"TrustedLaunchSupported","startsAtVersion":null}' + End + + It "accepts an $1 boundary without modifying the staging definition" + INITIAL_DEFINITION=$(jq --argjson feature "$2" ' + .properties.features += [$feature] + ' <<<"${INITIAL_DEFINITION}") + + When call ensure_sig_trusted_launch_supported "${DEFINITION_ID}" + The status should be success + The output should equal '' + The stderr should equal '' + The contents of file "${AZ_CALLS}" should not include 'resource update' + The contents of file "${AZ_CALLS}" should not include 'image-definition wait' + The file "${UPDATE_FEATURES}" should not be exist + End + End + + It 'reuses an existing supported staging definition without a boundary during build setup' + MODE="windowsVhdMode" + ENABLE_TRUSTED_LAUNCH="False" + TRUSTED_LAUNCH_SUPPORTED="True" + jq '.properties.features += [{name:"SecurityType",value:"TrustedLaunchSupported"}]' \ + <<<"${INITIAL_DEFINITION}" > "${TEST_DIR}/created-definition" + + When call ensure_sig_vhd_exists + The status should be success + The output should include 'Image definition windows-2025-gen2 existing' + The stderr should equal '' + The contents of file "${AZ_CALLS}" should not include 'resource update' + The contents of file "${AZ_CALLS}" should not include 'image-definition wait' + The contents of file "${AZ_CALLS}" should not include 'image-definition create' + The file "${UPDATE_FEATURES}" should not be exist + End + + Describe 'valid existing capability boundaries' + Parameters + equal '26100.1.261006' + numeric '26100.1.9' + minor '26100.0.999999' + End + + It "preserves an existing $1 boundary at or below the build version" + INITIAL_DEFINITION=$(jq --arg version "$2" '.properties.hyperVGeneration = "V2" | .properties.features[0].startsAtVersion = $version' <<<"${UPDATED_DEFINITION}") + + When call ensure_sig_trusted_launch_supported "${DEFINITION_ID}" + The status should be success + The output should equal '' + The contents of file "${AZ_CALLS}" should not include 'resource update' + The file "${UPDATE_FEATURES}" should not be exist + End + End + + Describe 'invalid existing capability boundaries' + Parameters + empty '' + malformed 'invalid' + short '26100.1' + nonnumeric '26100.1.bad' + future '26100.1.261007' + futureminor '26100.10.1' + outofrange '26100.1.2147483648' + End + + It "rejects an existing $1 boundary without updating it" + INITIAL_DEFINITION=$(jq --arg version "$2" ' + .properties.hyperVGeneration = "V2" | + .properties.features[0].startsAtVersion = $version + ' <<<"${UPDATED_DEFINITION}") + + When call ensure_sig_trusted_launch_supported "${DEFINITION_ID}" + The status should be failure + The stderr should include 'cannot support build version' + The output should equal '' + The contents of file "${AZ_CALLS}" should not include 'resource update' + The file "${UPDATE_FEATURES}" should not be exist + End + End + + It 'refuses to mark a Gen1 definition as capable' + INITIAL_DEFINITION='{"properties":{"hyperVGeneration":"V1","features":[]}}' + + When call ensure_sig_trusted_launch_supported "${DEFINITION_ID}" + The status should be failure + The stderr should include 'must be Gen2' + The contents of file "${AZ_CALLS}" should not include 'resource update' + End + + Describe 'Azure failures' + Parameters + show 'Failed to read image definition' + update 'Failed to update Trusted Launch support' + End + + It "stops when $1 fails" + FAIL_OPERATION="$1" + + When call ensure_sig_trusted_launch_supported "${DEFINITION_ID}" + The status should be failure + The stderr should include "$2" + The output should not include 'did not converge' + End + End + + It 'rejects a completed update with incorrect capability' + UPDATED_DEFINITION='{"properties":{"provisioningState":"Succeeded","features":[{"name":"SecurityType","value":"TrustedLaunch"}]}}' + + When call ensure_sig_trusted_launch_supported "${DEFINITION_ID}" + The status should be failure + The output should include 'Updating image definition' + The stderr should include 'did not converge to TrustedLaunchSupported' + End + + It 'rejects an update that has not succeeded' + UPDATED_DEFINITION=$(jq '.properties.provisioningState = "Updating"' <<<"${UPDATED_DEFINITION}") + + When call ensure_sig_trusted_launch_supported "${DEFINITION_ID}" + The status should be failure + The output should include 'Updating image definition' + The stderr should include 'did not converge to TrustedLaunchSupported' + End + + Describe 'returned capability boundary' + Parameters + missing '{"properties":{"provisioningState":"Succeeded","features":[{"name":"SecurityType","value":"TrustedLaunchSupported"}]}}' + older '{"properties":{"provisioningState":"Succeeded","features":[{"name":"SecurityType","value":"TrustedLaunchSupported","startsAtVersion":"26100.1.260901"}]}}' + newer '{"properties":{"provisioningState":"Succeeded","features":[{"name":"SecurityType","value":"TrustedLaunchSupported","startsAtVersion":"26100.1.261007"}]}}' + End + + It "rejects a completed update with a $1 boundary" + UPDATED_DEFINITION="$2" + + When call ensure_sig_trusted_launch_supported "${DEFINITION_ID}" + The status should be failure + The output should include 'Updating image definition' + The stderr should include 'did not converge to TrustedLaunchSupported' + End + End +End diff --git a/spec/vhdbuilder/packer/ensure_sig_vhd_exists_spec.sh b/spec/vhdbuilder/packer/ensure_sig_vhd_exists_spec.sh index ea07aa4c7aa..0ae454c107b 100644 --- a/spec/vhdbuilder/packer/ensure_sig_vhd_exists_spec.sh +++ b/spec/vhdbuilder/packer/ensure_sig_vhd_exists_spec.sh @@ -18,6 +18,7 @@ Describe 'ensure_sig_vhd_exists function' HYPERV_GENERATION="" OS_TYPE="" ENABLE_TRUSTED_LAUNCH="" + TRUSTED_LAUNCH_SUPPORTED="False" # Mock variables to control az command behavior MOCK_AZ_SIG_SHOW_STATE="" @@ -61,6 +62,7 @@ Describe 'ensure_sig_vhd_exists function' return 0 ;; "create") + MOCK_AZ_SIG_IMAGE_DEFINITION_EXISTS="true" echo "az $*" return 0 ;; @@ -125,7 +127,84 @@ Describe 'ensure_sig_vhd_exists function' BeforeEach 'setup_environment' + Describe 'shared Windows Gen2 image creation' + Parameters + windows-2022-containerd-gen2 False True TrustedLaunchSupported + windows-2025-gen2 False True TrustedLaunchSupported + windows-2025-gen2-tl True True TrustedLaunch + windows-2025-gen2 False False '' + End + + It "creates $1 with TL enabled=$2 supported=$3" + MODE="windowsVhdMode" + AZURE_RESOURCE_GROUP_NAME="test-rg" + SIG_GALLERY_NAME="test-gallery" + SIG_IMAGE_NAME="$1" + AZURE_LOCATION="eastus" + OS_TYPE="Windows" + HYPERV_GENERATION="V2" + ARCHITECTURE="x64" + ENABLE_TRUSTED_LAUNCH="$2" + TRUSTED_LAUNCH_SUPPORTED="$3" + MOCK_AZ_SIG_SHOW_EXISTS="true" + MOCK_AZ_SIG_SHOW_STATE="Succeeded" + MOCK_AZ_SIG_IMAGE_DEFINITION_EXISTS="false" + + # shellcheck disable=SC2329 + ensure_sig_trusted_launch_supported() { + echo "Reconciled $1" + } + + When call ensure_sig_vhd_exists + The status should be success + The output should include "--gallery-image-definition $1" + The output should include 'DiskControllerTypes=SCSI,NVMe' + if [ -n "$4" ]; then + The output should include "SecurityType=$4" + else + The output should not include 'SecurityType=' + fi + The output should not include 'Reconciled' + End + End + Describe 'Basic function execution' + It 'reconciles capability for an existing shared Windows Gen2 definition' + MODE="windowsVhdMode" + ENABLE_TRUSTED_LAUNCH="False" + TRUSTED_LAUNCH_SUPPORTED="True" + MOCK_AZ_SIG_SHOW_EXISTS="true" + MOCK_AZ_SIG_SHOW_STATE="Succeeded" + MOCK_AZ_SIG_IMAGE_DEFINITION_EXISTS="true" + + # shellcheck disable=SC2329 + ensure_sig_trusted_launch_supported() { + echo "Reconciled $1" + } + + When call ensure_sig_vhd_exists + The status should be success + The output should include 'Reconciled mock-value' + End + + It 'propagates an existing shared Windows capability update failure' + MODE="windowsVhdMode" + ENABLE_TRUSTED_LAUNCH="False" + TRUSTED_LAUNCH_SUPPORTED="True" + MOCK_AZ_SIG_SHOW_EXISTS="true" + MOCK_AZ_SIG_SHOW_STATE="Succeeded" + MOCK_AZ_SIG_IMAGE_DEFINITION_EXISTS="true" + + # shellcheck disable=SC2329 + ensure_sig_trusted_launch_supported() { + return 1 + } + + When call ensure_sig_vhd_exists + The status should be failure + The output should include 'Image definition' + End + It 'should complete successfully with minimal setup when gallery does not exist' MODE="linuxVhdMode" AZURE_RESOURCE_GROUP_NAME="test-rg" diff --git a/spec/vhdbuilder/packer/generate_vhd_publishing_info_spec.sh b/spec/vhdbuilder/packer/generate_vhd_publishing_info_spec.sh index 8edecbcf47b..e5cca863c26 100644 --- a/spec/vhdbuilder/packer/generate_vhd_publishing_info_spec.sh +++ b/spec/vhdbuilder/packer/generate_vhd_publishing_info_spec.sh @@ -4,7 +4,8 @@ Describe 'generate-vhd-publishing-info.sh' setup() { SCRIPT_PATH="${PWD}/vhdbuilder/packer/generate-vhd-publishing-info.sh" - TEST_DIR=$(mktemp -d "${SHELLSPEC_WORKDIR}/publishing-info.XXXXXX") + TEST_DIR="${SHELLSPEC_WORKDIR}/publishing-info.${SHELLSPEC_EXAMPLE_NO}" + mkdir -p "${TEST_DIR}" OUTPUT="${TEST_DIR}/vhd-publishing-info.json" export AZ_CALLS="${TEST_DIR}/az-calls" : > "$AZ_CALLS" @@ -221,6 +222,21 @@ Describe 'generate-vhd-publishing-info.sh' End Describe 'Windows output' + Parameters + windows-2022-containerd-gen2 + windows-2025-gen2 + End + + It "publishes $1 capability without relying on a TL image name" + OS_NAME=windows OFFER_NAME=Windows SKU_NAME="$1" SECURITY_TYPE_FEATURE=TrustedLaunchSupported + AZ_VERSIONS='' + + When call generate_publishing_info + The status should be success + The output should include 'COPY ME ---> https://example.invalid/images/test.vhd' + The value "$(read_metadata '[.os_name, .sku_name, .hyperv_generation, .security_type_feature]')" should equal "[\"windows\",\"$1\",\"V2\",\"TrustedLaunchSupported\"]" + End + Parameters '' stale-gallery diff --git a/spec/vhdbuilder/packer/resolve_security_type_feature_spec.sh b/spec/vhdbuilder/packer/resolve_security_type_feature_spec.sh new file mode 100644 index 00000000000..d35beee4ebb --- /dev/null +++ b/spec/vhdbuilder/packer/resolve_security_type_feature_spec.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +Describe 'resolve_security_type_feature' + Include './vhdbuilder/packer/produce-packer-settings-functions.sh' + + Parameters + False True None TrustedLaunchSupported + True True None TrustedLaunch + True False None TrustedLaunch + False False cvm ConfidentialVMSupported + False False None Standard + true true None TrustedLaunch + false true None TrustedLaunchSupported + End + + It "resolves enabled=$1 supported=$2 flags=$3 to $4" + ENABLE_TRUSTED_LAUNCH="$1" + TRUSTED_LAUNCH_SUPPORTED="$2" + FEATURE_FLAGS="$3" + + When call resolve_security_type_feature + The status should be success + The variable SECURITY_TYPE_FEATURE should equal "$4" + End +End diff --git a/vhdbuilder/packer/README.md b/vhdbuilder/packer/README.md index dc5523e20be..a20b3bab0fd 100644 --- a/vhdbuilder/packer/README.md +++ b/vhdbuilder/packer/README.md @@ -17,6 +17,23 @@ external-vhd-src->(func/init: ensure dest existing/static gallery/definition, in # Roadmap Goal1: remove mariner workflow so things will be simplified. +# Shared Windows Gen2 staging definitions + +The Windows build template sets `trustedLaunchSupported` for every Gen2 image. +Ordinary WS2022 and WS2025 Gen2 images advertise `TrustedLaunchSupported`, so the +same image can serve Standard and Trusted Launch virtual machines. Gen1 is unchanged. +New staging definitions are created with that capability directly, without a follow-up +capability update or `startsAtVersion` boundary because they have no old versions. +Dedicated Trusted Launch builds and their metadata precedence are unchanged. +This image capability does not change AKS RP's OS eligibility checks. + +An existing Standard definition is updated in place, preserving unrelated features +and setting `startsAtVersion` to the current build version. Azure CLI performs the +update and waits for completion; the builder checks the returned capability and +boundary. Already-capable definitions with an absent or null boundary are reused +without a write or boundary backfill. +An explicit boundary must be valid and at or below the current build version. + # Linux build steps The pipeline's `Build Go binaries` step runs `make -f packer.mk -j4 build-tools` diff --git a/vhdbuilder/packer/produce-packer-settings-functions.sh b/vhdbuilder/packer/produce-packer-settings-functions.sh index e2173dd5a37..ddc1302f863 100644 --- a/vhdbuilder/packer/produce-packer-settings-functions.sh +++ b/vhdbuilder/packer/produce-packer-settings-functions.sh @@ -465,8 +465,62 @@ function prepare_windows_vhd() { fi } +function ensure_sig_trusted_launch_supported() { + local definition_id="$1" + local api_version="2024-03-03" + local definition + if ! definition=$(az resource show --ids "${definition_id}" --api-version "${api_version}" -o json); then + echo "Failed to read image definition ${definition_id} for Trusted Launch support" >&2 + return 1 + fi + + local security_type + security_type=$(jq -r '(.properties.features // []) | map(select(.name == "SecurityType")) | .[0].value // ""' <<<"${definition}") + if [ "${security_type}" = "TrustedLaunchSupported" ]; then + # Existing staging definitions may advertise support without a version boundary. + if ! jq -e --arg version "${SIG_IMAGE_VERSION:-}" ' + def gallery_version: + if type == "string" and test("^[0-9]+\\.[0-9]+\\.[0-9]+$") then + split(".") | map(tonumber) | + if all(.[]; . <= 2147483647) then . else error("gallery version component out of range") end + else error("invalid gallery image version") end; + (.properties.features | map(select(.name == "SecurityType")) | .[0].startsAtVersion) as $start + | if $start == null then true + else ($start | gallery_version) <= ($version | gallery_version) end + ' <<<"${definition}" >/dev/null; then + echo "Image definition ${definition_id} cannot support build version ${SIG_IMAGE_VERSION:-}: invalid or newer Trusted Launch boundary" >&2 + return 1 + fi + return 0 + fi + if [ "$(jq -r '.properties.hyperVGeneration' <<<"${definition}")" != "V2" ]; then + echo "Image definition ${definition_id} must be Gen2 to support Trusted Launch" >&2 + return 1 + fi + + local features + features=$(jq -c --arg version "${SIG_IMAGE_VERSION:?SIG_IMAGE_VERSION is required to update Trusted Launch support}" ' + ((.properties.features // []) | map(select(.name != "SecurityType"))) + + [{name: "SecurityType", value: "TrustedLaunchSupported", startsAtVersion: $version}] + ' <<<"${definition}") || return 1 + echo "Updating image definition ${definition_id} to TrustedLaunchSupported from version ${SIG_IMAGE_VERSION}" + if ! definition=$(az resource update --ids "${definition_id}" --api-version "${api_version}" \ + --set properties.allowUpdateImage=true "properties.features=${features}" \ + --remove properties.provisioningState -o json); then + echo "Failed to update Trusted Launch support for ${definition_id}" >&2 + return 1 + fi + if ! jq -e --arg version "${SIG_IMAGE_VERSION}" '.properties.provisioningState == "Succeeded" and + any(.properties.features[]; .name == "SecurityType" and .value == "TrustedLaunchSupported" and .startsAtVersion == $version)' \ + <<<"${definition}" >/dev/null; then + echo "Image definition ${definition_id} did not converge to TrustedLaunchSupported from version ${SIG_IMAGE_VERSION}" >&2 + return 1 + fi +} + function ensure_sig_vhd_exists() { echo "SIG existence checking for $MODE" + local supported_features="DiskControllerTypes=SCSI,NVMe SecurityType=TrustedLaunchSupported" is_need_create=true state=$(az sig show --resource-group "${AZURE_RESOURCE_GROUP_NAME}" --gallery-name "${SIG_GALLERY_NAME}" | jq -r '.provisioningState') || state="" @@ -567,7 +621,7 @@ function ensure_sig_vhd_exists() { --hyper-v-generation ${HYPERV_GENERATION} \ --location ${AZURE_LOCATION} \ --architecture Arm64 \ - --features "DiskControllerTypes=SCSI,NVMe SecurityType=TrustedLaunchSupported" + --features "${supported_features}" else az sig image-definition create \ --resource-group ${AZURE_RESOURCE_GROUP_NAME} \ @@ -648,7 +702,7 @@ function ensure_sig_vhd_exists() { --os-type ${OS_TYPE} \ --hyper-v-generation ${HYPERV_GENERATION} \ --location ${AZURE_LOCATION} \ - --features "DiskControllerTypes=SCSI,NVMe SecurityType=TrustedLaunchSupported" + --features "${supported_features}" else # For vanilla Gen2, mark only NVMe az sig image-definition create \ @@ -664,7 +718,18 @@ function ensure_sig_vhd_exists() { --features DiskControllerTypes=SCSI,NVMe fi fi + local create_status=$? + if [ "${create_status}" -ne 0 ]; then + return "${create_status}" + fi else echo "Image definition ${SIG_IMAGE_NAME} existing in gallery ${SIG_GALLERY_NAME} resource group ${AZURE_RESOURCE_GROUP_NAME}" + if [ "${MODE}" = "windowsVhdMode" ] && + [ "${ENABLE_TRUSTED_LAUNCH,,}" != "true" ] && + [ "${TRUSTED_LAUNCH_SUPPORTED,,}" = "true" ]; then + local definition_id + definition_id=$(jq -r '.id' <<<"${id}") + ensure_sig_trusted_launch_supported "${definition_id}" || return 1 + fi fi }