Author : MD TAREQ HASSAN | Updated : 2021/10/18

Stack Name When Using Blob Container as Backend

Required Credentials and Environment Variables

Create Service Principal

Create new App Registration in Azure AD

App Registration in Azure AD

Copy ObjectId & ApplicationId

opy ObjectId & ApplicationId of Service Principal

Copy TenantId

Copy TenantId of Service Principal

Add Client Secret

Add Service Principal Client Secret

Gathered information (service_principal_credential.json)

{
  "AZURE_CLIENT_SECRET": "xxx",
  "AZURE_TENANT_ID": "xxx",
  "AZURE_CLIENT_ID": "xxx",
  "AZURE_SERVICE_PRINCIPAL_OBJECT_ID": "xxx",
  "AZURE_SERVICE_PRINCIPAL_DISPLAY_NAME": "pulumi-demo-sp"
}

Create Resource Group and Blob Container

Copy Storage Account Key

Copy Storage Account Key

Gathered information (storage_container_credential.json)

{
  "AZURE_STORAGE_ACCOUNT": "pulumibackendsa",
  "AZURE_STORAGE_KEY": "xxx==",
  "AZURE_STORAGE_CONTAINER": "pulumi-backend-container"
}

Create KeyVault and Key

KayVault (RSA) key for Pulumi secrets

Azure KayVault (RSA) key for Pulumi secrets

Assign Role to Service Principal

Pulumi will use service principal to deploy stack to the target subscription. Assign “Contributor” role to the service principal at subscription scope.

Assign Contributor role to Service Principal at Subscription scope

Assign yourself “Key Vault Administrator” role (otherwise you would not be able to create key) and assign service principal “Key Vault Crypto User” (so that pulumi can read key).

Assign 'Key Vault Administrator' role and 'Key Vault Crypto User' role

Environment Variables

Pulumi Credential (from service principal credential) (pulumi_credential.json)

{
  "ARM_SUBSCRIPTION_ID": "xxx",
  "ARM_TENANT_ID": "xxx",
  "ARM_CLIENT_ID": "xxx",
  "ARM_CLIENT_SECRET": "xxx",
}

Now, set required environment variables

Set required environment variables for Pulumi

Create Pulumi Project and Deploy to Target Subscription

#
# Set variables for pulumi command
#
$blobContainerName = 'pulumi-backend-container'
$resourceGroupLocation = 'Japan East'
$kvName = 'pulumi-backend-kv'
$kvKeyName = 'pulumi-secret-key'

$solutionName = 'demo-iac-pulumi' # VS solution
$pulumiProjectName = 'demo-azure-infra' # Pulumi project -> VS project (will be added to VS solution)
$pulumiProjectDescription = 'A demo pulumi IaC project'
$pulumiStackName = 'dev'


#
# Create solution directory and VS solution
#
if (Test-Path .\$solutionName) {
    # Clean up folder
    Remove-Item .\$solutionName\* -Force -Recurse  | Out-Null
} else {
    # Create folder
    New-Item -Path .\$solutionName -ItemType Directory | Out-Null
}
Set-Location $solutionName
dotnet new sln --name $solutionName


#
# Pulumi login to Azure (blob container as stack state backend)
#
pulumi login azblob://$blobContainerName

#
# Pulumi new project (Azure KeyVault key for pulumi secret)
#
pulumi new azure-csharp `
    --stack="$pulumiStackName" `
    --secrets-provider="azurekeyvault://$kvName.vault.azure.net/keys/$kvKeyName" `
    --name="$pulumiProjectName" `
    --description="$pulumiProjectDescription" `
    --dir="$pulumiProjectName" `
    --config="azure-native:location=$resourceGroupLocation" `
    --force


#
# Add pulumi project to VS solution
#
dotnet sln add ./$pulumiProjectName/$pulumiProjectName.csproj


#
# Deploy pulumi stack to Azure (target subscription)
#


# Prevent Pulumi from using Azure CLI token -> logout from all accounts
az account clear
az logout

Set-Location $pulumiProjectName

pulumi up #--yes

Automating the Whole Process with PowerShell Script

Service principal information (credential)

service_principal_credential.json (this file is expected in current folder if service principal is created in advance)

{
  "AZURE_CLIENT_SECRET": "xxx",
  "AZURE_TENANT_ID": "xxx",
  "AZURE_CLIENT_ID": "xxx",
  "AZURE_SERVICE_PRINCIPAL_OBJECT_ID": "xxx",
  "AZURE_SERVICE_PRINCIPAL_DISPLAY_NAME": "pulumi-demo-sp"
}

Storage Account and blob container information (credential)

storage_container_credential.json (this file is expected in current folder if storage account and container are created in advance)

{
  "AZURE_STORAGE_ACCOUNT": "pulumibackendsa",
  "AZURE_STORAGE_KEY": "xxx==",
  "AZURE_STORAGE_CONTAINER": "pulumi-backend-container"
}

KeyVault and Key

Environment Variables

Pulumi commands generation

utility-functions.ps1

#
# ----------------------------------------------------------------------------------------------------
# Functions - Login / Connect to Azure
# ----------------------------------------------------------------------------------------------------
#
function Connect-AzAccountTargetSubscription {
    param (

        [Parameter(Mandatory)]
        [string] $SubscriptionName
    )

    # In Azure CloudShell, by default managed identity is used to connect to Azure Account (account will be some thing like "MSxxx")
    #
    $loggedInAccount = (Get-AzContext).Account
    if (!( "$loggedInAccount".Contains("@") )) {

        Write-Host "Login is required `r`n"

        Connect-AzAccount -UseDeviceAuthentication

        Set-AzContext -Subscription (Get-AzSubscription -SubscriptionName $SubscriptionName).Id
        Start-Sleep -Seconds 2

        $currentSubscriptionName = (Get-AzContext).Subscription.Name
        if ($currentSubscriptionName -ne $SubscriptionName) {
            throw "Context (subscription) mis-match"
        } else {
            Write-Host "Subscription '$SubscriptionName' has been set as context"
        }
    }
    else {

        Write-Host "User is already logged in `r`n"
    }
}



#
# ----------------------------------------------------------------------------------------------------
# Functions - Service Principal
# ----------------------------------------------------------------------------------------------------
#
function ConvertTo-String {
    param(
        [Parameter(Mandatory)]
        [Security.SecureString] $SecureString
    )
    try {
        $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureString)
        [Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr)
    }
    finally {
        if ( $bstr -ne [IntPtr]::Zero ) {
            [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
        }
    }
}

function New-ServicePrincipal {
    param (

        [Parameter(Mandatory)]
        [string] $ServicePrincipalDisplayName
    )

    $spCred = @{}

    try {

        $newSp = (New-AzADServicePrincipal -DisplayName $ServicePrincipalDisplayName)
        Start-Sleep -Seconds 3 # cool down

        $spSecretAsStringObj = ConvertTo-String -SecureString $newSp.Secret

        $spCred['AZURE_SERVICE_PRINCIPAL_DISPLAY_NAME'] = $ServicePrincipalDisplayName
        $spCred['AZURE_SERVICE_PRINCIPAL_OBJECT_ID'] = $newSp.Id
        $spCred['AZURE_TENANT_ID'] = (Get-AzTenant).Id
        $spCred['AZURE_CLIENT_ID'] = $newSp.ApplicationId
        $spCred['AZURE_CLIENT_SECRET'] = $spSecretAsStringObj

        <# $envVarKeySpDisplayName = "AZURE_SERVICE_PRINCIPAL_DISPLAY_NAME"
        $envVarKeySpObjectId = "AZURE_SERVICE_PRINCIPAL_OBJECT_ID"
        $envVarKeyTenantId = "AZURE_TENANT_ID"
        $envVarKeyClientId = "AZURE_CLIENT_ID"
        $envVarKeyClientSecret = "AZURE_CLIENT_SECRET" #>
    }
    catch {
        Write-Warning $Error[0]
    }

    return $spCred
}



#
# ----------------------------------------------------------------------------------------------------
# Functions - File Operations
# ----------------------------------------------------------------------------------------------------
#

function Read-JsonFileAsHashTable {
    param (
        [Parameter(Mandatory)]
        [string] $JsonFileUriWithExtension
    )

    $ht = @{}

    try {

        $ht = Get-Content -Raw -Path $JsonFileUriWithExtension | ConvertFrom-Json -AsHashtable
    }
    catch {

        Write-Warning $Error[0]
    }

    return $ht
}

function WriteTo-File {
    param (
        [Parameter(Mandatory)]
        [string] $Content,
        [Parameter(Mandatory)]
        [string] $FileUriWithExtension
    )

    try {
        Write-Output $Content > $FileUriWithExtension
    }
    catch {
        Write-Warning $Error[0]
    }
}



#
# ----------------------------------------------------------------------------------------------------
# Functions - Storage Account & Blob Container
# ----------------------------------------------------------------------------------------------------
#

function  Get-StorageAccountKey {
    param (
        [Parameter(Mandatory)]
        [string] $ResourceGroupName,
        [Parameter(Mandatory)]
        [string] $StorageAccountName
    )

    $saKeys = Get-AzStorageAccountKey -ResourceGroupName  $ResourceGroupName -Name $StorageAccountName
    $key1 = $saKeys[0].Value
    
    return $key1
}



#
# ----------------------------------------------------------------------------------------------------
# Functions - User
# ----------------------------------------------------------------------------------------------------
#
function Get-LoggedInUserObjectId {
    (Get-AzADUser -UserPrincipalName (Get-AzContext).Account).Id
}



#
# ----------------------------------------------------------------------------------------------------
# Functions - Role & Role Assignment
# ----------------------------------------------------------------------------------------------------
#
function New-RoleAssignement {

    param (

        [Parameter(Mandatory)]
        [string] $RoleDefinitionName,

        [Parameter(Mandatory)]
        [string] $ObjectId,

        [Parameter(Mandatory)]
        [string] $Scope
    )

    #
    # Before trying to assign role,
    # Check that is given role is already assigned or not
    #
    $existingRoleAssignment = (Get-AzRoleAssignment -RoleDefinitionName $RoleDefinitionName -ObjectId $ObjectId -Scope $Scope)
        
    if ($null -eq $existingRoleAssignment) {

        Write-Host "[Role '$($RoleDefinitionName)' at Scope '$($Scope)'] does not exist, creating new role assignment... `r`n"

        $raSplat = @{
            RoleDefinitionName = $RoleDefinitionName
            ObjectId           = $ObjectId
            Scope              = $Scope
        }

        New-AzRoleAssignment @raSplat | Out-Null

        Write-Host "Role '$($RoleDefinitionName)' has been assigned at scope '$($Scope)' `r`n"
    }
    else {

        Write-Host "[Role '$($RoleDefinitionName)' at Scope '$($Scope)']  already exists `r`n"
    }
}

prepare_pulumi_backend.ps1

#
# ----------------------------------------------------------------------------------------------------
# Required variables
# ----------------------------------------------------------------------------------------------------
#

#
# Subscription
#
$subscriptionName = 'Pay-As-You-Go Dev/Test'
$subscriptionId = "" # will be set later
$subscriptionRoleServicePrincipal = 'Contributor' # the role that service principal should have at subscription scope

#
# Service Principal
#
$spDisplayName = 'pulumi-demo-sp'
$spCredJsonFile = 'service_principal_credential.json'
$envVarKeySpObjectId = 'AZURE_SERVICE_PRINCIPAL_OBJECT_ID'
$envVarKeySpTenantId = 'AZURE_TENANT_ID'
$envVarKeySpClientId = 'AZURE_CLIENT_ID'
$envVarKeySpClientSecret = 'AZURE_CLIENT_SECRET'

#
# Resource Group
#
$rgName = 'pulumi-backend-rg'
$rgLocation = 'Japan East'

#
# Storage Account & Blob Container
#
$saName = 'pulumibackendsa'
$saSku = 'Standard_ZRS'
$saKind = 'StorageV2'
$blobContainerName = 'pulumi-backend-container' # Pulumi will use this to save state file
$blobContainerCredJsonFile = 'storage_container_credential.json'
$envVarKeyStorageAccountName = 'AZURE_STORAGE_ACCOUNT'
$envVarKeyBlobContainerName = 'AZURE_STORAGE_CONTAINER'
$envVarKeyStorageAccountKey = 'AZURE_STORAGE_KEY' #"AZURE_STORAGE_SAS_TOKEN"

#
# KeyVault & Key
#
$kvName = 'pulumi-backend-kv'
$kvKeyName = 'pulumi-secret-key' # Pulumi will use this key to encrypt secrets
$kvRoleLoggedInUser = 'Key Vault Administrator' # so that current user can create key in the KeyVault
$kvRoleServicePrincipal = 'Key Vault Crypto User' # so that service principal can access key (RBAC -> vault access policy)


#
# Credential for Pulumi to deploy to the target subscription
#
$envVarKeyArmSubscriptionId = 'ARM_SUBSCRIPTION_ID'
$envVarKeyArmTetantId = 'ARM_TENANT_ID'
$envVarKeyArmClientId = 'ARM_CLIENT_ID'
$envVarKeyArmClientSecret = 'ARM_CLIENT_SECRET'


#
# To generate Pulumi commands
#
$solutionName = 'demo-iac-pulumi' # VS solution
$pulumiProjectName = 'demo-azure-infra' # Pulumi project & VS project
$pulumiProjectDescription = "A demo pulumi IaC project"
$pulumiStackName = 'dev'
$pulumiCmdPowerShellFile = "pulumi_login_and_new_project_cmd.ps1"



#
# ----------------------------------------------------------------------------------------------------
# Import functions (dot sourcing)
# ----------------------------------------------------------------------------------------------------
#
. .\utility-functions.ps1



#
# ----------------------------------------------------------------------------------------------------
# Login/Connect to Azure
# ----------------------------------------------------------------------------------------------------
#
try {
    Connect-AzAccountTargetSubscription -SubscriptionName $subscriptionName
}
catch {
    Write-Error $Error[0]
    return
}


#
# ----------------------------------------------------------------------------------------------------
# Service Principal
# ----------------------------------------------------------------------------------------------------
#
$sp = $null
try {
    $sp = Get-AzADServicePrincipal -DisplayName $spDisplayName -ErrorAction Stop
}
catch {
    Write-Warning $Error[0]
}

$spCredential = @{}

if ($null -eq $sp) {

    Write-Host "Service principal '$($spDisplayName)' does not exist. Creating new... `r`n"

    $spCredential = New-ServicePrincipal -ServicePrincipalDisplayName $spDisplayName
    #Start-Sleep -Seconds 2

    #
    # Since service principal secret is only available at creation time, we need to save to file
    # 
    Write-Host "Saving service principal information to '$($spCredJsonFile)' `r`n"

    $spFileSplat = @{
        Content              = $($spCredential | ConvertTo-Json)
        FileUriWithExtension = $spCredJsonFile
    }
    WriteTo-File @spFileSplat

    Write-Host "Service principal information has been saved `r`n"
}
else {

    Write-Warning "Service principal '$($spDisplayName)' already exists, therefore '$($spCredJsonFile)' must be present in the current folder`r`n"

    if (Test-Path $spCredJsonFile) {

        Write-Host "Fetching service principal information from '$($spCredJsonFile)' `r`n"
    
        $spCredential = Read-JsonFileAsHashTable -JsonFileUriWithExtension $spCredJsonFile
    }
    else {

        Write-Error "Service principal '$($spDisplayName)' exists but '$($spCredJsonFile)' is not present in the current folder (you must provide service principal information in this file) `r`n"

        $spCredential = @{}
    }
}

if ($spCredential.Count -lt 5) {
    # displayName, objectId, tenantId, clientId, clientSecret
    Write-Error "One or more service principal information is missing, terminating script execution `r`n"
    return
}

#
# Assign role to service principal at subscription scope
#
$subscriptionId = (Get-AzSubscription -SubscriptionName $subscriptionName).Id
$subscriptionResourceId = "/subscriptions/$subscriptionId"

$spObjectId = $spCredential[$envVarKeySpObjectId]

$subscriptionRaSpSplat = @{
    RoleDefinitionName = $subscriptionRoleServicePrincipal
    ObjectId           = $spObjectId
    Scope              = $subscriptionResourceId
}
New-RoleAssignement @subscriptionRaSpSplat # if service principal is created by this script, by default 'Contributor' role will already be assigned at subscription scope




#
# ----------------------------------------------------------------------------------------------------
# Pulumi credential (environment variables) -> for Pulumi deployment to target subscription using Service principal
# ----------------------------------------------------------------------------------------------------
#
$pulumiCredential = @{}
$pulumiCredential[$envVarKeyArmSubscriptionId] = $subscriptionId
$pulumiCredential[$envVarKeyArmTetantId] = $spCredential[$envVarKeySpTenantId]
$pulumiCredential[$envVarKeyArmClientId] = $spCredential[$envVarKeySpClientId]
$pulumiCredential[$envVarKeyArmClientSecret] = $spCredential[$envVarKeySpClientSecret]



#
# ----------------------------------------------------------------------------------------------------
# Resource group
# ----------------------------------------------------------------------------------------------------
#
$rgSplat = @{
    Name     = $rgName
    Location = $rgLocation
}

$rg = $null
try {
    $rg = Get-AzResourceGroup @rgSplat -ErrorAction Stop
}
catch {
    Write-Warning $Error[0]
}

if ($null -eq $rg) { 

    Write-Host "Resource group '$($rgName)' does not exist, creating new... `r`n"

    $rg = (New-AzResourceGroup @rgSplat)
    
    Start-Sleep -Seconds 3
}
else {

    Write-Host "Resource group '$($rgName)' already exists `r`n"
}

#
# Make sure resource group is ready
# (there could be some delay due to some problems in Azure)
#
$rg = (Get-AzResourceGroup @rgSplat)
while ($null -eq $rg) {
    Write-Host "Resource group not found, trying to re-fetch..."
    Start-Sleep -Secomds 2
    $rg = (Get-AzResourceGroup @rgSplat)
}

#$rgResourceId = $rg.ResourceId



#
# ----------------------------------------------------------------------------------------------------
# Storage account & blob container
# ----------------------------------------------------------------------------------------------------
#

#
# Storage account
#
$getSaSplat = @{
    Name              = $saName
    ResourceGroupName = $rgName
}

$sa = $null
try {
    $sa = Get-AzStorageAccount @getSaSplat -ErrorAction Stop
}
catch {
    Write-Warning $Error[0]
}

if ($null -eq $sa) {

    Write-Host "Stoarge Account '$($saName)' does not exist, creating new...`r`n"
    Write-Host "`r`n-------------------------------------------------------------------------------------------`r`n"

    $newSaSplat = $getSaSplat + @{
        Location = $rgLocation
        SkuName  = $saSku
        Kind     = $saKind
    }

    New-AzStorageAccount @newSaSplat -AsJob | Out-Null

    Start-Sleep -Seconds 2
    #Get-Job

    $provisioningState = (Get-AzStorageAccount @getSaSplat).ProvisioningState

    while ('Succeeded' -ne $provisioningState) {
        Write-Host "[$(Get-Date -DisplayHint Time)] Storage account is not created yet -> ProvisioningState: $provisioningState `r`n"
        Start-Sleep -Seconds 3
        $provisioningState = (Get-AzStorageAccount @getSaSplat).ProvisioningState
    }   

    Write-Host "`r`n-------------------------------------------------------------------------------------------`r`n"
    Write-Host "Stoarge Account '$($saName)' has been created `r`n"
}
else {
    
    Write-Host "Stoarge Account '$($saName)' already exists `r`n"
}
#$saResourceId = $sa.ResourceId


#
# Storage account key (for Pulumi to access blob container)
# 
$saKeySplat = @{
    ResourceGroupName  = $rgName
    StorageAccountName = $saName
}
$saKey = Get-StorageAccountKey @saKeySplat
#Write-Host $saKey


#
# Storage account context
#
# To create blob container, storage account context is needed
# There are multiple ways to create storage account context
# Account key will be used in this case
#
$saContextSplat = @{
    StorageAccountName = $saName
    StorageAccountKey  = $saKey
}
$saContext = New-AzStorageContext @saContextSplat


#
# Blob container
#
$blobContainer = $null
Try {

    $blobContainer = Get-AzStorageContainer -Name $blobContainerName -Context $saContext -ErrorAction Stop
}
catch {
    
    Write-Warning $Error[0]
}

if ($null -eq $blobContainer) {
    
    Write-Host "Container '$($blobContainerName)' does not exist, creating new... `r`n"

    # Create blob container
    #
    $blobContainerSplat = @{
        Name       = $blobContainerName
        Context    = $saContext
        Permission = 'Off'
    }
    $blobContainer = New-AzStorageContainer @blobContainerSplat
}
else {

    Write-Host "Container '$($blobContainerName)' already exists `r`n"
}
#$cbc = $blobContainer.CloudBlobContainer
#Write-Host $cbc.Uri


#
# Storage container credential (for Pulumi to access blob container)
#
$blobContainerCredential = @{}

if (Test-Path $blobContainerCredJsonFile) {

    Write-Host "'$($blobContainerCredJsonFile)' is present in current folder, storage container information will be fetched from it `r`n"

    $blobContainerCredential = Read-JsonFileAsHashTable -JsonFileUriWithExtension $blobContainerCredJsonFile

    $saNameFromFile = $blobContainerCredential[$envVarKeyStorageAccountName]
    $bcNameFromFile = $blobContainerCredential[$envVarKeyBlobContainerName]
    $saKeyFromFile = $blobContainerCredential[$envVarKeyStorageAccountKey]

    if ( ($saName -ne $saNameFromFile) -or ($blobContainerName -ne $bcNameFromFile) -or ($saKeyFromFile -ne $saKey) ) {
        Write-Error "Storage account information mismatch"
        $blobContainerCredential = @{}
    }
}
else {

    Write-Host "'$($blobContainerCredJsonFile)' is not present in the script folder. Creating container credential `r`n"

    $blobContainerCredential[$envVarKeyStorageAccountKey] = $saKey
    $blobContainerCredential[$envVarKeyStorageAccountName] = $saName
    $blobContainerCredential[$envVarKeyBlobContainerName] = $blobContainerName

    # Save container information to file
    #
    $bccSplat = @{
        Content              = ($blobContainerCredential | ConvertTo-Json)
        FileUriWithExtension = $blobContainerCredJsonFile
    }
    WriteTo-File @bccSplat

    Write-Host "Storage container credential has been saved to '$($blobContainerCredJsonFile)' `r`n"
}

if ($blobContainerCredential.Count -lt 3) {
    Write-Error "One or more storage container information is missing, terminating script execution"
    return
}



#
# ----------------------------------------------------------------------------------------------------
# KeyVault and Key
# ----------------------------------------------------------------------------------------------------
#

#
# Create KeyVault if does not exist
#
$kvSplat = @{
    VaultName         = $kvName
    ResourceGroupName = $rgName
}

$kv = $null
try {
    $kv = Get-AzKeyVault @kvSplat -ErrorAction Stop
}
catch {
    Write-Warning $Error[0]
}

if ($null -eq $kv) {

    #
    # Check if KeyVault is soft deleted
    # if soft deleted -> purge it and create new KeyVault with same name
    #
    $softDeleted = $false

    $softDeletedVaults = Get-AzKeyVault -InRemovedState  # fetch all deleted vaults in the current subscription

    if ($softDeletedVaults.Count -gt 0) {
        $softDeletedVault = $softDeletedVaults.Where{ $_.Name -like $kvName }
        $softDeleted = $null -ne $softDeletedVault
    }

    if ($softDeleted) {

        Write-Host "Vault '$($kvName)' is currently in 'deleted but recoverable' state `r`n"

        $kvRmSplat = @{
            VaultName = $kvName
            Location  = $rgLocation
        }

        try {

            #
            # Purge soft deleted vault
            #
            Remove-AzKeyVault @kvRmSplat -InRemovedState -Force | Out-Null
            Start-Sleep -Seconds 2

            Write-Host "Vault '$($kvName)' was purged `r`n"
        }
        catch {
            Write-Warning $Error[0]
        }
    }
    else {

        Write-Host "Vault '$($kvName)' does not exist `r`n"
    }

    #
    # Create vault
    #
    $kvNewSplat = @{
        Name                      = $kvName
        ResourceGroupName         = $rgName
        Location                  = $rgLocation
        Sku                       = 'Standard'
        SoftDeleteRetentionInDays = 7
    }

    Write-Host "Creating new vault... `r`n"

    $kv = New-AzKeyVault @kvNewSplat -EnableRbacAuthorization

    Write-Host "KeyVault '$($kvName)' has been created `r`n"
}
else {

    Write-Host "KeyVault '$($kvName)' already exists `r`n"
}
$kvResourceId = $kv.ResourceId # will be used as scope to assign role


#
# Assign role to current user so that current user can create key
#
$userObjectId = Get-LoggedInUserObjectId
$userRaSplat = @{
    RoleDefinitionName = $kvRoleLoggedInUser
    ObjectId           = $userObjectId
    Scope              = $kvResourceId
}
New-RoleAssignement @userRaSplat


#
# Assign role to service principal so that service principal can access key (RBAC -> vault access policy)
#
$spRaSplat = @{
    RoleDefinitionName = $kvRoleServicePrincipal
    ObjectId           = $spObjectId
    Scope              = $kvResourceId
}
New-RoleAssignement @spRaSplat


#
# Create key (this key will be used to encrypt/decrypt Pulumi secrets)
#
$kvKey = $null
try {
    $kvKey = Get-AzKeyVaultKey -VaultName $kvName -Name $kvKeyName -ErrorAction Stop
}
catch {
    Write-Warning $Error[0]
}

if ($null -eq $kvKey) {

    $softDeleted = $false

    $softDeletedKeys = Get-AzKeyVaultKey -VaultName $kvName -InRemovedState
    if ($softDeletedKeys.Count -gt 0) {
        $softDeletedKey = $softDeletedKeys.Where{ $_.Name -like $kvKeyName }
        $softDeleted = $null -ne $softDeletedKey
    }

    if ($softDeleted) {

        Write-Host "Key '$($kvKeyName)' is currently in 'deleted but recoverable' state `r`n"

        try {

            $rmKeySplat = @{
                Name      = $kvKeyName
                VaultName = $kvName
            }
            Remove-AzKeyVaultKey @rmKeySplat -InRemovedState -Force

            Write-Host "'$($kvKeyName)' was purged. Creating new key with same name... `r`n"
            Start-Sleep -Seconds 3

            $kvKey = (Add-AzKeyVaultKey -VaultName $kvName -Name $kvKeyName -Destination "Software")

            Write-Host "New key '$($kvKeyName)' has been created `r`n"

        }
        catch {
            Write-Warning $Error[0]
        }
    }
    else {

        Write-Host "Key '$($kvKeyName)' does not exist, creating new... `r`n"

        $kvKey = (Add-AzKeyVaultKey -VaultName $kvName -Name $kvKeyName -Destination "Software")

        Write-Host "Key '$($kvKeyName)' has been created `r`n"
    }
}
else {
    
    Write-Host "Key '$($kvKeyName)' already exists `r`n"
}

if ($null -eq $kvKey) {
    Write-Error "Failed to create key '$($kvKeyName)'. Terninating script execution"
    return
}

#$kvKeyId = $kvKey.Id




#
# ----------------------------------------------------------------------------------------------------
# Environment variables for Pulumi
# ----------------------------------------------------------------------------------------------------
#
$envVars = $spCredential + $pulumiCredential + $blobContainerCredential

Write-Host "Setting environment variables..."
foreach ($key in $envVars.keys) {
    [Environment]::SetEnvironmentVariable($key, $envVars[$key], "User")
}
Write-Host "All environment variables has been set `r`n"




#
# ----------------------------------------------------------------------------------------------------
# Generate Pulumi commands
# ----------------------------------------------------------------------------------------------------
#

#
# Now, create Pulumi commands for using Custom Backend (Azure KeyVault and Storage Container)
#

#
# START: pulumi commands multi-line string ----------------------------------------------------------------------
$pulumiCmdStr = @"

#
# Restart PowerShell / VS Code
# (Make sure that Pulumi fetches latest Environments Variables)
#
# Set-Location `$PSScriptRoot
#./$pulumiCmdPowerShellFile
# pulumi stack rm --stack=$pulumiStackName --yes #--preserve-config
#

#
# Set variables for pulumi command
#
`$blobContainerName = '$blobContainerName'
`$resourceGroupLocation = '$rgLocation'
`$kvName = '$kvName'
`$kvKeyName = '$kvKeyName'

`$solutionName = '$solutionName' # VS solution
`$pulumiProjectName = '$pulumiProjectName' # Pulumi project -> VS project (will be added to VS solution)
`$pulumiProjectDescription = '$pulumiProjectDescription'
`$pulumiStackName = '$pulumiStackName'


#
# Create solution directory and VS solution
#
if (Test-Path .\`$solutionName) {
    # Clean up folder
    Remove-Item .\`$solutionName\* -Force -Recurse  | Out-Null
} else {
    # Create folder
    New-Item -Path .\`$solutionName -ItemType Directory | Out-Null
}
Set-Location `$solutionName
dotnet new sln --name `$solutionName

#
# Pulumi login to Azure (blob container as stack state backend)
#
pulumi login azblob://`$blobContainerName

#
# Pulumi new project (Azure KeyVault key for pulumi secret)
#
pulumi new azure-csharp ``
    --stack="`$pulumiStackName" ``
    --secrets-provider="azurekeyvault://`$kvName.vault.azure.net/keys/`$kvKeyName" ``
    --name="`$pulumiProjectName" ``
    --description="`$pulumiProjectDescription" ``
    --dir="`$pulumiProjectName" ``
    --config="azure-native:location=`$resourceGroupLocation" ``
    --force

#
# Add pulumi project to VS solution
#
dotnet sln add ./`$pulumiProjectName/`$pulumiProjectName.csproj


#
# Deploy pulumi stack to Azure (target subscription)
#

# Prevent Pulumi from using Azure CLI token -> logout from all accounts
az account clear
az logout

Set-Location `$pulumiProjectName

pulumi up #--yes

"@
# END: pulumi commands multi-line string -----------------------------------------------------------------------
#


#
# Save Pulumi commands to .ps1 file
#
Write-Host "Saving Pulumi commands to '$($pulumiCmdPowerShellFile)' `r`n"

$pulumiCmdSplat = @{
    Content              = $pulumiCmdStr
    FileUriWithExtension = $pulumiCmdPowerShellFile
}
WriteTo-File @pulumiCmdSplat



#
# Restart PowerShell / VS Code
# (Make sure that Pulumi fetches latest Environments Variables)
#

Next