PSTerraformParser.psm1

using module .\PSTerraformParser-Classes.psm1
function Get-ModuleNames {
    param(
        [parameter(Mandatory, ValueFromPipeline)]
        [string]
        $string
    )

    process {
        $output = @()
        $sendIt = $false
        foreach ($element in $string.split(".")) {
            if ($true -eq $sendIt) {
                $output += $element
            }

            $sendIt = $element -eq "module"
        }

        Write-Output ($output -join '.')
    }
}

function Read-TerraformPlan {
    param(
        # Specifies a path to one or more locations.
        [Parameter(Mandatory = $true,
            Position = 0,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true)]
        [Alias("PSPath")]
        [ValidateNotNullOrEmpty()]
        [string]
        $Path
    )

    begin {
        $noChangesStr = 'No changes. Infrastructure is up-to-date.'
        $contentStartStr = 'Terraform will perform the following actions:'
        $contentEndStr = 'Plan:'
        $changesSeperator = ' => '
        $newResourceForced = ' (forces new resource)'

        $actionMapping = @{
            '+'   = [action]::create
            '-'   = [action]::destroy
            '-/+' = [action]::replace
            '~'   = [action]::update
            '<='  = [action]::read
        }
    }

    process {
        $Data = Get-Content $Path

        $result = [terraformplan]::new()

        # error out if no start is found
        if (($data -match "^$contentStartStr").count -eq 0) {
            $result.errors += [error]@{
                code    = "UNABLE_TO_FIND_STARTING_POSITION_WITHIN_FILE"
                message = "Did not find magic starting string: $contentStartStr"
            }
            return $result
        }

        # error out if no end is found
        if (($data -match "^$contentEndStr").count -eq 0) {
            $result.errors += [error]@{
                code    = "UNABLE_TO_FIND_ENDING_POSITION_WITHIN_FILE"
                message = "Did not find magic ending string: $contentEndStr"
            }
            return $result
        }

        # return empty result because no changes were found
        if ($data -match $noChangesStr) {
            return $result
        }

        $startDetected = $false
        $newResource = $true

        foreach ($line in $data) {
            # start processing since we've found the start
            if ($false -eq $startDetected -and $line -match "^$contentStartStr") {
                $startDetected = $true
                continue
            }

            # stop if at the end
            if ($startDetected -and $line -match "^$contentEndStr") {
                break;
            }

            if ($true -eq $startDetected) {
                # ignore blanks lines and start new resources
                if ([string]::IsNullOrWhiteSpace($line)) {
                    if ($null -ne $changedResource) {
                        if ($changedResource.action -eq [action]::read) {
                            $result.changedDataSources += $changedResource
                        } else {
                            $result.changedResources += $changedResource
                        }
                        $changedResource = $null
                    }
                    $newResource = $true
                    continue
                }

                # process new resources
                if ($true -eq $newResource) {
                    $newResource = $false
                    $changedResource = [change]::new()

                    if ($line -match '^ *(?<action>[\+\-\/~<=]+) (?<resource>\S+)(( \(((?<tainted>tainted)|(?<newresource>new resource required))\))*)?') {
                        $changedResource.action = $actionMapping[$matches.action]
                        $changedResource.path = $matches.resource

                        $resourceComponents = $matches.resource.Split(".")
                        $changedResource.name = $resourceComponents[-1]
                        $changedResource.type = $resourceComponents[-2]
                        $changedResource.tainted = (![string]::IsNullOrEmpty($matches.tainted))
                        $changedResource.newResourceRequired = (![string]::IsNullOrEmpty($matches.newresource))
                        $changedResource.module = $changedResource.path | Get-ModuleNames

                    } else {
                        $result.errors += [error]@{
                            code    = "UNABLE_TO_PARSE_CHANGE_LINE"
                            message = "Unable to parse '$line' (ignoring)"
                        }
                    }
                }

                # add properties to new resource
                if ($null -ne $changedResource -and $line -match '^ +(?<attribute>\S+): *(?<value>.+)') {
                    $attributechanges = [changedattribute]::new()

                    $old, $new = $matches.Value.split("=>").trim()

                    $attributechanges.old = [attributevalue]::new($old)

                    if ($null -ne $new) {
                        $attributechanges.new = [attributevalue]::new($new)
                    }

                    $attributechanges.forcesNewResource = $value -match ' \(forces new resource\) *$'

                    $attribute = @{$matches.attribute = $attributechanges }

                    $changedResource.changedAttributes += $attribute
                }
            }
        }

        return $result
    }
}
Export-ModuleMember -Function Read-TerraformPlan