diff --git a/src/ConnectedKubernetes/ConnectedKubernetes.Autorest/custom/New-AzConnectedKubernetes.ps1 b/src/ConnectedKubernetes/ConnectedKubernetes.Autorest/custom/New-AzConnectedKubernetes.ps1 index 95b9712b038c..a37cf7858fa7 100644 --- a/src/ConnectedKubernetes/ConnectedKubernetes.Autorest/custom/New-AzConnectedKubernetes.ps1 +++ b/src/ConnectedKubernetes/ConnectedKubernetes.Autorest/custom/New-AzConnectedKubernetes.ps1 @@ -32,6 +32,7 @@ https://learn.microsoft.com/powershell/module/az.connectedkubernetes/new-azconne [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification='Kubernetes is a recognised term', Scope='Function', Target='New-AzConnectedKubernetes')] +[CmdletBinding()] param() function New-AzConnectedKubernetes { @@ -270,10 +271,13 @@ function New-AzConnectedKubernetes { ${GatewayResourceId} ) + # Write-Debug "Outside of process" + process { . "$PSScriptRoot/helpers/HelmHelper.ps1" . "$PSScriptRoot/helpers/ConfigDPHelper.ps1" . "$PSScriptRoot/helpers/AZCloudMetadataHelper.ps1" + Write-Debug "Debug: Inside of process" if($AzureHybridBenefit){ if(!$AcceptEULA){ $legalTermPath = Join-Path $PSScriptRoot -ChildPath "LegalTerm.txt" @@ -409,7 +413,11 @@ function New-AzConnectedKubernetes { $ConfigmapRgName = $Configmap.data.AZURE_RESOURCE_GROUP $ConfigmapClusterName = $Configmap.data.AZURE_RESOURCE_NAME try { - $ExistConnectedKubernetes = Get-AzConnectedKubernetes -ResourceGroupName $ConfigmapRgName -ClusterName $ConfigmapClusterName @CommonPSBoundParameters + $ExistConnectedKubernetes = Get-AzConnectedKubernetes ` + -ResourceGroupName $ConfigmapRgName ` + -ClusterName $ConfigmapClusterName ` + @CommonPSBoundParameters ` + -ErrorAction 'silentlycontinue' if (($ResourceGroupName -eq $ConfigmapRgName) -and ($ClusterName -eq $ConfigmapClusterName)) { # This performs a re-PUT of an existing connected cluster which should really be done using @@ -423,7 +431,8 @@ function New-AzConnectedKubernetes { return } catch { # This is attempting to delete Azure Arc resources that are orphaned. - helm delete azure-arc --namespace $ReleaseNamespace --kubeconfig $KubeConfig --kube-context $KubeContext + # We are catching and ignoring any messages here. + $null = helm delete azure-arc --ignore-not-found --namespace $ReleaseNamespace --kubeconfig $KubeConfig --kube-context $KubeContext } } @@ -667,16 +676,35 @@ function New-AzConnectedKubernetes { $configDpinfo = Get-ConfigDPEndpoint -location $Location -Cloud $cloudMetadata $configDPEndpoint = $configDpInfo.configDPEndpoint $adResourceId = $configDpInfo.adResourceId - Invoke-ConfigDPHealthCheck -configDPEndpoint $configDPEndpoint -Resource $adResourceId + + # If the health check fails (not 200 response), an exception is thrown + # so we can ignore the output. + $null = Invoke-ConfigDPHealthCheck -configDPEndpoint $configDPEndpoint -Resource $adResourceId # This call does the "pure ARM" update of the ARM objects. Write-Debug "Writing Connected Kubernetes ARM objects." - $PSBoundParameters.Add('AgentPublicKeyCertificate', $AgentPublicKey) + + # We sometimes see the AgentPublicKeyCertificate present with value $null. + # If this is the case, update rather than adding. + if ($PSBoundParameters.ContainsKey('AgentPublicKeyCertificate')) { + $PSBoundParameters['AgentPublicKeyCertificate'] = $AgentPublicKey + } else { + $PSBoundParameters.Add('AgentPublicKeyCertificate', $AgentPublicKey) + } $Response = Az.ConnectedKubernetes.internal\New-AzConnectedKubernetes @PSBoundParameters # Retrieving Helm chart OCI (Open Container Initiative) Artifact location Write-Debug "Retrieving Helm chart OCI (Open Container Initiative) Artifact location." - $helmValuesDp = Get-HelmValues -configDPEndpoint $configDPEndpoint -releaseTrain $ReleaseTrain -requestBody $Response + Write-Debug "PUT response: $Response" + $ResponseStr = "$Response" + $helmValuesDp = Get-HelmValues ` + -configDPEndpoint $configDPEndpoint ` + -releaseTrain $ReleaseTrain ` + -requestBody $ResponseStr ` + -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true) ` + -Debug:($PSCmdlet.MyInvocation.BoundParameters["Debug"].IsPresent -eq $true) + + Write-Debug "helmValuesDp: $helmValuesDp" Write-Debug "OCI Artifact location: ${helmValuesDp.repositoryPath}." # Allow a custom OCI registry to be set via environment variables. @@ -690,16 +718,17 @@ function New-AzConnectedKubernetes { # USERPROFILE # $registryPath = if ($env:HELMREGISTRY) { $env:HELMREGISTRY } else { $helmValuesDp.repositoryPath } + # !!PDS: Why do these not get logged? Write-Debug "RegistryPath: ${registryPath}." - $helmContentValues = $helmValuesDp["helmValuesContent"] - Write-Debug "Helm values: ${helmContentValues}." + $helmValuesContent = $helmValuesDp.helmValuesContent + Write-Debug "Helm values: ${helmValuesContent}." - foreach ($key in $helmContentValues.Keys) { - if ($key -in @("global.httpsProxy", "global.httpProxy", "global.noProxy", "global.proxyCert")) { + foreach ($field in $helmValuesContent.PSObject.Properties) { + if ($field.Name -in @("global.httpsProxy", "global.httpProxy", "global.noProxy", "global.proxyCert")) { continue } - $options += " --set $Key=$($helmContentValues[$Key])" + $options += " --set $($field.Name)=$($field.Value)" } # !!PDS: Is there any telemetry in Powershell cmdlets? @@ -719,9 +748,9 @@ function New-AzConnectedKubernetes { # !!PDS Aren't we supposed to read the helm config from the Cluster Config DP? # !!PDS: I think we might have done above, but why are we setting many options? $TenantId = [Microsoft.Azure.Commands.Common.Authentication.Abstractions.AzureRmProfileProvider]::Instance.Profile.DefaultContext.Tenant.Id + Write-Debug $options -ErrorAction Continue try { helm upgrade ` - --debug ` --install azure-arc ` $ChartPath ` --namespace $ReleaseInstallNamespace ` diff --git a/src/ConnectedKubernetes/ConnectedKubernetes.Autorest/custom/Set-AzConnectedKubernetes.ps1 b/src/ConnectedKubernetes/ConnectedKubernetes.Autorest/custom/Set-AzConnectedKubernetes.ps1 index 0a6e34072bfc..e2cb52c0a342 100644 --- a/src/ConnectedKubernetes/ConnectedKubernetes.Autorest/custom/Set-AzConnectedKubernetes.ps1 +++ b/src/ConnectedKubernetes/ConnectedKubernetes.Autorest/custom/Set-AzConnectedKubernetes.ps1 @@ -34,7 +34,7 @@ https://learn.microsoft.com/powershell/module/az.connectedkubernetes/new-azconne Justification='Kubernetes is a recognised term', Scope='Function', Target='New-AzConnectedKubernetes')] param() -function New-AzConnectedKubernetes { +function Set-AzConnectedKubernetes { [OutputType([Microsoft.Azure.PowerShell.Cmdlets.ConnectedKubernetes.Models.Api20240701Preview.IConnectedCluster])] [CmdletBinding(DefaultParameterSetName='CreateExpanded', PositionalBinding=$false, SupportsShouldProcess, ConfirmImpact='Medium')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '', diff --git a/src/ConnectedKubernetes/ConnectedKubernetes.Autorest/custom/helpers/AZCloudMetadataHelper.ps1 b/src/ConnectedKubernetes/ConnectedKubernetes.Autorest/custom/helpers/AZCloudMetadataHelper.ps1 index 885f39365c1c..0d75cc0befc9 100644 --- a/src/ConnectedKubernetes/ConnectedKubernetes.Autorest/custom/helpers/AZCloudMetadataHelper.ps1 +++ b/src/ConnectedKubernetes/ConnectedKubernetes.Autorest/custom/helpers/AZCloudMetadataHelper.ps1 @@ -11,30 +11,11 @@ function Get-AZCloudMetadataResourceId { # Search the $armMetadata hash for the entry where the "name" parameter matches # $cloud and then find the login endpoint, from which we can discern the # appropriate "cloud based domain ending". - return $cloudMetadata.authentication.audiences[0] + Write-Debug -Message "cloudMetaData in: $($cloudMetaData | ConvertTo-Json -Depth 10)." + return $cloudMetadata.ResourceManagerUrl } Function Get-AzCloudMetadata { - param ( - [string]$ApiVersion = "2022-09-01" - ) - - # This is a known endpoint. - $MetadataEndpoint = "https://management.azure.com/metadata/endpoints?api-version=$ApiVersion" - - try { - $Response = Invoke-RestMethod -Uri $MetadataEndpoint -Method Get -StatusCodeVariable StatusCode - - if ($StatusCode -ne 200) { - $Msg = "ARM metadata endpoint '$MetadataEndpoint' returned status code $($StatusCode)." - throw $Msg - } - } - catch { - $Msg = "Failed to request ARM metadata $MetadataEndpoint." - Write-Error "$Msg Please ensure you have network connection. Error: $_" - } - # The current cloud in use is set by the user so query it and then we can use # it to index into the ARM Metadata. $context = $null @@ -47,10 +28,15 @@ Function Get-AzCloudMetadata { } $cloudName = $context.Environment.Name - # Search the $armMetadata hash for the entry where the "name" parameter matches - # $cloud and then find the login endpoint, from which we can discern the - # appropriate "cloud based domain ending". - $cloud = $Response | Where-Object { $_.name -eq $cloudName } + try { + # $Response = Invoke-RestMethod -Uri $MetadataEndpoint -Method Get -StatusCodeVariable StatusCode + $cloud = Get-AzureEnvironment -Name $cloudName + } + catch { + Write-Error "Failed to request ARM metadata. Error: $_" + } + Write-Debug -Message "cloudMetaData out: $($cloud | ConvertTo-Json -Depth 10)." + return $cloud } diff --git a/src/ConnectedKubernetes/ConnectedKubernetes.Autorest/custom/helpers/ConfigDPHelper.ps1 b/src/ConnectedKubernetes/ConnectedKubernetes.Autorest/custom/helpers/ConfigDPHelper.ps1 index 71dc87faf995..cb6ba979c611 100644 --- a/src/ConnectedKubernetes/ConnectedKubernetes.Autorest/custom/helpers/ConfigDPHelper.ps1 +++ b/src/ConnectedKubernetes/ConnectedKubernetes.Autorest/custom/helpers/ConfigDPHelper.ps1 @@ -12,7 +12,7 @@ function Invoke-ConfigDPHealthCheck { $apiVersion = "2024-07-01-preview" $chartLocationUrlSegment = "azure-arc-k8sagents/healthCheck?api-version=$apiVersion" $chartLocationUrl = "$configDPEndpoint/$chartLocationUrlSegment" - $uriParameters = @{} + $uriParameters = [ordered]@{} $headers = @{} # Check if key AZURE_ACCESS_TOKEN exists in environment variables if ($env:AZURE_ACCESS_TOKEN) { @@ -20,13 +20,8 @@ function Invoke-ConfigDPHealthCheck { } # Sending request with retries - # $r = Invoke-RestMethodWithRetries -method 'post' -url $chartLocationUrl -headers $headers -faultType $consts.Get_HelmRegistery_Path_Fault_Type -summary 'Error while performing DP health check' -uriParameters $uriParameters -resource $resource Invoke-RestMethodWithUriParameters -Method 'post' -Uri $chartLocationUrl -Headers $headers -UriParameters $uriParameters -MaximumRetryCount 5 -RetryIntervalSec 3 -StatusCodeVariable statusCode - if ($statusCode -eq 200) { - Write-Output "Health check for DP is successful." - return $true - } - else { + if ($statusCode -ne 200) { throw "Error while performing DP health check, StatusCode: ${statusCode}" } } @@ -53,23 +48,28 @@ function Get-ConfigDPEndpoint { # $ReleaseTrain = $result.ReleaseTrain # } + # It is currently not clear what information might appear here in the future + # so the check of "arcConfigEndpoint" is left is a best guess!". # Get the values or endpoints required for retrieving the Helm registry URL. - if ($cloudMetadata.dataplaneEndpoints -and $cloudMetadata.dataplaneEndpoints.arcConfigEndpoint) { - $ConfigDpEndpoint = $armMetadata.dataplaneEndpoints.arcConfigEndpoint + if ($null -ne $cloudMetadata.ArcConfigEndpoint) { + $ConfigDpEndpoint = $cloudMetadata.ArcConfigEndpoint } else { - Write-Debug "'arcConfigEndpoint' doesn't exist under 'dataplaneEndpoints' in the ARM metadata." + Write-Debug "'ArcConfigEndpoint' doesn't exist in the ARM cloud metadata." } # Get the default config dataplane endpoint. - if (-not $ConfigDpEndpoint) { + if ($null -eq $ConfigDpEndpoint) { $ConfigDpEndpoint = Get-ConfigDpDefaultEndpoint -Location $Location -CloudMetadata $cloudMetadata } - $ADResourceId = Get-AZCloudMetadataResourceId -CloudMetadata $cloudMetadata + # !!PDS: This appears to be unused. + # $ADResourceId = Get-AZCloudMetadataResourceId -CloudMetadata $cloudMetadata + $ADResourceId = $null return @{ ConfigDpEndpoint = $ConfigDpEndpoint; ReleaseTrain = $ReleaseTrain; ADResourceId = $ADResourceId } } +# !!PDS: What? Looks like there is a function to do this? Perhaps because we did not hide it? function Get-ConfigDpDefaultEndpoint { param ( [Parameter(Mandatory=$true)] @@ -78,10 +78,13 @@ function Get-ConfigDpDefaultEndpoint { [PSCustomObject]$cloudMetadata ) - # Search the $armMetadata hash for the entry where the "name" parameter matches - # $cloud and then find the login endpoint, from which we can discern the - # appropriate "cloud based domain ending". - $cloudBasedDomain = ($cloudMetadata.authentication.loginEndpoint -split "\.")[2] + # The DP endpoint uses the same final URL portion as the AAD authority. But + # we also need to trim the trailing "/". + $cloudBasedDomain = ($cloudMetadata.ActiveDirectoryAuthority -split "\.")[2] + + # Remove optional trailing "/" from $cloudBasedDomain + $cloudBasedDomain = $cloudBasedDomain.TrimEnd('/') + $configDpEndpoint = "https://${location}.dp.kubernetesconfiguration.azure.${cloudBasedDomain}" return $configDpEndpoint } @@ -91,7 +94,7 @@ function Invoke-RestMethodWithUriParameters { [String]$method, [String]$uri, [Hashtable]$headers, - [Hashtable]$uriParameters, + [System.Collections.Specialized.OrderedDictionary]$uriParameters, [String]$requestBody, [Int]$maximumRetryCount, [Int]$retryIntervalSec, @@ -100,10 +103,12 @@ function Invoke-RestMethodWithUriParameters { # Add URI parameters to end of URL if there are any. $uriParametersArray = @() - foreach ($Key in $hash.Keys) { - $uriParametersArray.Add("$($Key)=$($UriParameters[$Key])") - $uriParametersString = $uriParametersArray -join '&' - $uri = "$url?$uriParametersString" + if ($uriParameters.count -gt 0) { + # Create an array by joining hash index and value using '=' + $uriParametersArray = $uriParameters.GetEnumerator() | ForEach-Object { "$($_.Key)=$($_.Value)" } + $uriParametersString = $uriParametersArray -join "&" + $uri = $uri + "?" + $uriParametersString + # Write-Error "URI: >$uri<" } # if ($uriParameters.count -gt 0) { @@ -111,10 +116,20 @@ function Invoke-RestMethodWithUriParameters { # $uriParametersArray = $uriParameters.GetEnumerator() | ForEach-Object { "$($_.Key)=$($_.Value)" } | ForEach-Object { $_ -join '=' } | ForEach-Object { $_ -join '&' } # } Write-Debug "Issue REST request to ${uri} with method ${method} and headers ${headers} and body ${requestBody}" - $rsp = Invoke-RestMethod -Method $method -Uri $uri -Headers $headers -Body $requestBody -ContentType "application/json" -MaximumRetryCount $maximumRetryCount -RetryIntervalSec $retryintervalSec -StatusCodeVariable statusCode + $rsp = Invoke-RestMethod ` + -Method $method ` + -Uri $uri ` + -Headers $headers ` + -Body $requestBody ` + -ContentType "application/json" ` + -MaximumRetryCount $maximumRetryCount ` + -RetryIntervalSec $retryintervalSec ` + -StatusCodeVariable statusCode ` + -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true) ` + -Debug:($PSCmdlet.MyInvocation.BoundParameters["Debug"].IsPresent -eq $true) + + Write-Debug "Response: $($rsp | ConvertTo-Json -Depth 10)" + Set-Variable -Name "${statusCodeVariable}" -Value $statusCode -Scope script - if ($statusCode -ne 200) { - throw "health check failed, StatusCode: ${statusCode}." - } return $rsp } diff --git a/src/ConnectedKubernetes/ConnectedKubernetes.Autorest/custom/helpers/HelmHelper.ps1 b/src/ConnectedKubernetes/ConnectedKubernetes.Autorest/custom/helpers/HelmHelper.ps1 index 88f187bf7aa5..d5821272a3f8 100644 --- a/src/ConnectedKubernetes/ConnectedKubernetes.Autorest/custom/helpers/HelmHelper.ps1 +++ b/src/ConnectedKubernetes/ConnectedKubernetes.Autorest/custom/helpers/HelmHelper.ps1 @@ -1,5 +1,6 @@ [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification='Helm values is a recognised term', Scope='Function', Target='Get-HelmValues')] +[CmdletBinding()] param() function Set-HelmClientLocation { @@ -109,18 +110,22 @@ function Get-HelmValues { [Parameter(Mandatory=$true)] $ConfigDpEndpoint, [string]$ReleaseTrainCustom, - $RequestBody + [Parameter(Mandatory=$true)] + [string]$RequestBody ) # Setting uri $apiVersion = "2024-07-01-preview" - $chartLocationUrlSegment = "azure-arc-k8sagents/GetHelmSettings?api-version=$apiVersion" + $chartLocationUrlSegment = "azure-arc-k8sagents/GetHelmSettings" $releaseTrain = if ($env:RELEASETRAIN) { $env:RELEASETRAIN } else { "stable" } $chartLocationUrl = "$ConfigDpEndpoint/$chartLocationUrlSegment" if ($ReleaseTrainCustom) { $releaseTrain = $ReleaseTrainCustom } - $uriParameters = @{releaseTrain=$releaseTrain} + $uriParameters = [ordered]@{ + "api-version" = $apiVersion + releaseTrain = $releaseTrain + } $headers = @{ "Content-Type" = "application/json" } @@ -128,50 +133,50 @@ function Get-HelmValues { $headers["Authorization"] = "Bearer $($env:AZURE_ACCESS_TOKEN)" } - $dpRequestIdentity = $RequestBody.identity - $id = $RequestBody.id - # $request_body = $request_body.serialize() - $RequestBody = $RequestBody | ConvertTo-Json | ConvertFrom-Json -AsHashtable - $RequestBody["Identity"] = @{ - tenantId = $dpRequestIdentity.tenantId - principalId = $dpRequestIdentity.principalId - } - $RequestBody["Id"] = $id - - # Convert $request_body to JSON - $jsonBody = $RequestBody | ConvertTo-Json - Write-Error "Request body: $jsonBody" - # Sending request with retries try { - $r = Invoke-RestMethodWithUriParameters -Method 'post' -Uri $chartLocationUrl -Headers $headers -UriParameters $uriParameters -RequestBody $JsonBody -MaximumRetryCount 5 -RetryIntervalSec 3 -StatusCodeVariable statusCodeVariable + $r = Invoke-RestMethodWithUriParameters ` + -Method 'post' ` + -Uri $chartLocationUrl ` + -Headers $headers ` + -UriParameters $uriParameters ` + -RequestBody $RequestBody ` + -MaximumRetryCount 5 ` + -RetryIntervalSec 3 ` + -StatusCodeVariable StatusCode ` + -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true) ` + -Debug:($PSCmdlet.MyInvocation.BoundParameters["Debug"].IsPresent -eq $true) # Response is a Hashtable of JSON values. - if ($statusCode -eq 200 -and $r) { + if ($StatusCode -eq 200 -and $r) { return $r } - else { - throw "No content was found in helm registry path response, StatusCode: ${statusCode}." - } } catch { $errorMessage = "Error while fetching helm values from DP from JSON response: $_" Write-Error $errorMessage throw $errorMessage } + # Reach here and we received either a non-200 status code or no response. + throw "No content was found in helm registry path response, StatusCode: ${StatusCode}." } function Get-HelmChartPath { param ( + [Parameter(Mandatory)] [string]$RegistryPath, + [Parameter(Mandatory)] + [string]$HelmClientLocation, [string]$KubeConfig, [string]$KubeContext, - [string]$HelmClientLocation, [string]$ChartFolderName = 'AzureArcCharts', [string]$ChartName = 'azure-arc-k8sagents', [bool]$NewPath = $true ) + # Special path! + $PreOnboardingHelmChartsFolderName = 'PreOnboardingChecksCharts' + # Exporting Helm chart $ChartExportPath = Join-Path $env:USERPROFILE ('.azure', $ChartFolderName -join '\') try { @@ -180,14 +185,14 @@ function Get-HelmChartPath { } } catch { - Write-Warning "Unable to cleanup the $ChartFolderName already present on the machine. In case of failure, please cleanup the directory '$ChartExportPath' and try again." + Write-Warning -Message "Unable to cleanup the $ChartFolderName already present on the machine. In case of failure, please cleanup the directory '$ChartExportPath' and try again." } Get-HelmChart -RegistryPath $RegistryPath -ChartExportPath $ChartExportPath -KubeConfig $KubeConfig -KubeContext $KubeContext -HelmClientLocation $HelmClientLocation -NewPath $NewPath -ChartName $ChartName # Returning helm chart path $HelmChartPath = Join-Path $ChartExportPath $ChartName - if ($ChartFolderName -eq $consts.Pre_Onboarding_Helm_Charts_Folder_Name) { + if ($ChartFolderName -eq $PreOnboardingHelmChartsFolderName) { $ChartPath = $HelmChartPath } else { @@ -199,10 +204,13 @@ function Get-HelmChartPath { function Get-HelmChart { param ( + [Parameter(Mandatory)] [string]$RegistryPath, + [Parameter(Mandatory)] [string]$ChartExportPath, [string]$KubeConfig, [string]$KubeContext, + [Parameter(Mandatory)] [string]$HelmClientLocation, [bool]$NewPath, [string]$ChartName = 'azure-arc-k8sagents', @@ -225,6 +233,7 @@ function Get-HelmChart { # the results. $basePath, $imageName = if ($chartUrl -match "(^.*?)/([^/]+$)") {$matches[1], $matches[2]} $chartUrl = "$basePath/v2/$imageName" + # Write-Error "Chart URL: $chartUrl" } $cmdHelmChartPull = @($HelmClientLocation, "pull", "oci://$chartUrl", "--untar", "--untardir", $ChartExportPath, "--version", $chartVersion) @@ -238,17 +247,18 @@ function Get-HelmChart { Write-Debug "Pull helm chart: $cmdHelmChartPull[0] $cmdHelmChartPull[1..($cmdHelmChartPull.Count - 1)]" for ($i = 0; $i -lt $RetryCount; $i++) { try { - & $cmdHelmChartPull[0] $cmdHelmChartPull[1..($cmdHelmChartPull.Count - 1)] + Invoke-ExternalCommand $cmdHelmChartPull[0] $cmdHelmChartPull[1..($cmdHelmChartPull.Count - 1)] break } catch { - if ($i -eq $RetryCount - 1) { - # Assuming telemetry.set_exception and consts.Pull_HelmChart_Fault_Type are handled elsewhere - throw "Unable to pull $ChartName helm chart from the registry '$RegistryPath': $_" - } Start-Sleep -Seconds $RetryDelay } } + + if ($i -eq $RetryCount) { + # Assuming telemetry.set_exception and consts.Pull_HelmChart_Fault_Type are handled elsewhere + throw "Unable to pull '$ChartName' helm chart from the registry '$RegistryPath'." + } } # !!PDS: no dogfood so no need for this? @@ -268,3 +278,13 @@ function Get-HelmValuesFile { return $null } +# This method exists to allow us to effectively Mock the call operator (&). +# We cannnot do that directly so instead we have this wrapper, which we can mock! +function Invoke-ExternalCommand { + param ( + [Parameter(Mandatory=$true)] + [string]$Command, + [array]$Arguments + ) + & $Command $Arguments +} \ No newline at end of file diff --git a/src/ConnectedKubernetes/ConnectedKubernetes.Autorest/test/New-AzConnectedKubernetes.Tests.ps1 b/src/ConnectedKubernetes/ConnectedKubernetes.Autorest/test/New-AzConnectedKubernetes.Tests.ps1 index 4afb100e8058..1756b266ee9e 100644 --- a/src/ConnectedKubernetes/ConnectedKubernetes.Autorest/test/New-AzConnectedKubernetes.Tests.ps1 +++ b/src/ConnectedKubernetes/ConnectedKubernetes.Autorest/test/New-AzConnectedKubernetes.Tests.ps1 @@ -5,217 +5,612 @@ if(($null -eq $TestName) -or ($TestName -contains 'New-AzConnectedKubernetes')) $loadEnvPath = Join-Path $PSScriptRoot '..\loadEnv.ps1' } . ($loadEnvPath) - $TestRecordingFile = Join-Path $PSScriptRoot 'New-AzConnectedKubernetes.Recording.json' $currentPath = $PSScriptRoot while(-not $mockingPath) { $mockingPath = Get-ChildItem -Path $currentPath -Recurse -Include 'HttpPipelineMocking.ps1' -File $currentPath = Split-Path -Path $currentPath -Parent } . ($mockingPath | Select-Object -First 1).FullName + # !!PDS: Better way to do this? + . "$PSScriptRoot/../custom/helpers/HelmHelper.ps1" + . "$PSScriptRoot/../custom/helpers/ConfigDPHelper.ps1" + . "$PSScriptRoot/../custom/helpers/AzCloudMetadataHelper.ps1" } +# The custom tests make helm requests and the framework does not mock these so +# record/replay will not work. Describe 'New-AzConnectedKubernetes' { It 'CreateExpanded' -skip { { throw [System.NotImplementedException] } | Should -Not -Throw } } -Describe 'Invoke-HealthCheckDP' { - It 'Golden path' -skip { - { throw [System.NotImplementedException] } | Should -Not -Throw - } - - It 'Env access token' -skip { - { throw [System.NotImplementedException] } | Should -Not -Throw - } - - It 'Unhealthy (not 200 response)' -skip { - { throw [System.NotImplementedException] } | Should -Not -Throw - } -} - -Describe 'Invoke-RestMethodWithRetries' { - It 'Golden path' -skip { - { throw [System.NotImplementedException] } | Should -Not -Throw - } - - It 'URI parameters' -skip { - { throw [System.NotImplementedException] } | Should -Not -Throw - } - - It 'Retry required' -skip { - { throw [System.NotImplementedException] } | Should -Not -Throw - } - - It 'request failed' -skip { - { throw [System.NotImplementedException] } | Should -Not -Throw - } -} - -Describe 'Invoke-RawRequest' { - It 'Golden path' -skip { - { throw [System.NotImplementedException] } | Should -Not -Throw - } - - It 'JSON headers' -skip { - { throw [System.NotImplementedException] } | Should -Not -Throw - } - - It 'String headers' -skip { - { throw [System.NotImplementedException] } | Should -Not -Throw - } - - It 'Authorization provided' -skip { - { throw [System.NotImplementedException] } | Should -Not -Throw - } - - It 'JSON body' -skip { - { throw [System.NotImplementedException] } | Should -Not -Throw - } - - It 'Unrecognised body type' -skip { - { throw [System.NotImplementedException] } | Should -Not -Throw - } - - It 'URL parameters' -skip { - { throw [System.NotImplementedException] } | Should -Not -Throw +Describe 'Invoke-ConfigDPHealthCheck' { + # Note that we Mock Invoke-RestMethod and not Invoke-RestMethodWithUriParameters + # because it appears that Pester has a problem handling ordered hashtable + # as a parameter to Mock. This is a workaround. + It 'Golden path' { + Mock Invoke-RestMethod { + $Script:StatusCode = 200 + return + } + { Invoke-ConfigDPHealthCheck } | Should -Not -Throw + Assert-MockCalled "Invoke-RestMethod" -Times 1 + Assert-VerifiableMock } - It 'No protocol or system name' -skip { - { throw [System.NotImplementedException] } | Should -Not -Throw + It 'Env access token' { + $env:AZURE_ACCESS_TOKEN = "This is an access token" + Mock Invoke-RestMethod { + $Script:StatusCode = 200 + return + } + { Invoke-ConfigDPHealthCheck } | Should -Not -Throw + Assert-MockCalled "Invoke-RestMethod" -Times 1 + Assert-VerifiableMock } - It 'Error response' -skip { - { throw [System.NotImplementedException] } | Should -Not -Throw + It 'Unhealthy (not 200 response)' { + Mock Invoke-RestMethod { + $Script:StatusCode = 500 + return + } + { Invoke-ConfigDPHealthCheck } | Should -Throw "Error while performing DP health check, StatusCode: 500" + Assert-MockCalled "Invoke-RestMethod" -Times 1 + Assert-VerifiableMock } } -Describe 'Get-SubscriptionIdFromResourceId' { - It 'Finds the subscription id from a resource id' { - InModuleScope Az.ConnectedKubernetes.custom { - $resourceId = '/subscriptions/12345678-9ABC-DEF0-1234-56789ABCDEF0/resourceGroups/rg/providers/Microsoft.Kubernetes/connectedClusters/cluster' - $subscriptionId = Get-SubscriptionIdFromResourceId -ResourceId $resourceId - $subscriptionId | Should -Be '12345678-9ABC-DEF0-1234-56789ABCDEF0' +Describe 'Invoke-RestMethodWithUriParameters' { + It 'Golden path' { + Mock Invoke-RestMethod { + $Script:StatusCode = 200 + return + } + { + $uriParameters = [ordered]@{} + Invoke-RestMethodWithUriParameters ` + -Method "POST" ` + -Uri "https://invalid.invalid/some/page/nowhere" ` + -Headers @{} ` + -UriParameters $uriParameters ` + -RequestBody @{} ` + -MaximumRetryCount 5 ` + -RetryIntervalSec 2 ` + -StatusCodeVariable YesOrNo + } | Should -Not -Throw + Assert-MockCalled "Invoke-RestMethod" -Times 1 -ParameterFilter { $Uri.AbsoluteUri -eq "https://invalid.invalid/some/page/nowhere" } + Assert-VerifiableMock + $YesOrNo | Should -Be 200 + } + + It 'URI parameters' { + Mock Invoke-RestMethod { + $Script:StatusCode = 200 + return } - } - It 'Finds no subscription id in a resource id' { - InModuleScope Az.ConnectedKubernetes.custom { - $resourceId = '/not-a-subscription/12345678-9ABC-DEF0-1234-56789ABCDEF0/resourceGroups/rg/providers/Microsoft.Kubernetes/connectedClusters/cluster' - $subscriptionId = Get-SubscriptionIdFromResourceId -ResourceId $resourceId - $subscriptionId | Should -Be $null + { + # Create a hashtable with sample key-value pairs + $uriParameters = [ordered]@{"key1"="value1"; "key2"="value2"} + + Invoke-RestMethodWithUriParameters ` + -Method "POST" ` + -Uri "https://invalid.invalid/some/page/nowhere" ` + -Headers @{} ` + -UriParameters $uriParameters ` + -RequestBody @{} ` + -MaximumRetryCount 5 ` + -RetryIntervalSec 2 ` + -StatusCodeVariable YesOrNo + } | Should -Not -Throw + Assert-MockCalled "Invoke-RestMethod" -Times 1 -ParameterFilter { $Uri.AbsoluteUri -eq "https://invalid.invalid/some/page/nowhere?key1=value1&key2=value2" } + Assert-VerifiableMock + $YesOrNo | Should -Be 200 + } + + It 'request failed' { + Mock Invoke-RestMethod { + $Script:StatusCode = 500 + return } + { + Invoke-RestMethodWithUriParameters ` + -Method "POST" ` + -Uri "https://invalid.invalid/some/page/nowhere" ` + -Headers @{} ` + -UriParameters @{} ` + -RequestBody @{} ` + -MaximumRetryCount 5 ` + -RetryIntervalSec 2 ` + -StatusCodeVariable YesOrNo + } | Should -Not -Throw + Assert-VerifiableMock + $YesOrNo | Should -Be 500 } } -Describe 'Get-ConfigDpEndpoint' { - It 'Golden path' -skip { - { throw [System.NotImplementedException] } | Should -Not -Throw - } - - It 'Read ARM metadata' -skip { - { throw [System.NotImplementedException] } | Should -Not -Throw - } - - It 'No arcConfigEndpoint' -skip { - { throw [System.NotImplementedException] } | Should -Not -Throw - } - - It 'Read default ConfigDPEndoint' -skip { - { throw [System.NotImplementedException] } | Should -Not -Throw +Describe 'Get-ConfigDpDefaultEndpoint' { + It 'Golden path' { + { + $cloudMetadata = [PSCustomObject]@{ + ActiveDirectoryAuthority = "https://login.microsoftonline.com/" + } + $script:configDpEndpoint = Get-ConfigDpDefaultEndpoint ` + -Location "eastus2" ` + -CloudMetadata $cloudMetadata + } | Should -Not -Throw + $configDpEndpoint | Should -Be "https://eastus2.dp.kubernetesconfiguration.azure.com" + } + + # !!PDS: How do we validate this? Need to check endpoint on a sovereign cloud. + It 'Sovereign cloud' { + { + $cloudMetadata = [PSCustomObject]@{ + ActiveDirectoryAuthority = "https://login.sovereign.invalid/" + } + $script:configDpEndpoint = Get-ConfigDpDefaultEndpoint ` + -Location "westus3" ` + -CloudMetadata $cloudMetadata + } | Should -Not -Throw + $configDpEndpoint | Should -Be "https://westus3.dp.kubernetesconfiguration.azure.invalid" } } -Describe 'Get-MetaData' { - It 'Golden path' -skip { - { throw [System.NotImplementedException] } | Should -Not -Throw - } - - It 'Error response' -skip { - { throw [System.NotImplementedException] } | Should -Not -Throw - } - - It 'Exception during REST API.' -skip { - { throw [System.NotImplementedException] } | Should -Not -Throw +Describe 'Get-ConfigDpEndpoint' { + It 'Golden path' { + { + $cloudMetadata = [PSCustomObject]@{ + ArcConfigEndpoint = "https://arc.microsoftonline.com" + ActiveDirectoryAuthority = "https://login.microsoftonline.com" + } + $script:configDpEndpoint = Get-ConfigDpEndpoint ` + -Location "eastus2" ` + -CloudMetadata $cloudMetadata + } | Should -Not -Throw + + $configDpEndpoint.ConfigDpEndpoint | Should -Be "https://arc.microsoftonline.com" + $configDpEndpoint.ReleaseTrain | Should -Be $null + # !!PDS: Don't believe we require this! + $configDpEndpoint.ADResourceId | Should -Be $null + } + + It 'No ArcconfigEndpoints' { + { + $cloudMetadata = [PSCustomObject]@{ + ActiveDirectoryAuthority = "https://login.microsoftonline.com" + } + $script:configDpEndpoint = Get-ConfigDpEndpoint ` + -Location "eastus2" ` + -CloudMetadata $cloudMetadata + } | Should -Not -Throw + + $configDpEndpoint.ConfigDpEndpoint | Should -Be "https://eastus2.dp.kubernetesconfiguration.azure.com" + $configDpEndpoint.ReleaseTrain | Should -Be $null + # !!PDS: Don't believe we require this! + $configDpEndpoint.ADResourceId | Should -Be $null } } -Describe 'Get-ValuesFile' { - It 'Golden path' -skip { - { throw [System.NotImplementedException] } | Should -Not -Throw - } - - It 'No values filename' -skip { - { throw [System.NotImplementedException] } | Should -Not -Throw - } - - It 'No values file' -skip { - { throw [System.NotImplementedException] } | Should -Not -Throw +Describe 'Get-AzCloudMetadata' { + # For some reason Pester fails to "see" Get-AzureEnvirnomment so we create + # and empty instance here that we can the mock. + BeforeEach { + Function Get-AzureEnvironment { + } } - It 'Starts/ends with quote or double-quote' -skip { - { throw [System.NotImplementedException] } | Should -Not -Throw + It 'Golden path' { + Mock Get-AzContext { + return [PSCustomObject]@{ + Environment = [PSCustomObject]@{ + Name = "SovereignAzureCloud" + } + } + } + Mock Get-AzureEnvironment { + $context = [PSCustomObject]@{ + Name = "AzureCloud" + } + return $context + } + { $Script:cloud = Get-AzCloudMetadata } | Should -Not -Throw + Assert-MockCalled "Get-AzContext" -Times 1 + Assert-MockCalled "Get-AzureEnvironment" -Times 1 + # Ref: https://github.com/pester/Pester/issues/2556 + # Assert-MockCalled "Get-AzureEnvironment" -Times 1 -ParameterFilter { $Local:Name -eq "SovereignAzureCloud" } + Assert-VerifiableMock + $cloud.name | Should -Be "AzureCloud" + } + + It 'Get-AzContext fails' { + Mock Get-AzContext { + throw "Some error!" + } + { Get-AzCloudMetadata } | Should -Throw "Failed to get the current Azure context. Error: Some error!" + Assert-MockCalled "Get-AzContext" -Times 1 + Assert-VerifiableMock + } + + It 'Get-AzureEnvironment fails' { + Mock Get-AzContext { + return [PSCustomObject]@{ + Environment = [PSCustomObject]@{ + Name = "SovereignAzureCloud" + } + } + } + Mock Get-AzureEnvironment { + throw "Some error!" + } + { Get-AzCloudMetadata } | Should -Throw "Failed to request ARM metadata. Error: Some error!" + Assert-MockCalled "Get-AzContext" -Times 1 + Assert-VerifiableMock } } Describe 'Get-HelmValues' { - It 'Golden path' -skip { - { throw [System.NotImplementedException] } | Should -Not -Throw - } - - It 'Custom release train' -skip { - { throw [System.NotImplementedException] } | Should -Not -Throw + It 'Golden path' { + $rq = { + identity = @{ + tenantId = "1234" + principalId = "5678" + } + id = "abcd" + } + $rsp = [PSObject]@{ + Frank = "Sinatra" + Dean = "Martin" + } + Mock Invoke-RestMethod { + $Script:StatusCode = 200 + return $rsp + } + { + $Script:helmValues = Get-HelmValues ` + -ConfigDpEndpoint "https://helm.azure.com" ` + -ReleaseTrainCustom $null ` + -RequestBody $rq + } | Should -Not -Throw + $helmValues.Frank | Should -Be "Sinatra" + $helmValues.Dean | Should be "Martin" + Assert-MockCalled "Invoke-RestMethod" -Times 1 -ParameterFilter { + $Uri.AbsoluteUri -eq "https://helm.azure.com/azure-arc-k8sagents/GetHelmSettings?api-version=2024-07-01-preview&releaseTrain=stable" + } + Assert-VerifiableMock } - It 'Env access token' -skip { - { throw [System.NotImplementedException] } | Should -Not -Throw + It 'Custom release train' { + $rq = @{ + identity = @{ + tenantId = "1234" + principalId = "5678" + } + id = "abcd" + } + $rsp = @{ + Frank = "Sinatra" + Dean = "Martin" + } + Mock Invoke-RestMethod { + $Script:StatusCode = 200 + return $rsp + } + { + $Script:helmValues = Get-HelmValues ` + -ConfigDpEndpoint "https://helm.azure.com" ` + -ReleaseTrainCustom "all-aboard"` + -RequestBody $rq + } | Should -Not -Throw + $helmValues.Frank | Should -Be "Sinatra" + $helmValues.Dean | Should be "Martin" + Assert-MockCalled "Invoke-RestMethod" -Times 1 -ParameterFilter { + $Uri.AbsoluteUri -eq "https://helm.azure.com/azure-arc-k8sagents/GetHelmSettings?api-version=2024-07-01-preview&releaseTrain=all-aboard" + } + Assert-VerifiableMock } - It 'Empty content' -skip { - { throw [System.NotImplementedException] } | Should -Not -Throw + It 'Env access token' { + $env:AZURE_ACCESS_TOKEN="Tell nobody!" + $rq = @{ + identity = @{ + tenantId = "1234" + principalId = "5678" + } + id = "abcd" + } + $rsp = @{ + Frank = "Sinatra" + Dean = "Martin" + } + Mock Invoke-RestMethod { + $Script:StatusCode = 200 + return $rsp + } + { + $Script:helmValues = Get-HelmValues ` + -ConfigDpEndpoint "https://helm.azure.com" ` + -ReleaseTrainCustom $null ` + -RequestBody $rq + } | Should -Not -Throw + $helmValues.Frank | Should -Be "Sinatra" + $helmValues.Dean | Should be "Martin" + Assert-MockCalled "Invoke-RestMethod" -Times 1 -ParameterFilter { + $Headers["Authorization"] -eq "Bearer Tell nobody!" + } + Assert-VerifiableMock } - It 'Exception reading content' -skip { - { throw [System.NotImplementedException] } | Should -Not -Throw + It 'REST call failed' { + $rq = @{ + identity = @{ + tenantId = "1234" + principalId = "5678" + } + id = "abcd" + } + $rsp = $null + Mock Invoke-RestMethod { + $Script:StatusCode = 500 + return $rsp + } + { + $Script:helmValues = Get-HelmValues ` + -ConfigDpEndpoint "https://helm.azure.com" ` + -ReleaseTrainCustom $null ` + -RequestBody $rq + } | Should -Throw "No content was found in helm registry path response, StatusCode: 500." + Assert-MockCalled "Invoke-RestMethod" -Times 1 + Assert-VerifiableMock + } + + It 'Empty content' { + $rq = @{ + identity = @{ + tenantId = "1234" + principalId = "5678" + } + id = "abcd" + } + $rsp = $null + Mock Invoke-RestMethod { + $Script:StatusCode = 200 + return $rsp + } + { + $Script:helmValues = Get-HelmValues ` + -ConfigDpEndpoint "https://helm.azure.com" ` + -ReleaseTrainCustom $null ` + -RequestBody $rq + } | Should -Throw "No content was found in helm registry path response, StatusCode: 200." + Assert-MockCalled "Invoke-RestMethod" -Times 1 + Assert-VerifiableMock + } + + It 'Exception reading content' { + $rq = @{ + identity = @{ + tenantId = "1234" + principalId = "5678" + } + id = "abcd" + } + Mock Invoke-RestMethod { + throw "Failed!" + } + { + $Script:helmValues = Get-HelmValues ` + -ConfigDpEndpoint "https://helm.azure.com" ` + -ReleaseTrainCustom $null ` + -RequestBody $rq + } | Should -Throw "Error while fetching helm values from DP from JSON response: Failed!" + Assert-MockCalled "Invoke-RestMethod" -Times 1 + Assert-VerifiableMock } } -Describe 'Get-ChartPath' { - It 'Golden Path' -skip { - { throw [System.NotImplementedException] } | Should -Not -Throw - } +Describe 'Get-HelmChartPath' { - It 'Cannot clean-up existing chart path' -skip { - { throw [System.NotImplementedException] } | Should -Not -Throw + It 'Golden Path' { + Mock Get-HelmChart { + } + { + $Script:ChartPath = Get-HelmChartPath ` + -RegistryPath "Dummy" ` + -HelmClientLocation "fake-helm-client.exe" + } | Should -Not -Throw + Assert-MockCalled "Get-HelmChart" -Times 1 + Assert-VerifiableMock + $ExpectedChartPath = Join-Path ` + -Path $env:USERPROFILE ` + -ChildPath ".azure" ` + -AdditionalChildPath "AzureArcCharts","azure-arc-k8sagents" + # Write-Error -Message "ChartPath: $ChartPath" -ErrorAction Continue + # Write-Error -Message "ExpectedChartPath: $ExpectedChartPath" -ErrorAction Continue + $ChartPath | Should -eq $ExpectedChartPath + } + + It 'Environment helm chart name' { + $helmChartName = "c:\\somewhere\\this-is-my-helm-chart" + $env:HELMCHART = $helmChartName + Mock Get-HelmChart { + } + { + $Script:ChartPath = Get-HelmChartPath ` + -RegistryPath "Dummy" ` + -HelmClientLocation "fake-helm-client.exe" + } | Should -Not -Throw + Assert-MockCalled "Get-HelmChart" -Times 1 + Assert-VerifiableMock + $ChartPath | Should -eq $helmChartName + } + + It 'Pre-onboarding' { + $helmChartName = "c:\\somewhere\\this-is-my-helm-chart" + $env:HELMCHART = $helmChartName + Mock Get-HelmChart { + } + { + $Script:ChartPath = Get-HelmChartPath ` + -RegistryPath "Dummy" ` + -HelmClientLocation "fake-helm-client.exe" ` + -ChartFolderName "PreOnboardingChecksCharts" + } | Should -Not -Throw + Assert-MockCalled "Get-HelmChart" -Times 1 + Assert-VerifiableMock + + # For Pre-onboarding we ignore environment variables. + $ExpectedChartPath = Join-Path ` + -Path $env:USERPROFILE ` + -ChildPath ".azure" ` + -AdditionalChildPath "PreOnboardingChecksCharts","azure-arc-k8sagents" + # Write-Error -Message "ChartPath: $ChartPath" -ErrorAction Continue + # Write-Error -Message "ExpectedChartPath: $ExpectedChartPath" -ErrorAction Continue + $ChartPath | Should -eq $ExpectedChartPath + $env:HELMCHART = $null + } + + It 'Cannot clean-up existing chart path' { + Mock Get-HelmChart { + } + Mock Remove-Item { + throw "Mock Remote-Item failure" + } + # Mock this just so that we can confirm that it is called. + Mock Write-Warning { + } + # Create a folder path to try and remove. + $folderPath = Join-Path -Path $env:USERPROFILE -ChildPath ".azure" -AdditionalChildPath "AzureArcCharts" + New-Item -Path $folderPath -ItemType Directory + + { + $Script:ChartPath = Get-HelmChartPath ` + -RegistryPath "Dummy" ` + -HelmClientLocation "fake-helm-client.exe" + } | Should -Not -Throw + Assert-MockCalled "Remove-Item" -Times 1 + Assert-MockCalled "Write-Warning" -Times 1 + Assert-MockCalled "Get-HelmChart" -Times 1 + Assert-VerifiableMock + $ExpectedChartPath = Join-Path ` + -Path $env:USERPROFILE ` + -ChildPath ".azure" ` + -AdditionalChildPath "AzureArcCharts","azure-arc-k8sagents" + $ChartPath | Should -eq $ExpectedChartPath } - - It 'Pre-onboarding' -skip { - { throw [System.NotImplementedException] } | Should -Not -Throw - } } Describe 'Get-HelmChart' { - It 'Golden path' -skip { - { throw [System.NotImplementedException] } | Should -Not -Throw - } - - It 'Agent older than 1.14.0' -skip { - { throw [System.NotImplementedException] } | Should -Not -Throw - } - - It 'NewPath' -skip { - { throw [System.NotImplementedException] } | Should -Not -Throw - } - - It 'Has KubeConfig' -skip { - { throw [System.NotImplementedException] } | Should -Not -Throw - } - - It 'Has KubeContext' -skip { - { throw [System.NotImplementedException] } | Should -Not -Throw - } - - It 'Requires retry' -skip { - { throw [System.NotImplementedException] } | Should -Not -Throw + It 'Golden path' { + Mock Invoke-ExternalCommand { + } + { + Get-HelmChart ` + -RegistryPath "SomePath/ImageName:1.20.3" ` + -ChartExportPath "c:\temp" ` + -HelmClientLocation "fake-helm-client.exe" + } | Should -Not -Throw + Assert-MockCalled "Invoke-ExternalCommand" -Times 1 -ParameterFilter { $Arguments -contains "oci://SomePath/ImageName" } + Assert-VerifiableMock + } + + It 'NewPath' { + Mock Invoke-ExternalCommand { + } + { + Get-HelmChart ` + -RegistryPath "SomePath/ImageName:1.20.3" ` + -ChartExportPath "c:\temp" ` + -HelmClientLocation "fake-helm-client.exe" ` + -NewPath $true + } | Should -Not -Throw + Assert-MockCalled "Invoke-ExternalCommand" -Times 1 -ParameterFilter { $Arguments -contains "oci://SomePath/v2/ImageName" } + Assert-VerifiableMock + } + + It 'Agent older than 1.14.0' { + { + Get-HelmChart ` + -RegistryPath "SomePath/ImageName:1.2.3" ` + -ChartExportPath "c:\temp" ` + -NewPath $true ` + -HelmClientLocation "fake-helm-client.exe" + } | Should -Throw "Operation not supported on older Agents: This CLI version does not support upgrading to Agents versions older than v1.14" + } + + It 'Has KubeConfig' { + Mock Invoke-ExternalCommand { + } + { + Get-HelmChart ` + -RegistryPath "SomePath:1.2.3" ` + -ChartExportPath "c:\temp" ` + -HelmClientLocation "fake-helm-client.exe" ` + -KubeConfig "Some-kube-setting" ` + } | Should -Not -Throw + Assert-MockCalled "Invoke-ExternalCommand" -Times 1 -ParameterFilter { $Command -contains "fake-helm-client.exe" } + Assert-MockCalled "Invoke-ExternalCommand" -Times 1 -ParameterFilter { $Arguments -contains "--kubeconfig" } + Assert-MockCalled "Invoke-ExternalCommand" -Times 1 -ParameterFilter { $Arguments -contains "some-kube-setting" } + Assert-VerifiableMock + } + + It 'Has KubeContext' { + Mock Invoke-ExternalCommand { + } + { + Get-HelmChart ` + -RegistryPath "SomePath:1.2.3" ` + -ChartExportPath "c:\temp" ` + -HelmClientLocation "fake-helm-client.exe" ` + -KubeContext "some-kube-context" + } | Should -Not -Throw + Assert-MockCalled "Invoke-ExternalCommand" -Times 1 -ParameterFilter { $Command -contains "fake-helm-client.exe" } + Assert-MockCalled "Invoke-ExternalCommand" -Times 1 -ParameterFilter { $Arguments -contains "--kube-context" } + Assert-MockCalled "Invoke-ExternalCommand" -Times 1 -ParameterFilter { $Arguments -contains "some-kube-context" } + Assert-VerifiableMock + } + + It 'Requires retry' { + $Script:RetryCount = 2 + Mock Invoke-ExternalCommand { + if ($Script:RetryCount -gt 0) { + $Script:RetryCount-- + throw "Failed" + } + } + { + Get-HelmChart ` + -RegistryPath "SomePath:1.2.3" ` + -ChartExportPath "c:\temp" ` + -HelmClientLocation "fake-helm-client.exe" + # -KubeConfig ` + # -KubeContext ` + # -NewPath + # -ChartName = 'azure-arc-k8sagents' ` + # -RetryCount = 5 ` + # -RetryDelay = 3 + } | Should -Not -Throw + Assert-MockCalled "Invoke-ExternalCommand" -Times 3 + Assert-VerifiableMock + } + + It 'Fails after retry' { + Mock Invoke-ExternalCommand { + throw "Failed" + } + { + Get-HelmChart ` + -RegistryPath "SomePath:1.20.3" ` + -ChartExportPath "c:\temp" ` + -HelmClientLocation "fake-helm-client.exe" + # -KubeConfig ` + # -KubeContext ` + # -NewPath + # -ChartName = 'azure-arc-k8sagents' ` + # -RetryCount = 5 ` + # -RetryDelay = 3 + } | Should -Throw "Unable to pull 'azure-arc-k8sagents' helm chart from the registry 'SomePath:1.20.3'." + Assert-MockCalled "Invoke-ExternalCommand" -Times 5 + Assert-VerifiableMock } }