pf-azFuncDeploy.psm1

$ErrorActionPreference = 'stop'

function Ensure-Module ($name) {
    if (Get-Module -name $name ) { return }

    if (-not (Get-Module -name $name -ListAvailable )) {
        Set-PSRepository -name PsGallery -InstallationPolicy Trusted
        Install-Module -Name $name -Repository PsGallery
    }
    Import-Module $name
}

function Get-ScriptTopCallerFolder() {
    $scriptPath = Get-PSCallStack | where ScriptName | select -Last 1
    $result = Split-Path $scriptPath.ScriptName -Parent
    return $result;
}

function New-TemporaryDirectory {
    $parent = [System.IO.Path]::GetTempPath()
    $name = [System.IO.Path]::GetRandomFileName()
    New-Item -ItemType Directory -Path (Join-Path $parent $name)
}


function Prepare-Git_CLI {
    # Git return messages in sdterr by default, which can be interpreted as
    # an execution error even when the call succeded.
    # The following ensures output is redirected to stdout.
    # Execution error should be checked using $LASTEXITCODE
    $env:GIT_REDIRECT_STDERR = '2>&1'
}

function Get-GitBranchName {
    git symbolic-ref HEAD | % { $_.TrimStart("refs/heads/") }
}

function Get-GitTopFolder {
    $folder = git rev-parse --show-toplevel
    if ($LASTEXITCODE -eq 0) {
        $folder = $folder -replace '/', '\'
        return $folder 
    }
} 

function Get-GitRepositoryName($remote = 'origin') {
    $remoteUrl = git remote get-url --push $remote
    if ($LASTEXITCODE -eq 0) {
        $result = Split-Path -Path $remoteUrl -Leaf
        return $result
    }
}

function Get-GitInfo($path) {
    $result = @{}

    try {
        if ($path) {
            Push-Location $path
        }
        Prepare-Git_CLI
        $result.Name = Get-GitRepositoryName
        $result.Folder = Get-GitTopFolder
        $result.Branch = Get-GitBranchName
        $result.LastAuthor = git log -1 --pretty=format:'%an'
        $result.CommitCount = git rev-list HEAD --count
        return $result
    }
    finally {
        if ($path) {
            Pop-Location
        }
    }
}

$azLocationAbbreviationMap = @{
    UKS = 'UK South'
    UKW = 'UK West'
}

function Get-AzLocationAbbreviation($location){
   $item = $azLocationAbbreviationMap.GetEnumerator() | ? { $_.Value -eq $location  } 
   return $item.Key
}
function Get-AzLocationAbbreviation:::Example{
   Get-AzLocationAbbreviation 'UK South'
}

function Ensure-Member { 
    Param (
        [Parameter(ValueFromPipeline=$true)]
        $InputObject,
        $Name,
        $value,
        [Switch]$force,
        [Switch]$PassThru
    )
    Process {
        if (!$InputObject) { return }

        $doAssignValue = (-not $InputObject.$Name) -or $force.IsPresent

        $member = Get-Member -InputObject $InputObject -Name $Name
        if (!$member) {
            $doAssignValue = $true
            $member = Add-Member -InputObject $InputObject -Name $Name -Value $null `
                -MemberType NoteProperty -PassThru
        }

        if ($doAssignValue) {
            $valueResult = if ($Value -is [ScriptBlock]) {
                $Value.InvokeReturnAsIs($InputObject)
            }
            else {
                $Value
            }
            $InputObject.$Name = $valueResult
        }
        if ($PassThru) {
            return $InputObject
        }
    }
}

function Ensure-Member:::Example {
    $obj = ConvertFrom-Json "{ A : 1, B : 2}"
    $obj | Ensure-Member -Name D -Value { 4 }
    $obj | Ensure-Member -Name D -Value { 5 }
    $obj | Ensure-Member -Name D -Value { 6 } -force
    $obj | Ensure-Member -Name E -Value 7
    $obj | Ensure-Member -Name F -Value { $_.D }
}


function Ensure-DeployManifest {
    $scriptFolder = Get-ScriptTopCallerFolder
    $gitInfo = Get-GitInfo -path $scriptFolder
    $content = gc -Path "$($gitInfo.Folder)\deploy\deployManifest.json" -Raw
    $result = ConvertFrom-Json $content
    $result | Ensure-Member -Name ScriptFolder -value $scriptFolder
    $result | Ensure-Member -Name ServiceName -value $gitInfo.Name
    $result | Ensure-Member -Name Folder -value $gitInfo.Folder
    $result | Ensure-Member -Name ResourceGroup -value {
        $azLocationAbbreviation = Get-AzLocationAbbreviation $_.Location
        $ownerAndEnvironment = $gitInfo.Branch.Split('!\/')[0]
        return "$ownerAndEnvironment-$($_.ServiceName)-$azLocationAbbreviation"
    }
    $result | Ensure-Member -Name AppInsights -value { $_.ResourceGroup + "-Insights" }
    $result | Ensure-Member -Name TemplateFile -value  { $_.ScriptFolder + "\template.json" }

    return $result
}

function Dev-Prepare {
    npm install -g azure-functions-core-tools
}

function Ensure-AzContext {
    $azSubscription = $deployManifest.Subscription
    $azContext = Get-AzContext
    if (-not $azContext) {
        Connect-AzAccount -Subscription $azSubscription | Out-Null
    }
    else {
        Set-AzContext -Subscription $azSubscription | Out-Null
    }
}

function Get-AzStorageRandomName {
    Get-RandomString -seed 'AnyObject' -validChars (Get-Chars -LowerCase -number )
}

function Deploy-AzService {
    # Install-Module -Name Az -AllowClobber -Scope CurrentUser
    Ensure-AzContext

    $azFunctionApp = $deployManifest.ResourceGroup

    function Get-AzDeploymentMode {
        $resourceGroup = Get-AzResourceGroup -Name $deployManifest.ResourceGroup -ErrorAction SilentlyContinue
        if(!$resourceGroup) {
            $resourceGroup = New-AzResourceGroup -Name $deployManifest.ResourceGroup -Location $deployManifest.Location
            return 'Complete'
        }
        return 'Incremental'
    }

    $DeploymentMode = Get-AzDeploymentMode

    if ($deployManifest.TemplateFile) {
        if (Test-Path ($deployManifest.TemplateFile)) {
            New-AzResourceGroupDeployment -Name $deployManifest.TemplateName `
                -Mode $DeploymentMode -Force -Confirm:$false `
                -ResourceGroupName $deployManifest.ResourceGroup `
                -TemplateFile $deployManifest.TemplateFile #`
             # -TemplateParameterFile $deployManifest.ParametersFile;

            $pv = Get-AzResourceGroupDeployment -Name $deployManifest.TemplateName `
                -ResourceGroupName $deployManifest.ResourceGroup

            write-host $pv.ProvisioningState
        }
    }

    function Generate-AzStorageName([string]$sourceName) {
        [string]$result = $sourceName.ToLowerInvariant()
        $result = $result.replace('-','')
        return $result + "st"
    }

    $azFuncStorage = Generate-AzStorageName -sourceName $azFunctionApp


    function Ensure-AzApplicationInsights {
        $applicationInsights = Get-AzApplicationInsights -Name $deployManifest.AppInsights `
            -ResourceGroupName $deployManifest.ResourceGroup -ErrorAction SilentlyContinue
        if (!$applicationInsights) {
            $applicationInsights = New-AzApplicationInsights -Name $deployManifest.AppInsights `
                -ResourceGroupName $deployManifest.ResourceGroup `
                -location $deployManifest.Location
        }
    }

    Ensure-AzApplicationInsights

    $azWebApp = Get-AzWebApp -Name $azFunctionApp -ResourceGroupName `
        $deployManifest.ResourceGroup -ErrorAction SilentlyContinue
    if (!$azWebApp) {

        $storage = Get-AzStorageAccount -Name $azFuncStorage `
            -ResourceGroupName $deployManifest.ResourceGroup -ErrorAction SilentlyContinue
        if (!$storage) {
            $storage = New-AzStorageAccount -Name $azFuncStorage -Kind StorageV2 -AccessTier Hot `
                -ResourceGroupName $deployManifest.ResourceGroup -AssignIdentity `
                -SkuName Standard_LRS -Location $deployManifest.Location
        } 

        $output = az functionapp create --name $azFunctionApp -g $deployManifest.ResourceGroup `
          -s $storage.StorageAccountName `
          --disable-app-insights `
          --consumption-plan-location uksouth
    } 

# az functionapp delete --name $azFunctionApp -g $deployManifest.ResourceGroup

    $output = az functionapp config appsettings set --name $azFunctionApp `
        --resource-group $deployManifest.ResourceGroup `
        --settings FUNCTIONS_EXTENSION_VERSION=~2

    function Ensure-AzFunctionAppInsightKey {
        $appInsights = Get-AzApplicationInsights `
                        -ResourceGroupName $deployManifest.ResourceGroup `
                        -Name $deployManifest.AppInsights

        $appInsightKey = $appInsights.InstrumentationKey
        if ($appInsightKey) {
            $output = az functionapp config appsettings set --name $azFunctionApp `
                --resource-group $deployManifest.ResourceGroup `
                --settings APPINSIGHTS_INSTRUMENTATIONKEY=$appInsightKey
        }
    }

    Ensure-AzFunctionAppInsightKey

    Publish-AzFunction
    Ensure-AzKeyVault
}

function Get-AzFunctionProjects($path) {
    $funcProjectList = gci -Path $path -Filter *.csproj -Recurse |
        Select-String -SimpleMatch '<AzureFunctionsVersion>'
    $funcProjectList.Path | % { Split-Path $_ -Parent }
}


function Publish-AzFunction($projectPath)  {
    if (!$projectPath) {
        $projectPath = Get-AzFunctionProjects -path $deployManifest.Folder
    }

    try {

        $tmpFolder = New-TemporaryDirectory

        $tmpFolderPublish = $tmpFolder.FullName + "\publish"
        $zipPath = $tmpFolder.FullName + "\deploy.zip"

        mkdir -Path "$tmpFolderPublish\bin" -Force | Out-Host
        dotnet publish $projectPath --configuration debug -o $tmpFolderPublish | Out-Host

        Compress-Archive -Path "$tmpFolderPublish\*" -DestinationPath $zipPath `
            -CompressionLevel Optimal -Force -Confirm:$false | Out-Host

<#
        az functionapp deployment source config-zip `
            -g $deployManifest.ResourceGroup -n $azFunctionApp `
            --src $zipPath
 
        az functionapp deployment source config-zip `
            -g djl02_drawioAzureBridge -n PBITOpsDrawIO20191108073052 `
            --src $zipPath
#>


        Publish-AzWebapp -ArchivePath $zipPath -ResourceGroupName $deployManifest.ResourceGroup `
            -Name $azFunctionApp -Force -Confirm:$false
    }
    finally {
        if ($tmpFolder) {
            $tmpFolder | Remove-Item -Recurse -Force -Confirm:$false -ErrorAction SilentlyContinue
        }
    }
}

function Ensure-AzKeyVault {
#https://gist.github.com/pascalnaber/75412a97a0d0b059314d193c3ab37c4c
    Set-AzWebApp -AssignIdentity $true -Name $azFunctionApp -ResourceGroupName $deployManifest.ResourceGroup
    $app = Get-AzWebApp -ResourceGroupName $deployManifest.ResourceGroup -Name $azFunctionApp

    $azVaultName = $deployManifest.ServiceName + "-kv"
    $azVault = Get-AzKeyVault -Name $azVaultName 
    if (-not $azVault) {
        $azVault = Get-AzKeyVault -Name $azVaultName -InRemovedState -Location $deployManifest.Location
        if (-not $azVault) {
            $azVault = New-AzKeyVault -Name $azVaultName -ResourceGroupName $deployManifest.ResourceGroup `
                -Location $deployManifest.Location -EnablePurgeProtection -EnableSoftDelete
        }
        else {
            Undo-AzKeyVaultRemoval -VaultName $azVaultName -ResourceGroupName $deployManifest.ResourceGroup `
                -Location $deployManifest.Location
        }
    }

    az keyvault set-policy --secret-permissions get -n $azVaultName -g $deployManifest.ResourceGroup `
        --object-id $app.Identity.PrincipalId | Out-Host

    $localSettingsFile = gci -Path $deployManifest.Folder -Filter local.settings.json -Recurse

    $settingsPath = $localSettingsFile[0].FullName

    function Get-AppLocalSettings($path, $name) {
        $settings = gc -path $path -Raw | ConvertFrom-Json 
        $SecretSettings = $settings.Values | Get-Member -MemberType NoteProperty | where Name -Like $name
        $result = @{}
        $SecretSettings.Name | % { $result.Add($_ , $settings.Values.$_ ) }
        return $result
    }

    $vaultSecrets = Get-AppLocalSettings -path $settingsPath -name 'Secret-*'
    $vaultSecrets.GetEnumerator()  | Set-AzSecretSetting -VaultName $azVaultName
}

function Set-AzSecretSetting {
    Param (
        $VaultName, 
        [parameter(ValueFromPipelineByPropertyName=$true,Position=0)]
        [Alias('Name')]
        [String]$SecretName,
        [parameter(ValueFromPipelineByPropertyName=$true,Position=1)]
        [Alias('Value')]
        [String]$SecretValue
    )

    begin {
        $app = Get-AzWebApp -ResourceGroupName $deployManifest.ResourceGroup -Name $azFunctionApp
        $appSettings = $app.SiteConfig.AppSettings | 
            % { $_tmpHash = @{} } { $_tmpHash[$_.Name] = $_.Value } { $_tmpHash }
    }
    process {
        $SecureValue = ConvertTo-SecureString -String $SecretValue -AsPlainText -Force
        $azVaultSecret = Set-AzKeyVaultSecret -VaultName $VaultName -Name $SecretName `
                                -SecretValue $SecureValue
        $settingValue = "@Microsoft.KeyVault(SecretUri=$($azVaultSecret.Id))"
        $appSettings[$SecretName] = $settingValue

<#
        # The following does not work as it can truncate the last character
        az functionapp config appsettings set --name $azFunctionApp `
            --resource-group $deployManifest.ResourceGroup --settings $settingValue
#>


    }
    end {
        Set-AzWebApp -ResourceGroupName $deployManifest.ResourceGroup -Name $azFunctionApp `
            -AppSettings $appSettings
    }
}

function Export-AzResources {

    Export-AzResourceGroup -ResourceGroupName $deployManifest.ResourceGroup -Path $deployManifest.TemplateFile `
        -IncludeParameterDefaultValue  -Force -Confirm:$false

    $armContent = Get-Content $deployManifest.TemplateFile -Raw | ConvertFrom-Json

    $armResourcesTypesToRemove = @(
        # Ignored in order to support SoftDelete
        "Microsoft.KeyVault/vaults", 
        # Ignored as require values that are not obtained during Export
        "Microsoft.KeyVault/vaults/secrets",
        # Previous deployments are not relevant
        "Microsoft.Web/sites/deployments"
         )

    $armContent.resources = $armContent.resources | ? { $_.type -notin $armResourcesTypesToRemove }

    $jsonContent = $armContent | ConvertTo-Json -Depth 100 
    $jsonContent = Format-Json -Json $jsonContent -Indentation 2
    $jsonContent | Out-File -FilePath $deployManifest.TemplateFile -Encoding ascii
}

function Remove-AzService {
    Remove-AzResourceGroup $deployManifest.ResourceGroup -Force -Confirm:$false
}

function Setup-Planuml_Container {

    # docker run -d --name plantuml -p 8080:8080 plantuml/plantuml-server:tomcat

    $containerName = "$azEnv-plantuml"
    $containerGroup = New-AzureRmContainerGroup -ResourceGroupName $deployManifest.ResourceGroup `
        -Name $containerName `
        -Image 'plantuml/plantuml-server:tomcat' -DnsNameLabel plantuml -Port 8080

    $basepath = "http://" +  $containerGroup.Fqdn + ":8080"
    $puml =  "https://raw.githubusercontent.com/anoff/devradar/master/assets/editor-build.puml"

    start "$basepath/proxy?src=$puml"

    # $containerGroup | Remove-AzureRmContainerGroup
}

function Get-FunctionsLocal {
    $scriptPath = Get-PSCallStack | where ScriptName | select -first 1
    $functions = Get-Command | ? { $_.ScriptBlock } | ? { $_.ScriptBlock.File } | 
        ? { $_.ScriptBlock.File -eq $scriptPath.ScriptName }
    return $functions
}

$toExport = Get-FunctionsLocal

Export-ModuleMember -Function $toExport