diff --git a/staging/cse/windows/containerdfunc.ps1 b/staging/cse/windows/containerdfunc.ps1 index ed8adb5cbaf..79f132ff4b0 100644 --- a/staging/cse/windows/containerdfunc.ps1 +++ b/staging/cse/windows/containerdfunc.ps1 @@ -321,8 +321,9 @@ function Install-Containerd { Logs-To-Event -TaskName "AKS.WindowsCSE.DownloadContainerdWithOras" -TaskMessage "Start to download containerd with oras. ContainerdVersionTag: $containerdVersionTag, BootstrapProfileContainerRegistryServer: $global:BootstrapProfileContainerRegistryServer" $orasReference = "$sanitizedRegistry/aks/packages/containerd/containerd:$containerdVersionTag" + $cachedFileName = Get-FileNameFromUrl -Url $ContainerdUrl try { - Retry-Command -Command "DownloadFileWithOras" -Args @{Reference=$orasReference; DestinationPath=$tarfile} -Retries 5 -RetryDelaySeconds 10 + Retry-Command -Command "DownloadFileWithOras" -Args @{Reference=$orasReference; DestinationPath=$tarfile; CachedFile=$cachedFileName} -Retries 5 -RetryDelaySeconds 10 } catch { Set-ExitCode -ExitCode $global:WINDOWS_CSE_ERROR_ORAS_PULL_CONTAINERD -ErrorMessage "Exhausted retries for oras pull $orasReference. Error: $_" } diff --git a/staging/cse/windows/kubeletfunc.ps1 b/staging/cse/windows/kubeletfunc.ps1 index 866f3e54515..149113d5e05 100644 --- a/staging/cse/windows/kubeletfunc.ps1 +++ b/staging/cse/windows/kubeletfunc.ps1 @@ -219,8 +219,9 @@ function Get-KubePackage { } Logs-To-Event -TaskName "AKS.WindowsCSE.DownloadKubeletBinariesWithOras" -TaskMessage "Start to download kubelet binaries with oras. KubeBinariesVersion: $global:KubeBinariesVersion, BootstrapProfileContainerRegistryServer: $global:BootstrapProfileContainerRegistryServer" $orasReference = "$($global:BootstrapProfileContainerRegistryServer)/aks/packages/kubernetes/windowszip:v$($global:KubeBinariesVersion)" + $cachedFileName = Get-FileNameFromUrl -Url $KubeBinariesSASURL try { - Retry-Command -Command "DownloadFileWithOras" -Args @{Reference=$orasReference; DestinationPath=$zipfile} -Retries 5 -RetryDelaySeconds 10 + Retry-Command -Command "DownloadFileWithOras" -Args @{Reference=$orasReference; DestinationPath=$zipfile; CachedFile=$cachedFileName} -Retries 5 -RetryDelaySeconds 10 } catch { Set-ExitCode -ExitCode $global:WINDOWS_CSE_ERROR_ORAS_PULL_WINDOWSZIP_FAIL -ErrorMessage "Exhausted retries for oras pull $orasReference. Error: $_" } diff --git a/staging/cse/windows/networkisolatedclusterfunc.ps1 b/staging/cse/windows/networkisolatedclusterfunc.ps1 index c869975b376..9dddc5eafc6 100644 --- a/staging/cse/windows/networkisolatedclusterfunc.ps1 +++ b/staging/cse/windows/networkisolatedclusterfunc.ps1 @@ -366,6 +366,14 @@ function Get-BootstrapRegistryDomainName { return $registryDomainName } +function Get-FileNameFromUrl { + param( + [Parameter(Mandatory = $true)][string]$Url + ) + $cleanUrl = $Url.Split('?')[0] + return [IO.Path]::GetFileName($cleanUrl) +} + function DownloadFileWithOras { Param( [Parameter(Mandatory = $true)][string] @@ -373,8 +381,28 @@ function DownloadFileWithOras { [Parameter(Mandatory = $true)][string] $DestinationPath, [Parameter(Mandatory = $false)][string] - $Platform = "windows/amd64" + $Platform = "windows/amd64", + [Parameter(Mandatory = $false)][string] + $CachedFile = "" ) + # If CachedFile is provided and exists, copy it to the destination path instead of downloading + # If NetworkIsolatedClusterTestMode is enabled (only in e2e test), skip using cached file to ensure we cover the download logic + if (-not [string]::IsNullOrWhiteSpace($CachedFile) -and (-not $global:NetworkIsolatedClusterTestMode)) { + $fileName = [IO.Path]::GetFileName($CachedFile) + + $search = @() + if ($global:CacheDir -and (Test-Path $global:CacheDir)) { + $search = [IO.Directory]::GetFiles($global:CacheDir, $fileName, [IO.SearchOption]::AllDirectories) + } + + if ($search.Count -ne 0) { + Write-Log "Using cached version of $fileName - Copying file from $($search[0]) to $DestinationPath" + Copy-Item -Path $search[0] -Destination $DestinationPath -Force + return + } + + Write-Log "Cached file $fileName was not found in cache directory '$($global:CacheDir)'. Falling back to oras pull." + } Write-Log "Downloading $Reference to $DestinationPath via oras pull (platform=$Platform)" diff --git a/staging/cse/windows/networkisolatedclusterfunc.tests.ps1 b/staging/cse/windows/networkisolatedclusterfunc.tests.ps1 index 7864b21363a..fad72ab28db 100644 --- a/staging/cse/windows/networkisolatedclusterfunc.tests.ps1 +++ b/staging/cse/windows/networkisolatedclusterfunc.tests.ps1 @@ -383,6 +383,32 @@ Describe "Get-BootstrapRegistryDomainName" { } } +Describe "Get-FileNameFromUrl" { + It "should return file name for url without query string" { + $url = "https://contoso.blob.core.windows.net/packages/windowszip.zip" + + Get-FileNameFromUrl -Url $url | Should -Be "windowszip.zip" + } + + It "should strip query string before extracting file name" { + $url = "https://contoso.blob.core.windows.net/packages/windowszip.zip?sv=2025-01-01&sig=token" + + Get-FileNameFromUrl -Url $url | Should -Be "windowszip.zip" + } + + It "should return the last segment for nested paths" { + $url = "https://contoso.blob.core.windows.net/packages/release/v1.30.0/kubernetes-node-image.tar.gz" + + Get-FileNameFromUrl -Url $url | Should -Be "kubernetes-node-image.tar.gz" + } + + It "should return empty when url ends with slash" { + $url = "https://contoso.blob.core.windows.net/packages/release/v1.30.0/" + + Get-FileNameFromUrl -Url $url | Should -Be "" + } +} + Describe "DownloadFileWithOras" { BeforeEach { $global:OrasPath = "Mock-OrasCli" @@ -472,4 +498,67 @@ Describe "DownloadFileWithOras" { { DownloadFileWithOras -Reference $reference -DestinationPath $destPath -Platform "linux/amd64" } | Should -Not -Throw } + + It "should copy from cache and skip oras pull when CachedFile is provided" { + $cacheRoot = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString()) + $cacheSubDir = Join-Path $cacheRoot "nested" + $cachedFileName = "windowszip.zip" + $cachedFilePath = Join-Path $cacheSubDir $cachedFileName + $destPath = "c:\k.zip" + $reference = "myregistry.azurecr.io/aks/packages/kubernetes/windowszip:1.29.2" + + New-Item -ItemType Directory -Path $cacheSubDir -Force | Out-Null + Set-Content -Path $cachedFilePath -Value "cached-content" -NoNewline + + $global:CacheDir = $cacheRoot + $script:orasInvoked = $false + function global:Mock-OrasCli { + param([Parameter(ValueFromRemainingArguments = $true)]$Args) + $script:orasInvoked = $true + $global:LASTEXITCODE = 0 + } + + Mock Copy-Item -MockWith {} + + try { + { DownloadFileWithOras -Reference $reference -DestinationPath $destPath -CachedFile $cachedFileName } | Should -Not -Throw + Assert-MockCalled -CommandName 'Copy-Item' -Exactly -Times 1 -ParameterFilter { + $Path -eq $cachedFilePath -and $Destination -eq $destPath -and $Force + } + Assert-MockCalled -CommandName 'Move-Item' -Times 0 + $script:orasInvoked | Should -Be $false + } + finally { + Remove-Item -Path $cacheRoot -Recurse -Force -ErrorAction SilentlyContinue + $global:CacheDir = "c:\akse-cache" + } + } + + It "should invoke oras pull and skip cache copy when CachedFile is provided but missing from cache" { + $cacheRoot = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString()) + $cachedFileName = "windowszip.zip" + $destPath = "c:\k.zip" + $reference = "myregistry.azurecr.io/aks/packages/kubernetes/windowszip:1.29.2" + + New-Item -ItemType Directory -Path $cacheRoot -Force | Out-Null + + $global:CacheDir = $cacheRoot + $script:orasInvoked = $false + function global:Mock-OrasCli { + param([Parameter(ValueFromRemainingArguments = $true)]$Args) + $script:orasInvoked = $true + $global:LASTEXITCODE = 0 + } + + Mock Copy-Item -MockWith {} + + try { + { DownloadFileWithOras -Reference $reference -DestinationPath $destPath -CachedFile $cachedFileName } | Should -Not -Throw + Assert-MockCalled -CommandName 'Copy-Item' -Times 0 + $script:orasInvoked | Should -Be $true + } + finally { + Remove-Item -Path $cacheRoot -Recurse -Force -ErrorAction SilentlyContinue + } + } }