Public/Set-CommonTenantVariable.ps1
|
function Set-CommonTenantVariable { <# .SYNOPSIS Sets or resets common tenant variables with support for environment scoping. .DESCRIPTION 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. Accepts tenant name, ID, or TenantResource object. .PARAMETER VariableSet 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. Used when setting a single variable. .PARAMETER Value 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 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 '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 'Acme Corp' -VariableSet 'Customer Variables' -Name 'Password' -Value '' Resets the 'Password' variable back to its default value. .EXAMPLE 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 '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 ( [parameter(Mandatory = $true, ParameterSetName = 'Hash')] [parameter(Mandatory = $true, ParameterSetName = 'Value')] [TenantSingleTransformation()] [Octopus.Client.Model.TenantResource] $Tenant, [parameter(Mandatory = $true, ParameterSetName = 'Hash')] [parameter(Mandatory = $true, ParameterSetName = 'Value')] [LibraryVariableSetSingleTransformation()] [Octopus.Client.Model.LibraryVariableSetResource] $VariableSet, [parameter(Mandatory = $true, ParameterSetName = 'Value')] [String]$Name, [parameter(Mandatory = $true, ParameterSetName = 'Value')] [AllowEmptyString()] [String]$Value, [parameter(Mandatory = $true, ParameterSetName = 'Hash')] [hashtable]$VariableHash, [parameter(Mandatory = $false, ParameterSetName = 'Hash')] [parameter(Mandatory = $false, ParameterSetName = 'Value')] [EnvironmentTransformation()] [Octopus.Client.Model.EnvironmentResource[]]$Environment ) begin { # testing connection to octopus try { ValidateConnection } catch { $PSCmdlet.ThrowTerminatingError($_) } } process { try { # Create the variable hash if using Name/Value parameters if ($PSCmdlet.ParameterSetName -eq "Value") { $VariableHash = @{} $VariableHash[$Name] = $Value } 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) } # 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 ($currentVariables.Name -notcontains $h.Name) { $message = "Couldn't find {0} in variable set {1}" -f $h.Name, $VariableSet.Name $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 $targetEnvIds = [Octopus.Client.Model.ReferenceCollection]::new($Environment.id) $targetScope = [Octopus.Client.Model.TenantVariables.CommonVariableScope]::new($targetEnvIds) # Create payloads for all variables $payloads = @() # 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 } $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 } # 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 } # 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 Write-Verbose "Successfully updated scoped variables for tenant '$($Tenant.Name)'" } catch { $PSCmdlet.ThrowTerminatingError($_) } } end {} } |