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

                # 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 }
                        $ipPermissions = New-PasmSecurityGroupEntry -Rule $sg.Rules

                        if ($null -eq $target) {
                            $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
                            }
                            if ($sg.FlowDirection -eq 'Egress') {
                                Grant-EC2SecurityGroupEgress -GroupId $target.GroupId -IpPermission $ipPermissions
                            }

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

                            Out-PasmDeploymentResult -ResourceType SecurityGroup -ResourceName $target.GroupName -ResourceId $target.GroupId -Action 'Create'
                        }
                        else {
                            # 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
                            }
                            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
                            }

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

                            Out-PasmDeploymentResult -ResourceType 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 }

                        if ($null -eq $target) {
                            $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

                            Out-PasmDeploymentResult -ResourceType NetworkAcl -ResourceName $target.Tags.Value -ResourceId $target.NetworkAclId -Action 'Create'
                        }
                        else {
                            $naclIngressRuleNumbers = $target.Entries.Where( { $_.Egress -eq $false -and $_.RuleNumber -ne 32767 } ).RuleNumber
                            $naclEgressRuleNumbers = $target.Entries.Where( { $_.Egress -eq $true -and $_.RuleNumber -ne 32767 } ).RuleNumber

                            # Remove entries from the network acl
                            foreach ($naclIngressRuleNumber in $naclIngressRuleNumbers) {
                                Remove-EC2NetworkAclEntry -NetworkAclId $target.NetworkAclId -RuleNumber $naclIngressRuleNumber -Egress $false -Confirm:$false
                            }
                            foreach ($naclEgressRuleNumber in $naclEgressRuleNumbers) {
                                Remove-EC2NetworkAclEntry -NetworkAclId $target.NetworkAclId -RuleNumber $naclEgressRuleNumber  -Egress $true  -Confirm:$false
                            }

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

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

                            Out-PasmDeploymentResult -ResourceType 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 }
                        $entries = New-PasmPrefixListEntry -Rule $pl.Rules

                        if ($null -eq $target) {
                            $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

                            Out-PasmDeploymentResult -ResourceType PrefixList -ResourceName $target.PrefixListName -ResourceId $target.PrefixListId -Action 'Create'
                        }
                        else {
                            $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

                            Out-PasmDeploymentResult -ResourceType PrefixList -ResourceName $target.PrefixListName -ResourceId $target.PrefixListId -Action 'Sync'
                        }
                    }
                }

                # Clear AWS default settings for this session
                Clear-AWSDefaultConfiguration -SkipProfileStore

                # Update metadata section
                if ($obj.Contains('MetaData')) {
                    $metadata = [ordered]@{}
                    $metadata.UpdateNumber = $obj.MetaData.UpdateNumber
                    $metadata.DeployNumber = if ($obj.MetaData.Contains('DeployNumber')) { $obj.MetaData.DeployNumber + 1 } else { 1 }
                    $metadata.PublishedAt = Get-AWSPublicIpAddressRange -OutputPublicationDate
                    $metadata.CreatedAt = ([datetime]$obj.MetaData.CreatedAt).ToUniversalTime()
                    $metadata.UpdatedAt = ([datetime]$obj.MetaData.UpdatedAt).ToUniversalTime()
                    $metadata.DeployedAt = (Get-Date).ToUniversalTime()
                    $obj.MetaData = $metadata
                }

                # Convert the object to Yaml format and overwrite the file
                $obj | ConvertTo-Yaml -OutFile $file -Force
            }
        }
        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
    #>

}