Engines/Terraform/Invoke-AvmTerraformTransform.ps1
|
function Resolve-AvmMapotfConfigDir { <# .SYNOPSIS Resolve a scoped directory holding vendored mapotf configs. .DESCRIPTION Returns the absolute path to the '*.mptf.hcl' bundle passed to 'mapotf transform --mptf-dir'. Resolution order: 1. $env:AVM_MPTF_CONFIG_DIR/<profile>. 2. <Root>/config/mapotf/<profile>. 3. <ModuleRoot>/Resources/mapotf/<profile>. Each candidate must be a directory containing at least one '*.mptf.hcl' file. Throws AvmConfigurationException when none resolve, so the transform engine surfaces as 'skipped' (a deliberate placeholder) rather than running mapotf against an empty config set. .PARAMETER Root The consumer repository root, used to locate an optional 'config/mapotf/<profile>' override. Pass $Context.Root. .PARAMETER Profile Config profile to resolve: common, module, root, module-call, variable-order, example, provider-cleanup, location-inspect, unit-test-inspect, unit-test, or an optional test override. .PARAMETER Optional Return $null instead of throwing when the profile does not exist. .OUTPUTS [string] absolute path to the resolved config directory. #> [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory)] [string] $Root, [Parameter(Mandatory)] [ValidateSet('common', 'module', 'root', 'module-call', 'variable-order', 'example', 'provider-cleanup', 'test', 'location-inspect', 'unit-test-inspect', 'unit-test')] [string] $ProfileName, [switch] $Optional ) Set-StrictMode -Version 3.0 $ErrorActionPreference = 'Stop' $candidates = New-Object System.Collections.Generic.List[string] if ($env:AVM_MPTF_CONFIG_DIR) { $candidates.Add((Join-Path $env:AVM_MPTF_CONFIG_DIR $ProfileName)) } $candidates.Add((Join-Path $Root (Join-Path 'config' (Join-Path 'mapotf' $ProfileName)))) $moduleRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) $candidates.Add((Join-Path $moduleRoot (Join-Path 'Resources' (Join-Path 'mapotf' $ProfileName)))) foreach ($candidate in $candidates) { if (-not $candidate) { continue } if (-not (Test-Path -LiteralPath $candidate -PathType Container)) { continue } $configs = @(Get-ChildItem -LiteralPath $candidate -Filter '*.mptf.hcl' -File -ErrorAction SilentlyContinue) if ($configs.Count -gt 0) { return (Resolve-Path -LiteralPath $candidate).ProviderPath } } if ($Optional) { return $null } throw [AvmConfigurationException]::new( ("Cannot resolve the mapotf '{0}' config profile (looked in: {1}). " -f $ProfileName, ($candidates -join '; ')) + ("The profile normally ships inside the module under Resources/mapotf/{0}; " -f $ProfileName) + ("set AVM_MPTF_CONFIG_DIR or add config/mapotf/{0}/*.mptf.hcl to override it." -f $ProfileName)) } function Get-AvmTerraformFile { <# .SYNOPSIS Enumerate the '*.tf' files mapotf would touch under a module root. .DESCRIPTION Returns FileInfo records for every '*.tf' and '*.tftest.hcl' file beneath $Root, excluding any path segment that begins with '.' (e.g. '.terraform', '.git') or equals 'node_modules'. Used by Invoke-AvmTerraformTransform to snapshot file hashes before/after the transform so the engine can report which files mapotf changed. Always returns an array (empty when nothing matches) so callers can rely on '.Count'. .PARAMETER Root The module root to walk. .OUTPUTS [object[]] of System.IO.FileInfo. #> [CmdletBinding()] [OutputType([object[]])] param( [Parameter(Mandatory)] [string] $Root ) Set-StrictMode -Version 3.0 $ErrorActionPreference = 'Stop' $terraformFiles = @(Get-ChildItem -LiteralPath $Root -Recurse -File -Filter '*.tf' -ErrorAction SilentlyContinue) $testFiles = @(Get-ChildItem -LiteralPath $Root -Recurse -File -Filter '*.tftest.hcl' -ErrorAction SilentlyContinue) return @( @($terraformFiles + $testFiles) | Where-Object { $rel = [System.IO.Path]::GetRelativePath($Root, $_.FullName) $parts = $rel -split '[\\/]' -not ($parts | Where-Object { $_.StartsWith('.') -or $_ -eq 'node_modules' }) } ) } function Get-AvmTerraformTransformTarget { [CmdletBinding()] [OutputType([pscustomobject[]])] param( [Parameter(Mandatory)] [string] $Root ) Set-StrictMode -Version 3.0 $ErrorActionPreference = 'Stop' $targets = New-Object System.Collections.Generic.List[object] $scopes = @(Get-AvmMetadataScope -Context ([pscustomobject]@{ Root = $Root Ecosystem = 'terraform' })) foreach ($scope in $scopes) { if ($scope.ChildModule -and @(Get-ChildItem -LiteralPath $scope.Path -File -Filter '*.tf').Count -eq 0) { continue } $metadataPath = Join-Path $scope.Path 'metadata.json' $metadataFile = @(Get-ChildItem -LiteralPath $scope.Path -File | Where-Object { $_.Name -ceq 'metadata.json' }) if ($metadataFile.Count -eq 0) { throw [AvmConfigurationException]::new( "Terraform module '$($scope.Path)' requires metadata.json before telemetry transformation.") } try { $metadata = Get-Content -LiteralPath $metadataPath -Raw -ErrorAction Stop | ConvertFrom-Json -AsHashtable -ErrorAction Stop } catch { throw [AvmConfigurationException]::new( "Cannot read Terraform module metadata '$metadataPath': $($_.Exception.Message)") } if ($metadata -isnot [System.Collections.IDictionary]) { throw [AvmConfigurationException]::new( "Terraform module metadata '$metadataPath' must contain a JSON object.") } $prefix = [string]$metadata['telemetryIdPrefix'] if ($metadata.Contains('telemetryIdPrefix') -and [string]::IsNullOrWhiteSpace($prefix)) { throw [AvmConfigurationException]::new( "Terraform module metadata '$metadataPath' has an empty telemetryIdPrefix.") } if ($prefix -and $prefix -cnotmatch '^46d3xtrf\.(res|ptn|utl)\.[0-9a-f]{7}$') { throw [AvmConfigurationException]::new( "Terraform module metadata '$metadataPath' telemetryIdPrefix must end in seven lowercase hexadecimal characters.") } $profiles = if ($prefix) { @('root', 'module', 'common') } else { @('module', 'common') } $targets.Add([pscustomobject]@{ Path = $scope.Path Scope = if ($scope.ChildModule) { 'module' } else { 'root' } Profiles = $profiles }) } $examplesDir = Join-Path $Root 'examples' if (Test-Path -LiteralPath $examplesDir -PathType Container) { foreach ($example in Get-ChildItem -LiteralPath $examplesDir -Directory -ErrorAction SilentlyContinue | Sort-Object Name) { $terraformFiles = @(Get-ChildItem -LiteralPath $example.FullName -File -Filter '*.tf' -ErrorAction SilentlyContinue) if ($terraformFiles.Count -eq 0) { continue } $targets.Add([pscustomobject]@{ Path = $example.FullName Scope = 'example' Profiles = @('example', 'provider-cleanup', 'common') }) } } foreach ($moduleTarget in @($targets | Where-Object { $_.Profiles -contains 'root' })) { $testsDir = Join-Path $moduleTarget.Path 'tests' if (Test-Path -LiteralPath $testsDir -PathType Container) { $testRoots = Get-ChildItem -LiteralPath $testsDir -Recurse -File -Filter '*.tf' -ErrorAction SilentlyContinue | ForEach-Object { $_.Directory.FullName } | Sort-Object -Unique foreach ($testRoot in $testRoots) { $rel = [System.IO.Path]::GetRelativePath($testsDir, $testRoot) if (@($rel -split '[\\/]' | Where-Object { $_.StartsWith('.') -or $_ -eq 'node_modules' }).Count -gt 0) { continue } $targets.Add([pscustomobject]@{ Path = $testRoot Scope = 'test' Profiles = @('module-call', 'variable-order', 'provider-cleanup', 'test') }) } } } return $targets.ToArray() } function Test-AvmTerraformProviderInUse { [CmdletBinding()] [OutputType([bool])] param( [Parameter(Mandatory)] [string] $ModulePath, [Parameter(Mandatory)] [object[]] $ModuleTargets, [Parameter(Mandatory)] [string] $TestDirectory, [Parameter(Mandatory)] [ValidateSet('modtm', 'random')] [string] $Provider ) Set-StrictMode -Version 3.0 $ErrorActionPreference = 'Stop' $modulePrefix = $ModulePath + [System.IO.Path]::DirectorySeparatorChar $directories = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) foreach ($target in $ModuleTargets) { if ($target.Profiles -contains 'example' -and $target.Path -cne $ModulePath) { continue } if ($target.Path -cne $ModulePath -and -not $target.Path.StartsWith($modulePrefix, [System.StringComparison]::Ordinal)) { continue } $null = $directories.Add($target.Path) } $null = $directories.Add($TestDirectory) foreach ($directory in Get-ChildItem -LiteralPath $TestDirectory -Recurse -Directory) { $relativePath = [System.IO.Path]::GetRelativePath($TestDirectory, $directory.FullName) if (@($relativePath -split '[\\/]' | Where-Object { $_.StartsWith('.') -or $_ -eq 'node_modules' }).Count -eq 0) { $null = $directories.Add($directory.FullName) } } foreach ($directory in $directories) { foreach ($file in Get-ChildItem -LiteralPath $directory -File) { if ($file.Name.EndsWith('.tf.json', [System.StringComparison]::OrdinalIgnoreCase)) { try { Assert-AvmTerraformLocalsOnlyJson -Path $file.FullName ` -ReviewMessage "Preserving $Provider mock: JSON configuration needs dependency review" } catch [AvmConfigurationException] { Write-AvmLog $_.Exception.Message -Level Verbose | Out-Null return $true } continue } if (-not $file.Name.EndsWith('.tf', [System.StringComparison]::OrdinalIgnoreCase)) { continue } $source = [System.IO.File]::ReadAllText($file.FullName) if ($source -match ('(?m)^[ \t]*' + $Provider + '[ \t]*=[ \t]*\{') -or $source -match ('(?:hashicorp|azure)/' + $Provider) -or $source -match ('(?m)^[ \t]*(?:resource|data|ephemeral)[ \t]+"' + $Provider + '_[^"]+"[ \t]+"[^"]+"')) { return $true } $moduleBlocks = [regex]::Matches($source, '(?m)^[ \t]*module[ \t]+"[^"]+"[ \t]*\{') if ([regex]::Matches($source, '(?m)^[ \t]*module\b(?!\.)').Count -ne $moduleBlocks.Count) { Write-AvmLog "Preserving $Provider mock: module declarations in '$($file.FullName)' need dependency review." -Level Verbose | Out-Null return $true } foreach ($block in $moduleBlocks) { $sourceMatch = [regex]::Match( $source.Substring($block.Index + $block.Length), '\A\s*source[ \t]*=[ \t]*"(?<path>\.\.?/[^"]+)"') if (-not $sourceMatch.Success) { Write-AvmLog "Preserving $Provider mock: a module source in '$($file.FullName)' is not a known local dependency." -Level Verbose | Out-Null return $true } $dependencyPath = [System.IO.Path]::GetFullPath($sourceMatch.Groups['path'].Value, $directory) if (-not $directories.Contains([System.IO.Path]::TrimEndingDirectorySeparator($dependencyPath))) { Write-AvmLog "Preserving $Provider mock: module dependency '$dependencyPath' was not scanned." -Level Verbose | Out-Null return $true } } } } return $false } function Test-AvmTerraformScopedProviderInUse { [CmdletBinding()] [OutputType([bool])] param( [Parameter(Mandatory)] [object] $Scope, [Parameter(Mandatory)] [object[]] $ModuleTargets, [AllowEmptyCollection()] [string[]] $TargetPaths = @(), [Parameter(Mandatory)] [hashtable] $Cache, [Parameter(Mandatory)] [ValidateSet('modtm', 'random')] [string] $Provider ) Set-StrictMode -Version 3.0 $ErrorActionPreference = 'Stop' $testDirectory = Split-Path -Parent $Scope.File.FullName $ownerPrefix = $Scope.Owner.Path + [System.IO.Path]::DirectorySeparatorChar $ownerScannedPaths = @($ModuleTargets | Where-Object { $_.Profiles -notcontains 'example' -and $_.Path.StartsWith($ownerPrefix, [System.StringComparison]::Ordinal) } | ForEach-Object { $_.Path }) foreach ($path in @($Scope.Owner.Path) + $TargetPaths) { if ($path -cin $ownerScannedPaths) { continue } $key = "$Provider|$path|$testDirectory" if (-not $Cache.ContainsKey($key)) { $Cache[$key] = Test-AvmTerraformProviderInUse ` -ModulePath $path -ModuleTargets $ModuleTargets -TestDirectory $testDirectory -Provider $Provider } if ($Cache[$key]) { return $true } } return $false } function Test-AvmTerraformScopedRandomProviderInUse { [CmdletBinding()] [OutputType([bool])] param( [Parameter(Mandatory)] [object] $Scope, [Parameter(Mandatory)] [object[]] $ModuleTargets, [AllowEmptyCollection()] [string[]] $TargetPaths = @(), [Parameter(Mandatory)] [hashtable] $Cache ) Test-AvmTerraformScopedProviderInUse -Scope $Scope -ModuleTargets $ModuleTargets ` -TargetPaths $TargetPaths -Cache $Cache -Provider random } function Remove-AvmLegacyTelemetryTestMock { [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory)] [string] $Root, [Parameter(Mandatory)] [object[]] $ModuleTargets, [object[]] $UnitTestPlans = @() ) Set-StrictMode -Version 3.0 $ErrorActionPreference = 'Stop' $changes = [System.Collections.Generic.List[object]]::new() $emptyMock = '(?m)^[ \t]*mock_provider[ \t]+"modtm"[ \t]*\{[ \t\r\n]*\}[ \t]*(?:\r?\n)?' $customMock = '(?m)^[ \t]*mock_provider[ \t]+"modtm"[ \t]*\{' $azapiMock = '(?m)^[ \t]*mock_provider[ \t]+"azapi"[ \t]*\{' $emptyAzapiMock = '(?m)^[ \t]*mock_provider[ \t]+"azapi"[ \t]*\{[ \t\r\n]*\}[ \t]*(?:\r?\n)?' $legacyReference = '(?<![A-Za-z0-9_])modtm_telemetry\.telemetry(?![A-Za-z0-9_])' $legacyIdentifierReference = '(?<![A-Za-z0-9_-])random_uuid(?:\s|/\*[\s\S]*?\*/|//[^\r\n]*|#[^\r\n]*)*\.' + '(?:\s|/\*[\s\S]*?\*/|//[^\r\n]*|#[^\r\n]*)*telemetry(?![A-Za-z0-9_-])' $emptyRandomMock = '(?m)^[ \t]*mock_provider[ \t]+"random"[ \t]*\{[ \t\r\n]*\}[ \t]*(?:\r?\n)?' $randomMock = '(?m)^[ \t]*mock_provider[ \t]+"random"[ \t]*\{' $randomUse = @{} $modtmUse = @{} foreach ($scope in Get-AvmTerraformTestFileScope -Root $Root -ModuleTargets $ModuleTargets) { $file = $scope.File $owner = $scope.Owner $unitPlan = $UnitTestPlans | Where-Object { $_.Path -ceq $file.FullName } | Select-Object -First 1 $validatedMocks = $null -ne $unitPlan -and $null -ne $unitPlan.PSObject.Properties['MockMigrationValidated'] -and $unitPlan.MockMigrationValidated -eq $true $retiredProviderReferences = if ($validatedMocks) { $unitPlan.RetiredProviderReferences } else { @{ modtm = 0; random = 0 } } $instrumentedTargets = @($ModuleTargets | Where-Object { $null -ne $unitPlan -and $_.Path -cin $unitPlan.TargetPaths -and $_.Profiles -contains 'root' }) if ($owner.Profiles -notcontains 'root' -and $instrumentedTargets.Count -eq 0) { continue } $original = [System.IO.File]::ReadAllText($file.FullName) $identifierReferences = [regex]::Matches($original, $legacyIdentifierReference, [System.Text.RegularExpressions.RegexOptions]::CultureInvariant, [TimeSpan]::FromSeconds(1)).Count $validatedIdentifierReferences = if ($validatedMocks -and $null -ne $unitPlan.PSObject.Properties['TelemetryCountMigration']) { $unitPlan.TelemetryCountMigration.ReferenceCount } else { 0 } if ($identifierReferences -ne $validatedIdentifierReferences) { throw [AvmConfigurationException]::new( "Test file '$($file.FullName)' still references retired random_uuid.telemetry; only complete zero-or-one count assertions, optionally prefixed by a ModTM or AzAPI telemetry-count assertion joined with &&, on instrumented unit-test targets can be migrated automatically. Review other references manually.") } $isUnitTest = $scope.IsUnitTest $retiredMock = [regex]::Match($original, $emptyMock) $withoutModtmMocks = [regex]::Replace($original, $emptyMock, '') if ([regex]::IsMatch($withoutModtmMocks, $customMock) -and -not ($validatedMocks -and $null -ne $unitPlan.PSObject.Properties['RetainModtmMock'] -and $unitPlan.RetainModtmMock)) { throw [AvmConfigurationException]::new( "Test file '$($file.FullName)' contains a non-empty modtm mock; remove or rewrite it manually before telemetry migration.") } $retainModtm = if ($null -ne $unitPlan -and $null -ne $unitPlan.PSObject.Properties['RetainModtmMock']) { $unitPlan.RetainModtmMock } else { $targetPaths = @() if ($null -ne $unitPlan) { $targetPaths = @($unitPlan.TargetPaths) } Test-AvmTerraformScopedProviderInUse -Scope $scope -ModuleTargets $ModuleTargets ` -TargetPaths $targetPaths -Cache $modtmUse -Provider modtm } $updated = if ($retainModtm) { $original } else { $withoutModtmMocks } $retainedModtmReferences = if ($validatedMocks -and $null -ne $unitPlan.PSObject.Properties['RetainedProviderReferences']) { $unitPlan.RetainedProviderReferences.modtm } else { 0 } $emptyAzapi = [regex]::Match($updated, $emptyAzapiMock) $hasAzapiMock = [regex]::IsMatch($updated, $azapiMock) $needsAzapiMock = ($retiredMock.Success -and -not $hasAzapiMock) -or ($validatedMocks -and $unitPlan.AddDefaultAzapiMock) if ($isUnitTest -and ($retiredMock.Success -or $emptyAzapi.Success -or $validatedMocks)) { $modtmReferences = Get-AvmTerraformRetiredTestProviderReferenceCount -Text $updated -Provider modtm if ($updated -match '(?m)^[ \t]*provider[ \t]+"azapi"[ \t]*\{' -or ($null -eq $unitPlan -and $updated -match '(?m)^[ \t]*module[ \t]*\{') -or $modtmReferences -ne ($retiredProviderReferences.modtm + $retainedModtmReferences) -or (-not $validatedMocks -and $hasAzapiMock -and $updated -match '\balias[ \t]*=')) { throw [AvmConfigurationException]::new( "Cannot automatically migrate telemetry mocks in '$($file.FullName)': an authored provider, alias, provider mapping, or delegated test module needs review.") } } if ($isUnitTest -and ($needsAzapiMock -or $emptyAzapi.Success)) { $lineEnding = if ($original.Contains("`r`n")) { "`r`n" } else { "`n" } $replacementMock = @' mock_provider "azapi" { mock_data "azapi_client_config" { defaults = { subscription_resource_id = "/subscriptions/00000000-0000-0000-0000-000000000000" } } } '@ $replacementMock = ($replacementMock.Replace("`r`n", "`n").TrimEnd() + "`n").Replace("`n", $lineEnding) if ($emptyAzapi.Success) { $updated = $updated.Remove($emptyAzapi.Index, $emptyAzapi.Length).Insert($emptyAzapi.Index, $replacementMock) } else { $index = if ($retiredMock.Success) { $retiredMock.Index } else { 0 } $updated = $updated.Insert($index, $replacementMock) } } $updated = [regex]::Replace($updated, $legacyReference, 'azapi_resource.telemetry') if ($isUnitTest -and [regex]::IsMatch($updated, $randomMock)) { $targetPaths = @() if ($null -ne $unitPlan) { $targetPaths = @($unitPlan.TargetPaths) } $randomInUse = Test-AvmTerraformScopedRandomProviderInUse -Scope $scope ` -ModuleTargets $ModuleTargets -TargetPaths $targetPaths -Cache $randomUse if (-not $randomInUse) { $withoutEmptyMocks = [regex]::Replace($updated, $emptyRandomMock, '') $randomReferences = Get-AvmTerraformRetiredTestProviderReferenceCount -Text $withoutEmptyMocks -Provider random if ([regex]::IsMatch($withoutEmptyMocks, $randomMock) -or $randomReferences -ne $retiredProviderReferences.random -or ($null -eq $unitPlan -and $withoutEmptyMocks -match '(?m)^[ \t]*module[ \t]*\{')) { throw [AvmConfigurationException]::new( "Test file '$($file.FullName)' still uses a random mock or resource after the telemetry-only random provider was removed; review it manually.") } $updated = $withoutEmptyMocks } } if ($updated -cne $original) { $changes.Add([pscustomobject]@{ Path = $file.FullName; Content = $updated }) } } $encoding = [System.Text.UTF8Encoding]::new($false) foreach ($change in $changes) { if ($PSCmdlet.ShouldProcess($change.Path, 'migrate retired telemetry test mocks and references')) { [System.IO.File]::WriteAllText($change.Path, $change.Content, $encoding) } } } function Get-AvmTerraformLocationInspection { [CmdletBinding()] [OutputType([hashtable])] param( [Parameter(Mandatory)][string] $Path, [Parameter(Mandatory)][object] $Options ) Set-StrictMode -Version 3.0 $ErrorActionPreference = 'Stop' $result = Invoke-AvmProcess -FilePath $Options.ToolPath -ArgumentList @( 'debug', '--tf-dir', $Path, '--mptf-dir', $Options.ProfileDirs['location-inspect'], '--eval', 'data.variable.location.result' ) -WorkingDirectory $Path -EnvVars $Options.EnvVars try { $inspection = ConvertFrom-Json -InputObject $result.StdOut -AsHashtable -ErrorAction Stop } catch { throw [AvmConfigurationException]::new( "Cannot inspect the location input in '$Path': invalid MaPoTF JSON. $($_.Exception.Message)") } if ($inspection -isnot [System.Collections.IDictionary] -or $inspection.Count -gt 1 -or ($inspection.Count -eq 1 -and -not $inspection.Contains('location'))) { throw [AvmConfigurationException]::new("Cannot inspect the location input in '$Path': invalid variable inspection.") } if ($inspection.Count -eq 0) { return $null } $declaration = $inspection['location'] if ($declaration -isnot [System.Collections.IDictionary] -or $declaration['mptf'] -isnot [System.Collections.IDictionary] -or $declaration.mptf['block_address'] -cne 'variable.location' -or $declaration.mptf['range'] -isnot [System.Collections.IDictionary]) { throw [AvmConfigurationException]::new("Cannot inspect the location input in '$Path': missing native source metadata.") } return $declaration } function Set-AvmRequiredLocationLintDirective { [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory)][object[]] $Targets, [Parameter(Mandatory)][object] $Options ) Set-StrictMode -Version 3.0 $ErrorActionPreference = 'Stop' $directive = '# tflint-ignore: terraform_unused_declarations' foreach ($target in $Targets) { $declaration = Get-AvmTerraformLocationInspection -Path $target.Path -Options $Options if ($null -eq $declaration -or $declaration.Count -ne 4 -or $declaration['type'] -cne 'string' -or $declaration['nullable'] -isnot [bool] -or $declaration.nullable -or $declaration['description'] -cne "The Azure region for this module's resources or telemetry deployment." -or $declaration.mptf.range['file_name'] -cne 'variables.tf') { continue } $path = Join-Path $target.Path 'variables.tf' $content = [System.IO.File]::ReadAllText($path) $lines = @($content -split '\r?\n') $startLine = $declaration.mptf.range['start_line'] if (($startLine -isnot [int] -and $startLine -isnot [long]) -or $startLine -lt 1 -or $startLine -gt $lines.Count -or $lines[$startLine - 1] -cne 'variable "location" {') { throw [AvmConfigurationException]::new("Cannot annotate the generated location input in '$path': invalid native source range.") } if ($startLine -gt 1 -and $lines[$startLine - 2] -ceq $directive) { continue } $offset = [regex]::Matches($content, '(?m)^')[$startLine - 1].Index $lineEnding = if ($content.Contains("`r`n")) { "`r`n" } else { "`n" } if ($PSCmdlet.ShouldProcess($path, 'exempt the newly generated mandatory location input from the unused-declaration rule')) { [System.IO.File]::WriteAllText( $path, $content.Insert($offset, $directive + $lineEnding), [System.Text.UTF8Encoding]::new($false)) } } } function Set-AvmTelemetryTagLintDirective { [CmdletBinding(SupportsShouldProcess)] param([Parameter(Mandatory)][object[]] $Targets) Set-StrictMode -Version 3.0 $ErrorActionPreference = 'Stop' $directive = '# tflint-ignore: avm_azapi_resource_tags_required' $encoding = [System.Text.UTF8Encoding]::new($false) foreach ($target in $Targets | Where-Object { $_.Profiles -contains 'root' }) { $path = Join-Path $target.Path 'main.telemetry.tf' if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { throw [AvmConfigurationException]::new("Instrumented Terraform module '$($target.Path)' has no main.telemetry.tf.") } $original = [System.IO.File]::ReadAllText($path) $content = $original $resources = [regex]::Matches( $content, '(?ms)^resource[ \t]+"azapi_resource"[ \t]+"telemetry"[ \t]*\{(?<body>.*?)^\}') if ($resources.Count -ne 1) { throw [AvmConfigurationException]::new("Expected one azapi_resource.telemetry block in '$path'.") } $body = $resources[0].Groups['body'] $cleanBody = [regex]::Replace( $body.Value, '(?m)^[ \t]*# tflint-ignore: avm_azapi_resource_tags_required\r?\n', '') if ($cleanBody -cne $body.Value) { $content = $content.Remove($body.Index, $body.Length).Insert($body.Index, $cleanBody) $resources = [regex]::Matches( $content, '(?ms)^resource[ \t]+"azapi_resource"[ \t]+"telemetry"[ \t]*\{(?<body>.*?)^\}') } $offset = $resources[0].Index $priorLines = @($content.Substring(0, $offset) -split '\r?\n') if ($priorLines.Count -lt 2 -or $priorLines[-2] -cne $directive) { $lineEnding = if ($content.Contains("`r`n")) { "`r`n" } else { "`n" } $content = $content.Insert($offset, $directive + $lineEnding) } if ($content -cne $original -and $PSCmdlet.ShouldProcess($path, 'exempt generated telemetry deployment from the standard resource-tag rule')) { [System.IO.File]::WriteAllText($path, $content, $encoding) } } } function Get-AvmRemainingModtmIssue { [CmdletBinding()] [OutputType([pscustomobject[]])] param( [Parameter(Mandatory)] [string] $Root, [Parameter(Mandatory)] [object[]] $Targets ) Set-StrictMode -Version 3.0 $issues = [System.Collections.Generic.List[object]]::new() foreach ($target in $Targets | Where-Object { $_.Profiles -contains 'root' -or $_.Scope -in @('test', 'example') }) { foreach ($file in Get-ChildItem -LiteralPath $target.Path -Filter '*.tf' -File) { $lineNumber = 0 foreach ($line in [System.IO.File]::ReadAllLines($file.FullName)) { $lineNumber++ if ($line -cmatch '^\s*(resource|data)\s+"modtm_[^"]+"\s+"[^"]+"') { $issues.Add([pscustomobject][ordered]@{ File = [System.IO.Path]::GetRelativePath($Root, $file.FullName) Line = $lineNumber Column = 1 Severity = 'error' Code = 'avm.tf.modtm-remains' Message = 'An author-owned modtm block remains after telemetry migration; migrate it before removing the modtm provider.' }) } } } } return $issues.ToArray() } function Invoke-AvmMapotfTransformTarget { [CmdletBinding()] param( [Parameter(Mandatory)] [object] $Target, [Parameter(Mandatory)] [object] $Options ) Set-StrictMode -Version 3.0 $ErrorActionPreference = 'Stop' $transformArguments = New-Object System.Collections.Generic.List[string] $transformArguments.Add('transform') foreach ($profileName in $Target.Profiles) { $profileDir = $Options.ProfileDirs[$profileName] if (-not $profileDir) { continue } $transformArguments.Add('--mptf-dir') $transformArguments.Add($profileDir) } $transformArguments.Add('--tf-dir') $transformArguments.Add($Target.Path) if ($Target.Scope -eq 'test' -and $Target.Profiles -contains 'module-call') { $transformArguments.Add('--mptf-var') $transformArguments.Add('test_wrapper=true') } $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() $transform = Invoke-AvmProcess ` -FilePath $Options.ToolPath ` -ArgumentList $transformArguments.ToArray() ` -WorkingDirectory $Target.Path ` -EnvVars $Options.EnvVars ` -IgnoreExitCode ` -RetryNetworkFailure ` -Label ('mapotf transform {0} target' -f $Target.Scope) if ($transform.ExitCode -ne 0) { $message = Add-AvmProcessFailureDetail ` -Message ('mapotf transform exited with code {0} for {1} target {2}.' -f $transform.ExitCode, $Target.Scope, $Target.Path) ` -StdOut $transform.StdOut ` -StdErr $transform.StdErr throw [AvmProcessException]::new($message) } $stopwatch.Stop() Write-AvmLog ( 'transform: {0} target completed in {1}: {2}' -f $Target.Scope, (Format-AvmDuration -Duration $stopwatch.Elapsed), $Target.Path ) -Level Verbose | Out-Null } function Invoke-AvmTerraformTransform { <# .SYNOPSIS Apply the AVM mapotf HCL transforms to a Terraform module. .DESCRIPTION Engine implementation called by Invoke-AvmTransform when the module context is Ecosystem='terraform'. Resolves the 'mapotf' binary via Resolve-AvmTool and the vendored config bundle via Resolve-AvmMapotfConfigDir, then runs, against $Context.Root: mapotf transform --mptf-dir <profile> [...] --tf-dir <target> mapotf clean-backup --tf-dir <root> Metadata-backed roots and child modules with a telemetryIdPrefix run root, module, common. Children without a prefix run module, common. Module calls then run module-call, variable-order, common from deepest to root after every child has its inputs, forwarding location and enable_telemetry where supported. That pass sorts variables after missing location inputs are generated, keeping the first transform idempotent. Native test-file inspection snapshots local run targets and input declarations before transformation. Direct provider-mocked unit runs receive a test-only location only when their local target gained a required input and neither global nor run variables already set it. Unknown run targets are rejected rather than rewritten. Examples run example, provider-cleanup, common after the module calls have settled. Standalone test-module directories run module-call, variable-order, provider-cleanup and an optional consumer test profile, deepest first. Their module calls receive only missing required locations; authored locations, optional defaults and telemetry opt-outs remain unchanged. Empty modtm test mocks are retired only when selected targets no longer require their provider. Unchanged dependencies retain the mock, its proven source requirement and same-provider module bindings, including known local callers and example wrappers, without restoring unrelated unused requirements. Proven same-provider aliases and local literal source client defaults retain their authored values. Telemetry references and empty random mocks in direct unit tests with no remaining random provider usage are migrated. Custom mocks that cannot be removed safely fail with an actionable error. The final call removes '*.tf.mptfbackup' files. Several of the vendored configs (e.g. order_resource_attrs) read provider schemas, so mapotf shells out to 'terraform init' + 'terraform providers schema'. mapotf locates 'terraform' by name on PATH, but GitHub-hosted runners no longer ship terraform on PATH (it was removed from the images). The engine therefore resolves the pinned terraform via Resolve-AvmTool and prepends its directory to PATH for the mapotf subprocess; environment variables propagate to mapotf's own terraform grandchild, so the schema reads succeed against the managed binary. A terraform that cannot be resolved (AvmToolException) propagates so the chain reports 'skipped', matching missing-mapotf. File-hash snapshots taken before and after the transform populate the 'Changed' field (relative paths of every '*.tf' or '*.tftest.hcl' file added, removed or modified). Drift mode (-CheckDrift, used by pr-check): mapotf has no dry-run, so the transform still runs and any 'Changed' file becomes a Status='fail' Issue. The transformed content is then rolled back, so drift mode leaves the working copy byte-identical. The contract is "a module that already ran pre-commit has nothing for mapotf to change"; a non-empty change set in CI therefore means the author did not run pre-commit, and pr-check flags it. Root and local-module targets finish before module calls and examples inspect their inputs. Each group runs through the bounded Invoke-AvmParallel scheduler. Mapotf uses each target's working-directory providers rather than TF_PLUGIN_CACHE_DIR, allowing independent targets and separate AVM processes to run without racing Terraform's shared provider cache. mapotf exit codes: 0 = success. A transform failure caused by a transient network error, such as a provider download timeout, is retried with the shared network retry policy; other failures and retry exhaustion surface as AvmProcessException. A missing mapotf binary (AvmToolException) or a missing config bundle (AvmConfigurationException) propagates so the composition chain reports the step as 'skipped' on an unconfigured workstation. .PARAMETER Context Module context produced by Get-AvmModuleContext. Must have Ecosystem='terraform'. .PARAMETER AllowPathFallback Pass through to Resolve-AvmTool. .PARAMETER CheckDrift When set, treat any file mapotf changed as a failure (Status='fail' with one Issue per changed file) instead of a silent fix. Used by the pr-check chain. .PARAMETER ThrottleLimit Maximum number of independent root, module, or example targets to transform at once. Defaults to one for direct engine calls. .OUTPUTS pscustomobject with Engine, Tool, ToolPath, ToolSource, Status, FilesProcessed, Changed, Issues. #> [CmdletBinding(SupportsShouldProcess)] [OutputType([pscustomobject])] param( [Parameter(Mandatory)] $Context, [switch] $AllowPathFallback, [switch] $CheckDrift, [ValidateRange(1, 32)] [int] $ThrottleLimit = 1 ) Set-StrictMode -Version 3.0 $ErrorActionPreference = 'Stop' if ($Context.Ecosystem -ne 'terraform') { throw [System.ArgumentException]::new( "Invoke-AvmTerraformTransform requires a terraform context (got Ecosystem='$($Context.Ecosystem)').") } $tool = Resolve-AvmTool -Name 'mapotf' -ModuleRoot $Context.Root -AllowPathFallback:$AllowPathFallback $profileDirs = @{ common = Resolve-AvmMapotfConfigDir -Root $Context.Root -ProfileName 'common' module = Resolve-AvmMapotfConfigDir -Root $Context.Root -ProfileName 'module' root = Resolve-AvmMapotfConfigDir -Root $Context.Root -ProfileName 'root' 'module-call' = Resolve-AvmMapotfConfigDir -Root $Context.Root -ProfileName 'module-call' 'variable-order' = Resolve-AvmMapotfConfigDir -Root $Context.Root -ProfileName 'variable-order' example = Resolve-AvmMapotfConfigDir -Root $Context.Root -ProfileName 'example' 'provider-cleanup' = Resolve-AvmMapotfConfigDir -Root $Context.Root -ProfileName 'provider-cleanup' 'location-inspect' = Resolve-AvmMapotfConfigDir -Root $Context.Root -ProfileName 'location-inspect' 'unit-test-inspect' = Resolve-AvmMapotfConfigDir -Root $Context.Root -ProfileName 'unit-test-inspect' 'unit-test' = Resolve-AvmMapotfConfigDir -Root $Context.Root -ProfileName 'unit-test' test = Resolve-AvmMapotfConfigDir -Root $Context.Root -ProfileName 'test' -Optional } $targets = @(Get-AvmTerraformTransformTarget -Root $Context.Root) Write-AvmLog ("transform: discovered {0} target(s)" -f $targets.Count) -Level Verbose | Out-Null $beforeFiles = Get-AvmTerraformFile -Root $Context.Root Write-AvmLog ("transform: discovered {0} terraform file(s)" -f $beforeFiles.Count) -Level Verbose | Out-Null if (-not $PSCmdlet.ShouldProcess($Context.Root, ("mapotf transform across {0} scoped target(s)" -f $targets.Count))) { return [pscustomobject][ordered]@{ Engine = 'terraform' Tool = ('{0}/{1}' -f $tool.Name, $tool.Version) ToolPath = $tool.Path ToolSource = $tool.Source Status = 'skipped' FilesProcessed = $beforeFiles.Count Changed = @() Issues = @() } } $before = @{} foreach ($f in $beforeFiles) { $before[$f.FullName] = (Get-FileHash -LiteralPath $f.FullName -Algorithm SHA256).Hash } # Drift mode must not mutate the caller's working copy. mapotf has no # dry-run, so snapshot Terraform and test files up front and roll back in the finally - # drift is still computed from the post-transform tree, but the tree is left # byte-identical even if mapotf throws part-way through. $snapshot = $null $legacyIssues = @() if ($CheckDrift) { Write-AvmLog ("transform: check-drift mode; snapshotting {0} file(s)" -f $beforeFiles.Count) -Level Verbose | Out-Null $snapshot = Get-AvmFileSnapshot -Path @($beforeFiles | ForEach-Object { $_.FullName }) } $inspectionRoot = Join-Path ([System.IO.Path]::GetTempPath()) ('avm-provider-inspection-' + [guid]::NewGuid().ToString('N')) try { # mapotf reads provider schemas (order_resource_attrs et al.) by shelling # out to terraform, which it finds by name on PATH. GitHub-hosted runners # no longer ship terraform on PATH, so resolve the pinned terraform the # same way as mapotf (managed cache, not a stray PATH binary) and prepend # its directory to PATH for the mapotf subprocess. The override propagates # to mapotf's terraform grandchild. A missing terraform throws # AvmToolException, which the chain surfaces as 'skipped' just like a # missing mapotf binary. $terraform = Resolve-AvmTool -Name 'terraform' -ModuleRoot $Context.Root -AllowPathFallback:$AllowPathFallback Write-AvmLog ("transform: resolved terraform dependency at {0}" -f $terraform.Path) -Level Verbose | Out-Null $mapotfEnv = New-AvmToolPathEnvironment ` -ToolPath $terraform.Path ` -ToolName 'terraform' $mapotfEnv.TF_PLUGIN_CACHE_DIR = $null $mapotfEnv.MAPOTF_PROVIDER_SCHEMA_CACHE_DIR = Join-Path (Get-AvmFolder -Kind Cache) 'mapotf-provider-schema' $null = New-Item ` -ItemType Directory ` -Path $mapotfEnv.MAPOTF_PROVIDER_SCHEMA_CACHE_DIR ` -Force ` -ErrorAction Stop $transformOptions = [pscustomobject]@{ ToolPath = $tool.Path TerraformPath = $terraform.Path ProfileDirs = $profileDirs EnvVars = $mapotfEnv InspectionRoot = $inspectionRoot } $moduleTargets = @($targets | Where-Object { $_.Scope -in @('root', 'module') }) $testTargets = @($targets | Where-Object { $_.Scope -eq 'test' }) $exampleTargets = @($targets | Where-Object { $_.Scope -eq 'example' }) $unitTestTargets = @($moduleTargets + $testTargets + $exampleTargets) $newLocationTargets = @( foreach ($target in $moduleTargets | Where-Object { $_.Profiles -notcontains 'root' }) { if ($null -eq (Get-AvmTerraformLocationInspection -Path $target.Path -Options $transformOptions)) { $target } } ) $unitSnapshots = @(Get-AvmTerraformUnitTestSnapshot -Root $Context.Root ` -ModuleTargets $unitTestTargets -Options $transformOptions) Invoke-AvmParallel ` -InputObject $moduleTargets ` -FunctionName 'Invoke-AvmMapotfTransformTarget' ` -Argument $transformOptions ` -ThrottleLimit $ThrottleLimit $moduleCallTargets = @($moduleTargets | ForEach-Object { $relativePath = [System.IO.Path]::GetRelativePath($Context.Root, $_.Path) [pscustomobject]@{ Path = $_.Path Scope = $_.Scope Depth = @($relativePath -split '[\\/]').Count Profiles = @('module-call', 'variable-order', 'common') } }) if ($moduleCallTargets.Count -gt 0) { foreach ($depthGroup in @($moduleCallTargets | Group-Object Depth | Sort-Object { [int]$_.Name } -Descending)) { Invoke-AvmParallel ` -InputObject @($depthGroup.Group) ` -FunctionName 'Invoke-AvmMapotfTransformTarget' ` -Argument $transformOptions ` -ThrottleLimit $ThrottleLimit } } if ($exampleTargets.Count -gt 0) { Invoke-AvmParallel ` -InputObject $exampleTargets ` -FunctionName 'Invoke-AvmMapotfTransformTarget' ` -Argument $transformOptions ` -ThrottleLimit $ThrottleLimit } if ($testTargets.Count -gt 0) { $testDepthGroups = @($testTargets | Group-Object { @([System.IO.Path]::GetRelativePath($Context.Root, $_.Path) -split '[\\/]').Count } | Sort-Object { [int]$_.Name } -Descending) foreach ($depthGroup in $testDepthGroups) { Invoke-AvmParallel ` -InputObject @($depthGroup.Group) ` -FunctionName 'Invoke-AvmMapotfTransformTarget' ` -Argument $transformOptions ` -ThrottleLimit $ThrottleLimit } } Invoke-AvmTerraformUnitTestMigration -Root $Context.Root -ModuleTargets $unitTestTargets ` -Snapshots $unitSnapshots -Options $transformOptions Set-AvmTelemetryTagLintDirective -Targets $moduleTargets if ($newLocationTargets.Count -gt 0) { Set-AvmRequiredLocationLintDirective -Targets $newLocationTargets -Options $transformOptions } Write-AvmLog 'transform: mapotf scoped transforms completed' -Level Verbose | Out-Null foreach ($target in $targets) { $clean = Invoke-AvmProcess ` -FilePath $tool.Path ` -ArgumentList @('clean-backup', '--tf-dir', $target.Path) ` -WorkingDirectory $target.Path ` -EnvVars $mapotfEnv ` -IgnoreExitCode if ($clean.ExitCode -ne 0) { $message = Add-AvmProcessFailureDetail ` -Message ('mapotf clean-backup exited with code {0} for {1} target {2}.' -f $clean.ExitCode, $target.Scope, $target.Path) ` -StdOut $clean.StdOut ` -StdErr $clean.StdErr throw [AvmProcessException]::new( $message) } } Write-AvmLog 'transform: mapotf clean-backup completed' -Level Verbose | Out-Null $afterFiles = Get-AvmTerraformFile -Root $Context.Root $seen = New-Object 'System.Collections.Generic.HashSet[string]' $changed = New-Object System.Collections.Generic.List[string] foreach ($f in $afterFiles) { $null = $seen.Add($f.FullName) $rel = [System.IO.Path]::GetRelativePath($Context.Root, $f.FullName) $hash = (Get-FileHash -LiteralPath $f.FullName -Algorithm SHA256).Hash if (-not $before.ContainsKey($f.FullName)) { $changed.Add($rel) } elseif ($before[$f.FullName] -ne $hash) { $changed.Add($rel) } } foreach ($key in $before.Keys) { if (-not $seen.Contains($key)) { $changed.Add([System.IO.Path]::GetRelativePath($Context.Root, $key)) } } $legacyIssues = @(Get-AvmRemainingModtmIssue -Root $Context.Root -Targets @($moduleTargets + $exampleTargets + $testTargets)) } finally { try { if ($null -ne $snapshot) { Write-AvmLog 'transform: restoring terraform snapshot after drift check' -Level Verbose | Out-Null $current = @(Get-AvmTerraformFile -Root $Context.Root | ForEach-Object { $_.FullName }) Restore-AvmFileSnapshot -Snapshot $snapshot -CurrentPath $current } } finally { if (Test-Path -LiteralPath $inspectionRoot) { Remove-Item -LiteralPath $inspectionRoot -Recurse -Force -ErrorAction Stop -ProgressAction SilentlyContinue } } } $status = 'pass' $issues = New-Object System.Collections.Generic.List[object] foreach ($legacyIssue in $legacyIssues) { $status = 'fail' $issues.Add($legacyIssue) } if ($CheckDrift -and $changed.Count -gt 0) { $status = 'fail' foreach ($rel in $changed) { $issues.Add([pscustomobject][ordered]@{ File = $rel Line = 0 Column = 0 Severity = 'error' Code = 'avm.tf.mapotf-drift' Message = ("mapotf transform modified '{0}'; run 'avm pre-commit -Ecosystem terraform' and commit the result." -f $rel) }) } Write-AvmLog ("transform: completed; processed={0}; changed={1}; status={2}" -f $beforeFiles.Count, $changed.Count, $status) -Level Verbose | Out-Null } return [pscustomobject][ordered]@{ Engine = 'terraform' Tool = ('{0}/{1}' -f $tool.Name, $tool.Version) ToolPath = $tool.Path ToolSource = $tool.Source Status = $status FilesProcessed = $beforeFiles.Count Changed = $changed.ToArray() Issues = $issues.ToArray() } } # SIG # Begin signature block # MIInQQYJKoZIhvcNAQcCoIInMjCCJy4CAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCC53lF/9dV5YVQM # trmf9fANhlkkWIn+HmJ7ebHaaFsoqqCCDLowggX1MIID3aADAgECAhMzAAACHU0Z # yE7XD1dIAAAAAAIdMA0GCSqGSIb3DQEBCwUAMFcxCzAJBgNVBAYTAlVTMR4wHAYD # VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBD # b2RlIFNpZ25pbmcgUENBIDIwMjQwHhcNMjYwNDE2MTg1OTQzWhcNMjcwNDE1MTg1 # OTQzWjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE # BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYD # VQQDExVNaWNyb3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IB # DwAwggEKAoIBAQDQvewXxx9gZZFC6Ys1WBay8BJ8kGA4JQnH5CMafqOASlTpK9H8 # o5ZXTXt0caVQTNMUPt445wXYD+dFtaKWTwDn1I52oUSrC9vJin1Gsqt+zyKJL5Dg # 3eQXbQNR61DmMy20GLTIO3SFed9Rfi/ophgCLGFLDR3r0KvHjwMb/jYWS0celV/4 # Lz27LfAekm8v9E5IXaeiXbAUYZKK090n4CVl3JBtbN+9DtI9SNu/yjvozW52/u7R # X/Ttpa/KDlpuokZ+Zcbvmtd9ur9gFLvZzh41o9MsE/clQtdaFWGvuo6Jua/ntpgk # ey3E5/vBFe+MJPG6phdnuo6r57ZudCudiI1bAgMBAAGjggGbMIIBlzAOBgNVHQ8B # Af8EBAMCB4AwHwYDVR0lBBgwFgYKKwYBBAGCN0wIAQYIKwYBBQUHAwMwHQYDVR0O # BBYEFH6QuMwqcPG0hQlQ6c5jCtTTLrVeMEUGA1UdEQQ+MDykOjA4MR4wHAYDVQQL # ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xFjAUBgNVBAUTDTIzMDAxMis1MDc1NTkw # HwYDVR0jBBgwFoAUf1k/VCHarU/vBeXmo9ctBpQSCDEwYAYDVR0fBFkwVzBVoFOg # UYZPaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jcmwvTWljcm9zb2Z0 # JTIwQ29kZSUyMFNpZ25pbmclMjBQQ0ElMjAyMDI0LmNybDBtBggrBgEFBQcBAQRh # MF8wXQYIKwYBBQUHMAKGUWh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMv # Y2VydHMvTWljcm9zb2Z0JTIwQ29kZSUyMFNpZ25pbmclMjBQQ0ElMjAyMDI0LmNy # dDAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4ICAQBKTbYOjzwTG/DXGaz9 # s6+fQeaTtDcFmMY+5UyVFCyj7Pv+5i37qfX8lSL/tBIfYQfWsMuBQlfZurJD6r4H # VJ2CeH+1fgiq8dcHdVKoZ3Sa2qXoX3cq9iS8cVb06B7+5/XJ7I0OxHH9fDsvJ3T3 # w5V/ZtAIFmLrl+P0CtG+92uzRsn0nTbdFjOkLMLWPLAU3THohKRlSEMgFJpPkm5n # 5UAZ35xX6FWCrDLsSKb555bTifwa8mJBwdlof0bmfYidH+dxZ1FdDxvLnNl9zeKs # A4kejaaIqqIPguhwAti5Ql7BlTNoJNwxCvBmqW2MQLnCkYN/VVUsR3V2x/rcTNzo # Bf/Z/SpROvdaA2ZOOd1uioXJt3tdLQ7vHpqpib0KfWr/FWXW10q38VxfCnRQBqzb # SuztR7nEMuzX7Ck+B/XaPDXd1qh72+QYyB0Z2VzWmO9zsnb9Uq/dwu8LGeQqnyu6 # 7SDGACvnXii2fb9+US492VTnXSnFKyqwgzUyFMtZK1/sHYTv6bG4TtQUygQxTN+Z # V+aJIlKO2MqZ7bKrAnOzS9m6NgoTdWOq11bTOZwKlIEV/EhV9SWkDmdpR/hPPT2v # 6TEj4F8PT/zHjRezIU5c/DGlt/VhY/pK0XkJtEyMmmS1BMtjU/rqBZVMIm3dnxQs # /TBByr+Cf8Z1r7aifQVQ+WSqzjCCBr0wggSloAMCAQICEzMAAAA5O7Y3Gb8GHWcA # AAAAADkwDQYJKoZIhvcNAQEMBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpX # YXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQg # Q29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRl # IEF1dGhvcml0eSAyMDExMB4XDTI0MDgwODIwNTQxOFoXDTM2MDMyMjIyMTMwNFow # VzELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEo # MCYGA1UEAxMfTWljcm9zb2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAyNDCCAiIwDQYJ # KoZIhvcNAQEBBQADggIPADCCAgoCggIBANgBnB7jOMeqlRYHNa265v4IY9fH8TKh # emHfPINe1gpLaV3dhg324WwH06LcHbpnsBukCDNitryo0dtS/EW6I/yEL/bLSY8h # KpbfQuWusBPr9qazYcDxCW/qnjb5JsI1s8bNOg3bVATvQVL4tcf03aTycsz8QeCd # M0l/yHRObJ9QqazM1r6VPEOJ7LL+uEEb73w6QCuhs89a1uv1zerOYMnsneRRwCbp # yW11IcggU0cRKDDq1pjVJzIbIF6+oiXXbReOsgeI8zu1FyQfK0fVkaya8SmVHQ/t # Of23mZ4W9k0Ri22QW9p3UgSC5OUDktKxxcCmGL6tXLfOGSWHIIV4YrTJTT6PNty5 # REojHJuZHArkF9VnHTERWoTjAzfI3kP+5b4alUdhgAZ7ttOu1bVnXfHaqPYl2rPs # 20ji03LOVWsh/radgE17es5hL+t6lV0eVHrVhsssROWJuz2MXMCt7iw7lFPG9LXK # Gjsmonn2gotGdHIuEg5JnJMJVmixd5LRlkmgYRZKzhxSCwyoGIq0PhaA7Y+VPct5 # pCHkijcIIDm0nlkK+0KyepolcqGm0T/GYQRMhHJlGOOmVQop36wUVUYklUy++vDW # eEgEo4s7hxN6mIbf2MSIQ/iIfMZgJxC69oukMUXCrOC3SkE/xIkgpfl22MM1itkZ # 35nNXkMolU1lAgMBAAGjggFOMIIBSjAOBgNVHQ8BAf8EBAMCAYYwEAYJKwYBBAGC # NxUBBAMCAQAwHQYDVR0OBBYEFH9ZP1Qh2q1P7wXl5qPXLQaUEggxMBkGCSsGAQQB # gjcUAgQMHgoAUwB1AGIAQwBBMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU # ci06AjGQQ7kUBU7h6qfHMdEjiTQwWgYDVR0fBFMwUTBPoE2gS4ZJaHR0cDovL2Ny # bC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0MjAx # MV8yMDExXzAzXzIyLmNybDBeBggrBgEFBQcBAQRSMFAwTgYIKwYBBQUHMAKGQmh0 # dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljUm9vQ2VyQXV0MjAx # MV8yMDExXzAzXzIyLmNydDANBgkqhkiG9w0BAQwFAAOCAgEAFJQfOChP7onn6fLI # MKrSlN1WYKwDFgAddymOUO3FrM8d7B/W/iQ6DxXsDn7D5W4wMwYeLystcEqfkjz4 # NURRgazyMu5yRzQh4LqjA4tStTcJh1opExo7nn5PuPBYnbu0+THSuVHTe0VTTPVh # ily/piFrDo3axQ9P4C+Ol5yet+2gTfekICS5xS+cYfSIvgn0JksVBVMYVI5QFu/q # hnLhsEFEUzG8fvv0hjgkO+lkpV9ty6GkN4vdnd7ya6Q6aR9y34aiM1qmxaxBi6OU # nyNl6fkuun/diTFnYDLTppOkr/mg5WSfCiDVMNCxtj4wPKC5OmHm1DQIt/MNokbb # H3UGsFP1QbzsLocuSqLCvH09Io3fDPTmscR9Y75G4qX7RTX8AdBPo0I6OEojf39z # uFZt0qOHm65YWQE69cZM2ueE1MB05dNNgHK9gTE7zKvK/fg8B2qjW88MT/WF5V5u # vZGtqa9FSL2RazArA+rDPuf6JGYz4HpgMZHB4S6szWSKYBv0VisCzfxgeU+dquXW # 9bd0auYlOB58DPcOYKdc3Se94g+xL4pcEhbB54JOgAkwYTu/9dLeH2pDqeJZAABV # DWRQCaXfO5LgyKwKCLYXpigrZYCjUSBcr+Ve8PFWMhVTQl0v4q8J/AUmQN5W4n10 # 1cY2L4A7GTQG1h32HHAvfQESWP0xghndMIIZ2QIBATBuMFcxCzAJBgNVBAYTAlVT # MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jv # c29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMjQCEzMAAAIdTRnITtcPV0gAAAAAAh0w # DQYJYIZIAWUDBAIBBQCggZAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwLwYJ # KoZIhvcNAQkEMSIEIMlvqIlfRRG+cl7xW2UIK2bxsloLCu03hXVCnXApFHnzMEIG # CisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8AcwBvAGYAdKEagBhodHRwOi8v # d3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEBBQAEggEAwUNO5OK7W42fix52 # XRM977cW2vs7J2iCTxoQ4KfJwE7mR/1I7s+mz73IlHo1dcIkJPUs+1Wy3naPQqXP # nlPUwyfQjuxo1IkSAaI/qXFzmrEnvtzM/8i+laGDt+ajj/2Ofi2PVegG0Lj0JzLx # KS42MV+uWyYmGkPnjW/qoS/RDTox8qoK/j6D5QYCyVra7Vbqy7IQ5TB2EAeyiHf8 # ICMDFD6MBWp3zTPfYXMHlw8SknLxYMNI+1qs7QQKO+SwbK3+nmmZ7VPc+qhSy6Cw # k1vF0Jo9/QA/CVuV4MCoBFmtS0DzDXY0GGql7JkgiJnrARdL8SpFM1ZWdHS6IFPV # JV2v36GCF60wghepBgorBgEEAYI3AwMBMYIXmTCCF5UGCSqGSIb3DQEHAqCCF4Yw # gheCAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFaBgsqhkiG9w0BCRABBKCCAUkEggFF # MIIBQQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFlAwQCAQUABCAVME7ddcR50vfY # s3wv8Mfjtp07jmRFQl3dfI/7AUMWTQIGaq6xsAuLGBMyMDI2MTAxNTE5MDY0MS45 # OTNaMASAAgH0oIHZpIHWMIHTMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGlu # Z3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv # cmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExp # bWl0ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVTTjo0MzFBLTA1RTAtRDk0NzEl # MCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaCCEfswggcoMIIF # EKADAgECAhMzAAACHUvAkoc4hX45AAEAAAIdMA0GCSqGSIb3DQEBCwUAMHwxCzAJ # BgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25k # MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jv # c29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMB4XDTI1MDgxNDE4NDgzM1oXDTI2MTEx # MzE4NDgzM1owgdMxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw # DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x # LTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJhdGlvbnMgTGltaXRlZDEn # MCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOjQzMUEtMDVFMC1EOTQ3MSUwIwYDVQQD # ExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNlMIICIjANBgkqhkiG9w0BAQEF # AAOCAg8AMIICCgKCAgEAorSgaAA8oOl4ph574zw29egUN8DDepRHLX8FM1zHNJmX # G6KrSqUKwzcKafopuYdPTETTCvb9aJfESuAU0iGNUFI/D6R0kvdfpe2oPX+E3sbT # QvGi4JPH5qdIYUaJ45V/4bqe8eNvbWzpC+ZKjH193DeiI1XAI918JoQmBhlEXo/T # on1721luZJgincsf5LjMY3jX84WyXUSX3dsS7h/7xVI+w1yjg7pa+0y3o/me2Tsv # 6UJUdSTQap5ORGSfCnclnP1z3IiiWIWr3Vo7aIPWsgJzq3m5GxpxUHCQk8qzUhk5 # 0y/uB+LGE3WIK2C77iy9iFsSfSLUnyMEzGRDW9mXHT4PH7Ozz6CHqQEiNvwcHqlv # lCh1pHQh1NXQSAqOoVBs5mi6easf6yxWTfe5DrR79503r8pU6VqC2Y9XMRU4wH9Q # bYXYsIUZ33Jmndy22W1LBDAbxBPQHCBlncGDU3BgdhVUVLe80mggFO98FdkWho67 # w4kPdCTRkvdvkY8PrQYE/nQjHXCa0g7LcMttZb6ejMHfQ+tUWXv6+nZ4Ynkr2Oka # xclFCw4RIYNMWD26AWbQj/WEdzga18fKtw66L5gzXPza6jFBfPJeKE3H8QAuwpir # mH4ms+5nUjNNQOmNgqJn0U1+3Yn7ClswD79YN0r3fdbYBMDApBZJpNlK7q7HXRsC # AwEAAaOCAUkwggFFMB0GA1UdDgQWBBSEWfBxNEamZtXm8gl92Yq80jfxXTAfBgNV # HSMEGDAWgBSfpxVdAF5iXYP05dJlpxtTNRnpcjBfBgNVHR8EWDBWMFSgUqBQhk5o # dHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBU # aW1lLVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcmwwbAYIKwYBBQUHAQEEYDBeMFwG # CCsGAQUFBzAChlBodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRz # L01pY3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNydDAMBgNV # HRMBAf8EAjAAMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMA4GA1UdDwEB/wQEAwIH # gDANBgkqhkiG9w0BAQsFAAOCAgEAkdweB4yxvLspLKq0D+miyD4Q0EcxVFpNZuJx # iR54gWRkeTDDuymNeB03JhlsBpbwSYJ5uZSgDBCvwHED2VL8lJpFlOprJzxsXWC2 # NTfA+O+PO5Fk5jw6LHh6jeBADDEdQAx3Hqi7Zm0JwvQ93z5f6dtxkm29WqOcHYXR # XfAQwy1hSrLXyfeblqR66jpP/9n0fCkWU4ggsUjQpQ2Ngj1DV09J4Y3y7p9Nd81+ # Xs6qYo++7RKm8qiB/5NDeigOLjlAeFgiEXIRUJW+mJyqpQw+OORlaqcFjR8Hu0G+ # /7bMdek68YX+kPpDBk7Ue+I/xgiYJ1xcDRBn/vczLtN72+RIlD4UgXYLuBSCk//p # DEPX5z39Cr+rkc6E4Y28FPk4BhloAyvp628P4xfElQY8TcxraUbZShypocE6ny95 # D1K1BkltZmrHVKCxmglnuOlM15NKIrXFlXCzdqpCtIwQ417wNAVF/QDPvzzbumPd # Ti6fb0tLbScYobV6zvbBsMsKEME4Tj1b9oIXC8dybJq4nbboEXYpRwi1QAbpSNrn # +PxGW9uf1q63FnMJu4gm3Oh63njW/iVf723quzyHrSijWMgY0HiRiHQi0Jyu0h8M # dhRUp7mxbmLQckPiOFwAlIaUN/k725y/aLWpkRU6fqmLlEOyH5WpyLd23AYy9r8v # +Qoba6swggdxMIIFWaADAgECAhMzAAAAFcXna54Cm0mZAAAAAAAVMA0GCSqGSIb3 # DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4G # A1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIw # MAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAx # MDAeFw0yMTA5MzAxODIyMjVaFw0zMDA5MzAxODMyMjVaMHwxCzAJBgNVBAYTAlVT # MRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQK # ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1l # LVN0YW1wIFBDQSAyMDEwMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA # 5OGmTOe0ciELeaLL1yR5vQ7VgtP97pwHB9KpbE51yMo1V/YBf2xK4OK9uT4XYDP/ # XE/HZveVU3Fa4n5KWv64NmeFRiMMtY0Tz3cywBAY6GB9alKDRLemjkZrBxTzxXb1 # hlDcwUTIcVxRMTegCjhuje3XD9gmU3w5YQJ6xKr9cmmvHaus9ja+NSZk2pg7uhp7 # M62AW36MEBydUv626GIl3GoPz130/o5Tz9bshVZN7928jaTjkY+yOSxRnOlwaQ3K # Ni1wjjHINSi947SHJMPgyY9+tVSP3PoFVZhtaDuaRr3tpK56KTesy+uDRedGbsoy # 1cCGMFxPLOJiss254o2I5JasAUq7vnGpF1tnYN74kpEeHT39IM9zfUGaRnXNxF80 # 3RKJ1v2lIH1+/NmeRd+2ci/bfV+AutuqfjbsNkz2K26oElHovwUDo9Fzpk03dJQc # NIIP8BDyt0cY7afomXw/TNuvXsLz1dhzPUNOwTM5TI4CvEJoLhDqhFFG4tG9ahha # YQFzymeiXtcodgLiMxhy16cg8ML6EgrXY28MyTZki1ugpoMhXV8wdJGUlNi5UPkL # iWHzNgY1GIRH29wb0f2y1BzFa/ZcUlFdEtsluq9QBXpsxREdcu+N+VLEhReTwDwV # 2xo3xwgVGD94q0W29R6HXtqPnhZyacaue7e3PmriLq0CAwEAAaOCAd0wggHZMBIG # CSsGAQQBgjcVAQQFAgMBAAEwIwYJKwYBBAGCNxUCBBYEFCqnUv5kxJq+gpE8RjUp # zxD/LwTuMB0GA1UdDgQWBBSfpxVdAF5iXYP05dJlpxtTNRnpcjBcBgNVHSAEVTBT # MFEGDCsGAQQBgjdMg30BATBBMD8GCCsGAQUFBwIBFjNodHRwOi8vd3d3Lm1pY3Jv # c29mdC5jb20vcGtpb3BzL0RvY3MvUmVwb3NpdG9yeS5odG0wEwYDVR0lBAwwCgYI # KwYBBQUHAwgwGQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwCwYDVR0PBAQDAgGG # MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU1fZWy4/oolxiaNE9lJBb186a # GMQwVgYDVR0fBE8wTTBLoEmgR4ZFaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3Br # aS9jcmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYtMjMuY3JsMFoGCCsG # AQUFBwEBBE4wTDBKBggrBgEFBQcwAoY+aHR0cDovL3d3dy5taWNyb3NvZnQuY29t # L3BraS9jZXJ0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcnQwDQYJKoZIhvcN # AQELBQADggIBAJ1VffwqreEsH2cBMSRb4Z5yS/ypb+pcFLY+TkdkeLEGk5c9MTO1 # OdfCcTY/2mRsfNB1OW27DzHkwo/7bNGhlBgi7ulmZzpTTd2YurYeeNg2LpypglYA # A7AFvonoaeC6Ce5732pvvinLbtg/SHUB2RjebYIM9W0jVOR4U3UkV7ndn/OOPcbz # aN9l9qRWqveVtihVJ9AkvUCgvxm2EhIRXT0n4ECWOKz3+SmJw7wXsFSFQrP8DJ6L # GYnn8AtqgcKBGUIZUnWKNsIdw2FzLixre24/LAl4FOmRsqlb30mjdAy87JGA0j3m # Sj5mO0+7hvoyGtmW9I/2kQH2zsZ0/fZMcm8Qq3UwxTSwethQ/gpY3UA8x1RtnWN0 # SCyxTkctwRQEcb9k+SS+c23Kjgm9swFXSVRk2XPXfx5bRAGOWhmRaw2fpCjcZxko # JLo4S5pu+yFUa2pFEUep8beuyOiJXk+d0tBMdrVXVAmxaQFEfnyhYWxz/gq77EFm # PWn9y8FBSX5+k77L+DvktxW/tM4+pTFRhLy/AsGConsXHRWJjXD+57XQKBqJC482 # 2rpM+Zv/Cuk0+CQ1ZyvgDbjmjJnW4SLq8CdCPSWU5nR0W2rRnj7tfqAxM328y+l7 # vzhwRNGQ8cirOoo6CGJ/2XBjU02N7oJtpQUQwXEGahC0HVUzWLOhcGbyoYIDVjCC # Aj4CAQEwggEBoYHZpIHWMIHTMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGlu # Z3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv # cmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExp # bWl0ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVTTjo0MzFBLTA1RTAtRDk0NzEl # MCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaIjCgEBMAcGBSsO # AwIaAxUAuoO+BKbfXzqyfi9GLEdWHkCLeT+ggYMwgYCkfjB8MQswCQYDVQQGEwJV # UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE # ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGlt # ZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQsFAAIFAO57dDUwIhgPMjAyNjEw # MTUxNTQ4MDVaGA8yMDI2MTAxNjE1NDgwNVowdDA6BgorBgEEAYRZCgQBMSwwKjAK # AgUA7nt0NQIBADAHAgEAAgI6MDAHAgEAAgITbzAKAgUA7nzFtQIBADA2BgorBgEE # AYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIBAAIDB6EgoQowCAIBAAIDAYag # MA0GCSqGSIb3DQEBCwUAA4IBAQAzSqUhzUz6vtZViC8Bs10EZcXah2C3vdodo7qi # Ji1REdkUg+ZqADnwDYxu0Hij6dQOGhdFT7gOkxXnBDahrWL2rNAxKas5OPZuVyxz # 4BiC85U2roTz/unoEeX9X2s+eTL7WTkjq6uSpkKU3P/mcqxohGsJUgSCPhm5Tgye # iSYLEp19bDj6arpez+/WXhd8uK5mhg2KKzV+Kx7QDrdlsdKM/9IEjv+4py27Q2sd # DaOAtILi/u0vHSbKHUue5XOFz8gt1q/vXP62bZe2paj8w5qMruXOvV6c+jKaQPAA # mWANKt8z4HpCgYEEURbSSrin3k5eiF2I5Lc2GXx2mQSn6wN1MYIEDTCCBAkCAQEw # gZMwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcT # B1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UE # AxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTACEzMAAAIdS8CShziFfjkA # AQAAAh0wDQYJYIZIAWUDBAIBBQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0B # CRABBDAvBgkqhkiG9w0BCQQxIgQgVGfmwis7vhvy63DReqKFBU5n5ZYJfo41YaCw # FlpB6oYwgfoGCyqGSIb3DQEJEAIvMYHqMIHnMIHkMIG9BCCxtpXMXEiLJzrqM77e # p4rTNwrMOj6gpWN9hZvpj5QFUTCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1w # IFBDQSAyMDEwAhMzAAACHUvAkoc4hX45AAEAAAIdMCIEIGGtrLikVdvCzu7AQY4+ # UybqDtd0J8aMnjt7M078tVzlMA0GCSqGSIb3DQEBCwUABIICAE/pCTdeyO3E712N # yqGtu7qTe/f89KMNnEMFdHs55tbXLog7kaCefPt8/IBwAHIkB7uK86s7ODcMh/Np # qWwigNzwEgBJ2wEDhhTIxYDcEfcz7xUOGWDnu6l4K96cTPtEsyus5uoIxeNcia2g # oZDFtNq394MX/vC2U+MrxDRbhLrqt6qx+M3G3EVg3CZclDiZYsLjRWAHpcYG8xBw # kqTkrEuCN1T5wlC971/GhYrR7E+pnr/oZZJbn7rFHtVCTOmwqihDUzMz3s1v7SRF # Wui9nIpA0zkCg4IF7ZeYT9atGxUzNtWPABDng8Wr/Q4Ic71HvqnIhL6L1n3ws5DM # ciAqcH7Tyj8y8F2zdvpg/S2jl390n2m131LTwpS6GNDXaJOtg4zSxGMhOy4uQcpL # 3JcV/mpow3p482Nq21JqqjbDAEOm+xjMm6t/P/EvzaNPJ2ybNpbD1Ju8v/W79zu2 # evIyX1T0cUsWTX36iwZ4HnAaq36qZGK93QMKrjgglE+FkJpEmQ8qrf26NgimJEh9 # p65ya+NqITlQsrw8S58P7L8QQwKx7NUDCI/fR9OcxuhNktVgbg3jBECIShgFi8j7 # jggmSBjhkLwyo5THoDoaXV4/VV4l7aEADtCHGOc69n4nWF51nMc2EkUpEBv1r6fW # MnPrRRZAxjARwxv5+13dfMwgvfIU # SIG # End signature block |