@@ -2,13 +2,20 @@ param (
22 [string ]$RunTerraformInit = " true" ,
33 [string ]$RunTerraformPlan = " true" ,
44 [string ]$RunTerraformPlanDestroy = " false" ,
5- [string ]$RunTerraformApply = " false " ,
5+ [string ]$RunTerraformApply = " true " ,
66 [string ]$RunTerraformDestroy = " false" ,
7+ [string []]$TerraformPlanExtraArgs = $null ,
8+ [string []]$TerraformPlanDestroyExtraArgs = $null ,
9+ [string []]$TerraformApplyExtraArgs = $null ,
10+ [string []]$TerraformDestroyExtraArgs = $null ,
711 [string ]$DebugMode = " false" ,
812 [string ]$DeletePlanFiles = " true" ,
913 [string ]$TerraformVersion = " latest" ,
10- [string ]$RunCheckov = " false" ,
11- [string ]$TfPlanFileName = " tfplan.plan" ,
14+ [string ]$RunCheckov = " true" ,
15+ [string ]$CheckovSkipCheck = " CKV2_AZURE_31" ,
16+ [string ]$CheckovSoftfail = " true" ,
17+ [string ]$TerraformPlanFileName = " tfplan.plan" ,
18+ [string ]$TerraformDestroyPlanFileName = " tfplan-destroy.plan" ,
1219 [string ]$TerraformCodeLocation = " terraform" ,
1320 [string []]$TerraformStackToRun = @ (' all' ),
1421 [string ]$CreateTerraformWorkspace = " true" ,
@@ -28,7 +35,7 @@ $fullTerraformCodePath = Join-Path -Path $currentWorkingDirectory -ChildPath $Te
2835$scriptDir = Split-Path - Path $MyInvocation.MyCommand.Definition - Parent
2936
3037# Import all required modules
31- $modules = @ (" Logger" , " Utils" , " AzureCliLogin" , " Terraform" , " Storage " , " Homebrew" , " Checkov" , " Tenv" , " Choco" )
38+ $modules = @ (" Logger" , " Utils" , " AzureCliLogin" , " Terraform" , " Homebrew" , " Checkov" , " Tenv" , " Choco" , " TerraformDocs " )
3239foreach ($module in $modules )
3340{
3441 $modulePath = Join-Path - Path $scriptDir - ChildPath " PowerShellModules/$module .psm1"
119126 $convertedRunCheckov = ConvertTo-Boolean $RunCheckov
120127 _LogMessage - Level ' DEBUG' - Message " RunCheckov: `" $RunCheckov `" → $convertedRunCheckov " - InvocationName " $ ( $MyInvocation.MyCommand.Name ) "
121128
129+ $convertedCheckovSoftfail = ConvertTo-Boolean $CheckovSoftfail
130+ _LogMessage - Level ' DEBUG' - Message " CheckovSoftfail: `" $CheckovSoftfail `" → $convertedCheckovSoftfail " - InvocationName " $ ( $MyInvocation.MyCommand.Name ) "
131+
132+
122133 $convertedCreateTerraformWorkspace = ConvertTo-Boolean $CreateTerraformWorkspace
123134 _LogMessage - Level ' DEBUG' - Message " CreateTerraformWorkspace: `" $CreateTerraformWorkspace `" → $convertedCreateTerraformWorkspace " - InvocationName " $ ( $MyInvocation.MyCommand.Name ) "
124135
162173 _LogMessage - Level ' ERROR' - Message $msg - InvocationName $MyInvocation.MyCommand.Name
163174 throw $msg
164175 }
176+
177+ $processedStacks = @ ()
165178 try
166179 {
167180
@@ -179,17 +192,98 @@ try
179192 - CodeRoot $fullTerraformCodePath `
180193 - StacksToRun $TerraformStackToRun
181194
195+ # ──────────────────── REVERSE execution order for destroys ────────────────
196+ if ($convertedRunTerraformPlanDestroy -or $convertedRunTerraformDestroy )
197+ {
198+
199+ # 1. sort numerically by the leading digits in the folder name
200+ $stackFolders = $stackFolders |
201+ Sort-Object {
202+ # “C:\...\1_network” → 1
203+ [int ](
204+ (($_ -split ' [\\/]' )[-1 ]) -replace ' ^(\d+)_.*' , ' $1'
205+ )
206+ }
207+
208+ # 2. reverse (static .NET call – do **not** pipe this!)
209+ [array ]::Reverse($stackFolders )
210+ }
211+
182212 foreach ($folder in $stackFolders )
183213 {
184- # Example: validate + fmt-check for each stack
214+
215+ $processedStacks += $folder
216+
217+ # terraform fmt – always safe
185218 Invoke-TerraformFmtCheck - CodePath $folder
186- Invoke-TerraformInit - CodePath $folder
187- if ($convertedCreateTerraformWorkspace -and -not [string ]::IsNullOrWhiteSpace($TerraformWorkspace ))
219+
220+ # ── INIT ──────────────────────────────────────────────────────────────
221+ if ($convertedRunTerraformInit )
222+ {
223+ Invoke-TerraformInit - CodePath $folder - InitArgs ' -input=false' , ' -upgrade=true'
224+ }
225+
226+ # workspace (needs an init first)
227+ if ($convertedRunTerraformInit -and
228+ $convertedCreateTerraformWorkspace -and
229+ -not [string ]::IsNullOrWhiteSpace($TerraformWorkspace ))
188230 {
231+
189232 Invoke-TerraformWorkspaceSelect - CodePath $folder - WorkspaceName $TerraformWorkspace
190233 }
191- Invoke-TerraformValidate - CodePath $folder
234+
235+ # ── VALIDATE ──────────────────────────────────────────────────────────
236+ if ($convertedRunTerraformInit )
237+ {
238+ Invoke-TerraformValidate - CodePath $folder
239+ }
240+
241+ # ── PLAN / PLAN-DESTROY ───────────────────────────────────────────────
242+ if ($convertedRunTerraformPlan )
243+ {
244+ Invoke-TerraformPlan - CodePath $folder - PlanArgs $TerraformPlanExtraArgs - PlanFile $TerraformPlanFileName
245+ }
246+ elseif ($convertedRunTerraformPlanDestroy )
247+ {
248+ Invoke-TerraformPlanDestroy - CodePath $folder - PlanArgs $TerraformPlanDestroyExtraArgs - PlanFile $TerraformDestroyPlanFileName
249+ }
250+
251+ # JSON + Checkov need a plan file
252+ if ($convertedRunTerraformPlan -or $convertedRunTerraformPlanDestroy )
253+ {
254+
255+ if ($convertedRunTerraformPlan )
256+ {
257+ $TfPlanFileName = $TerraformPlanFileName
258+ }
259+
260+ if ($convertedRunTerraformPlanDestroy )
261+ {
262+ $TfPlanFileName = $TerraformDestroyPlanFileName
263+ }
264+
265+ Convert-TerraformPlanToJson - CodePath $folder - PlanFile $TfPlanFileName
266+
267+ if ($convertedRunCheckov -and $convertedRunTerraformPlan )
268+ {
269+ Invoke-Checkov `
270+ - CodePath $folder `
271+ - CheckovSkipChecks $CheckovSkipCheck `
272+ - SoftFail: $convertedCheckovSoftfail
273+ }
274+ }
275+
276+ # ── APPLY / DESTROY ───────────────────────────────────────────────────
277+ if ($convertedRunTerraformApply )
278+ {
279+ Invoke-TerraformApply - CodePath $folder - SkipApprove - ApplyArgs $TerraformApplyExtraArgs
280+ }
281+ elseif ($convertedRunTerraformDestroy )
282+ {
283+ Invoke-TerraformDestroy - CodePath $folder - SkipApprove - DestroyArgs $TerraformDestroyExtraArgs
284+ }
192285 }
286+
193287 }
194288 catch
195289 {
@@ -207,30 +301,48 @@ finally
207301{
208302 if ($convertedDeletePlanFiles )
209303 {
210- foreach ($file in $TfPlanFileName , " ${TfPlanFileName} .json" )
304+
305+ $patterns = @ (
306+ $TfPlanFileName ,
307+ " ${TfPlanFileName} .json" ,
308+ " ${TfPlanFileName} -destroy.tfplan" ,
309+ " ${TfPlanFileName} -destroy.tfplan.json"
310+ )
311+
312+ foreach ($folder in $processedStacks )
211313 {
212- if ( Test-Path $file )
314+ foreach ( $pat in $patterns )
213315 {
214- try
316+
317+ $file = Join-Path $folder $pat
318+ if (Test-Path $file )
215319 {
216- Remove-Item - Path $file - Force - ErrorAction Stop
217- _LogMessage - Level ' DEBUG' - Message " Deleted $file " - InvocationName " $ ( $MyInvocation.MyCommand.Name ) "
320+ try
321+ {
322+ Remove-Item $file - Force - ErrorAction Stop
323+ _LogMessage - Level DEBUG - Message " Deleted $file " `
324+ - InvocationName $MyInvocation.MyCommand.Name
325+ }
326+ catch
327+ {
328+ _LogMessage - Level WARN - Message " Failed to delete $file – $ ( $_.Exception.Message ) " `
329+ - InvocationName $MyInvocation.MyCommand.Name
330+ }
218331 }
219- catch
332+ else
220333 {
221- _LogMessage - Level ' WARN' - Message " Failed to delete $file – $ ( $_.Exception.Message ) " - InvocationName " $ ( $MyInvocation.MyCommand.Name ) "
334+ _LogMessage - Level DEBUG - Message " No file to delete: $file " `
335+ - InvocationName $MyInvocation.MyCommand.Name
222336 }
223337 }
224- else
225- {
226- _LogMessage - Level ' DEBUG' - Message " File not present, nothing to delete: $file " - InvocationName " $ ( $MyInvocation.MyCommand.Name ) "
227- }
228338 }
229339 }
230340 else
231341 {
232- _LogMessage - Level ' DEBUG' - Message ' DeletePlanFiles is false – leaving plan files in place.' - InvocationName " $ ( $MyInvocation.MyCommand.Name ) "
342+ _LogMessage - Level DEBUG - Message ' DeletePlanFiles is false – leaving plan files in place.' `
343+ - InvocationName $MyInvocation.MyCommand.Name
233344 }
345+
234346 if ($convertedUseAzureUserLogin )
235347 {
236348 Disconnect-AzureCli - IsUserDeviceLogin $true
0 commit comments