AVDManagementFramework.psm1

$script:ModuleRoot = $PSScriptRoot
$script:ModuleVersion = (Import-PowerShellDataFile -Path "$($script:ModuleRoot)\AVDManagementFramework.psd1").ModuleVersion

# Detect whether at some level dotsourcing was enforced
$script:doDotSource = Get-PSFConfigValue -FullName AVDManagementFramework.Import.DoDotSource -Fallback $false
if ($AVDManagementFramework_dotsourcemodule) { $script:doDotSource = $true }

<#
Note on Resolve-Path:
All paths are sent through Resolve-Path/Resolve-PSFPath in order to convert them to the correct path separator.
This allows ignoring path separators throughout the import sequence, which could otherwise cause trouble depending on OS.
Resolve-Path can only be used for paths that already exist, Resolve-PSFPath can accept that the last leaf my not exist.
This is important when testing for paths.
#>


# Detect whether at some level loading individual module files, rather than the compiled module was enforced
$importIndividualFiles = Get-PSFConfigValue -FullName AVDManagementFramework.Import.IndividualFiles -Fallback $false
if ($AVDManagementFramework_importIndividualFiles) { $importIndividualFiles = $true }
if (Test-Path (Resolve-PSFPath -Path "$($script:ModuleRoot)\..\.git" -SingleItem -NewChild)) { $importIndividualFiles = $true }
if ("<was compiled>" -eq '<was not compiled>') { $importIndividualFiles = $true }

function Import-ModuleFile
{
    <#
        .SYNOPSIS
            Loads files into the module on module import.
 
        .DESCRIPTION
            This helper function is used during module initialization.
            It should always be dotsourced itself, in order to proper function.
 
            This provides a central location to react to files being imported, if later desired
 
        .PARAMETER Path
            The path to the file to load
 
        .EXAMPLE
            PS C:\> . Import-ModuleFile -File $function.FullName
 
            Imports the file stored in $function according to import policy
    #>

    [CmdletBinding()]
    Param (
        [string]
        $Path
    )

    $resolvedPath = $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($Path).ProviderPath
    if ($doDotSource) { . $resolvedPath }
    else { $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create([io.file]::ReadAllText($resolvedPath))), $null, $null) }
}

#region Load individual files
if ($importIndividualFiles)
{
    # Execute Preimport actions
    foreach ($path in (& "$ModuleRoot\internal\scripts\preimport.ps1")) {
        . Import-ModuleFile -Path $path
    }

    # Import all internal functions
    foreach ($function in (Get-ChildItem "$ModuleRoot\internal\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore))
    {
        . Import-ModuleFile -Path $function.FullName
    }

    # Import all public functions
    foreach ($function in (Get-ChildItem "$ModuleRoot\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore))
    {
        . Import-ModuleFile -Path $function.FullName
    }

    # Execute Postimport actions
    foreach ($path in (& "$ModuleRoot\internal\scripts\postimport.ps1")) {
        . Import-ModuleFile -Path $path
    }

    # End it here, do not load compiled code below
    return
}
#endregion Load individual files

#region Load compiled code
<#
This file loads the strings documents from the respective language folders.
This allows localizing messages and errors.
Load psd1 language files for each language you wish to support.
Partial translations are acceptable - when missing a current language message,
it will fallback to English or another available language.
#>

Import-PSFLocalizedString -Path "$($script:ModuleRoot)\en-us\*.psd1" -Module 'AVDManagementFramework' -Language 'en-US'

function New-AVDMFResourceName {
    <#
    .SYNOPSIS
        This function generates resource names as per the naming convention.
    .DESCRIPTION
        The function reads naming conventions and abbreviations from configuration files and outputs resource names to use.
    .EXAMPLE
        TODO: Add Examples
        PS C:\> <example usage>
        Explanation of what the example does
    .INPUTS
        Inputs (if any)
    .OUTPUTS
        Output (if any)
    .NOTES
        General notes
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = "Does not change any states")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")]
    [CmdletBinding()]
    param (
        [Parameter()]
        [string] $ResourceType,
        [string] $DeploymentStage = $script:DeploymentStage,
        [string] $ResourceCategory,
        [string] $AccessLevel, # Enterprise, Specialist, Privileged
        [string] $HostPoolType, # Shared, Dedicated

        [string] $HostPoolInstance,

        [string] $ParentName,
        [string] $AddressPrefix,


        [Int] $InstanceNumber

        #TODO: Change parameters to overloads so we don't have to provide them. (Except deployment stage?)
    )

    $namingStyle = $script:NamingStyles | Where-Object { $_.ResourceType -eq $ResourceType }
    if (-not $namingStyle) {
        $namingStyle = $script:NamingStyles | Where-Object { $_.ResourceType -eq 'Default' }
    }

    [array] $nameArray = foreach ($component in $namingStyle.NameComponents) {
        if ($component -like "*Abv") {
            $componentName = $component -replace "Abv", ""
            $componentNC = $component -replace "Abv", "NC"

            #Check if component naming convention is avaialbe
            # TODO: Use hashtable for naming conventions instead of dynamic variable names!
            # Assumption - hashtable stored in $script:namingConvention
            try { Get-Variable -Name $componentNC -ErrorAction Stop | Out-Null }
            catch { throw "Could not find a naming convention for component: $componentName. It should be supplied in configuration as .\NamingConvention\Components\$($componentName).json" }

            # Default or custom abbreviation
            $componentNCmembers = (get-variable -Name $componentNC).value | Get-Member -MemberType NoteProperty | Where-Object Name -NE $componentName
            $abbreviationMarker = ($componentNCmembers | Where-Object Name -EQ ("{0}Abv" -f $ResourceType)).Name
            if (-Not $abbreviationMarker) { $abbreviationMarker = "Abbreviation" }

            if ($componentName -eq 'Subscription') {

                $namingConvention = (Get-Variable -Name $componentNC -Scope Script).Value
                $filterScript = [ScriptBlock]::Create("`$_.DeploymentStage -eq `$DeploymentStage")
                #$Command = "(`$Script:$componentNC | Where-Object DeploymentStage -contains `$DeploymentStage).$abbreviationMarker" #TODO: Remove me
            }
            else {
                $namingConvention = (Get-Variable -Name $componentNC -Scope Script).Value
                $filterScript = [ScriptBlock]::Create("`$_.$componentName -eq `$$componentName")

                #$Command = "(`$Script:$componentNC | Where-Object $componentName -eq `$$componentName).$abbreviationMarker" #TODO: Remove me
                #TODO: Review with Fred

            }
            #FRED: $script:namingConvention[$componentName].$abbreviationMarker
            #$abv = Invoke-Expression -Command $Command #TODO: Remove me
            $abv = ($namingConvention | Where-Object -FilterScript $filterScript).$abbreviationMarker
            if (-not $abv) {throw "Could not find any abbreviation for $componentName`: $((Get-Variable -Name $componentName).Value)" }
            $abv
        }
        if ($component -in ('-', '_')) { $component }

        if ($component -eq 'ParentName') {
            $ParentName
        }

        if ($component -eq 'AddressPrefix') {
            $AddressPrefix -replace "/", "-"
        }
        if ($component -eq 'HostPoolInstance') {
            $HostPoolInstance
        }
    }
    $resourceName = $nameArray -join "" -replace "-All", "" -replace "All", ""
    if ($namingStyle.LowerCase) { $resourceName = $resourceName.ToLower() }

    if ($namingStyle.NameComponents[-1] -eq 'InstanceNumber') {
        #TODO: Move this part to the main loop.
        if ($InstanceNumber) {
            $resourceName = "{0}{1:D2}" -f $resourceName, $InstanceNumber
        }
        else {
            $scriptResourceType = (Get-Variable -Name "$($ResourceType)s" -Scope Script).Value
            $filterScript = [ScriptBlock]::Create("`$_ -like `"$resourceName*`"")
            $count = ($scriptResourceType.Keys | Where-Object -FilterScript $filterScript).Count

            #TODO: Fix this once we have resource name attribute for all resource
            if($count -eq 0){
                $count = ($scriptResourceType.GetEnumerator() | ForEach-Object{$_.Value.ResourceName} | Where-Object -FilterScript $filterScript).count
            }

            $resourceName = "{0}{1:D2}" -f $resourceName, ($Count + 1)
        }
    }

    if ($resourceName.length -gt $namingStyle.MaxLength) { throw "resulting resource name is longer than $($namingStyle.MaxLength) characters '$resourceName'" }

    $resourceName
}


function Convert-HashtableToArray {
    [OutputType('System.Array')]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline=$true )]
        [Hashtable] $InputObject
    )
    process {
        $output = foreach ($key in $InputObject.Keys){
            $object = @{
                Name = $key
            }
            $Members = $InputObject[$key] | Get-Member -MemberType NoteProperty # TODO: $hash['h104p01-vnet-pv-01'].psobject.Properties.name
            $Members | ForEach-Object {$object[$_.Name] = $InputObject[$key].($_.Name)}

            $object
        }

        ,$output # "The comma makes it output an array ALWAYS, that's it" -Fred!
    }
}

function Get-AVDMFResourceInfo {
    [CmdletBinding()]
    param (
        [string] $ResourceId
    )
    $pattern = '^\/subscriptions\/(?<SubscriptionId>.+)\/resourceGroups\/(?<ResourceGroupName>.+)\/providers.+\/(?<ResourceName>.+$)'
    if($ResourceId -match $pattern){
        [PSCustomObject]@{
            SubscriptionId = $Matches.SubscriptionId
            ResourceGroupName = $Matches.ResourceGroupName
            ResourceName = $Matches.ResourceName
        }
    }
    else {throw "Resource ID is not valid: $ResourceId"}
}


function Get-RandomPassword {
    #Link: https://gist.github.com/onlyann/00d9bb09d4b1338ffc88a213509a6caf

    param(
        [Parameter(Mandatory = $false)]
        [ValidateRange(12, 256)]
        [int]
        $length = 14
    )
    $symbols = '!@#$%^&*'.ToCharArray()
    $characterList = 'a'..'z' + 'A'..'Z' + '0'..'9' + $symbols
    do {
        $password = ""
        for ($i = 0; $i -lt $length; $i++) {
            $randomIndex = [System.Security.Cryptography.RandomNumberGenerator]::GetInt32(0, $characterList.Length)
            $password += $characterList[$randomIndex]
        }

        [int]$hasLowerChar = $password -cmatch '[a-z]'
        [int]$hasUpperChar = $password -cmatch '[A-Z]'
        [int]$hasDigit = $password -match '[0-9]'
        [int]$hasSymbol = $password.IndexOfAny($symbols) -ne -1

    }
    until (($hasLowerChar + $hasUpperChar + $hasDigit + $hasSymbol) -ge 3)

    $password #| ConvertTo-SecureString -AsPlainText
}

function New-AVDMFSubnetRange {
    [cmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '',Justification = "Does not change any states")]
    param(

        # Address Space for the subnet, this can be any of the address spaces created under the vNet in the format X.X.X.X/X
        [Parameter(Mandatory = $true)][string]$AddressSpace,

        # Mask bits of the new subnet, written as XX (example 27)
        [Parameter(Mandatory = $true)][int]$NewSubnetMaskBits

    )
    #region: functions
    function ConvertFrom-DecimalIPtoBinary ([string]$DecimalIPAddress) {
        #Create an empty variable
        $Binary = $null

        #Extract octets from IP Address
        $Octets = $DecimalIPAddress.Split('.')

        #Convert each octet to Binary and add to the variable $Binary
        # Here we use ToString with '2' as the base, 2 means binary
        # We are also using padleft to make sure each octet is 8 bits long with leading zeros if needed
        $Octets | ForEach-Object { $Binary += ([convert]::ToString($_, 2)).PadLeft(8, "0") }

        return $Binary
    }
    function ConvertFrom-BinaryIPtoDecimal ([string]$BinaryIPAddress) {
        #Create an empty string
        $Decimal = $null

        #Split Binary address into 4 octets - And convert to decimal
        # Again, 2 is for the base.
        $Octets = for ($i = 0; $i -lt 4; $i++) {
            [convert]::ToInt32($BinaryIPAddress.Substring($i * 8, 8), 2)
        }

        # Join the octets into one string with "." as delimeter
        $Decimal = $Octets -join "."

        return $Decimal
    }
    function Find-IPAddressesInRange ($FirstIPAddress, $LastIPAddress) {
        # First we confirm the IP Addresses to Binary, then to int64
        $Int64IP1 = [convert]::Toint64((ConvertFrom-DecimalIPtoBinary $FirstIPAddress), 2)
        $Int64IP2 = [convert]::Toint64((ConvertFrom-DecimalIPtoBinary $LastIPAddress), 2)

        # Then we just create a loop of all the values in the range of int64
        $IPAddresses = for ($i = $Int64IP1; $i -le $Int64IP2; $i++) {
            #Finally, we convert the int64 to binary then back to a decimal IP Address
            ConvertFrom-BinaryIPtoDecimal ([convert]::ToString($i, 2)).padleft(32, "0")
        }
        return $IPAddresses
    }
    function Get-SubnetDetails ($IPAddress, $MaskBits) {
        $BinaryIPAddress = ConvertFrom-DecimalIPtoBinary $IPAddress
        $SubnetID = ConvertFrom-BinaryIPtoDecimal $BinaryIPAddress.Substring(0, $MaskBits).PadRight(32, '0')
        $BroadcastIP = ConvertFrom-BinaryIPtoDecimal $BinaryIPAddress.Substring(0, $MaskBits).PadRight(32, '1')
        return [PSCustomObject]@{
            SubnetID      = $SubnetID
            BroadcastIP   = $BroadcastIP
            AddressPrefix = "$SubnetID/$MaskBits"
        }
    }
    function Test-OverlappingSubnets ($SubnetA, $SubnetB) {
        $SubnetAIDDigital = [convert]::Toint64((ConvertFrom-DecimalIPtoBinary $SubnetA.SubnetID), 2)
        $SubnetABCDigital = [convert]::Toint64((ConvertFrom-DecimalIPtoBinary $SubnetA.BroadcastIP), 2)

        $SubnetBIDDigital = [convert]::Toint64((ConvertFrom-DecimalIPtoBinary $SubnetB.SubnetID), 2)
        $SubnetBBCDigital = [convert]::Toint64((ConvertFrom-DecimalIPtoBinary $SubnetB.BroadcastIP), 2)

        if ($SubnetAIDDigital -ge $SubnetBIDDigital -and $SubnetAIDDigital -le $SubnetBBCDigital) { $Overlap = $true }
        elseif ($SubnetABCDigital -ge $SubnetBIDDigital -and $SubnetABCDigital -le $SubnetBBCDigital) { $Overlap = $true }
        else { $Overlap = $false }

        return $Overlap
    }
    #endregion: functions


    #region: Analyzing Address Space
    Write-Verbose -Message "Analyzing Address Space"
    #Find the position of '/' in the provided address space.
    $AddressSpaceIndexOfMaskBits = $AddressSpace.IndexOf("/")
    $AddressSpaceID = $AddressSpace.Substring(0, $AddressSpaceIndexOfMaskBits)
    $AddressSpaceMaskBits = $AddressSpace.Substring($AddressSpaceIndexOfMaskBits + 1)

    Write-Verbose -Message "ID: $AddressSpaceID - MaskbBits: $AddressSpaceMaskBits"


    $AddressSpaceSize = [math]::Pow(2, 32 - $AddressSpaceMaskBits)
    Write-Verbose -Message "Address Space Size: $AddressSpaceSize"


    Write-Verbose -Message "Finished Analyzing Address Space"
    #endregion: Analyzing Address Space

    #region: Find all possible subnets
    Write-Verbose -Message "ENTER: Find all possible subnets"

    $NewSubnetSize = [math]::Pow(2, (32 - $NewSubnetMaskBits))
    Write-Verbose -Message "New Subnet Size: $NewSubnetSize"

    $NumberOfPossibleSubnets = $AddressSpaceSize / $NewSubnetSize
    Write-Verbose -Message "Number of Possible Subnets: $NumberOfPossibleSubnets"

    $PossibleSubnetsArray = @(Get-SubnetDetails $AddressSpaceID $NewSubnetMaskBits)
    for ($i = 1; $i -lt $NumberOfPossibleSubnets; $i++) {
        $LastSubnetInt64 = ([convert]::Toint64((ConvertFrom-DecimalIPtoBinary $PossibleSubnetsArray[$i - 1].BroadcastIP), 2))
        $NextSubnetID = ConvertFrom-BinaryIPtoDecimal ([convert]::ToString($LastSubnetInt64 + 1, 2)).padleft(32, '0')

        $PossibleSubnetsArray += Get-SubnetDetails $NextSubnetID $NewSubnetMaskBits
    }

    Write-Verbose -Message "Calculated $($PossibleSubnetsArray.Count) possible subnets."

    Write-Verbose -Message "Exit: Find all possible subnets"
    #endregion: Find all possible subnets

    #region: Collect vNet information
    Write-Verbose -Message "ENTER: Collect vNet information"

    $vNetSubnets = foreach ($key in $script:Subnets.Keys) {
        if ((ConvertFrom-DecimalIPtoBinary ($script:Subnets[$key].Properties.AddressPrefix.Substring(0, $script:Subnets[$key].Properties.AddressPrefix.IndexOf("/")))).Substring(0, $AddressSpaceMaskBits) `
                -eq
            (ConvertFrom-DecimalIPtoBinary ($AddressSpaceID)).Substring(0, $AddressSpaceMaskBits)) {
            $script:Subnets[$key]
        }
    }

    Write-Verbose -Message "Found $($vNetSubnets.count) subnets in the vNet belonging to the address space $AddressSpace"

    $UtilizedAddressesArray = foreach ($Subnet in $vNetSubnets) {
        $IndexOfSubnetMask = $Subnet.AddressPrefix.indexOf("/")
        $SubnetID = $Subnet.AddressPrefix.Substring(0, $IndexOfSubnetMask)
        $MaskBits = $Subnet.AddressPrefix.Substring($IndexOfSubnetMask + 1)
        Get-SubnetDetails -IPAddress $SubnetID -MaskBits $MaskBits
    }


    Write-Verbose -Message "Calculated utilized addresses"

    Write-Verbose -Message "Exit: Collect vNet information"
    #endregion: Collect vNet information

    #region: Return the first free subnet
    Write-Verbose -Message "ENTER: Return the first free subnet"

    foreach ($PossibleSubnet in $PossibleSubnetsArray) {
        foreach ($ExistingSubnet in $UtilizedAddressesArray) {
            $Overlap = $false
            if (Test-OverlappingSubnets $PossibleSubnet $ExistingSubnet) {
                $Overlap = $true
                break
            }
        }
        if (!($Overlap)) { return $PossibleSubnet }
    }

    Write-Verbose -Message "Exit: Return the first free subnet"
    #endregion: Return the first free subnet

    # if we did not return any subnet, throw an error
    throw "Could not find any free subnets"

}

function Set-AVDMFNameMapping {
    <#
    .SYNOPSIS
        Takes a dataset and converts any %XXXX% into mapping.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [Hashtable] $Dataset
    )

    foreach ($item in ($dataset.GetEnumerator() | Where-Object { $null -ne $_.Value } )){

        #if ($null -eq $item.Value) { continue } # Value is null nothing to replace
        if ($item.Value.GetType().Name -eq 'String'){
            $stringMappings = ([regex]::Matches($item.Value, '%.+?%')).Value | ForEach-Object { if ($_) { $_ -replace "%", "" } }
            foreach ($mapping in $stringMappings) {
                $mappedValue = $script:NameMappings[$mapping]
                $item.Value = $item.Value -replace "%$mapping%", $mappedValue
            }
            $dataset[$item.Key] = $item.Value
        }
        if ($item.Value.GetType().Name -eq 'PSCustomObject') {
            $dataset[$item.Key] =[PSCustomObject] (Set-AVDMFNameMapping -Dataset ($item.Value | ConvertTo-PSFHashtable))
        }
    }
    $Dataset
}

function Set-AVDMFStageEntries {
    <#
    .SYNOPSIS
        This function replaces "Stages" token in json objects depending on the current stage or a default one.
    .Example
        $json = @"
        {
            "SampleProperty": {
                "DeploymentStage": {
                    "Development": 10,
                    "Production": 5,
                    "Default": 15
                }
            }
        }
        "@
        $dataset = $json | ConvertFrom-Json | ConvertTo-PSFHashtable
        Set-AVDMFStageEntries -Dataset $dataset
 
        Assuming the current stage name is "Development", the output will be that SampleProperty = 10
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [Hashtable] $Dataset,

        [string] $DeploymentStage = $script:DeploymentStage,
        [string] $StageToken = "DeploymentStage"
    )
    foreach ($key in ([array]$Dataset.Keys)) {

        if ($null -eq $Dataset[$key]) { continue }

        if ($Dataset[$key].GetType().Name -eq 'PSCustomObject') {
            if ($Dataset[$key] | Get-Member -MemberType NoteProperty -Name $StageToken) {
                # Get list of configured stages under the stage token
                $configuredStages = ($Dataset[$key].$StageToken | Get-Member -MemberType NoteProperty).Name

                if ( $configuredStages -contains $DeploymentStage) {
                    $Dataset[$key] = $Dataset[$key].$StageToken.($DeploymentStage)
                    Write-PSFMessage -Level Verbose -Message "Set $key to $DeploymentStage value: $($Dataset[$key])"
                }
                elseif ( $configuredStages -contains "Default" ) {
                    $Dataset[$key] = $Dataset[$key].$StageToken.Default
                    Write-PSFMessage -Level Verbose -Message "Set $key to Default value: $($Dataset[$key])"
                }
                else {
                    throw "Could not resolve stage value ($DeploymentStage) for `r`n $($Dataset | Out-String)"
                }
            }
            else { # key is a PSCustomObject that does not have a stage token, maybe one of its children.
                $Dataset[$key] = [PSCustomObject] (Set-AVDMFStageEntries -Dataset ($Dataset[$key] | ConvertTo-PSFHashtable))
            }
        }
    }
    $Dataset
}


function Invoke-AVDMFConfiguration {
    [CmdletBinding()]
    param (

    )

    # Create resource groups
    Write-PSFMessage -Level Host -Message "Invoking resource groups."
    foreach ($rg in $script:ResourceGroups.Keys) {
        $newAzResourceGroup = @{
            Name = $rg
            Location = $script:Location
            Force = $true
        }
        if($script:ResourceGroups[$rg].Tags){
            $newAzResourceGroup['Tags'] = $script:ResourceGroups[$rg].Tags
        }
        New-AzResourceGroup @newAzResourceGroup
    }
    #TODO: Decide if we want to create RGs here or with deployment. decide on parallelism

    # Create network resources
    Write-PSFMessage -Level Host -Message "Invoking network resources."
    Invoke-AVDMFNetwork -ErrorAction Stop

    #Create storage resources
    Write-PSFMessage -Level Host -Message "Invoking Storage resources."
    Invoke-AVDMFStorage -ErrorAction Stop

    # Create Host Pools and Session Hosts
    Write-PSFMessage -Level Host -Message "Invoking Desktop Virtualization resources."
    Invoke-AVDMFDesktopVirtualization -ErrorAction Stop
}

function Set-AVDMFConfiguration {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = "Does not change any states")]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $ConfigurationPath,

        [string] $AzSubscriptionId = (Get-AzContext).Subscription.Id,
        [switch] $Force

    )

    #region: Initialize Variables
        $configurationVersion = '1.0.15'
    #endregion: Initialize Variables

    #region: Load Custom Environment Variables
    $environmentVariablesFilePath = Join-Path -Path $ConfigurationPath -ChildPath 'EnvironmentVariables.json'
    if (Test-Path -Path $environmentVariablesFilePath) {
        Write-Warning -Message "EnvironmentVariables.json file detected. This is not supposed to exist on DevOps. Please add it to .gitignore"

        $environmentVariables = Get-Content -Path $environmentVariablesFilePath | ConvertFrom-Json | ConvertTo-PSFHashtable
        $null = $environmentVariables.GetEnumerator() | ForEach-Object { New-Item -Path $_.Key -Value $_.Value -Force }
    }
    #endregion: Load Custom Environment Variables
    #region: Set DeploymentStage
    $script:DeploymentStage = $env:SYSTEM_STAGEDISPLAYNAME
    if ([string]::IsNullOrEmpty($DeploymentStage) -or [string]::IsNullOrWhiteSpace($DeploymentStage)) {
        throw "Deployment Stage is not defined, if running from local device create EnvironmentVariables.json file. Otherwise review environment variables."
        #TODO: Include environment variable name in error message.
    }

    #endregion: Set DeploymentStage

    #region: Register Name Mappings

    $nameMappingConfigPath = Join-Path -Path $ConfigurationPath -ChildPath "NameMappings"
    if (Test-Path $nameMappingConfigPath) {
        foreach ($file in Get-ChildItem -Path $nameMappingConfigPath -Filter "*.json") {
            foreach ($dataset in (Get-Content -Path $file.FullName | ConvertFrom-Json -ErrorAction Stop | ConvertTo-PSFHashtable )) {
                Register-AVDMFNameMapping @dataset
            }
        }
    }
    #endregion: Register Name Mappings

    #region: Populate Script Variables
    $script:AzSubscriptionId = $AzSubscriptionId

    #endregion: Populate Script Variables


    if ($script:WVDConfigurationLoaded -and -not $Force) { throw "Configuration is already loaded. Use the force to reload." }
    if ($Force) { & "$moduleRoot\internal\scripts\variables.ps1" }

    #region: General Configuration
    $generalConfiguration = Get-Content -Path (Join-Path -Path $ConfigurationPath -ChildPath '\GeneralConfiguration\GeneralConfiguration.json' -ErrorAction Stop ) | ConvertFrom-Json -ErrorAction Stop

    if($generalConfiguration.ConfigurationVersion -ne $configurationVersion) {
        throw "current configuration version $($generalConfiguration.ConfigurationVersion) must match $configurationVersion."
    }

    $script:Location = $GeneralConfiguration.Location
    $script:TimeZone = $generalConfiguration.TimeZone

    $Script:DomainJoinUserName = $generalConfiguration.DomainJoinCredential.SecretName
    $Script:DomainJoinPassword = Get-AzKeyVaultSecret -ResourceId $generalConfiguration.DomainJoinCredential.KeyVaultID -Name $generalConfiguration.DomainJoinCredential.SecretName -AsPlainText
    <#
    $script:DomainJoinCredential = @{
            reference = @{
                keyVault = @{ id = $generalConfiguration.DomainJoinCredential.KeyVaultID}
                secretName = $generalConfiguration.DomainJoinCredential.SecretName
            }
    }
    #>

    #endregion

    #region: Naming Conventions
    $namingConventionsRoot = Join-Path -Path $ConfigurationPath -ChildPath NamingConventions

    $script:NamingStyles = Get-Content -Path $namingConventionsRoot\NamingStyles.json -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop

    $namingConventionsComponentsRoot = Join-Path -Path $namingConventionsRoot -ChildPath "Components"

    foreach ($componentNC in (Get-ChildItem -Path $namingConventionsComponentsRoot -Filter "*.json")) {
        # We create a script variable for each component by adding 'NC' to the name of the file

        $NCContent = Get-Content -Path $componentNC.FullName -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
        New-Variable -Scope 'script' -Name ("{0}NC" -f $componentNC.BaseName) -Value $NCContent
    }

    #endregion: Naming Conventions

    #region: Define Registrable Components
    $components = [ordered] @{
        # Tags
        'GlobalTags'            = @{Command = (Get-Command -Name Register-AVDMFGlobalTag); ConfigurationPath = (Join-Path -Path $ConfigurationPath -ChildPath "GlobalTags") }
        # Network
        'AddressSpaces'         = @{Command = (Get-Command Register-AVDMFAddressSpace); ConfigurationPath = (Join-Path -Path $ConfigurationPath -ChildPath "Network\AddressSpaces") }
        'VirtualNetworks'       = @{Command = (Get-Command Register-AVDMFVirtualNetwork); ConfigurationPath = (Join-Path -Path $ConfigurationPath -ChildPath "Network\VirtualNetworks") }
        'RouteTables'           = @{Command = (Get-Command Register-AVDMFRouteTable); ConfigurationPath = (Join-Path -Path $ConfigurationPath -ChildPath "Network\RouteTables") }
        'NetworkSecurityGroups' = @{Command = (Get-Command Register-AVDMFNetworkSecurityGroup); ConfigurationPath = (Join-Path -Path $ConfigurationPath -ChildPath "Network\NetworkSecurityGroups") }
        # Storage
        'StorageAccounts'       = @{Command = (Get-Command Register-AVDMFStorageAccount); ConfigurationPath = (Join-Path -Path $ConfigurationPath -ChildPath "Storage\StorageAccounts") }
        # Desktop Virtualization
        'Workspaces'            = @{Command = (Get-Command Register-AVDMFWorkspace); ConfigurationPath = (Join-Path -Path $ConfigurationPath -ChildPath "DesktopVirtualization\Workspaces") }
        'VMTemplates'           = @{Command = (Get-Command Register-AVDMFVMTemplate); ConfigurationPath = (Join-Path -Path $ConfigurationPath -ChildPath "DesktopVirtualization\VMTemplates") }
        'HostPools'             = @{Command = (Get-Command Register-AVDMFHostPool); ConfigurationPath = (Join-Path -Path $ConfigurationPath -ChildPath "DesktopVirtualization\HostPools") }
    }
    #endregion: Define Registrable Components

    #region: Load Component Configuration
    foreach ($key in $components.Keys) {
        if (-not (Test-Path $components[$key].ConfigurationPath)) { continue }

        Write-PSFMessage -Level Verbose -Message "Loading configuration for $key"

        foreach ($file in Get-ChildItem -Path $components[$key].ConfigurationPath -Recurse -Filter "*.json") {
            Write-PSFMessage -Level Verbose -Message "`tLoading $key from $($file.FullName)"

            foreach ($dataset in (Get-Content -Path $file.FullName | ConvertFrom-Json -ErrorAction Stop | ConvertTo-PSFHashtable -Include $($components[$key].Command.Parameters.Keys))) {

                Write-PSFMessage -Level Verbose -Message "`t`tRegistering dataset:`r`n $($dataset | Format-List | Out-String -Width 120)"
                $dataset = Set-AVDMFNameMapping -Dataset $dataset
                $dataset = Set-AVDMFStageEntries -Dataset $dataset
                & $components[$key].Command @dataset -ErrorAction Stop
            }
        }
    }
    #endregion: Load Component Configuration

    #region: Add Tags
    $taggedResources = @(
        'ResourceGroup'
        'VirtualNetwork'
        'NetworkSecurityGroup'
        'RouteTable'
        'StorageAccount'
        'PrivateLink'
        'HostPool'
        'ApplicationGroup'
        'Workspace'
        'SessionHost'
    )
    foreach ($resourceType in $taggedResources) {
        #if($resourceType -eq 'RouteTable' ) {$BP="HERE"}
        $scriptVariable = Get-Variable -Scope script -Name "$($resourceType)s" -ValueOnly
        if (($script:GlobalTags.keys -contains $resourceType) -or ($script:GlobalTags.keys -contains 'All')) {
            $keys = [array] $scriptVariable.Keys
            foreach ($key in $keys) { $scriptVariable[$key] = Add-AVDMFTag -ResourceType $resourceType -ResourceObject $scriptVariable[$key] }
        }
    }

    #endregion: Add Tags

    $script:WVDConfigurationLoaded = $true
}

function Initialize-AVDMFDesktopVirtualization {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")]
    [CmdletBinding()]
    [OutputType('System.Collections.Hashtable')]

    param (
        [string] $ResourceGroupName,
        [string] $ResourceCategory
    )
    switch ($ResourceCategory) {
        'HostPool' {
            $filteredHostPools = @{}
            $script:HostPools.GetEnumerator() | Where-Object { $_.value.ResourceGroupName -eq $ResourceGroupName } | ForEach-Object { $filteredHostPools.Add($_.Key, $_.Value) }

            $filteredApplicationGroups = @{}
            $script:ApplicationGroups.GetEnumerator() | Where-Object { $_.value.ResourceGroupName -eq $ResourceGroupName } | ForEach-Object { $filteredApplicationGroups.Add($_.Key, $_.Value) }

            $filteredSessionHosts = @{}
            $script:SessionHosts.GetEnumerator() | Where-Object { $_.value.ResourceGroupName -eq $ResourceGroupName } | ForEach-Object { $filteredSessionHosts.Add($_.Key, $_.Value) }

            $templateParams = @{
                HostPools         = [array] ($filteredHostPools | Convert-HashtableToArray)
                ApplicationGroups = [array] ($filteredApplicationGroups | Convert-HashtableToArray)
                SessionHosts      = [array] ($filteredSessionHosts | Convert-HashtableToArray)
            }
        }
        'Workspace' {
            $filteredWorkspaces = @{}
            $script:Workspaces.GetEnumerator() | Where-Object { $_.value.ResourceGroupName -eq $ResourceGroupName } | ForEach-Object { $filteredWorkspaces.Add($_.Key, $_.Value) }
            $templateParams = @{
                Workspaces = [array] ($filteredWorkspaces | Convert-HashtableToArray)
            }
        }
    }
    $templateParams
}

function Invoke-AVDMFDesktopVirtualization {
    [CmdletBinding()]
    param (

    )

    #region: Initialize Variables

    $bicepWorkspaces = "$($moduleRoot)\internal\Bicep\DesktopVirtualization\Workspaces.bicep"
    $bicepHostPools = "$($moduleRoot)\internal\Bicep\DesktopVirtualization\HostPools.bicep"
    #endregion: Initialize Variables

    # Host Pools
    $hostPoolJobs = @()
    foreach ($rg in $script:ResourceGroups.Keys) {
        if ($script:ResourceGroups[$rg].ResourceCategory -eq 'HostPool') {
            $templateParams = Initialize-AVDMFDesktopVirtualization -ResourceGroupName $rg -ResourceCategory 'HostPool'

            try {
                $null = Get-AzResourceGroup -Name $rg -ErrorAction Stop
            }
            catch {
                New-AzResourceGroup -Name $rg -Location $script:Location
            }
            $hostPoolJobs += New-AzResourceGroupDeployment -ResourceGroupName $rg -Mode Complete -TemplateFile $bicepHostPools @templateParams -ErrorAction Stop -Confirm:$false -Force -AsJob

        }
        $dateTime = Get-Date
        while ($hostPoolJobs.State -contains "Running") {
            Start-Sleep -Seconds 5
            $timeSpan = New-TimeSpan -Start $dateTime -End (Get-Date)
            $count = ($hostPoolJobs | Where-Object { $_.State -eq "Running" }).count
            Write-PSFMessage -Level Host -Message "Waiting for $count hostpool deployments to complete - Been waiting for $($timeSpan.ToString())"
        }
        Write-PSFMessage -Level Host -Message "Hostpool jobs completed. See output below."
        $hostPoolJobs | Receive-Job

        #region: Update SessionDesktop name
        #TODO: Check if there is a put method yet for 'Microsoft.DesktopVirtualization/applicationgroups/desktops'
            foreach ($item in $script:ApplicationGroups.GetEnumerator()){
                Write-PSFMessage -Level Verbose -Message 'Updating SessionDesktop Friendly Name'
                $null = Update-AzWvdDesktop -ResourceGroupName $item.Value.ResourceGroupName -ApplicationGroupName $item.Key -Name 'SessionDesktop' -FriendlyName $item.Value.FriendlyName -ErrorAction Stop
            }
        #endregion
    }

    # Workspaces
    Write-PSFMessage -Level Host -Message "Creating workspaces"
    foreach ($rg in $script:ResourceGroups.Keys) {
        if ($script:ResourceGroups[$rg].ResourceCategory -eq 'Workspace') {
            $templateParams = Initialize-AVDMFDesktopVirtualization -ResourceGroupName $rg -ResourceCategory 'Workspace'

            try {
                $null = Get-AzResourceGroup -Name $rg -ErrorAction Stop
            }
            catch {
                New-AzResourceGroup -Name $rg -Location $script:Location
            }
            New-AzResourceGroupDeployment -ResourceGroupName $rg -Mode Complete -TemplateFile $bicepWorkspaces @templateParams -ErrorAction Stop -Confirm:$false -Force
        }
    }

}

function Test-AVDMFDesktopVirtualization {
    [CmdletBinding()]
    param (

    )

    #region: Initialize Variables
    $bicepHostPools = "$($moduleRoot)\internal\Bicep\DesktopVirtualization\HostPools.bicep"
    $bicepWorkspaces = "$($moduleRoot)\internal\Bicep\DesktopVirtualization\Workspaces.bicep"
    #endregion: Initialize Variables

    # Host Pools
    foreach ($rg in $script:ResourceGroups.Keys) {
        if ($script:ResourceGroups[$rg].ResourceCategory -eq 'HostPool') {
            $templateParams = Initialize-AVDMFDesktopVirtualization -ResourceGroupName $rg -ResourceCategory 'HostPool'

            New-AzResourceGroupDeployment -ResourceGroupName $rg -Mode Complete -TemplateFile $bicepHostPools @templateParams -ErrorAction Stop -WhatIf
        }
    }
    # Workspaces
    foreach ($rg in $script:ResourceGroups.Keys) {
        if ($script:ResourceGroups[$rg].ResourceCategory -eq 'Workspace') {
            $templateParams = Initialize-AVDMFDesktopVirtualization -ResourceGroupName $rg -ResourceCategory 'Workspace'

            New-AzResourceGroupDeployment -ResourceGroupName $rg -Mode Complete -TemplateFile $bicepWorkspaces @templateParams -ErrorAction Stop -WhatIf
        }
    }
}

function Get-AVDMFApplicationGroup {
    $script:ApplicationGroups
}

function Register-AVDMFApplicationGroup {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $HostPoolName,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $HostPoolResourceId,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $ResourceGroupName,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $FriendlyName,

        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [string[]] $Users,

        [PSCustomObject] $Tags = [PSCustomObject]@{}
    )
    process {
        $ResourceName = New-AVDMFResourceName -ResourceType 'ApplicationGroup' -ParentName $HostPoolName

        $resourceID = "/Subscriptions/$script:AzSubscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.DesktopVirtualization/applicationgroups/$ResourceName"

        $principalId = @()
        if ($Users.count -ge 1) {
            $principalId += foreach ($user in $Users) {
                try {
                    if ($user -like "*@*" ) {
                        Write-PSFMessage -Level Verbose -Message "Resolving Id for user: $user"
                        $id = (Get-AzADUser -UserPrincipalName $user -ErrorAction Stop).Id
                    }
                    else {
                        Write-PSFMessage -Level Verbose -Message "Resolving Id for group: $user"
                        $id = (Get-AzADGroup -DisplayName $user -ErrorAction Stop).Id
                    }
                    if ($null -eq $id) {
                        throw
                    }
                    $id
                }
                catch {
                    throw "Could not resolve id for $user - If the name is correct then ensure the service principal used is assigned 'Directory readers' role."
                }
            }
        }

        $script:ApplicationGroups[$ResourceName] = [PSCustomObject]@{
            PSTypeName        = 'AVDMF.DesktopVirtualization.ApplicationGroup'
            ResourceGroupName = $ResourceGroupName
            HostPoolId        = $HostPoolResourceId
            FriendlyName      = $FriendlyName
            PrincipalId       = $principalId
            Tags              = $Tags
        }

        # Link Application group to workspace
        $script:Workspaces.GetEnumerator() | Where-Object { $_.value.ReferenceName -eq $script:hostpools.$hostpoolname.WorkspaceReference } | ForEach-Object { $_.value.ApplicationGroupReferences += $resourceID }

    }
}



function Get-AVDMFHostPool {
    $script:HostPools
}

function Register-AVDMFHostPool {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $AccessLevel,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $PoolType,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [int] $MaxSessionLimit,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [int] $NumberOfSessionHosts,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $FriendlyName,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $WorkSpaceReference,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $VirtualNetworkReference,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $SubnetNSG,

        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [string] $SubnetRouteTable,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $StorageAccountReference,

        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [string[]] $Users,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $VMTemplate,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $OrganizationalUnitDN,

        [PSCustomObject] $Tags = [PSCustomObject]@{}
    )
    process {
        $ResourceName = New-AVDMFResourceName -ResourceType 'HostPool' -AccessLevel $AccessLevel -HostPoolType $PoolType
        $resourceGroupName = New-AVDMFResourceName -ResourceType "ResourceGroup" -ResourceCategory 'HostPool' -AccessLevel $AccessLevel -HostPoolType $PoolType
        Register-AVDMFResourceGroup -Name $resourceGroupName -ResourceCategory 'HostPool'

        $resourceID = "/Subscriptions/$script:AzSubscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.DesktopVirtualization/hostpools/$ResourceName"

        #Register Subnet
        $subnetParams = @{
            Scope              = $AccessLevel + 'Access'  #TODO: Change the parameter name from scope to Access Level, also change it in subnet configurations
            NamePrefix         = $resourceName
            VirtualNetworkName = $script:VirtualNetworks[$VirtualNetworkReference].ResourceName
            VirtualNetworkID   = $script:VirtualNetworks[$VirtualNetworkReference].ResourceID
            NSGID              = $script:NetworkSecurityGroups[$SubnetNSG].ResourceID
            RouteTableID       = $script:RouteTables[$SubnetRouteTable].ResourceID
        }

        $subnetID = Register-AVDMFSubnet @subnetParams -PassThru

        # Pickup Storage Account
        #TODO: Change Storage Accounts into HashTables
        $StorageAccountRef = $StorageAccountReference #There is a bug in Script Analyzer that causes the parameter to report unused.
        $storageAccount = $script:StorageAccounts[$StorageAccountRef]
        Register-AVDMFFileShare -Name $resourceName.ToLower() -StorageAccountName $storageAccount.Name -ResourceGroupName $storageAccount.ResourceGroupName

        $script:HostPools[$ResourceName] = [PSCustomObject]@{
            PSTypeName           = 'AVDMF.DesktopVirtualization.HostPool'
            ResourceGroupName    = $resourceGroupName
            ResourceID           = $resourceID

            PoolType             = $PoolType
            MaxSessionLimit      = $MaxSessionLimit
            NumberOfSessionHosts = $NumberOfSessionHosts

            WorkSpaceReference   = $WorkSpaceReference

            SubnetID             = $subnetID

            VMTemplate           = $VMTemplate

            Tags                 = $Tags

        }

        #TODO: Change this into splatting and check if users are provided.
        Register-AVDMFApplicationGroup -HostPoolName $resourceName -ResourceGroupName $resourceGroupName -HostPoolResourceId $resourceID -Tags $Tags -Users $Users -FriendlyName $FriendlyName

        # Register Session Host
        $hostPoolInstance = $ResourceName.Substring($ResourceName.Length - 2, 2)

        $domainName = ($OrganizationalUnitDN -split "," | Where-Object { $_ -like "DC=*" } | ForEach-Object { $_.replace("DC=", "") }) -join "."

        for ($i = 1; $i -le $NumberOfSessionHosts; $i++) {
            #TODO: Change all parameters to use splatting
            $SessionHostParams = @{
                subnetID   = $subnetID
                DomainName = $domainName
                OUPath     = $OrganizationalUnitDN
            }
            Register-AVDMFSessionHost -ResourceGroupName $resourceGroupName -AccessLevel $AccessLevel -HostPoolType $PoolType -HostPoolInstance $hostPoolInstance -InstanceNumber $i -VMTemplate $script:VMTemplates[$VMTemplate] @SessionHostParams -Tags $Tags
        }
    }
}



function Get-AVDMFSessionHost {
    $script:SessionHosts
}

function Register-AVDMFSessionHost {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $AccessLevel,
        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $HostPoolType,
        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $HostPoolInstance,
        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $ResourceGroupName,
        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [int] $InstanceNumber,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [object] $VMTemplate,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [object] $SubnetID,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $DomainName,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $OUPath,

        [PSCustomObject] $Tags = [PSCustomObject]@{}

    )
    process {
        $ResourceName = New-AVDMFResourceName -ResourceType 'VirtualMachine' -AccessLevel $AccessLevel -HostPoolType $HostPoolType -HostPoolInstance $HostPoolInstance -InstanceNumber $InstanceNumber

        $script:SessionHosts[$resourceName] = [PSCustomObject]@{
            ResourceGroupName = $ResourceGroupName
            VMSize            = $VMTemplate.VMSize
            TimeZone          = $script:TimeZone
            SubnetID          = $SubnetID
            AdminUsername     = $VMTemplate.AdminUserName
            AdminPassword     = $VMTemplate.AdminPassword
            ImageReference    = $VMTemplate.ImageReference
            Tags = $Tags

            # Add Session Host
            WVDArtifactsURL   = $VMTemplate.WVDArtifactsURL

            # Domain Join
            DomainName = $DomainName
            OUPath = $OUPath
            DomainJoinUserName = $script:DomainJoinUserName
            DomainJoinPassword = $script:DomainJoinPassword
        }
    }
}



function Get-AVDMFVMTemplate {
    $script:VMTemplates
}

function Register-AVDMFVMTemplate {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $ReferenceName,
        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $AdminUsername,
        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $VMSize,
        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [Object] $ImageReference,
        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $WVDArtifactsURL
    )
    process {
        $script:VMTemplates[$ReferenceName] = @{
            AdminUserName   = $AdminUsername
            AdminPassword   = Get-RandomPassword
            VMSize          = $VMSize
            ImageReference  = $ImageReference | ConvertTo-PSFHashtable
            WVDArtifactsURL = $WVDArtifactsURL
        }
    }
}



function Get-AVDMFWorkspace {
    $script:Workspaces
}

function Register-AVDMFWorkspace {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $AccessLevel,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $HostPoolType,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $ReferenceName,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $FriendlyName,

        [PSCustomObject] $Tags = [PSCustomObject]@{}
    )
    process {
        $ResourceName = New-AVDMFResourceName -ResourceType 'Workspace' -AccessLevel $AccessLevel -HostPoolType $HostPoolType
        $resourceGroupName = New-AVDMFResourceName -ResourceType "ResourceGroup" -ResourceCategory 'Workspace' -AccessLevel $AccessLevel -HostPoolType $HostPoolType
        Register-AVDMFResourceGroup -Name $resourceGroupName -ResourceCategory 'Workspace'

        $resourceID = "/Subscriptions/$script:AzSubscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.DesktopVirtualization/workspaces/$ResourceName"

        $script:Workspaces[$ResourceName] = [PSCustomObject]@{
            PSTypeName                 = 'AVDMF.DesktopVirtualization.Workspace'
            ResourceID                 = $resourceID
            ReferenceName              = $ReferenceName
            ResourceGroupName          = $resourceGroupName
            FriendlyName               = $FriendlyName
            ApplicationGroupReferences = @()
            Tags                       = $Tags
        }
    }
}



function Register-AVDMFGlobalSettings {
    param (
        # Stage
        [string]
        $Stage
    )
    $script:AVDMFGlobalSettings = [PSCustomObject]@{
        Stage = $Stage
    }
}

function Add-AVDMFTag {
    <#
    .SYNOPSIS
        Adds tags to resources
    #>

    [CmdletBinding()]
    param (
        # ResourceType
        [Parameter(Mandatory = $true)]
        [string] $ResourceType,

        # Resource Object
        [Parameter(Mandatory = $true)]
        $ResourceObject
    )
    # Tags that apply to all resources
    if($script:GlobalTags['All']){
        $effectiveTags = $script:GlobalTags['All'].Clone()
    }

    # Tags that apply to all instaces of a specific resource type
    if($script:GlobalTags[$ResourceType]){
        $resourceTypeTags = $script:GlobalTags[$ResourceType]
        foreach($item in $resourceTypeTags.GetEnumerator()) {$effectiveTags[$item.Key] = $item.Value}
    }

    if($ResourceObject.Tags){
        $resourceSpecificTags = $ResourceObject.Tags | ConvertTo-PSFHashtable
        foreach($item in $resourceSpecificTags.GetEnumerator()) {$effectiveTags[$item.Key] = $item.Value}
    }



    if ($effectiveTags) {
        $ResourceObject | Add-Member -MemberType NoteProperty -Name Tags -Value $effectiveTags -Force
    }

    $ResourceObject
}

function Get-AVDMFGlobalTag {
    $script:GlobalTags
}

function Register-AVDMFGlobalTag {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string] $ResourceType,
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [PSCustomObject] $Tags
    )
    process {
        $Tags = $Tags | ConvertTo-PSFHashtable
        $script:GlobalTags[$ResourceType] = $Tags
    }
}

function Get-AVDMFNameMapping {
    $script:NameMappings
}

function Register-AVDMFNameMapping {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $Name,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $VariableName,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [Object] $Value


    )
    process {
        if($VariableName -like "$*"){
            throw "Variable names in Name Mapping cannot start with '$'"
        }
        $variableNameValue = if($VariableName -like "env:*"){
            (Get-Item -Path $VariableName).Value
        }
        else{
            (Get-Variable -Name $VariableName).Value
        }
        $script:NameMappings[$Name] = $Value.$variableNameValue
    }
}

function Initialize-AVDMFNetwork {
    [CmdletBinding()]
    [OutputType('System.Collections.Hashtable')]

    param (
        [string] $ResourceGroupName
    )

    $filteredVirtualNetworks = @{}
    $filteredSubnets = @{}
    $filteredNetworkSecurityGroups = @{}
    $filteredRouteTables = @{}


    $script:VirtualNetworks.GetEnumerator() | Where-Object { $_.value.ResourceGroupName -eq $ResourceGroupName } -PipelineVariable vNet | ForEach-Object {
        $filteredVirtualNetworks.Add($vNet.Key, $vNet.Value)
        $script:Subnets.GetEnumerator() | Where-Object { $_.value.VirtualNetworkName -eq $vNet.Value.ResourceName } | ForEach-Object { $filteredSubnets.Add($_.Key, $_.Value) }
    }

    $script:RouteTables.GetEnumerator() | Where-Object { $_.value.ResourceGroupName -eq $ResourceGroupName } -PipelineVariable routeTable | ForEach-Object {
        $filteredRouteTables.Add($routeTable.Key, $routeTable.Value)
    }

    foreach ($nsg in $script:NetworkSecurityGroups.keys) {
        if ($script:NetworkSecurityGroups[$nsg].ResourceGroupName -eq $ResourceGroupName) {
            $filteredNetworkSecurityGroups[$nsg] = $script:NetworkSecurityGroups[$nsg]
        }
    }

    $templateParams = @{

        VirtualNetworks       = [array] ($filteredVirtualNetworks | Convert-HashtableToArray)
        Subnets               = [array] ($filteredSubnets | Convert-HashtableToArray)
        NetworkSecurityGroups = [array] ($filteredNetworkSecurityGroups | Convert-HashtableToArray)
        RouteTables           = [array] ($filteredRouteTables | Convert-HashtableToArray)

    }
    $templateParams

}

function Initialize-AVDMFRemotePeering {
    [CmdletBinding()]
    [OutputType('System.Collections.Hashtable')]

    $templateParams = @{
        RemotePeerings = [array] ($script:RemotePeerings | Convert-HashtableToArray)
    }
    $templateParams

}

function Invoke-AVDMFNetwork {
    [CmdletBinding()]
    param (

    )

    #Initialize Variables
    $bicepVirtualNetwork = "$($moduleRoot)\internal\Bicep\Network\Network.bicep"


    foreach ($rg in $script:ResourceGroups.Keys) {
        if ($script:ResourceGroups[$rg].ResourceCategory -eq 'Network') {
            $templateParams = Initialize-AVDMFNetwork -ResourceGroupName $rg
            $BP = 'HERE'
            try{
                $null = Get-AzResourceGroup -Name $rg -ErrorAction Stop
            }
            catch{
                New-AzResourceGroup -Name $rg -Location $script:Location
            }

            New-AzResourceGroupDeployment -ResourceGroupName $rg -Mode Complete -TemplateFile $bicepVirtualNetwork @templateParams -ErrorAction Stop -Confirm:$false -Force
        }
    }

    # Create remote peerings
    if($script:RemotePeerings.count){
        $bicepRemotePeerings = "$($moduleRoot)\internal\Bicep\Network\RemotePeerings.bicep"
        $templateParams =  Initialize-AVDMFRemotePeering
        New-AzResourceGroupDeployment -ResourceGroupName $rg -TemplateFile $bicepRemotePeerings @templateParams -ErrorAction Stop -Confirm:$false -Force
    }


}

function Test-AVDMFNetwork {
    [CmdletBinding()]
    param (

    )

    #region: Initialize Variables
    $bicepVirtualNetwork = "$($moduleRoot)\internal\Bicep\Network\Network.bicep"
    #endregion: Initialize Variables

    foreach ($rg in $script:ResourceGroups.Keys) {

        if ($script:ResourceGroups[$rg].ResourceCategory -eq 'Network') {
            $templateParams = Initialize-AVDMFNetwork -ResourceGroupName $rg
            New-AzResourceGroupDeployment -ResourceGroupName $rg -Mode Complete -TemplateFile $bicepVirtualNetwork @templateParams -ErrorAction Stop -WhatIf
        }
    }
}

function Get-AVDMFAddressSpace {
    $script:AddressSpaces
}

function Register-AVDMFAddressSpace {
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string] $Scope,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string] $AddressSpace,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [int] $SubnetMask
    )
    process {
        $Script:AddressSpaces += [PSCustomObject]@{
            Scope        = $Scope
            AddressSpace = $AddressSpace
            subnetMask   = $SubnetMask
        }
    }
}



function Get-AVDMFNetworkSecurityGroup {
    $script:NetworkSecurityGroups
}

function Register-AVDMFNetworkSecurityGroup {
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string] $ReferenceName,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [array] $SecurityRules,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string] $AccessLevel,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string] $HostPoolType,

        [PSCustomObject] $Tags = [PSCustomObject]@{}
    )
    process {
        $resourceName = New-AVDMFResourceName -ResourceType 'NetworkSecurityGroup' -AccessLevel $AccessLevel -HostPoolType $HostPoolType

        #Register Resource Group if needed
        $resourceGroupName = New-AVDMFResourceName -ResourceType "ResourceGroup" -ResourceCategory 'Network' -AccessLevel $AccessLevel -HostPoolType $HostPoolType -InstanceNumber 1
        # At the moment we do not have a reason for multiple network RGs.
        Register-AVDMFResourceGroup -Name $resourceGroupName -ResourceCategory 'Network'

        $resourceID = "/Subscriptions/$script:AzSubscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Network/networkSecurityGroups/$resourceName"


        $script:NetworkSecurityGroups[$ReferenceName] = [PSCustomObject]@{
            PSTypeName        = 'AVDMF.Network.NetworkSecurityGroup'
            ResourceName      = $resourceName
            ResourceGroupName = $resourceGroupName
            ResourceID        = $resourceID
            ReferenceName     = $ReferenceName
            SecurityRules     = @($SecurityRules | ForEach-Object { $_ | ConvertTo-PSFHashtable })
            Tags = $Tags
        }
    }
}



function Get-AVDMFRemotePeering {
    $script:RemotePeerings
}

function Register-AVDMFRemotePeering {
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string] $RemoteVNetResourceID,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string] $LocalVNetResourceId
    )
    process {

        $remoteVNet = Get-AVDMFResourceInfo -ResourceId $RemoteVNetResourceID
        $localVNet = Get-AVDMFResourceInfo -ResourceId $LocalVNetResourceId

        $referenceName = "Peering_{0}_To_{1}" -f $RemoteVNet.ResourceName, $LocalVNet.ResourceName #this is used for the hashtable.
        $name = "Peering_To_{0}" -f $LocalVNet.ResourceName


        $script:RemotePeerings[$referenceName] = [PSCustomObject]@{
            PSTypeName          = 'AVDMF.Network.RemotePeering'
            Name                = $name
            SubscriptionId      = $remoteVNet.SubscriptionId #TODO: Implement Remote Subscription Support.
            ResourceGroupName   = $remoteVNet.ResourceGroupName
            RemoteVNetName      = $remoteVNet.ResourceName
            LocalVNetResourceId = $LocalVNetResourceId
        }
    }

}

function Get-AVDMFRouteTable {
    $script:RouteTables
}

function Register-AVDMFRouteTable {
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string] $ReferenceName,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [array] $Routes,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [Boolean] $DisableBgpRoutePropagation,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string] $AccessLevel,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string] $HostPoolType,

        [PSCustomObject] $Tags = [PSCustomObject]@{}
    )
    process {
        $resourceName = New-AVDMFResourceName -ResourceType 'RouteTable' -AccessLevel $AccessLevel -HostPoolType $HostPoolType

        #Register Resource Group if needed
        $resourceGroupName = New-AVDMFResourceName -ResourceType "ResourceGroup" -ResourceCategory 'Network' -AccessLevel $AccessLevel -HostPoolType $HostPoolType -InstanceNumber 1
        # At the moment we do not have a reason for multiple network RGs.
        Register-AVDMFResourceGroup -Name $resourceGroupName -ResourceCategory 'Network'

        $resourceID = "/Subscriptions/$script:AzSubscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Network/routeTables/$resourceName"

        $routesHashTable = @($Routes | ForEach-Object { $_ | ConvertTo-PSFHashtable })
        foreach ($item in $routesHashTable) {
            $item.properties = $item.properties | ConvertTo-PSFHashtable
        }

        $script:RouteTables[$ReferenceName] = [PSCustomObject]@{
            PSTypeName                 = 'AVDMF.Network.RouteTable'
            ResourceName               = $resourceName
            ResourceGroupName          = $resourceGroupName
            ResourceID                 = $resourceID
            ReferenceName              = $ReferenceName
            Routes                     = $routesHashTable #@($Routes | ForEach-Object { $_ | ConvertTo-PSFHashtable })
            DisableBgpRoutePropagation = $DisableBgpRoutePropagation
            Tags                       = $Tags
        }
    }
}

function Get-AVDMFSubnet {
    $script:Subnets
}

function Register-AVDMFSubnet {
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string] $Scope,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string] $NamePrefix,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string] $VirtualNetworkName,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string] $VirtualNetworkID,

        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        [bool] $PrivateLink = $false,

        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        [string] $NSGID ,

        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        [string] $RouteTableID ,

        [switch] $PassThru

    )

    process {
        #region: Calculate subnet range and prefix
        [array] $scope = ($Script:AddressSpaces | Where-Object { $_.Scope -eq $Scope })
        if ($scope.count -gt 1) { throw "Found multiple scopes, please review address spaces configuration and avoid duplicates." }

        [string] $addressSpace = $scope.AddressSpace
        [int] $subnetMask = $scope.SubnetMask
        Write-Verbose "Will use the address space $addressSpace and subnet mask $subnetMask"

        if (-not ($addressSpace -match '^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\/\d{2}$')) {
            throw "$addressSpace is not a valid address space"
        }

        $addressPrefix = (New-AVDMFSubnetRange -AddressSpace $addressSpace -NewSubnetMaskBits $subnetMask -ErrorAction 'Stop').AddressPrefix
        #endregion: Calculate subnet range and prefix

        $resourceName = New-AVDMFResourceName -ResourceType 'Subnet' -ParentName $NamePrefix -AddressPrefix $addressPrefix
        $resourceID = "$VirtualNetworkID/subnets/$resourceName"

        #Build Subnet properties
        $properties = @{
            addressPrefix                  = $addressPrefix
            privateEndpointNetworkPolicies = if ($PrivateLink) { "Disabled" } else { "Enabled" }
        }
        if ($NSGID) {
            $properties['networkSecurityGroup'] = @{id = $NSGID }
        }
        if ($RouteTableID) {
            $properties['routeTable'] = @{id = $RouteTableID }
        }

        $script:Subnets[$resourceName] = [PSCustomObject]@{
            PSTypeName         = 'AVDMF.Network.Subnet'
            VirtualNetworkName = $VirtualNetworkName
            ResourceID         = $resourceID
            Properties         = $properties
        }

        if ($PassThru) { $resourceID }
    }

}



function Get-AVDMFVirtualNetwork {
    $script:VirtualNetworks
}

function Register-AVDMFVirtualNetwork {
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string] $ReferenceName,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string[]] $DNSServers,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [array] $DefaultSubnets,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [array] $VirtualNetworkPeerings,

        [string] $AccessLevel = 'All',
        [string] $HostPoolType = 'All',

        [PSCustomObject] $Tags = [PSCustomObject]@{}
    )
    process {
        $resourceName = New-AVDMFResourceName -ResourceType 'VirtualNetwork' -AccessLevel $AccessLevel -HostPoolType $HostPoolType

        #Register Resource Group if needed
        $resourceGroupName = New-AVDMFResourceName -ResourceType "ResourceGroup" -ResourceCategory 'Network' -AccessLevel $AccessLevel -HostPoolType $HostPoolType -InstanceNumber 1
        Register-AVDMFResourceGroup -Name $resourceGroupName -ResourceCategory 'Network'
        # At the moment we do not have a reason for multiple network RGs.

        $resourceID = "/Subscriptions/$script:AzSubscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Network/VirtualNetworks/$resourceName"

        #Register Virtual Networks
        [string]$addressSpace = ($Script:AddressSpaces | Where-Object Scope -EQ 'VirtualNetwork').AddressSpace

        if (-not ($addressSpace -match '^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\/\d{2}$')) {
            throw "$addressSpace is not a valid address space"
        }

        # Configure Peerings
        $peerings = @(foreach($peering in $VirtualNetworkPeerings){
            $RemoteNetworkName = ($peering.RemoteVnetId -split "/")[-1]
            Write-PSFMessage -Level Verbose -Message "Configuring peering with $RemoteNetworkName"
            @{
                Name = "PeeringTo_$RemoteNetworkName"
                RemoteNetworkID = $peering.RemoteVnetId
                UseRemoteGateways = [bool] $peering.useRemoteGateways
            }
            if($peering.CreateRemotePeering){
                Write-PSFMessage -Level Verbose -Message "Registering remote peering."
                Register-AVDMFRemotePeering -RemoteVNetResourceID $peering.RemoteVNetId -LocalVNetResourceId $resourceID
            }
            else {
                Write-PSFMessage -Level Warning -Message "Peering of Virtual Network '$ReferenceName ($resourceName)' to '$RemoteNetworkName' is not configured to create remote peering. You must manually create peering in the remote network." # Add link to help on website.
            }
        })



        $script:VirtualNetworks[$ReferenceName] = [PSCustomObject]@{
            PSTypeName             = 'AVDMF.Network.VirtualNetwork'
            ResourceName           = $resourceName
            ResourceGroupName      = $resourceGroupName
            ResourceID             = $resourceID
            AddressSpace           = $addressSpace
            DNSServers             = $DNSServers
            VirtualNetworkPeerings = $peerings
            Tags = $Tags
        }

        #Register Default Subnets
        foreach ($subnet in $DefaultSubnets) {
            $subnet | Register-AVDMFSubnet -VirtualNetworkName $resourceName -VirtualNetworkID $resourceID -ErrorAction Stop
            #TODO Utilize value from pipeline of subnet object
        }
    }

}



function Get-AVDMFResourceGroup {
    $script:ResourceGroups
}



function Register-AVDMFResourceGroup {
    [CmdletBinding()]
    param (
        [string] $Name,
        [string] $ResourceCategory
    )

    $script:ResourceGroups[$Name] = [PSCustomObject]@{
        PSTypeName  = 'AVDMF.ResourceGroup'
        ResourceCategory = $ResourceCategory
    }

}





function Initialize-AVDMFStorage {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")]
    [CmdletBinding()]
    [OutputType('System.Collections.Hashtable')]

    param (
        [string] $ResourceGroupName
    )

    $filteredStorageAccounts = @{}
    $filteredPrivateLinks = @{}
    $filteredFileShares = @{}

    $script:StorageAccounts.GetEnumerator() | Where-Object { $_.value.ResourceGroupName -eq $ResourceGroupName } | ForEach-Object { $filteredStorageAccounts.Add($_.Key, $_.Value) }

    $script:PrivateLinks.GetEnumerator() | Where-Object { $_.value.ResourceGroupName -eq $ResourceGroupName } | ForEach-Object { $filteredPrivateLinks.Add($_.Key, $_.Value) }

    $script:FileShares.GetEnumerator() | Where-Object { $_.value.ResourceGroupName -eq $ResourceGroupName } | ForEach-Object {$filteredFileShares.Add($_.Key, $_.Value)}


    $templateParams = @{
        StorageAccounts = [array] ($filteredStorageAccounts | Convert-HashtableToArray)
        PrivateLinks = [array] ($filteredPrivateLinks | Convert-HashtableToArray)
        FileShares = [array] ($filteredFileShares | Convert-HashtableToArray)
    }
    $templateParams

}

function Invoke-AVDMFStorage {
    [CmdletBinding()]
    param (

    )
    #region: Initialize Variables
    $bicepStorage = "$($moduleRoot)\internal\Bicep\Storage\Storage.bicep"
    #endregion: Initialize Variables

    foreach ($rg in $script:ResourceGroups.Keys) {
        if ($script:ResourceGroups[$rg].ResourceCategory -eq 'Storage') {
            $templateParams = Initialize-AVDMFStorage -ResourceGroupName $rg

            try{
                Get-AzResourceGroup -Name $rg -ErrorAction Stop | Out-Null
            }
            catch{
                New-AzResourceGroup -Name $rg -Location $script:Location
            }

            New-AzResourceGroupDeployment -ResourceGroupName $rg -Mode Incremental -TemplateFile $bicepStorage @templateParams -ErrorAction Stop -Confirm:$false -Force
            # Cannot use Complete mode with Private links, see: https://feedback.azure.com/forums/217313-networking/suggestions/40395946-private-endpoint-arm-template-deployment-fix-comp

        }
    }
}

function Test-AVDMFStorage {
    [CmdletBinding()]
    param (

    )
    #region: Initialize Variables
    $bicepStorage = "$($moduleRoot)\internal\Bicep\Storage\Storage.bicep"
    #endregion: Initialize Variables

    foreach ($rg in $script:ResourceGroups.Keys) {
        if ($script:ResourceGroups[$rg].ResourceCategory -eq 'Storage') {
            $templateParams = Initialize-AVDMFStorage -ResourceGroupName $rg
            try{
                Get-AzResourceGroup -Name $rg -ErrorAction Stop | Out-Null
            }
            catch{
                Write-Warning -Message "Resourcegroup $rg does not exist. Skipping test for: `r`n$($templateParams.Values.ResourceID | out-string)"
                continue
            }
            New-AzResourceGroupDeployment -ResourceGroupName $rg -Mode Complete -TemplateFile $bicepStorage @templateParams -ErrorAction Stop -WhatIf
        }
    }
}

function Get-AVDMFFileShare {
    $script:FileShares
}

function Register-AVDMFFileShare {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string] $Name,
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string] $StorageAccountName,
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string] $ResourceGroupName
    )
    process {
        $script:FileShares[$Name] = [PSCustomObject]@{
            PSTypeName         = 'AVDMF.Storage.FileShare'
            ResourceName       = $Name
            ResourceGroupName  = $resourceGroupName
            StorageAccountName = $StorageAccountName
        }
    }
}



function Get-AVDMFPrivateLink {
    $script:PrivateLinks
}

function Register-AVDMFPrivateLink {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $ResourceGroupName,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $StorageAccountName,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $StorageAccountID,

        [PSCustomObject] $Tags = [PSCustomObject]@{}

    )
    process {
        $SubnetId = $script:subnets[($script:subnets.keys | Where-Object { $_ -like 'PrivateLinks*' })].ResourceId

        $resourceName = New-AVDMFResourceName -ResourceType 'PrivateLink' -ParentName $StorageAccountName

        $resourceID = "/Subscriptions/$script:AzSubscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Network/privateEndpoints/$ResourceName"

        $script:PrivateLinks[$resourceName]= [PSCustomObject]@{
            PSTypeName        = 'AVDMF.Storage.PrivateLink'
            ResourceGroupName = $resourceGroupName
            ResourceID        = $resourceID

            StorageAccountID  = $StorageAccountID
            SubnetID          = $SubnetId

            Tags = $Tags

        }
    }
}



function Get-AVDMFStorageAccount {
    $script:StorageAccounts
}

function Register-AVDMFStorageAccount {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $AccessLevel,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $accountType,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $HostPoolType,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $Kind,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $ReferenceName,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [int] $shareSoftDeleteRetentionDays,

        [PSCustomObject] $Tags = [PSCustomObject]@{}
    )
    process {
        $ResourceName = New-AVDMFResourceName -ResourceType 'StorageAccount' -AccessLevel $AccessLevel -HostPoolType $HostPoolType
        $resourceGroupName = New-AVDMFResourceName -ResourceType "ResourceGroup" -ResourceCategory 'Storage' -AccessLevel 'All' -HostPoolType 'All' -InstanceNumber 1
        Register-AVDMFResourceGroup -Name $resourceGroupName -ResourceCategory 'Storage'
        # At the moment we do not have a reason for multiple storage RGs.

        $resourceID = "/Subscriptions/$script:AzSubscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Storage/storageAccounts/$ResourceName"

        $script:StorageAccounts[$ReferenceName] = [PSCustomObject]@{
            PSTypeName        = 'AVDMF.Storage.StorageAccount'
            ResourceGroupName = $resourceGroupName
            ResourceID        = $resourceID
            Name              = $ResourceName
            ReferenceName     = $ReferenceName
            AccountType       = $accountType
            Kind              = $Kind
            SoftDeleteDays    = $ShareSoftDeleteRetentionDays
            Tags              = $Tags
        }

        #register Private Link
        Register-AVDMFPrivateLink -ResourceGroupName $resourceGroupName -StorageAccountName $ResourceName -StorageAccountID $resourceID
    }
}



<#
This is an example configuration file
 
By default, it is enough to have a single one of them,
however if you have enough configuration settings to justify having multiple copies of it,
feel totally free to split them into multiple files.
#>


<#
# Example Configuration
Set-PSFConfig -Module 'AVDManagementFramework' -Name 'Example.Setting' -Value 10 -Initialize -Validation 'integer' -Handler { } -Description "Example configuration setting. Your module can then use the setting using 'Get-PSFConfigValue'"
#>


Set-PSFConfig -Module 'AVDManagementFramework' -Name 'Import.DoDotSource' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be dotsourced on import. By default, the files of this module are read as string value and invoked, which is faster but worse on debugging."
Set-PSFConfig -Module 'AVDManagementFramework' -Name 'Import.IndividualFiles' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be imported individually. During the module build, all module code is compiled into few files, which are imported instead by default. Loading the compiled versions is faster, using the individual files is easier for debugging and testing out adjustments."

<#
Stored scriptblocks are available in [PsfValidateScript()] attributes.
This makes it easier to centrally provide the same scriptblock multiple times,
without having to maintain it in separate locations.
 
It also prevents lengthy validation scriptblocks from making your parameter block
hard to read.
 
Set-PSFScriptblock -Name 'AVDManagementFramework.ScriptBlockName' -Scriptblock {
 
}
#>


<#
# Example:
Register-PSFTeppScriptblock -Name "AVDManagementFramework.alcohol" -ScriptBlock { 'Beer','Mead','Whiskey','Wine','Vodka','Rum (3y)', 'Rum (5y)', 'Rum (7y)' }
#>


<#
# Example:
Register-PSFTeppArgumentCompleter -Command Get-Alcohol -Parameter Type -Name AVDManagementFramework.alcohol
#>


New-PSFLicense -Product 'AVDManagementFramework' -Manufacturer 'wmoselhy' -ProductVersion $script:ModuleVersion -ProductType Module -Name MIT -Version "1.0.0.0" -Date (Get-Date "2021-04-26") -Text @"
Copyright (c) 2021 wmoselhy
 
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
 
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
 
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"@


# General Settings
$script:WVDConfigurationLoaded = $false
$script:NameMappings = @{}

# Resource Groups
$script:ResourceGroups = @{}

# Network
$script:VirtualNetworks = @{}
$script:Subnets = @{}
$Script:AddressSpaces = @()
$script:NetworkSecurityGroups = @{}
$script:RemotePeerings = @{}
$script:RouteTables = @{}

# Storage
$script:StorageAccounts = @{}
$script:FileShares = @{}
$script:PrivateLinks = @{}

# DesktopVirtualization
$script:HostPools = @{}
$script:ApplicationGroups = @{}
$script:Workspaces = @{}
$script:VMTemplates = @{}
$script:SessionHosts = @{}

# Tags
$script:GlobalTags = @{}
#endregion Load compiled code