├── .gitignore ├── azure-pipelines.ps1 ├── azure-pipelines.yml ├── LICENSE ├── SECURITY.md ├── README.md ├── Update-AutomationAzureModulesForAccount.ps1 └── Tests └── Update-AutomationAzureModulesForAccount.Tests.ps1 /.gitignore: -------------------------------------------------------------------------------- 1 | Test-Pester.XML 2 | -------------------------------------------------------------------------------- /azure-pipelines.ps1: -------------------------------------------------------------------------------- 1 | Install-Module -Name Pester -MinimumVersion 4.8.1 -Force -SkipPublisherCheck 2 | 3 | Invoke-Pester -Script "./Tests/" -OutputFile "./Test-Pester.XML" -OutputFormat 'NUnitXML' ` 4 | -CodeCoverage "./Update-AutomationAzureModulesForAccount.ps1" -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # Starter pipeline 2 | # Start with a minimal pipeline that you can customize to build and deploy your code. 3 | # Add steps that build, run tests, deploy, and more: 4 | # https://aka.ms/yaml 5 | 6 | trigger: 7 | - master 8 | 9 | pool: 10 | vmImage: 'windows-2019' 11 | 12 | steps: 13 | - task: PowerShell@2 14 | inputs: 15 | filePath: 'azure-pipelines.ps1' 16 | pwsh: false 17 | 18 | - task: PublishTestResults@2 19 | inputs: 20 | testResultsFormat: 'NUnit' 21 | testResultsFiles: '**/TEST-*.xml' -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Updating Azure PowerShell modules in Azure Automation accounts 2 | 3 | [![Build Status](https://dev.azure.com/AzureAutomation-Account-Modules-Update/AzureAutomation-Account-Modules-Update/_apis/build/status/microsoft.AzureAutomation-Account-Modules-Update?branchName=master)](https://dev.azure.com/AzureAutomation-Account-Modules-Update/AzureAutomation-Account-Modules-Update/_build/latest?definitionId=1&branchName=master) 4 | 5 | ## Purpose 6 | 7 | This Azure Automation runbook updates Azure PowerShell modules imported into an Azure Automation 8 | account with the module versions published to the PowerShell Gallery. See 9 | [How to update Azure PowerShell modules in Azure Automation](https://docs.microsoft.com/azure/automation/automation-update-azure-modules) 10 | for more details. 11 | 12 | ## Usage 13 | 14 | Import this runbook into your Automation account, and [start](https://docs.microsoft.com/azure/automation/automation-starting-a-runbook) it as a regular Automation runbook. 15 | 16 | ## Notes 17 | 18 | * If you import this runbook with the original name (**Update-AutomationAzureModulesForAccount**), 19 | it will override the internal runbook with this name. As a result, the imported runbook will 20 | run when the **Update Azure Modules** button is pushed or when this runbook is invoked directly 21 | via ARM API for this Automation account. If this is not what you want, specify a different name 22 | when importing this runbook. 23 | * **Azure** and **AzureRM.\*** modules are currently supported by default. 24 | * The new [Azure PowerShell Az modules](https://docs.microsoft.com/powershell/azure/new-azureps-module-az) 25 | are also supported. You **must** supply the `AzureModuleClass` runbook parameter with `Az` if 26 | your runbooks use only Az modules to avoid conflicts. More information can be found in the 27 | [Microsoft Docs](https://docs.microsoft.com/azure/automation/az-modules) and 28 | issue [#5](https://github.com/microsoft/AzureAutomation-Account-Modules-Update/issues/5). 29 | * Before starting this runbook, make sure your Automation account has a System assigned Managed Identity created. 30 | * You can use this code as a regular PowerShell script instead of a runbook: just login to Azure 31 | using the [Connect-AzureRmAccount](https://docs.microsoft.com/powershell/module/azurerm.profile/connect-azurermaccount) 32 | command first, then pass `-Login $false` to the script. 33 | * To use this runbook on the sovereign clouds, provide the appropriate value to the `AzureEnvironment` 34 | parameter. Please also make sure you read the 35 | [compatibility notes](https://docs.microsoft.com/azure/automation/automation-update-azure-modules#alternative-ways-to-update-your-modules). 36 | * When facing compatibility issues, you may want to use specific older module versions instead of 37 | the latest available on the PowerShell Gallery. In this case, provide the required versions in 38 | the `ModuleVersionOverrides` parameter. 39 | 40 | ## Contributing 41 | 42 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 43 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 44 | the rights to use your contribution. For details, visit https://cla.microsoft.com. 45 | 46 | When you submit a pull request, a CLA-bot will automatically determine whether you need to provide 47 | a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions 48 | provided by the bot. You will only need to do this once across all repos using our CLA. 49 | 50 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 51 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 52 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 53 | -------------------------------------------------------------------------------- /Update-AutomationAzureModulesForAccount.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT License. 4 | #> 5 | 6 | <# 7 | .SYNOPSIS 8 | Update Azure PowerShell modules in an Azure Automation account. 9 | 10 | .DESCRIPTION 11 | This Azure Automation runbook updates Azure PowerShell modules imported into an 12 | Azure Automation account with the module versions published to the PowerShell Gallery. 13 | 14 | Prerequisite: an Azure Automation account with a System assigned Managed Identity Enabled. 15 | 16 | .PARAMETER ResourceGroupName 17 | The Azure resource group name. 18 | 19 | .PARAMETER AutomationAccountName 20 | The Azure Automation account name. 21 | 22 | .PARAMETER SimultaneousModuleImportJobCount 23 | (Optional) The maximum number of module import jobs allowed to run concurrently. 24 | 25 | .PARAMETER AzureModuleClass 26 | (Optional) The class of module that will be updated (AzureRM or Az) 27 | If set to Az, this script will rely on only Az modules to update other modules. 28 | Set this to Az if your runbooks use only Az modules to avoid conflicts. 29 | 30 | .PARAMETER AzureEnvironment 31 | (Optional) Azure environment name. 32 | 33 | .PARAMETER Login 34 | (Optional) If $false, do not login to Azure. 35 | 36 | .PARAMETER ModuleVersionOverrides 37 | (Optional) Module versions to use instead of the latest on the PowerShell Gallery. 38 | If $null, the currently published latest versions will be used. 39 | If not $null, must contain a JSON-serialized dictionary, for example: 40 | '{ "AzureRM.Compute": "5.8.0", "AzureRM.Network": "6.10.0" }' 41 | or 42 | @{ 'AzureRM.Compute'='5.8.0'; 'AzureRM.Network'='6.10.0' } | ConvertTo-Json 43 | 44 | .PARAMETER PsGalleryApiUrl 45 | (Optional) PowerShell Gallery API URL. 46 | 47 | .LINK 48 | https://docs.microsoft.com/en-us/azure/automation/automation-update-azure-modules 49 | #> 50 | 51 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "")] 52 | param( 53 | [Parameter(Mandatory = $true)] 54 | [string] $ResourceGroupName, 55 | 56 | [Parameter(Mandatory = $true)] 57 | [string] $AutomationAccountName, 58 | 59 | [Parameter(Mandatory = $true)] 60 | [string] $SubscriptionId, 61 | 62 | [int] $SimultaneousModuleImportJobCount = 10, 63 | 64 | [string] $AzureModuleClass = 'AzureRM', 65 | 66 | [string] $AzureEnvironment = 'AzureCloud', 67 | 68 | [bool] $Login = $true, 69 | 70 | [string] $ModuleVersionOverrides = $null, 71 | 72 | [string] $PsGalleryApiUrl = 'https://www.powershellgallery.com/api/v2' 73 | ) 74 | 75 | $ErrorActionPreference = "Continue" 76 | 77 | #region Constants 78 | 79 | $script:AzureRMProfileModuleName = "AzureRM.Profile" 80 | $script:AzureRMAutomationModuleName = "AzureRM.Automation" 81 | $script:GetAzureRmAutomationModule = "Get-AzureRmAutomationModule" 82 | $script:NewAzureRmAutomationModule = "New-AzureRmAutomationModule" 83 | 84 | $script:AzAccountsModuleName = "Az.Accounts" 85 | $script:AzAutomationModuleName = "Az.Automation" 86 | $script:GetAzAutomationModule = "Get-AzAutomationModule" 87 | $script:NewAzAutomationModule = "New-AzAutomationModule" 88 | 89 | $script:AzureSdkOwnerName = "azure-sdk" 90 | 91 | #endregion 92 | 93 | #region Functions 94 | 95 | function ConvertJsonDictTo-HashTable($JsonString) { 96 | try{ 97 | $JsonObj = ConvertFrom-Json $JsonString -ErrorAction Stop 98 | } catch [System.ArgumentException] { 99 | throw "Unable to deserialize the JSON string for parameter ModuleVersionOverrides: ", $_ 100 | } 101 | 102 | $Result = @{} 103 | foreach ($Property in $JsonObj.PSObject.Properties) { 104 | $Result[$Property.Name] = $Property.Value 105 | } 106 | 107 | $Result 108 | } 109 | 110 | # Use the Run As connection to login to Azure 111 | function Login-AzureAutomation([bool] $AzModuleOnly) { 112 | try { 113 | Write-Output "Logging in to Azure ($AzureEnvironment)..." 114 | 115 | if ($AzModuleOnly) { 116 | # Ensures you do not inherit an AzContext in your runbook 117 | Disable-AzContextAutosave -Scope Process 118 | # Connect to Azure with system-assigned managed identity. 119 | # Please enable appropriate RBAC permissions to the system identity of this automation account. Otherwise, the runbook may fail... 120 | $context = (Connect-AzAccount -Identity -Environment $AzureEnvironment).Context 121 | 122 | # set and store context 123 | $AzureContext = Set-AzContext -SubscriptionId $SubscriptionId -ErrorAction Stop 124 | Select-AzSubscription -SubscriptionId $SubscriptionId | Write-Verbose 125 | } else { 126 | # Connect to Azure with system-assigned managed identity. 127 | # Please enable appropriate RBAC permissions to the system identity of this automation account. Otherwise, the runbook may fail... 128 | $context = (Connect-AzureRmAccount -Identity -Environment $AzureEnvironment).Context 129 | $AzureContext = Set-AzureRmContext -SubscriptionId $SubscriptionId -ErrorAction Stop 130 | Select-AzureRmSubscription -SubscriptionId $SubscriptionId | Write-Verbose 131 | } 132 | } catch { 133 | throw $_.Exception 134 | } 135 | } 136 | 137 | # Checks the PowerShell Gallery for the latest available version for the module 138 | function Get-ModuleDependencyAndLatestVersion([string] $ModuleName) { 139 | 140 | $ModuleUrlFormat = "$PsGalleryApiUrl/Search()?`$filter={1}&searchTerm=%27{0}%27&targetFramework=%27%27&includePrerelease=false&`$skip=0&`$top=40" 141 | 142 | $ForcedModuleVersion = $ModuleVersionOverridesHashTable[$ModuleName] 143 | 144 | $CurrentModuleUrl = 145 | if ($ForcedModuleVersion) { 146 | $ModuleUrlFormat -f $ModuleName, "Version%20eq%20'$ForcedModuleVersion'" 147 | } else { 148 | $ModuleUrlFormat -f $ModuleName, 'IsLatestVersion' 149 | } 150 | 151 | $SearchResult = Invoke-RestMethod -Method Get -Uri $CurrentModuleUrl -UseBasicParsing 152 | 153 | if (!$SearchResult) { 154 | Write-Verbose "Could not find module $ModuleName on PowerShell Gallery. This may be a module you imported from a different location. Ignoring this module" 155 | } else { 156 | if ($SearchResult.Length -and $SearchResult.Length -gt 1) { 157 | $SearchResult = $SearchResult | Where-Object { $_.title.InnerText -eq $ModuleName } 158 | } 159 | 160 | if (!$SearchResult) { 161 | Write-Verbose "Could not find module $ModuleName on PowerShell Gallery. This may be a module you imported from a different location. Ignoring this module" 162 | } else { 163 | $PackageDetails = Invoke-RestMethod -Method Get -UseBasicParsing -Uri $SearchResult.id 164 | 165 | # Ignore the modules that are not published as part of the Azure SDK 166 | if ($PackageDetails.entry.properties.Owners -ne $script:AzureSdkOwnerName) { 167 | Write-Warning "Module : $ModuleName is not part of azure sdk. Ignoring this." 168 | } else { 169 | $ModuleVersion = $PackageDetails.entry.properties.version 170 | $Dependencies = $PackageDetails.entry.properties.dependencies 171 | 172 | @($ModuleVersion, $Dependencies) 173 | } 174 | } 175 | } 176 | } 177 | 178 | function Get-ModuleContentUrl($ModuleName) { 179 | $ModuleContentUrlFormat = "$PsGalleryApiUrl/package/{0}" 180 | $VersionedModuleContentUrlFormat = "$ModuleContentUrlFormat/{1}" 181 | 182 | $ForcedModuleVersion = $ModuleVersionOverridesHashTable[$ModuleName] 183 | if ($ForcedModuleVersion) { 184 | $VersionedModuleContentUrlFormat -f $ModuleName, $ForcedModuleVersion 185 | } else { 186 | $ModuleContentUrlFormat -f $ModuleName 187 | } 188 | } 189 | 190 | # Imports the module with given version into Azure Automation 191 | function Import-AutomationModule([string] $ModuleName, [bool] $UseAzModule = $false) { 192 | 193 | $NewAutomationModule = $null 194 | $GetAutomationModule = $null 195 | if ($UseAzModule) { 196 | $GetAutomationModule = $script:GetAzAutomationModule 197 | $NewAutomationModule = $script:NewAzAutomationModule 198 | } else { 199 | $GetAutomationModule = $script:GetAzureRmAutomationModule 200 | $NewAutomationModule = $script:NewAzureRmAutomationModule 201 | } 202 | 203 | 204 | $LatestModuleVersionOnGallery = (Get-ModuleDependencyAndLatestVersion $ModuleName)[0] 205 | 206 | $ModuleContentUrl = Get-ModuleContentUrl $ModuleName 207 | # Find the actual blob storage location of the module 208 | do { 209 | $ModuleContentUrl = (Invoke-WebRequest -Uri $ModuleContentUrl -MaximumRedirection 0 -UseBasicParsing -ErrorAction Ignore).Headers.Location 210 | } while (!$ModuleContentUrl.Contains(".nupkg")) 211 | 212 | $CurrentModule = & $GetAutomationModule ` 213 | -Name $ModuleName ` 214 | -ResourceGroupName $ResourceGroupName ` 215 | -AutomationAccountName $AutomationAccountName 216 | 217 | if ($CurrentModule.Version -eq $LatestModuleVersionOnGallery) { 218 | Write-Output "Module : $ModuleName is already present with version $LatestModuleVersionOnGallery. Skipping Import" 219 | } else { 220 | Write-Output "Importing $ModuleName module of version $LatestModuleVersionOnGallery to Automation" 221 | 222 | & $NewAutomationModule ` 223 | -ResourceGroupName $ResourceGroupName ` 224 | -AutomationAccountName $AutomationAccountName ` 225 | -Name $ModuleName ` 226 | -ContentLink $ModuleContentUrl > $null 227 | } 228 | } 229 | 230 | # Parses the dependency got from PowerShell Gallery and returns name and version 231 | function GetModuleNameAndVersionFromPowershellGalleryDependencyFormat([string] $Dependency) { 232 | if ($null -eq $Dependency) { 233 | throw "Improper dependency format" 234 | } 235 | 236 | $Tokens = $Dependency -split":" 237 | if ($Tokens.Count -ne 3) { 238 | throw "Improper dependency format" 239 | } 240 | 241 | $ModuleName = $Tokens[0] 242 | $ModuleVersion = $Tokens[1].Trim("[","]") 243 | 244 | @($ModuleName, $ModuleVersion) 245 | } 246 | 247 | # Validates if the given list of modules has already been added to the module import map 248 | function AreAllModulesAdded([string[]] $ModuleListToAdd) { 249 | $Result = $true 250 | 251 | foreach ($ModuleToAdd in $ModuleListToAdd) { 252 | $ModuleAccounted = $false 253 | 254 | # $ModuleToAdd is specified in the following format: 255 | # ModuleName:ModuleVersionSpecification: 256 | # where ModuleVersionSpecification follows the specifiation 257 | # at https://docs.microsoft.com/en-us/nuget/reference/package-versioning#version-ranges-and-wildcards 258 | # For example: 259 | # AzureRm.profile:[4.0.0]: 260 | # or 261 | # AzureRm.profile:3.0.0: 262 | # In any case, the dependency version specification is always separated from the module name with 263 | # the ':' character. The explicit intent of this runbook is to always install the latest module versions, 264 | # so we want to completely ignore version specifications here. 265 | $ModuleNameToAdd = $ModuleToAdd -replace '\:.*', '' 266 | 267 | foreach($AlreadyIncludedModules in $ModuleImportMapOrder) { 268 | if ($AlreadyIncludedModules -contains $ModuleNameToAdd) { 269 | $ModuleAccounted = $true 270 | break 271 | } 272 | } 273 | 274 | if (!$ModuleAccounted) { 275 | $Result = $false 276 | break 277 | } 278 | } 279 | 280 | $Result 281 | } 282 | 283 | # Creates a module import map. This is a 2D array of strings so that the first 284 | # element in the array consist of modules with no dependencies. 285 | # The second element only depends on the modules in the first element, the 286 | # third element only dependes on modules in the first and second and so on. 287 | function Create-ModuleImportMapOrder([bool] $AzModuleOnly) { 288 | $ModuleImportMapOrder = $null 289 | $ProfileOrAccountsModuleName = $null 290 | $GetAutomationModule = $null 291 | 292 | # Use the relevant module class to avoid conflicts 293 | if ($AzModuleOnly) { 294 | $ProfileOrAccountsModuleName = $script:AzAccountsModuleName 295 | $GetAutomationModule = $script:GetAzAutomationModule 296 | } else { 297 | $ProfileOrAccountsModuleName = $script:AzureRmProfileModuleName 298 | $GetAutomationModule = $script:GetAzureRmAutomationModule 299 | } 300 | 301 | # Get all the non-conflicting modules in the current automation account 302 | $CurrentAutomationModuleList = & $GetAutomationModule ` 303 | -ResourceGroupName $ResourceGroupName ` 304 | -AutomationAccountName $AutomationAccountName | 305 | ?{ 306 | ($AzModuleOnly -and ($_.Name -eq 'Az' -or $_.Name -like 'Az.*')) -or 307 | (!$AzModuleOnly -and ($_.Name -eq 'AzureRM' -or $_.Name -like 'AzureRM.*' -or 308 | $_.Name -eq 'Azure' -or $_.Name -like 'Azure.*')) 309 | } 310 | 311 | # Get the latest version of the AzureRM.Profile OR Az.Accounts module 312 | $VersionAndDependencies = Get-ModuleDependencyAndLatestVersion $ProfileOrAccountsModuleName 313 | 314 | $ModuleEntry = $ProfileOrAccountsModuleName 315 | $ModuleEntryArray = ,$ModuleEntry 316 | $ModuleImportMapOrder += ,$ModuleEntryArray 317 | 318 | do { 319 | $NextAutomationModuleList = $null 320 | $CurrentChainVersion = $null 321 | # Add it to the list if the modules are not available in the same list 322 | foreach ($Module in $CurrentAutomationModuleList) { 323 | $Name = $Module.Name 324 | 325 | Write-Verbose "Checking dependencies for $Name" 326 | $VersionAndDependencies = Get-ModuleDependencyAndLatestVersion $Module.Name 327 | if ($null -eq $VersionAndDependencies) { 328 | continue 329 | } 330 | 331 | $Dependencies = $VersionAndDependencies[1].Split("|") 332 | 333 | $AzureModuleEntry = $Module.Name 334 | 335 | # If the previous list contains all the dependencies then add it to current list 336 | if ((-not $Dependencies) -or (AreAllModulesAdded $Dependencies)) { 337 | Write-Verbose "Adding module $Name to dependency chain" 338 | $CurrentChainVersion += ,$AzureModuleEntry 339 | } else { 340 | # else add it back to the main loop variable list if not already added 341 | if (!(AreAllModulesAdded $AzureModuleEntry)) { 342 | Write-Verbose "Module $Name does not have all dependencies added as yet. Moving module for later import" 343 | $NextAutomationModuleList += ,$Module 344 | } 345 | } 346 | } 347 | 348 | $ModuleImportMapOrder += ,$CurrentChainVersion 349 | $CurrentAutomationModuleList = $NextAutomationModuleList 350 | 351 | } while ($null -ne $CurrentAutomationModuleList) 352 | 353 | $ModuleImportMapOrder 354 | } 355 | 356 | # Wait and confirm that all the modules in the list have been imported successfully in Azure Automation 357 | function Wait-AllModulesImported( 358 | [Collections.Generic.List[string]] $ModuleList, 359 | [int] $Count, 360 | [bool] $UseAzModule = $false) { 361 | 362 | $GetAutomationModule = if ($UseAzModule) { 363 | $script:GetAzAutomationModule 364 | } else { 365 | $script:GetAzureRmAutomationModule 366 | } 367 | 368 | $i = $Count - $SimultaneousModuleImportJobCount 369 | if ($i -lt 0) { $i = 0 } 370 | 371 | for ( ; $i -lt $Count; $i++) { 372 | $Module = $ModuleList[$i] 373 | 374 | Write-Output ("Checking import Status for module : {0}" -f $Module) 375 | while ($true) { 376 | $AutomationModule = & $GetAutomationModule ` 377 | -Name $Module ` 378 | -ResourceGroupName $ResourceGroupName ` 379 | -AutomationAccountName $AutomationAccountName 380 | 381 | $IsTerminalProvisioningState = ($AutomationModule.ProvisioningState -eq "Succeeded") -or 382 | ($AutomationModule.ProvisioningState -eq "Failed") 383 | 384 | if ($IsTerminalProvisioningState) { 385 | break 386 | } 387 | 388 | Write-Verbose ("Module {0} is getting imported" -f $Module) 389 | Start-Sleep -Seconds 30 390 | } 391 | 392 | if ($AutomationModule.ProvisioningState -ne "Succeeded") { 393 | Write-Error ("Failed to import module : {0}. Status : {1}" -f $Module, $AutomationModule.ProvisioningState) 394 | } else { 395 | Write-Output ("Successfully imported module : {0}" -f $Module) 396 | } 397 | } 398 | } 399 | 400 | # Uses the module import map created to import modules. 401 | # It will only import modules from an element in the array if all the modules 402 | # from the previous element have been added. 403 | function Import-ModulesInAutomationAccordingToDependency([string[][]] $ModuleImportMapOrder, [bool] $UseAzModule) { 404 | 405 | foreach($ModuleList in $ModuleImportMapOrder) { 406 | $i = 0 407 | Write-Output "Importing Array of modules : $ModuleList" 408 | foreach ($Module in $ModuleList) { 409 | Write-Verbose ("Importing module : {0}" -f $Module) 410 | Import-AutomationModule -ModuleName $Module -UseAzModule $UseAzModule 411 | $i++ 412 | if ($i % $SimultaneousModuleImportJobCount -eq 0) { 413 | # It takes some time for the modules to start getting imported. 414 | # Sleep for sometime before making a query to see the status 415 | Start-Sleep -Seconds 20 416 | Wait-AllModulesImported -ModuleList $ModuleList -Count $i -UseAzModule $UseAzModule 417 | } 418 | } 419 | 420 | if ($i -lt $SimultaneousModuleImportJobCount) { 421 | Start-Sleep -Seconds 20 422 | Wait-AllModulesImported -ModuleList $ModuleList -Count $i -UseAzModule $UseAzModule 423 | } 424 | } 425 | } 426 | 427 | function Update-ProfileAndAutomationVersionToLatest([string] $AutomationModuleName) { 428 | # Get the latest azure automation module version 429 | $VersionAndDependencies = Get-ModuleDependencyAndLatestVersion $AutomationModuleName 430 | 431 | # Automation only has dependency on profile 432 | $ModuleDependencies = GetModuleNameAndVersionFromPowershellGalleryDependencyFormat $VersionAndDependencies[1] 433 | $ProfileModuleName = $ModuleDependencies[0] 434 | 435 | # Create web client object for downloading data 436 | $WebClient = New-Object System.Net.WebClient 437 | 438 | # Download AzureRM.Profile to temp location 439 | $ModuleContentUrl = Get-ModuleContentUrl $ProfileModuleName 440 | $ProfileURL = (Invoke-WebRequest -Uri $ModuleContentUrl -MaximumRedirection 0 -UseBasicParsing -ErrorAction Ignore).Headers.Location 441 | $ProfilePath = Join-Path $env:TEMP ($ProfileModuleName + ".zip") 442 | $WebClient.DownloadFile($ProfileURL, $ProfilePath) 443 | 444 | # Download AzureRM.Automation to temp location 445 | $ModuleContentUrl = Get-ModuleContentUrl $AutomationModuleName 446 | $AutomationURL = (Invoke-WebRequest -Uri $ModuleContentUrl -MaximumRedirection 0 -UseBasicParsing -ErrorAction Ignore).Headers.Location 447 | $AutomationPath = Join-Path $env:TEMP ($AutomationModuleName + ".zip") 448 | $WebClient.DownloadFile($AutomationURL, $AutomationPath) 449 | 450 | # Create folder for unzipping the Module files 451 | $PathFolderName = New-Guid 452 | $PathFolder = Join-Path $env:TEMP $PathFolderName 453 | 454 | # Unzip files 455 | $ProfileUnzipPath = Join-Path $PathFolder $ProfileModuleName 456 | Expand-Archive -Path $ProfilePath -DestinationPath $ProfileUnzipPath -Force 457 | $AutomationUnzipPath = Join-Path $PathFolder $AutomationModuleName 458 | Expand-Archive -Path $AutomationPath -DestinationPath $AutomationUnzipPath -Force 459 | 460 | # Import modules 461 | Import-Module (Join-Path $ProfileUnzipPath ($ProfileModuleName + ".psd1")) -Force -Verbose 462 | Import-Module (Join-Path $AutomationUnzipPath ($AutomationModuleName + ".psd1")) -Force -Verbose 463 | } 464 | 465 | #endregion 466 | 467 | #region Main body 468 | 469 | if ($ModuleVersionOverrides) { 470 | $ModuleVersionOverridesHashTable = ConvertJsonDictTo-HashTable $ModuleVersionOverrides 471 | } else { 472 | $ModuleVersionOverridesHashTable = @{} 473 | } 474 | 475 | 476 | $UseAzModule = $null 477 | $AutomationModuleName = $null 478 | 479 | # We want to support updating Az modules. This means this runbook should support upgrading using only Az modules 480 | if ($AzureModuleClass -eq "Az") { 481 | $UseAzModule = $true 482 | $AutomationModuleName = $script:AzAutomationModuleName 483 | } elseif ( $AzureModuleClass -eq "AzureRM") { 484 | $UseAzModule = $false 485 | $AutomationModuleName = $script:AzureRMAutomationModuleName 486 | } else { 487 | Write-Error "Invalid AzureModuleClass: '$AzureModuleClass'. Must be either Az or AzureRM" -ErrorAction Stop 488 | } 489 | 490 | # Import the latest version of the Az automation and accounts version to the local sandbox 491 | Update-ProfileAndAutomationVersionToLatest $AutomationModuleName 492 | 493 | if ($Login) { 494 | Login-AzureAutomation $UseAzModule 495 | } 496 | 497 | $ModuleImportMapOrder = Create-ModuleImportMapOrder $UseAzModule 498 | Import-ModulesInAutomationAccordingToDependency $ModuleImportMapOrder $UseAzModule 499 | 500 | 501 | #endregion 502 | -------------------------------------------------------------------------------- /Tests/Update-AutomationAzureModulesForAccount.Tests.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the MIT License. 4 | #> 5 | 6 | #requires -Modules @{ ModuleName='Pester'; ModuleVersion='4.1.1' } 7 | 8 | $here = Split-Path -Parent $MyInvocation.MyCommand.Path 9 | $sutDirectory = Split-Path -Parent $here 10 | $sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.' 11 | 12 | Describe 'Update-AutomationAzureModulesForAccount runbook' { 13 | 14 | #region Stub external commands 15 | 16 | function Import-Module { 17 | [CmdletBinding()] 18 | param($Name, [switch]$Force) 19 | } 20 | 21 | function New-Object($TypeName) { } 22 | 23 | function Start-Sleep($Seconds) { } 24 | 25 | function Invoke-RestMethod($Method, $Uri, [switch]$UseBasicParsing) { } 26 | 27 | function Invoke-WebRequest($Uri, $MaximumRedirection, [switch]$UseBasicParsing, $ErrorAction) {} 28 | 29 | function Expand-Archive($Path, $DestinationPath, [switch]$Force) { } 30 | 31 | function Get-AzureRmAutomationModule($Name, $ResourceGroupName, $AutomationAccountName) { } 32 | 33 | function New-AzureRmAutomationModule($ResourceGroupName, $AutomationAccountName, $Name, $ContentLink) { } 34 | 35 | function Get-AzAutomationModule($Name, $ResourceGroupName, $AutomationAccountName) { } 36 | 37 | function New-AzAutomationModule($ResourceGroupName, $AutomationAccountName, $Name, $ContentLink) { } 38 | 39 | #endregion 40 | 41 | #region Global mocks 42 | 43 | Mock New-Object { 44 | $TypeName | Should be 'System.Net.WebClient' 45 | [FakeWebClient]::New() 46 | } 47 | 48 | #endregion 49 | 50 | #region Test utilities 51 | 52 | function Invoke-Update-AutomationAzureModulesForAccount($OptionalParameters) { 53 | $script:LastErrors = $null 54 | 55 | if ($null -eq $OptionalParameters) { 56 | $OptionalParameters = @{ } 57 | } 58 | 59 | & "$sutDirectory\$sut" ` 60 | -ResourceGroupName 'Fake RG' ` 61 | -AutomationAccountName 'Fake account' ` 62 | -Login $false ` 63 | -ErrorAction SilentlyContinue ` 64 | -ErrorVariable script:LastErrors ` 65 | @OptionalParameters 66 | 67 | It 'Reports no errors' { $script:LastErrors | Should be $null } 68 | } 69 | 70 | class FakeWebClient { 71 | [void]DownloadFile($SourceUrl, $DestinationPath) { } 72 | } 73 | 74 | function Assert-CorrectSearchUri($Uri, $ModuleName, $Filter = 'IsLatestVersion') { 75 | $ExpectedUri = "https://www.powershellgallery.com/api/v2/Search()?`$filter=$Filter&searchTerm=%27$ModuleName%27&targetFramework=%27%27&includePrerelease=false&`$skip=0&`$top=40" 76 | $Uri | Should be $ExpectedUri > $null 77 | } 78 | 79 | #endregion 80 | 81 | Context 'No overridden module versions' { 82 | Mock Get-AzureRmAutomationModule { 83 | @{ 84 | Name = 'AzureRM.FakeAzureModule' 85 | Version = '1.0.0' 86 | ProvisioningState = 'Succeeded' 87 | } 88 | } -Verifiable 89 | 90 | Mock Invoke-RestMethod -ParameterFilter { 91 | $Uri -match '%27AzureRM\.Automation%27' 92 | } -MockWith { 93 | $Method | Should be 'Get' > $null 94 | Assert-CorrectSearchUri -Uri $Uri -ModuleName AzureRM.Automation 95 | 96 | @{ 97 | id = 'fake AzureRM.Automation search result id' 98 | } 99 | } -Verifiable 100 | 101 | Mock Invoke-RestMethod -ParameterFilter { 102 | $Uri -eq 'fake AzureRM.Automation search result id' 103 | } -MockWith { 104 | $Method | Should be 'Get' > $null 105 | 106 | @{ 107 | entry = @{ 108 | properties = @{ 109 | version = 'fake version' 110 | dependencies = 'AzureRM.Profile:[1.0.0]:' 111 | owners = 'azure-sdk' 112 | } 113 | } 114 | } 115 | } -Verifiable 116 | 117 | Mock Invoke-RestMethod -ParameterFilter { 118 | $Uri -match '%27AzureRM\.Profile%27' 119 | } -MockWith { 120 | $Method | Should be 'Get' > $null 121 | Assert-CorrectSearchUri -Uri $Uri -ModuleName AzureRM.Profile 122 | 123 | @{ 124 | id = 'fake AzureRM.Profile search result id' 125 | } 126 | } -Verifiable 127 | 128 | Mock Invoke-RestMethod -ParameterFilter { 129 | $Uri -eq 'fake AzureRM.Profile search result id' 130 | } -MockWith { 131 | $Method | Should be 'Get' > $null 132 | 133 | @{ 134 | entry = @{ 135 | properties = @{ 136 | version = 'fake version' 137 | dependencies = '' 138 | owners = 'azure-sdk' 139 | } 140 | } 141 | } 142 | } -Verifiable 143 | 144 | Mock Invoke-WebRequest -ParameterFilter { 145 | $Uri -eq 'https://www.powershellgallery.com/api/v2/package/AzureRM.Profile' 146 | } -MockWith { 147 | @{ 148 | Headers = @{ 149 | Location = 'Fake/AzureRM.Profile/Content/Location.nupkg' 150 | } 151 | } 152 | } -Verifiable 153 | 154 | Mock New-AzureRmAutomationModule -ParameterFilter { 155 | $Name -eq 'AzureRM.Profile' 156 | } -MockWith { 157 | $ContentLink | Should be 'Fake/AzureRM.Profile/Content/Location.nupkg' > $null 158 | } -Verifiable 159 | 160 | Mock Invoke-RestMethod -ParameterFilter { 161 | $Uri -match '%27AzureRM.FakeAzureModule%27' 162 | } -MockWith { 163 | $Method | Should be 'Get' > $null 164 | Assert-CorrectSearchUri -Uri $Uri -ModuleName AzureRM.FakeAzureModule 165 | 166 | @{ 167 | id = 'fake FakeAzureModule search result id' 168 | } 169 | } -Verifiable 170 | 171 | Mock Invoke-RestMethod -ParameterFilter { 172 | $Uri -eq 'fake FakeAzureModule search result id' 173 | } -MockWith { 174 | $Method | Should be 'Get' > $null 175 | 176 | @{ 177 | entry = @{ 178 | properties = @{ 179 | version = 'fake version' 180 | dependencies = 'AzureRM.Profile:[1.0.0]:' 181 | owners = 'azure-sdk' 182 | } 183 | } 184 | } 185 | } -Verifiable 186 | 187 | Mock Invoke-WebRequest -ParameterFilter { 188 | $Uri -match 'https://www.powershellgallery.com/api/v2/package/AzureRM.FakeAzureModule' 189 | } -MockWith { 190 | $Uri | Should be 'https://www.powershellgallery.com/api/v2/package/AzureRM.FakeAzureModule' > $null 191 | 192 | @{ 193 | Headers = @{ 194 | Location = 'Fake/AzureRM.FakeAzureModule/Content/Location.nupkg' 195 | } 196 | } 197 | } -Verifiable 198 | 199 | Mock New-AzureRmAutomationModule -ParameterFilter { 200 | $Name -eq 'AzureRM.FakeAzureModule' 201 | } -MockWith { 202 | $ContentLink | Should be 'Fake/AzureRM.FakeAzureModule/Content/Location.nupkg' > $null 203 | } -Verifiable 204 | 205 | Invoke-Update-AutomationAzureModulesForAccount -OptionalParameters @{ 206 | ModuleVersionOverrides = '{ }' 207 | } 208 | 209 | It 'Updates AzureRM.Profile module' { 210 | Assert-MockCalled New-AzureRmAutomationModule -ParameterFilter { $Name -eq 'AzureRM.Profile' } -Times 1 -Exactly 211 | } 212 | 213 | It 'Updates fake Azure module' { 214 | Assert-MockCalled New-AzureRmAutomationModule -ParameterFilter { $Name -eq 'AzureRM.FakeAzureModule' } -Times 1 -Exactly 215 | } 216 | 217 | Assert-VerifiableMock 218 | } 219 | 220 | Context 'With overridden module versions' { 221 | Mock Get-AzureRmAutomationModule { 222 | @{ 223 | Name = 'AzureRM.FakeAzureModule' 224 | Version = '1.0.0' 225 | ProvisioningState = 'Succeeded' 226 | } 227 | } -Verifiable 228 | 229 | Mock Invoke-RestMethod -ParameterFilter { 230 | $Uri -match '%27AzureRM\.Automation%27' 231 | } -MockWith { 232 | $Method | Should be 'Get' > $null 233 | Assert-CorrectSearchUri -Uri $Uri -ModuleName AzureRM.Automation 234 | 235 | @{ 236 | id = 'fake AzureRM.Automation search result id' 237 | } 238 | } -Verifiable 239 | 240 | Mock Invoke-RestMethod -ParameterFilter { 241 | $Uri -eq 'fake AzureRM.Automation search result id' 242 | } -MockWith { 243 | $Method | Should be 'Get' > $null 244 | 245 | @{ 246 | entry = @{ 247 | properties = @{ 248 | version = 'fake version' 249 | dependencies = 'AzureRM.Profile:[1.0.0]:' 250 | owners = 'azure-sdk' 251 | } 252 | } 253 | } 254 | } -Verifiable 255 | 256 | Mock Invoke-RestMethod -ParameterFilter { 257 | $Uri -match '%27AzureRM\.Profile%27' 258 | } -MockWith { 259 | $Method | Should be 'Get' > $null 260 | Assert-CorrectSearchUri -Uri $Uri -ModuleName AzureRM.Profile -Filter "Version%20eq%20'2.0.0'" 261 | 262 | @{ 263 | id = 'fake AzureRM.Profile search result id' 264 | } 265 | } -Verifiable 266 | 267 | Mock Invoke-RestMethod -ParameterFilter { 268 | $Uri -eq 'fake AzureRM.Profile search result id' 269 | } -MockWith { 270 | $Method | Should be 'Get' > $null 271 | 272 | @{ 273 | entry = @{ 274 | properties = @{ 275 | version = 'fake version' 276 | dependencies = '' 277 | owners = 'azure-sdk' 278 | } 279 | } 280 | } 281 | } -Verifiable 282 | 283 | Mock Invoke-WebRequest -ParameterFilter { 284 | $Uri -match 'AzureRM\.Profile' 285 | } -MockWith { 286 | $Uri | Should be 'https://www.powershellgallery.com/api/v2/package/AzureRM.Profile/2.0.0' > $null 287 | 288 | @{ 289 | Headers = @{ 290 | Location = 'Fake/AzureRM.Profile/Content/Location.nupkg' 291 | } 292 | } 293 | } -Verifiable 294 | 295 | Mock New-AzureRmAutomationModule -ParameterFilter { 296 | $Name -eq 'AzureRM.Profile' 297 | } -MockWith { 298 | $ContentLink | Should be 'Fake/AzureRM.Profile/Content/Location.nupkg' > $null 299 | } -Verifiable 300 | 301 | Mock Invoke-RestMethod -ParameterFilter { 302 | $Uri -match '%27AzureRM.FakeAzureModule%27' 303 | } -MockWith { 304 | $Method | Should be 'Get' > $null 305 | Assert-CorrectSearchUri -Uri $Uri -ModuleName AzureRM.FakeAzureModule 306 | 307 | @{ 308 | id = 'fake FakeAzureModule search result id' 309 | } 310 | } -Verifiable 311 | 312 | Mock Invoke-RestMethod -ParameterFilter { 313 | $Uri -eq 'fake FakeAzureModule search result id' 314 | } -MockWith { 315 | $Method | Should be 'Get' > $null 316 | 317 | @{ 318 | entry = @{ 319 | properties = @{ 320 | version = 'fake version' 321 | dependencies = 'AzureRM.Profile:[1.0.0]:' 322 | owners = 'azure-sdk' 323 | } 324 | } 325 | } 326 | } -Verifiable 327 | 328 | Mock Invoke-WebRequest -ParameterFilter { 329 | $Uri -match 'https://www.powershellgallery.com/api/v2/package/AzureRM.FakeAzureModule' 330 | } -MockWith { 331 | $Uri | Should be 'https://www.powershellgallery.com/api/v2/package/AzureRM.FakeAzureModule' > $null 332 | 333 | @{ 334 | Headers = @{ 335 | Location = 'Fake/AzureRM.FakeAzureModule/Content/Location.nupkg' 336 | } 337 | } 338 | } -Verifiable 339 | 340 | Mock New-AzureRmAutomationModule -ParameterFilter { 341 | $Name -eq 'AzureRM.FakeAzureModule' 342 | } -MockWith { 343 | $ContentLink | Should be 'Fake/AzureRM.FakeAzureModule/Content/Location.nupkg' > $null 344 | } -Verifiable 345 | 346 | Invoke-Update-AutomationAzureModulesForAccount -OptionalParameters @{ 347 | ModuleVersionOverrides = "{ 348 | 'AzureRM.Profile' : '2.0.0' 349 | }" 350 | } 351 | 352 | It 'Updates AzureRM.Profile module' { 353 | Assert-MockCalled New-AzureRmAutomationModule -ParameterFilter { $Name -eq 'AzureRM.Profile' } -Times 1 -Exactly 354 | } 355 | 356 | It 'Updates fake Azure module' { 357 | Assert-MockCalled New-AzureRmAutomationModule -ParameterFilter { $Name -eq 'AzureRM.FakeAzureModule' } -Times 1 -Exactly 358 | } 359 | 360 | Assert-VerifiableMock 361 | } 362 | 363 | Context 'When found multiple modules with similar name' { 364 | #region Expect these calls, but don't assert anything, as they are not relevant to this specific test 365 | 366 | Mock Invoke-RestMethod -ParameterFilter { $Uri -match '%27AzureRM\.Automation%27' } ` 367 | -MockWith { @{ id = 'fake AzureRM.Automation search result id' } } 368 | 369 | Mock Invoke-RestMethod -ParameterFilter { 370 | $Uri -eq 'fake AzureRM.Automation search result id' 371 | } -MockWith { 372 | @{ 373 | entry = @{ 374 | properties = @{ 375 | version = 'fake version' 376 | dependencies = 'AzureRM.Profile:[1.0.0]:' 377 | owners = 'azure-sdk' 378 | } 379 | } 380 | } 381 | } 382 | 383 | Mock Invoke-RestMethod -ParameterFilter { $Uri -match '%27AzureRM\.Profile%27' } ` 384 | -MockWith { @{ id = 'fake AzureRM.Profile search result id' } } 385 | 386 | Mock Invoke-RestMethod -ParameterFilter { 387 | $Uri -eq 'fake AzureRM.Profile search result id' 388 | } -MockWith { 389 | @{ 390 | entry = @{ 391 | properties = @{ 392 | version = 'fake version' 393 | dependencies = '' 394 | owners = 'azure-sdk' 395 | } 396 | } 397 | } 398 | } 399 | 400 | Mock Invoke-WebRequest -ParameterFilter { 401 | $Uri -match 'AzureRM\.Profile' 402 | } -MockWith { 403 | @{ 404 | Headers = @{ 405 | Location = 'Fake/AzureRM.Profile/Content/Location.nupkg' 406 | } 407 | } 408 | } 409 | 410 | #endregion 411 | 412 | Mock Get-AzureRmAutomationModule { 413 | @{ 414 | Name = 'AzureRM.FakeAzureModule' 415 | Version = '1.0.0' 416 | ProvisioningState = 'Succeeded' 417 | } 418 | } -Verifiable 419 | 420 | Mock Invoke-RestMethod -ParameterFilter { 421 | $Uri -match '%27AzureRM.FakeAzureModule%27' 422 | } -MockWith { 423 | $Method | Should be 'Get' > $null 424 | Assert-CorrectSearchUri -Uri $Uri -ModuleName AzureRM.FakeAzureModule -Filter "Version%20eq%20'2.0.0'" 425 | 426 | @{ 427 | id = 'fake FakeAzureModule search result id 1' 428 | title = @{ InnerText = 'AzureRM.FakeAzureModule.Different' } 429 | } 430 | 431 | @{ 432 | id = 'fake FakeAzureModule search result id 2' 433 | title = @{ InnerText = 'AzureRM.FakeAzureModule' } 434 | } 435 | 436 | @{ 437 | id = 'fake FakeAzureModule search result id 3' 438 | title = @{ InnerText = 'AzureRM.FakeAzureModule.AnotherOne' } 439 | } 440 | } -Verifiable 441 | 442 | Mock Invoke-RestMethod -ParameterFilter { 443 | $Uri -eq 'fake FakeAzureModule search result id 2' 444 | } -MockWith { 445 | $Method | Should be 'Get' > $null 446 | 447 | @{ 448 | entry = @{ 449 | properties = @{ 450 | version = '2.0.0' 451 | dependencies = '' 452 | owners = 'azure-sdk' 453 | } 454 | } 455 | } 456 | } -Verifiable 457 | 458 | Mock Invoke-WebRequest -ParameterFilter { 459 | $Uri -match 'https://www.powershellgallery.com/api/v2/package/AzureRM.FakeAzureModule' 460 | } -MockWith { 461 | $Uri | Should be 'https://www.powershellgallery.com/api/v2/package/AzureRM.FakeAzureModule/2.0.0' > $null 462 | 463 | @{ 464 | Headers = @{ 465 | Location = 'Fake/AzureRM.FakeAzureModule/Content/Location.nupkg' 466 | } 467 | } 468 | } -Verifiable 469 | 470 | Mock New-AzureRmAutomationModule -ParameterFilter { 471 | $Name -eq 'AzureRM.FakeAzureModule' 472 | } -MockWith { 473 | $ContentLink | Should be 'Fake/AzureRM.FakeAzureModule/Content/Location.nupkg' > $null 474 | } -Verifiable 475 | 476 | Invoke-Update-AutomationAzureModulesForAccount -OptionalParameters @{ 477 | ModuleVersionOverrides = "{ 478 | 'AzureRM.FakeAzureModule' : '2.0.0' 479 | }" 480 | } 481 | 482 | It 'Updates fake Azure module' { 483 | Assert-MockCalled New-AzureRmAutomationModule -ParameterFilter { $Name -eq 'AzureRM.FakeAzureModule' } -Times 1 -Exactly 484 | } 485 | 486 | Assert-VerifiableMock 487 | } 488 | 489 | Context 'Az module present with default ModuleClassName AzureRM' { 490 | Mock Get-AzureRmAutomationModule { 491 | @{ 492 | Name = 'Az.FakeAzModule' 493 | Version = '1.0.0' 494 | ProvisioningState = 'Succeeded' 495 | } 496 | } -Verifiable 497 | 498 | Mock Invoke-RestMethod -ParameterFilter { 499 | $Uri -match '%27AzureRM\.Automation%27' 500 | } -MockWith { 501 | $Method | Should be 'Get' > $null 502 | Assert-CorrectSearchUri -Uri $Uri -ModuleName AzureRM.Automation 503 | 504 | @{ 505 | id = 'fake AzureRM.Automation search result id' 506 | } 507 | } -Verifiable 508 | 509 | Mock Invoke-RestMethod -ParameterFilter { 510 | $Uri -eq 'fake AzureRM.Automation search result id' 511 | } -MockWith { 512 | $Method | Should be 'Get' > $null 513 | 514 | @{ 515 | entry = @{ 516 | properties = @{ 517 | version = 'fake version' 518 | dependencies = 'AzureRM.Profile:[1.0.0]:' 519 | owners = 'azure-sdk' 520 | } 521 | } 522 | } 523 | } -Verifiable 524 | 525 | Mock Invoke-RestMethod -ParameterFilter { 526 | $Uri -match '%27AzureRM\.Profile%27' 527 | } -MockWith { 528 | $Method | Should be 'Get' > $null 529 | Assert-CorrectSearchUri -Uri $Uri -ModuleName AzureRM.Profile 530 | 531 | @{ 532 | id = 'fake AzureRM.Profile search result id' 533 | } 534 | } -Verifiable 535 | 536 | Mock Invoke-RestMethod -ParameterFilter { 537 | $Uri -eq 'fake AzureRM.Profile search result id' 538 | } -MockWith { 539 | $Method | Should be 'Get' > $null 540 | 541 | @{ 542 | entry = @{ 543 | properties = @{ 544 | version = 'fake version' 545 | dependencies = '' 546 | owners = 'azure-sdk' 547 | } 548 | } 549 | } 550 | } -Verifiable 551 | 552 | Mock Invoke-WebRequest -ParameterFilter { 553 | $Uri -eq 'https://www.powershellgallery.com/api/v2/package/AzureRM.Profile' 554 | } -MockWith { 555 | @{ 556 | Headers = @{ 557 | Location = 'Fake/AzureRM.Profile/Content/Location.nupkg' 558 | } 559 | } 560 | } -Verifiable 561 | 562 | Mock New-AzureRmAutomationModule -ParameterFilter { 563 | $Name -eq 'AzureRM.Profile' 564 | } -MockWith { 565 | $ContentLink | Should be 'Fake/AzureRM.Profile/Content/Location.nupkg' > $null 566 | } -Verifiable 567 | 568 | Invoke-Update-AutomationAzureModulesForAccount -OptionalParameters @{ 569 | ModuleVersionOverrides = '{ }' 570 | } 571 | 572 | It 'Updates AzureRM.Profile module' { 573 | Assert-MockCalled New-AzureRmAutomationModule -ParameterFilter { $Name -eq 'AzureRM.Profile' } -Times 1 -Exactly 574 | } 575 | 576 | It 'Ignores fake Az module' { 577 | Assert-MockCalled New-AzureRmAutomationModule -ParameterFilter { $Name -eq 'Az.FakeAzModule' } -Times 0 -Exactly 578 | } 579 | 580 | Assert-VerifiableMock 581 | } 582 | 583 | Context 'AzureRM module present with ModuleClassName Az' { 584 | Mock Get-AzAutomationModule { 585 | @{ 586 | Name = 'AzureRM.FakeAzureModule' 587 | Version = '1.0.0' 588 | ProvisioningState = 'Succeeded' 589 | } 590 | } -Verifiable 591 | 592 | Mock Invoke-RestMethod -ParameterFilter { 593 | $Uri -match '%27Az\.Automation%27' 594 | } -MockWith { 595 | $Method | Should be 'Get' > $null 596 | Assert-CorrectSearchUri -Uri $Uri -ModuleName Az.Automation 597 | 598 | @{ 599 | id = 'fake Az.Automation search result id' 600 | } 601 | } -Verifiable 602 | 603 | Mock Invoke-RestMethod -ParameterFilter { 604 | $Uri -eq 'fake Az.Automation search result id' 605 | } -MockWith { 606 | $Method | Should be 'Get' > $null 607 | 608 | @{ 609 | entry = @{ 610 | properties = @{ 611 | version = 'fake version' 612 | dependencies = 'Az.Accounts:[1.0.0]:' 613 | owners = 'azure-sdk' 614 | } 615 | } 616 | } 617 | } -Verifiable 618 | 619 | Mock Invoke-RestMethod -ParameterFilter { 620 | $Uri -match '%27Az\.Accounts%27' 621 | } -MockWith { 622 | $Method | Should be 'Get' > $null 623 | Assert-CorrectSearchUri -Uri $Uri -ModuleName Az.Accounts 624 | 625 | @{ 626 | id = 'fake Az.Accounts search result id' 627 | } 628 | } -Verifiable 629 | 630 | Mock Invoke-RestMethod -ParameterFilter { 631 | $Uri -eq 'fake Az.Accounts search result id' 632 | } -MockWith { 633 | $Method | Should be 'Get' > $null 634 | 635 | @{ 636 | entry = @{ 637 | properties = @{ 638 | version = 'fake version' 639 | dependencies = '' 640 | owners = 'azure-sdk' 641 | } 642 | } 643 | } 644 | } -Verifiable 645 | 646 | Mock Invoke-WebRequest -ParameterFilter { 647 | $Uri -eq 'https://www.powershellgallery.com/api/v2/package/Az.Accounts' 648 | } -MockWith { 649 | @{ 650 | Headers = @{ 651 | Location = 'Fake/Az.Accounts/Content/Location.nupkg' 652 | } 653 | } 654 | } -Verifiable 655 | 656 | Mock New-AzAutomationModule -ParameterFilter { 657 | $Name -eq 'Az.Accounts' 658 | } -MockWith { 659 | $ContentLink | Should be 'Fake/Az.Accounts/Content/Location.nupkg' > $null 660 | } -Verifiable 661 | 662 | Invoke-Update-AutomationAzureModulesForAccount -OptionalParameters @{ 663 | ModuleVersionOverrides = '{ }' 664 | AzureModuleClass = 'Az' 665 | } 666 | 667 | It 'Updates Az.Accounts module' { 668 | Assert-MockCalled New-AzAutomationModule -ParameterFilter { $Name -eq 'Az.Accounts' } -Times 1 -Exactly 669 | } 670 | 671 | It 'Ignores fake AzureRM module' { 672 | Assert-MockCalled New-AzAutomationModule -ParameterFilter { $Name -eq 'AzureRM.FakeAzureModule' } -Times 0 -Exactly 673 | } 674 | 675 | Assert-VerifiableMock 676 | } 677 | 678 | Context 'Az module present with ModuleClassName Az' { 679 | Mock Get-AzAutomationModule { 680 | @{ 681 | Name = 'Az.FakeAzModule' 682 | Version = '1.0.0' 683 | ProvisioningState = 'Succeeded' 684 | } 685 | } -Verifiable 686 | 687 | Mock Invoke-RestMethod -ParameterFilter { 688 | $Uri -match '%27Az\.Automation%27' 689 | } -MockWith { 690 | $Method | Should be 'Get' > $null 691 | Assert-CorrectSearchUri -Uri $Uri -ModuleName Az.Automation 692 | 693 | @{ 694 | id = 'fake Az.Automation search result id' 695 | } 696 | } -Verifiable 697 | 698 | Mock Invoke-RestMethod -ParameterFilter { 699 | $Uri -eq 'fake Az.Automation search result id' 700 | } -MockWith { 701 | $Method | Should be 'Get' > $null 702 | 703 | @{ 704 | entry = @{ 705 | properties = @{ 706 | version = 'fake version' 707 | dependencies = 'Az.Accounts:[1.0.0]:' 708 | owners = 'azure-sdk' 709 | } 710 | } 711 | } 712 | } -Verifiable 713 | 714 | Mock Invoke-RestMethod -ParameterFilter { 715 | $Uri -match '%27Az\.Accounts%27' 716 | } -MockWith { 717 | $Method | Should be 'Get' > $null 718 | Assert-CorrectSearchUri -Uri $Uri -ModuleName Az.Accounts 719 | 720 | @{ 721 | id = 'fake Az.Accounts search result id' 722 | } 723 | } -Verifiable 724 | 725 | Mock Invoke-RestMethod -ParameterFilter { 726 | $Uri -eq 'fake Az.Accounts search result id' 727 | } -MockWith { 728 | $Method | Should be 'Get' > $null 729 | 730 | @{ 731 | entry = @{ 732 | properties = @{ 733 | version = 'fake version' 734 | dependencies = '' 735 | owners = 'azure-sdk' 736 | } 737 | } 738 | } 739 | } -Verifiable 740 | 741 | Mock Invoke-WebRequest -ParameterFilter { 742 | $Uri -eq 'https://www.powershellgallery.com/api/v2/package/Az.Accounts' 743 | } -MockWith { 744 | @{ 745 | Headers = @{ 746 | Location = 'Fake/Az.Accounts/Content/Location.nupkg' 747 | } 748 | } 749 | } -Verifiable 750 | 751 | Mock New-AzAutomationModule -ParameterFilter { 752 | $Name -eq 'Az.Accounts' 753 | } -MockWith { 754 | $ContentLink | Should be 'Fake/Az.Accounts/Content/Location.nupkg' > $null 755 | } -Verifiable 756 | 757 | Mock Invoke-RestMethod -ParameterFilter { 758 | $Uri -match '%27Az.FakeAzModule%27' 759 | } -MockWith { 760 | $Method | Should be 'Get' > $null 761 | Assert-CorrectSearchUri -Uri $Uri -ModuleName Az.FakeAzModule 762 | 763 | @{ 764 | id = 'fake FakeAzModule search result id' 765 | } 766 | } -Verifiable 767 | 768 | Mock Invoke-RestMethod -ParameterFilter { 769 | $Uri -eq 'fake FakeAzModule search result id' 770 | } -MockWith { 771 | $Method | Should be 'Get' > $null 772 | 773 | @{ 774 | entry = @{ 775 | properties = @{ 776 | version = 'fake version' 777 | dependencies = 'Az.Accounts:[1.0.0]:' 778 | owners = 'azure-sdk' 779 | } 780 | } 781 | } 782 | } -Verifiable 783 | 784 | Mock Invoke-WebRequest -ParameterFilter { 785 | $Uri -match 'https://www.powershellgallery.com/api/v2/package/Az.FakeAzModule' 786 | } -MockWith { 787 | $Uri | Should be 'https://www.powershellgallery.com/api/v2/package/Az.FakeAzModule' > $null 788 | 789 | @{ 790 | Headers = @{ 791 | Location = 'Fake/Az.FakeAzModule/Content/Location.nupkg' 792 | } 793 | } 794 | } -Verifiable 795 | 796 | Mock New-AzAutomationModule -ParameterFilter { 797 | $Name -eq 'Az.FakeAzModule' 798 | } -MockWith { 799 | $ContentLink | Should be 'Fake/Az.FakeAzModule/Content/Location.nupkg' > $null 800 | } -Verifiable 801 | 802 | Invoke-Update-AutomationAzureModulesForAccount -OptionalParameters @{ 803 | ModuleVersionOverrides = '{ }' 804 | AzureModuleClass = 'Az' 805 | } 806 | 807 | It 'Updates Az.Accounts module' { 808 | Assert-MockCalled New-AzAutomationModule -ParameterFilter { $Name -eq 'Az.Accounts' } -Times 1 -Exactly 809 | } 810 | 811 | It 'Updates fake Az module' { 812 | Assert-MockCalled New-AzAutomationModule -ParameterFilter { $Name -eq 'Az.FakeAzModule' } -Times 1 -Exactly 813 | } 814 | 815 | Assert-VerifiableMock 816 | } 817 | 818 | } 819 | --------------------------------------------------------------------------------