diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 5ac5085..b00938d 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -29,6 +29,59 @@ jobs: uses: gittools/actions/gitversion/execute@v0 with: useConfigFile: true + - name: Update CHANGELOG + shell: pwsh + run: | + $version = "${{ steps.gitversion.outputs.semVer }}" + $date = Get-Date -Format "yyyy-MM-dd" + $changelogPath = ".\CHANGELOG.md" + + Write-Host "Updating CHANGELOG.md with version $version" + $content = Get-Content $changelogPath -Raw + $content = $content -replace '## \[Unreleased\]', "## [$version] - $date" + Set-Content $changelogPath -Value $content -NoNewline + + Write-Host "CHANGELOG.md updated successfully" + + - name: Extract current version changelog + id: changelog + shell: pwsh + run: | + $version = "${{ steps.gitversion.outputs.semVer }}" + $changelogPath = ".\CHANGELOG.md" + + $content = Get-Content $changelogPath -Raw + + # Extract content between current version and next version/end + $pattern = "(?s)## \[$version\].*?\n(.*?)(?=\n## \[|$)" + if ($content -match $pattern) { + $changelogContent = $matches[1].Trim() + + # Escape for GitHub Actions output + $changelogContent = $changelogContent -replace '%', '%25' + $changelogContent = $changelogContent -replace '\n', '%0A' + $changelogContent = $changelogContent -replace '\r', '%0D' + + # Use new GitHub Actions output syntax + "CHANGELOG_CONTENT< + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [Octopus.Client.Model.LibraryVariableSetResource]$VariableSet, + + [Parameter(Mandatory = $true)] + [string]$Name + ) + + process { + $template = $VariableSet.Templates | Where-Object Name -EQ $Name + return $template + } +} diff --git a/OctopusDeploy/Private/New-TenantCommonVariablePayload.ps1 b/OctopusDeploy/Private/New-TenantCommonVariablePayload.ps1 new file mode 100644 index 0000000..fb94768 --- /dev/null +++ b/OctopusDeploy/Private/New-TenantCommonVariablePayload.ps1 @@ -0,0 +1,85 @@ +function New-TenantCommonVariablePayload { + <# + .SYNOPSIS + Creates a TenantCommonVariablePayload object. + .DESCRIPTION + Helper function to create the payload for updating tenant common variables. + .PARAMETER LibraryVariableSetId + The ID of the library variable set. + .PARAMETER TemplateId + The ID of the variable template. + .PARAMETER Value + The value of the variable. Can be a string or PropertyValueResource. + .PARAMETER Scope + The scope of the variable. Can be CommonVariableScope, ReferenceCollection, or array of environment IDs. + .PARAMETER VariableId + The ID of the existing variable, if updating. + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$LibraryVariableSetId, + + [Parameter(Mandatory = $true)] + [string]$TemplateId, + + [Parameter(Mandatory = $true)] + $Value, + + [Parameter(Mandatory = $false)] + [bool]$IsSensitive = $false, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [AllowNull()] + $Scope, + + [Parameter(Mandatory = $false)] + [string]$VariableId + ) + + process { + # Handle Value + if ($Value -isnot [Octopus.Client.Model.PropertyValueResource]) { + if ([string]::IsNullOrEmpty($Value)) { + return $null + } + $Value = [Octopus.Client.Model.PropertyValueResource]::new($Value, $IsSensitive) + } + + # Handle Scope + if ($Scope -isnot [Octopus.Client.Model.TenantVariables.CommonVariableScope]) { + if ($Scope -is [Octopus.Client.Model.ReferenceCollection]) { + $Scope = [Octopus.Client.Model.TenantVariables.CommonVariableScope]::new($Scope) + } + elseif ($Scope -is [System.Collections.IEnumerable] -and $Scope -isnot [string]) { + $collection = [Octopus.Client.Model.ReferenceCollection]::new() + foreach ($id in $Scope) { + $collection.Add($id) | Out-Null + } + $Scope = [Octopus.Client.Model.TenantVariables.CommonVariableScope]::new($collection) + } + else { + # Assume it's a single ID or empty + $collection = [Octopus.Client.Model.ReferenceCollection]::new(@($Scope)) + $Scope = [Octopus.Client.Model.TenantVariables.CommonVariableScope]::new($collection) + } + } + + $payload = [Octopus.Client.Model.TenantVariables.TenantCommonVariablePayload]::new( + $LibraryVariableSetId, + $TemplateId, + $Value, + $Scope + ) + + if (-not [string]::IsNullOrEmpty($VariableId)) { + $payload.Id = $VariableId + } + else { + $payload.Id = [string]::Empty + } + + return $payload + } +} diff --git a/OctopusDeploy/Public/Add-RoleToMachine.ps1 b/OctopusDeploy/Public/Add-RoleToMachine.ps1 index 684d033..6aa8b26 100644 --- a/OctopusDeploy/Public/Add-RoleToMachine.ps1 +++ b/OctopusDeploy/Public/Add-RoleToMachine.ps1 @@ -57,7 +57,13 @@ if ($pscmdlet.ShouldProcess("$($Machine.name)", "$wiMessages$($role -join ', ')")) { foreach ($_role in $Role) { - $Machine.Roles.Add($_role) + $added = $Machine.Roles.Add($_role) + if ($added){ + Write-Verbose "Added role $_role to machine $($Machine.Name)" + } + else { + Write-Verbose "Role $_role already exists on machine $($Machine.Name)" + } try { # Modify will return an update MachineResource. Only the last one will be returned to the user $lastMachineUpdate = $repo._repository.Machines.Modify($Machine) diff --git a/OctopusDeploy/Public/Compare-EnvironmentScope.ps1 b/OctopusDeploy/Public/Compare-EnvironmentScope.ps1 new file mode 100644 index 0000000..7ce7b0b --- /dev/null +++ b/OctopusDeploy/Public/Compare-EnvironmentScope.ps1 @@ -0,0 +1,35 @@ +function Compare-EnvironmentScope { + param( + [AllowNull()] + [AllowEmptyCollection()] + [string[]]$ExistingScope, + + # allow empty for new scope to represent unscoped + [AllowNull()] + [AllowEmptyCollection()] + [string[]]$NewScope + ) + $existing = if ($ExistingScope) { $ExistingScope | Sort-Object -Unique } else { @() } + $new = if ($NewScope) { $NewScope | Sort-Object -Unique } else { @() } + + # Exact match + if (($existing -join ',') -eq ($new -join ',')) { + return [pscustomobject]@{ Status = 'Equal'; ExistingScope = $existing; NewScope = $null } + } + + $intersection = $existing | Where-Object { $new -contains $_ } + $remaining = $existing | Where-Object { $new -notcontains $_ } + + # No overlap + if (-not $intersection) { + return [pscustomobject]@{ Status = 'Disjoint'; ExistingScope = $existing; NewScope = $new } + } + + # Partial overlap where existing has items not in new (Existing is superset or mixed) + if ($remaining) { + return [pscustomobject]@{ Status = 'Overlap'; ExistingScope = $remaining; NewScope = $new } + } + + # Intersection exists and no remaining items in existing (Existing is subset of New) + return [pscustomobject]@{ Status = 'Contained'; ExistingScope = $null; NewScope = $new } +} diff --git a/OctopusDeploy/Public/Get-CommonTenantVariable.ps1 b/OctopusDeploy/Public/Get-CommonTenantVariable.ps1 index 3bc03f9..0a985c3 100644 --- a/OctopusDeploy/Public/Get-CommonTenantVariable.ps1 +++ b/OctopusDeploy/Public/Get-CommonTenantVariable.ps1 @@ -3,20 +3,37 @@ .SYNOPSIS Returns a list of common tenant variables .DESCRIPTION - Returns a list of common tenant variables for a specific tenant and variable set + Returns a list of common tenant variables for a specific tenant and variable set. + This function retrieves variables from Library Variable Sets that are associated with the tenant. + It returns both default values (from the Library Variable Set templates) and tenant-specific overridden values. + .PARAMETER Tenant + The tenant to retrieve variables for. + .PARAMETER VariableSet + The Library Variable Set to filter by. If not provided, all variable sets associated with the tenant are processed. + .PARAMETER Environment + The environment to filter by. If provided, only variables scoped to this environment (or unscoped variables) are returned. .EXAMPLE Get-CommonTenantVariable -Tenant XXROM001 -VariableSet 'Customer Variables' - All variable from tenant "XXROM001" saved in "Customer Variables" + Returns all variables for tenant "XXROM001" from the "Customer Variables" set. + .EXAMPLE + Get-CommonTenantVariable -Tenant XXROM001 + Returns all common variables for tenant "XXROM001" from all associated variable sets. + .EXAMPLE + Get-CommonTenantVariable -Tenant XXROM001 -Environment Production + Returns all common variables for tenant "XXROM001" that are relevant to the Production environment. #> [CmdletBinding()] param ( [parameter(Mandatory = $true)] [TenantSingleTransformation()] [Octopus.Client.Model.TenantResource]$Tenant, - [parameter(Mandatory = $true, - ValueFromPipeline = $true)] + [parameter(Mandatory = $false, + ValueFromPipeline = $true)] [LibraryVariableSetSingleTransformation()] - [Octopus.Client.Model.LibraryVariableSetResource]$VariableSet + [Octopus.Client.Model.LibraryVariableSetResource]$VariableSet, + [parameter(Mandatory = $false)] + [EnvironmentSingleTransformation()] + [Octopus.Client.Model.EnvironmentResource]$Environment ) begin { @@ -28,17 +45,16 @@ } } process { - # variables types [System.Enum]::GetNames([Octopus.Client.Model.VariableSetContentType]) - $tenantVar = $repo._repository.Tenants.GetVariables($Tenant) - $libVars = $tenantVar.LibraryVariables."$($VariableSet.Id)" - $libVars.Templates | ForEach-Object { $setvar = $libVars.Variables."$($_.id)"; [pscustomobject]@{ - Name = $_.name - Value = if ($_.DefaultValue.IsSensitive) { "*****" }else { if ($setvar.value) { $setvar.value }else { $_.DefaultValue.value } } - IsDefaultValue = if ($setvar.value -or $setvar.SensitiveValue) { $false }else { $true } - } } - + $result = GetCommonTenantVariable @PSBoundParameters + $result | ForEach-Object { + if ($_.IsSensitive) { + $_.Value = "*****" + } + $_ + } | Select-Object -Property VariableSetName, Name , Value, IsDefaultValue, Scope } + end {} } -#Get-CommonTenantVariable -Tenant XXROM001 -VariableSet "customer variables" + diff --git a/OctopusDeploy/Public/Get-Task.ps1 b/OctopusDeploy/Public/Get-Task.ps1 index 835bacf..e9afb0c 100644 --- a/OctopusDeploy/Public/Get-Task.ps1 +++ b/OctopusDeploy/Public/Get-Task.ps1 @@ -93,7 +93,7 @@ catch { $PSCmdlet.ThrowTerminatingError($_) } - } + } process { if ($PSCmdlet.ParameterSetName -eq 'ByID') { $repo._repository.Tasks.Get($TaskID) @@ -103,7 +103,8 @@ function checkpath ($path) { if ($path -eq '/api/tasks') { return '/api/tasks?' - } else { return ($path + "&") } + } + else { return ($path + "&") } } # always add space @@ -148,9 +149,10 @@ } return $results.Items - } catch { + } + catch { - Throw $_ + throw $_ } @@ -161,9 +163,13 @@ foreach ($r in $Regarding) { if ($r -is [Octopus.Client.Model.RunbookSnapshotResource]) { $tasks = (Get-RunbookRun -RunbookSnapshot $r).links.task - } elseif ($r -is [Octopus.Client.Model.ReleaseResource]) { + } + elseif ($r -is [Octopus.Client.Model.ReleaseResource]) { $tasks = (Get-Deployment -Release $r).links.task } + elseif ($r -is [Octopus.Client.Model.DeploymentResource]) { + $tasks = @($r.TaskId) + } foreach ($t in $tasks) { $repo._repository.Tasks.get((($t -split '/')[-1])) } diff --git a/OctopusDeploy/Public/GetCommonTenantVariable.ps1 b/OctopusDeploy/Public/GetCommonTenantVariable.ps1 new file mode 100644 index 0000000..afc1262 --- /dev/null +++ b/OctopusDeploy/Public/GetCommonTenantVariable.ps1 @@ -0,0 +1,101 @@ +function GetCommonTenantVariable { + + [CmdletBinding()] + param ( + [parameter(Mandatory = $true)] + [TenantSingleTransformation()] + [Octopus.Client.Model.TenantResource]$Tenant, + [parameter(Mandatory = $false, + ValueFromPipeline = $true)] + [LibraryVariableSetSingleTransformation()] + [Octopus.Client.Model.LibraryVariableSetResource]$VariableSet, + [parameter(Mandatory = $false)] + [EnvironmentSingleTransformation()] + [Octopus.Client.Model.EnvironmentResource]$Environment + + ) + begin { + try { + ValidateConnection + } + catch { + $PSCmdlet.ThrowTerminatingError($_) + } + } + process { + # variables types [System.Enum]::GetNames([Octopus.Client.Model.VariableSetContentType]) + $tenantVar = $repo._repository.Tenants.GetVariables($Tenant) + + $environments = Get-Environment + # Determine which Variable Sets to process + $VariableSets = @() + if ($PSBoundParameters['VariableSet']) { + $VariableSets = $VariableSet + } + else { + $VariableSets = Get-VariableSet | Where-Object { $_.id -in $tenantVar.LibraryVariables.Keys } + } + $results = @() + foreach ($vSet in $VariableSets) { + # get default variables from each set + $libVars = $tenantVar.LibraryVariables."$($vSet.Id)" + $results += $libVars.Templates | ForEach-Object { $setvar = $libVars.Variables."$($_.id)"; + # only output default values that are not overridden + if ((-not $setvar.Value) -and (-not $setvar.SensitiveValue)) { + [pscustomobject]@{ + VariableSetName = $vSet.Name + Name = $_.name + Value = $_.DefaultValue.value + ValueObject = $_.DefaultValue + IsDefaultValue = $true + Scope = $null + ScopeIds = $null + TemplateID = $_.id + IsSensitive = $_.DefaultValue.IsSensitive + VariableId = $null + LibraryVariableSetId = $vSet.Id + origObject = $_ + } + } + } + # get all non default variables + $commonTenantVarRequest = [Octopus.Client.Model.TenantVariables.GetCommonVariablesByTenantIdRequest]::new($Tenant.id, $Tenant.SpaceId) + $tenantVars = $repo._repository.TenantVariables.get($commonTenantVarRequest) + + + $vars = $tenantVars.Variables | Where-Object LibraryVariableSetId -EQ $vSet.Id + + $results += $vars | ForEach-Object { + [pscustomobject]@{ + VariableSetName = $vSet.Name + Name = $_.template.name + Value = $_.value.value + ValueObject = $_.value + IsDefaultValue = $false + Scope = [String[]]($_.scope.EnvironmentIds | ForEach-Object { $environments | Where-Object id -Like $_ }).name + ScopeIds = [String[]]($_.scope.EnvironmentIds) + TemplateID = $_.TemplateId + IsSensitive = $_.Value.IsSensitive + VariableId = $_.Id + LibraryVariableSetId = $_.LibraryVariableSetId + origObject = $_ + + } + } + # if an environment was specified, return all scoped variables for that environment and unscoped variables a far the variable has no environment scope + if ($PSBoundParameters['Environment']) { + [System.Array]$results = $results | Where-Object { ($_.Scope -contains $Environment.Name) -or ($null -eq $_.Scope -and ($results | Where-Object Name -EQ $_.name).count -eq 1) } + } + + + + } + # order results and output + $results = $results | Sort-Object VariableSetName, Name + $results + } + + end {} + +} + diff --git a/OctopusDeploy/Public/Invoke-RunbookRun.ps1 b/OctopusDeploy/Public/Invoke-RunbookRun.ps1 index f474611..57fb3ca 100644 --- a/OctopusDeploy/Public/Invoke-RunbookRun.ps1 +++ b/OctopusDeploy/Public/Invoke-RunbookRun.ps1 @@ -260,7 +260,7 @@ $runbookRun.TenantId = $_tenant.id try { Write-Verbose "Running traditional runbook snapshot for tenant '$($_tenant.Name)'" - return $repo._repository.RunbookRuns.Create($runbookRun) + $repo._repository.RunbookRuns.Create($runbookRun) } catch { $PSCmdlet.WriteError($_) @@ -271,7 +271,7 @@ # Execute runbook without tenant try { Write-Verbose "Running traditional runbook snapshot (untenanted)" - return $repo._repository.RunbookRuns.Create($runbookRun) + $repo._repository.RunbookRuns.Create($runbookRun) } catch { $PSCmdlet.WriteError($_) diff --git a/OctopusDeploy/Public/Set-CommonTenantVariable.ps1 b/OctopusDeploy/Public/Set-CommonTenantVariable.ps1 index 56a2f9f..2951408 100644 --- a/OctopusDeploy/Public/Set-CommonTenantVariable.ps1 +++ b/OctopusDeploy/Public/Set-CommonTenantVariable.ps1 @@ -1,35 +1,65 @@ function Set-CommonTenantVariable { <# .SYNOPSIS - Sets or resets a common variable for a tenant, optionally scoped to specific environments. + Sets or resets common tenant variables with support for environment scoping. .DESCRIPTION - This function allows you to set or reset a common variable for a specified tenant in Octopus Deploy. - You can provide a single variable name and value, or a hashtable of multiple variables to set at once. - Additionally, you can scope the variable to specific environments if needed. + This function manages common tenant variables in Octopus Deploy, allowing you to set or reset variable values + with optional environment scoping. The function intelligently handles scope conflicts by comparing existing and + new scopes, preserving non-overlapping values while updating target environments. + + Key features: + - Set single or multiple variables at once using a hashtable + - Scope variables to specific environments or leave unscoped + - Automatically handles scope conflicts (disjoint, overlapping, equal, contained) + - Reset variables to default by providing an empty string value + - Preserves all other tenant variables not being modified + + When updating scoped variables, the function compares the existing scope with the target scope: + - Disjoint: Keeps existing scoped value and adds new value with target scope + - Equal/Contained: Updates the existing variable with the new value + - Overlap: Splits the variable - preserves non-overlapping environments with old value, updates target environments with new value .PARAMETER Tenant - The tenant to modify. + The tenant to modify. Accepts tenant name, ID, or TenantResource object. .PARAMETER VariableSet - The variable set to modify. + The library variable set containing the common variables. Accepts variable set name, ID, or LibraryVariableSetResource object. .PARAMETER Name - The name of the variable to modify. + The name of the variable to modify. Used when setting a single variable. .PARAMETER Value - The new value for the variable. + The new value for the variable. Use an empty string ('') to reset the variable to its default value. .PARAMETER VariableHash - A hashtable of variable names and values to set multiple variables at once. + A hashtable of variable names and values to set multiple variables in a single operation. + Example: @{Port = "1111"; IP = "1.2.3.4"} .PARAMETER Environment An array of environment names to scope the variable to. If not provided, the variable will be unscoped. + Accepts environment names, IDs, or EnvironmentResource objects. .EXAMPLE - Set-CommonTenantVariable -Tenant Tenant -VariableSet 'Customer Variables' -Name 'Password' -Value '123' - Sets the variable to 123 + Set-CommonTenantVariable -Tenant 'Acme Corp' -VariableSet 'Customer Variables' -Name 'Password' -Value 'P@ssw0rd' + + Sets the unscoped variable 'Password' to 'P@ssw0rd' for tenant 'Acme Corp'. .EXAMPLE - Set-CommonTenantVariable -Tenant Tenant -VariableSet 'Customer Variables' -Name 'Password' -Value '' - Resets the variable back to default + Set-CommonTenantVariable -Tenant 'Acme Corp' -VariableSet 'Customer Variables' -Name 'Password' -Value '' + + Resets the 'Password' variable back to its default value. .EXAMPLE - Set-CommonTenantVariable -Tenant Tenant -VariableSet 'Customer Variables' -VariableHash @{Port = "1111"; IP = "1.2.3.4"} - Sets multiple variables by passing a hashtable + Set-CommonTenantVariable -Tenant 'Acme Corp' -VariableSet 'Customer Variables' -VariableHash @{Port = "1111"; IP = "1.2.3.4"} + + Sets multiple unscoped variables at once using a hashtable. + .EXAMPLE + Set-CommonTenantVariable -Tenant 'Acme Corp' -VariableSet 'Customer Variables' -Name 'DatabaseType' -Value 'PostgreSQL' -Environment 'Production' + + Sets the 'DatabaseType' variable to 'PostgreSQL' scoped to the Production environment only. + .EXAMPLE + Set-CommonTenantVariable -Tenant 'Acme Corp' -VariableSet 'Customer Variables' -Name 'ConnectionString' -Value 'Server=new-server' -Environment 'Test','QA','Production' + + Sets the 'ConnectionString' variable scoped to multiple environments. If the variable already has values in other environments, + those will be preserved while the Test, QA, and Production environments will be updated with the new value. .EXAMPLE - Set-CommonTenantVariable -Tenant $tenant -VariableSet $variableSet -Name "DatabaseType" -Value "PostgreSQL" -Verbose -Environment $environment - Sets the variable scoped to a specific environment + Set-CommonTenantVariable -Tenant 'Acme Corp' -VariableSet 'Customer Variables' -VariableHash @{Port = "5432"; DatabaseType = "PostgreSQL"} -Environment 'Development' + + Sets multiple variables scoped to a specific environment using a hashtable. + .NOTES + This function uses the Octopus Deploy Client API to modify tenant variables. All changes are atomic - + either all variables are updated successfully or none are changed. #> [CmdletBinding()] param ( @@ -66,132 +96,321 @@ ParameterSetName = 'Hash')] [parameter(Mandatory = $false, ParameterSetName = 'Value')] - [string[]]$Environment = @() + [EnvironmentTransformation()] + [Octopus.Client.Model.EnvironmentResource[]]$Environment ) + begin { # testing connection to octopus try { ValidateConnection - } catch { + } + catch { $PSCmdlet.ThrowTerminatingError($_) } - # Function to compare environment scopes - # function Test-ScopeMatch($scope1, $scope2) { - # $env1 = @($scope1.EnvironmentIds | Sort-Object) - # $env2 = @($scope2.EnvironmentIds | Sort-Object) - - # if ($env1.Count -ne $env2.Count) { return $false } - - # for ($i = 0; $i -lt $env1.Count; $i++) { - # if ($env1[$i] -ne $env2[$i]) { return $false } - # } - - # return $true - # } } process { + + + + try { - # Fix the parameter set name comparison (case sensitive) + # Create the variable hash if using Name/Value parameters if ($PSCmdlet.ParameterSetName -eq "Value") { $VariableHash = @{} $VariableHash[$Name] = $Value } - Write-Verbose "Processing variables: $($VariableHash | ConvertTo-Json -Compress)" - # Get variable set resource - # $VariableSet = $repo._repository.LibraryVariableSets.FindByName($VariableSet) + if ($PSBoundParameters['Environment']) { + $envString = " scoped to environments: $($Environment.name -join ', ')" + Write-Verbose "Processing variables for tenant '$($Tenant.Name)' in variable set '$($VariableSet.Name)'$envString" + } + else { + Write-Verbose "Processing unscoped variables for tenant '$($Tenant.Name)' in variable set '$($VariableSet.Name)'" + } + Write-Verbose "Variables to update: $($VariableHash.Keys -join ', ')" + + + # check the tenant has variables from the variable set passed in as a parameter + $currentVariables = GetCommonTenantVariable -Tenant $Tenant + if ($currentVariables.LibraryVariableSetId -notcontains $VariableSet.Id) { + $message = "Tenant $($Tenant.Name) does not have any variables from variable set `"$($VariableSet.Name)`"" + $err = [System.Management.Automation.ErrorRecord]::new( + [System.Management.Automation.ItemNotFoundException]::new("$message"), + 'NotSpecified', + 'InvalidData', + "$($Tenant.name) / $($Variableset.name)" + ) + $errorDetails = [System.Management.Automation.ErrorDetails]::new("$message") + $errorDetails.RecommendedAction = "Ensure tenant has variables from variable set" + $err.ErrorDetails = $errorDetails + $PSCmdlet.ThrowTerminatingError($err) + } - # Get ALL existing common variables for this tenant - $getRequest = [Octopus.Client.Model.TenantVariables.GetCommonVariablesByTenantIdRequest]::new($Tenant.Id, $Tenant.SpaceId) - $allExistingVariables = $repo._repository.TenantVariables.Get($getRequest).Variables - - # Check that all the variables are defined in template + # Check that the variables to set exist in the variable set + $currentVarNames = ($currentVariables | Where-Object { $_.LibraryVariableSetId -eq $VariableSet.id }).Name + $missingVars = @() + foreach ($varName in $VariableHash.Keys) { + if ($currentVarNames -notcontains $varName) { + $missingVars += $varName + } + } + if ($missingVars) { + $message = "The following variables were not found in variable set {0}: {1}" -f $VariableSet.Name, ($missingVars -join ', ') + $err = [System.Management.Automation.ErrorRecord]::new( + [System.Management.Automation.ItemNotFoundException]::new("$message"), + 'NotSpecified', + 'InvalidData', + "$($Variableset.name) / $($missingVars -join ', ')" + ) + $errorDetails = [System.Management.Automation.ErrorDetails]::new("$message") + $errorDetails.RecommendedAction = "Check variable names exist in variable set" + $err.ErrorDetails = $errorDetails + $PSCmdlet.ThrowTerminatingError($err) + } + + # Check that all the variables are defined in Variable Set foreach ($h in $VariableHash.GetEnumerator()) { - if ($VariableSet.Templates.name -notcontains $h.Name) { + if ($currentVariables.Name -notcontains $h.Name) { $message = "Couldn't find {0} in variable set {1}" -f $h.Name, $VariableSet.Name - throw $message - } else { + $err = [System.Management.Automation.ErrorRecord]::new( + [System.Management.Automation.ItemNotFoundException]::new("$message"), + 'NotSpecified', + 'InvalidData', + "$($Variableset.name) / $($h.name)" + ) + $errorDetails = [System.Management.Automation.ErrorDetails]::new("$message") + $errorDetails.RecommendedAction = "Check variable exists in variable set" + $err.ErrorDetails = $errorDetails + $PSCmdlet.ThrowTerminatingError($err) + } + else { $message = "Found variable {0} in variable set {1}" -f $h.Name, $VariableSet.Name Write-Verbose $message } } - # Create the target scope we want to match (move outside the loop) - $targetEnvIds = [Octopus.Client.Model.ReferenceCollection]::new() - foreach ($_environment in $Environment) { - $envObj = $repo._repository.Environments.FindByName($_environment) - if (-not $envObj) { - $message = "Couldn't find environment {0}" -f $_environment - throw $message - } - $message = "Found environment {0} with ID {1}" -f $envObj.Name, $envObj.Id - Write-Verbose $message - $targetEnvIds.Add($envObj.Id) | Out-Null - } + + # Create the target scope we want to match + $targetEnvIds = [Octopus.Client.Model.ReferenceCollection]::new($Environment.id) $targetScope = [Octopus.Client.Model.TenantVariables.CommonVariableScope]::new($targetEnvIds) + + + # Create payloads for all variables $payloads = @() - # Add all existing variables (unchanged) to preserve them, except those we're updating - $variablesToUpdate = $VariableHash.Keys - foreach ($existingVar in $allExistingVariables) { - $varTemplate = $VariableSet.Templates | Where-Object Id -EQ $existingVar.TemplateId - $isTargetVariable = $variablesToUpdate -contains $varTemplate.Name #-and (Test-ScopeMatch $existingVar.Scope $targetScope) - - if (-not $isTargetVariable) { - # This is a different variable - preserve it as-is - $payload = [Octopus.Client.Model.TenantVariables.TenantCommonVariablePayload]::new( - $existingVar.LibraryVariableSetId, - $existingVar.TemplateId, - $existingVar.Value, - $existingVar.Scope - ) - $payload.Id = $existingVar.Id - $payloads += $payload + # Variable to preserve (not being updated) + $variableToPreserve = $currentVariables | Where-Object { ($_.name.ToLower() -notin $VariableHash.Keys.ToLower() -and -not $_.IsDefaultValue -and $_.LibraryVariableSetId -eq $VariableSet.id ) -or + (-not $_.IsDefaultValue -and $_.LibraryVariableSetId -ne $VariableSet.id ) } + + if ($variableToPreserve) { + Write-Verbose "Preserving $($variableToPreserve.Count) existing variable(s)" + } + foreach ($var in $variableToPreserve) { + $newTenantCommonVariablePayloadSplat = @{ + LibraryVariableSetId = $var.LibraryVariableSetId + TemplateId = $var.TemplateId + Value = $var.ValueObject + Scope = $var.ScopeIds + VariableId = $var.VariableId } + $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSplat + $payloads += $payload } - # Now add/update each variable from the hash - foreach ($h in $VariableHash.GetEnumerator()) { - # get the template object. Id is needed to identify and set variable - $varTemplate = $VariableSet.Templates | Where-Object Name -EQ $h.Name - - # Find the specific variable we want to update (match library set, template, and scope) - $targetVariable = $allExistingVariables | Where-Object { - $_.LibraryVariableSetId -eq $VariableSet.Id -and - $_.TemplateId -eq $varTemplate.Id #-and - #(Test-ScopeMatch $_.Scope $targetScope) + $variablesToChange = $currentVariables | Where-Object { $_.name -in $VariableHash.Keys } + + + if (-not $Environment) { + Write-Verbose "Updating unscoped variables only" + # We are updating unscoped variables only + # We will preserve all scoped variables as-is + foreach ($var in $variablesToChange | Where-Object { $_.Scope }) { + $newTenantCommonVariablePayloadSplat = @{ + LibraryVariableSetId = $var.LibraryVariableSetId + TemplateId = $var.TemplateId + Value = $var.ValueObject + Scope = $var.ScopeIds + VariableId = $var.VariableId + } + $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSplat + $payloads += $payload + } + # update only unscoped variables + foreach ($var in $variablesToChange | Where-Object { -not $_.Scope }) { + if ($VariableHash.Keys -contains $Var.Name) { + $newTenantCommonVariablePayloadSplat = @{ + LibraryVariableSetId = $var.LibraryVariableSetId + TemplateId = $var.TemplateId + Value = $VariableHash[$var.Name] + IsSensitive = $var.IsSensitive + Scope = @() # unscoped + VariableId = if ($var.VariableId) { $var.VariableId } else { $null } + } + $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSplat + $payloads += $payload + } } + # Execute update using new API with all variables - any excluded variables are deleted + Write-Verbose "Applying $($payloads.Count) variable payload(s) to tenant '$($Tenant.Name)'" + $command = [Octopus.Client.Model.TenantVariables.ModifyCommonVariablesByTenantIdCommand]::new($Tenant.Id, $Tenant.SpaceId, $payloads) + $repo._repository.TenantVariables.Modify($command) | Out-Null + Write-Verbose "Successfully updated unscoped variables for tenant '$($Tenant.Name)'" + return # exit function as we are done handling unscoped only case - # Create payload for this variable - $payload = [Octopus.Client.Model.TenantVariables.TenantCommonVariablePayload]::new( - $VariableSet.Id, - $varTemplate.Id, - [Octopus.Client.Model.PropertyValueResource]::new($h.Value, $false), - $targetScope - ) - - if ($targetVariable) { - $payload.Id = $targetVariable.Id - $action = "updated ID: $($targetVariable.Id)" - } else { - $payload.Id = [string]::Empty - $action = "created new" + } + # We are updating scoped variables + Write-Verbose "Updating scoped variables" + # first we preserve all unscoped variables as-is + foreach ($var in $variablesToChange | Where-Object { -not $_.Scope -and -not $_.IsDefaultValue }) { + $newTenantCommonVariablePayloadSplat = @{ + LibraryVariableSetId = $var.LibraryVariableSetId + TemplateId = $var.TemplateId + Value = $var.ValueObject + Scope = $var.ScopeIds + VariableId = $var.VariableId } - + $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSplat $payloads += $payload + } + + - $scope = if ($Environment.Count -eq 0) { "unscoped" } else { "scoped to: $($Environment -join ', ')" } - Write-Verbose "Successfully processed '$($h.Name)' = '$($h.Value)' for tenant '$($Tenant.Name)' ($scope, $action)" + # convert the value hashtable to an array and add property to remember if already added + $newVariable = $VariableHash.GetEnumerator() | ForEach-Object { + [PSCustomObject]@{ + Name = $_.Name + Value = $_.Value + Added = $false + } } - + + foreach ($var in $variablesToChange | Where-Object { $_.Scope }) { + # Determine if this variable is one we want to update and if both are scoped + if ($newVariable.name -contains $var.Name) { + if ($newVariable | Where-Object { $_.Name -eq $var.Name -and $_.Added -eq $true } ) { + # var already added. remove scope. if one or more scoping we need to update + # remove target scope from variable scope + + $newVarScope = $var.ScopeIds | Where-Object { $Environment.id -notcontains $_ } + + if ($newVarScope) { + # update existing variable with new scope + $newTenantCommonVariablePayloadSplat = @{ + LibraryVariableSetId = $var.LibraryVariableSetId + TemplateId = $var.TemplateId + Value = $var.ValueObject + Scope = $newVarScope + VariableId = $var.VariableId + } + $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSplat + $payloads += $payload + + } + continue + } + $comparison = Compare-EnvironmentScope -ExistingScope $var.ScopeIds -NewScope $Environment.id + Write-Verbose "Scope comparison for variable '$($var.Name)': $($comparison.Status)" + # update old variable and new variable depending on comparison result + if ($comparison.Status -eq 'Disjoint') { + Write-Verbose "Variable '$($var.Name)': Keeping existing scope and adding new scoped value" + + # add old variable as-is to payloads + $newTenantCommonVariablePayloadSplat = @{ + LibraryVariableSetId = $var.LibraryVariableSetId + TemplateId = $var.TemplateId + Value = $var.ValueObject + Scope = $var.ScopeIds + VariableId = $var.VariableId + } + $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSplat + # check the payload does not already exist before adding to $payloads + + + $payloads += $payload + + } + elseif ($comparison.Status -in 'Equal', 'Contained') { + Write-Verbose "Variable '$($var.Name)': Updating existing scoped variable with new value" + $newTenantCommonVariablePayloadSplat = @{ + LibraryVariableSetId = $var.LibraryVariableSetId + TemplateId = $var.TemplateId + Value = $VariableHash[$var.Name] + IsSensitive = $var.IsSensitive + Scope = if (-not $null -eq $comparison.ExistingScope) { $($comparison.ExistingScope) }else { $($comparison.NewScope) } + VariableId = $var.VariableId + } + $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSplat + + if ($payload) { $payloads += $payload } + $newVariable | Where-Object { $_.Name -eq $var.Name } | ForEach-Object { $_.Added = $true } + + } + elseif ($comparison.Status -eq 'Overlap') { + Write-Verbose "Variable '$($var.Name)': Splitting overlapping scope - preserving non-overlapping environments and updating target environments" + # update old variable with new scope + $newTenantCommonVariablePayloadSplat = @{ + LibraryVariableSetId = $var.LibraryVariableSetId + TemplateId = $var.TemplateId + Value = $var.ValueObject + Scope = $comparison.ExistingScope + VariableId = $var.VariableId + } + $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSplat + + $payloads += $payload + + # there seems to be and issue with setting sensitive variables to empty string in overlapping scope scenario + # workaround is to set a value to something else first then set to empty string in a second call + if ($var.IsSensitive -and [string]::IsNullOrEmpty($VariableHash[$var.Name])) { + Set-CommonTenantVariable -Tenant $Tenant -VariableSet $VariableSet -Name $var.Name -Value 'TemporaryValueForSensitiveVariable' -Environment $Environment -Verbose:$false + } + + # add new variable with updated value and target scope + $newTenantCommonVariablePayloadSplat = @{ + LibraryVariableSetId = $var.LibraryVariableSetId + TemplateId = $var.TemplateId + Value = $VariableHash[$var.Name] + IsSensitive = $var.IsSensitive + Scope = $comparison.NewScope + } + $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSplat + + if ($payload) { $payloads += $payload } + $newVariable | Where-Object { $_.Name -eq $var.Name } | ForEach-Object { $_.Added = $true } + } + else { + throw "Unhandled comparison status: $($comparison.Status)" + } + + } + } + + # Add any new variables that were not already added + foreach ($nv in $newVariable | Where-Object { $_.Added -eq $false }) { + $varInfo = $currentVariables | Where-Object { $_.Name -eq $nv.Name } | Select-Object -First 1 + $newTenantCommonVariablePayloadSplat = @{ + LibraryVariableSetId = $varInfo.LibraryVariableSetId + TemplateId = $varInfo.TemplateID + Value = $nv.Value + IsSensitive = $varInfo.IsSensitive + Scope = $Environment.Id + } + $payload = New-TenantCommonVariablePayload @newTenantCommonVariablePayloadSplat + if ($payload) { $payloads += $payload } + } + + # Execute update using new API with all variables - any excluded variables are deleted + Write-Verbose "Applying $($payloads.Count) variable payload(s) to tenant '$($Tenant.Name)'" $command = [Octopus.Client.Model.TenantVariables.ModifyCommonVariablesByTenantIdCommand]::new($Tenant.Id, $Tenant.SpaceId, $payloads) $repo._repository.TenantVariables.Modify($command) | Out-Null - - } catch { + Write-Verbose "Successfully updated scoped variables for tenant '$($Tenant.Name)'" + } + catch { $PSCmdlet.ThrowTerminatingError($_) } }