AtlassianPS.Configuration.psm1
|
#region Dependencies Import-Module Metadata -MinimumVersion 1.5.7 -MaximumVersion '1.*' -Global -ErrorAction Stop $incompatibleConfiguration = @(Get-Module Configuration -All | Where-Object { $_.Version -lt [Version]'1.6.0' -or $_.Version.Major -ne 1 }) if ($incompatibleConfiguration.Count) { throw 'An incompatible Configuration dependency is loaded. Start a fresh process with Configuration >=1.6.0 within major 1.' } if (('AtlassianPS.ServerData' -as [Type]) -and -not ('AtlassianPS.Connection' -as [Type])) { throw 'An older Configuration type assembly is loaded. Start a fresh PowerShell session before importing this version.' } # Load the Module's namespace from C# if (-not("AtlassianPS.ServerData" -as [Type])) { $assemblyReferences = @( 'Microsoft.CSharp' 'Microsoft.PowerShell.Commands.Utility' 'System.Management.Automation' 'System.Runtime.Extensions' 'System.Security.Cryptography.X509Certificates' ) if ($PSVersionTable.PSEdition -eq 'Core') { $assemblyReferences += 'System.Security.Cryptography' } Add-Type -Path (Join-Path $PSScriptRoot AtlassianPS.Configuration.Types.cs) -ReferencedAssemblies $assemblyReferences } if ($PSVersionTable.PSVersion.Major -lt 5) { Add-Type -Path (Join-Path $PSScriptRoot AtlassianPS.Configuration.Attributes.cs) -ReferencedAssemblies Microsoft.CSharp, Microsoft.PowerShell.Commands.Utility, System.Management.Automation, System.Runtime.Extensions, System.Security.Cryptography.X509Certificates } #endregion Dependencies if ($env:ATLASSIANPS_CONFIGURATION_ROOT) { $configurationRoot = $env:ATLASSIANPS_CONFIGURATION_ROOT if (-not [IO.Path]::IsPathRooted($configurationRoot)) { throw 'Configuration root must be an absolute disposable path.' } $null = New-Item -Path $configurationRoot -ItemType Directory -Force if ((Get-Module Configuration).PrivateData.AtlassianPSTestRoot -ne $configurationRoot) { Import-Module Configuration -MinimumVersion 1.6.0 -MaximumVersion '1.*' -Global -Force -ArgumentList @{}, $configurationRoot, $configurationRoot, $configurationRoot -ErrorAction Stop (Get-Module Configuration).PrivateData.AtlassianPSTestRoot = $configurationRoot } } #region ModuleConfig $script:RegistryGeneration = [Guid]::NewGuid() $script:NextConnectionId = 0 $script:NextGeneratedName = 0 $script:Connections = @{} $script:DefaultConnections = @{} $script:RequestContexts = [Collections.Generic.List[WeakReference]]::new() $ExecutionContext.SessionState.Module.OnRemove = { foreach ($connection in @($script:Connections.Values)) { $connection.Dispose() } foreach ($reference in $script:RequestContexts) { $context = $reference.Target if ($context) { $context.Dispose() } } $script:RequestContexts.Clear() $script:Connections.Clear() $script:DefaultConnections.Clear() } # Add our own Converters for serialization Add-MetadataConverter @{ [AtlassianPS.MessageStyle] = { "AtlassianPSMessageStyle -Indent {0} -TimeStamp {1} -BreadCrumbs {2} -FunctionName {3}" -f (ConvertTo-Metadata $_.Indent), (ConvertTo-Metadata $_.TimeStamp), (ConvertTo-Metadata $_.BreadCrumbs), (ConvertTo-Metadata $_.FunctionName) } AtlassianPSMessageStyle = { param($Indent, $TimeStamp, $BreadCrumbs, $FunctionName) [AtlassianPS.MessageStyle]$PSBoundParameters } [AtlassianPS.ServerData] = { "AtlassianPSServerData -Id {0} -Name {1} -Uri {2} -Type {3} -Headers {4}" -f $_.Id, (ConvertTo-Metadata $_.Name), (ConvertTo-Metadata $_.Uri.AbsoluteUri), (ConvertTo-Metadata $_.Type.ToString()), (ConvertTo-Metadata $_.Headers) } AtlassianPSServerData = { param($Id, $Name, $Uri, $Type, $Headers) if ([string]::IsNullOrEmpty($Headers)) { $Headers = $null } [AtlassianPS.ServerData]@{ Id = $Id; Name = $Name; Uri = $Uri; Type = $Type; Headers = $Headers } } } #region LoadFunctions #region Add-ServerConfiguration function Add-ServerConfiguration { # .ExternalHelp ..\AtlassianPS.Configuration-help.xml [CmdletBinding( ConfirmImpact = 'Low', SupportsShouldProcess = $true )] [OutputType( [void] )] param( [Parameter( Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName )] [Alias('Url', 'Address')] [ValidateScript( { $_.IsAbsoluteUri } )] [Uri] $Uri, [Parameter( ValueFromPipelineByPropertyName )] [ValidateNotNullOrEmpty()] [Alias('ServerName', 'Alias')] [String] $Name, [Parameter( Mandatory, ValueFromPipelineByPropertyName )] [AtlassianPS.ServerType] $Type, [Parameter( ValueFromPipelineByPropertyName )] [Microsoft.PowerShell.Commands.WebRequestSession] $Session, [Parameter( ValueFromPipelineByPropertyName )] [Hashtable] $Headers ) begin { Write-Verbose "Function started" $serverList = [System.Collections.Generic.List[AtlassianPS.ServerData]]::new() foreach ($server in @(Get-ServerConfiguration)) { $serverList.Add($server) } $configurationChanged = $false } process { Write-DebugMessage "ParameterSetName: $($PsCmdlet.ParameterSetName)" if ($PSBoundParameters.ContainsKey('Session')) { Write-Warning 'Profile Session is deprecated and is never used by Connect. Supply authentication to Connect instead.' } $entryName = if ($PSBoundParameters.ContainsKey('Name')) { $Name } else { $Uri.Authority } $entryHeaders = if ($PSBoundParameters.ContainsKey('Headers')) { $Headers } else { @{} } if ($serverList | Where-Object Name -eq $entryName) { $writeErrorSplat = @{ ExceptionType = "System.ApplicationException" Message = "An entry with name [$entryName] already exists" ErrorId = "AtlassianPS.ServerData.EntryExists" Category = "InvalidData" TargetObject = $entryName } WriteError @writeErrorSplat } elseif ($PSCmdlet.ShouldProcess($entryName, "Add server configuration")) { if (-not ($index = ($serverList.Id | Measure-Object -Maximum).Maximum)) { $index = 0 } $index++ $config = [AtlassianPS.ServerData]@{ Id = $index Name = $entryName Uri = ([Uri]($Uri.AbsoluteUri -replace "\/$", "")) Type = $Type # IsCloudServer = (Test-ServerIsCloud -Type $Type -Uri $Uri -Headers $Headers -ErrorAction Stop -verbose) Session = $Session Headers = $entryHeaders } Write-Verbose "Adding server #$($index): [$($config.Name)]" Write-DebugMessage "Adding server `$config: $($config.Name) @ index $index" -BreakPoint $serverList.Add($config) $configurationChanged = $true } } end { if ($configurationChanged) { Write-DebugMessage "Persisting ServerList" $script:Configuration["ServerList"] = $serverList Save-Configuration } Write-Verbose "Function ended" } } #endregion Add-ServerConfiguration #region Disconnect-Connection function Disconnect-Connection { # .ExternalHelp ..\AtlassianPS.Configuration-help.xml [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'Object')] param( [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'Object')][AtlassianPS.Connection]$Connection, [Parameter(Mandatory, ParameterSetName = 'Name')][String]$Name, [Parameter(Mandatory, ParameterSetName = 'Name')][String]$Product, [Parameter(Mandatory, ParameterSetName = 'Id')][Int]$Id ) process { if ($PSCmdlet.ParameterSetName -ne 'Object') { $filter = @{} foreach ($key in @('Product', 'Name', 'Id')) { if ($PSBoundParameters.ContainsKey($key)) { $filter[$key] = $PSBoundParameters[$key] } } $Connection = Get-Connection @filter } if (-not $Connection) { throw 'Connection not found.' } Assert-ConnectionActive -Connection $Connection -RegisteredOnly if ($PSCmdlet.ShouldProcess($Connection.ToString(), 'Disconnect')) { $script:Connections.Remove($Connection.InstanceId) if ($script:DefaultConnections[$Connection.Product] -eq $Connection.InstanceId) { $script:DefaultConnections.Remove($Connection.Product) } $Connection.Dispose() } } } #endregion Disconnect-Connection #region Export-ServerConfiguration function Export-ServerConfiguration { # .ExternalHelp ..\AtlassianPS.Configuration-help.xml [CmdletBinding(SupportsShouldProcess)] param([Parameter(Mandatory)][String]$Path, [String[]]$Name) $profiles = @( foreach ($server in @(Get-ServerConfiguration)) { if (-not $Name -or $server.Name -in $Name) { ConvertTo-PortableServer -Server $server } } ) $document = @{ SchemaVersion = 1; ServerList = $profiles } | ConvertTo-Json -Depth 4 if ($PSCmdlet.ShouldProcess($Path, 'Export non-secret server profiles')) { Set-Content -LiteralPath $Path -Value $document -Encoding UTF8 } } #endregion Export-ServerConfiguration #region Get-Configuration function Get-Configuration { # .ExternalHelp ..\AtlassianPS.Configuration-help.xml [CmdletBinding( DefaultParameterSetName = 'asObject' )] [OutputType( [PSCustomObject], ParameterSetName = 'asObject' )] [OutputType( [PSObject], ParameterSetName = 'asValue' )] [OutputType( [Hashtable], ParameterSetName = 'asHashTable' )] param( [Parameter( ValueFromPipeline, ValueFromPipelineByPropertyName )] [ValidateNotNullOrEmpty()] [SupportsWildcards()] [ArgumentCompleter( { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $command = "Get-Configuration" $module = (Get-Command -Name $commandName).Module $commandName = $module.ExportedCommands.Keys | Where-Object { $_ -like ($command -replace "-", "-$($module.Prefix)") } & $commandName | Where-Object { $_.Name -like "$wordToComplete*" } | ForEach-Object { [System.Management.Automation.CompletionResult]::new( $_.Name, $_.Name, [System.Management.Automation.CompletionResultType]::ParameterValue, $_.Name ) } } )] [String[]] $Name = '*', [Parameter( Mandatory, ParameterSetName = 'asValue' )] [Switch] $ValueOnly, [Parameter( Mandatory, ParameterSetName = 'asHashtable' )] [Switch] $AsHashtable ) begin { Write-Verbose "Function started" } process { Write-DebugMessage "ParameterSetName: $($PsCmdlet.ParameterSetName)" Write-DebugMessage "PSBoundParameters: $($PSBoundParameters | Out-String)" $data = @{} foreach ($_name in $Name) { Write-Verbose "Filtering for [name = $_name]" foreach ($key in ($script:Configuration.Keys | Where-Object { $_ -like $_name })) { Write-DebugMessage "Found [key = $key]" $data[$key] = $script:Configuration[$key] } } if ($AsHashtable) { $data } elseif ($ValueOnly) { $data.Values } else { foreach ($key in $data.Keys) { New-Object -TypeName PSCustomObject -Property @{ Name = $key; Value = $data[$key] } } } } end { Write-Verbose "Function ended" } } #endregion Get-Configuration #region Get-Connection function Get-Connection { # .ExternalHelp ..\AtlassianPS.Configuration-help.xml [CmdletBinding()] [OutputType([AtlassianPS.Connection])] param([String]$Product, [String]$Name, [Int]$Id, [Switch]$Default) $script:Connections.Values | Where-Object { (-not $Product -or $_.Product -eq $Product) -and (-not $Name -or $_.Name -eq $Name) -and (-not $PSBoundParameters.ContainsKey('Id') -or $_.Id -eq $Id) -and (-not $Default -or $script:DefaultConnections[$_.Product] -eq $_.InstanceId) } | Sort-Object Id } #endregion Get-Connection #region Get-ServerConfiguration function Get-ServerConfiguration { # .ExternalHelp ..\AtlassianPS.Configuration-help.xml [CmdletBinding( DefaultParameterSetName = '_All' )] [OutputType( [AtlassianPS.ServerData] )] param( [Parameter( Position = 0, Mandatory, ParameterSetName = 'ServerDataByUri' )] [ArgumentCompleter( { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $command = "Get-ServerConfiguration" $module = (Get-Command -Name $commandName).Module $commandName = $module.ExportedCommands.Keys | Where-Object { $_ -like ($command -replace "-", "-$($module.Prefix)") } & $commandName | Where-Object { $_.Name -like "$wordToComplete*" } | ForEach-Object { [System.Management.Automation.CompletionResult]::new( $_.Name, $_.Name, [System.Management.Automation.CompletionResultType]::ParameterValue, $_.Name ) } } )] [Alias('Url', 'Address')] [Uri] $Uri, [Parameter( Position = 0, Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'ServerDataByName' )] [ArgumentCompleter( { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $command = "Get-ServerConfiguration" $module = (Get-Command -Name $commandName).Module $commandName = $module.ExportedCommands.Keys | Where-Object { $_ -like ($command -replace "-", "-$($module.Prefix)") } & $commandName | Where-Object { $_.Uri -like "$wordToComplete*" } | ForEach-Object { [System.Management.Automation.CompletionResult]::new( $_.Name, $_.Name, [System.Management.Automation.CompletionResultType]::ParameterValue, $_.Name ) } } )] [Alias('ServerName', 'Alias')] [String[]] $Name ) begin { Write-Verbose "Function started" $serverList = Get-Configuration -Name ServerList -ValueOnly } process { Write-DebugMessage "ParameterSetName: $($PsCmdlet.ParameterSetName)" Write-DebugMessage "PSBoundParameters: $($PSBoundParameters | Out-String)" switch ($PsCmdlet.ParameterSetName) { 'ServerDataByName' { Write-Verbose "Filtering ServerList for [Name = $Name]" $serverList | Where-Object { $_.Name -in $Name } } 'ServerDataByUri' { Write-Verbose "Filtering ServerList for [URI = $Uri]" $serverList | Where-Object { $_.Uri -like $Uri } } '_All' { $serverList } } } end { Write-Verbose "Function ended" } } #endregion Get-ServerConfiguration #region Import-ServerConfiguration function Import-ServerConfiguration { # .ExternalHelp ..\AtlassianPS.Configuration-help.xml [CmdletBinding(SupportsShouldProcess)] param([Parameter(Mandatory)][String]$Path, [Switch]$Replace) $document = Get-Content -LiteralPath $Path -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop if ($document.SchemaVersion -ne 1 -or -not $document.PSObject.Properties['ServerList'] -or @($document.PSObject.Properties.Name | Where-Object { $_ -notin @('SchemaVersion', 'ServerList') }).Count) { throw 'Unsupported profile document. Expected SchemaVersion 1 and ServerList.' } $newList = [System.Collections.Generic.List[AtlassianPS.ServerData]]::new() foreach ($server in @(Get-ServerConfiguration)) { $newList.Add($server) } $names = @{} foreach ($entry in @($document.ServerList)) { if (-not $entry -or @($entry.PSObject.Properties.Name | Where-Object { $_ -notin @('Id', 'Name', 'Uri', 'Type') }).Count -or [string]::IsNullOrWhiteSpace($entry.Name) -or $names.ContainsKey($entry.Name)) { throw 'Invalid profile fields or duplicate profile names.' } $names[$entry.Name] = $true $server = [AtlassianPS.ServerData]@{ Id = 1; Name = $entry.Name; Uri = $entry.Uri; Type = $entry.Type } $null = ConvertTo-PortableServer -Server $server $existing = @($newList | Where-Object Name -eq $server.Name) if ($existing.Count -and -not $Replace) { throw 'A profile with that name exists. Use Replace to replace it.' } foreach ($item in $existing) { $null = $newList.Remove($item) } $server.Id = 1 + [int](($newList.Id | Measure-Object -Maximum).Maximum) $newList.Add($server) } if ($PSCmdlet.ShouldProcess($Path, 'Import validated non-secret server profiles')) { $previous = $script:Configuration['ServerList'] try { $script:Configuration['ServerList'] = $newList Save-Configuration -ErrorAction Stop } catch { $script:Configuration['ServerList'] = $previous throw } } } #endregion Import-ServerConfiguration #region New-ConnectionContext function New-ConnectionContext { # .ExternalHelp ..\AtlassianPS.Configuration-help.xml [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Creates an unregistered in-memory candidate; it does not perform HTTP or persist state.')] [CmdletBinding()] [OutputType([AtlassianPS.Connection])] param( [ValidateNotNullOrEmpty()][String]$Name, [Parameter(Mandatory)][ValidateNotNullOrEmpty()][String]$Product, [Parameter(Mandatory)][Uri]$BaseUri, [Uri]$SiteUri, [String]$CloudId, [PSCredential]$Credential, [Alias('Email')][String]$EmailAddress, [Object]$ApiToken, [Alias('PAT', 'BearerToken')][Object]$PersonalAccessToken, [Switch]$Anonymous ) if (-not $PSBoundParameters.ContainsKey('Name')) { do { $script:NextGeneratedName++ $Name = '{0}-{1}' -f $Product, $script:NextGeneratedName } while (Get-Connection -Product $Product -Name $Name) } if (-not $BaseUri.IsAbsoluteUri -or $BaseUri.Scheme -notin @('http', 'https') -or $BaseUri.UserInfo -or $BaseUri.Query -or $BaseUri.Fragment) { throw 'BaseUri must be an absolute HTTP(S) product endpoint without user information, query or fragment.' } $modes = @('Credential', 'ApiToken', 'PersonalAccessToken', 'Anonymous' | Where-Object { $PSBoundParameters.ContainsKey($_) }) if ($modes.Count -ne 1) { throw 'Supply exactly one authentication mode.' } if ($PSBoundParameters.ContainsKey('EmailAddress') -and $modes[0] -ne 'ApiToken') { throw 'EmailAddress requires ApiToken.' } if ($modes[0] -eq 'Anonymous' -and -not $Anonymous) { throw 'Anonymous must be enabled explicitly.' } if ($modes[0] -eq 'Credential' -and (-not $Credential -or $Credential -eq [PSCredential]::Empty -or [string]::IsNullOrWhiteSpace($Credential.UserName) -or $Credential.Password.Length -eq 0)) { throw 'Credential must contain a username and password.' } if ($modes[0] -eq 'ApiToken' -and [string]::IsNullOrWhiteSpace($EmailAddress)) { throw 'ApiToken requires EmailAddress.' } $token = $null try { if ($modes[0] -eq 'ApiToken') { $token = ConvertTo-ConnectionToken -Token $ApiToken } if ($modes[0] -eq 'PersonalAccessToken') { $token = ConvertTo-ConnectionToken -Token $PersonalAccessToken } $context = [AtlassianPS.Connection]::new( $Name, $Product, [Uri]$BaseUri.AbsoluteUri.TrimEnd('/'), $SiteUri, $CloudId, $modes[0], $EmailAddress, $Credential, $token, $script:RegistryGeneration ) for ($index = $script:RequestContexts.Count - 1; $index -ge 0; $index--) { $existing = $script:RequestContexts[$index].Target if (-not $existing -or $existing.State -eq 'Disconnected') { $script:RequestContexts.RemoveAt($index) } } $script:RequestContexts.Add([WeakReference]::new($context)) $context } finally { if ($token) { $token.Dispose() } } } #endregion New-ConnectionContext #region Register-Connection function Register-Connection { # .ExternalHelp ..\AtlassianPS.Configuration-help.xml [CmdletBinding()] [OutputType([AtlassianPS.Connection])] param( [Parameter(Mandatory)][AtlassianPS.Connection]$Connection, [Switch]$Default ) if ($Connection.RegistryGeneration -ne $script:RegistryGeneration -or $Connection.State -ne 'Candidate') { throw 'Only a candidate from this runtime can be registered.' } if (-not $Connection.DeploymentType) { throw 'Validate the product endpoint and identity before registration.' } if (Get-Connection -Product $Connection.Product -Name $Connection.Name) { throw 'A connection with that product and name already exists.' } $script:NextConnectionId++ $Connection.Register($script:NextConnectionId) $script:Connections[$Connection.InstanceId] = $Connection if ($Default -or -not $script:DefaultConnections.ContainsKey($Connection.Product)) { $script:DefaultConnections[$Connection.Product] = $Connection.InstanceId } $Connection } #endregion Register-Connection #region Remove-Configuration function Remove-Configuration { # .ExternalHelp ..\AtlassianPS.Configuration-help.xml [CmdletBinding( ConfirmImpact = 'Low', SupportsShouldProcess = $true )] [OutputType( [void] )] param( [Parameter( Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName )] [ValidateNotNullOrEmpty()] [ArgumentCompleter( { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $command = "Get-Configuration" $module = (Get-Command -Name $commandName).Module $commandName = $module.ExportedCommands.Keys | Where-Object { $_ -like ($command -replace "-", "-$($module.Prefix)") } & $commandName | Where-Object { $_.Name -like "$wordToComplete*" } | ForEach-Object { [System.Management.Automation.CompletionResult]::new( $_.Name, $_.Name, [System.Management.Automation.CompletionResultType]::ParameterValue, $_.Name ) } } )] [String[]] $Name ) begin { Write-Verbose "Function started" $reservedNames = @('ServerList') $configurationChanged = $false } process { Write-DebugMessage "ParameterSetName: $($PsCmdlet.ParameterSetName)" Write-DebugMessage "PSBoundParameters: $($PSBoundParameters | Out-String)" foreach ($_name in $Name) { if ($_name -in $reservedNames) { $writeErrorSplat = @{ ExceptionType = "System.ApplicationException" Message = "Configuration key [$_name] is reserved and cannot be removed with Remove-Configuration" ErrorId = "AtlassianPS.Configuration.ReservedKey" Category = "InvalidArgument" TargetObject = $_name } WriteError @writeErrorSplat continue } Write-Verbose "Filtering for [name = $_name]" if ($PSCmdlet.ShouldProcess($_name, "Remove configuration key")) { $configurationChanged = $script:Configuration.Remove($_name) -or $configurationChanged } } } end { if ($configurationChanged) { Save-Configuration } Write-Verbose "Function ended" } } #endregion Remove-Configuration #region Remove-ServerConfiguration function Remove-ServerConfiguration { # .ExternalHelp ..\AtlassianPS.Configuration-help.xml [CmdletBinding( ConfirmImpact = 'Low', SupportsShouldProcess = $true )] [OutputType( [void] )] param( [Parameter( Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName )] [ArgumentCompleter( { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $commandName = (Get-Command -Module "AtlassianPS.Configuration" -Name "Get-*ServerConfiguration").Name & $commandName | Where-Object { $_.Name -like "$wordToComplete*" } | ForEach-Object { [System.Management.Automation.CompletionResult]::new( $_.Name, $_.Name, [System.Management.Automation.CompletionResultType]::ParameterValue, $_.Name ) } } )] [Alias('ServerName', 'Alias')] [ValidateNotNullOrEmpty()] [String[]] $Name ) begin { Write-Verbose "Function started" $serverList = [System.Collections.Generic.List[AtlassianPS.ServerData]]::new() foreach ($server in @(Get-ServerConfiguration)) { $serverList.Add($server) } $configurationChanged = $false } process { Write-DebugMessage "ParameterSetName: $($PsCmdlet.ParameterSetName)" Write-DebugMessage "PSBoundParameters: $($PSBoundParameters | Out-String)" foreach ($serverToRemove in $Name) { if ($serverToRemove -notin $serverList.Name) { $writeErrorSplat = @{ ExceptionType = "System.ApplicationException" ErrorId = "AtlassianPS.ServerData.ServerNotFound" Category = "ObjectNotFound" Message = "No server '$serverToRemove' could be found." TargetObject = $serverToRemove Cmdlet = $PSCmdlet } WriteError @writeErrorSplat } } foreach ($serverToRemove in $Name) { if ($serverToRemove -in $serverList.Name -and $PSCmdlet.ShouldProcess($serverToRemove, "Remove server configuration")) { $remainingServers = [System.Collections.Generic.List[AtlassianPS.ServerData]]::new() foreach ($server in $serverList) { if ($server.Name -ne $serverToRemove) { $remainingServers.Add($server) } } $serverList = $remainingServers $configurationChanged = $true } } } end { if ($configurationChanged) { Write-DebugMessage "Persisting ServerList" $script:Configuration.Remove("ServerList") $script:Configuration.Add("ServerList", $serverList) Save-Configuration } Write-Verbose "Function ended" } } #endregion Remove-ServerConfiguration #region Resolve-RequestContext function Resolve-RequestContext { # .ExternalHelp ..\AtlassianPS.Configuration-help.xml [CmdletBinding()] [OutputType([AtlassianPS.Connection])] param( [Parameter(Mandatory)][String]$Product, [Parameter(DontShow)][AtlassianPS.Connection]$RequestContext, [AtlassianPS.Connection]$Connection, [String]$ConnectionName, [Uri]$BaseUri, [PSCredential]$Credential, [Alias('Email')][String]$EmailAddress, [Object]$ApiToken, [Alias('PAT', 'BearerToken')][Object]$PersonalAccessToken, [Switch]$Anonymous ) if ($PSBoundParameters.ContainsKey('RequestContext')) { $conflicts = @('Connection', 'ConnectionName', 'BaseUri', 'Credential', 'EmailAddress', 'ApiToken', 'PersonalAccessToken', 'Anonymous') $hasConflict = @($conflicts | Where-Object { $PSBoundParameters.ContainsKey($_) }).Count -gt 0 if (-not $RequestContext -or $RequestContext.Product -ne $Product -or $RequestContext.RegistryGeneration -ne $script:RegistryGeneration -or $RequestContext.State -eq 'Disconnected' -or $hasConflict) { throw 'Invalid internal request context.' } if ($RequestContext.State -eq 'Active' -or $RequestContext.IsOverride) { Assert-ConnectionActive -Connection $RequestContext -Product $Product } return $RequestContext } $selectors = @('Connection', 'ConnectionName', 'BaseUri' | Where-Object { $PSBoundParameters.ContainsKey($_) }) if ($selectors.Count -gt 1) { throw 'Connection, ConnectionName and BaseUri are mutually exclusive.' } if ($PSBoundParameters.ContainsKey('Connection') -and -not $Connection) { throw 'Connection cannot be null.' } if ($PSBoundParameters.ContainsKey('ConnectionName') -and [string]::IsNullOrWhiteSpace($ConnectionName)) { throw 'ConnectionName cannot be empty.' } $auth = @{} foreach ($key in @('Credential', 'EmailAddress', 'ApiToken', 'PersonalAccessToken', 'Anonymous')) { if ($PSBoundParameters.ContainsKey($key)) { $auth[$key] = $PSBoundParameters[$key] } } if ($PSBoundParameters.ContainsKey('BaseUri') -and -not $BaseUri) { throw 'BaseUri cannot be null.' } if ($BaseUri) { $context = New-ConnectionContext -Name 'Standalone' -Product $Product -BaseUri $BaseUri @auth $context.MakeStandalone([Guid]::Empty) return $context } if ($ConnectionName) { $Connection = Get-Connection -Product $Product -Name $ConnectionName } elseif (-not $PSBoundParameters.ContainsKey('Connection')) { $Connection = Get-Connection -Product $Product -Default } if (-not $Connection) { throw "No $Product connection selected. Connect or select a connection first." } Assert-ConnectionActive -Connection $Connection -Product $Product if ($auth.Count -eq 0) { return $Connection } $context = New-ConnectionContext -Name $Connection.Name -Product $Product -BaseUri $Connection.BaseUri -SiteUri $Connection.SiteUri -CloudId $Connection.CloudId @auth $context.SetValidationResult($Connection.DeploymentType, $null) $context.MakeStandalone($Connection.InstanceId) $context } #endregion Resolve-RequestContext #region Select-Connection function Select-Connection { # .ExternalHelp ..\AtlassianPS.Configuration-help.xml [CmdletBinding(DefaultParameterSetName = 'Object')] [OutputType([AtlassianPS.Connection])] param( [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'Object')][AtlassianPS.Connection]$Connection, [Parameter(Mandatory, ParameterSetName = 'Name')][String]$Name, [Parameter(Mandatory, ParameterSetName = 'Name')][String]$Product, [Parameter(Mandatory, ParameterSetName = 'Id')][Int]$Id ) process { if ($PSCmdlet.ParameterSetName -ne 'Object') { $filter = @{} foreach ($key in @('Product', 'Name', 'Id')) { if ($PSBoundParameters.ContainsKey($key)) { $filter[$key] = $PSBoundParameters[$key] } } $Connection = Get-Connection @filter } if (-not $Connection) { throw 'Connection not found.' } Assert-ConnectionActive -Connection $Connection -RegisteredOnly $script:DefaultConnections[$Connection.Product] = $Connection.InstanceId $Connection } } #endregion Select-Connection #region Set-Configuration function Set-Configuration { # .ExternalHelp ..\AtlassianPS.Configuration-help.xml [CmdletBinding( ConfirmImpact = 'Low', SupportsShouldProcess = $true )] [OutputType( [PSCustomObject] )] param( [Parameter( Mandatory, ValueFromPipelineByPropertyName )] [ValidateNotNullOrEmpty()] [ArgumentCompleter( { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $command = "Get-Configuration" $module = (Get-Command -Name $commandName).Module $commandName = $module.ExportedCommands.Keys | Where-Object { $_ -like ($command -replace "-", "-$($module.Prefix)") } & $commandName | Where-Object { $_.Name -like "$wordToComplete*" } | ForEach-Object { [System.Management.Automation.CompletionResult]::new( $_.Name, $_.Name, [System.Management.Automation.CompletionResultType]::ParameterValue, $_.Name ) } } )] [String] $Name, [Parameter( ValueFromPipelineByPropertyName )] [AllowNull()] [AllowEmptyCollection()] [AllowEmptyString()] [Object] $Value, [Switch] $Append, [Switch] $Passthru ) begin { Write-Verbose "Function started" $reservedNames = @('ServerList') $configurationChanged = $false } process { Write-DebugMessage "ParameterSetName: $($PsCmdlet.ParameterSetName)" Write-DebugMessage "PSBoundParameters: $($PSBoundParameters | Out-String)" if ($Name -in $reservedNames) { $writeErrorSplat = @{ ExceptionType = "System.ApplicationException" Message = "Configuration key [$Name] is reserved and cannot be changed with Set-Configuration" ErrorId = "AtlassianPS.Configuration.ReservedKey" Category = "InvalidArgument" TargetObject = $Name } WriteError @writeErrorSplat return } if ($Append) { Write-Verbose "Appending to existing value" $oldValue = (Get-Configuration -Name $Name -ValueOnly) if ($null -eq $oldValue) { $newValue = @($Value) } else { try { $newValue = @(@($oldValue) + @($Value)) -as ($oldValue.GetType()) if (-not $newValue) { throw "failed to cast to $($oldValue.GetType().Name)" } } catch { Write-DebugMessage $_ $newValue = @(@($oldValue) + @($Value)) } } $Value = $newValue } if ($Value) { $dataType = $Value.GetType().Name } else { $dataType = "null" } Write-Verbose "Storing value [$dataType] to [name = $Name]" if ($PSCmdlet.ShouldProcess($Name, "Set configuration value")) { $script:Configuration.Remove($Name) $script:Configuration.Add($Name, $Value) $configurationChanged = $true if ($Passthru) { Get-Configuration -Name $Name } } } end { if ($configurationChanged) { Save-Configuration } Write-Verbose "Function ended" } } #endregion Set-Configuration #region Set-ServerConfiguration function Set-ServerConfiguration { # .ExternalHelp ..\AtlassianPS.Configuration-help.xml [CmdletBinding( ConfirmImpact = 'Low', SupportsShouldProcess = $true )] [OutputType( [void] )] param( [Parameter( Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName )] [ValidateRange(1, [UInt32]::MaxValue)] [UInt32] $Id, [Parameter()] [ValidateNotNullOrEmpty()] [Alias('Url', 'Address')] [ValidateScript( { $_.IsAbsoluteUri } )] [Uri] $Uri, [Parameter()] [ValidateNotNullOrEmpty()] [Alias('ServerName', 'Alias')] [String] $Name = $Uri.Authority, [Parameter()] [ValidateNotNullOrEmpty()] [AtlassianPS.ServerType] $Type, [Parameter()] [Microsoft.PowerShell.Commands.WebRequestSession] $Session, [Parameter()] [Hashtable] $Headers ) begin { Write-Verbose "Function started" $parametersToIgnore = @( 'Id' 'Verbose' 'Debug' 'ErrorAction' 'WarningAction' 'InformationAction' 'ErrorVariable' 'WarningVariable' 'InformationVariable' 'OutVariable' 'OutBuffer' 'PipelineVariable' 'WhatIf' 'Confirm' ) $configurationChanged = $false } process { Write-DebugMessage "ParameterSetName: $($PsCmdlet.ParameterSetName)" if ($PSBoundParameters.ContainsKey('Session')) { Write-Warning 'Profile Session is deprecated and is never used by Connect. Supply authentication to Connect instead.' } $serverEntry = Get-ServerConfiguration | Where-Object { $_.Id -eq $Id } if ($serverEntry) { if ($PSBoundParameters.ContainsKey('Name') -and (Get-ServerConfiguration | Where-Object { $_.Id -ne $Id -and $_.Name -eq $Name })) { $writeErrorSplat = @{ ExceptionType = "System.ApplicationException" Message = "An entry with name [$Name] already exists" ErrorId = "AtlassianPS.ServerData.EntryExists" Category = "InvalidData" TargetObject = $Name } WriteError @writeErrorSplat return } if ($PSCmdlet.ShouldProcess("#$Id ($($serverEntry.Name))", "Update server configuration")) { foreach ($property in ($PSBoundParameters.Keys | Where-Object { $_ -notin $parametersToIgnore } )) { Write-Verbose "Changing [$property] of entry #$Id" $serverEntry.$property = Get-Variable $property -ValueOnly $configurationChanged = $true } } } else { $writeErrorSplat = @{ ExceptionType = "System.ApplicationException" Message = "No entry could be found at index $Id" ErrorId = "AtlassianPS.ServerData.NoEntryExists" Category = "InvalidData" TargetObject = $Id } WriteError @writeErrorSplat } } end { if ($configurationChanged) { Save-Configuration } Write-Verbose "Function ended" } } #endregion Set-ServerConfiguration #region ConvertFrom-QueryString function ConvertFrom-QueryString { # .ExternalHelp ..\AtlassianPS.Configuration-help.xml [CmdletBinding(DefaultParameterSetName = 'ByString')] [OutputType([Hashtable])] param( [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'ByUri')] [Uri] $Uri, [Parameter(Mandatory, Position = 0, ValueFromPipeline, ParameterSetName = 'ByString')] [String] $Query ) process { $getParameter = @{} if ($Uri) { $Query = $Uri.Query } if (($Query -match '^\?.+') -or ($Query -match '=')) { $Query.TrimStart('?').Split('&') | ForEach-Object { $key, $value = $_.Split('=', 2) if (-not [String]::IsNullOrEmpty($key)) { $decodedKey = ConvertFrom-URLEncoded -InputString $key $decodedValue = if ([String]::IsNullOrEmpty($value)) { $value } else { ConvertFrom-URLEncoded -InputString $value } $getParameter[$decodedKey] = $decodedValue } } } $getParameter } } #endregion ConvertFrom-QueryString #region ConvertFrom-URLEncoded function ConvertFrom-URLEncoded { # .ExternalHelp ..\AtlassianPS.Configuration-help.xml [CmdletBinding()] [OutputType([String])] param( [Parameter(Mandatory, ValueFromPipeline)] [String[]] $InputString ) begin { Import-HttpUtility } process { foreach ($string in $InputString) { [System.Web.HttpUtility]::UrlDecode($string) } } } #endregion ConvertFrom-URLEncoded #region ConvertTo-Hashtable function ConvertTo-Hashtable { # .ExternalHelp ..\AtlassianPS.Configuration-help.xml [CmdletBinding()] [OutputType([Hashtable])] param( [Parameter(Mandatory, ValueFromPipeline)] [PSObject] $InputObject ) process { $hash = @{} foreach ($property in $InputObject.PSObject.Properties) { $hash[$property.Name] = $property.Value } $hash } } #endregion ConvertTo-Hashtable #region ConvertTo-QueryString function ConvertTo-QueryString { # .ExternalHelp ..\AtlassianPS.Configuration-help.xml [CmdletBinding()] [OutputType([String])] param( [Parameter(Mandatory, ValueFromPipeline)] [Hashtable] $InputObject ) process { if ($InputObject.Count -eq 0) { return '' } $pairs = foreach ($key in $InputObject.Keys) { $encodedKey = ConvertTo-URLEncoded -InputString "$key" $value = $InputObject[$key] if ($null -eq $value -or "$value" -eq '') { "$encodedKey=" } else { "{0}={1}" -f $encodedKey, (ConvertTo-URLEncoded -InputString "$value") } } '?' + ($pairs -join '&') } } #endregion ConvertTo-QueryString #region ConvertTo-URLEncoded function ConvertTo-URLEncoded { # .ExternalHelp ..\AtlassianPS.Configuration-help.xml [CmdletBinding()] [OutputType([String])] param( [Parameter(Mandatory, ValueFromPipeline)] [String[]] $InputString ) begin { Import-HttpUtility } process { foreach ($string in $InputString) { [System.Web.HttpUtility]::UrlEncode($string) } } } #endregion ConvertTo-URLEncoded #region Join-Hashtable function Join-Hashtable { # .ExternalHelp ..\AtlassianPS.Configuration-help.xml [CmdletBinding()] [OutputType([Hashtable])] param( [Parameter(Mandatory, ValueFromPipeline)] [AllowNull()] [System.Collections.IDictionary[]] $Hashtable, [Parameter()] [ValidateSet('Overwrite', 'Error')] [String] $OnDuplicate = 'Overwrite' ) begin { [Hashtable]$table = @{} $isInitialized = $false } process { foreach ($item in $Hashtable) { if ($null -eq $item) { continue } if (-not $isInitialized) { # Start from the first dictionary so key comparison behavior # (for example case sensitivity) is preserved. $table = [System.Collections.Hashtable]::new($item) $isInitialized = $true continue } foreach ($entry in $item.GetEnumerator()) { if ($OnDuplicate -eq 'Error' -and $table.Contains($entry.Key)) { $errorItem = [System.Management.Automation.ErrorRecord]::new( ([System.ArgumentException]::new("Duplicate key '$($entry.Key)' was encountered while joining hashtables.")), 'JoinHashtable.DuplicateKey', [System.Management.Automation.ErrorCategory]::InvalidArgument, $entry.Key ) $PSCmdlet.ThrowTerminatingError($errorItem) } $table[$entry.Key] = $entry.Value } } } end { $table } } #endregion Join-Hashtable #region Resolve-DefaultParameterValue function Resolve-DefaultParameterValue { # .ExternalHelp ..\AtlassianPS.Configuration-help.xml [CmdletBinding()] [OutputType([Hashtable])] param( [Parameter(Mandatory)] [Hashtable] $Reference, [Parameter(Mandatory)] [String[]] $CommandName, [Parameter()] [Hashtable] $Target = @{}, [Parameter()] [String[]] $ParameterName = '*' ) begin { $defaultItems = New-Object -TypeName System.Collections.ArrayList foreach ($key in $Reference.Keys) { $null = $defaultItems.Add( [PSCustomObject]@{ Key = $key Value = $Reference[$key] Command = $key.Split(':')[0] Parameter = $key.Split(':')[1] } ) } } process { foreach ($command in $CommandName) { foreach ($item in $defaultItems) { if ($command -notlike $item.Command) { continue } foreach ($parameter in $ParameterName) { if ($item.Parameter -like $parameter) { if ($parameter -ne '*') { $Target["${command}:$parameter"] = $item.Value } else { $Target["${command}:$($item.Parameter)"] = $item.Value } } } } } } end { $Target } } #endregion Resolve-DefaultParameterValue #region Resolve-FilePath function Resolve-FilePath { # .ExternalHelp ..\AtlassianPS.Configuration-help.xml [CmdletBinding()] [OutputType([String])] param( [Parameter(Mandatory, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [Alias('PSPath', 'LiteralPath')] [String] $Path ) process { $folder = Split-Path -Path $Path -Parent $file = Split-Path -Path $Path -Leaf if (-not [String]::IsNullOrEmpty($folder)) { $folderResolved = Resolve-Path -Path $folder } else { $folderResolved = Resolve-Path -Path $ExecutionContext.SessionState.Path.CurrentFileSystemLocation } Join-Path -Path $folderResolved.ProviderPath -ChildPath $file } } #endregion Resolve-FilePath #region Write-DebugMessage function Write-DebugMessage { # .ExternalHelp ..\AtlassianPS.Configuration-help.xml [CmdletBinding()] param( [Parameter(ValueFromPipeline)] [String] $Message, [Parameter()] [Switch] $BreakPoint, [Parameter()] [ValidateNotNullOrEmpty()] [System.Management.Automation.PSCmdlet] $Cmdlet = $( try { (Get-Variable -Scope 1 -Name PSCmdlet -ErrorAction Stop).Value } catch { $PSCmdlet } ) ) begin { Import-MqcnAlias -Alias "WriteDebug" -Command "Microsoft.PowerShell.Utility\Write-Debug" $oldDebugPreference = $DebugPreference if (-not $BreakPoint -and $DebugPreference -ne 'SilentlyContinue') { $DebugPreference = 'Continue' } } process { $caller = Get-PSCallStack | Select-Object -Last 1 -Skip 1 | Select-Object -First 1 if ($caller -and $caller.Arguments -and $caller.Arguments.Contains("Debug")) { $DebugPreference = 'Continue' } Format-MessageStyle -Message $Message -Cmdlet $Cmdlet | ForEach-Object { WriteDebug $_ } } end { $DebugPreference = $oldDebugPreference } } #endregion Write-DebugMessage #region Write-NonTerminatingError function Write-NonTerminatingError { # .ExternalHelp ..\AtlassianPS.Configuration-help.xml [CmdletBinding(DefaultParameterSetName = 'ExistingException')] param( [Parameter()] [ValidateNotNullOrEmpty()] [System.Management.Automation.PSCmdlet] $Cmdlet = $( try { (Get-Variable -Scope 1 -Name PSCmdlet -ErrorAction Stop).Value } catch { $PSCmdlet } ), [Parameter(Mandatory, ParameterSetName = 'ExistingException', Position = 1, ValueFromPipeline, ValueFromPipelineByPropertyName)] [Parameter(ParameterSetName = 'NewException')] [ValidateNotNullOrEmpty()] [System.Exception] $Exception, [Parameter(ParameterSetName = 'NewException', Position = 2)] [ValidateNotNullOrEmpty()] [String] $ExceptionType = 'System.Management.Automation.RuntimeException', [Parameter(Mandatory, ParameterSetName = 'NewException', Position = 3)] [ValidateNotNullOrEmpty()] [String] $Message, [Parameter()] [Object] $TargetObject, [Parameter(Mandatory, ParameterSetName = 'ExistingException', Position = 10)] [Parameter(Mandatory, ParameterSetName = 'NewException', Position = 10)] [ValidateNotNullOrEmpty()] [String] $ErrorId, [Parameter(Mandatory, ParameterSetName = 'ExistingException', Position = 11)] [Parameter(Mandatory, ParameterSetName = 'NewException', Position = 11)] [ValidateNotNull()] [System.Management.Automation.ErrorCategory] $Category, [Parameter(Mandatory, ParameterSetName = 'Rethrow', Position = 1)] [System.Management.Automation.ErrorRecord] $ErrorRecord ) process { if (-not $Cmdlet) { throw 'Cmdlet runtime was not provided. Call this helper from an advanced function or pass -Cmdlet.' } if (-not $ErrorRecord) { $mode = if ($PSCmdlet.ParameterSetName -eq 'NewException') { 'NewException' } else { 'ExistingException' } $ErrorRecord = New-ErrorRecord ` -Mode $mode ` -Exception $Exception ` -ExceptionType $ExceptionType ` -Message $Message ` -ErrorId $ErrorId ` -Category $Category ` -TargetObject $TargetObject } $Cmdlet.WriteError($ErrorRecord) } } #endregion Write-NonTerminatingError #region Write-TerminatingError function Write-TerminatingError { # .ExternalHelp ..\AtlassianPS.Configuration-help.xml [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] [CmdletBinding(DefaultParameterSetName = 'ExistingException')] param( [Parameter()] [ValidateNotNullOrEmpty()] [System.Management.Automation.PSCmdlet] $Cmdlet = $( try { (Get-Variable -Scope 1 -Name PSCmdlet -ErrorAction Stop).Value } catch { $PSCmdlet } ), [Parameter(Mandatory, ParameterSetName = 'ExistingException', Position = 1, ValueFromPipeline, ValueFromPipelineByPropertyName)] [Parameter(ParameterSetName = 'NewException')] [ValidateNotNullOrEmpty()] [System.Exception] $Exception, [Parameter(ParameterSetName = 'NewException', Position = 2)] [ValidateNotNullOrEmpty()] [String] $ExceptionType = 'System.Management.Automation.RuntimeException', [Parameter(Mandatory, ParameterSetName = 'NewException', Position = 3)] [ValidateNotNullOrEmpty()] [String] $Message, [Parameter()] [Object] $TargetObject, [Parameter(Mandatory, ParameterSetName = 'ExistingException', Position = 10)] [Parameter(Mandatory, ParameterSetName = 'NewException', Position = 10)] [ValidateNotNullOrEmpty()] [String] $ErrorId, [Parameter(Mandatory, ParameterSetName = 'ExistingException', Position = 11)] [Parameter(Mandatory, ParameterSetName = 'NewException', Position = 11)] [ValidateNotNull()] [System.Management.Automation.ErrorCategory] $Category, [Parameter(Mandatory, ParameterSetName = 'Rethrow', Position = 1)] [System.Management.Automation.ErrorRecord] $ErrorRecord ) process { if (-not $Cmdlet) { throw 'Cmdlet runtime was not provided. Call this helper from an advanced function or pass -Cmdlet.' } if (-not $ErrorRecord) { $mode = if ($PSCmdlet.ParameterSetName -eq 'NewException') { 'NewException' } else { 'ExistingException' } $ErrorRecord = New-ErrorRecord ` -Mode $mode ` -Exception $Exception ` -ExceptionType $ExceptionType ` -Message $Message ` -ErrorId $ErrorId ` -Category $Category ` -TargetObject $TargetObject } $Cmdlet.ThrowTerminatingError($ErrorRecord) } } #endregion Write-TerminatingError #region Write-VerboseMessage function Write-VerboseMessage { # .ExternalHelp ..\AtlassianPS.Configuration-help.xml [CmdletBinding()] param( [Parameter(Mandatory, ValueFromPipeline)] [String] $Message, [Parameter()] [ValidateNotNullOrEmpty()] [System.Management.Automation.PSCmdlet] $Cmdlet = $( try { (Get-Variable -Scope 1 -Name PSCmdlet -ErrorAction Stop).Value } catch { $PSCmdlet } ) ) begin { Import-MqcnAlias -Alias "WriteVerbose" -Command "Microsoft.PowerShell.Utility\Write-Verbose" } process { $caller = Get-PSCallStack | Select-Object -Last 1 -Skip 1 | Select-Object -First 1 if ($caller -and $caller.Arguments -and $caller.Arguments.Contains("Verbose")) { $VerbosePreference = 'Continue' } Format-MessageStyle -Message $Message -Cmdlet $Cmdlet | ForEach-Object { WriteVerbose $_ } } } #endregion Write-VerboseMessage #region Assert-ConnectionActive function Assert-ConnectionActive { [CmdletBinding()] param([Parameter(Mandatory)][AtlassianPS.Connection]$Connection, [String]$Product, [Switch]$RegisteredOnly) $sourceId = if ($Connection.IsOverride) { $Connection.SourceInstanceId } else { $Connection.InstanceId } $registered = $script:Connections[$sourceId] if ($Connection.RegistryGeneration -ne $script:RegistryGeneration -or ($RegisteredOnly -and $Connection.State -ne 'Active') -or $Connection.State -eq 'Disconnected' -or -not $registered -or $registered.State -ne 'Active' -or (-not $Connection.IsOverride -and -not [Object]::ReferenceEquals($registered, $Connection))) { throw 'Connection is not active in this runtime. Connect again in this PowerShell session.' } if ($Product -and $Connection.Product -ne $Product) { throw "Connection belongs to another product; expected $Product." } } #endregion Assert-ConnectionActive #region ConvertTo-ConnectionToken function ConvertTo-ConnectionToken { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '', Justification = 'The public contract accepts CI string tokens; normalization retains an owned SecureString copy.')] [CmdletBinding()] [OutputType([System.Security.SecureString])] param([Parameter(Mandatory)][AllowNull()][AllowEmptyString()][Object]$Token) if ($Token -is [System.Security.SecureString]) { if ($Token.Length -eq 0) { throw 'Token cannot be empty.' } return $Token.Copy() } if ($Token -is [string] -and -not [string]::IsNullOrWhiteSpace($Token)) { return ConvertTo-SecureString -String $Token -AsPlainText -Force } throw 'Token must be a nonempty string or SecureString.' } #endregion ConvertTo-ConnectionToken #region ConvertTo-PortableServer function ConvertTo-PortableServer { [CmdletBinding()] param([Parameter(Mandatory)][AtlassianPS.ServerData]$Server) if ($Server.Certificate -or ($Server.Headers -and $Server.Headers.Count -gt 0)) { throw 'Legacy profile contains headers or a certificate. Remove those settings and supply authentication to Connect before saving or exporting.' } if (-not $Server.Uri.IsAbsoluteUri -or $Server.Uri.UserInfo -or $Server.Uri.Query -or $Server.Uri.Fragment) { throw 'Profile URI must be absolute and contain no user information, query or fragment.' } [ordered]@{ Id = $Server.Id; Name = $Server.Name; Uri = $Server.Uri.AbsoluteUri; Type = $Server.Type.ToString() } } #endregion ConvertTo-PortableServer #region Get-BreadCrumb function Get-BreadCrumb { param( [String]$Delimiter = " > " ) $depth = 1 $path = New-Object -TypeName System.Collections.ArrayList while ($depth) { try { $null = $path.Add((Get-Variable MyInvocation -Scope $depth -ValueOnly).MyCommand.Name) $depth++ } catch { $depth = 0 } } $path.Remove("") $path -join $Delimiter } #endregion Get-BreadCrumb #region Import-MqcnAlias function Import-MqcnAlias { <# .SYNOPSIS Create an alias for a full command name .DESCRIPTION Create an alias for a full command name This can be used to create a mockable call to a full command name .EXAMPLE Import-MqcnAlias -Alias "GetItem" -Command Microsoft.PowerShell.Management\Get-Item --------- Description Create an alias "GetItem" for the normal Get-Item #> [CmdletBinding()] param( # Name of the alias to be used [Parameter( Mandatory )] [String] $Alias, # Name of the command for which to create the alias [Parameter( Mandatory )] [String] $Command ) begin { Set-Alias -Name $Alias -Value $Command -Scope 1 -WhatIf:$false } } #endregion Import-MqcnAlias #region Invoke-WebRequest function Invoke-WebRequest { # For Version up to 5.1 <# .ForwardHelpTargetName Microsoft.PowerShell.Utility\Invoke-WebRequest .ForwardHelpCategory Cmdlet #> [CmdletBinding(HelpUri = 'https://go.microsoft.com/fwlink/?LinkID=217035')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute( "PSAvoidUsingConvertToSecureStringWithPlainText", "", Justification = "Converting received plaintext token to SecureString" )] param( [switch] ${UseBasicParsing}, [Parameter(Mandatory = $true, Position = 0)] [ValidateNotNullOrEmpty()] [uri] ${Uri}, [Microsoft.PowerShell.Commands.WebRequestSession] ${WebSession}, [Alias('SV')] [string] ${SessionVariable}, [pscredential] [System.Management.Automation.CredentialAttribute()] ${Credential} = [System.Management.Automation.PSCredential]::Empty, [switch] ${UseDefaultCredentials}, [ValidateNotNullOrEmpty()] [string] ${CertificateThumbprint}, [ValidateNotNull()] [System.Security.Cryptography.X509Certificates.X509Certificate] ${Certificate}, [string] ${UserAgent}, [switch] ${DisableKeepAlive}, [ValidateRange(0, 2147483647)] [int32] ${TimeoutSec}, [System.Collections.IDictionary] ${Headers}, [ValidateRange(0, 2147483647)] [int] ${MaximumRedirection}, [Microsoft.PowerShell.Commands.WebRequestMethod] ${Method}, [uri] ${Proxy}, [pscredential] [System.Management.Automation.CredentialAttribute()] ${ProxyCredential} = [System.Management.Automation.PSCredential]::Empty, [switch] ${ProxyUseDefaultCredentials}, [Parameter(ValueFromPipeline = $true)] [System.Object] ${Body}, [string] ${ContentType}, [ValidateSet('chunked', 'compress', 'deflate', 'gzip', 'identity')] [string] ${TransferEncoding}, [string] ${InFile}, [string] ${OutFile}, [switch] ${PassThru}) begin { if ($Credential) { $SecureCreds = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes( $('{0}:{1}' -f $Credential.UserName, $Credential.GetNetworkCredential().Password) )) $Headers["Authorization"] = "Basic $($SecureCreds)" $PSBoundParameters.Remove("Credential") } if ($InFile) { $boundary = [System.Guid]::NewGuid().ToString() $enc = [System.Text.Encoding]::GetEncoding("iso-8859-1") $fileName = Split-Path -Path $InFile -Leaf $readFile = Get-Content -Path $InFile -Encoding Byte $fileEnc = $enc.GetString($readFile) $PSBoundParameters["Body"] = @' --{0} Content-Disposition: form-data; name="file"; filename="{1}" Content-Type: application/octet-stream {2} --{0}-- '@ -f $boundary, $fileName, $fileEnc $PSBoundParameters["Headers"]['X-Atlassian-Token'] = 'nocheck' $PSBoundParameters["ContentType"] = "multipart/form-data; boundary=`"$boundary`"" $null = $PSBoundParameters.Remove("InFile") } try { $outBuffer = $null if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer)) { $PSBoundParameters['OutBuffer'] = 1 } $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Utility\Invoke-WebRequest', [System.Management.Automation.CommandTypes]::Cmdlet) $scriptCmd = { & $wrappedCmd @PSBoundParameters } $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin) $steppablePipeline.Begin($PSCmdlet) } catch { throw } } process { try { $steppablePipeline.Process($_) if ($SessionVariable) { Set-Variable -Name $SessionVariable -Value (Get-Variable $SessionVariable).Value -Scope 1 } } catch { throw } } end { try { $steppablePipeline.End() } catch { throw } } } if ($PSVersionTable.PSVersion.Major -ge 6) { function Invoke-WebRequest { #require -Version 6 <# .ForwardHelpTargetName Microsoft.PowerShell.Utility\Invoke-WebRequest .ForwardHelpCategory Cmdlet #> [CmdletBinding(DefaultParameterSetName = 'StandardMethod', HelpUri = 'https://go.microsoft.com/fwlink/?LinkID=217035')] param( [switch] ${UseBasicParsing}, [Parameter(Mandatory = $true, Position = 0)] [ValidateNotNullOrEmpty()] [uri] ${Uri}, [Microsoft.PowerShell.Commands.WebRequestSession] ${WebSession}, [Alias('SV')] [string] ${SessionVariable}, [switch] ${AllowUnencryptedAuthentication}, [Microsoft.PowerShell.Commands.WebAuthenticationType] ${Authentication}, [pscredential] [System.Management.Automation.CredentialAttribute()] ${Credential}, [switch] ${UseDefaultCredentials}, [ValidateNotNullOrEmpty()] [string] ${CertificateThumbprint}, [ValidateNotNull()] [X509Certificate] ${Certificate}, [switch] ${SkipCertificateCheck}, [Microsoft.PowerShell.Commands.WebSslProtocol] ${SslProtocol}, [securestring] ${Token}, [string] ${UserAgent}, [switch] ${DisableKeepAlive}, [ValidateRange(0, 2147483647)] [int32] ${TimeoutSec}, [System.Collections.IDictionary] ${Headers}, [ValidateRange(0, 2147483647)] [int] ${MaximumRedirection}, [Parameter(ParameterSetName = 'StandardMethod')] [Parameter(ParameterSetName = 'StandardMethodNoProxy')] [Microsoft.PowerShell.Commands.WebRequestMethod] ${Method}, [Parameter(ParameterSetName = 'CustomMethod', Mandatory = $true)] [Parameter(ParameterSetName = 'CustomMethodNoProxy', Mandatory = $true)] [Alias('CM')] [ValidateNotNullOrEmpty()] [string] ${CustomMethod}, [Parameter(ParameterSetName = 'CustomMethodNoProxy', Mandatory = $true)] [Parameter(ParameterSetName = 'StandardMethodNoProxy', Mandatory = $true)] [switch] ${NoProxy}, [Parameter(ParameterSetName = 'StandardMethod')] [Parameter(ParameterSetName = 'CustomMethod')] [uri] ${Proxy}, [Parameter(ParameterSetName = 'StandardMethod')] [Parameter(ParameterSetName = 'CustomMethod')] [pscredential] [System.Management.Automation.CredentialAttribute()] ${ProxyCredential}, [Parameter(ParameterSetName = 'StandardMethod')] [Parameter(ParameterSetName = 'CustomMethod')] [switch] ${ProxyUseDefaultCredentials}, [Parameter(ValueFromPipeline = $true)] [System.Object] ${Body}, [string] ${ContentType}, [ValidateSet('chunked', 'compress', 'deflate', 'gzip', 'identity')] [string] ${TransferEncoding}, [string] ${InFile}, [string] ${OutFile}, [switch] ${PassThru}, [switch] ${PreserveAuthorizationOnRedirect}, [switch] ${SkipHeaderValidation}) begin { if ($Credential -and (-not ($Authentication))) { $PSBoundParameters["Authentication"] = "Basic" } if ($InFile) { $multipartContent = [System.Net.Http.MultipartFormDataContent]::new() $FileStream = [System.IO.FileStream]::new($InFile, [System.IO.FileMode]::Open) $fileHeader = [System.Net.Http.Headers.ContentDispositionHeaderValue]::new("form-data") $fileHeader.Name = "file" $fileHeader.FileName = ([System.io.FileInfo]$InFile).name $fileContent = [System.Net.Http.StreamContent]::new($FileStream) $fileContent.Headers.ContentDisposition = $fileHeader $fileContent.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::Parse("application/octet-stream") $multipartContent.Add($fileContent) $PSBoundParameters["Headers"]['X-Atlassian-Token'] = 'nocheck' $PSBoundParameters["Body"] = $multipartContent $null = $PSBoundParameters.Remove("InFile") } try { $outBuffer = $null if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer)) { $PSBoundParameters['OutBuffer'] = 1 } $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Utility\Invoke-WebRequest', [System.Management.Automation.CommandTypes]::Cmdlet) $scriptCmd = { & $wrappedCmd @PSBoundParameters } $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin) $steppablePipeline.Begin($PSCmdlet) } catch { throw } } process { try { $steppablePipeline.Process($_) if ($SessionVariable) { Set-Variable -Name $SessionVariable -Value (Get-Variable $SessionVariable).Value -Scope 1 } } catch { throw } } end { try { $steppablePipeline.End() } catch { throw } } } } #endregion Invoke-WebRequest #region Save-Configuration function Save-Configuration { [CmdletBinding()] param() begin { Write-Verbose "Function started" Import-MqcnAlias -Alias "ExportConfiguration" -Command "Configuration\Export-Configuration" $configuration = Get-Configuration -AsHashtable $export = @{} foreach ($key in $configuration.Keys) { $export[$key] = $configuration[$key] } $serverList = [System.Collections.Generic.List[AtlassianPS.ServerData]]::new() foreach ($server in @($export["ServerList"])) { if (-not $server) { continue } $portable = ConvertTo-PortableServer -Server $server $serverData = [AtlassianPS.ServerData]$portable $serverList.Add($serverData) } $export["ServerList"] = $serverList ExportConfiguration -InputObject $export -ErrorAction Stop 3>$null Write-Verbose "Function ended" } } #endregion Save-Configuration #region Format-MessageStyle function Format-MessageStyle { [CmdletBinding()] [OutputType([String])] param( [Parameter(Mandatory, ValueFromPipeline)] [AllowEmptyString()] [String] $Message, [Parameter()] [AllowNull()] [System.Management.Automation.PSCmdlet] $Cmdlet, [Parameter()] [AllowNull()] [AtlassianPS.MessageStyle] $MessageSettings = $script:Configuration["Message"] ) process { if (-not $MessageSettings) { $MessageSettings = [AtlassianPS.MessageStyle]::new() } $indent, $functionName, $timeStamp = "" if ($MessageSettings.BreadCrumbs) { "[$((Get-BreadCrumb) -replace '^Format-MessageStyle > ', '')]:" if ($MessageSettings.Indent) { $indent = " " * $MessageSettings.Indent } else { $indent = " " * 4 } } elseif ($MessageSettings.FunctionName -and $Cmdlet) { $functionName = "[$($Cmdlet.MyInvocation.MyCommand.Name)] " } if ($MessageSettings.TimeStamp) { $timeStamp = "[$(Get-Date -Format "HH:mm:ss")] " } "{0}{1}{2}{3}" -f $timeStamp, $functionName, $indent, $Message } } #endregion Format-MessageStyle #region Import-HttpUtility function Import-HttpUtility { [CmdletBinding()] param() if ('System.Web.HttpUtility' -as [Type]) { return } if ($PSVersionTable.PSEdition -eq 'Desktop') { Add-Type -AssemblyName System.Web -ErrorAction Stop } else { Add-Type -AssemblyName System.Web.HttpUtility -ErrorAction Stop } } #endregion Import-HttpUtility #region New-ErrorRecord function New-ErrorRecord { [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] [CmdletBinding()] [OutputType([System.Management.Automation.ErrorRecord])] param( [Parameter(Mandatory)] [ValidateSet('ExistingException', 'NewException')] [String] $Mode, [Parameter()] [System.Exception] $Exception, [Parameter()] [ValidateNotNullOrEmpty()] [String] $ExceptionType = 'System.Management.Automation.RuntimeException', [Parameter()] [String] $Message, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [String] $ErrorId, [Parameter(Mandatory)] [ValidateNotNull()] [System.Management.Automation.ErrorCategory] $Category, [Parameter()] [Object] $TargetObject ) if ($Mode -eq 'NewException') { if (-not $Message) { throw 'Message is required when creating a new exception.' } if ($Exception) { $Exception = New-Object -TypeName $ExceptionType -ArgumentList $Message, $Exception } else { $Exception = New-Object -TypeName $ExceptionType -ArgumentList $Message } } if (-not $Exception) { throw 'Exception is required when creating an error record from an existing exception.' } return [System.Management.Automation.ErrorRecord]::new($Exception, $ErrorId, $Category, $TargetObject) } #endregion New-ErrorRecord #region Resolve-FullPath function Resolve-FullPath { # .ExternalHelp ..\AtlassianPS.Configuration-help.xml [CmdletBinding()] [OutputType([String])] param( [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [ValidateScript( { if (-not (Test-Path $_ -PathType Leaf)) { $errorItem = [System.Management.Automation.ErrorRecord]::new( ([System.ArgumentException]'File not found'), 'ParameterValue.FileNotFound', [System.Management.Automation.ErrorCategory]::ObjectNotFound, $_ ) $errorItem.ErrorDetails = "No file could be found with the provided path '$_'." $PSCmdlet.ThrowTerminatingError($errorItem) } else { return $true } } )] [Alias('FullName', 'PSPath')] [String] $Path ) process { $PSCmdlet.GetUnresolvedProviderPathFromPSPath($Path) } } #endregion Resolve-FullPath #region ThrowError function ThrowError { <# .SYNOPSIS Utility to throw a terminating errorrecord .NOTES Thanks to Jaykul: https://github.com/PoshCode/Configuration/blob/master/Source/Metadata.psm1 #> param ( [Parameter()] [ValidateNotNullOrEmpty()] [System.Management.Automation.PSCmdlet] $Cmdlet = $((Get-Variable -Scope 1 PSCmdlet).Value), [Parameter(Mandatory = $true, ParameterSetName = "ExistingException", Position = 1, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Parameter(ParameterSetName = "NewException")] [ValidateNotNullOrEmpty()] [System.Exception] $Exception, [Parameter(ParameterSetName = "NewException", Position = 2)] [ValidateNotNullOrEmpty()] [System.String] $ExceptionType = "System.Management.Automation.RuntimeException", [Parameter(Mandatory = $true, ParameterSetName = "NewException", Position = 3)] [ValidateNotNullOrEmpty()] [System.String] $Message, [Parameter(Mandatory = $false)] [System.Object] $TargetObject, [Parameter(Mandatory = $true, ParameterSetName = "ExistingException", Position = 10)] [Parameter(Mandatory = $true, ParameterSetName = "NewException", Position = 10)] [ValidateNotNullOrEmpty()] [System.String] $ErrorId, [Parameter(Mandatory = $true, ParameterSetName = "ExistingException", Position = 11)] [Parameter(Mandatory = $true, ParameterSetName = "NewException", Position = 11)] [ValidateNotNull()] [System.Management.Automation.ErrorCategory] $Category, [Parameter(Mandatory = $true, ParameterSetName = "Rethrow", Position = 1)] [System.Management.Automation.ErrorRecord]$ErrorRecord ) process { if (!$ErrorRecord) { if ($PSCmdlet.ParameterSetName -eq "NewException") { if ($Exception) { $Exception = New-Object $ExceptionType $Message, $Exception } else { $Exception = New-Object $ExceptionType $Message } } $errorRecord = New-Object System.Management.Automation.ErrorRecord $Exception, $ErrorId, $Category, $TargetObject } $Cmdlet.ThrowTerminatingError($errorRecord) } } #endregion ThrowError #region Write-Verbose function Write-Verbose { <# .SYNOPSIS Write a verbose message .DESCRIPTION Write a verbose message This function allows the user to decide how the Verbose Message should be formatted. The configuration is inside `$scrpit:Configuration` and supports the following structure (json representation): { "message": { // show a line with the Bread Crumbs of the caller stack "breadcrumbs": true, // how many whitespaces should be used for indenting the // message "indent": true, // show the name of the calling function - this is ignored // if breadcrumbs is active "functionname": true, // show the timestamp (HH:mm:ss format) of the message "timestamp": true } } .EXAMPLE Write-DebugMessage "The value of `$var is: $var" ---------- Description Shows the message if the user added `-Debug` to the command but does not create a breakpoint #> [CmdletBinding()] param( [Parameter( Mandatory, ValueFromPipeline )] [String] $Message, [Parameter()] [ValidateNotNullOrEmpty()] [System.Management.Automation.PSCmdlet] $Cmdlet = ((Get-Variable -Scope 1 PSCmdlet).Value) ) begin { $indent, $functionName, $timeStamp = "" Import-MqcnAlias -Alias "WriteVerbose" -Command "Microsoft.PowerShell.Utility\Write-Verbose" $messageSettings = $script:Configuration["Message"] } process { if ((Get-PSCallstack | Select-Object -Last 1 -Skip 1).Arguments.Contains("Verbose")) { $VerbosePreference = 'Continue' } if ($messageSettings.Breadcrumbs) { WriteVerbose "[$(Get-BreadCrumb)]:" if ($messageSettings.Indent) { $indent = " " * $messageSettings.Indent } else { $indent = " " * 4 } } else { if ($messageSettings.FunctionName) { $functionName = "[$($Cmdlet.MyInvocation.MyCommand.Name)] " } } if ($messageSettings.Timestamp) { $timeStamp = "[$(Get-Date -f "HH:mm:ss")] " } WriteVerbose ("{0}{1}{2}{3}" -f $timeStamp, $functionName, $indent, $Message) } } #endregion Write-Verbose #region WriteError function WriteError { <# .SYNOPSIS Utility to write an errorrecord to the errstd .NOTES Thanks to Jaykul: https://github.com/PoshCode/Configuration/blob/master/Source/Metadata.psm1 #> param ( [Parameter()] [ValidateNotNullOrEmpty()] [System.Management.Automation.PSCmdlet] $Cmdlet = $((Get-Variable -Scope 1 PSCmdlet).Value), [Parameter(Mandatory = $true, ParameterSetName = "ExistingException", Position = 1, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Parameter(ParameterSetName = "NewException")] [ValidateNotNullOrEmpty()] [System.Exception] $Exception, [Parameter(ParameterSetName = "NewException", Position = 2)] [ValidateNotNullOrEmpty()] [System.String] $ExceptionType = "System.Management.Automation.RuntimeException", [Parameter(Mandatory = $true, ParameterSetName = "NewException", Position = 3)] [ValidateNotNullOrEmpty()] [System.String] $Message, [Parameter(Mandatory = $false)] [System.Object] $TargetObject, [Parameter(Mandatory = $true, Position = 10)] [ValidateNotNullOrEmpty()] [System.String] $ErrorId, [Parameter(Mandatory = $true, Position = 11)] [ValidateNotNull()] [System.Management.Automation.ErrorCategory] $Category, [Parameter(Mandatory = $true, ParameterSetName = "Rethrow", Position = 1)] [System.Management.Automation.ErrorRecord]$ErrorRecord ) process { if (!$ErrorRecord) { if ($PSCmdlet.ParameterSetName -eq "NewException") { if ($Exception) { $Exception = New-Object $ExceptionType $Message, $Exception } else { $Exception = New-Object $ExceptionType $Message } } $errorRecord = New-Object System.Management.Automation.ErrorRecord $Exception, $ErrorId, $Category, $TargetObject } $Cmdlet.WriteError($errorRecord) } } #endregion WriteError #endregion LoadFunctions # Load configuration using # https://github.com/PoshCode/Configuration [Hashtable]$script:Configuration = Import-Configuration -CompanyName "AtlassianPS" -Name "AtlassianPS.Configuration" if (-not $script:Configuration) { $script:Configuration = @{} } if (-not $script:Configuration.ContainsKey("ServerList")) { $script:Configuration.Add("ServerList", [System.Collections.Generic.List[AtlassianPS.ServerData]]::new()) } #endregion ModuleConfig |