TD.Util.psm1
[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "")] param( [parameter(Mandatory = $false)][HashTable]$ImportVars ) $quiet = $false if ($ImportVars -and $ImportVars.ContainsKey('Quiet')) { $quiet = $ImportVars.Quiet } if (!($quiet)) { $tdUtilModule = 'TD.Util' $manifest = Test-ModuleManifest -Path (Join-Path (Split-Path $MyInvocation.MyCommand.Path) "$tdUtilModule.psd1") -WarningAction SilentlyContinue Write-Host "$tdUtilModule Version $($manifest.Version.ToString()) by $($manifest.Author)" Write-Host "Proudly created in Schiedam (NLD), $($manifest.Copyright)"; } function Add-AdoAzureArmEndpoint { param( [alias('u', 'Uri')][string]$AdoUri, [alias('t', 'Token', 'Pat')][string]$AdoAuthToken, $Organization, $Project, $ProjectId, $ApiVersion = '6.0-preview.4', $Name, $SubscriptionId, $SubscriptionName, $TenantId, $ServicePrincipalId, $ServicePrincipalKey ) $endPoint = @{ data = @{ subscriptionId = $SubscriptionId subscriptionName = $SubscriptionName environment = "AzureCloud" scopeLevel = "Subscription" creationMode = "Manual" } name = $Name type = "AzureRM" url = "https=//management.azure.com/" authorization = @{ parameters = @{ tenantid = $TenantId serviceprincipalid = $ServicePrincipalId authenticationType = "spnKey" serviceprincipalkey = $ServicePrincipalKey } scheme = "ServicePrincipal" } isShared = $false isReady = $true serviceEndpointProjectReferences = @( @{ projectReference = @{ id = $ProjectId name = $Project } name = $Name } ) } # alternative: az devops service-endpoint create # $endPoint = @" # { # "data": { # "subscriptionId": "$SubscriptionId", # "subscriptionName": "$SubscriptionName", # "environment": "AzureCloud", # "scopeLevel": "Subscription", # "creationMode": "Manual" # }, # "name": "$Name", # "type": "AzureRM", # "url": "https://management.azure.com/", # "authorization": { # "parameters": { # "tenantid": "$TenantId", # "serviceprincipalid": "$ServicePrincipalId", # "authenticationType": "spnKey", # "serviceprincipalkey": "$ServicePrincipalKey" # }, # "scheme": "ServicePrincipal" # }, # "isShared": false, # "isReady": true, # "serviceEndpointProjectReferences": [ # { # "projectReference": { # "id": "$ProjectId", # "name": "$Project" # }, # "name": "$Name" # } # ] # } # "@ $res = Add-AdoEndpoint -u $Uri -t $AdoAuthToken -Organization $Organization -Project $Project -EndPoint $endPoint -ApiVersion $ApiVersion return $res } function Add-AdoBuildDefinition { param([alias('u', 'Uri')][string]$AdoUri, [alias('t', 'Token', 'Pat')][string]$AdoAuthToken, $Organization, $Project, $Definition, [switch]$AsJson, $ApiVersion = '6.0') $url = "$(Get-AdoUri -Uri $AdoUri -Project $Project -Organization $Organization)/_apis/build/definitions?api-version=$($ApiVersion)" if ($AsJson.IsPresent) { $body = $Definition } else { $body = $Definition | ConvertTo-Json -Depth 10 -Compress } $result = Invoke-RestMethod -Method Post -Uri $url -Body $body -Headers @{Authorization = "$(New-AdoAuthenticationToken $AdoAuthToken)" } -ContentType 'application/json' -TimeoutSec 60 return $result } function Add-AdoEndpoint { param([alias('u', 'Uri')][string]$AdoUri, [alias('t', 'Token', 'Pat')][string]$AdoAuthToken, $Organization, $Project, $EndPoint, [switch]$AsJson, $ApiVersion = '6.0-preview.4') $url = "$(Get-AdoUri -Uri $AdoUri -Project $Project -Organization $Organization)/_apis/serviceendpoint/endpoints?api-version=$($ApiVersion)" if ($AsJson.IsPresent) { $body = $EndPoint } else { $body = $EndPoint | ConvertTo-Json -Depth 10 -Compress } $result = Invoke-RestMethod -Method Post -Uri $url -Body $body -Headers @{Authorization = "$(New-AdoAuthenticationToken $AdoAuthToken)" } -ContentType 'application/json' -TimeoutSec 60 return $result } function Add-AdoEnvironment { param([alias('u', 'Uri')][string]$AdoUri, [alias('t', 'Token', 'Pat')][string]$AdoAuthToken, $Organization, $Project, $Name, $Description, $ApiVersion = '7.0') $url = "$(Get-AdoUri -Uri $AdoUri -Project $Project -Organization $Organization)/_apis/distributedtask/environments?api-version=$($ApiVersion)" $body = @{ name = $Name description = $Description } $jsonBody = $body | ConvertTo-Json -Depth 5 $result = Invoke-RestMethod -Method Post -Uri $url -Headers @{Authorization = "$(New-AdoAuthenticationToken $AdoAuthToken)" } -ContentType 'application/json' -TimeoutSec 60 -Body $jsonBody return $result.Value } function Add-AdoPool { param([alias('u', 'Uri')][string]$AdoUri, [alias('t', 'Token', 'Pat')][string]$AdoAuthToken, $Organization, $Name, $Body, $ApiVersion = '7.0') $url = "$(Get-AdoUri -Uri $AdoUri -Organization $Organization)/_apis/distributedtask/pools?api-version=$($ApiVersion)" if ($null -eq $Body) { $Body = @{ name = $Name } } if (($Body -is [HashTable]) -or ($Body -is [PSCustomObject])) { $jsonBody = $Body | ConvertTo-Json -Depth 5 } else { $jsonBody = $Body } $result = Invoke-RestMethod -Method Post -Uri $url -Headers @{Authorization = "$(New-AdoAuthenticationToken $AdoAuthToken)" } -ContentType 'application/json' -TimeoutSec 60 -Body $jsonBody return $result.Value } function Add-AdoProject { param([alias('u', 'Uri')][string]$AdoUri, [alias('t', 'Token', 'Pat')][string]$AdoAuthToken, $ApiVersion = '5.0', $Organization, $Project, $Description, [switch]$Wait) $url = "$(Get-AdoUri $AdoUri '' $Organization)/_apis/projects?api-version=$ApiVersion" $request = @{ name = $Project description = $Description capabilities = @{ versioncontrol = @{ sourceControlType = 'Git' } processTemplate = @{ templateTypeId = '6b724908-ef14-45cf-84f8-768b5384da45' } } } $body = $request | ConvertTo-Json -Depth 10 -Compress $result = Invoke-RestMethod -Method Post -Uri $url -Body $body -Headers @{Authorization = "$(New-AdoAuthenticationToken $AdoAuthToken)" } -ContentType 'application/json' -TimeoutSec 60 if ($Wait.IsPresent) { $timeOut = 60 * 5 $url = $result.url $timer = [Diagnostics.Stopwatch]::StartNew() $finished = $false while (!$finished) { Start-Sleep -Seconds 5 $o = Invoke-RestMethod -Method Get -Uri $url -Headers @{Authorization = "$(New-AdoAuthenticationToken $AdoAuthToken)" } -ContentType 'application/json' -TimeoutSec 60 if ($o.status -eq 'succeeded') { $finished = $true } if ($timer.Elapsed.TotalSeconds -gt $timeOut ) { Write-Warning "Project creation to long $($timeout)s"; $finished = $true } #[Console]::Write('.') } $timer.Stop() } return $result } function Add-AdoPullRequest { param([alias('u', 'Uri')][string]$AdoUri, [alias('t', 'Token', 'Pat')][string]$AdoAuthToken, $Organization, $Project, $RepoId, $SourceRefName, $TargetRefName = 'master', $Description = 'PR Created via REST API', $User = 'monitor', $ApiVersion = '6.0') $url = "$(Get-AdoUri -Uri $AdoUri -Project $Project -Organization $Organization)/_apis/git/repositories/$RepoId/pullRequests?api-version=$($ApiVersion)" $ref = "refs/heads/" $sourceBranch = "$Ref$SourceRefName" $targetBranch = "$Ref$TargetRefName" $request = @{ sourceRefName = $sourceBranch targetRefName = $targetBranch title = "Merge $SourceRefName to $TargetRefName by $User" description = $Description autoCompleteSetBy = @{ id = $User displayName = $User } createdBy = @{ id = $User displayName = $User } } $body = $request | ConvertTo-Json -Depth 10 -Compress $result = Invoke-RestMethod -Method Post -Uri $url -Body $body -Headers @{Authorization = "$(New-AdoAuthenticationToken $AdoAuthToken)" } -ContentType 'application/json' -TimeoutSec 60 return $result.pullRequestId } function Add-AdoRepository { param([alias('u', 'Uri')][string]$AdoUri, [alias('t', 'Token', 'Pat')][string]$AdoAuthToken, $Organization, $Project, $Repo, $ApiVersion = '6.0') $url = "$(Get-AdoUri -Uri $AdoUri -Project $Project -Organization $Organization)/_apis/git/repositories?api-version=$($ApiVersion)" $request = @{ name = $Repo } $body = $request | ConvertTo-Json -Depth 10 -Compress $result = Invoke-RestMethod -Method Post -Uri $url -Body $body -Headers @{Authorization = "$(New-AdoAuthenticationToken $AdoAuthToken)" } -ContentType 'application/json' -TimeoutSec 60 return $result } function Add-AdoVariableGroup { param([alias('u', 'Uri')][string]$AdoUri, [alias('t', 'Token', 'Pat')][string]$AdoAuthToken, $Organization, $Project, $Vars, $Type, $Name, $Description, $ApiVersion = '5.1-preview.1') $url = "$(Get-AdoUri -Uri $AdoUri -Project $Project -Organization $Organization)/_apis/distributedtask/variablegroups?api-version=$($ApiVersion)" $body = @{ variables = $Vars type = $Type name = $Name description = $Description } | ConvertTo-Json -Depth 10 -Compress $result = Invoke-RestMethod -Method Post -Uri $url -Body $body -Headers @{Authorization = "$(New-AdoAuthenticationToken $AdoAuthToken)" } -ContentType 'application/json' -TimeoutSec 60 return $result.id } function Get-AdoAllRepositories { param([alias('u', 'Uri')][string]$AdoUri, [alias('t', 'Token', 'Pat')][string]$AdoAuthToken, $Organization, $Filter = { $_ }, $ApiVersion = '5.0') $repositories = @() $projects = Get-AdoProjects -AdoUri $AdoUri -AdoAuthToken $AdoAuthToken -Organization $Organization -ApiVersion $ApiVersion foreach ($project in $projects) { $projectRepositories = Get-AdoRepositories -AdoUri $AdoUri -AdoAuthToken $AdoAuthToken -Organization $Organization -Project $project.name $projectRepositories = $projectRepositories | Where-Object $Filter foreach ($r in $projectRepositories) { $defBranch = '' if (Test-PSProperty $r 'defaultBranch') { $defBranch = $r.defaultBranch } $repositories += [PSCustomObject]@{ Project = $project.name Name = $r.name Id = $r.id DefaultBranch = $defBranch RemoteUrl = $r.RemoteUrl Url = $r.url WebUrl = $r.webUrl Object = $r } } } return , ($repositories | Sort-Object -Property Project, Name) } function Get-AdoApiResult([alias('u', 'Uri')][string]$AdoUri, [alias('t', 'Token', 'Pat')][string]$AdoAuthToken, $Method = 'Get', [alias('Body')]$aBody, $ContentType) { $params = @{} if ($aBody) { $params.Add('Body', $aBody) } if ($ContentType) { $params.Add('ContentType', $ContentType) } return Invoke-RestMethod -Uri $AdoUri -Method $Method -Headers @{Authorization = "$(New-AdoAuthenticationToken $AdoAuthToken)" } -TimeoutSec 60 @params } function Get-AdoBuild { param( [ValidateNotNullOrEmpty()][alias('u', 'Uri')][string]$AdoUri, [ValidateNotNullOrEmpty()][alias('t', 'Token', 'Pat')][string]$AdoAuthToken, [ValidateNotNullOrEmpty()]$Organization, [ValidateNotNullOrEmpty()]$Project, [ValidateNotNull()]$Id, $ApiVersion = '5.0' ) $url = "$(Get-AdoUri $AdoUri $Project $Organization)/_apis/build/builds/$($Id)?api-version=$ApiVersion" $build = Invoke-RestMethod -Uri $url -Headers @{Authorization = "$(New-AdoAuthenticationToken $AdoAuthToken)" } -TimeoutSec 60 return $build } function Get-AdoBuildDefinitions { param( [ValidateNotNullOrEmpty()][alias('u', 'Uri')][string]$AdoUri, [ValidateNotNullOrEmpty()][alias('t', 'Token', 'Pat')][string]$AdoAuthToken, [ValidateNotNullOrEmpty()]$Organization, [ValidateNotNullOrEmpty()]$Project, $ApiVersion = '6.0' ) $url = "$(Get-AdoUri $AdoUri $project $Organization)/_apis/build/definitions?api-version=$ApiVersion" $defs = Invoke-RestMethod -Uri $url -Headers @{Authorization = "$(New-AdoAuthenticationToken $AdoAuthToken)" } -TimeoutSec 60 return , $defs.Value } function Get-AdoBuildLog { param( [ValidateNotNullOrEmpty()][alias('u', 'Uri')][string]$AdoUri, [ValidateNotNullOrEmpty()][alias('t', 'Token', 'Pat')][string]$AdoAuthToken, [ValidateNotNullOrEmpty()]$Organization, [ValidateNotNullOrEmpty()]$Project, [ValidateNotNull()]$Id, [ValidateNotNull()]$LogId, $ApiVersion = '6.0' ) $url = "$(Get-AdoUri $AdoUri $Project $Organization)/_apis/build/builds/$($Id)/logs/$($LogId)?api-version=$ApiVersion" $log = Invoke-RestMethod -Uri $url -Headers @{Authorization = "$(New-AdoAuthenticationToken $AdoAuthToken)" } -TimeoutSec 60 return $log } function Get-AdoBuildLogs { param( [ValidateNotNullOrEmpty()][alias('u', 'Uri')][string]$AdoUri, [ValidateNotNullOrEmpty()][alias('t', 'Token', 'Pat')][string]$AdoAuthToken, [ValidateNotNullOrEmpty()]$Organization, [ValidateNotNullOrEmpty()]$Project, [ValidateNotNull()]$Id, $ApiVersion = '6.0' ) $url = "$(Get-AdoUri $AdoUri $Project $Organization)/_apis/build/builds/$($Id)/logs?api-version=$ApiVersion" $logs = Invoke-RestMethod -Uri $url -Headers @{Authorization = "$(New-AdoAuthenticationToken $AdoAuthToken)" } -TimeoutSec 60 return , $logs.value } function Get-AdoBuildOutput { param( [ValidateNotNullOrEmpty()][alias('u', 'Uri')][string]$AdoUri, [ValidateNotNullOrEmpty()][alias('t', 'Token', 'Pat')][string]$AdoAuthToken, [ValidateNotNullOrEmpty()]$Organization, [ValidateNotNullOrEmpty()]$Project, [ValidateNotNull()]$Id, $ApiVersion = '6.0' ) $params = @{ Organization = $Organization Project = $Project } if ($AdoUri) { [void]$params.Add('AdoUri', $AdoUri) } if ($AdoAuthToken) { [void]$params.Add('AdoAuthToken', $AdoAuthToken) } $output = $null $logs = Get-AdoBuildLogs @params -Id $Id foreach ($log in $logs) { $output += Get-AdoBuildLog @params -Id $Id -LogId $log.id } return $output } function Get-AdoBuilds { param( [ValidateNotNullOrEmpty()][alias('u', 'Uri')][string]$AdoUri, [ValidateNotNullOrEmpty()][alias('t', 'Token', 'Pat')][string]$AdoAuthToken, [ValidateNotNullOrEmpty()]$Organization, [ValidateNotNullOrEmpty()]$Project, [ValidateNotNull()]$DefinitionId, $StatusFilter, $ApiVersion = '6.0' ) $url = "$(Get-AdoUri $AdoUri $Project $Organization)/_apis/build/builds?definitions=$($DefinitionId)&statusFilter=$StatusFilter&api-version=$ApiVersion" $builds = Invoke-RestMethod -Uri $url -Headers @{Authorization = "$(New-AdoAuthenticationToken $AdoAuthToken)" } -TimeoutSec 60 return , $builds.value } function Get-AdoBuildTimeline { param( [ValidateNotNullOrEmpty()][alias('u', 'Uri')][string]$AdoUri, [ValidateNotNullOrEmpty()][alias('t', 'Token', 'Pat')][string]$AdoAuthToken, [ValidateNotNullOrEmpty()]$Organization, [ValidateNotNullOrEmpty()]$Project, [ValidateNotNull()]$Id, [ValidateNotNull()]$TimelineId, $ApiVersion = '6.0' ) $url = "$(Get-AdoUri $AdoUri $Project $Organization)/_apis/build/builds/$($Id)/timeline/$($TimelineId)?api-version=$ApiVersion" $build = Invoke-RestMethod -Uri $url -Headers @{Authorization = "$(New-AdoAuthenticationToken $AdoAuthToken)" } -TimeoutSec 60 return $build } function Get-AdoEndPoints { param([alias('u', 'Uri')][string]$AdoUri, [alias('t', 'Token', 'Pat')][string]$AdoAuthToken, $Organization, $Project, $ApiVersion = '6.0-preview.4', $Type, [switch]$Detailed) $detailOption = '' if ($detailed.IsPresent) { $detailOption = 'includeDetails=true&' } $typeOption = '' if ($Type) { $typeOption = "type=$type&" } $url = "$(Get-AdoUri $AdoUri $Project $Organization)/_apis/serviceendpoint/endpoints?$($typeOption)$($detailOption)api-version=$ApiVersion" $endpoints = Invoke-RestMethod -Uri $url -Headers @{Authorization = "$(New-AdoAuthenticationToken $AdoAuthToken)" } -TimeoutSec 60 return , $endpoints.Value } function Get-AdoEnvironments { param([alias('u', 'Uri')][string]$AdoUri, [alias('t', 'Token', 'Pat')][string]$AdoAuthToken, $Organization, $Project, $ApiVersion = '7.0') $url = "$(Get-AdoUri -Uri $AdoUri -Project $Project -Organization $Organization)/_apis/distributedtask/environments?api-version=$($ApiVersion)" $result = Invoke-RestMethod -Method Get -Uri $url -Headers @{Authorization = "$(New-AdoAuthenticationToken $AdoAuthToken)" } -ContentType 'application/json' -TimeoutSec 60 return $result.Value } function Get-AdoPipelineApprovals { param( [ValidateNotNullOrEmpty()][alias('u', 'Uri')][string]$AdoUri, [ValidateNotNullOrEmpty()][alias('t', 'Token', 'Pat')][string]$AdoAuthToken, [ValidateNotNullOrEmpty()]$Organization, [ValidateNotNullOrEmpty()]$Project, $Ids, $ApiVersion = '7.1-preview.1' ) $url = "$(Get-AdoUri $AdoUri $Project $Organization)/_apis/pipelines/approvals?approvalIds=$Ids&api-version=$ApiVersion" $approvals = Invoke-RestMethod -Uri $url -Headers @{Authorization = "$(New-AdoAuthenticationToken $AdoAuthToken)" } -TimeoutSec 60 return , $approvals.value } function Get-AdoPipelineChecks { param( [ValidateNotNullOrEmpty()][alias('u', 'Uri')][string]$AdoUri, [ValidateNotNullOrEmpty()][alias('t', 'Token', 'Pat')][string]$AdoAuthToken, [ValidateNotNullOrEmpty()]$Organization, [ValidateNotNullOrEmpty()]$Project, $ApiVersion = '7.1-preview.1' ) $url = "$(Get-AdoUri $AdoUri $Project $Organization)/_apis/pipelines/checks/configurations?api-version=$ApiVersion" $approvals = Invoke-RestMethod -Uri $url -Headers @{Authorization = "$(New-AdoAuthenticationToken $AdoAuthToken)" } -TimeoutSec 60 return , $approvals.value } function Get-AdoPipelinePendingApprovals { param( [ValidateNotNullOrEmpty()][alias('u', 'Uri')][string]$AdoUri, [ValidateNotNullOrEmpty()][alias('t', 'Token', 'Pat')][string]$AdoAuthToken, [ValidateNotNullOrEmpty()]$Organization, [ValidateNotNullOrEmpty()]$Project, $BuildDefinitionId, $ApiVersion = '7.1-preview.1' ) $splat = @{ Token = $AdoAuthToken Organization = $Organization Project = $Project } if ($BuildDefinitionId) { $bd = @{ DefinitionId = $BuildDefinitionId } } else { $bd = @{} } $builds = Get-AdoBuilds @splat -StatusFilter 'inProgress,notStarted' @bd $approvals = @() foreach ($b in $builds) { $bt = Get-AdoBuildTimeline @splat -Id $b.id -TimelineId $b.orchestrationPlan.planId $buildApprovals = $bt.records | Where-Object { ($_.type -eq 'Checkpoint.Approval' -and $_.state -eq 'inProgress') } foreach ($buildApproval in $buildApprovals) { $apr = Get-AdoPipelineApprovals @splat -Ids $buildApproval.id if ($apr -and ($null -ne $apr[0])) { $checkpoint = $bt.records | Where-Object { $_.id -eq $buildApproval.parentId } $stage = $bt.records | Where-Object { $_.id -eq $checkpoint.parentId } $approvals += @{ BuildDefinition = $b.Definition.id BuildName = $b.Definition.name BuildId = $b.Id BuildUrl = $b._links.web.href StageId = $stage.id Stage = $stage.identifier StageName = $stage.name ApprovalId = $apr[0].id Approval = $apr[0] } } } } return $approvals } function Get-AdoPipelinePermission { param([alias('u', 'Uri')][string]$AdoUri, [alias('t', 'Token', 'Pat')][string]$AdoAuthToken, $Organization, $Project, $ResourceType, $ResourceId, $ApiVersion = '6.1-preview.1') $url = "$(Get-AdoUri -Uri $AdoUri -Project $Project -Organization $Organization)/_apis/pipelines/pipelinePermissions/$($ResourceType)/$($ResourceId)?api-version=$($ApiVersion)" $result = Invoke-RestMethod -Method Get -Uri $url -Headers @{Authorization = "$(New-AdoAuthenticationToken $AdoAuthToken)" } -ContentType 'application/json' -TimeoutSec 60 return $result } function Get-AdoPool { param([alias('u', 'Uri')][string]$AdoUri, [alias('t', 'Token', 'Pat')][string]$AdoAuthToken, $Organization, $PoolId, $ApiVersion = '7.0') $url = "$(Get-AdoUri -Uri $AdoUri -Organization $Organization)/_apis/distributedtask/pools/$($PoolId)?api-version=$ApiVersion" $p = Invoke-RestMethod -Method Get -Uri $url -Headers @{Authorization = "$(New-AdoAuthenticationToken $AdoAuthToken)" } -ContentType 'application/json' -TimeoutSec 60 return $p } function Get-AdoPools { param([alias('u', 'Uri')][string]$AdoUri, [alias('t', 'Token', 'Pat')][string]$AdoAuthToken, $Organization, $ApiVersion = '7.0') $url = "$(Get-AdoUri -Uri $AdoUri -Organization $Organization)/_apis/distributedtask/pools?api-version=$($ApiVersion)" $pools = Invoke-RestMethod -Method Get -Uri $url -Headers @{Authorization = "$(New-AdoAuthenticationToken $AdoAuthToken)" } -ContentType 'application/json' -TimeoutSec 60 return , $pools.value } function Get-AdoProject { [CmdletBinding()] param([alias('u', 'Uri')][string]$AdoUri, [alias('t', 'Token', 'Pat')][string]$AdoAuthToken, $ApiVersion = '5.0', $Organization, $Project) try { $prj = $null $url = "$(Get-AdoUri $AdoUri '' $Organization)/_apis/projects/$($Project)?includeCapabilities=true&includeHistory=true&api-version=$ApiVersion" $prj = Invoke-RestMethod -Uri $url -Headers @{Authorization = "$(New-AdoAuthenticationToken $AdoAuthToken)" } -TimeoutSec 60 } catch { if ($ErrorActionPreference -ne 'Ignore') { Throw } } return $prj } function Get-AdoProjects { param([alias('u', 'Uri')][string]$AdoUri, [alias('t', 'Token', 'Pat')][string]$AdoAuthToken, $ApiVersion = '5.0', $Organization) $url = "$(Get-AdoUri $AdoUri '' $Organization)/_apis/projects?api-version=$ApiVersion" $projects = Invoke-RestMethod -Uri $url -Headers @{Authorization = "$(New-AdoAuthenticationToken $AdoAuthToken)" } -TimeoutSec 60 return , $projects.Value } function Get-AdoQueue { param([alias('u', 'Uri')][string]$AdoUri, [alias('t', 'Token', 'Pat')][string]$AdoAuthToken, $ApiVersion = '6.0-preview.1', $Organization, $Project, $Queue) $url = "$(Get-AdoUri $AdoUri $Project $Organization)/_apis/distributedtask/queues?queueName=$($Queue)&api-version=$ApiVersion" $q = Invoke-RestMethod -Uri $url -Headers @{Authorization = "$(New-AdoAuthenticationToken $AdoAuthToken)" } -TimeoutSec 60 return $q } function Get-AdoReleaseApprovals { param( [ValidateNotNullOrEmpty()][alias('u', 'Uri')][string]$AdoUri, [ValidateNotNullOrEmpty()][alias('t', 'Token', 'Pat')][string]$AdoAuthToken, [ValidateNotNullOrEmpty()]$Organization, [ValidateNotNullOrEmpty()]$Project, [ValidateNotNull()]$StatusFilter = 'pending', $ApiVersion = '6.0' ) $url = "$(Get-AdoUri $AdoUri $Project $Organization)/_apis/release/approvals?api-version=$ApiVersion" $url = Get-AdoVsRmUrl $url $approvals = Invoke-RestMethod -Uri $url -Headers @{Authorization = "$(New-AdoAuthenticationToken $AdoAuthToken)" } -TimeoutSec 60 return , $approvals.value } function Get-AdoRepositories { param([alias('u', 'Uri')][string]$AdoUri, [alias('t', 'Token', 'Pat')][string]$AdoAuthToken, $Organization, $Project, $ApiVersion = '5.0') $url = "$(Get-AdoUri $AdoUri $Project $Organization)/_apis/git/repositories?includeLinks=true&includeAllUrls=true&includeHidden=true&api-version=$ApiVersion" $repositories = Invoke-RestMethod -Uri $url -Headers @{Authorization = "$(New-AdoAuthenticationToken $AdoAuthToken)" } -TimeoutSec 60 return , $repositories.Value } function Get-AdoRepository { param([alias('u', 'Uri')][string]$AdoUri, [alias('t', 'Token', 'Pat')][string]$AdoAuthToken, $Organization, $Project, $Repo, $ApiVersion = '5.0') $url = "$(Get-AdoUri $AdoUri $Project $Organization)/_apis/git/repositories/$($Repo)?includeLinks=true&includeAllUrls=true&includeHidden=true&api-version=$ApiVersion" $repo = Invoke-RestMethod -Uri $url -Headers @{Authorization = "$(New-AdoAuthenticationToken $AdoAuthToken)" } -TimeoutSec 60 return $repo } function Get-AdoRepositoryPolicy { param([alias('u', 'Uri')][string]$AdoUri, [alias('t', 'Token', 'Pat')][string]$AdoAuthToken, $Organization, $Project, $RepoId, $RefName, $ApiVersion = '5.0') if ($RepoId) { $url = "$(Get-AdoUri $AdoUri $Project $Organization)/_apis/git/policy/configurations?repositoryId=$($RepoId)&refName=$($RefName)&api-version=$ApiVersion" } else { $url = "$(Get-AdoUri $AdoUri $Project $Organization)/_apis/git/policy/configurations?api-version=$ApiVersion" } $policies = Invoke-RestMethod -Uri $url -Headers @{Authorization = "$(New-AdoAuthenticationToken $AdoAuthToken)" } -TimeoutSec 60 return ,$policies.Value } function Get-AdoUri { param($Uri, $Project, $Organization) if ($Uri) { $lUri = [System.Uri]$Uri } else { if ($env:SYSTEM_TEAMFOUNDATIONCOLLECTIONURI) { $lUri = [System.Uri]$env:SYSTEM_TEAMFOUNDATIONCOLLECTIONURI } else { if ($Organization) { $lUri = [System.Uri]"https://dev.azure.com/$Organization" } else { Throw "Unable to create Azure DevOps organization url, no Organization defined" } } } if ($Project) { $uriBuilder = [System.UriBuilder]$lUri if ($uriBuilder.Path.EndsWith('/')) { $s = '' } else { $s = '/' } $uriBuilder.Path += "$s$Project" $lUri = $uriBuilder.Uri } return $lUri.ToString() } function Get-AdoVariableGroup { param([alias('u', 'Uri')][string]$AdoUri, [alias('t', 'Token', 'Pat')][string]$AdoAuthToken, $Organization, $Project, $Name, $ApiVersion = '5.1-preview.1') $url = "$(Get-AdoUri -Uri $AdoUri -Project $Project -Organization $Organization)/_apis/distributedtask/variablegroups/?groupName=$($Name)&actionFilter=none&api-version=$($ApiVersion)" $result = Invoke-RestMethod -Method Get -Uri $url -Headers @{Authorization = "$(New-AdoAuthenticationToken $AdoAuthToken)" } -ContentType 'application/json' -TimeoutSec 60 return $result.Value } function Get-AdoVsRmUrl($Url) { return $Url.Replace('dev.azure.com', 'vsrm.dev.azure.com') } <# .SYNOPSIS Get the Azure DevOps Personal Access Token from Azure Devops Hosted Agent (In build/deploy) or the Windows Credential Store .DESCRIPTION Get the Azure DevOps Personal Access Token from Azure Devops Hosted Agent (In build/deploy) or the Windows Credential Store. This function is MS Windows only when running local. .PARAMETER Url Url of the Azure DevOps subscription like https://(mycompany)@dev.azure.com/(mycompany) .Example $token = Get-AzureDevOpsAccessToken 'https://mycompany@dev.azure.com/mycompany') #> function Get-AzureDevOpsAccessToken([Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$Url) { Write-Verbose "Get-AzureDevOpsAccessToken $Url" $token = $env:SYSTEM_ACCESSTOKEN if ([string]::IsNullOrEmpty($token)) { if ($env:windir) { if (-not(Get-Module CredentialManager -ListAvailable)) { Install-Module CredentialManager -Scope CurrentUser -Force } Import-Module CredentialManager $credential = Get-StoredCredential -Target "git:$Url" if ($null -eq $credential) { Throw "No Azure DevOps credentials found in credential store" } Write-Verbose "Using Azure DevOps Access Token from Windows Credential Store" $token = $credential.GetNetworkCredential().Password } else { Write-Warning "Unable to resolve Azure DevOps (ADO) Credential on platforms other than Windows" } } return $token } <# .SYNOPSIS Get the Azure DevOps Credentials from Azure Devops Hosted Agent (In build/deploy) or the Windows Credential Store .DESCRIPTION Get the Azure DevOps Credentials from Azure Devops Hosted Agent (In build/deploy) or the Windows Credential Store. This function is MS Windows only when running local. .PARAMETER Url Url of the Azure DevOps subscription like https://(mycompany)@dev.azure.com/(mycompany) .Example $cred = Get-AzureDevOpsCredential 'https://mycompany@dev.azure.com/mycompany') #> function Get-AzureDevOpsCredential([Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$Url) { Write-Verbose "Get-AzureDevOpsCredential $Url" $token = $env:SYSTEM_ACCESSTOKEN if ([string]::IsNullOrEmpty($token)) { if ($env:windir) { if (-not(Get-Module CredentialManager -ListAvailable)) { Install-Module CredentialManager -Scope CurrentUser -Force } Import-Module CredentialManager $credential = Get-StoredCredential -Target "git:$Url" if ($null -eq $credential) { Throw "No Azure DevOps credentials found. It should be passed in via env:SYSTEM_ACCESSTOKEN." } Write-Verbose "Using Azure DevOps Access Token from Windows Credential Store" } else { Write-Warning "Unable to resolve Azure DevOps (ADO) Credential on platforms other than Windows" } } else { Write-Verbose "Using Azure DevOps Access Token from Hosted Agent" $secureToken = $token | ConvertTo-SecureString -AsPlainText -Force $credential = New-Object System.Management.Automation.PSCredential(".", $secureToken) } return $credential } function Grant-AdoPipelineResourceAccess { param([alias('u', 'Uri')][string]$AdoUri, [alias('t', 'Token', 'Pat')][string]$AdoAuthToken, $Organization, $Project, $Access, [switch]$AsJson, $ApiVersion = '6.0') $url = "$(Get-AdoUri -Uri $AdoUri -Project $Project -Organization $Organization)/_apis/pipelines/pipelinePermissions?api-version=$($ApiVersion)" if ($AsJson.IsPresent) { $body = $Access } else { $body = $Access | ConvertTo-Json -Depth 10 -Compress } $result = Invoke-RestMethod -Method Patch -Uri $url -Body $body -Headers @{Authorization = "$(New-AdoAuthenticationToken $AdoAuthToken)" } -ContentType 'application/json' -TimeoutSec 60 return $result } <# .SYNOPSIS Import PowerShell module(s) and if not found install them from Azure DevOps Artifacts .DESCRIPTION Import PowerShell module(s) and if not found install them from Azure DevOps Artifacts .PARAMETER PackageSource Azure DevOps packagesource name .PARAMETER Modules Array of modules to import .PARAMETER Credential Credentials to access feed .PARAMETER Latest Always import latest modules .EXAMPLE Register-AzureDevOpsPackageSource -Name myFeed -Url https://pkgs.dev.azure.com/myCompany/_packaging/myFeed/nuget/v2 Import-AzureDevOpsModules -PackageSource 'myFeed' -Modules @('myModule') -Latest #> function Import-AzureDevOpsModules([Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$PackageSource, [Parameter(Mandatory = $true)]$Modules, [System.Management.Automation.PSCredential]$Credential, [Switch]$Latest) { Write-Verbose "Import-AzureDevOpsModules '$Modules' from $PackageSource" foreach ($module in $Modules) { if (-not (Get-Module -ListAvailable -Name $module) -or $Latest.IsPresent) { Install-Module $module -Repository $PackageSource -Scope CurrentUser -Force -AllowClobber -Credential $Credential } else { Import-Module $module } } } function Invoke-AdoCreateRelease { param([alias('u', 'Uri')][string]$AdoUri, [alias('t', 'Token', 'Pat')][string]$AdoAuthToken, $Organization, $Project, $DefinitionId, $BuildId, $BuildAlias, $Reason = 'UserCreated', $ApiVersion = '5.0') $body = @{ "definitionId" = "$($DefinitionId)"; "reason" = "$Reason" } $body.Add("artifacts", @()) $artifact = @{ "alias" = "$($BuildAlias)" "instanceReference" = @{ "id" = "$($BuildId)" "name" = $null } } $body.artifacts += $artifact $jsonBody = $body | ConvertTo-Json -Depth 5 $url = "$(Get-AdoUri $AdoUri $Project $Organization)/_apis/release/releases?api-version=$ApiVersion" $url = Get-AdoVsRmUrl $url $release = Get-AdoApiResult -AdoUri $url -AdoAuthToken $AdoAuthToken -Method Post -Body $jsonBody -ContentType 'application/json' return $release } function Invoke-AdoQueueBuild { param([alias('u', 'Uri')][string]$AdoUri, [alias('t', 'Token', 'Pat')][string]$AdoAuthToken, $Organization, $Project, $Id, $Branch, $Reason = 'UserCreated', $ApiVersion = '5.0') if ($Branch) { $lBranch = $Branch } else { $lBranch = '' } $body = "{`"definition`": { `"id`": $Id }, reason: `"$Reason`", priority: `"Normal`", tags: `"`", sourceBranch: `"$lBranch`"}" $url = "$(Get-AdoUri $AdoUri $Project $Organization)/_apis/build/builds?api-version=$ApiVersion" $build = Get-AdoApiResult -AdoUri $url -AdoAuthToken $AdoAuthToken -Method Post -Body $body -ContentType 'application/json' return $build } function Invoke-AdoQueuePipeline { # Remark: To use templatedParameters you need to supply all parameters. Tip: use browser dev console tools to see usage of call in ADO Web Application # ref: https://learn.microsoft.com/en-us/rest/api/azure/devops/pipelines/runs/run-pipeline?view=azure-devops-rest-7.0 param( [alias('u', 'Uri')][string]$AdoUri, [alias('t', 'Token', 'Pat')][string]$AdoAuthToken, $Organization, $Project, $Id, $Branch, [HashTable]$Parameters, [HashTable]$Variables, [Array]$StagesToSkip, [Switch]$Preview, [Switch]$Debug, $ApiVersion = '7.0') $body = @{} if ($Branch) { $body.resources = @{ "repositories" = @{ "self" = @{ "refName" = $Branch } } } } if ($Preview) { $body.previewRun = $true } if ($StagesToSkip) { $body.stagesToSkip = $StagesToSkip } if ($Parameters) { $body.templateParameters = $Parameters } else { $body.templateParameters = @{} } if ($Variables) { $body.variables = $Variables } else { $body.variables = @{} } if ($Debug) { $body.variables.'system.debug' = $true $body.variables.'agent.diagnostic' = $true } $url = "$(Get-AdoUri $AdoUri $Project $Organization)/_apis/pipelines/$($Id)/runs?api-version=$ApiVersion" $build = Get-AdoApiResult -AdoUri $url -AdoAuthToken $AdoAuthToken -Method Post -Body ($body | ConvertTo-Json -Depth 10 -Compress) -ContentType 'application/json' return $build } function New-AdoAuthenticationToken([alias('p', 'Pat')][string] $Token) { $accessToken = ""; if ($Token) { $user = "" $encodedToken = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $User, $Token))) $accessToken = "Basic $encodedToken" } elseif ($env:SYSTEM_ACCESSTOKEN) { $accessToken = "Bearer $($env:SYSTEM_ACCESSTOKEN)" } else { Throw "No AccessToken or Personal Access Token (PAT) supplied" } return $accessToken; } <# .SYNOPSIS Publish the PowerShell Package to the Azure Devops Feed / Artifacts .DESCRIPTION Publish the PowerShell Package to the Azure Devops Feed / Artifacts. Depends on nuget.exe installed and in environment path. Strategy: - Register feed with nuget - Register local temp feed to use Powershell Publish-Module command - Publish locally created module to feed with nuget.exe .PARAMETER ModuleName Name of the PowerShell Module to publish .PARAMETER ModulePath Root path of the module .PARAMETER Feedname Name of the Azure DevOps feed .PARAMETER FeedUrl Url of the Azure DevOps feed .PARAMETER AccessToken Personal AccessToken used for Azure DevOps Feed push/publish .Example Publish-PackageToAzureDevOps -ModuleName 'MyModule' -ModulePath './Output' -Feedname 'MyFeed' -FeedUrl 'https://pkgs.dev.azure.com/mycompany/_packaging/MyFeed/nuget/v2' -AccessToken 'sasasasa' #> function Publish-PackageToAzureDevOps([Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$ModuleName, $ModulePath = './Output', [Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$Feedname, [Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$FeedUrl, [Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$AccessToken) { Write-Verbose "Publish-PackageToAzureDevOps $ModuleName" $packageSource = $Feedname $packageFeedUrl = $FeedUrl $deployPath = Join-Path $ModulePath $ModuleName # register nuget feed $nuGet = (Get-Command 'nuget').Source &$nuGet sources Remove -Name $packageSource [string]$r = &$nuGet sources if (!($r.Contains($packageSource))) { # add as NuGet feed Write-Verbose "Add NuGet source" &$nuGet sources Add -Name $packageSource -Source $packageFeedUrl -username "." -password $AccessToken } # get module version $manifestFile = "./$ModuleName/$ModuleName.psd1" $manifest = Import-PowerShellDataFile -Path $manifestFile $version = $manifest.Item('ModuleVersion') if (!$version) { Throw "No module version found in $manifestFile" } else { Write-Host "$moduleName version: $version" } $tmpFeedPath = Join-Path ([System.IO.Path]::GetTempPath()) "$(New-Guid)-Localfeed" New-Item -Path $tmpFeedPath -ItemType Directory -ErrorAction Ignore -Force | Out-Null try { # register temp feed for export package # maybe need to use temp name for feed !! if (Get-PSRepository -Name LocalFeed -ErrorAction Ignore) { Unregister-PSRepository -Name LocalFeed } Register-PSRepository -Name LocalFeed -SourceLocation $tmpFeedPath -PublishLocation $tmpFeedPath -InstallationPolicy Trusted try { # publish to temp feed $packageName = "$moduleName.$version.nupkg" $package = (Join-Path $tmpFeedPath $packageName) Write-Verbose "Publish Module $package" Publish-Module -Path $deployPath -Repository LocalFeed -Force -ErrorAction Ignore if (!(Test-Path $package)) { Throw "Nuget package $package not created" } # publish package from tmp/local feed to PS feed Write-Verbose "Push package $packageName in $tmpFeedPath" Push-Location $tmpFeedPath try { nuget push $packageName -source $packageSource -Apikey Az -NonInteractive if ($LastExitCode -ne 0) { Throw "Error pushing nuget package $packageName to feed $packageSource ($packageFeedUrl)" } } finally { Pop-Location } } finally { Unregister-PSRepository -Name LocalFeed -ErrorAction Ignore } } finally { Remove-Item -Path $tmpFeedPath -Force -Recurse } } <# .SYNOPSIS Registers a package source from AzureDevOps Feed / Artifacts .DESCRIPTION Registers a package source from AzureDevOps Feed /Artifacts. If already found removes reference first. .PARAMETER Name Name of package source .PARAMETER Url Url of package feed .PARAMETER Credential Credentials to access feed .Example Register-AzureDevOpsPackageSource -Name myFeed -Url https://pkgs.dev.azure.com/myCompany/_packaging/myFeed/nuget/v2 #> function Register-AzureDevOpsPackageSource([Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$Name, [Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$Url, [System.Management.Automation.PSCredential]$Credential) { Write-Verbose "Register-AzureDevOpsPackageSource $Name" if ($Credential) { Write-Verbose "Performing Credential check..." try { Invoke-RestMethod -Uri $Url -Credential $Credential | Out-Null # check for access to artifacts with credential } catch { Throw "Register-AzureDevOpsPackageSource error for $Url : $($_.Exception.Message)" } } try { if (Get-PSRepository -Name $Name -ErrorAction Ignore) { Unregister-PSRepository -Name $Name } Register-PSRepository -Name $Name -SourceLocation $Url -InstallationPolicy Trusted -Credential $Credential } catch { if ($env:windir) { if ($_.Exception.Message -eq "The property 'Name' cannot be found on this object. Verify that the property exists.") { Write-Warning "Maybe invalid PSRepositories.xml detected in 'C:\Users\$($env:USERNAME)\AppData\Local\Microsoft\Windows\PowerShell\PowerShellGet', check file for correctness" } } Write-Host $_ Throw } } function Remove-AdoBuildDefinition { [CmdletBinding()] param([alias('u', 'Uri')][string]$AdoUri, [alias('t', 'Token', 'Pat')][string]$AdoAuthToken, $ApiVersion = '6.0', $Organization, $Project, $BuildDefinitionId) $url = "$(Get-AdoUri -Uri $AdoUri -Project $Project -Organization $Organization)/_apis/build/definitions/$($BuildDefinitionId)?api-version=$ApiVersion" Invoke-RestMethod -Method Delete -Uri $url -Headers @{Authorization = "$(New-AdoAuthenticationToken $AdoAuthToken)" } -TimeoutSec 60 } function Remove-AdoEnvironment { param([alias('u', 'Uri')][string]$AdoUri, [alias('t', 'Token', 'Pat')][string]$AdoAuthToken, $Organization, $Project, $EnvironmentId, $ApiVersion = '7.0') $url = "$(Get-AdoUri -Uri $AdoUri -Project $Project -Organization $Organization)/_apis/distributedtask/environments/$($EnvironmentId)?api-version=$($ApiVersion)" Invoke-RestMethod -Method Delete -Uri $url -Headers @{Authorization = "$(New-AdoAuthenticationToken $AdoAuthToken)" } -TimeoutSec 60 } function Remove-AdoPool { param([alias('u', 'Uri')][string]$AdoUri, [alias('t', 'Token', 'Pat')][string]$AdoAuthToken, $Organization, $PoolId, $ApiVersion = '7.0') $url = "$(Get-AdoUri -Uri $AdoUri -Organization $Organization)/_apis/distributedtask/pools/$($PoolId)?api-version=$($ApiVersion)" Invoke-RestMethod -Method Delete -Uri $url -Headers @{Authorization = "$(New-AdoAuthenticationToken $AdoAuthToken)" } -TimeoutSec 60 } function Remove-AdoProject { [CmdletBinding()] param([alias('u', 'Uri')][string]$AdoUri, [alias('t', 'Token', 'Pat')][string]$AdoAuthToken, $ApiVersion = '6.0', $Organization, $ProjectId) try { $prj = $null $url = "$(Get-AdoUri $AdoUri '' $Organization)/_apis/projects/$($ProjectId)?api-version=$ApiVersion" $prj = Invoke-RestMethod -Method Delete -Uri $url -Headers @{Authorization = "$(New-AdoAuthenticationToken $AdoAuthToken)" } -TimeoutSec 60 } catch { if ($ErrorActionPreference -ne 'Ignore') { Throw } } return $prj } function Remove-AdoRepository { [CmdletBinding()] param([alias('u', 'Uri')][string]$AdoUri, [alias('t', 'Token', 'Pat')][string]$AdoAuthToken, $ApiVersion = '6.0', $Organization, $Project, $RepoId) $url = "$(Get-AdoUri -Uri $AdoUri -Project $Project -Organization $Organization)/_apis/git/repositories/$($RepoId)?api-version=$ApiVersion" Invoke-RestMethod -Method Delete -Uri $url -Headers @{Authorization = "$(New-AdoAuthenticationToken $AdoAuthToken)" } -TimeoutSec 60 } function Remove-AdoServiceConnection { [CmdletBinding()] param([alias('u', 'Uri')][string]$AdoUri, [alias('t', 'Token', 'Pat')][string]$AdoAuthToken, $ApiVersion = '6.0-preview.4', $Organization, $ProjectIds, $EndPointId, [switch]$DeleteSpn) $url = "$(Get-AdoUri -Uri $AdoUri -Project '' -Organization $Organization)/_apis/serviceendpoint/endpoints/$($EndpointId)?projectIds=$($ProjectIds)&deep=$($DeleteSpn.IsPresent)&api-version=$ApiVersion" Invoke-RestMethod -Method Delete -Uri $url -Headers @{Authorization = "$(New-AdoAuthenticationToken $AdoAuthToken)" } -TimeoutSec 60 } function Remove-AdoVariableGroup { param([alias('u', 'Uri')][string]$AdoUri, [alias('t', 'Token', 'Pat')][string]$AdoAuthToken, $Organization, $Project, [alias('Id')]$aId, $ApiVersion = '5.1-preview.1') $url = "$(Get-AdoUri -Uri $AdoUri -Project $Project -Organization $Organization)/_apis/distributedtask/variablegroups/$($aId)?api-version=$($ApiVersion)" Invoke-RestMethod -Method Delete -Uri $url -Headers @{Authorization = "$(New-AdoAuthenticationToken $AdoAuthToken)" } -ContentType 'application/json' -TimeoutSec 60 } function Test-BuildDefinitionExists($BuildDefinitions, $BuildDefinitionName) { foreach ($def in $BuildDefinitions) { if ($def.name -eq $BuildDefinitionName) { return $true } } return $false } function Test-EndPointExists($EndPoints, $EndPointName) { foreach ($ep in $EndPoints) { if ($ep.name -eq $EndPointName) { return $true } } return $false } function Test-ProjectExists($Projects, $ProjectName) { foreach ($project in $Projects) { if ($project.name -eq $ProjectName) { return $true } } return $false } function Test-RepositoryExists($Repos, $RepoName) { foreach ($repo in $Repos) { if ($repo.name -eq $RepoName) { return $true } } return $false } function Test-VariableGroupExists($VariableGroups, $VariableGroupName) { foreach ($grp in $VariableGroups) { if ($grp.name -eq $VariableGroupName) { return $true } } return $false } function Update-AdoEnvironment { param([alias('u', 'Uri')][string]$AdoUri, [alias('t', 'Token', 'Pat')][string]$AdoAuthToken, $Organization, $Project, $EnvironmentId, $Name, $Description, $ApiVersion = '7.0') $url = "$(Get-AdoUri -Uri $AdoUri -Project $Project -Organization $Organization)/_apis/distributedtask/environments/$($EnvironmentId)?api-version=$($ApiVersion)" $body = @{ name = $Name description = $Description } $jsonBody = $body | ConvertTo-Json -Depth 5 $result = Invoke-RestMethod -Method Patch -Uri $url -Headers @{Authorization = "$(New-AdoAuthenticationToken $AdoAuthToken)" } -ContentType 'application/json' -TimeoutSec 60 -Body $jsonBody return $result.Value } function Update-AdoPipelineApproval { param( [ValidateNotNullOrEmpty()][alias('u', 'Uri')][string]$AdoUri, [ValidateNotNullOrEmpty()][alias('t', 'Token', 'Pat')][string]$AdoAuthToken, [ValidateNotNullOrEmpty()]$Organization, [ValidateNotNullOrEmpty()]$Project, [Parameter(Mandatory)][ValidateNotNull()]$ApprovalId, [ValidateSet('approved', 'canceled', 'skipped', 'rejected')]$Status, $Comment = '', $ApiVersion = '6.1-preview.1' ) $url = "$(Get-AdoUri -Uri $AdoUri -Project $Project -Organization $Organization)/_apis/pipelines/approvals?api-version=$($ApiVersion)" $approvals = @( @{ approvalId = $ApprovalId comment = $Comment status = $Status } ) $body = $approvals | ConvertTo-Json -AsArray -Depth 10 -Compress $result = Invoke-RestMethod -Method Patch -Uri $url -Body $body -Headers @{Authorization = "$(New-AdoAuthenticationToken $AdoAuthToken)" } -ContentType 'application/json' -TimeoutSec 60 return $result } function Update-AdoPipelinePermission { param([alias('u', 'Uri')][string]$AdoUri, [alias('t', 'Token', 'Pat')][string]$AdoAuthToken, $Organization, $Project, $ResourceType, $ResourceId, $PipelineId, $ApiVersion = '6.1-preview.1') $url = "$(Get-AdoUri -Uri $AdoUri -Project $Project -Organization $Organization)/_apis/pipelines/pipelinePermissions/$($ResourceType)/$($ResourceId)?api-version=$($ApiVersion)" $permission = @{ pipelines = @( @{ id = $PipelineId authorized = $true } ) } $body = $permission | ConvertTo-Json -Depth 10 -Compress $result = Invoke-RestMethod -Method Patch -Uri $url -Body $body -Headers @{Authorization = "$(New-AdoAuthenticationToken $AdoAuthToken)" } -ContentType 'application/json' -TimeoutSec 60 return $result } function Update-AdoPipelinePermissions { param([alias('u', 'Uri')][string]$AdoUri, [alias('t', 'Token', 'Pat')][string]$AdoAuthToken, $Organization, $Project, $Resources, $ApiVersion = '6.1-preview.1') $url = "$(Get-AdoUri -Uri $AdoUri -Project $Project -Organization $Organization)/_apis/pipelines/pipelinePermissions?api-version=$($ApiVersion)" $body = $Resources | ConvertTo-Json -Depth 10 -Compress $result = Invoke-RestMethod -Method Patch -Uri $url -Body $body -Headers @{Authorization = "$(New-AdoAuthenticationToken $AdoAuthToken)" } -ContentType 'application/json' -TimeoutSec 60 return $result } function Update-AdoPool { param([alias('u', 'Uri')][string]$AdoUri, [alias('t', 'Token', 'Pat')][string]$AdoAuthToken, $Organization, $PoolId, $Body, $ApiVersion = '7.0') $url = "$(Get-AdoUri -Uri $AdoUri -Organization $Organization)/_apis/distributedtask/pools/$($PoolId)?api-version=$($ApiVersion)" if ($null -eq $Body) { throw "No details (Body) information supplied in Update-AdoPool, see https://learn.microsoft.com/en-us/rest/api/azure/devops/distributedtask/pools/update?view=azure-devops-rest-7.0 for more details" } if (($Body -is [HashTable]) -or ($Body -is [PSCustomObject])) { $jsonBody = $Body | ConvertTo-Json -Depth 5 } else { $jsonBody = $Body } $result = Invoke-RestMethod -Method Patch -Uri $url -Headers @{Authorization = "$(New-AdoAuthenticationToken $AdoAuthToken)" } -ContentType 'application/json' -TimeoutSec 60 -Body $jsonBody return $result.Value } function Update-AdoVariableGroup { param([alias('u', 'Uri')][string]$AdoUri, [alias('t', 'Token', 'Pat')][string]$AdoAuthToken, $Organization, $Project, [alias('Id')]$aId, $Vars, $Type, $Name, $Description, $ApiVersion = '5.1-preview.1') $url = "$(Get-AdoUri -Uri $AdoUri -Project $Project -Organization $Organization)/_apis/distributedtask/variablegroups/$($aId)?api-version=$($ApiVersion)" $vars = @{ variables = $Vars name = $Name } if ($Type) { $vars.Add('type', $Type) | Out-Null } if ($Description) { $vars.Add('description', $Description) | Out-Null } $body = $vars | ConvertTo-Json -Depth 10 -Compress $result = Invoke-RestMethod -Method Put -Uri $url -Body $body -Headers @{Authorization = "$(New-AdoAuthenticationToken $AdoAuthToken)" } -ContentType 'application/json' -TimeoutSec 60 return $result.id } function Wait-AdoBuildCompleted { param( [ValidateNotNullOrEmpty()][alias('u', 'Uri')][string]$AdoUri, [ValidateNotNullOrEmpty()][alias('t', 'Token', 'Pat')][string]$AdoAuthToken, [ValidateNotNullOrEmpty()]$Organization, [ValidateNotNullOrEmpty()]$Project, [ValidateNotNull()]$Id, $TimeOut = 600, $ApiVersion = '5.0' ) $timer = [Diagnostics.Stopwatch]::StartNew() $finished = $false while (!$finished) { Start-Sleep -Seconds 5 $b = Get-AdoBuild -Token $AdoAuthToken -Organization $Organization -Project $Project -Id $Id if ($b.status -eq 'completed') { $finished = $true } elseif ($timer.Elapsed.TotalSeconds -gt $TimeOut) { Write-Warning "Build running to long $($Timeout)s, aborting wait for build completion" $finished = $true } [Console]::Write('.') } $timer.Stop() return $finished } <# .SYNOPSIS Get Azure Keyvault secrets and add them to token collection .DESCRIPTION Get secrets from Azure Keyvault and add them to token collection, use default logged-in account to Azure or try to get it from 'az cli' .PARAMETER Vault Name of the Azure KeyVault .PARAMETER Tokens Hashtable to add secrets to .PARAMETER SubscriptionId Azure Subscription ID .Example $Tokens = @{} Add-TokensFromAzureKeyVault -Vault 'MyVaultName' -Tokens $Tokens -SubscriptionId 'mySubscriptionId' #> function Add-TokensFromAzureKeyVault([Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$Vault, [Parameter(Mandatory = $true)]$Tokens, $SubscriptionId) { Write-Verbose "Add-TokensFromAzureKeyVault" Write-Verbose " Vault: $Vault" Write-Verbose " SubscriptionId: $SubscriptionId" function Add-Secret($Name, $Value) { if (!$Tokens.ContainsKey($Name)) { Write-Host "Adding secret $Name : ******* to Token Store" $Tokens.Add($Name, $Value) } } Connect-ToAzure if ($SubscriptionId) { Select-AzureDefaultSubscription -SubscriptionId $SubscriptionId } $warning = (Get-Item Env:\SuppressAzurePowerShellBreakingChangeWarnings -ErrorAction Ignore) -eq 'true' Set-Item Env:\SuppressAzurePowerShellBreakingChangeWarnings "true" try { try { $secrets = Get-AzKeyVaultSecret -VaultName $Vault foreach ($secret in $secrets) { $s = Get-AzKeyVaultSecret -VaultName $Vault -Name $secret.Name -ErrorAction Ignore if ($s) { #$pass = $s.SecretValue | ConvertFrom-SecureString -AsPlainText $cred = New-Object System.Management.Automation.PSCredential($secret.Name, $s.SecretValue) Add-Secret $secret.Name $cred # Secret alternative name support by using KeyVault Secrets Tags 'alt-name', additional second token is created with this name. # Use it to circumvent Azure KeyVault Secret name character restrictions if ($s.Tags -and $s.Tags.ContainsKey('alt-name')) { $cred = New-Object System.Management.Automation.PSCredential($s.Tags.'alt-name', $s.SecretValue) Add-Secret $s.Tags.'alt-name' $cred } } } } catch { # Generic KeyVaultErrorException if ($_.Exception.Message.Contains("Operation returned an invalid status code 'Forbidden'")) { Write-Warning "Check if your service principal '$(([Microsoft.Azure.Commands.Common.Authentication.Abstractions.AzureRmProfileProvider]::Instance.Profile).DefaultContext.Account.Id)' has Secret and Certificate Permissions (List,Get) for this KeyVault '$Vault'. Check the Vaults Access Policies" } Throw } } finally { Set-Item Env:\SuppressAzurePowerShellBreakingChangeWarnings $warning } } <# .SYNOPSIS Assert if logged-in to Azure with powershell Az modules .DESCRIPTION Assert if logged-in to Azure with powershell Az modules .Example Assert-AzureConnected #> function Assert-AzureConnected { Initialize-AzureModules $azProfile = [Microsoft.Azure.Commands.Common.Authentication.Abstractions.AzureRmProfileProvider]::Instance.Profile if (-not $azProfile.Accounts.Count) { Throw "Powershell Az error: Ensure you are logged in." } } <# .SYNOPSIS Connect to Azure with Powershell Az modules .DESCRIPTION Connect to Azure with Powershell Az modules, use 'az cli' as fallback to connect .PARAMETER Force Always re-authenticated when used .Example Connect-ToAzure #> function Connect-ToAzure([Switch]$Force) { Write-Verbose "Connect-ToAzure" # check already logged-in to Azure if (!(Test-AzureConnected) -or $Force.IsPresent) { # try to find logged-in user via az cli if installed Write-Verbose 'Connect to azure with Azure Cli configuration' try { $token = $(az account get-access-token --query accessToken --output tsv) $id = $(az account show --query user.name --output tsv) if ($token -and $id) { Connect-AzAccount -AccessToken $token -AccountId $id -Scope Process } } catch { # use default, already connected user in this session } } Assert-AzureConnected $azProfile = [Microsoft.Azure.Commands.Common.Authentication.Abstractions.AzureRmProfileProvider]::Instance.Profile Write-Verbose "Az Account: $($azProfile.DefaultContext.Account.Id)" Write-Verbose "Az Subscription: $($azProfile.DefaultContext.Subscription.Name) - $($azProfile.DefaultContext.Subscription.Id)" } <# .SYNOPSIS Initializes (install or import) the Azure Az modules into current Powershell session .DESCRIPTION Initializes (install or import) the Azure Az modules into current Powershell session .Example Initialize-AzureModules #> function Initialize-AzureModules { if ($Global:AzureInitialized) { return } if ($null -eq (Get-Module -ListAvailable 'Az')) { Write-Host "Installing Az modules, can take some time." Install-Module -Name Az -AllowClobber -Scope CurrentUser -Repository PSGallery -Force } else { if (!(Get-Module -Name Az)) { Import-Module Az -Scope local -Force } } if ($null -eq (Get-Module -ListAvailable 'Az.Accounts')) { Install-Module -Name Az.Accounts -AllowClobber -Scope CurrentUser -Repository PSGallery -Force } else { if (!(Get-Module -Name Az.Accounts)) { Import-Module Az.Accounts -Scope local -Force } } if ($null -eq (Get-Module -ListAvailable 'Az.KeyVault')) { Install-Module -Name Az.KeyVault -AllowClobber -Scope CurrentUser -Repository PSGallery -Force } else { if (!(Get-Module -Name Az.KeyVault)) { Import-Module Az.KeyVault -Scope local -Force } } $Global:AzureInitialized = $true } $Global:AzureInitialized = $false <# .SYNOPSIS Select the Azure default subscription .DESCRIPTION Select the Azure default subscription .PARAMETER SubscriptionId The Azure subscription Id .Example Select-AzureDefaultSubscription -SubscriptionId 'myid' #> function Select-AzureDefaultSubscription([Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$SubscriptionId) { Assert-AzureConnected $ctxList = Get-AzContext -ListAvailable foreach ($ctx in $ctxList) { if ($ctx.Subscription.Id -eq $SubscriptionId) { Write-Verbose "Select context: $($ctx.Name)" Select-AzContext -Name $ctx.Name return } } Throw "Azure subscription '$SubscriptionId' not found" } <# .SYNOPSIS Test if logged-in to Azure with powershell Az modules .DESCRIPTION Test if logged-in to Azure with powershell Az modules .Example Test-AzureConnected #> function Test-AzureConnected { Initialize-AzureModules try { $azProfile = [Microsoft.Azure.Commands.Common.Authentication.Abstractions.AzureRmProfileProvider]::Instance.Profile return !(-not $azProfile.Accounts.Count) } catch { return $false } } <# .SYNOPSIS Get tokens from config repository and add them to token collection .DESCRIPTION Get tokens from xml config repository and add them to token collection .PARAMETER ConfigPath Root path of the xml config files .PARAMETER Tokens HashTable to add tokens to .PARAMETER Env Token environment filter, filter the tokens by environment like local, develop, test etc... .Example $Tokens = @{} Add-TokensFromConfig -ConfigPath "$PSScriptRoot/config" -Tokens $Tokens -Env 'local' #> function Add-TokensFromConfig([Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$ConfigPath, [Parameter(Mandatory = $true)]$Tokens, [Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$Env, $Module) { function Add-Var($Nodes, $NameProp = 'name', $ValueProp = 'value', $Prefix) { foreach ($node in $Nodes) { $name = '' $value = $null if (Test-PSProperty $node $NameProp) { $name = $node."$NameProp" } if (Test-PSProperty $node $ValueProp) { $value = Get-PSPropertyValue $node "$ValueProp" if ($value -and $value.StartsWith('$')) { $value = Invoke-Expression "Write-Output `"$($value)`"" } } $pre = $Prefix if ($node.LocalName -eq 'node') { if ($node.ParentNode.ParentNode.name -ne $Env) { continue } } elseif ($node.LocalName -eq 'system-user') { if ($node.ParentNode.LocalName -eq 'application') { $name = '' $pre = "$Prefix$($node.ParentNode.name)" } } if ($pre) { $kn = "$pre$name" Write-Host "Adding variable '$kn' = '$value' to Token Store" if (!$Tokens.ContainsKey($kn)) { $Tokens.Add($kn, $value) } } else { if (!$Tokens.ContainsKey($name)) { Write-Host "Adding variable '$name' = '$value' to Token Store" $Tokens.Add($name, $value) } } } } function Add-Modules($Nodes, $Module) { foreach ($node in $Nodes) { if ($Module -and ($node.name -ne $Module)) { continue } Write-Host "Adding module '$($node.name)' to Token Store" $Tokens.Add("module-$($node.name)", $node.name) $Tokens.Add("module-$($node.name)-role", (Get-PSPropertyValue $node role)) $Tokens.Add("module-$($node.name)-depends", (Get-PSPropertyValue $node depends)) $Tokens.Add("module-$($node.name)-folder", (Get-PSPropertyValue $node folder)) $nodeApps = $node.SelectNodes(".//application") foreach ($nodeApp in $NodeApps) { Write-Host "Adding module '$($node.name)' application '$($nodeApp.name)' to Token Store" $Tokens.Add("module-$($node.name)-application-$($nodeApp.name)", $nodeApp.name) $Tokens.Add("module-$($node.name)-application-$($nodeApp.name)-type", (Get-PSPropertyValue $nodeApp type)) $Tokens.Add("module-$($node.name)-application-$($nodeApp.name)-role", (Get-PSPropertyValue $nodeApp role)) $Tokens.Add("module-$($node.name)-application-$($nodeApp.name)-service", (Get-PSPropertyValue $nodeApp service)) $Tokens.Add("module-$($node.name)-application-$($nodeApp.name)-exe", (Get-PSPropertyValue $nodeApp exe)) $Tokens.Add("module-$($node.name)-application-$($nodeApp.name)-dotnet-version", (Get-PSPropertyValue $nodeApp 'dotnet-version')) if (!$Tokens.ContainsKey("application-$($nodeApp.name)")) { $Tokens.Add("application-$($nodeApp.name)", $($node.name)) } else { Write-Warning "Duplicate application name '$("application-$($nodeApp.name)")' found" } } } } $modules = @() Get-ChildItem "$ConfigPath\*.xml" -Recurse | ForEach-Object { $doc = [xml] (Get-Content $_.FullName) $nodes = $doc.SelectNodes("//variable[@environment='$Env' or not(@environment)]") Add-Var $nodes $nodes = $doc.SelectNodes("//node") if ($nodes.Count -gt 0) { Add-Var $nodes -NameProp 'role' -ValueProp 'name' -Prefix 'node-' } $nodes = $doc.SelectNodes("//service[@environment='$Env' or not(@environment)]") if ($nodes.Count -gt 0) { Add-Var $nodes -Prefix 'service-' Add-Var $nodes -Prefix 'service-cert-hash-' -ValueProp 'cert-hash' Add-Var $nodes -Prefix 'service-cert-name-' -ValueProp 'cert-name' Add-Var $nodes -Prefix 'service-type-' -ValueProp 'type' Add-Var $nodes -Prefix 'service-healthcheck-' -ValueProp 'healthcheck' Add-Var $nodes -Prefix 'service-healthcheck-type-' -ValueProp 'healthcheck-type' Add-Var $nodes -Prefix 'service-healthcheck-interval-' -ValueProp 'healthcheck-interval' } $nodes = $doc.SelectNodes("//system-user[@environment='$Env' or not(@environment)]") if ($nodes.Count -gt 0) { Add-Var $nodes -NameProp 'system-user' -ValueProp 'name' -Prefix 'system-user-' } $nodes = $doc.SelectNodes("//module") if ($nodes.Count -gt 0) { Add-Modules -Nodes $nodes -Module $Module $nodes | ForEach-Object { $modules += $_.name } } $envNode = $doc.SelectSingleNode("//environment[@name='$Env']") if ($envNode) { $Tokens.Add('env-name', $envNode.'name') $Tokens.Add('env-group', (Get-PSPropertyValue $envNode 'group')) $Tokens.Add('env-name-short', (Get-PSPropertyValue $envNode 'name-short')) $Tokens.Add('env-name-suffix', (Get-PSPropertyValue $envNode 'name-suffix')) $Tokens.Add('env-type', (Get-PSPropertyValue $envNode 'type')) $Tokens.Add('env-active', (Get-PSPropertyValue $envNode 'active')) $Tokens.Add('env-domain', (Get-PSPropertyValue $envNode 'domain')) $Tokens.Add('env-domain-full', (Get-PSPropertyValue $envNode 'domain-full')) $Tokens.Add('env-domain-description', (Get-PSPropertyValue $envNode 'description')) $Tokens.Add('env-domain-owner', (Get-PSPropertyValue $envNode 'owner')) $Tokens.Add('env-domain-notes', (Get-PSPropertyValue $envNode 'notes')) $Tokens.Add('env-ps-remote-user', (Get-PSPropertyValue $envNode 'ps-remote-user')) $Tokens.Add('env-subscription-id', (Get-PSPropertyValue $envNode 'subscription-id')) $Tokens.Add('env-vault', (Get-PSPropertyValue $envNode 'vault')) } } $Tokens.Add('modules', $modules) } <# .SYNOPSIS Convert the tokens in file to their actual values .DESCRIPTION Convert the tokens in file to their actual values .PARAMETER FileName Name of the file to convert .PARAMETER PrefixToken Token prefix .PARAMETER SuffixToken Token suffix .PARAMETER DestFileName File name of converted file .PARAMETER ShowTokensUsed Switch to echo tokens replaced .PARAMETER SecondPass Switch to signal that same file is used in multiple conversions .PARAMETER Tokens Hashtable to add tokens to .Example $Tokens = @{} Add-TokensFromConfig -ConfigPath "$PSScriptRoot/config" -Tokens $Tokens -Env 'local' Get-ChildItem .\$ConfigLocation\*.* | ForEach-Object { $destFile = Join-Path $ArtifactsLocation $_.Name Convert-TokensInFile -FileName $_.Fullname -DestFileName $destFile -Tokens $Tokens } #> function Convert-TokensInFile([Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$FileName, $PrefixToken = '__', $SuffixToken = '__', $DestFileName, [Switch]$ShowTokensUsed, [Switch]$SecondPass, $Tokens) { if (!$DestFileName) { $DestFileName = $FileName } if (Test-Path $FileName) { $regex = [regex] "${PrefixToken}((?:(?!${SuffixToken}).)*)${SuffixToken}" $content = [System.IO.File]::ReadAllText($FileName); if (!$Tokens) { $Tokens = @{} } $script:cnt = 0 $callback = { param([System.Text.RegularExpressions.Match] $Match) $value = $Match.Groups[1].Value # check env first $newTokenValue = [Environment]::GetEnvironmentVariable($value) if ($null -eq $newTokenValue) { if ($Tokens.ContainsKey($value)) { $v = $Tokens[$value] if ($null -eq $v) { $v = '' } if ($v -is [PSCredential]) { $newTokenValue = $v.GetNetworkCredential().Password } else { $newTokenValue = $v.ToString() # detect expression in variable if ($newTokenValue.StartsWith('$')) { $newTokenValue = Invoke-Expression "Write-Output `"$($newTokenValue)`"" } } } } if ($null -eq $newTokenValue) { $script:HasReplaceVarErrors = $true; Write-Warning "Token not found in replace: '$value'" return "" } $script:cnt++ if ($ShowTokensUsed.IsPresent -or ($Global:VerbosePreference -eq 'Continue')) { Write-Host "Replacing token '$value' with '$newTokenValue'" } return $newTokenValue } $content = $regex.Replace($content, $callback) New-Item -ItemType Directory (Split-Path -Path $DestFileName) -Force -ErrorAction Ignore | Out-Null Set-Content -Path $DestFileName -Value $content -Encoding UTF8 if ( ($Global:VerbosePreference -eq 'Continue') -or ( ($script:cnt -gt 0) -and $ShowTokensUsed.IsPresent ) ) { if ($SecondPass.IsPresent -and ($script:cnt -eq 0) ) { Write-Host "$($script:cnt) Tokens replaced in '$FileName'" } else { Write-Host "$($script:cnt) Tokens replaced in '$FileName'" } } } else { Throw "Convert-TokensInFile error: file not found '$FileName'" } } function Invoke-CloneGitRepo($Root, $RepoUrl, [switch]$Reset) { New-Item -Path $Root -ItemType Directory -ErrorAction Ignore -Force | Out-Null $uri = [System.Uri]$RepoUrl if ($uri.Segments.Count -gt 0) { $defBranch = 'master' $folder = $uri.Segments[$uri.Segments.Count - 1] $repoLocation = Join-Path $Root $folder Write-Host "Repo: $repoLocation" if (Test-Path $repoLocation) { Push-Location -Path $repoLocation try { $output = &git status if ($output) { Write-Host $output $hasChanges = !$output.Contains("nothing to commit, working tree clean") if ($reset.IsPresent) { &git reset --hard &git clean -xdf $hasChanges = $false } if (!$hasChanges) { #&git fetch Invoke-Git "fetch" # detect main branch name #&git branch --remotes --list '*/HEAD' $br = Invoke-Git "branch --remotes --list '*/HEAD'" if ($null -ne $br -and $br.Contains('/main')) { $defBranch = 'main' } #&git checkout master Invoke-Git "checkout $defBranch" #&git pull Invoke-Git "pull" Write-Host -ForegroundColor Green " Repo $($r.Name) switched to $defBranch and updated." } else { Write-Host -ForegroundColor Yellow " Repo $($r.Name) contains changes." } } else { Write-Host -ForegroundColor Yellow " Repo $($r.Name) not found, no .git folder? or no repo?" } } finally { Pop-Location } } else { Write-Host -ForegroundColor Yellow " Repo $($r.Name) not found locally. Cloning..." Push-Location $Root try { #&git clone $r.RemoteUrl Invoke-Git "clone $($RepoUrl)" $folder = $uri.Segments[$uri.Segments.Count - 1] $repoLocation = Join-Path $Root $folder if ( (Test-Path $repoLocation) -and ($r.DefaultBranch -ne '')) { Push-Location $repoLocation try { # detect main branch name #&git branch --remotes --list '*/HEAD' $br = Invoke-Git "branch --remotes --list '*/HEAD'" if ($null -ne $br -and $br.Contains('/main')) { $defBranch = 'main' } #&git checkout master Invoke-Git "checkout $defBranch" } finally { Pop-Location } } else { Write-Warning "Repo $($r.Name) not cloned see error above" } } finally { Pop-Location } } } } function Invoke-CloneGitRepos($Root, $Repos, [switch]$Reset) { # use repo's from Get-AdoAllRepositories # use git.exe for cloning $repos = $Repos | Sort-Object -Property Name, Project | Where-Object { $_ } foreach ($r in $repos) { Invoke-CloneGitRepo -Root $Root -RepoUrl $r.RemoteUrl -Reset:$Reset } } function Invoke-Git($Command, $WorkingDirectory) { $invokeErrors = New-Object System.Collections.ArrayList 256 $currentEncoding = [Console]::OutputEncoding $errorCount = $global:Error.Count $prevErrorActionPreference = $ErrorActionPreference try { $ErrorActionPreference = 'Continue' $LastExitCode = 0 Invoke-Expression "git $Command" *>&1 } finally { if ($currentEncoding.IsSingleByte) { [Console]::OutputEncoding = $currentEncoding } $ErrorActionPreference = $prevErrorActionPreference if ($global:Error.Count -gt $errorCount) { $numNewErrors = $global:Error.Count - $errorCount $invokeErrors.InsertRange(0, $global:Error.GetRange(0, $numNewErrors)) if ($invokeErrors.Count -gt 256) { $invokeErrors.RemoveRange(256, ($invokeErrors.Count - 256)) } $global:Error.RemoveRange(0, $numNewErrors) for ($i = 0; $i -lt $numNewErrors; $i++) { Write-Host "Git result $LastExitCode $($invokeErrors[$i].Exception.Message)" } } } } function New-SecureStringStorage([ValidateNotNullOrEmpty()]$String) { return [SecureStringStorage]::New($String) } function Read-ObjectFromJsonFile($Path, $Depth = 10) { if (Test-Path $Path) { $object = Get-Content -Path $Path | ConvertFrom-Json -Depth $Depth } else { $object = [PSCustomObject]@{} } function Set-Props($Object) { foreach ($prop in $Object.PsObject.Properties) { if ($prop.Value -is [HashTable]) { if ($prop.Value.ContainsKey('TypeName') -and ($prop.Value.TypeName -eq 'SecureStringStorage') ) { $prop.Value = [SecureStringStorage]$prop.Value } } elseif ($prop.Value -is [PSCustomObject]) { if ($prop.Value.TypeName -and ($prop.Value.TypeName -eq 'SecureStringStorage') ) { $prop.Value = [SecureStringStorage]$prop.Value } else { Set-Props $prop.Value } } } } # fix SecureString references Set-Props $object return $object } function Save-ObjectToJsonFile($Path, $Object, $Depth = 10) { function Set-Props($Object) { foreach ($prop in $Object.PsObject.Properties) { if ($prop.Value -is [SecureString]) { $prop.Value = [SecureStringStorage]$prop.Value } } } # fix SecureString references Set-Props $object $Object | ConvertTo-Json -Depth $Depth | Set-Content -Path $Path -Force } class SecureStringStorage { hidden [String] $String [String] $TypeName = 'SecureStringStorage' SecureStringStorage($String) { if (($String -is [PSCustomObject]) -and ($String.TypeName -eq 'SecureStringStorage') ) { $this.String = $String.String } elseif (($String -is [SecureString])) { $this.String = $String | ConvertFrom-SecureString } else { $this.String = ConvertTo-SecureString -String $String -AsPlainText -Force | ConvertFrom-SecureString } } [string]ToString() { return $this.String } [SecureString]GetSecureString() { $secureString = ConvertTo-SecureString -String $this.String -Force return $secureString } [string]GetPlainString() { $plain = ConvertTo-SecureString -String $this.String -Force | ConvertFrom-SecureString -AsPlainText return $plain } } function Test-IsSecureStringStorageObject([ValidateNotNull()]$Object) { return ($Object -is [SecureStringStorage]) } <# .SYNOPSIS Send a msg to Slack Channel .DESCRIPTION Send a msg to Slack Channel via the Incoming Webhook integration App. See in slack: Browse Apps / Custom Integrations / Incoming WebHook or see notes below .PARAMETER Msg The message to send .PARAMETER Channel The Channel to send to .PARAMETER Username The user of the message .PARAMETER IconUrl The url of the icon to display in the message, otherwise use emoji .PARAMETER Emoji The emoji to use like ':ghost:' or ':bom:' see slack documentation for more Emoji. Use IconUrl for custom emoji .PARAMETER AsUser Send msg as this User .PARAMETER Token The Incoming WebHook Token .PARAMETER Attachments The json structured attachment. See Slack documentation like $attachment = @{ fallback = $msg pretext = "Sample message: <http://url_to_task|Test out Slack message attachments>" color = "danger" # good, warning fields = @( @{ title = "[Alert]]" value = "This is much easier than I thought it would be. <https://www.sample.com/logo.png>|Logo" short = "false" } ) } .Example Send-ToSlack -m 'Hello' -c 'TestChannel' -u 'me' -e ':bomb:' -t 'mytoken...' .NOTES for documentation about configuring Slack/Acquire token see https://api.slack.com/messaging/webhooks or https://api.slack.com/legacy/custom-integrations #> function Send-ToSlack ([alias('m')]$Msg, [alias('c')]$Channel, [alias('u')]$Username, [alias('iu')]$IconUrl, [alias('e')]$Emoji, [alias('a')][Switch]$AsUser, [alias('t')]$Token, $Attachments) { $slackUri = "https://hooks.slack.com/services/$Token" if ($Channel -and !($Channel.StartsWith('@'))) { $channel = "#$Channel" } else { $channel = $Channel } $body = @{ channel = $channel username = $Username text = $Msg icon_url = $IconUrl icon_emoji = $Emoji } if ($null -eq $Emoji) { $body.Remove('icon_emoji') } if ($null -eq $IconUrl ) { $body.Remove('icon_url') } if ($Attachments) { [void]$body.Add('attachments', $Attachments) } try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $response = Invoke-RestMethod -Uri $slackUri -Method Post -Body ($body | ConvertTo-Json -Compress -Depth 10) -ContentType 'application/json' } catch { Throw "Send-ToSlack error: $($_.Exception.Message)" } if ($response -ne 'ok') { Throw "Send-ToSlack error: $($response)" } } function Convert-ObjectToHashTable([object[]]$Object) { foreach ($o in $Object) { $output = @{} $o | Get-Member -MemberType *Property | ForEach-Object { $value = $o.($_.name) if ($value -is [PSCustomObject]) { $value = Convert-ObjectToHashTable -Object $value } $output.($_.name) = $value } $output } } function ConvertTo-TitleCase([alias('t')]$Text, [alias('c')][string] $Culture = 'en-US') { $c = New-Object System.Globalization.CultureInfo($Culture) return ($c.TextInfo.ToTitleCase($Text)) } function Copy-ObjectPropertyValues($FromObject, $ToObject, [switch]$Deep) { if ($Deep.IsPresent) { throw "Deep copy properties not supported in Copy-ObjectPropertyValues" } foreach ($prop in $FromObject.PSObject.Properties) { if (Test-PSProperty $ToObject $prop.Name) { $ToObject."$($prop.Name)" = $prop.Value } } return $ToObject } function Get-ArrayItemCount($Array) { if ($Array -is [array] -or $Array -is [System.Collections.ArrayList]) { return $Array.Count } elseif ($Array) { return 1 } else { return 0; } } <# .SYNOPSIS Gets an environment variable .DESCRIPTION Gets an environment variable, supports empty environment variable and case sensitivity .PARAMETER Name Name of the environment variable .PARAMETER Default Default value of the environment variable when not found .PARAMETER IgnoreCasing Ignores casing by checking ToLower and ToUpper variants #> function Get-EnvironmentVar { param([alias('n')][string]$Name, [alias('d')][string]$Default = $null, [switch]$IgnoreCasing) $r = [Environment]::GetEnvironmentVariable($Name); if ($null -eq $r -and $IgnoreCasing.IsPresent) { $r = [Environment]::GetEnvironmentVariable($Name.ToLower()); if ($null -eq $r) { $r = [Environment]::GetEnvironmentVariable($Name.ToUpper()); } } if ($r -eq [char]0x2422) { $r = '' } if (($r -eq '') -or ($null -eq $r)) { $r = $Default } if ($r -eq '') { return $null } else { return $r } } <# .SYNOPSIS Gets property value from Object .DESCRIPTION Gets property value from Object, first checks if property exists, if not returns default value. In Set-StrictMode -Latest every property used is checked for existence --> runtime exception .PARAMETER Object Object to get property value from .PARAMETER Name Name of property .PARAMETER Default Default value if property does not exists #> function Get-PSPropertyValue { param([alias('o')][object]$Object, [alias('p')][string]$Name, [alias('d')]$Default = '') if (Test-PSProperty -o $Object -p $Name -Exact) { return $Object."$Name" } else { return $Default } } function Get-RandomString($Length = 20) { $chars = 65..90 + 97..122 $chars += 48..57 $s = $null Get-Random -Count $Length -Input ($chars) | ForEach-Object { $s += [char]$_ } return $s.ToString() } <# .SYNOPSIS Get an Unique Id, default it's a guid .DESCRIPTION Get an Unique Id, default it's a guid. Decrease size when you need shorter unique id but sacrifice on id accuracy / collision possibility .PARAMETER Size Size of Id, default 32 characters #>function Get-UniqueId([ValidateRange(6, 32)][Int]$Size = 32) { return ([Guid]::NewGuid().ToString('n')).SubString(0, $Size) } function Invoke-RetryBlock([alias('c', 'Code')][ScriptBlock]$ScriptBlock, [alias('r', 'Retry')][int]$RetryCnt = 3, [alias('w', 'WaitSec', 'DelaySec')][int]$RetryDelaySec = 5, [switch][bool]$NoException, [alias('Msg', 'm')]$Message, [alias('mn')]$MutexName = $null) { $stopLoop = $false [int]$retryCount = 1 $m = $null if ($MutexName) { $m = Wait-OnMutex -Name $MutexName } try { do { try { Invoke-Command -ScriptBlock $ScriptBlock -ErrorAction Stop $stopLoop = $true } catch { if ($retryCount -ge $RetryCnt) { $stopLoop = $true if (!($NoException.IsPresent)) { Write-Verbose "Retry block failed after $RetryCnt retries with delay of $RetryDelaySec"; Throw } } else { $retryCount ++ if ($Message) { Write-Host "$Message retry '$retryCount' delay $($RetryDelaySec)s" } else { Write-Verbose "In retry '$retryCount' with delay of $($RetryDelaySec)s" } Write-Verbose $_.Exception.Message Start-Sleep -Seconds $RetryDelaySec } } } while (!$stopLoop) } finally { if ($m) { Exit-Mutex $m Close-Mutex $m } } } function Merge-Objects($Objects) { $props = @{} foreach ($o in $Objects) { foreach ($property in $o.PSObject.Properties) { if ($props.ContainsKey($property.Name)) { # ignore duplicates } else { [void]$props.Add($property.Name, $property.value) } } } return [PSCustomObject]$props } <# .SYNOPSIS Checks if property exists on Object .DESCRIPTION Checks if property exists on Object. In Set-StrictMode -Latest every property used is checked for existence --> runtime exception .PARAMETER Object Object to test for property .PARAMETER Name Name of property .PARAMETER Exact Use exact match in property name checking #> function Test-PSProperty { param([alias('o')][object]$Object, [alias('p')][string]$Name, [alias('e')][switch]$Exact) try { foreach ($prop in $Object.PSObject.Properties) { if ($Exact.IsPresent) { if ($prop.Name -eq $Name) { return $true } elseif ($prop.Name -match 'Keys') { if ($prop.Value -eq $Name) { return $true } } } else { if ($prop.Name -match $Name) { return $true } elseif ($prop.Name -match 'Keys') { if ($prop.Value -match $Name) { return $true } } } } } catch { # not found } return $false } <# .SYNOPSIS Write script header to console host .DESCRIPTION Write script header to console host, useful for logging and debugging your PowerShell scripts .PARAMETER Title Script title .PARAMETER Invocation Invocation to use like $PSCommandPath .PARAMETER ExtraParams Extra parameters you want to write to console .PARAMETER ShowHost Show also console details .EXAMPLE Write-ScriptHeader -Title 'My Script' -Invocation $PSCommandPath -ExtraParams @{MyVar='hello'} -ShowHost .REMARKS $PSCommandPath contains information about the invoker or calling script, not the current script #> function Write-Header($Title, $Invocation, $ExtraParams, [switch]$ShowHost) { $header = '' if ($Invocation) { if ($Invocation -is [System.Management.Automation.InvocationInfo]) { $header = $Invocation.MyCommand.Name } else { $header = Split-Path $Invocation -Leaf -ErrorAction Ignore } } Write-Host ''.PadRight(78, '-'); Write-Host "$Title $header" Write-Host ''.PadRight(78, '-'); if ($Invocation) { Write-Host 'Parameters' Write-Params $Invocation } if ($ExtraParams) { if ($ExtraParams.Count -gt 0) { if ($Invocation) { Write-Host '' } Write-Host 'More Parameters' foreach ($k in $ExtraParams.GetEnumerator()) { $n = $k.Name Write-Host "$($n.PadLeft(20)): $($k.value)" } } } if ($ShowHost.IsPresent) { Write-Host '' Write-HostDetails } if ($Invocation -or $ExtraParams -or $ShowHost.IsPresent) { Write-Host ''.PadRight(78, '-'); } } function Write-HostDetails { Write-Host 'Host details' Write-Host " Time: $(Get-Date -Format s)" Write-Host " Computer: $([Environment]::MachineName)/$(hostname)" Write-Host " WhoAmI: $([Environment]::UserName)" Write-Host " Powershell: $($PSVersionTable.PsVersion)" Write-Host " OS: $([Environment]::OSVersion.VersionString)" Write-Host " Culture: $(Get-Culture)" Write-Host " Current Folder: $(Get-Location)" } <# .SYNOPSIS Write parameter names & values to console host .DESCRIPTION Write or echo's parameter names and values to console host, useful for logging and debugging your PowerShell scripts .PARAMETER Invocation Invocation to use like $PSCommandPath or $MyInvocation .EXAMPLE Write-Params -Invocation $PSCommandPath .REMARKS $PSCommandPath contains information about the invoker or calling script, not the current script $MyInvocation contains information about current context #> function Write-Params($Invocation) { if ($Invocation) { $ivo = $Invocation } else { try { $ivo = $PSCmdlet.MyInvocation } catch { return; } } try { $i = Get-Command -Name $ivo -ErrorAction SilentlyContinue if (!$i) { $i = Get-Command -Name $ivo.InvocationName -ErrorAction SilentlyContinue } if ($i) { $parameterList = $i.ParameterSets.Parameters if (!$parameterList) { return; } } else { return } } catch { Write-Host 'Unable to write parameters' return; } # detect unwrap if ($parameterList -is [System.Management.Automation.CommandParameterInfo]) { $list = @($parameterList) } else { $list = $parameterList } foreach ($key in $list.GetEnumerator()) { $var = Get-Variable -Name $key.Name -ErrorAction SilentlyContinue; if ($var) { $n = $var.name foreach ($alias in $key.Aliases) { if ($var.name -like "*$alias*") { $n = $alias } } if ($n -in 'pw', 'pwd', 'password', 'secret') { Write-Host "$($n.PadLeft(20)): *************" } else { if ($var.value -and $var.value -is [HashTable]) { Write-Host "$($n.PadLeft(20)) " foreach ($item in $var.Value.Keys) { Write-Host "$($item.PadLeft(24)): $($var.value[$item])" } } else { Write-Host "$($n.PadLeft(20)): $($var.value)" } } } } } # SIG # Begin signature block # MIIr0gYJKoZIhvcNAQcCoIIrwzCCK78CAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB # gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR # AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUI9jUdT46P2B6vgGoB6n/iFKN # YGuggiUPMIIFbzCCBFegAwIBAgIQSPyTtGBVlI02p8mKidaUFjANBgkqhkiG9w0B # AQwFADB7MQswCQYDVQQGEwJHQjEbMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVy # MRAwDgYDVQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEh # MB8GA1UEAwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTIxMDUyNTAwMDAw # MFoXDTI4MTIzMTIzNTk1OVowVjELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1NlY3Rp # Z28gTGltaXRlZDEtMCsGA1UEAxMkU2VjdGlnbyBQdWJsaWMgQ29kZSBTaWduaW5n # IFJvb3QgUjQ2MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjeeUEiIE # JHQu/xYjApKKtq42haxH1CORKz7cfeIxoFFvrISR41KKteKW3tCHYySJiv/vEpM7 # fbu2ir29BX8nm2tl06UMabG8STma8W1uquSggyfamg0rUOlLW7O4ZDakfko9qXGr # YbNzszwLDO/bM1flvjQ345cbXf0fEj2CA3bm+z9m0pQxafptszSswXp43JJQ8mTH # qi0Eq8Nq6uAvp6fcbtfo/9ohq0C/ue4NnsbZnpnvxt4fqQx2sycgoda6/YDnAdLv # 64IplXCN/7sVz/7RDzaiLk8ykHRGa0c1E3cFM09jLrgt4b9lpwRrGNhx+swI8m2J # mRCxrds+LOSqGLDGBwF1Z95t6WNjHjZ/aYm+qkU+blpfj6Fby50whjDoA7NAxg0P # OM1nqFOI+rgwZfpvx+cdsYN0aT6sxGg7seZnM5q2COCABUhA7vaCZEao9XOwBpXy # bGWfv1VbHJxXGsd4RnxwqpQbghesh+m2yQ6BHEDWFhcp/FycGCvqRfXvvdVnTyhe # Be6QTHrnxvTQ/PrNPjJGEyA2igTqt6oHRpwNkzoJZplYXCmjuQymMDg80EY2NXyc # uu7D1fkKdvp+BRtAypI16dV60bV/AK6pkKrFfwGcELEW/MxuGNxvYv6mUKe4e7id # FT/+IAx1yCJaE5UZkADpGtXChvHjjuxf9OUCAwEAAaOCARIwggEOMB8GA1UdIwQY # MBaAFKARCiM+lvEH7OKvKe+CpX/QMKS0MB0GA1UdDgQWBBQy65Ka/zWWSC8oQEJw # IDaRXBeF5jAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zATBgNVHSUE # DDAKBggrBgEFBQcDAzAbBgNVHSAEFDASMAYGBFUdIAAwCAYGZ4EMAQQBMEMGA1Ud # HwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29tL0FBQUNlcnRpZmlj # YXRlU2VydmljZXMuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggrBgEFBQcwAYYYaHR0 # cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBDAUAA4IBAQASv6Hvi3Sa # mES4aUa1qyQKDKSKZ7g6gb9Fin1SB6iNH04hhTmja14tIIa/ELiueTtTzbT72ES+ # BtlcY2fUQBaHRIZyKtYyFfUSg8L54V0RQGf2QidyxSPiAjgaTCDi2wH3zUZPJqJ8 # ZsBRNraJAlTH/Fj7bADu/pimLpWhDFMpH2/YGaZPnvesCepdgsaLr4CnvYFIUoQx # 2jLsFeSmTD1sOXPUC4U5IOCFGmjhp0g4qdE2JXfBjRkWxYhMZn0vY86Y6GnfrDyo # XZ3JHFuu2PMvdM+4fvbXg50RlmKarkUT2n/cR/vfw1Kf5gZV6Z2M8jpiUbzsJA8p # 1FiAhORFe1rYMIIFjTCCBHWgAwIBAgIQDpsYjvnQLefv21DiCEAYWjANBgkqhkiG # 9w0BAQwFADBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkw # FwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1 # cmVkIElEIFJvb3QgQ0EwHhcNMjIwODAxMDAwMDAwWhcNMzExMTA5MjM1OTU5WjBi # MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 # d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg # RzQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAi # MGkz7MKnJS7JIT3yithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnny # yhHS5F/WBTxSD1Ifxp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE # 5nQ7bXHiLQwb7iDVySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm # 7nfISKhmV1efVFiODCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5 # w3jHtrHEtWoYOAMQjdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsD # dV14Ztk6MUSaM0C/CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1Z # XUJ2h4mXaXpI8OCiEhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS0 # 0mFt6zPZxd9LBADMfRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hk # pjPRiQfhvbfmQ6QYuKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m8 # 00ERElvlEFDrMcXKchYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+i # sX4KJpn15GkvmB0t9dmpsh3lGwIDAQABo4IBOjCCATYwDwYDVR0TAQH/BAUwAwEB # /zAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wHwYDVR0jBBgwFoAUReui # r/SSy4IxLVGLp6chnfNtyA8wDgYDVR0PAQH/BAQDAgGGMHkGCCsGAQUFBwEBBG0w # azAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEMGCCsGAQUF # BzAChjdodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVk # SURSb290Q0EuY3J0MEUGA1UdHwQ+MDwwOqA4oDaGNGh0dHA6Ly9jcmwzLmRpZ2lj # ZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcmwwEQYDVR0gBAowCDAG # BgRVHSAAMA0GCSqGSIb3DQEBDAUAA4IBAQBwoL9DXFXnOF+go3QbPbYW1/e/Vwe9 # mqyhhyzshV6pGrsi+IcaaVQi7aSId229GhT0E0p6Ly23OO/0/4C5+KH38nLeJLxS # A8hO0Cre+i1Wz/n096wwepqLsl7Uz9FDRJtDIeuWcqFItJnLnU+nBgMTdydE1Od/ # 6Fmo8L8vC6bp8jQ87PcDx4eo0kxAGTVGamlUsLihVo7spNU96LHc/RzY9HdaXFSM # b++hUD38dglohJ9vytsgjTVgHAIDyyCwrFigDkBjxZgiwbJZ9VVrzyerbHbObyMt # 9H5xaiNrIv8SuFQtJ37YOtnwtoeW/VvRXKwYw02fc7cBqZ9Xql4o4rmUMIIGGjCC # BAKgAwIBAgIQYh1tDFIBnjuQeRUgiSEcCjANBgkqhkiG9w0BAQwFADBWMQswCQYD # VQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMS0wKwYDVQQDEyRTZWN0 # aWdvIFB1YmxpYyBDb2RlIFNpZ25pbmcgUm9vdCBSNDYwHhcNMjEwMzIyMDAwMDAw # WhcNMzYwMzIxMjM1OTU5WjBUMQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGln # byBMaW1pdGVkMSswKQYDVQQDEyJTZWN0aWdvIFB1YmxpYyBDb2RlIFNpZ25pbmcg # Q0EgUjM2MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAmyudU/o1P45g # BkNqwM/1f/bIU1MYyM7TbH78WAeVF3llMwsRHgBGRmxDeEDIArCS2VCoVk4Y/8j6 # stIkmYV5Gej4NgNjVQ4BYoDjGMwdjioXan1hlaGFt4Wk9vT0k2oWJMJjL9G//N52 # 3hAm4jF4UjrW2pvv9+hdPX8tbbAfI3v0VdJiJPFy/7XwiunD7mBxNtecM6ytIdUl # h08T2z7mJEXZD9OWcJkZk5wDuf2q52PN43jc4T9OkoXZ0arWZVeffvMr/iiIROSC # zKoDmWABDRzV/UiQ5vqsaeFaqQdzFf4ed8peNWh1OaZXnYvZQgWx/SXiJDRSAolR # zZEZquE6cbcH747FHncs/Kzcn0Ccv2jrOW+LPmnOyB+tAfiWu01TPhCr9VrkxsHC # 5qFNxaThTG5j4/Kc+ODD2dX/fmBECELcvzUHf9shoFvrn35XGf2RPaNTO2uSZ6n9 # otv7jElspkfK9qEATHZcodp+R4q2OIypxR//YEb3fkDn3UayWW9bAgMBAAGjggFk # MIIBYDAfBgNVHSMEGDAWgBQy65Ka/zWWSC8oQEJwIDaRXBeF5jAdBgNVHQ4EFgQU # DyrLIIcouOxvSK4rVKYpqhekzQwwDgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQI # MAYBAf8CAQAwEwYDVR0lBAwwCgYIKwYBBQUHAwMwGwYDVR0gBBQwEjAGBgRVHSAA # MAgGBmeBDAEEATBLBgNVHR8ERDBCMECgPqA8hjpodHRwOi8vY3JsLnNlY3RpZ28u # Y29tL1NlY3RpZ29QdWJsaWNDb2RlU2lnbmluZ1Jvb3RSNDYuY3JsMHsGCCsGAQUF # BwEBBG8wbTBGBggrBgEFBQcwAoY6aHR0cDovL2NydC5zZWN0aWdvLmNvbS9TZWN0 # aWdvUHVibGljQ29kZVNpZ25pbmdSb290UjQ2LnA3YzAjBggrBgEFBQcwAYYXaHR0 # cDovL29jc3Auc2VjdGlnby5jb20wDQYJKoZIhvcNAQEMBQADggIBAAb/guF3YzZu # e6EVIJsT/wT+mHVEYcNWlXHRkT+FoetAQLHI1uBy/YXKZDk8+Y1LoNqHrp22AKMG # xQtgCivnDHFyAQ9GXTmlk7MjcgQbDCx6mn7yIawsppWkvfPkKaAQsiqaT9DnMWBH # VNIabGqgQSGTrQWo43MOfsPynhbz2Hyxf5XWKZpRvr3dMapandPfYgoZ8iDL2OR3 # sYztgJrbG6VZ9DoTXFm1g0Rf97Aaen1l4c+w3DC+IkwFkvjFV3jS49ZSc4lShKK6 # BrPTJYs4NG1DGzmpToTnwoqZ8fAmi2XlZnuchC4NPSZaPATHvNIzt+z1PHo35D/f # 7j2pO1S8BCysQDHCbM5Mnomnq5aYcKCsdbh0czchOm8bkinLrYrKpii+Tk7pwL7T # jRKLXkomm5D1Umds++pip8wH2cQpf93at3VDcOK4N7EwoIJB0kak6pSzEu4I64U6 # gZs7tS/dGNSljf2OSSnRr7KWzq03zl8l75jy+hOds9TWSenLbjBQUGR96cFr6lEU # fAIEHVC1L68Y1GGxx4/eRI82ut83axHMViw1+sVpbPxg51Tbnio1lB93079WPFnY # aOvfGAA0e0zcfF/M9gXr+korwQTh2Prqooq2bYNMvUoUKD85gnJ+t0smrWrb8dee # 2CvYZXD5laGtaAxOfy/VKNmwuWuAh9kcMIIGczCCBNugAwIBAgIQeYR98Ng1Tj8U # ctnupVAebDANBgkqhkiG9w0BAQwFADBUMQswCQYDVQQGEwJHQjEYMBYGA1UEChMP # U2VjdGlnbyBMaW1pdGVkMSswKQYDVQQDEyJTZWN0aWdvIFB1YmxpYyBDb2RlIFNp # Z25pbmcgQ0EgUjM2MB4XDTIxMDcyODAwMDAwMFoXDTI0MDcyNzIzNTk1OVowXDEL # MAkGA1UEBhMCTkwxETAPBgNVBAcMCFNjaGllZGFtMRwwGgYDVQQKDBNUZWRvbiBU # ZWNobm9sb2d5IEJWMRwwGgYDVQQDDBNUZWRvbiBUZWNobm9sb2d5IEJWMIICIjAN # BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1S7lOvHhvowTe7AbeHH+DjQj3CRs # f8kurAHBIuD/JXGiDqNKg0d8p+6zjvqZYOfEviyVSm7IXpASqarrVHozM9t5HAw1 # rVI7aOfY09VqId3XHUzhusa6UN0kP56Bf2jq0dp0Ya2Q8s93PXE+8hJfHYW14Pxf # 4XAT9L+SLn8i6CCW1NfCNUTfsvb4CcTL8MKidGAk6H+EnJQlExOl+hCKfezWfmay # rWRjJbIQQsrTdPnn1VQdV2AsnQ0518lMLkBkMcaS8mf5avN1M760aTbd/j1JIf2D # 046se9LEAVZw+wjga7HwJvZhj6OsPKvxL6ZV1vai1ZW07StFH3kGd+Osa2OjbO9T # DVfKl57FWFfY4Nw7iGtVmPUBvmp1L+fUGC0PsIhbMltLqZfDNJHk0q4Jub3dPcxj # mXoH9OE3GVWH+rn6zUFm4LDg0jmfmuLpAm+sppuErvVY61SvJw6ITCm1DphRbvpT # S56qy+ck9+uo6qlm6VMohgCm/icO2RtYoxLERQrN1x0i/d9nM6jd+LpbndVvcRdv # Bpd+nOciTAUtSJkWIdbBRND9WVfpRPBUunZynkYZ+KdS8QtFES0k/RQu2wWBNqIt # kwZziuYI72FXN4m2HyzJVJUzker6OaUCfwCxLwoUdpfa0ep1QPfpl/s3kkBmMiC0 # l8Bx5Cuy59W8l8UCAwEAAaOCAbcwggGzMB8GA1UdIwQYMBaAFA8qyyCHKLjsb0iu # K1SmKaoXpM0MMB0GA1UdDgQWBBTv5cqB48RXvR408SsZJbSKP3ruojAOBgNVHQ8B # Af8EBAMCB4AwDAYDVR0TAQH/BAIwADATBgNVHSUEDDAKBggrBgEFBQcDAzARBglg # hkgBhvhCAQEEBAMCBBAwSgYDVR0gBEMwQTA1BgwrBgEEAbIxAQIBAwIwJTAjBggr # BgEFBQcCARYXaHR0cHM6Ly9zZWN0aWdvLmNvbS9DUFMwCAYGZ4EMAQQBMEkGA1Ud # HwRCMEAwPqA8oDqGOGh0dHA6Ly9jcmwuc2VjdGlnby5jb20vU2VjdGlnb1B1Ymxp # Y0NvZGVTaWduaW5nQ0FSMzYuY3JsMHkGCCsGAQUFBwEBBG0wazBEBggrBgEFBQcw # AoY4aHR0cDovL2NydC5zZWN0aWdvLmNvbS9TZWN0aWdvUHVibGljQ29kZVNpZ25p # bmdDQVIzNi5jcnQwIwYIKwYBBQUHMAGGF2h0dHA6Ly9vY3NwLnNlY3RpZ28uY29t # MBkGA1UdEQQSMBCBDmluZm9AdGVkb24uY29tMA0GCSqGSIb3DQEBDAUAA4IBgQAW # Su+2u7XZGbyh7tDXp9YskQ2vx3ZtT3NpN3JbN78FMSUKyEPIOshV2XWaP9GtCGmH # YGRlmIS0qY0zZwei8rjFg0wNon6GakCER99XBNAqE9UPpNNC+fJCT+H+rXCShjAK # 6ohXry5tOGYJ2Mjz/TCwL2LX4MN42JbS5tL0/ZivsZuk/xNBkSRPwSmTl5xAwnWZ # IN21eN11vypef5IldBTvoGAC4vTxBAgevLkn5ji0hTMZdTLbvlZdL+4n/dtl0c/B # v1FamuPc4eYaJnsD0iEv3VUxwOHJpZc2KpWS6xV1/OjrB5q75EXSmtCaEU/ssqjI # ZwUUvmwyJKjKA3Biz2Mq+INTTGaeIGdILYSwq0EZm4U3X+FXf6RTbRqyRmRn/mOu # BQIPipFLjsQgUG0VCl7G1+gOGg3YC7gmAmZQurdGofuf49YLiF9gCfP2QaVAjKdM # 06xftWsphFGIcbCmMo76gdx3TG6q8gJI5qRi8qcFxPzRQRWnq5EuiAXqa0ekuFIw # ggauMIIElqADAgECAhAHNje3JFR82Ees/ShmKl5bMA0GCSqGSIb3DQEBCwUAMGIx # CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 # dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH # NDAeFw0yMjAzMjMwMDAwMDBaFw0zNzAzMjIyMzU5NTlaMGMxCzAJBgNVBAYTAlVT # MRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1 # c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0EwggIiMA0GCSqG # SIb3DQEBAQUAA4ICDwAwggIKAoICAQDGhjUGSbPBPXJJUVXHJQPE8pE3qZdRodbS # g9GeTKJtoLDMg/la9hGhRBVCX6SI82j6ffOciQt/nR+eDzMfUBMLJnOWbfhXqAJ9 # /UO0hNoR8XOxs+4rgISKIhjf69o9xBd/qxkrPkLcZ47qUT3w1lbU5ygt69OxtXXn # HwZljZQp09nsad/ZkIdGAHvbREGJ3HxqV3rwN3mfXazL6IRktFLydkf3YYMZ3V+0 # VAshaG43IbtArF+y3kp9zvU5EmfvDqVjbOSmxR3NNg1c1eYbqMFkdECnwHLFuk4f # sbVYTXn+149zk6wsOeKlSNbwsDETqVcplicu9Yemj052FVUmcJgmf6AaRyBD40Nj # gHt1biclkJg6OBGz9vae5jtb7IHeIhTZgirHkr+g3uM+onP65x9abJTyUpURK1h0 # QCirc0PO30qhHGs4xSnzyqqWc0Jon7ZGs506o9UD4L/wojzKQtwYSH8UNM/STKvv # mz3+DrhkKvp1KCRB7UK/BZxmSVJQ9FHzNklNiyDSLFc1eSuo80VgvCONWPfcYd6T # /jnA+bIwpUzX6ZhKWD7TA4j+s4/TXkt2ElGTyYwMO1uKIqjBJgj5FBASA31fI7tk # 42PgpuE+9sJ0sj8eCXbsq11GdeJgo1gJASgADoRU7s7pXcheMBK9Rp6103a50g5r # mQzSM7TNsQIDAQABo4IBXTCCAVkwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4E # FgQUuhbZbU2FL3MpdpovdYxqII+eyG8wHwYDVR0jBBgwFoAU7NfjgtJxXWRM3y5n # P+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMIMHcG # CCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQu # Y29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGln # aUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8v # Y3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNybDAgBgNV # HSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwDQYJKoZIhvcNAQELBQADggIB # AH1ZjsCTtm+YqUQiAX5m1tghQuGwGC4QTRPPMFPOvxj7x1Bd4ksp+3CKDaopafxp # wc8dB+k+YMjYC+VcW9dth/qEICU0MWfNthKWb8RQTGIdDAiCqBa9qVbPFXONASIl # zpVpP0d3+3J0FNf/q0+KLHqrhc1DX+1gtqpPkWaeLJ7giqzl/Yy8ZCaHbJK9nXzQ # cAp876i8dU+6WvepELJd6f8oVInw1YpxdmXazPByoyP6wCeCRK6ZJxurJB4mwbfe # Kuv2nrF5mYGjVoarCkXJ38SNoOeY+/umnXKvxMfBwWpx2cYTgAnEtp/Nh4cku0+j # Sbl3ZpHxcpzpSwJSpzd+k1OsOx0ISQ+UzTl63f8lY5knLD0/a6fxZsNBzU+2QJsh # IUDQtxMkzdwdeDrknq3lNHGS1yZr5Dhzq6YBT70/O3itTK37xJV77QpfMzmHQXh6 # OOmc4d0j/R0o08f56PGYX/sr2H7yRp11LB4nLCbbbxV7HhmLNriT1ObyF5lZynDw # N7+YAN8gFk8n+2BnFqFmut1VwDophrCYoCvtlUG3OtUVmDG0YgkPCr2B2RP+v6TR # 81fZvAT6gt4y3wSJ8ADNXcL50CN/AAvkdgIm2fBldkKmKYcJRyvmfxqkhQ/8mJb2 # VVQrH4D6wPIOK+XW+6kvRBVK5xMOHds3OBqhK/bt1nz8MIIGwDCCBKigAwIBAgIQ # DE1pckuU+jwqSj0pB4A9WjANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJVUzEX # MBUGA1UEChMORGlnaUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0 # ZWQgRzQgUlNBNDA5NiBTSEEyNTYgVGltZVN0YW1waW5nIENBMB4XDTIyMDkyMTAw # MDAwMFoXDTMzMTEyMTIzNTk1OVowRjELMAkGA1UEBhMCVVMxETAPBgNVBAoTCERp # Z2lDZXJ0MSQwIgYDVQQDExtEaWdpQ2VydCBUaW1lc3RhbXAgMjAyMiAtIDIwggIi # MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDP7KUmOsap8mu7jcENmtuh6BSF # dDMaJqzQHFUeHjZtvJJVDGH0nQl3PRWWCC9rZKT9BoMW15GSOBwxApb7crGXOlWv # M+xhiummKNuQY1y9iVPgOi2Mh0KuJqTku3h4uXoW4VbGwLpkU7sqFudQSLuIaQyI # xvG+4C99O7HKU41Agx7ny3JJKB5MgB6FVueF7fJhvKo6B332q27lZt3iXPUv7Y3U # TZWEaOOAy2p50dIQkUYp6z4m8rSMzUy5Zsi7qlA4DeWMlF0ZWr/1e0BubxaompyV # R4aFeT4MXmaMGgokvpyq0py2909ueMQoP6McD1AGN7oI2TWmtR7aeFgdOej4TJEQ # ln5N4d3CraV++C0bH+wrRhijGfY59/XBT3EuiQMRoku7mL/6T+R7Nu8GRORV/zbq # 5Xwx5/PCUsTmFntafqUlc9vAapkhLWPlWfVNL5AfJ7fSqxTlOGaHUQhr+1NDOdBk # +lbP4PQK5hRtZHi7mP2Uw3Mh8y/CLiDXgazT8QfU4b3ZXUtuMZQpi+ZBpGWUwFjl # 5S4pkKa3YWT62SBsGFFguqaBDwklU/G/O+mrBw5qBzliGcnWhX8T2Y15z2LF7OF7 # ucxnEweawXjtxojIsG4yeccLWYONxu71LHx7jstkifGxxLjnU15fVdJ9GSlZA076 # XepFcxyEftfO4tQ6dwIDAQABo4IBizCCAYcwDgYDVR0PAQH/BAQDAgeAMAwGA1Ud # EwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwIAYDVR0gBBkwFzAIBgZn # gQwBBAIwCwYJYIZIAYb9bAcBMB8GA1UdIwQYMBaAFLoW2W1NhS9zKXaaL3WMaiCP # nshvMB0GA1UdDgQWBBRiit7QYfyPMRTtlwvNPSqUFN9SnDBaBgNVHR8EUzBRME+g # TaBLhklodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRS # U0E0MDk2U0hBMjU2VGltZVN0YW1waW5nQ0EuY3JsMIGQBggrBgEFBQcBAQSBgzCB # gDAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFgGCCsGAQUF # BzAChkxodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVk # RzRSU0E0MDk2U0hBMjU2VGltZVN0YW1waW5nQ0EuY3J0MA0GCSqGSIb3DQEBCwUA # A4ICAQBVqioa80bzeFc3MPx140/WhSPx/PmVOZsl5vdyipjDd9Rk/BX7NsJJUSx4 # iGNVCUY5APxp1MqbKfujP8DJAJsTHbCYidx48s18hc1Tna9i4mFmoxQqRYdKmEIr # UPwbtZ4IMAn65C3XCYl5+QnmiM59G7hqopvBU2AJ6KO4ndetHxy47JhB8PYOgPvk # /9+dEKfrALpfSo8aOlK06r8JSRU1NlmaD1TSsht/fl4JrXZUinRtytIFZyt26/+Y # siaVOBmIRBTlClmia+ciPkQh0j8cwJvtfEiy2JIMkU88ZpSvXQJT657inuTTH4YB # ZJwAwuladHUNPeF5iL8cAZfJGSOA1zZaX5YWsWMMxkZAO85dNdRZPkOaGK7DycvD # +5sTX2q1x+DzBcNZ3ydiK95ByVO5/zQQZ/YmMph7/lxClIGUgp2sCovGSxVK05iQ # RWAzgOAj3vgDpPZFR+XOuANCR+hBNnF3rf2i6Jd0Ti7aHh2MWsgemtXC8MYiqE+b # vdgcmlHEL5r2X6cnl7qWLoVXwGDneFZ/au/ClZpLEQLIgpzJGgV8unG1TnqZbPTo # ntRamMifv427GFxD9dAq6OJi7ngE273R+1sKqHB+8JeEeOMIA11HLGOoJTiXAdI/ # Otrl5fbmm9x+LMz/F0xNAKLY1gEOuIvu5uByVYksJxlh9ncBjDGCBi0wggYpAgEB # MGgwVDELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1NlY3RpZ28gTGltaXRlZDErMCkG # A1UEAxMiU2VjdGlnbyBQdWJsaWMgQ29kZSBTaWduaW5nIENBIFIzNgIQeYR98Ng1 # Tj8UctnupVAebDAJBgUrDgMCGgUAoHgwGAYKKwYBBAGCNwIBDDEKMAigAoAAoQKA # ADAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3AgELMQ4wDAYK # KwYBBAGCNwIBFTAjBgkqhkiG9w0BCQQxFgQUH86cok/L28u5CadwijY0Fj5DPfMw # DQYJKoZIhvcNAQEBBQAEggIAfKGSWS8h1KrFsOh6e4lNrRLpEeJTzSa8K0hATVXv # nyoBY6h9A8h7wDaJxJ41uXiilmZjzlfgFZDfNhpLEWi3x7Ze4q/L4g8LDCTxua4S # 8+SnJYPz06tpKS0ysIDDiMH8kNa+vn77H1q9LIE+FRnnAMzzsLhw7jB94tDNJRF4 # zlF/ee1/Z2Xbb+u81F45EbkNXJPX80rAvLKVoT7gCzdeVMX78sjHKXlxOO4GnYvD # 3+RJZS54c0wZFSsmTgQfJNlndZ2hGOuuma/2/8GB1zwNPHOH7TQCGkFECpMijUEZ # +yWvt0JHbmiOp3NCVHhS+9TTRq6x3dUwY+XglfgIJFjSv0rrR3SeFhn6VpuIIRJa # JbiK3ZAq5L0VSophPiLCi7NlRNKRuN57hTzEOW9l2Ja8io3KyXtdPV2oyacjdsm1 # qV2ytv5+JQUWewLHuyil4q7A0u0UvetIwSEh128a7/XsTinEYq8yA/9kwGlWx/7t # nxmjNwzvDybgPXbtbJrzIBpIR8jZ9itNGDt/sriQ25S+IdnFbUfr4BQlDfvTS6DU # KqGIjIGTcsujJnUIcGZ71n50brGjaS8On2PgLvx8lBQ0lcfI+xRK0aiclMhPUC8t # m4XWYmCdJ3Ol/D+cKugpMicmNfttsGLV9L9vb1OoAeMvekbzo/M4+sRIFszqJ6pl # 0WChggMgMIIDHAYJKoZIhvcNAQkGMYIDDTCCAwkCAQEwdzBjMQswCQYDVQQGEwJV # UzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRy # dXN0ZWQgRzQgUlNBNDA5NiBTSEEyNTYgVGltZVN0YW1waW5nIENBAhAMTWlyS5T6 # PCpKPSkHgD1aMA0GCWCGSAFlAwQCAQUAoGkwGAYJKoZIhvcNAQkDMQsGCSqGSIb3 # DQEHATAcBgkqhkiG9w0BCQUxDxcNMjMwMjA0MTM0MDE1WjAvBgkqhkiG9w0BCQQx # IgQg0IOaE75pPLLigCeS2/iHckI56JaEkWQ1NlpGsLQOVFowDQYJKoZIhvcNAQEB # BQAEggIASuLakuTC2bDivZRnFQgl2bu5l1YQWqgtLsZ404lMu+vWqzwtxxSi7vDm # pCo8q3s2KyKN0kFnjX5yEYoSqIk49K9oaM1q8PDiYN+25+4tULP88vK/U+kKemSv # Wn2380b2zWhQGO9IaiGNd2SV5g8eKmhoLA3tKpFrobh2WOyUS6QfikyZTlCBarbQ # nYr+UYGZP7vrO+ukSYHAa5qNJH/hdWdbsJEu7BNBJP9YVLzkOYERc2ob0IaBkCo8 # Pecjzs/6GIjqRbGjIMSXX8gNEZALhi06xTPEt3aR5t2rpzicqxS/hZ943C8XvhtC # tSLKZhKbc4QW2asQI3Fj2Olyry/OCHMhJ7HOM0VB1LgxieRMMIEyDR6Q68q3R/Xv # vsaBlLAgPbodVEuhVcs01H/AGMH9KAPHyKuuMSU0BvALKCUzl1krN8cKr63ObEVv # poCjmjG3Q2e8tZp1OmVnRkeRaM/jL1kJETU2oBKKIvWiA5pUBbcJe0rw72k/ivh7 # VYd9QlWwmdaZ1hBTumYM+oVttcQm/85/hXdHQkXcK3QfWkXOQ63NEKfLCh+K0JHa # JKjlomsPZZdNd6aPJ+T/KGIiN21g27KGxqNERsbKyvAsH2MnZ7I5a8ZjxXQWlNCx # vtvl+qQu96ozu4fIC4rv5ZhkAdb4p0ng5Vo4KQQzSDtcCIP8nso= # SIG # End signature block |