Public/Set-CommonTenantVariable.ps1

function Set-CommonTenantVariable {
    <#
    .SYNOPSIS
        Sets or resets a common variable for a tenant, optionally scoped to specific environments.
    .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.
    .PARAMETER Tenant
        The tenant to modify.
    .PARAMETER VariableSet
        The variable set to modify.
    .PARAMETER Name
        The name of the variable to modify.
    .PARAMETER Value
        The new value for the variable.
    .PARAMETER VariableHash
        A hashtable of variable names and values to set multiple variables at once.
    .PARAMETER Environment
        An array of environment names to scope the variable to. If not provided, the variable will be unscoped.
    .EXAMPLE
        Set-CommonTenantVariable -Tenant Tenant -VariableSet 'Customer Variables' -Name 'Password' -Value '123'
        Sets the variable to 123
    .EXAMPLE
        Set-CommonTenantVariable -Tenant Tenant -VariableSet 'Customer Variables' -Name 'Password' -Value ''
        Resets the variable back to default
    .EXAMPLE
        Set-CommonTenantVariable -Tenant Tenant -VariableSet 'Customer Variables' -VariableHash @{Port = "1111"; IP = "1.2.3.4"}
        Sets multiple variables by passing a hashtable
    .EXAMPLE
        Set-CommonTenantVariable -Tenant $tenant -VariableSet $variableSet -Name "DatabaseType" -Value "PostgreSQL" -Verbose -Environment $environment
        Sets the variable scoped to a specific environment
    #>

    [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')]
        [string[]]$Environment = @()
    )
    begin {
        # testing connection to octopus
        try {
            ValidateConnection
        } 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)
            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)

            # 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
            foreach ($h in $VariableHash.GetEnumerator()) {
                if ($VariableSet.Templates.name -notcontains $h.Name) {
                    $message = "Couldn't find {0} in variable set {1}" -f $h.Name, $VariableSet.Name
                    throw $message
                } 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
            }
            $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
                }
            }

            # 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)
                }

                # 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"
                }
                
                $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)"
            }
    
            # Execute update using new API with all variables - any excluded variables are deleted
            $command = [Octopus.Client.Model.TenantVariables.ModifyCommonVariablesByTenantIdCommand]::new($Tenant.Id, $Tenant.SpaceId, $payloads)
            $repo._repository.TenantVariables.Modify($command) | Out-Null

        } catch {
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
    
    end {}
}