LibreDevOpsHelpers.Terraform/LibreDevOpsHelpers.Terraform.psm1
# Run 'terraform validate' function Invoke-TerraformValidate { param ( [string]$CodePath ) if (-not (Test-Path $CodePath)) { _LogMessage -Level "ERROR" -Message "Terraform code not found: $TemplatePath" -InvocationName "$( $MyInvocation.MyCommand.Name )" throw "Terraform code not found: $CodePath" } _LogMessage -Level "INFO" -Message "Validating Terraform: $CodePath" -InvocationName "$( $MyInvocation.MyCommand.Name )" Set-Location $CodePath & terraform validate } # Run 'terraform validate' function Invoke-TerraformFmtCheck { param ( [string]$CodePath ) if (-not (Test-Path $CodePath)) { _LogMessage -Level "ERROR" -Message "Terraform code not found: $TemplatePath" -InvocationName "$( $MyInvocation.MyCommand.Name )" throw "Terraform code not found: $CodePath" } _LogMessage -Level "INFO" -Message "Validating Terraform: $CodePath" -InvocationName "$( $MyInvocation.MyCommand.Name )" Set-Location $CodePath & terraform fmt -check } function Get-TerraformStackFolders { [CmdletBinding()] param( [Parameter(Mandatory)] [string] $CodeRoot, [Parameter(Mandatory)] [string[]]$StacksToRun ) if (-not (Test-Path $CodeRoot)) { _LogMessage -Level 'ERROR' -Message "Code root not found: $CodeRoot" ` -InvocationName $MyInvocation.MyCommand.Name throw "Code root not found: $CodeRoot" } $allDirs = Get-ChildItem -Path $CodeRoot -Directory if (-not $allDirs) { _LogMessage -Level 'ERROR' -Message "No stack folders found underneath $CodeRoot" ` -InvocationName $MyInvocation.MyCommand.Name throw "No stack folders found underneath $CodeRoot" } $stackLookup = @{ } foreach ($dir in $allDirs) { if ($dir.Name -match '^(?<order>\d+)[-_](?<name>.+)$') { $stackLookup[$matches.name.ToLower()] = @{ Path = $dir.FullName Order = [int]$matches.order IsNumbered = $true } } elseif ($dir.Name -match '^allstackskip[-_](?<rest>.+)$') { $stackName = $matches.rest -replace '^\d+[-_]', '' $stackLookup[$stackName.ToLower()] = @{ Path = $dir.FullName Order = 9999 IsStackSkip = $true IsNumbered = $false } } else { $stackLookup[$dir.Name.ToLower()] = @{ Path = $dir.FullName Order = 9999 IsNumbered = $false } } } $requested = @( $StacksToRun | ForEach-Object { $_.Trim() } | Where-Object { $_ } ) if ($requested -contains 'all' -and $requested.Count -gt 1) { _LogMessage -Level 'WARN' ` -Message "'all' cannot be combined with explicit stack names – ignoring 'all' and using the named stacks only." ` -InvocationName $MyInvocation.MyCommand.Name $requested = $requested | Where-Object { $_.ToLower() -ne 'all' } } $result = [System.Collections.Generic.List[string]]::new() if (($requested.Count -eq 1) -and ($requested[0].ToLower() -eq 'all')) { _LogMessage -Level 'INFO' -Message 'Running ALL stacks (numeric order)' ` -InvocationName $MyInvocation.MyCommand.Name $stackLookup.GetEnumerator() | Where-Object { $_.Value.IsNumbered -eq $true -and (-not ($_.Value.PSObject.Properties['IsStackSkip'] -and $_.Value.IsStackSkip)) } | Sort-Object { $_.Value.Order } | ForEach-Object { [void]$result.Add($_.Value.Path) } } else { foreach ($stack in $requested) { $key = $stack.ToLower() if (-not $stackLookup.ContainsKey($key)) { _LogMessage -Level 'ERROR' -Message "Stack '$stack' not found under $CodeRoot" ` -InvocationName $MyInvocation.MyCommand.Name throw "Stack '$stack' not found under $CodeRoot" } [void]$result.Add($stackLookup[$key].Path) } } _LogMessage -Level 'DEBUG' ` -Message "Stack execution order → $( $result -join ', ' )" ` -InvocationName $MyInvocation.MyCommand.Name return $result } ############################################################################### # Run `terraform init` ############################################################################### function Invoke-TerraformInit { [CmdletBinding()] param( [Parameter(Mandatory)][string]$CodePath, [string[]]$InitArgs = @(), [bool]$CreateBackendKey = $false, [string]$BackendKeyPrefix = $null, [string]$BackendKeySuffix = $null, [string]$StackFolderName = $null ) $inv = $MyInvocation.MyCommand.Name $orig = Get-Location try { if (-not (Test-Path $CodePath)) { _LogMessage -Level 'ERROR' -Message "Terraform code not found: $CodePath" -InvocationName $inv throw "Terraform code not found: $CodePath" } Set-Location $CodePath # Determine if a backend key is already specified in InitArgs $backendKeyPassed = $InitArgs | Where-Object { $_ -match '^-backend-config=key=' } if ($CreateBackendKey -and (-not $backendKeyPassed)) { # Auto-generate backend key if ($StackFolderName) { $folderName = Split-Path -Path $StackFolderName -Leaf } else { # Default to the last folder in CodePath if StackFolderName not provided $folderName = Split-Path -Path $CodePath -Leaf } $backendKey = "" if ($BackendKeyPrefix) { $backendKey += "$BackendKeyPrefix-" } $backendKey += ($folderName -replace '_', '-') if ($BackendKeySuffix) { $backendKey += "-$BackendKeySuffix" } $backendKey += ".terraform.tfstate" _LogMessage -Level 'DEBUG' -Message "Computed backend key name: $backendKey" -InvocationName $inv $InitArgs += "-backend-config=key=$backendKey" } _LogMessage -Level 'INFO' -Message "Running *terraform init ${InitArgs}* in: $CodePath" -InvocationName $inv & terraform init @InitArgs $code = $LASTEXITCODE _LogMessage -Level 'DEBUG' -Message "terraform init exit-code: $code" -InvocationName $inv if ($code -ne 0) { throw "terraform init failed (exit $code)." } } catch { _LogMessage -Level 'ERROR' -Message $_.Exception.Message -InvocationName $inv throw } finally { Set-Location $orig } } ############################################################################### # Run `terraform workspace select -or-create=true <name>` ############################################################################### function Invoke-TerraformWorkspaceSelect { [CmdletBinding()] param( [Parameter(Mandatory)][string]$CodePath, [Parameter(Mandatory)][string]$WorkspaceName ) $inv = $MyInvocation.MyCommand.Name $orig = Get-Location try { if (-not (Test-Path $CodePath)) { _LogMessage -Level 'ERROR' -Message "Terraform code not found: $CodePath" -InvocationName $inv throw "Terraform code not found: $CodePath" } _LogMessage -Level 'INFO' -Message "Selecting workspace '$WorkspaceName' (auto-create) in $CodePath" -InvocationName $inv Set-Location $CodePath & terraform workspace select -or-create=true $WorkspaceName $code = $LASTEXITCODE _LogMessage -Level 'DEBUG' -Message "terraform workspace select exit-code: $code" -InvocationName $inv if ($code -ne 0) { throw "workspace selection failed (exit $code)." } } catch { _LogMessage -Level 'ERROR' -Message $_.Exception.Message -InvocationName $inv throw } finally { Set-Location $orig } } function Invoke-TerraformPlan { [CmdletBinding()] param( [Parameter(Mandatory)][string] $CodePath, [string] $PlanFile = 'tfplan.plan', [string[]]$PlanArgs = @() ) $inv = $MyInvocation.MyCommand.Name $orig = Get-Location try { if (-not (Test-Path $CodePath)) { throw "Terraform code not found: $CodePath" } _LogMessage -Level 'INFO' -Message "terraform plan → $PlanFile" -InvocationName $inv Set-Location $CodePath $tfArgs = @('plan', '-input=false', '-out', $PlanFile) + $PlanArgs & terraform @tfArgs if ($LASTEXITCODE) { throw "terraform plan failed ($LASTEXITCODE)" } } finally { Set-Location $orig } } function Invoke-TerraformPlanDestroy { [CmdletBinding()] param( [Parameter(Mandatory)][string] $CodePath, [string] $PlanFile = 'tfplan.plan.destroy', [string[]]$PlanArgs = @() ) $inv = $MyInvocation.MyCommand.Name $orig = Get-Location try { if (-not (Test-Path $CodePath)) { throw "Terraform code not found: $CodePath" } _LogMessage -Level 'INFO' -Message "terraform plan -destroy → $PlanFile" -InvocationName $inv Set-Location $CodePath $tfArgs = @('plan', '-destroy', '-input=false', '-out', $PlanFile) + $PlanArgs & terraform @tfArgs if ($LASTEXITCODE) { throw "terraform plan -destroy failed ($LASTEXITCODE)" } } finally { Set-Location $orig } } function Invoke-TerraformApply { [CmdletBinding()] param( [Parameter(Mandatory)][string] $CodePath, $PlanFile = "tfplan.plan", [switch] $SkipApprove, [string[]]$ApplyArgs = @() ) $inv = $MyInvocation.MyCommand.Name $orig = Get-Location try { if (-not (Test-Path $CodePath)) { throw "Terraform code not found: $CodePath" } _LogMessage -Level 'INFO' -Message "terraform apply ‹$PlanFile›" -InvocationName $inv Set-Location $CodePath $cmd = @('apply') if (-not $SkipApprove) { $cmd += '-auto-approve' } $cmd += @($PlanFile) + $ApplyArgs & terraform @cmd if ($LASTEXITCODE) { throw "terraform apply failed ($LASTEXITCODE)" } } finally { Set-Location $orig } } function Invoke-TerraformDestroy { [CmdletBinding()] param( [Parameter(Mandatory)][string] $CodePath, $PlanFile = "tfplan-destroy.plan", [switch] $SkipApprove, [string[]]$DestroyArgs = @() ) $inv = $MyInvocation.MyCommand.Name $orig = Get-Location try { if (-not (Test-Path $CodePath)) { throw "Terraform code not found: $CodePath" } _LogMessage -Level 'INFO' -Message "terraform apply (destroy) ‹$PlanFile›" -InvocationName $inv Set-Location $CodePath $cmd = @('apply') if (-not $SkipApprove) { $cmd += '-auto-approve' } $cmd += @($PlanFile) + $ApplyArgs & terraform @cmd if ($LASTEXITCODE) { throw "terraform apply (destroy) failed ($LASTEXITCODE)" } } finally { Set-Location $orig } } function Convert-TerraformPlanToJson { [CmdletBinding()] param( [Parameter(Mandatory)][string] $CodePath, # Binary plan created by Invoke-TerraformPlan [string] $PlanFile = 'tfplan.plan', # Override JSON file name (default = <PlanFile>.json) [string] $JsonFile = $null, # Only emit the JSON path when caller asks for it [switch] $PassThru ) $inv = $MyInvocation.MyCommand.Name $orig = Get-Location if (-not $JsonFile) { $JsonFile = "${PlanFile}.json" } try { # ── checks ─────────────────────────────────────────────────────────── if (-not (Test-Path $CodePath)) { _LogMessage -Level 'ERROR' -Message "Terraform code not found: $CodePath" -InvocationName $inv throw "Terraform code not found: $CodePath" } $planPath = Join-Path $CodePath $PlanFile if (-not (Test-Path $planPath)) { _LogMessage -Level 'ERROR' -Message "Plan file not found: $planPath" -InvocationName $inv throw "Plan file not found: $planPath" } # ── convert ───────────────────────────────────────────────────────── _LogMessage -Level 'INFO' -Message "Converting $PlanFile → $JsonFile" -InvocationName $inv Set-Location $CodePath $jsonPath = Join-Path $CodePath $JsonFile terraform show -json $PlanFile | Out-File -FilePath $jsonPath -Encoding utf8 $code = $LASTEXITCODE _LogMessage -Level 'DEBUG' -Message "terraform show exit-code: $code" -InvocationName $inv if ($code -ne 0) { throw "terraform show failed (exit $code)." } if (-not (Test-Path $jsonPath)) { throw 'JSON output not created.' } _LogMessage -Level 'INFO' -Message "JSON plan written to $jsonPath" -InvocationName $inv if ($PassThru) { return $jsonPath } # ← only emit when requested } catch { _LogMessage -Level 'ERROR' -Message $_.Exception.Message -InvocationName $inv throw } finally { Set-Location $orig } } ############################################################################### # Update the module export list ############################################################################### Export-ModuleMember -Function ` Invoke-TerraformValidate, ` Invoke-TerraformFmtCheck, ` Get-TerraformStackFolders, ` Invoke-TerraformInit, ` Invoke-TerraformWorkspaceSelect, ` Invoke-TerraformPlan, ` Invoke-TerraformPlanDestroy, ` Invoke-TerraformApply, ` Invoke-TerraformDestroy, ` Convert-TerraformPlanToJson |