From 5479b55edab85324558a6dd6d55a9a24db6c3543 Mon Sep 17 00:00:00 2001 From: Robin Makkus Date: Tue, 15 Oct 2024 19:44:16 +0200 Subject: [PATCH] feat: Extension Management --- .../InstalledExtensions/Get-AzDoExtension.ps1 | 94 +++++++++++++ .../InstalledExtensions/New-AzDoExtension.ps1 | 91 +++++++++++++ .../Remove-AzDoExtension.ps1 | 70 ++++++++++ .../InstalledExtensions/Extensions.tests.ps1 | 128 ++++++++++++++++++ 4 files changed, 383 insertions(+) create mode 100644 AzureDevOpsPowerShell/Public/Api/ExtensionManagement/InstalledExtensions/Get-AzDoExtension.ps1 create mode 100644 AzureDevOpsPowerShell/Public/Api/ExtensionManagement/InstalledExtensions/New-AzDoExtension.ps1 create mode 100644 AzureDevOpsPowerShell/Public/Api/ExtensionManagement/InstalledExtensions/Remove-AzDoExtension.ps1 create mode 100644 tests/Api/ExtensionManagement/InstalledExtensions/Extensions.tests.ps1 diff --git a/AzureDevOpsPowerShell/Public/Api/ExtensionManagement/InstalledExtensions/Get-AzDoExtension.ps1 b/AzureDevOpsPowerShell/Public/Api/ExtensionManagement/InstalledExtensions/Get-AzDoExtension.ps1 new file mode 100644 index 00000000..bf8046a5 --- /dev/null +++ b/AzureDevOpsPowerShell/Public/Api/ExtensionManagement/InstalledExtensions/Get-AzDoExtension.ps1 @@ -0,0 +1,94 @@ + +function Get-AzDoExtension { + <# +.SYNOPSIS +Retrieves installed Azure DevOps extensions for a given organization. + +.DESCRIPTION +The Get-AzDoExtension function retrieves installed extensions from an Azure DevOps organization. +It supports filtering by extension name and extension ID. The function uses the Azure DevOps REST API +to fetch the extensions and returns detailed information about each extension. + +.PARAMETER CollectionUri +The URI of the Azure DevOps organization collection. This parameter is mandatory and accepts a string. + +.PARAMETER ExtensionName +The name(s) of the extension(s) to look for. This parameter accepts an array of strings and is optional. + +.PARAMETER ExtensionId +The ID(s) of the extension(s) to look for. This parameter accepts an array of strings and is optional. + +.EXAMPLE +PS> Get-AzDoExtension -CollectionUri "https://dev.azure.com/organization" -ExtensionName "extension1" + +Retrieves the extension named "extension1" from the specified Azure DevOps organization. + +.EXAMPLE +PS> Get-AzDoExtension -CollectionUri "https://dev.azure.com/organization" -ExtensionId "extension-id-123" + +Retrieves the extension with the ID "extension-id-123" from the specified Azure DevOps organization. + +.NOTES +This function uses the Azure DevOps REST API to fetch the installed extensions. +Ensure you have the necessary permissions to access the API. + +.LINK +https://learn.microsoft.com/en-us/rest/api/azure/devops/extensionmanagement/installed-extensions/get?view=azure-devops-rest-7.1&tabs=HTTP +#> + [CmdletBinding(SupportsShouldProcess)] + param ( + # Collection Uri of the organization + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] + [ValidateScript({ Validate-CollectionUri -CollectionUri $_ })] + [string] + $CollectionUri, + + # Name(s) of the extension(s) to look for + [Parameter(ValueFromPipelineByPropertyName, ValueFromPipeline)] + [string[]] + $ExtensionName, + + # Id(s) of the extension(s) to look for + [Parameter(ValueFromPipelineByPropertyName, ValueFromPipeline)] + [string[]] + $ExtensionId + ) + + begin { + Write-Verbose "Starting function: Get-AzDoExtension" + } + + process { + # For extensions a different base URI is used: https://learn.microsoft.com/en-us/rest/api/azure/devops/extensionmanagement/installed-extensions/get?view=azure-devops-rest-7.1&tabs=HTTP + $extensionCollectionUri = $CollectionUri -replace "//dev", "//extmgmt.dev" + + $params = @{ + uri = "$extensionCollectionUri/_apis/extensionmanagement/installedextensions" + version = "7.1-preview.1" + method = "GET" + } + + if ($PSCmdlet.ShouldProcess($CollectionUri, "Get Extension(s) in $CollectionUri")) { + $result = (Invoke-AzDoRestMethod @params).value | Where-Object { -not $ExtensionName -and -not $ExtensionId -or $_.extensionName -in $ExtensionName -or $_.extensionId -in $ExtensionId } + } else { + Write-Verbose "Calling Invoke-AzDoRestMethod with $($params| ConvertTo-Json -Depth 10)" + } + } + end { + if ($result) { + $result | ForEach-Object { + [PSCustomObject]@{ + CollectionURI = $CollectionUri + ExtensionCollectionURI = $extensionCollectionUri + ExtensionId = $_.extensionId + ExtensionName = $_.extensionName + ExtensionPublisherId = $_.PublisherId + ExtensionPublisherName = $_.PublisherName + ExtensionVersion = $_.version + ExtensionBaseUri = $_.baseUri + ExtensionFallbackBaseUri = $_.fallbackBaseUri + } + } + } + } +} diff --git a/AzureDevOpsPowerShell/Public/Api/ExtensionManagement/InstalledExtensions/New-AzDoExtension.ps1 b/AzureDevOpsPowerShell/Public/Api/ExtensionManagement/InstalledExtensions/New-AzDoExtension.ps1 new file mode 100644 index 00000000..be9e50e6 --- /dev/null +++ b/AzureDevOpsPowerShell/Public/Api/ExtensionManagement/InstalledExtensions/New-AzDoExtension.ps1 @@ -0,0 +1,91 @@ +function New-AzDoExtension { + <# +.SYNOPSIS +Installs an Azure DevOps extension in the specified organization. + +.DESCRIPTION +The New-AzDoExtension cmdlet installs an Azure DevOps extension in the specified organization. +It uses the Azure DevOps REST API to perform the installation. + +.PARAMETER CollectionUri +The URI of the Azure DevOps organization. + +.PARAMETER ExtensionId +The ID of the extension to install. + +.PARAMETER ExtensionPublisherId +The publisher ID of the extension to install. + +.PARAMETER ExtensionVersion +The version of the extension to install. If not specified, the latest version will be installed. + +.EXAMPLE +PS> New-AzDoExtension -CollectionUri "https://dev.azure.com/yourorganization" -ExtensionId "extensionId" -ExtensionPublisherId "publisherId" + +This command installs the specified extension in the given Azure DevOps organization. + +.EXAMPLE +PS> New-AzDoExtension -CollectionUri "https://dev.azure.com/yourorganization" -ExtensionId "extensionId" -ExtensionPublisherId "publisherId" -ExtensionVersion "1.0.0" + +This command installs version 1.0.0 of the specified extension in the given Azure DevOps organization. + +.NOTES +This cmdlet requires the Azure DevOps REST API and appropriate permissions to install extensions. + +.LINK +https://learn.microsoft.com/en-us/rest/api/azure/devops/extensionmanagement/installed-extensions/get?view=azure-devops-rest-7.1&tabs=HTTP +#> + [CmdletBinding(SupportsShouldProcess)] + param ( + # Collection Uri of the organization + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] + [ValidateScript({ Validate-CollectionUri -CollectionUri $_ })] + [string] + $CollectionUri, + + # Name(s) of the extension(s) to look for + [Parameter(Mandatory, ValueFromPipelineByPropertyName, ValueFromPipeline)] + [string] + $ExtensionId, + + # Id(s) of the extension(s) to look for + [Parameter(Mandatory, ValueFromPipelineByPropertyName, ValueFromPipeline)] + [string] + $ExtensionPublisherId, + + # Version of the extension to install + [Parameter(ValueFromPipelineByPropertyName, ValueFromPipeline)] + [string] + $ExtensionVersion + ) + + begin { + Write-Verbose "Starting function: New-AzDoExtensions" + } + + process { + # For extensions a different base URI is used: https://learn.microsoft.com/en-us/rest/api/azure/devops/extensionmanagement/installed-extensions/get?view=azure-devops-rest-7.1&tabs=HTTP + $extensionCollectionUri = $CollectionUri -replace "//dev", "//extmgmt.dev" + + if ($ExtensionVersion) { + $uri = "$extensionCollectionUri/_apis/extensionmanagement/installedextensionsbyname/$ExtensionPublisherId/$ExtensionId/$ExtensionVersion" + } else { + $uri = "$extensionCollectionUri/_apis/extensionmanagement/installedextensionsbyname/$ExtensionPublisherId/$ExtensionId" + } + + $params = @{ + uri = $uri + version = "7.1-preview.1" + method = "POST" + } + + if ($PSCmdlet.ShouldProcess($CollectionUri, "Install Extension $ExtensionName in $CollectionUri")) { + $result = (Invoke-AzDoRestMethod @params).value + } else { + Write-Verbose "Calling Invoke-AzDoRestMethod with $($params| ConvertTo-Json -Depth 10)" + } + } + end { + $result + } +} diff --git a/AzureDevOpsPowerShell/Public/Api/ExtensionManagement/InstalledExtensions/Remove-AzDoExtension.ps1 b/AzureDevOpsPowerShell/Public/Api/ExtensionManagement/InstalledExtensions/Remove-AzDoExtension.ps1 new file mode 100644 index 00000000..53ea3fb4 --- /dev/null +++ b/AzureDevOpsPowerShell/Public/Api/ExtensionManagement/InstalledExtensions/Remove-AzDoExtension.ps1 @@ -0,0 +1,70 @@ +function Remove-AzDoExtension { + <# + .SYNOPSIS + Removes an Azure DevOps extension from an organization. + + .DESCRIPTION + The `Remove-AzDoExtension` cmdlet removes an Azure DevOps extension from a specified organization. + It uses the Azure DevOps REST API to perform the deletion. + + .PARAMETER CollectionUri + Specifies the URI of the Azure DevOps organization. This parameter is mandatory and accepts a string. + + .PARAMETER ExtensionId + Specifies the ID of the extension to be removed. This parameter is mandatory and accepts a string. + + .PARAMETER ExtensionPublisherId + Specifies the publisher ID of the extension to be removed. This parameter is mandatory and accepts a string. + + .EXAMPLE + PS> Remove-AzDoExtension -CollectionUri "https://dev.azure.com/yourorganization" -ExtensionId "yourExtensionId" -ExtensionPublisherId "yourPublisherId" + + This command removes the specified extension from the specified Azure DevOps organization. + + .NOTES + For more information on the Azure DevOps REST API, see: + https://learn.microsoft.com/en-us/rest/api/azure/devops/extensionmanagement/installed-extensions/get?view=azure-devops-rest-7.1&tabs=HTTP +#> + [CmdletBinding(SupportsShouldProcess)] + param ( + # Collection Uri of the organization + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] + [ValidateScript({ Validate-CollectionUri -CollectionUri $_ })] + [string] + $CollectionUri, + + # Name(s) of the extension(s) to look for + [Parameter(Mandatory, ValueFromPipelineByPropertyName, ValueFromPipeline)] + [string] + $ExtensionId, + + # Id(s) of the extension(s) to look for + [Parameter(Mandatory, ValueFromPipelineByPropertyName, ValueFromPipeline)] + [string] + $ExtensionPublisherId + ) + + begin { + Write-Verbose "Starting function: Remove-AzDoExtension" + } + + process { + # For extensions a different base URI is used: https://learn.microsoft.com/en-us/rest/api/azure/devops/extensionmanagement/installed-extensions/get?view=azure-devops-rest-7.1&tabs=HTTP + $extensionCollectionUri = $CollectionUri -replace "//dev", "//extmgmt.dev" + + $params = @{ + uri = "$extensionCollectionUri/_apis/extensionmanagement/installedextensionsbyname/$ExtensionPublisherId/$ExtensionId" + version = "7.1-preview.1" + method = "DELETE" + } + + if ($PSCmdlet.ShouldProcess($CollectionUri, "Remove $ExtensionId from organization $CollectionUri")) { + $result = (Invoke-AzDoRestMethod @params).value + } else { + Write-Verbose "Calling Invoke-AzDoRestMethod with $($params| ConvertTo-Json -Depth 10)" + } + } + end { + $result + } +} diff --git a/tests/Api/ExtensionManagement/InstalledExtensions/Extensions.tests.ps1 b/tests/Api/ExtensionManagement/InstalledExtensions/Extensions.tests.ps1 new file mode 100644 index 00000000..615124d4 --- /dev/null +++ b/tests/Api/ExtensionManagement/InstalledExtensions/Extensions.tests.ps1 @@ -0,0 +1,128 @@ +BeforeDiscovery { + $ModuleName = 'AzureDevOpsPowerShell' + Get-Module $ModuleName | Remove-Module -Force -ErrorAction Ignore + $path = Join-Path -Path $PSScriptRoot -ChildPath "..\..\..\..\$ModuleName\$ModuleName.psm1" | Resolve-Path + Import-Module -Name $path -Verbose:$false -ErrorAction Stop +} + +InModuleScope $ModuleName { + BeforeAll { + $collectionUri = "https://dev.azure.com/AzureDevOpsPowerShell" + + $params = @{ + CollectionUri = $collectionUri + Confirm = $false + } + } + + Describe "Get-AzDoExtension" -Tag Local { + BeforeAll { + Mock Invoke-AzDoRestMethod { + @{ + value = @( + [PSCustomObject]@{ + CollectionURI = $CollectionUri + ExtensionCollectionURI = $extensionCollectionUri + ExtensionId = 'vss-testextension' + ExtensionName = 'extensionTest' + ExtensionPublisherId = 'rbnmk' + ExtensionPublisherName = 'RobinM' + ExtensionVersion = '0.0.1' + ExtensionBaseUri = 'baseUri' + ExtensionFallbackBaseUri = 'fallbackBaseUri' + } + [PSCustomObject]@{ + CollectionURI = $CollectionUri + ExtensionCollectionURI = $extensionCollectionUri + ExtensionId = 'vss-testextension2' + ExtensionName = 'extensionTest2' + ExtensionPublisherId = 'rbnmk2' + ExtensionPublisherName = 'RobinM2' + ExtensionVersion = '0.0.1' + ExtensionBaseUri = 'baseUri2' + ExtensionFallbackBaseUri = 'fallbackBaseUri2' + } + [PSCustomObject]@{ + CollectionURI = $CollectionUri + ExtensionCollectionURI = $extensionCollectionUri + ExtensionId = 'vss-testextension3' + ExtensionName = 'extensionTest3' + ExtensionPublisherId = 'rbnmk3' + ExtensionPublisherName = 'RobinM3' + ExtensionVersion = '0.0.1' + ExtensionBaseUri = 'baseUri3' + ExtensionFallbackBaseUri = 'fallbackBaseUri3' + } + ) + } + } + } + + It "It provides users with feedback via ShouldProcess when using WhatIf" { + Get-AzDoExtension @params -WhatIf -Verbose 4>&1 | Out-String | Should -BeLike "*Calling Invoke-AzDoRestMethod with {*" + } + + It "Outputs all extensions when no value to ExtensionName or ExtensionId was provided" { + (Get-AzDoExtension @params | Measure-Object).Count | Should -BeGreaterThan 1 + } + + It "Outputs extension which matches the name of ExtensionName" { + (Get-AzDoExtension @params -ExtensionName "extensionTest3").ExtensionName | Should -Be "extensionTest3" + } + + It "Outputs extension which matches the Id of ExtensionId" { + (Get-AzDoExtension @params -ExtensionId "vss-testextension2").ExtensionId | Should -Be "vss-testextension2" + } + + It "Outputs extensions which matches the Id of ExtensionId AND ExtensionName" { + (Get-AzDoExtension @params -ExtensionId "vss-testextension2" -ExtensionName "extensionTest3" | Measure-Object).count | Should -BeExactly 2 + } + + } + + Describe "New-AzDoExtension" -Tag Local { + BeforeAll { + + $params.Add("ExtensionId", "vss-testextension") + $params.Add("ExtensionPublisherId", "rbnmk") + + Mock Invoke-AzDoRestMethod { $null } + + } + + It "It provides users with feedback via ShouldProcess when using WhatIf" { + New-AzDoExtension @params -WhatIf -Verbose 4>&1 | Out-String | Should -BeLike "*Calling Invoke-AzDoRestMethod with {*" + } + + It "Installs AzDo extension when ExtensionId and ExtensionPublisherId are provided and returns null or empty" { + (New-AzDoExtension @params) | Should -BeNullOrEmpty + } + + It "Installs AzDo extension when ExtensionId, ExtensionPublisherId and ExtensionVersion are provided and returns null or empty" { + $params.Add("ExtensionVersion", "0.0.1") + (New-AzDoExtension @params) | Should -BeNullOrEmpty + } + + It "Throws exception when ExtensionId/PublisherName is already installed" { + Mock Invoke-AzDoRestMethod { throw "Extension already installed" } + { New-AzDoExtension @params } | Should -Throw -Because "Extension already installed" + } + + + } + + Describe "Remove-AzDoExtension" -Tag Local { + BeforeAll { + Mock Invoke-AzDoRestMethod { $null } + $params.Remove("ExtensionVersion") + } + + It "It provides users with feedback via ShouldProcess when using WhatIf" { + Remove-AzDoExtension @params -ExtensionId "vss-testextension" -ExtensionPublisherid "pesterpublisher" -WhatIf -Verbose 4>&1 | Out-String | Should -BeLike "*Calling Invoke-AzDoRestMethod with {*" + } + + It "Removes AzDo extension when ExtensionId and ExtensionPublisherId are provided and returns null or empty" { + (New-AzDoExtension @params -ExtensionId "vss-testextension" -ExtensionPublisherid "pesterpublisher") | Should -BeNullOrEmpty + } + } +}