Add queue hardening by modifying DeploymentJobTargets, DeploymentJobSteps, and DeploymentJobs tables

- Drop existing indexes on DeploymentJobTargets and DeploymentJobSteps
- Alter Status column to nvarchar(450) in DeploymentJobTargets, DeploymentJobSteps, and DeploymentJobs
- Add new columns: Finished, OutputMetadataJson, Started to DeploymentJobTargets; ErrorMessage, Finished, OutputMetadataJson, Started to DeploymentJobSteps; CorrelationId, HeartbeatAt, Priority, RowVersion, ScheduledAt, WorkerName to DeploymentJobs
- Create new indexes for improved query performance
- Add constraints to ensure OutputMetadataJson is valid JSON
- Record migration in __EFMigrationsHistory
This commit is contained in:
Torsten Brendgen
2026-07-09 08:50:09 +02:00
parent 97238c28c9
commit 01c2a5df2e
45 changed files with 8198 additions and 75 deletions

View File

@@ -0,0 +1,421 @@
param(
[string]$ApiBaseUrl = 'http://localhost:5286/api',
[string]$BgwRoot = ('F:\Kunden Auftr' + [char]0x00E4 + 'ge\BGW'),
[string]$DeploymentBatchId = '80000000-0000-0000-0000-000000000101',
[string]$MergeModulePath = 'F:\Projekte\Coding\PowerShell\Merge-DSCConfigurationData\Merge-DSCConfigurationData.psd1',
[string]$ResolveModulePath = 'F:\Projekte\Coding\PowerShell\Resolve-DSCConfigurationData\Resolve-DSCConfigurationData.psd1',
[int]$ApiTimeoutSec = 30,
[bool]$UseDefaultCredentials = $true
)
$ErrorActionPreference = 'Stop'
function Invoke-TestBgwMergeApiJson {
param(
[ValidateSet('GET', 'POST', 'PUT', 'DELETE')]
[string]$Method,
[string]$Path
)
$uri = ('{0}/{1}' -f $ApiBaseUrl.TrimEnd('/'), $Path.TrimStart('/'))
$parameters = @{
Method = $Method
Uri = $uri
TimeoutSec = $ApiTimeoutSec
ErrorAction = 'Stop'
}
if ($UseDefaultCredentials) {
$parameters.UseDefaultCredentials = $true
}
try {
Invoke-RestMethod @parameters
}
catch {
$responseBody = '<empty response body>'
if ($_.Exception.Response) {
try {
$stream = $_.Exception.Response.GetResponseStream()
if ($stream) {
$reader = [System.IO.StreamReader]::new($stream)
$text = $reader.ReadToEnd()
if (-not [string]::IsNullOrWhiteSpace($text)) {
$responseBody = $text
}
}
}
catch {
$responseBody = '<could not read response body>'
}
}
throw "API request failed. Method=[$Method], Uri=[$uri], Response=[$responseBody]. $($_.Exception.Message)"
}
}
function ConvertTo-TestBgwMergeConfigurationValue {
param([object]$Value)
if ($null -eq $Value) {
return $null
}
if ($Value -is [string] -or $Value -is [bool] -or $Value -is [int] -or $Value -is [long] -or $Value -is [double] -or $Value -is [decimal]) {
return $Value
}
if ($Value -is [System.Collections.IDictionary]) {
$result = [ordered]@{}
foreach ($key in $Value.Keys) {
$result[[string]$key] = ConvertTo-TestBgwMergeConfigurationValue -Value $Value[$key]
}
Write-Output -NoEnumerate $result
return
}
if ($Value -is [pscustomobject]) {
$result = [ordered]@{}
foreach ($property in $Value.PSObject.Properties) {
$result[$property.Name] = ConvertTo-TestBgwMergeConfigurationValue -Value $property.Value
}
Write-Output -NoEnumerate $result
return
}
if ($Value -is [System.Collections.IEnumerable] -and $Value -isnot [string]) {
$items = New-Object System.Collections.Generic.List[object]
foreach ($item in $Value) {
$items.Add((ConvertTo-TestBgwMergeConfigurationValue -Value $item))
}
Write-Output -NoEnumerate $items.ToArray()
return
}
return $Value
}
function ConvertFrom-TestBgwMergeTemplateJson {
param([string]$JsonData)
$document = $JsonData | ConvertFrom-Json
$metadata = ConvertTo-TestBgwMergeConfigurationValue -Value $document.metadata
if ($null -eq $metadata) {
$metadata = [ordered]@{}
}
if (-not $metadata.Contains('TemplateType')) {
$metadata['TemplateType'] = $document.templateType
}
[ordered]@{
Metadata = $metadata
Parameters = ConvertTo-TestBgwMergeConfigurationValue -Value $document.parameters
Variables = ConvertTo-TestBgwMergeConfigurationValue -Value $document.variables
Resources = ConvertTo-TestBgwMergeConfigurationValue -Value $document.resources
}
}
function ConvertFrom-TestBgwMergeTargetAssignments {
param([object[]]$TargetAssignments)
@($TargetAssignments | Sort-Object sortOrder | ForEach-Object {
$nodeData = ConvertTo-TestBgwMergeConfigurationValue -Value ($_.nodeDataJson | ConvertFrom-Json)
$node = @{}
foreach ($key in $nodeData.Keys) {
if ($key -eq 'nodeName') {
$node.NodeName = $nodeData[$key]
}
else {
$node[$key] = $nodeData[$key]
}
}
$node
})
}
function ConvertTo-TestBgwMergeStableValue {
param([object]$Value)
if ($null -eq $Value) {
return $null
}
if ($Value -is [string] -or $Value -is [bool] -or $Value -is [int] -or $Value -is [long] -or $Value -is [double] -or $Value -is [decimal]) {
return $Value
}
if ($Value -is [System.Collections.IDictionary]) {
$result = [ordered]@{}
foreach ($key in @($Value.Keys | Sort-Object)) {
$result[[string]$key] = ConvertTo-TestBgwMergeStableValue -Value $Value[$key]
}
Write-Output -NoEnumerate $result
return
}
if ($Value -is [pscustomobject]) {
$result = [ordered]@{}
foreach ($property in @($Value.PSObject.Properties | Sort-Object Name)) {
$result[$property.Name] = ConvertTo-TestBgwMergeStableValue -Value $property.Value
}
Write-Output -NoEnumerate $result
return
}
if ($Value -is [System.Collections.IEnumerable] -and $Value -isnot [string]) {
$items = New-Object System.Collections.Generic.List[object]
foreach ($item in $Value) {
$items.Add((ConvertTo-TestBgwMergeStableValue -Value $item))
}
Write-Output -NoEnumerate $items.ToArray()
return
}
return $Value
}
function ConvertTo-TestBgwMergeStableJson {
param([object]$Value)
(ConvertTo-TestBgwMergeStableValue -Value $Value) | ConvertTo-Json -Depth 100 -Compress
}
function Invoke-TestBgwMergeSnapshotScript {
param([string]$Script)
$encodedCommand = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($Script))
$output = & powershell.exe -NoProfile -ExecutionPolicy Bypass -EncodedCommand $encodedCommand
if ($LASTEXITCODE -ne 0) {
throw "Snapshot PowerShell process failed with exit code [$LASTEXITCODE]. Output: $($output -join [Environment]::NewLine)"
}
$json = ($output | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) -join [Environment]::NewLine
if ([string]::IsNullOrWhiteSpace($json)) {
throw 'Snapshot PowerShell process returned no JSON output.'
}
$json | ConvertFrom-Json
}
function New-TestBgwMergeSnapshotScript {
param(
[string]$ConfigurationDataPath,
[switch]$BuildLocalBaseline
)
$escapedBgwRoot = $BgwRoot.Replace("'", "''")
$escapedMergeModulePath = $MergeModulePath.Replace("'", "''")
$escapedResolveModulePath = $ResolveModulePath.Replace("'", "''")
$escapedConfigurationDataPath = if ($ConfigurationDataPath) { $ConfigurationDataPath.Replace("'", "''") } else { '' }
$buildLocalBaselineText = if ($BuildLocalBaseline) { 'True' } else { 'False' }
@"
`$ErrorActionPreference = 'Stop'
`$ProgressPreference = 'SilentlyContinue'
Import-Module '$escapedMergeModulePath' -Force
Import-Module '$escapedResolveModulePath' -Force
function ConvertTo-SnapshotStableValue {
param([object]`$Value)
if (`$null -eq `$Value) { return `$null }
if (`$Value -is [string] -or `$Value -is [bool] -or `$Value -is [int] -or `$Value -is [long] -or `$Value -is [double] -or `$Value -is [decimal]) { return `$Value }
if (`$Value -is [System.Collections.IDictionary]) {
`$result = [ordered]@{}
foreach (`$key in @(`$Value.Keys | Sort-Object)) {
`$result[[string]`$key] = ConvertTo-SnapshotStableValue -Value `$Value[`$key]
}
Write-Output -NoEnumerate `$result
return
}
if (`$Value -is [pscustomobject]) {
`$result = [ordered]@{}
foreach (`$property in @(`$Value.PSObject.Properties | Sort-Object Name)) {
`$result[`$property.Name] = ConvertTo-SnapshotStableValue -Value `$property.Value
}
Write-Output -NoEnumerate `$result
return
}
if (`$Value -is [System.Collections.IEnumerable] -and `$Value -isnot [string]) {
`$items = New-Object System.Collections.Generic.List[object]
foreach (`$item in `$Value) {
`$items.Add((ConvertTo-SnapshotStableValue -Value `$item))
}
Write-Output -NoEnumerate `$items.ToArray()
return
}
return `$Value
}
function ConvertTo-SnapshotStableJson {
param([object]`$Value)
(ConvertTo-SnapshotStableValue -Value `$Value) | ConvertTo-Json -Depth 100 -Compress
}
if ('$buildLocalBaselineText' -eq 'True') {
`$root = '$escapedBgwRoot'
`$sourceTemplates = @(
(Join-Path -Path `$root -ChildPath 'Environment\Test.psd1')
(Join-Path -Path `$root -ChildPath 'Domain\Contoso.psd1')
(Join-Path -Path `$root -ChildPath 'Service\SharePoint\Contoso.psd1')
(Join-Path -Path `$root -ChildPath 'Stage\Install.psd1')
)
`$allNodes = @(
@{ NodeName = 'CLD-SHP-01'; RunCentralAdministration = `$true }
@{ NodeName = 'CLD-SHP-02'; RunCentralAdministration = `$false }
@{ NodeName = 'CLD-SHP-03'; RunCentralAdministration = `$false }
)
`$configurationData = New-DSCConfigurationDataDeployment -Name 'Contoso-Test-SharePoint' -DeploymentId '8f6c2c1a' -SourceTemplatePath `$sourceTemplates -AllNodes `$allNodes
}
else {
`$configurationData = Import-PowerShellDataFile -LiteralPath '$escapedConfigurationDataPath'
}
`$resolved = Resolve-DSCConfigurationData -ConfigurationData `$configurationData -SkipSecrets
`$snapshot = [ordered]@{
ParametersJson = ConvertTo-SnapshotStableJson -Value `$configurationData.Parameters
VariablesJson = ConvertTo-SnapshotStableJson -Value `$configurationData.Variables
ResourcesJson = ConvertTo-SnapshotStableJson -Value `$configurationData.Resources
DatabasePrefix = `$resolved.Variables.DatabasePrefix
ServiceDbPrefix = `$resolved.Variables.ServiceDbPrefix
ConfigDbName = `$resolved.Variables.ConfigDbName
LcmConfigurationMode = `$resolved.Resources.NonNodeData.LocalConfigurationManager.ConfigurationMode
AllNodes = @(`$resolved.Resources.AllNodes | Sort-Object NodeName | ForEach-Object {
[ordered]@{
NodeName = `$_.NodeName
RunCentralAdministration = `$_.RunCentralAdministration
}
})
}
`$snapshot | ConvertTo-Json -Depth 100 -Compress
"@
}
function New-TestBgwMergeLocalSnapshot {
Invoke-TestBgwMergeSnapshotScript -Script (New-TestBgwMergeSnapshotScript -BuildLocalBaseline)
}
function New-TestBgwMergeSnapshotFromConfigurationData {
param([hashtable]$ConfigurationData)
Import-Module $MergeModulePath -Force
$tempPath = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath ("BgwMergeApiSnapshot-{0}.psd1" -f ([guid]::NewGuid().ToString('N')))
try {
Export-PowerShellDataFile -InputObject $ConfigurationData -Path $tempPath -Force
Invoke-TestBgwMergeSnapshotScript -Script (New-TestBgwMergeSnapshotScript -ConfigurationDataPath $tempPath)
}
finally {
if (Test-Path -LiteralPath $tempPath) {
Remove-Item -LiteralPath $tempPath -Force
}
}
}
function New-TestBgwMergeApiConfigurationData {
param([object]$Composition)
Import-Module $MergeModulePath -Force
$mergedTemplateData = @{}
foreach ($selection in @($Composition.templateSelections | Sort-Object sortOrder)) {
$templateData = ConvertFrom-TestBgwMergeTemplateJson -JsonData $selection.templateVersion.jsonData
$mergedTemplateData = Merge-DSCConfigurationData -Template $mergedTemplateData -Deployment $templateData
}
$deploymentData = @{
Resources = @{
AllNodes = ConvertFrom-TestBgwMergeTargetAssignments -TargetAssignments @($Composition.targetAssignments)
}
}
Merge-DSCConfigurationData -Template $mergedTemplateData -Deployment $deploymentData
}
Describe 'BGW Test.Merge.ps1 API DemoData composition' {
BeforeAll {
$script:expectedTemplateAliases = @(
'Environment-Test'
'Domain-Contoso'
'Service-SharePoint-Contoso'
'Stage-Install'
)
$script:expectedTemplateRoles = @(
'Environment'
'Domain'
'Service'
'Stage'
)
$script:localSnapshot = New-TestBgwMergeLocalSnapshot
$script:apiComposition = Invoke-TestBgwMergeApiJson -Method GET -Path "deployment-batches/$DeploymentBatchId/composition"
$script:apiConfigurationData = New-TestBgwMergeApiConfigurationData -Composition $script:apiComposition
$script:apiSnapshot = New-TestBgwMergeSnapshotFromConfigurationData -ConfigurationData $script:apiConfigurationData
}
It 'loads the seeded deployment composition from the API' {
$script:apiComposition.deploymentBatchId | Should Be $DeploymentBatchId
@($script:apiComposition.templateSelections).Count | Should Be 4
@($script:apiComposition.targetAssignments).Count | Should Be 3
}
It 'keeps the template selection order from Test.Merge.ps1' {
$aliases = @($script:apiComposition.templateSelections | Sort-Object sortOrder | ForEach-Object { $_.alias })
$roles = @($script:apiComposition.templateSelections | Sort-Object sortOrder | ForEach-Object { $_.templateRole })
($aliases -join '|') | Should Be ($script:expectedTemplateAliases -join '|')
($roles -join '|') | Should Be ($script:expectedTemplateRoles -join '|')
}
It 'keeps the AllNodes data from Test.Merge.ps1' {
$nodes = @($script:apiSnapshot.AllNodes | Sort-Object NodeName)
$nodes.Count | Should Be 3
$nodes[0].NodeName | Should Be 'CLD-SHP-01'
$nodes[0].RunCentralAdministration | Should Be $true
$nodes[1].NodeName | Should Be 'CLD-SHP-02'
$nodes[1].RunCentralAdministration | Should Be $false
$nodes[2].NodeName | Should Be 'CLD-SHP-03'
$nodes[2].RunCentralAdministration | Should Be $false
}
It 'resolves the same core values as the local Test.Merge.ps1 baseline' {
$script:apiSnapshot.DatabasePrefix | Should Be $script:localSnapshot.DatabasePrefix
$script:apiSnapshot.ServiceDbPrefix | Should Be $script:localSnapshot.ServiceDbPrefix
$script:apiSnapshot.ConfigDbName | Should Be $script:localSnapshot.ConfigDbName
$script:apiSnapshot.LcmConfigurationMode | Should Be $script:localSnapshot.LcmConfigurationMode
}
It 'matches merged Parameters, Variables and Resources' {
$script:apiSnapshot.ParametersJson | Should Be $script:localSnapshot.ParametersJson
$script:apiSnapshot.VariablesJson | Should Be $script:localSnapshot.VariablesJson
$script:apiSnapshot.ResourcesJson | Should Be $script:localSnapshot.ResourcesJson
}
}