Functions/Invoke-PasmDeployment.ps1

#Requires -Version 5.1
using namespace System.IO
using namespace System.Management.Automation
using namespace System.Collections.Generic
using namespace Amazon.EC2.Model

function Invoke-PasmDeployment {
    [CmdletBinding()]
    [OutputType([PSCustomObject[]])]
    param (
        # Specify the path to the Yaml template.
        [Parameter(Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('file')]
        [ValidateNotNullOrEmpty()]
        [string[]]$FilePath = $($PWD, $('{0}.yml' -f [Pasm.Template.Name]::blueprint) -join [path]::DirectorySeparatorChar)
    )

    begin {
        try {
            Set-StrictMode -Version Latest

            # Load helper functions
            . $($PSScriptRoot, 'Helpers', 'Helpers.ps1' -join [path]::DirectorySeparatorChar)
        }
        catch {
            $PSCmdlet.ThrowTerminatingError($PSItem)
        }
    }

    process {
        try {
            foreach ($file in $filePath) {
                # Load blueprint file
                $obj = Import-PasmFile -FilePath $file -Ordered

                # Rresource variables
                $resource = $obj.Resource
                if ($resource.Contains('SecurityGroup')) { $securityGroup = $obj.Resource.SecurityGroup }
                if ($resource.Contains('NetworkAcl')) { $networkAcl = $obj.Resource.NetworkAcl }
                if ($resource.Contains('PrefixList')) { $prefixList = $obj.Resource.PrefixList }

                # Set AWS default settings for this session
                Set-AWSCredential -ProfileName $obj.Common.ProfileName -Scope Local
                Set-DefaultAWSRegion -Region $obj.Common.Region -Scope Local

                # Create result object list
                $ret = [list[PSCustomObject]]::new()

                # Deploy SecurityGroup: creating a new one if the resouce id does not exist, or updating the entry if it does
                if ($resource.Contains('SecurityGroup')) {
                    foreach ($sg in $securityGroup) {
                        $target = Get-EC2SecurityGroup -Filter @{ Name = 'group-id'; Values = $sg.ResourceId }
                        $evidence = Get-EC2SecurityGroup -Filter @{ Name = 'group-name'; Values = $sg.ResourceName }
                        $ipPermissions = New-PasmSecurityGroupEntry -Rule $sg.Rules

                        # Even if there is no ID, if there is a name, it is considered to be a resource that already exists.
                        if ($null -eq $target -and $null -eq $evidence) {
                            $tags = @{ Key = 'Name'; Value = $sg.ResourceName }
                            $nameTag = [TagSpecification]::new()
                            $nameTag.ResourceType = 'security-group'
                            $nameTag.Tags.Add($tags)

                            $target = Get-EC2SecurityGroup -GroupId $(New-EC2SecurityGroup -GroupName $sg.ResourceName -Description $sg.Description -VpcId $sg.VpcId -TagSpecification $nameTag)

                            # Add entries
                            if ($sg.FlowDirection -eq 'Ingress') {
                                Grant-EC2SecurityGroupIngress -GroupId $target.GroupId -IpPermission $ipPermissions | Out-Null
                            }
                            if ($sg.FlowDirection -eq 'Egress') {
                                Grant-EC2SecurityGroupEgress -GroupId $target.GroupId -IpPermission $ipPermissions | Out-Null
                            }

                            # Overwrite ResourceId
                            $sg.ResourceId = $target.GroupId

                            # Add an object to the result list
                            $ret.Add(
                                [PSCustomObject]@{
                                    ResourceType = [Pasm.Parameter.Resource]::SecurityGroup
                                    ResourceName = $target.GroupName
                                    ResourceId = $target.GroupId
                                    Action = 'Create'
                                }
                            )
                        }
                        else {
                            if ($null -eq $target -and !($null -eq $evidence)) {
                                $target = $evidence
                            }

                            # Replacing entries
                            if ($sg.FlowDirection -eq 'Ingress') {
                                if ($target.IpPermissions) {
                                    Revoke-EC2SecurityGroupIngress -GroupId $target.GroupId -IpPermission $target.IpPermissions | Out-Null
                                }
                                Grant-EC2SecurityGroupIngress -GroupId $target.GroupId -IpPermission $ipPermissions | Out-Null
                            }
                            if ($sg.FlowDirection -eq 'Egress') {
                                if ($target.IpPermissionsEgress) {
                                    Revoke-EC2SecurityGroupEgress  -GroupId $target.GroupId -IpPermission $target.IpPermissionsEgress | Out-Null
                                }
                                Grant-EC2SecurityGroupEgress -GroupId $target.GroupId -IpPermission $ipPermissions | Out-Null
                            }

                            # Overwrite ResourceId
                            $sg.ResourceId = $target.GroupId

                            # Add an object to the result list
                            $ret.Add(
                                [PSCustomObject]@{
                                    ResourceType = [Pasm.Parameter.Resource]::SecurityGroup
                                    ResourceName = $target.GroupName
                                    ResourceId = $target.GroupId
                                    Action = 'Sync'
                                }
                            )
                        }
                    }
                }

                # Deploy NetworkAcl: creating a new one if the resouce id does not exist, or updating the entry if it does
                if ($resource.Contains('NetworkAcl')) {
                    foreach ($nacl in $networkAcl) {
                        $target = Get-EC2NetworkAcl -Filter @{ Name = 'network-acl-id'; Values = $nacl.ResourceId }
                        $evidence = Get-EC2NetworkAcl -Filter @{ Name = 'tag:Name'; Values = $nacl.ResourceName }

                        # Even if there is no ID, if there is a name, it is considered to be a resource that already exists.
                        if ($null -eq $target -and $null -eq $evidence) {
                            $tags = @{ Key = 'Name'; Value = $nacl.ResourceName }
                            $nameTag = [TagSpecification]::new()
                            $nameTag.ResourceType = 'network-acl'
                            $nameTag.Tags.Add($tags)

                            $target = New-EC2NetworkAcl -VpcId $nacl.VpcId -TagSpecification $nameTag

                            # Add entries to the network acl
                            New-PasmNetworkAclEntry $nacl -NetworkAcl $target

                            # Overwrite ResourceId
                            $nacl.ResourceId = $target.NetworkAclId

                            # Add an object to the result list
                            $ret.Add(
                                [PSCustomObject]@{
                                    ResourceType = [Pasm.Parameter.Resource]::NetworkAcl
                                    ResourceName = $target.Tags.Value
                                    ResourceId = $target.NetworkAclId
                                    Action = 'Create'
                                }
                            )
                        }
                        else {
                            if ($null -eq $target -and !($null -eq $evidence)) {
                                $target = $evidence
                            }

                            $naclIngressRules = $target.Entries.Where( { $_.Egress -eq $false -and $_.RuleNumber -ne 32767 } )
                            $naclEgressRules = $target.Entries.Where( { $_.Egress -eq $true -and $_.RuleNumber -ne 32767 } )

                            # Remove entries from the network acl
                            if ($null -ne $naclIngressRules) {
                                foreach ($naclIngressRule in $naclIngressRules) {
                                    Remove-EC2NetworkAclEntry -NetworkAclId $target.NetworkAclId -RuleNumber $naclIngressRule.RuleNumber -Egress $false -Confirm:$false | Out-Null
                                }
                            }
                            if ($null -ne $naclEgressRules) {
                                foreach ($naclEgressRule in $naclEgressRules) {
                                    Remove-EC2NetworkAclEntry -NetworkAclId $target.NetworkAclId -RuleNumber $naclEgressRule.RuleNumber  -Egress $true  -Confirm:$false | Out-Null
                                }
                            }

                            # Add entries to the network acl
                            New-PasmNetworkAclEntry $nacl -NetworkAcl $target

                            # Overwrite ResourceId
                            $nacl.ResourceId = $target.NetworkAclId

                            # Add an object to the result list
                            $ret.Add(
                                [PSCustomObject]@{
                                    ResourceType = [Pasm.Parameter.Resource]::NetworkAcl
                                    ResourceName = $target.Tags.Value
                                    ResourceId = $target.NetworkAclId
                                    Action = 'Sync'
                                }
                            )
                        }
                    }
                }

                # Deploy PrefixList: creating a new one if the resouce id does not exist, or updating the entry if it does
                if ($resource.Contains('PrefixList')) {
                    foreach ($pl in $prefixList) {
                        $target = Get-EC2ManagedPrefixList -Filter @{ Name = 'prefix-list-id'; Values = $pl.ResourceId }
                        $evidence = Get-EC2ManagedPrefixList -Filter @{ Name = 'prefix-list-name'; Values = $pl.ResourceName }
                        $entries = New-PasmPrefixListEntry -Rule $pl.Rules

                        # Even if there is no ID, if there is a name, it is considered to be a resource that already exists.
                        if ($null -eq $target -and $null -eq $evidence) {
                            $tags = @{ Key = 'Name'; Value = $pl.ResourceName }
                            $nameTag = [TagSpecification]::new()
                            $nameTag.ResourceType = 'prefix-list'
                            $nameTag.Tags.Add($tags)

                            $target = New-EC2ManagedPrefixList -PrefixListName $pl.ResourceName -AddressFamily $pl.AddressFamily -MaxEntry $pl.MaxEntry -Entry $entries -TagSpecification $nameTag

                            # Wait for the state to change
                            if ($target.PrefixListId) {
                                while ((Get-EC2ManagedPrefixList -PrefixListId $target.PrefixListId).State.Value -notin ('create-complete', 'modify-complete')) {
                                    Start-Sleep -Milliseconds 1
                                }
                            }

                            # Overwrite ResourceId
                            $pl.ResourceId = $target.PrefixListId

                            # Add an object to the result list
                            $ret.Add(
                                [PSCustomObject]@{
                                    ResourceType = [Pasm.Parameter.Resource]::PrefixList
                                    ResourceName = $target.PrefixListName
                                    ResourceId = $target.PrefixListId
                                    Action = 'Create'
                                }
                            )
                        }
                        else {
                            if ($null -eq $target -and !($null -eq $evidence)) {
                                $target = $evidence
                            }

                            $existingEntries = Get-EC2ManagedPrefixListEntry -PrefixListId $target.PrefixListId

                            # Create an list of entries removing from the prefix list
                            # Delete only those entries that are not included in configuration file
                            $removeEntries = [list[RemovePrefixListEntry]]::new()
                            foreach ($existingEntry in $existingEntries) {
                                if ($existingEntry.Cidr -notin $pl.Rules.Ranges.IpPrefix) {
                                    $removeEntry = [RemovePrefixListEntry]::new()
                                    $removeEntry.Cidr = $existingEntry.Cidr
                                    $removeEntries.Add($removeEntry)
                                }
                            }

                            # Create an list of entries adding to the prefix list
                            # Add to the list only those entries that are not included in '$existingEntries'
                            $addEntries = [list[AddPrefixListEntry]]::new()
                            foreach ($range in $pl.Rules.Ranges) {
                                if ($range.IpPrefix -notin $existingEntries.Cidr) {
                                    $addEntry = [AddPrefixListEntry]::new()
                                    $addEntry.Cidr = $range.IpPrefix
                                    $addEntry.Description = $range.Description
                                    $addEntries.Add($addEntry)
                                }
                            }

                            # Update the prefix list
                            if ($removeEntries -and $addEntries) {
                                Edit-EC2ManagedPrefixList -PrefixListId $target.PrefixListId -RemoveEntry $removeEntries -AddEntry $addEntries -CurrentVersion $target.Version | Out-Null
                            }
                            if ($removeEntries -and -not $addEntries) {
                                Edit-EC2ManagedPrefixList -PrefixListId $target.PrefixListId -RemoveEntry $removeEntries -CurrentVersion $target.Version | Out-Null
                            }
                            if (-not $removeEntries -and $addEntries) {
                                Edit-EC2ManagedPrefixList -PrefixListId $target.PrefixListId -AddEntry $addEntries -CurrentVersion $target.Version | Out-Null
                            }

                            # Wait for the state to change
                            if ($target.PrefixListId) {
                                while ((Get-EC2ManagedPrefixList -PrefixListId $target.PrefixListId).State.Value -notin ('create-complete', 'modify-complete')) {
                                    Start-Sleep -Milliseconds 1
                                }
                            }

                            # Overwrite ResourceId
                            $pl.ResourceId = $target.PrefixListId

                            # Add an object to the result list
                            $ret.Add(
                                [PSCustomObject]@{
                                    ResourceType = [Pasm.Parameter.Resource]::PrefixList
                                    ResourceName = $target.PrefixListName
                                    ResourceId = $target.PrefixListId
                                    Action = 'Sync'
                                }
                            )
                        }
                    }
                }

                # Update metadata section
                if ($obj.Contains('MetaData')) {
                    $metadata = [ordered]@{}
                    $metadata.UpdateNumber = if ($obj.MetaData.Contains('UpdateNumber')) { $obj.MetaData.UpdateNumber }
                    $metadata.DeployNumber = if ($obj.MetaData.Contains('DeployNumber')) { $obj.MetaData.DeployNumber + 1 } else { 1 }
                    $metadata.CleanUpNumber = if ($obj.MetaData.Contains('CleanUpNumber')) { $obj.MetaData.CleanUpNumber }
                    $metadata.PublishedAt = if ($obj.MetaData.Contains('PublishedAt')) { $obj.MetaData.PublishedAt }
                    $metadata.CreatedAt = if ($obj.MetaData.Contains('CreatedAt')) { ([datetime]$obj.Metadata.CreatedAt).ToUniversalTime() }
                    $metadata.UpdatedAt = if ($obj.MetaData.Contains('UpdatedAt')) { ([datetime]$obj.Metadata.UpdatedAt).ToUniversalTime() }
                    $metadata.DeployedAt = [datetime]::Now.ToUniversalTime()
                    $metadata.CleandAt = if ($obj.MetaData.Contains('CleandAt')) { ([datetime]$obj.Metadata.CleandAt).ToUniversalTime() }
                    $obj.MetaData = $metadata
                }

                # Convert the object to Yaml format and overwrite the file
                $obj | ConvertTo-Yaml -OutFile $file -Force

                # Return result list
                $PSCmdlet.WriteObject($ret)

                # Clear AWS default settings for this session
                Clear-AWSDefaultConfiguration -SkipProfileStore
            }
        }
        catch {
            $PSCmdlet.ThrowTerminatingError($PSItem)
        }
    }
    end {
        # Clean up processes, if any
    }

    <#
        .SYNOPSIS
        Reads the configuration generated by the blueprinting process and deploys the actual resources.
 
        .DESCRIPTION
        Reads the configuration generated by the blueprinting process and deploys the actual resources.
        See the following source for details: https://github.com/nekrassov01/Pasm/blob/main/src/Functions/Invoke-PasmDeployment.ps1
 
        .EXAMPLE
        # Default input file path: ${PWD}/blueprint.yml
        Invoke-PasmDeployment
 
        .EXAMPLE
        # Loading multiple files
        Invoke-PasmDeployment -FilePath 'C:/Pasm/blueprint-sg.yml', 'C:/Pasm/blueprint-nacl.yml', 'C:/Pasm/blueprint-pl.yml'
 
        .EXAMPLE
        # Loading multiple files from pipeline
        'C:/Pasm/blueprint-sg.yml', 'C:/Pasm/blueprint-nacl.yml', 'C:/Pasm/blueprint-pl.yml' | Invoke-PasmDeployment
 
        .LINK
        https://github.com/nekrassov01/Pasm
    #>

}