diff --git a/parts/windows/windowscsehelper.ps1 b/parts/windows/windowscsehelper.ps1 index 2fbac973d0e..4061fb3582a 100644 --- a/parts/windows/windowscsehelper.ps1 +++ b/parts/windows/windowscsehelper.ps1 @@ -88,9 +88,10 @@ $global:WINDOWS_CSE_ERROR_ORAS_PULL_CREDENTIAL_PROVIDER=81 # exit code for error $global:WINDOWS_CSE_ERROR_ORAS_PULL_POD_INFRA_CONTAINER=82 # exit code for error pulling pause image with oras from registry $global:WINDOWS_CSE_ERROR_NETWORK_ISOLATED_CLUSTER_CSE_NOT_CACHED=83 # exit code for cse of network isolated cluster not cached $global:WINDOWS_CSE_ERROR_ORAS_PULL_CONTAINERD=84 # exit code for error pulling containerd artifact with oras from registry +$global:WINDOWS_CSE_ERROR_ORAS_PULL_PACKAGE=85 # exit code for general error pulling package with oras from registry # WINDOWS_CSE_ERROR_MAX_CODE is only used in unit tests to verify whether new error code name is added in $global:ErrorCodeNames # Please use the current value of WINDOWS_CSE_ERROR_MAX_CODE as the value of the new error code and increment it by 1 -$global:WINDOWS_CSE_ERROR_MAX_CODE=85 +$global:WINDOWS_CSE_ERROR_MAX_CODE=86 # Please add new error code for downloading new packages in RP code too $global:ErrorCodeNames = @( @@ -178,7 +179,8 @@ $global:ErrorCodeNames = @( "WINDOWS_CSE_ERROR_ORAS_PULL_CREDENTIAL_PROVIDER", "WINDOWS_CSE_ERROR_ORAS_PULL_POD_INFRA_CONTAINER", "WINDOWS_CSE_ERROR_NETWORK_ISOLATED_CLUSTER_CSE_NOT_CACHED", - "WINDOWS_CSE_ERROR_ORAS_PULL_CONTAINERD" + "WINDOWS_CSE_ERROR_ORAS_PULL_CONTAINERD", + "WINDOWS_CSE_ERROR_ORAS_PULL_PACKAGE" ) # The package domain to be used diff --git a/staging/cse/windows/azurecnifunc.ps1 b/staging/cse/windows/azurecnifunc.ps1 index 94836535241..a9bf2947084 100644 --- a/staging/cse/windows/azurecnifunc.ps1 +++ b/staging/cse/windows/azurecnifunc.ps1 @@ -16,7 +16,33 @@ function Install-VnetPlugins { # Download Azure VNET CNI plugins. # Mirror from https://github.com/Azure/azure-container-networking/releases $zipfile = [Io.path]::Combine("$AzureCNIDir", "azure-vnet.zip") - DownloadFileOverHttp -Url $VNetCNIPluginsURL -DestinationPath $zipfile -ExitCode $global:WINDOWS_CSE_ERROR_DOWNLOAD_CNI_PACKAGE + if ([string]::IsNullOrEmpty($global:BootstrapProfileContainerRegistryServer)) { + DownloadFileOverHttp -Url $VNetCNIPluginsURL -DestinationPath $zipfile -ExitCode $global:WINDOWS_CSE_ERROR_DOWNLOAD_CNI_PACKAGE + } + else { + # ni path + # Extract package name and version from URL for ORAS reference. + # URL format: https://packages.aks.azure.com/azure-cni/v${version}/binaries/-windows-amd64-v${version}.zip + # packageName examples include azure-vnet-cni, azure-vnet-cni-overlay, azure-vnet-cni-swift, + # and Windows single-tenancy variants such as azure-vnet-cni-singletenancy (including suffixed forms). + $packageInfo = Get-PackageNameAndVersionFromCniUrl -Url $VNetCNIPluginsURL + if (-not $packageInfo) { + Set-ExitCode -ExitCode $global:WINDOWS_CSE_ERROR_DOWNLOAD_CNI_PACKAGE -ErrorMessage "Failed to extract Azure VNet CNI package version tag from URL: $VNetCNIPluginsURL" + } + $cniPackageVersionTag = $packageInfo.Version + $orasPackageName = $packageInfo.PackageName + + Logs-To-Event -TaskName "AKS.WindowsCSE.DownloadAzureVnetCniWithOras" -TaskMessage "Start to download Azure VNet CNI with oras. CniPackageVersionTag: $cniPackageVersionTag, BootstrapProfileContainerRegistryServer: $global:BootstrapProfileContainerRegistryServer" + $orasReference = "$global:BootstrapProfileContainerRegistryServer/aks/packages/azure/${orasPackageName}:${cniPackageVersionTag}" + $cachedFileName = Get-FileNameFromUrl -Url $VNetCNIPluginsURL + try { + Retry-Command -Command "DownloadFileWithOras" -Args @{Reference = $orasReference; DestinationPath = $zipfile; CachedFile = $cachedFileName } -Retries 5 -RetryDelaySeconds 10 + } + catch { + # TODO: modify WINDOWS_CSE_ERROR_DOWNLOAD_CNI_PACKAGE to WINDOWS_CSE_ERROR_ORAS_PULL_PACKAGE after new VHD release + Set-ExitCode -ExitCode $global:WINDOWS_CSE_ERROR_DOWNLOAD_CNI_PACKAGE -ErrorMessage "Exhausted retries for oras pull $orasReference. Error: $_" + } + } AKS-Expand-Archive -path $zipfile -DestinationPath $AzureCNIBinDir del $zipfile @@ -26,6 +52,25 @@ function Install-VnetPlugins { move $AzureCNIBinDir/*.conflist $AzureCNIConfDir } +function Get-PackageNameAndVersionFromCniUrl { + Param( + [Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()][string] + $Url + ) + + # Expected format: + # https://packages.aks.azure.com/azure-cni/vX.Y.Z/binaries/-windows-amd64-vX.Y.Z[-suffix].zip + $pattern = '/binaries/(?.+)-windows-amd64-(?v[0-9]+(?:\.[0-9]+)*(?:-[A-Za-z0-9._-]+)?)\.zip$' + if ($Url -match $pattern) { + return [PSCustomObject]@{ + PackageName = $matches['PackageName'] + Version = $matches['Version'] + } + } + + return $null +} + function Set-AzureCNIConfig { Param( [Parameter(Mandatory = $true)][string] diff --git a/staging/cse/windows/azurecnifunc.tests.ps1 b/staging/cse/windows/azurecnifunc.tests.ps1 index 0194a2fa379..395587b9803 100644 --- a/staging/cse/windows/azurecnifunc.tests.ps1 +++ b/staging/cse/windows/azurecnifunc.tests.ps1 @@ -70,6 +70,177 @@ Describe 'GetBroadestRangesForEachAddress' { } } +Describe 'Get-PackageNameAndVersionFromCniUrl' { + It 'Should parse package name and version for azure-vnet-cni URL' { + $url = 'https://packages.aks.azure.com/azure-cni/v1.6.20/binaries/azure-vnet-cni-windows-amd64-v1.6.20.zip' + + $result = Get-PackageNameAndVersionFromCniUrl -Url $url + + $result | Should -Not -BeNullOrEmpty + $result.PackageName | Should -Be 'azure-vnet-cni' + $result.Version | Should -Be 'v1.6.20' + } + + It 'Should parse package name and version for azure-vnet-cni-swift URL' { + $url = 'https://packages.aks.azure.com/azure-cni/v1.6.20/binaries/azure-vnet-cni-swift-windows-amd64-v1.6.20.zip' + + $result = Get-PackageNameAndVersionFromCniUrl -Url $url + + $result | Should -Not -BeNullOrEmpty + $result.PackageName | Should -Be 'azure-vnet-cni-swift' + $result.Version | Should -Be 'v1.6.20' + } + + It 'Should parse package name and hotfix version suffix from URL' { + $url = 'https://packages.aks.azure.com/azure-cni/v1.6.1-hotfix20241024ApipaGW/binaries/azure-vnet-cni-windows-amd64-v1.6.1-hotfix20241024ApipaGW.zip' + + $result = Get-PackageNameAndVersionFromCniUrl -Url $url + + $result | Should -Not -BeNullOrEmpty + $result.PackageName | Should -Be 'azure-vnet-cni' + $result.Version | Should -Be 'v1.6.1-hotfix20241024ApipaGW' + } + + It 'Should return null for invalid URL format' { + $url = 'https://packages.aks.azure.com/azure-cni/v1.6.20/binaries/azure-vnet-cni-linux-amd64-v1.6.20.tar.gz' + + $result = Get-PackageNameAndVersionFromCniUrl -Url $url + + $result | Should -Be $null + } +} + +Describe 'Install-VnetPlugins ORAS path' { + BeforeAll { + # Stub functions from networkisolatedclusterfunc.ps1 that are not sourced in tests + if (-not (Get-Command 'Get-FileNameFromUrl' -ErrorAction SilentlyContinue)) { + function global:Get-FileNameFromUrl { param($Url) return "azure-vnet-cni-windows-amd64-v1.6.20.zip" } + } + if (-not (Get-Command 'DownloadFileWithOras' -ErrorAction SilentlyContinue)) { + function global:DownloadFileWithOras { + param( + [string]$Reference, + [string]$DestinationPath, + [string]$Platform = "windows/amd64", + [string]$CachedFile = "" + ) + } + } + } + + BeforeEach { + Mock Set-ExitCode -MockWith { + param($ExitCode, $ErrorMessage) + throw $ErrorMessage + } -Verifiable + Mock Logs-To-Event -MockWith { } -Verifiable + Mock Create-Directory -MockWith { } -Verifiable + Mock AKS-Expand-Archive -MockWith { } -Verifiable + Mock Get-FileNameFromUrl -MockWith { return "azure-vnet-cni-windows-amd64-v1.6.20.zip" } -Verifiable + Mock DownloadFileWithOras -MockWith { } -Verifiable + Mock Start-Sleep -MockWith { } -Verifiable + + # Suppress move and del which operate on files that don't exist in test + Mock Move-Item -MockWith { } -Verifiable + Mock Remove-Item -MockWith { } -Verifiable + + $global:AzureCNIDir = $TestDrive + } + + AfterEach { + $global:BootstrapProfileContainerRegistryServer = $null + } + + Context 'ORAS reference construction' { + It 'Should call DownloadFileWithOras with correct reference for azure-vnet-cni' { + $global:BootstrapProfileContainerRegistryServer = "myregistry.azurecr.io" + + Install-VnetPlugins -AzureCNIConfDir "$TestDrive\cniconf" -AzureCNIBinDir "$TestDrive\cnibin" ` + -VNetCNIPluginsURL "https://packages.aks.azure.com/azure-cni/v1.6.20/binaries/azure-vnet-cni-windows-amd64-v1.6.20.zip" + + Assert-MockCalled -CommandName "DownloadFileWithOras" -Exactly -Times 1 -ParameterFilter { + $Reference -eq "myregistry.azurecr.io/aks/packages/azure/azure-vnet-cni:v1.6.20" + } + } + + It 'Should call DownloadFileWithOras with correct reference for azure-vnet-cni-overlay' { + $global:BootstrapProfileContainerRegistryServer = "myregistry.azurecr.io" + + Install-VnetPlugins -AzureCNIConfDir "$TestDrive\cniconf" -AzureCNIBinDir "$TestDrive\cnibin" ` + -VNetCNIPluginsURL "https://packages.aks.azure.com/azure-cni/v1.6.20/binaries/azure-vnet-cni-overlay-windows-amd64-v1.6.20.zip" + + Assert-MockCalled -CommandName "DownloadFileWithOras" -Exactly -Times 1 -ParameterFilter { + $Reference -eq "myregistry.azurecr.io/aks/packages/azure/azure-vnet-cni-overlay:v1.6.20" + } + } + + It 'Should call DownloadFileWithOras with correct reference for azure-vnet-cni-swift' { + $global:BootstrapProfileContainerRegistryServer = "myregistry.azurecr.io" + + Install-VnetPlugins -AzureCNIConfDir "$TestDrive\cniconf" -AzureCNIBinDir "$TestDrive\cnibin" ` + -VNetCNIPluginsURL "https://packages.aks.azure.com/azure-cni/v1.6.20/binaries/azure-vnet-cni-swift-windows-amd64-v1.6.20.zip" + + Assert-MockCalled -CommandName "DownloadFileWithOras" -Exactly -Times 1 -ParameterFilter { + $Reference -eq "myregistry.azurecr.io/aks/packages/azure/azure-vnet-cni-swift:v1.6.20" + } + } + + It 'Should pass correct destination path to DownloadFileWithOras' { + $global:BootstrapProfileContainerRegistryServer = "myregistry.azurecr.io" + + Install-VnetPlugins -AzureCNIConfDir "$TestDrive\cniconf" -AzureCNIBinDir "$TestDrive\cnibin" ` + -VNetCNIPluginsURL "https://packages.aks.azure.com/azure-cni/v1.6.20/binaries/azure-vnet-cni-windows-amd64-v1.6.20.zip" + + $expectedZipPath = [Io.path]::Combine("$global:AzureCNIDir", "azure-vnet.zip") + Assert-MockCalled -CommandName "DownloadFileWithOras" -Exactly -Times 1 -ParameterFilter { + $DestinationPath -eq $expectedZipPath + } + } + } + + Context 'URL parse failure' { + It 'Should set exit code when URL format is invalid' { + $global:BootstrapProfileContainerRegistryServer = "myregistry.azurecr.io" + + { Install-VnetPlugins -AzureCNIConfDir "$TestDrive\cniconf" -AzureCNIBinDir "$TestDrive\cnibin" ` + -VNetCNIPluginsURL "https://packages.aks.azure.com/azure-cni/v1.6.20/binaries/invalid-linux-amd64-v1.6.20.tar.gz" } | Should -Throw "*Failed to extract*" + + Assert-MockCalled -CommandName "Set-ExitCode" -Exactly -Times 1 -ParameterFilter { + $ExitCode -eq $global:WINDOWS_CSE_ERROR_DOWNLOAD_CNI_PACKAGE + } + } + } + + Context 'ORAS pull failure' { + It 'Should set exit code on pull failure' { + $global:BootstrapProfileContainerRegistryServer = "myregistry.azurecr.io" + + Mock DownloadFileWithOras -MockWith { throw "oras pull failed" } + Mock Start-Sleep -MockWith { } + + { Install-VnetPlugins -AzureCNIConfDir "$TestDrive\cniconf" -AzureCNIBinDir "$TestDrive\cnibin" ` + -VNetCNIPluginsURL "https://packages.aks.azure.com/azure-cni/v1.6.20/binaries/azure-vnet-cni-windows-amd64-v1.6.20.zip" } | Should -Throw "*Exhausted retries*" + + Assert-MockCalled -CommandName "Set-ExitCode" -Exactly -Times 1 -ParameterFilter { + $ExitCode -eq $global:WINDOWS_CSE_ERROR_DOWNLOAD_CNI_PACKAGE + } + } + } + + Context 'HTTP download path' { + It 'Should use DownloadFileOverHttp when BootstrapProfileContainerRegistryServer is not set' { + $global:BootstrapProfileContainerRegistryServer = $null + + Mock DownloadFileOverHttp -MockWith { } -Verifiable + + Install-VnetPlugins -AzureCNIConfDir "$TestDrive\cniconf" -AzureCNIBinDir "$TestDrive\cnibin" ` + -VNetCNIPluginsURL "https://packages.aks.azure.com/azure-cni/v1.6.20/binaries/azure-vnet-cni-windows-amd64-v1.6.20.zip" + + Assert-MockCalled -CommandName "DownloadFileOverHttp" -Exactly -Times 1 + } + } +} + Describe 'Set-AzureCNIConfig' { BeforeEach { $azureCNIConfDir = "$PSScriptRoot\azurecnifunc.tests.suites"