Public/Sync-PdqVariable.ps1

<#
.SYNOPSIS
Synchronizes variables between Deploy and Inventory. The most recent modification wins.
 
.NOTES
This function currently cannot directly sync System variables. Instead, it creates Custom variables prefixed with
the name of the product they came from.
This is because the only way to get Deploy and Inventory to see that I updated variables in their databases is to
restart their background services.
https://gitlab.com/ColbyBouma/pdqstuff/-/issues/82
 
Inventory --> Deploy
$(AppName7Zip) --> @(Inventory:AppName7Zip)
@(abc) --> @(abc)
 
.INPUTS
None.
 
.OUTPUTS
System.Management.Automation.PSCustomObject
System.Object[]
 
.EXAMPLE
Sync-PdqVariable
Synchronizes all variables.
 
.EXAMPLE
Sync-PdqVariable -Name 'AppName7Zip', 'MyFancyApp'
Only synchronizes 2 variables.
 
.EXAMPLE
Sync-PdqVariable -Type 'Custom'
Only synchronizes Custom variables.
#>

function Sync-PdqVariable {

    [CmdletBinding()]
    param (
        # The names of the variables you would like to sync.
        # By default, all variables are synced, except for a few System variables.
        [String[]]$Name,

        # Allows you to only sync Custom or System variables.
        [ValidateSet('Custom', 'System')]
        [String]$Type
    )

    $AllVariables = @{
        'Deploy'    = @{
            'Custom'  = @{}
            'System'  = @{}
            'Special' = @{}
        }
        'Inventory' = @{
            'Custom'  = @{}
            'System'  = @{}
            'Special' = @{}
        }
    }

    try {

        $DeployConnection = Open-PdqSqlConnection -Product 'Deploy'
        $InventoryConnection = Open-PdqSqlConnection -Product 'Inventory'

        foreach ( $Product in 'Deploy', 'Inventory' ) {

            $VariableData = Get-PdqVariable -Product $Product -ErrorAction 'SilentlyContinue'

            if ( $Product -eq 'Deploy' ) {

                $OppositeProduct = 'Inventory'

            } else {

                $OppositeProduct = 'Deploy'

            }

            $FilteredVariableData = @()
            if ( $Name ) {
                
                foreach ( $NameIterator in $Name ) {

                    $FilteredVariableData += $VariableData | Where-Object 'Name' -like $NameIterator

                    # Search for "System" variables.
                    $FilteredVariableData += $VariableData | Where-Object 'Name' -like "$($OppositeProduct):$($NameIterator)"

                }

            } else {

                $FilteredVariableData = $VariableData

            }

            if ( $Type ) {

                # Include "System" variables when -Type is 'System'.
                $TypeFilter = {($_.Type -eq $Type) -or ($_.Name.StartsWith("$($OppositeProduct):"))}
                $FilteredVariableData = $FilteredVariableData | Where-Object $TypeFilter

            }
            
            foreach ( $PdqVariable in $FilteredVariableData ) {

                if ( $PdqVariable.Type -eq 'System' ) {

                    # Skip time variables, and variables that are already synced.
                    if ( $PdqVariable.Name -match '^(Date|PDQ|Time)' ) {

                        continue

                    }

                }

                if ( $PdqVariable.Name -match '^(Deploy|Inventory):' ) {
                    
                    $VariableType = 'Special'
                    # $Name has to be cast to [String], otherwise the hashtable key gets created incorrectly. Why?!
                    [String]$VariableName = ($PdqVariable.Name -split ':', 2)[1]
                    $PdqVariable.Name = $VariableName

                } else {

                    $VariableType = $PdqVariable.Type
                    [String]$VariableName = $PdqVariable.Name

                }

                # Hashtables are more efficient when looking up values, but it's way easier to compare arrays.
                $AllVariables[$Product][$VariableType][$VariableName] = $PdqVariable
                $AllVariables[$Product][$VariableType]['PdqStuff-All'] += @($PdqVariable)

            }

        }

        $Param = @{
            'CaseSensitive' = $true
            'Property'      = 'Name', 'Value'
        }
        $Mutations = @(
            @{
                # Deploy Custom <--> Inventory Custom
                'ReferenceProduct'  = 'Deploy'
                'ReferenceType'     = 'Custom'
                'DifferenceProduct' = 'Inventory'
                'DifferenceType'    = 'Custom'
            },
            @{
                # Deploy System --> Inventory "System"
                'ReferenceProduct'  = 'Deploy'
                'ReferenceType'     = 'System'
                'DifferenceProduct' = 'Inventory'
                'DifferenceType'    = 'Special'
            },
            @{
                # Inventory System --> Deploy "System"
                'ReferenceProduct'  = 'Inventory'
                'ReferenceType'     = 'System'
                'DifferenceProduct' = 'Deploy'
                'DifferenceType'    = 'Special'
            }
        )
        foreach ( $Mutation in $Mutations ) {

            Write-Verbose "$($Mutation['ReferenceProduct']) $($Mutation['ReferenceType'])"

            $Param['ReferenceObject'] = $AllVariables[$Mutation['ReferenceProduct']][$Mutation['ReferenceType']]['PdqStuff-All']
            $Param['DifferenceObject'] = $AllVariables[$Mutation['DifferenceProduct']][$Mutation['DifferenceType']]['PdqStuff-All']

            # Both objects can be empty if -Name is specified, or if no Custom variables have been created.
            if ( (-not $Param['ReferenceObject']) -and (-not $Param['DifferenceObject']) ) {

                continue

            }
            
            # Compare-Object can't handle null objects, so I have to create a fake empty object for it to compare to.
            if ( -not $Param['ReferenceObject'] ) {

                $Param['ReferenceObject'] = [PSCustomObject]@{
                    'Name'  = $null
                    'Value' = $null
                }

            }

            if ( -not $Param['DifferenceObject'] ) {

                $Param['DifferenceObject'] = [PSCustomObject]@{
                    'Name'  = $null
                    'Value' = $null
                }

            }

            foreach ( $Difference in (Compare-Object @Param | Group-Object 'Name') ) {

                # Skip the null object which was added above.
                if ( -not $Difference.Name ) {

                    continue

                }

                # Only sync from System to "Special".
                if ( $Mutation['ReferenceType'] -eq 'System' ) {

                    if ( $Difference.Group.SideIndicator -eq '=>' ) {

                        continue

                    }

                }

                # The variable only exists in 1 product.
                if ( $Difference.Count -eq 1 ) {

                    $NewVariable = $true
                    
                    # Figure out which product the variable needs to be created in.
                    if ( $Difference.Group.SideIndicator -eq '=>' ) {

                        $ProductToSet = $Mutation['ReferenceProduct']
                        $OppositeProduct = $Mutation['DifferenceProduct']

                    } else {

                        $ProductToSet = $Mutation['DifferenceProduct']
                        $OppositeProduct = $Mutation['ReferenceProduct']

                    }

                    # Prepend the Deploy: or Inventory: decorator to "System" variables.
                    if ( $Mutation['ReferenceType'] -eq 'System' ) {

                        $VariableName = '{0}:{1}' -f $OppositeProduct, $Difference.Name

                    } else {

                        $VariableName = $Difference.Name

                    }

                    $VarParam = @{
                        'Name'          = $VariableName
                        'Value'         = $Difference.Group.Value
                        'Product'       = $ProductToSet
                        'WarningAction' = 'SilentlyContinue'
                    }
                    Set-PdqCustomVariable @VarParam

                }

                # The variable exists in both products.
                else {

                    $ReferenceModified = [DateTime]$AllVariables[$Mutation['ReferenceProduct']][$Mutation['ReferenceType']][$Difference.Name].Modified
                    $DifferenceModified = [DateTime]$AllVariables[$Mutation['DifferenceProduct']][$Mutation['DifferenceType']][$Difference.Name].Modified
                    $ModifiedDiff = ($ReferenceModified - $DifferenceModified).TotalSeconds
            
                    # The reference variable is newer.
                    if ( $ModifiedDiff -gt 0 ) {

                        $VarParam = @{
                            'Name'          = $Difference.Name
                            'Value'         = $AllVariables[$Mutation['ReferenceProduct']][$Mutation['ReferenceType']][$Difference.Name].Value
                            'Product'       = $Mutation['DifferenceProduct']
                            'WarningAction' = 'SilentlyContinue'
                        }

                    }

                    # The difference variable is newer.
                    elseif ( $ModifiedDiff -lt 0 ) {

                        $VarParam = @{
                            'Name'          = $Difference.Name
                            'Value'         = $AllVariables[$Mutation['DifferenceProduct']][$Mutation['DifferenceType']][$Difference.Name].Value
                            'Product'       = $Mutation['ReferenceProduct']
                            'WarningAction' = 'SilentlyContinue'
                        }

                    }

                    Set-PdqCustomVariable @VarParam

                }
        
            }

        }

        if ( $NewVariable ) {

            Write-Warning "You must refresh the PDQ $Product console to see the new variable."

        }

    } finally {

        Close-PdqSqlConnection -Product 'Deploy' -CloseConnection $DeployConnection
        Close-PdqSqlConnection -Product 'Inventory' -CloseConnection $InventoryConnection
    
    }

}