Engines/Terraform/Invoke-AvmTerraformTest.ps1
|
function Invoke-AvmTerraformTest { <# .SYNOPSIS Validate Terraform examples and warn about uncovered local modules. .DESCRIPTION Initializes and validates each immediate examples/ configuration, including examples marked .e2eignore. Libraries remain child modules, allowing deprecated outputs. Diagnostics are relative to Context.Root. Each initialization uses a fresh, process-local TF_DATA_DIR so stale installed-module records cannot inflate coverage. Direct and transitive module directories are compared with the root and immediate modules/ configurations. Uncovered modules produce warnings, not failures. FilesProcessed counts direct example configuration files. No examples returns Status='skipped'; real validation errors return Status='fail'. .PARAMETER Context Module context produced by Get-AvmModuleContext. Must have Ecosystem='terraform'. .PARAMETER AllowPathFallback Pass through to Resolve-AvmTool. .PARAMETER NoInit Validate examples using their existing initialization. Coverage is not assessed because existing module manifests can contain stale records; a warning reports that limitation. .OUTPUTS pscustomobject with Engine, Tool, ToolPath, ToolSource, Status, FilesProcessed, Issues. #> [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory)] $Context, [switch] $AllowPathFallback, [switch] $NoInit ) Set-StrictMode -Version 3.0 $ErrorActionPreference = 'Stop' if ($Context.Ecosystem -ne 'terraform') { throw [System.ArgumentException]::new( "Invoke-AvmTerraformTest requires a terraform context (got Ecosystem='$($Context.Ecosystem)').") } $tool = Resolve-AvmTool -Name 'terraform' -ModuleRoot $Context.Root -AllowPathFallback:$AllowPathFallback $scope = Get-AvmTerraformValidationScope -Root $Context.Root $issues = [System.Collections.Generic.List[object]]::new() $coverageIssues = [System.Collections.Generic.List[object]]::new() $coveredModules = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) $filesProcessed = 0 $anyFail = $false foreach ($example in $scope.Examples) { $dataDirectory = $null $ownsDataDirectory = $false $environment = @{} try { if (-not $NoInit) { $dataDirectory = Join-Path ([System.IO.Path]::GetTempPath()) ('avm-validate-' + [guid]::NewGuid().ToString('N').Substring(0, 12)) $null = New-Item -ItemType Directory -Path $dataDirectory -ErrorAction Stop $ownsDataDirectory = $true $environment.TF_DATA_DIR = $dataDirectory $initResult = Invoke-AvmProcess ` -FilePath $tool.Path ` -ArgumentList @('init', '-backend=false', '-upgrade', '-input=false', '-no-color') ` -WorkingDirectory $example.Path ` -EnvVars $environment ` -StreamOutput:(Test-AvmVerboseEnabled) ` -Label ('terraform init {0}' -f $example.RelativePath) ` -IgnoreExitCode ` -RetryNetworkFailure if ($initResult.ExitCode -ne 0) { $message = Add-AvmProcessFailureDetail ` -Message ("terraform init for '{0}' failed with exit code {1}." -f $example.RelativePath, $initResult.ExitCode) ` -StdOut $initResult.StdOut ` -StdErr $initResult.StdErr throw [AvmProcessException]::new($message) } } $result = Invoke-AvmProcess ` -FilePath $tool.Path ` -ArgumentList @('validate', '-no-color', '-json') ` -WorkingDirectory $example.Path ` -EnvVars $environment ` -IgnoreExitCode $filesProcessed += $example.Files.Count if ($result.ExitCode -notin @(0, 1)) { $message = Add-AvmProcessFailureDetail ` -Message ("terraform validate for '{0}' exited with code {1}." -f $example.RelativePath, $result.ExitCode) ` -StdOut $result.StdOut ` -StdErr $result.StdErr throw [AvmProcessException]::new($message) } try { $parsed = ConvertFrom-Json -InputObject ([string]$result.StdOut) -AsHashtable -ErrorAction Stop } catch [System.ArgumentException] { throw [AvmProcessException]::new( "Could not parse terraform validate -json output for '$($example.RelativePath)': $($_.Exception.Message)") } if ($parsed -isnot [System.Collections.IDictionary] -or -not $parsed.Contains('valid') -or $parsed.valid -isnot [bool] -or -not $parsed.Contains('diagnostics') -or $parsed.diagnostics -isnot [array]) { throw [AvmProcessException]::new( "Invalid terraform validate -json output for '$($example.RelativePath)': expected valid and diagnostics.") } $hasErrors = $false foreach ($diag in $parsed.diagnostics) { if ($diag -isnot [System.Collections.IDictionary] -or -not $diag.Contains('severity') -or $diag.severity -notin @('error', 'warning') -or -not $diag.Contains('summary') -or $diag.summary -isnot [string]) { throw [AvmProcessException]::new( "Invalid terraform validate diagnostic for '$($example.RelativePath)'.") } $severity = [string]$diag.severity if ($severity -eq 'error') { $hasErrors = $true } $detail = if ($diag.Contains('detail')) { [string]$diag.detail } else { '' } $message = if ($detail) { "$($diag.summary) - $detail" } else { [string]$diag.summary } $file = $example.RelativePath $line = 0 $column = 0 if ($diag.Contains('range') -and $diag.range) { if ($diag.range.filename) { $absoluteFile = [System.IO.Path]::GetFullPath([string]$diag.range.filename, $example.Path) $file = [System.IO.Path]::GetRelativePath($Context.Root, $absoluteFile).Replace('\', '/') } if ($diag.range.start) { $line = [int]$diag.range.start.line $column = [int]$diag.range.start.column } } $issues.Add([pscustomobject][ordered]@{ File = $file Line = $line Column = $column Severity = $severity Code = '' Message = ('[{0}] {1}' -f $example.RelativePath, $message) }) } if ($result.ExitCode -ne 0 -or -not $parsed.valid -or $hasErrors) { $anyFail = $true if (-not $hasErrors) { $message = Add-AvmProcessFailureDetail ` -Message ("terraform validate for '{0}' reported failure without error diagnostics (exit {1})." -f $example.RelativePath, $result.ExitCode) ` -StdOut '' ` -StdErr $result.StdErr $issues.Add([pscustomobject][ordered]@{ File = $example.RelativePath Line = 0 Column = 0 Severity = 'error' Code = '' Message = $message }) } continue } if ($NoInit) { continue } try { $manifestPath = Join-Path -Path (Join-Path -Path $dataDirectory -ChildPath 'modules') -ChildPath 'modules.json' $manifest = Get-Content -LiteralPath $manifestPath -Raw -ErrorAction Stop foreach ($directory in (ConvertFrom-AvmTerraformModuleManifest -Payload $manifest -WorkingDirectory $example.Path -TestFiles $example.TestFiles)) { $null = $coveredModules.Add($directory) } } catch [System.Management.Automation.ItemNotFoundException], [System.IO.IOException], [System.UnauthorizedAccessException], [AvmConfigurationException] { $coverageIssues.Add([pscustomobject][ordered]@{ File = $example.RelativePath Line = 0 Column = 0 Severity = 'warning' Code = 'terraform.module-coverage-unavailable' Message = "Could not determine module coverage for '$($example.RelativePath)': $($_.Exception.Message)" }) } } finally { if ($ownsDataDirectory -and (Test-Path -LiteralPath $dataDirectory -PathType Container)) { Remove-Item -LiteralPath $dataDirectory -Recurse -Force -ErrorAction Stop -ProgressAction SilentlyContinue } } } if ($scope.Examples.Count -eq 0) { $coverageIssues.Add([pscustomobject][ordered]@{ File = '' Line = 0 Column = 0 Severity = 'warning' Code = 'terraform.module-coverage' Message = 'No Terraform examples found under examples/; validation was skipped.' }) } if ($NoInit -and $scope.Examples.Count -gt 0) { $coverageIssues.Add([pscustomobject][ordered]@{ File = '' Line = 0 Column = 0 Severity = 'warning' Code = 'terraform.module-coverage-unavailable' Message = 'Module coverage is not assessed with -NoInit because existing module manifests may be stale. Rerun without -NoInit to check coverage.' }) } else { foreach ($module in $scope.Modules) { if ($coveredModules.Contains($module.Path)) { continue } $coverageIssues.Add([pscustomobject][ordered]@{ File = [System.IO.Path]::GetRelativePath($Context.Root, $module.Files[0]).Replace('\', '/') Line = 0 Column = 0 Severity = 'warning' Code = 'terraform.module-coverage' Message = "No example coverage was confirmed for module '$($module.RelativePath)'. Add an example that calls this checkout's module directly or transitively." }) } } foreach ($issue in $coverageIssues) { $issues.Add($issue) Write-AvmLog -Message $issue.Message -Level Warning -File $issue.File Register-AvmPresentedIssue -Issue $issue } return [pscustomobject][ordered]@{ Engine = 'terraform' Tool = ('{0}/{1}' -f $tool.Name, $tool.Version) ToolPath = $tool.Path ToolSource = $tool.Source Status = if ($anyFail) { 'fail' } elseif ($scope.Examples.Count -eq 0) { 'skipped' } else { 'pass' } FilesProcessed = $filesProcessed Issues = $issues.ToArray() } } # SIG # Begin signature block # MIInQwYJKoZIhvcNAQcCoIInNDCCJzACAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDw6XL1Hjpgh7hP # XLIKgn783FPE7ifBFOtAbXKwWtaxkqCCDLowggX1MIID3aADAgECAhMzAAACHU0Z # 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 # 1cY2L4A7GTQG1h32HHAvfQESWP0xghnfMIIZ2wIBATBuMFcxCzAJBgNVBAYTAlVT # MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jv # c29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMjQCEzMAAAIdTRnITtcPV0gAAAAAAh0w # DQYJYIZIAWUDBAIBBQCggZAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwLwYJ # KoZIhvcNAQkEMSIEIL7ev+pXAVErzYdo/yfHsoMM7OB46gCwHJor1/JyScK9MEIG # CisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8AcwBvAGYAdKEagBhodHRwOi8v # d3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEBBQAEggEAeuYrr12WdH3R61hp # h0iLP0MZhADgHWxiRV8ZY2a8Su1+HHPvEnLbiNwrHkAeDOCLdJlGBb6AUZPVttUz # 0xxonLc/m2nFCG43lZZGtKTjG5LPD6XAk6fSMMy/2slXWEaN1Yi2o0kkebXwEBJt # boxt+/e8+NTxXw+I9khmb+ggaEUTHTzUZgUQIFY/eKS1wklyiGhEggZua+wt87X2 # EFcApYPV6UiaCGM9R9esN2PssO5QamotUW5TAQp00nym9iMi9sq+VfjWc313xPbg # w0MXm+oDpi/l9Dbrodj3Lc8h8SpCLWezICsyywGXyYY/U29ZnCqQYi8evpEb+ZJS # rJKq9KGCF68wgherBgorBgEEAYI3AwMBMYIXmzCCF5cGCSqGSIb3DQEHAqCCF4gw # gheEAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFaBgsqhkiG9w0BCRABBKCCAUkEggFF # MIIBQQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFlAwQCAQUABCDq2Ju3oK8AaZJ8 # 7hfjIbvUBWk3AeHILe/4ZZ23+Yb25QIGasTkwbZ4GBMyMDI2MTAwNjIyNTcyNS41 # NjlaMASAAgH0oIHZpIHWMIHTMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGlu # Z3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv # cmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExp # bWl0ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVTTjozMjFBLTA1RTAtRDk0NzEl # MCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaCCEf0wggcoMIIF # EKADAgECAhMzAAACGqmgHQagD0OqAAEAAAIaMA0GCSqGSIb3DQEBCwUAMHwxCzAJ # BgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25k # MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jv # c29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMB4XDTI1MDgxNDE4NDgyOFoXDTI2MTEx # MzE4NDgyOFowgdMxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw # DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x # LTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJhdGlvbnMgTGltaXRlZDEn # MCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOjMyMUEtMDVFMC1EOTQ3MSUwIwYDVQQD # ExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNlMIICIjANBgkqhkiG9w0BAQEF # AAOCAg8AMIICCgKCAgEAmYEAwSTz79q2V3ZWzQ5Ev7RKgadQtMBy7+V3XQ8R0NL8 # R9mupxcqJQ/KPeZGJTER+9Qq/t7HOQfBbDy6e0TepvBFV/RY3w+LOPMKn0Uoh2/8 # IvdSbJ8qAWRVoz2S9VrJzZpB8/f5rQcRETgX/t8N66D2JlEXv4fZQB7XzcJMXr1p # uhuXbOt9RYEyN1Q3Z7YjRkhfBsRc+SD/C9F4iwZqfQgo82GG4wguIhjJU7+XMfrv # 4vxAFNVg3mn1PoMWGZWio+e14+PGYPVLKlad+0IhdHK5AgPyXKkqAhEZpYhYYVEI # tHOOvqrwukxVAJXMvWA3GatWkRZn33WDJVtghCW6XPLi1cDKiGE5UcXZSV4OjQIU # B8vp2LUMRXud5I49FIBcE9nT00z8A+EekrPM+OAk07aDfwZbdmZ56j7ub5fNDLf8 # yIb8QxZ8Mr4RwWy/czBuV5rkWQQ+msjJ5AKtYZxJdnaZehUgUNArU/u36SH1eXKM # QGRXr/xeKFGI8vvv5Jl1knZ8UqEQr9PxDbis7OXp2WSMK5lLGdYVH8VownYF3sbO # iRkx5Q5GaEyTehOQp2SfdbsJZlg0SXmHphGnoW1/gQ/5P6BgSq4PAWIZaDJj6AvL # LCdbURgR5apNQQed2zYUgUbjACA/TomA8Ll7Arrv2oZGiUO5Vdi4xxtA3BRTQTUC # AwEAAaOCAUkwggFFMB0GA1UdDgQWBBTwqyIJ3QMoPasDcGdGovbaY8IlNjAfBgNV # HSMEGDAWgBSfpxVdAF5iXYP05dJlpxtTNRnpcjBfBgNVHR8EWDBWMFSgUqBQhk5o # dHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBU # aW1lLVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcmwwbAYIKwYBBQUHAQEEYDBeMFwG # CCsGAQUFBzAChlBodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRz # L01pY3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNydDAMBgNV # HRMBAf8EAjAAMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMA4GA1UdDwEB/wQEAwIH # gDANBgkqhkiG9w0BAQsFAAOCAgEA1a72WFq7B6bJT3VOJ21nnToPJ9O/q51bw1bh # PfQy67uy+f8x8akipzNL2k5b6mtxuPbZGpBqpBKguDwQmxVpX8cGmafeo3wGr4a8 # Yk6Sy09tEh/Nwwlsyq7BRrJNn6bGOB8iG4OTy+pmMUh7FejNPRgvgeo/OPytm4NN # rMMg98UVlrZxGNOYsifpRJFg5jE/Yu6lqFa1lTm9cHuPYxWa2oEwC0sEAsTFb69i # KpN0sO19xBZCr0h5ClU9Pgo6ekiJb7QJoDzrDoPQHwbNA87Cto7TLuphj0m9l/I7 # 0gLjEq53SHjuURzwpmNxdm18Qg+rlkaMC6Y2KukOfJ7oCSu9vcNGQM+inl9gsNgi # rZ6yJk9VsXEsoTtoR7fMNU6Py6ufJQGMTmq6ZCq2eIGOXWMBb79ZF6tiKTa4qami # 3US0mTY41J129XmAglVy+ujSZkHu2lHJDRHs7FjnIXZVUE5pl6yUIl23jG50fRTL # QcStdwY/LvJUgEHCIzjvlLTqLt6JVR5bcs5aN4Dh0YPG95B9iDMZrq4rli5SnGNW # ev5LLsDY1fbrK6uVpD+psvSLsNpht27QcHRsYdAMALXM+HNsz2LZ8xiOfwt6rOsV # WXoiHV86/TeMy5TZFUl7qB59INoMSJgDRladVXeT9fwOuirFIoqgjKGk3vO2bELr # YMN0QVwwggdxMIIFWaADAgECAhMzAAAAFcXna54Cm0mZAAAAAAAVMA0GCSqGSIb3 # 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/2XBjU02N7oJtpQUQwXEGahC0HVUzWLOhcGbyoYIDWDCC # AkACAQEwggEBoYHZpIHWMIHTMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGlu # Z3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv # cmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExp # bWl0ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVTTjozMjFBLTA1RTAtRDk0NzEl # MCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaIjCgEBMAcGBSsO # AwIaAxUA8YrutmKpSrubCaAYsU4pt1Ft8DaggYMwgYCkfjB8MQswCQYDVQQGEwJV # UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE # ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGlt # ZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQsFAAIFAO5vYy0wIhgPMjAyNjEw # MDYxMjA4MTNaGA8yMDI2MTAwNzEyMDgxM1owdjA8BgorBgEEAYRZCgQBMS4wLDAK # AgUA7m9jLQIBADAJAgEAAgEuAgH/MAcCAQACAhJIMAoCBQDucLStAgEAMDYGCisG # AQQBhFkKBAIxKDAmMAwGCisGAQQBhFkKAwKgCjAIAgEAAgMHoSChCjAIAgEAAgMB # hqAwDQYJKoZIhvcNAQELBQADggEBADAXR/fqWT2wAlvBhuheSd3zIjbO92sXybPe # yFmuCViJm4FYxNcrsOy8qFGofi0JoX8n48EVdbjLhrAMkjQi7WHmifklgp1DGAmE # sObXlSxxpsQMbXi4M8pQl348izPN+HXP/uKKL6w3uyxhIRBIYzNd0DZ6XHej5/zi # KEIKA6BNj9KjVJR9DfBWH8EUF1jH3oECmcYxuLuy+eCH2bPfN2BCeuQy20Jhz0MZ # CbpyKdMk8FFf+GwyhgixTbKd1O/zwiu66ge59rjBzcPejY6QanmVOmOyOFgsaSoZ # JQAn5kvX4DGZnR4t9lYPdUfv8YhSyRRRu0qNBGPOLceIVikzFnUxggQNMIIECQIB # ATCBkzB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE # BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYD # VQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAhqpoB0GoA9D # qgABAAACGjANBglghkgBZQMEAgEFAKCCAUowGgYJKoZIhvcNAQkDMQ0GCyqGSIb3 # DQEJEAEEMC8GCSqGSIb3DQEJBDEiBCAujDYlYZeeLWJeP5uhXCRAwYzpA5IPI9U3 # VlKhGOZXIzCB+gYLKoZIhvcNAQkQAi8xgeowgecwgeQwgb0EIJ16Icetu2kpzAHb # R2hVTz1Ycg4fxLvDfu5odwOZ6Yr7MIGYMIGApH4wfDELMAkGA1UEBhMCVVMxEzAR # BgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1p # Y3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3Rh # bXAgUENBIDIwMTACEzMAAAIaqaAdBqAPQ6oAAQAAAhowIgQgxlaM62uq7SIh3d9W # YqAANbpVxQjBnjEIwR6NjiCcmWswDQYJKoZIhvcNAQELBQAEggIAEogxPBqZ7wKv # PGZ5SLy1TVk8iHoU3W/9y+sMqnA+LpIqm6rWFCdvHRyJ9k+HY01avyxyN4kxjHRq # RE5nt+NYaMM4QHL/9rkMysjYWvK7pbQe+U/cKfPe45FBbtNGAFoSLDb82P6qeySV # TxS6kCvAEMUCNSJQ8fZXdJMkNfd1Wpli4Y2u41MUEowzvU6POtMZUEffCw7f+f7G # zhggeR2emGhAHueHcg7AZXGfJyJBywWkJHZdE7tPhBm2gdtmCEFZGDrGuUG/KFcf # k9+kz/73OhR2nVT5hke2zmE8S5KG1uYc6oREl7CFLYfdD/GACNaWlGopcj581jSe # G2Vosq60j1yMgout7s/rlgxCIOBLu1NMwVTV0s4nkBAqyaqLDUdP/RqSaoz3uYgh # PEl1HRmQn4JYJOWmV4JheFPtoyPj46hwu9m5BlR6K2dgw9sp0Z+xChg8eJsoFB4j # dl8ffHuQB0ynESNBeawQrcrEpy7z3b6v1CBDJ/yao6qRnzZxX8guHIfBv04oKcq5 # VtFfYPD+2Jbvdi1OvmVtlpzWq7SzMB5ZDgMmOI6B++mwse54F7xED2NRIEIw6Dgf # LWD85zdehxCOR1qsrCMQwDW+oqv0pe5Enw5Cg++OjyCKHmiWQ5DKdG+7H6/b50/P # 2Rb5k9cy6mcY1/Y8N0/nwu1o9DPZKEY= # SIG # End signature block |