diff --git a/staging/cse/windows/README b/staging/cse/windows/README index 82f932e5169..d108a291619 100644 --- a/staging/cse/windows/README +++ b/staging/cse/windows/README @@ -11,6 +11,10 @@ 1. Run unit test: - look at the pipeline for how to do this. It has a comment about how to use Tags to run only a particular test: - .github/workflows/validate-windows-ut.yml + - Example commands: + - `Invoke-Pester parts/windows/*.tests.ps1 -Passthru` + - `Invoke-Pester staging/cse/windows/*.tests.ps1 -Passthru` + - `Invoke-Pester vhdbuilder/packer/windows/*.tests.ps1 -Passthru` 1. Watch out different powershell versions. Sometimes JSON parsing works differently in your local powershell and in the pipeline. Which is annoying. # Test with AKS RP diff --git a/staging/cse/windows/all.ps1 b/staging/cse/windows/all.ps1 index 4fc889eebc8..ca3dcb1ac38 100644 --- a/staging/cse/windows/all.ps1 +++ b/staging/cse/windows/all.ps1 @@ -1,12 +1,16 @@ +if (-not $WINDOWS_SCRIPTS_DIRECTORY) { + $WINDOWS_SCRIPTS_DIRECTORY = 'c:\AzureData\windows' +} # Dot-source cse scripts with functions that are bundled on the VHD -. c:\AzureData\windows\azurecnifunc.ps1 -. c:\AzureData\windows\calicofunc.ps1 -. c:\AzureData\windows\configfunc.ps1 -. c:\AzureData\windows\containerdfunc.ps1 -. c:\AzureData\windows\kubeletfunc.ps1 -. c:\AzureData\windows\kubernetesfunc.ps1 -. c:\AzureData\windows\nvidiagpudriverfunc.ps1 -. c:\AzureData\windows\securetlsbootstrapfunc.ps1 -. c:\AzureData\windows\windowsciliumnetworkingfunc.ps1 -. c:\AzureData\windows\networkisolatedclusterfunc.ps1 +. $WINDOWS_SCRIPTS_DIRECTORY\helpers.ps1 +. $WINDOWS_SCRIPTS_DIRECTORY\azurecnifunc.ps1 +. $WINDOWS_SCRIPTS_DIRECTORY\calicofunc.ps1 +. $WINDOWS_SCRIPTS_DIRECTORY\configfunc.ps1 +. $WINDOWS_SCRIPTS_DIRECTORY\containerdfunc.ps1 +. $WINDOWS_SCRIPTS_DIRECTORY\kubeletfunc.ps1 +. $WINDOWS_SCRIPTS_DIRECTORY\kubernetesfunc.ps1 +. $WINDOWS_SCRIPTS_DIRECTORY\nvidiagpudriverfunc.ps1 +. $WINDOWS_SCRIPTS_DIRECTORY\securetlsbootstrapfunc.ps1 +. $WINDOWS_SCRIPTS_DIRECTORY\windowsciliumnetworkingfunc.ps1 +. $WINDOWS_SCRIPTS_DIRECTORY\networkisolatedclusterfunc.ps1 diff --git a/staging/cse/windows/helpers.ps1 b/staging/cse/windows/helpers.ps1 new file mode 100644 index 00000000000..92a6392eda2 --- /dev/null +++ b/staging/cse/windows/helpers.ps1 @@ -0,0 +1,43 @@ +# common helper functions + +function Remove-ServiceIfExists +{ + param( + [Parameter(Mandatory = $true)][string]$ServiceName + ) + # A prior provisioning attempt may have already registered this service (e.g. CSE re-invoked + # after a partial failure). Best-effort remove it so the subsequent nssm.exe install doesn't + # fail against an already-existing service. + $svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue + if ($null -ne $svc) { + sc.exe delete $ServiceName + # sc.exe delete can legitimately return non-zero here (e.g. 1072 - service already marked for deletion) + # since this is best-effort cleanup of a pre-existing service, don't treat that as fatal. + if ($LASTEXITCODE -ne 0) { Write-Log "sc.exe failed to delete existing $ServiceName service (exit code $LASTEXITCODE), continuing anyway" } + } +} + +function Invoke-NssmExe +{ + # Thin wrapper around the path-qualified nssm.exe invocation so tests can Mock it; + # Pester cannot intercept a call like `& "$KubeDir\nssm.exe"` directly since path-qualified + # commands bypass function/command name resolution. + param( + [Parameter(Mandatory = $true)][string]$KubeDir, + [Parameter(Mandatory = $true, ValueFromRemainingArguments = $true)][string[]]$NssmArguments + ) + & "$KubeDir\nssm.exe" @NssmArguments +} + +function Invoke-Nssm +{ + param( + [Parameter(Mandatory = $true)][string]$KubeDir, + [Parameter(Mandatory = $true, ValueFromRemainingArguments = $true)][string[]]$NssmArguments + ) + Invoke-NssmExe -KubeDir $KubeDir -NssmArguments $NssmArguments | RemoveNulls + if ($LASTEXITCODE -ne 0) + { + throw "nssm.exe $( $NssmArguments -join ' ' ) failed (exit code $LASTEXITCODE)" + } +} diff --git a/staging/cse/windows/helpers.tests.ps1 b/staging/cse/windows/helpers.tests.ps1 new file mode 100644 index 00000000000..e268e9cd55e --- /dev/null +++ b/staging/cse/windows/helpers.tests.ps1 @@ -0,0 +1,82 @@ +BeforeAll { + . $PSScriptRoot\..\..\..\parts\windows\windowscsehelper.ps1 + . $PSCommandPath.Replace('.tests.ps1', '.ps1') + + # Get-Service and sc.exe are Windows-only; stub them so Mock can override them when + # tests run in isolation (e.g. locally on non-Windows, outside the full suite). + function Get-Service {} + function sc.exe {} +} + +Describe 'Remove-ServiceIfExists' { + Context 'when the service does not exist' { + BeforeEach { + $script:scExeCallCount = 0 + Mock Get-Service -MockWith { return $null } + Mock sc.exe -MockWith { $script:scExeCallCount++ } + } + + It 'does not call sc.exe' { + Remove-ServiceIfExists -ServiceName 'some-service' + + $script:scExeCallCount | Should -Be 0 + } + } + + Context 'when the service already exists' { + BeforeEach { + $script:scExeCallCount = 0 + $mockExistingSvc = [PSCustomObject]@{Name = 'some-service'; Status = 'Stopped'} + Mock Get-Service -MockWith { return $mockExistingSvc } + } + + It 'calls sc.exe delete to remove the existing service' { + Mock sc.exe -MockWith { $script:scExeCallCount++; $global:LASTEXITCODE = 0 } + + Remove-ServiceIfExists -ServiceName 'some-service' + + $script:scExeCallCount | Should -Be 1 + } + + It 'does not throw when sc.exe delete succeeds' { + Mock sc.exe -MockWith { $global:LASTEXITCODE = 0 } + + { Remove-ServiceIfExists -ServiceName 'some-service' } | Should -Not -Throw + } + + It 'does not throw when sc.exe delete fails (best-effort cleanup)' { + Mock sc.exe -MockWith { $global:LASTEXITCODE = 1 } + + { Remove-ServiceIfExists -ServiceName 'some-service' } | Should -Not -Throw + } + } +} + +Describe 'Invoke-Nssm' { + BeforeEach { + $script:nssmInvocations = @() + } + + It 'does not throw when nssm.exe succeeds' { + Mock Invoke-NssmExe -MockWith { + $script:nssmInvocations += , @($NssmArguments) + $global:LASTEXITCODE = 0 + return 'ok' + } + + { Invoke-Nssm -KubeDir 'C:\k' -NssmArguments 'install', 'some-service', 'C:\k\some-service.exe' } | Should -Not -Throw + + $script:nssmInvocations.Count | Should -Be 1 + $script:nssmInvocations[0] | Should -Be @('install', 'some-service', 'C:\k\some-service.exe') + } + + It 'throws with the exit code when nssm.exe fails' { + Mock Invoke-NssmExe -MockWith { + $global:LASTEXITCODE = 1 + return $null + } + + { Invoke-Nssm -KubeDir 'C:\k' -NssmArguments 'install', 'some-service', 'C:\k\some-service.exe' } | + Should -Throw '*failed (exit code 1)*' + } +}