Microsoft.PowerPlatform.DevOps.psm1

Write-Verbose "Importing Functions"

# Import everything in these folders
foreach ($folder in @('Private', 'Public', 'Classes')) {
    
    $root = Join-Path -Path $PSScriptRoot -ChildPath $folder
    if (Test-Path -Path $root) {
        Write-Verbose "processing folder $root"
        $files = Get-ChildItem -Path $root -Filter *.ps1

        # dot source each file
        $files | where-Object { $_.name -NotLike '*.Tests.ps1' } | 
        ForEach-Object { Write-Verbose $_.name; . $_.FullName }
    }
}

$setupTools = Join-Path -Path $PSScriptRoot -ChildPath "FrameworkTemplate\Solutions\Scripts\_SetupTools.ps1"
. $setupTools

if (!(Test-Path "$env:APPDATA\Microsoft.PowerPlatform.DevOps")) {
    New-Item -Path "$env:APPDATA\Microsoft.PowerPlatform.Devops" -ItemType Directory
}
if (!(Test-Path "$env:APPDATA\Microsoft.PowerPlatform.DevOps\devopsConfig.json")) {
    Copy-Item (Join-Path $PSScriptRoot "\devopsConfig.json") -Destination "$env:APPDATA\Microsoft.PowerPlatform.DevOps\devopsConfig.json"
}

function Install-PreReqs {
    $message = "Checking Pre-requisites"
    Write-Host $message

    try {
        Invoke-InstallPreRequisites
        $global:devops_configFile.PreReqsComplete = "True"
    }
    catch {
        $global:devops_configFile.PreReqsComplete = "Error"
        pause
    }
    $global:devops_configFile | ConvertTo-Json | Set-Content ("$env:APPDATA\Microsoft.PowerPlatform.DevOps\devopsConfig.json")
   
}

function Connect-AzureDevOps {
    $message = "Configuring Azure DevOps"
    Write-Host $message

    try {
        Invoke-AzureDevOps
        $global:devops_projectFile.ADOConfigured = "True"
    }
    catch {
        $global:devops_projectFile.ADOConfigured = "Error"
        pause
    }
    $global:devops_projectFile | ConvertTo-Json | Set-Content ("$global:devops_projectLocation\$global:devops_gitRepo.json")  
   
}

function Add-Solution {
    $message = "Adding D365 / CDS Solution"
    Write-Host $message

    try {
        Invoke-AddSolution
        $global:devops_projectFile.SolutionAdded = "True"
    }
    catch {
        $global:devops_projectFile.SolutionAdded = "Error"
        pause
    }
    $global:devops_projectFile | ConvertTo-Json | Set-Content ("$global:devops_projectLocation\$global:devops_gitRepo.json") 
   
}

function Add-CICDEnvironment {
    $message = "Configuring CI/CD Environment"
    Write-Host $message

    try {
        $CreateOrSelectEnv = Read-Host -Prompt "CI/CD Environment : Would you like to [P]rovision a new Power Platform Environment ? or [S]elect an Existing One (Default [S])"
        if ($CreateOrSelectEnv -eq "P") {
            Add-Environment
            Invoke-ConfigureCICD
        }
        else {
            Invoke-ConfigureCICD
        }
    }
    catch {
        $global:devops_projectFile.CICDEnvironmentName = "Error"
        pause
    }
    $global:devops_projectFile | ConvertTo-Json | Set-Content ("$global:devops_projectLocation\$global:devops_gitRepo.json") 
   
}

function Add-Project {
    if ($global:devops_configFile.Projects.Count -gt 1 -or $global:devops_configFile.Projects[0].ID -ne "placeholder") {
        $message = "Adding PowerPlatform DevOps Project"
        $options = "Select and Existing Project", "Browse for a local Repository", "Create a new Project", "Clone an existing Repo", "Quit"
        do {
            $sel = Invoke-Menu -MenuTitle "---- $message ------" -MenuOptions $options
        } until ($sel -ge 0)

        switch ($sel) {
            '0' { Select-GitRepo }
            '1' { Get-GitRepo }
            '2' { Add-GitRepo }
            '3' { Clone-GitRepo }
            '4' { return }
        }
    }
    else {
        Add-GitRepo
    }
    try {
    }
    catch {
        $global:devops_projectFile.SolutionAdded = "Error"
        pause
    }
    $global:devops_projectFile | ConvertTo-Json | Set-Content ("$global:devops_projectLocation\$global:devops_gitRepo.json") 
    $global:devops_configFile | ConvertTo-Json | Set-Content ("$env:APPDATA\Microsoft.PowerPlatform.DevOps\devopsConfig.json")
   
}

function Export-Solution {
    if ($global:devops_projectFile.CDSSolutions.Count -gt 0) {
        $options = $global:devops_projectFile.CDSSolutions | ForEach-Object { $_.SolutionName }

        do {
            $sel = Invoke-Menu -MenuTitle "---- Please Select the Solution to Export and Unpack ------" -MenuOptions $options
            $selectedSolution = $global:devops_projectFile.CDSSolutions[$sel].SolutionName
        } until ($selectedSolution -ne "")

        Write-Host $selectedSolution
        Write-Host $global:devops_projectLocation
        Set-Location -Path "$global:devops_projectLocation\$selectedSolution\Scripts"
        if ($global:devops_projectFile.ADOConfigured -eq "True") {
            try {
                git pull origin
            }
            catch {
                pause
            }            
        }
        & .\SolutionExport.ps1 -DevMode $global:devops_devMode
        Set-Location -Path $global:devops_projectLocation
    }
}

function Sync-Solution {
    $commitMessage = Read-Host "Enter a description for your Commit"
    git add -A
    git commit -m $commitMessage    
    if ($global:devops_projectFile.ADOConfigured -eq "True") {
        git push origin
    }
    pause
}

function Show-SolutionChanges {
    git status
    pause
}

function Enable-AzureDeploy {
    if ($global:devops_projectFile.ARMAdded -ne "True") {
        Copy-Item -Path (Join-Path $PSScriptRoot Snippets\AzureResources\.) -Destination $global:devops_projectLocation -Recurse -Force
        $buildYAML = Get-Content -Path "$global:devops_projectLocation\Build.yaml"
        $azureYAML = Get-Content -Path  (Join-Path $PSScriptRoot Snippets\AzureDeploy.yaml)
        $azureYAML = $azureYAML.Replace('replaceRepo', $global:devops_projectFile.gitRepo)
        $buildYAML + $azureYAML | Set-Content -Path  "$global:devops_projectLocation\Build.yaml"

        $azureParams = Get-Content -Path "$global:devops_projectLocation\AzureResources\azuredeploy.parameters.json"
        $azureParams = $azureParams.Replace('AddName', $global:devops_projectFile.gitRepo)
        $azureParams |  Set-Content -Path "$global:devops_projectLocation\AzureResources\azuredeploy.parameters.json"

        $adoOrg = $global:devops_projectFile.ADOOrgName
        $adoProject = $global:devops_projectFile.ADOProject
        $adoRepo = $global:devops_projectFile.gitRepo.ToLower()
        if ($adoRepo.Length -gt 12) {
            $adoRepoShort = $adoRepo.Substring(0, 12)
        }
        else {
            $adoRepoShort = $adoRepo
        }
        $varGroupAzure = az pipelines variable-group create --organization https://dev.azure.com/$adoOrg --project $adoProject --name "$($global:devops_projectFile.gitRepo).AzureEnvironment"  --variables AzureResourceGroup="" --authorize $true | ConvertFrom-Json
        az pipelines variable-group variable create --organization https://dev.azure.com/$adoOrg --project $adoProject --name AzureAppInsightsName --value "$adoRepo-staging-ai" --group-id $varGroupAzure.id
        az pipelines variable-group variable create --organization https://dev.azure.com/$adoOrg --project $adoProject --name AzureLocation --value "australiaeast" --group-id $varGroupAzure.id
        az pipelines variable-group variable create --organization https://dev.azure.com/$adoOrg --project $adoProject --name AzureStorageAccountName --value "$($adoRepo)staging" --group-id $varGroupAzure.id
        az pipelines variable-group variable create --organization https://dev.azure.com/$adoOrg --project $adoProject --name AzureFunctionAppName --value "$adoRepo-staging-fna" --group-id $varGroupAzure.id
        az pipelines variable-group variable create --organization https://dev.azure.com/$adoOrg --project $adoProject --name AzureWebAppName --value "$adoRepo-staging-wba" --group-id $varGroupAzure.id
        az pipelines variable-group variable create --organization https://dev.azure.com/$adoOrg --project $adoProject --name WorkspaceName --value "$adoRepo-staging-log" --group-id $varGroupAzure.id
        az pipelines variable-group variable create --organization https://dev.azure.com/$adoOrg --project $adoProject --name AzureKeyVaultName --value "$adoRepoShort-staging-kv" --group-id $varGroupAzure.id
        $global:devops_projectFile.ARMAdded = "True"
        $global:devops_projectFile | ConvertTo-Json | Set-Content ("$global:devops_projectLocation\$global:devops_gitRepo.json") 
        Write-Host "Please make sure you follow the instructions for configuring a Service Connection in Azure DevOps, as detailed in $global:devops_projectLocation\AzureResources\Instructions.md" -ForegroundColor Yellow
        Write-Host "Press Enter to Continue..." -ForegroundColor White
        pause
    }
    else {
        Write-Host "Azure Deployment Already Enabled for this Project"
        pause
    }   
}

function Enable-FunctionApp {
    if ($global:devops_projectFile.ARMAdded -eq "True") {
        if ($global:devops_projectFile.FunctionAppAdded -ne "True") {
            Copy-Item -Path (Join-Path $PSScriptRoot Snippets\FunctionApp\.) -Destination $global:devops_projectLocation -Recurse -Force
            dotnet sln $global:devops_gitRepon.sln add FunctionApp\FunctionApp.csproj

            $buildYAML = Get-Content -Path "$global:devops_projectLocation\Build.yaml"
            $azureYAML = Get-Content -Path  (Join-Path $PSScriptRoot Snippets\FunctionAppDeploy.yaml)
            $azureYAML = $azureYAML.Replace('replaceRepo', $global:devops_projectFile.gitRepo)
            $buildYAML + $azureYAML | Set-Content -Path  "$global:devops_projectLocation\Build.yaml"

            $global:devops_projectFile.FunctionAppAdded = "True"
            $global:devops_projectFile | ConvertTo-Json | Set-Content ("$global:devops_projectLocation\$global:devops_gitRepo.json") 
        }
        else {
            Write-Host "Function App Already Enabled for this Project"
            pause
        } 
    }
    else {
        Write-Host "Function App Requires that you first Enable Azure Resource Management Deployment"
        pause
    }  
}

function Invoke-OpenSolution {
    . "$global:devops_projectLocation\$global:devops_gitRepo.sln"
}

function Show-Menu {
    param (
        [string]$Title = 'Power Platform DevOps'
    )
    $devopsConfigMessage = "(ADO Org : $($global:devops_projectFile.ADOOrgName) | ADO Project : $($global:devops_projectFile.ADOProject) | git Repo : $($global:devops_projectFile.gitRepo))"
    $CICDConfigMessage = "(CI/CD Environment : $($global:devops_projectFile.CICDEnvironmentName) | CI/CD URL : $($global:devops_projectFile.CICDEnvironmentURL))"
    if ($global:devops_projectFile -eq "False") {
        $Title = "Power Platform DevOps (No Project Selected)"
    }
    else {
        $Title = "Power Platform DevOps ($($global:devops_projectFile.gitRepo))"
        $global:devops_projectTitle = $global:devops_projectFile.gitRepo
        Set-Location $global:devops_projectLocation
    }
    $global:devops_message = @"
 
    Welcome to the Power Platform DevOps script.
     
     ver: $global:devops_version (latest available version : $latestVersion)
     project: $global:devops_projectTitle
     repo : $global:devops_gitRepo
     azure subscription : $global:devops_selectedSubscriptionName ($global:devops_selectedSubscription)
     azure keyvault : $global:devops_AzureKeyVault
     tenant id : $global:devops_TenantID
     client id : $global:devops_ClientID
     
"@

    [console]::ForegroundColor = "White"
    Clear-Host
    Write-Host $global:devops_logo -ForegroundColor Magenta
    Write-Host $global:devops_message -ForegroundColor White
    $Repeater = "=" * $Title.Length
    Write-Host "================ $Title ================" -ForegroundColor White
    
    Write-Host "1: Run Pre-requisite checks (Install / Update)." -ForegroundColor (Set-ColourOption $global:devops_configFile.PreReqsComplete)
    if (($global:devops_configFile.PreReqsComplete -eq "True")) {
        Write-Host "2: Create a New Project or Select Existing" -ForegroundColor (Set-ColourOption $global:devops_projectFile)    
    }    
    if (!($global:devops_projectFile -eq "False") -and ($global:devops_configFile.PreReqsComplete -eq "True")) {
        Write-Host "3: Configure Azure DevOps" $devopsConfigMessage -ForegroundColor (Set-ColourOption $global:devops_projectFile.ADOConfigured)
        Write-Host "4: Add New D365 / CDS Solution." -ForegroundColor (Set-ColourOption $global:devops_projectFile.SolutionAdded)
        if ($global:devops_projectFile.ADOConfigured -eq "True") {
            Write-Host "5: Configure Continuous Deployment" $CICDConfigMessage -ForegroundColor (Set-ColourOption $global:devops_projectFile.CICDEnvironmentName)        
        }
        Write-Host "A: Enable [A]zure Resource Management Deployment." -ForegroundColor (Set-ColourOption $global:devops_projectFile.ARMAdded)
        Write-Host "F: Add Azure [F]unction App Project." -ForegroundColor (Set-ColourOption $global:devops_projectFile.FunctionAppAdded)
        if ($global:devops_projectFile.SolutionAdded -eq "True") {
            Write-Host "D: Add Additional [D]365 / CDS Solutions" -ForegroundColor Cyan
            Write-Host "E: [E]xport & Unpack Solution to Source Control" -ForegroundColor Cyan
        } 
        if ($global:devops_projectFile.ADOConfigured -eq "True") {       
            Write-Host "S: Commit and [S]ync changes to Source Control" -ForegroundColor Cyan
            Write-Host "V: [V]iew current change log for Source Control" -ForegroundColor Cyan
    
            if ($global:devops_projectFile.CICDEnvironmentName -ne "False") {
                Write-Host "T: Add Additional Deployment Environment [T]arget" -ForegroundColor Cyan
            }
        }
        Write-Host "O: [O]pen Project in Visual Studio" -ForegroundColor Cyan        
    }
    Write-Host "C: Advanced [C]onfiguration Management" -ForegroundColor Yellow
    Write-Host "U: Check for [U]pdates to Microsoft.PowerPlatform.DevOps" -ForegroundColor Cyan
    Write-Host "Q: Press 'Q' to quit." -ForegroundColor White
    Write-Host "=================$Repeater=================" -ForegroundColor White
    Write-Host ""
    Write-Host "* (White items still need to be completed)" -ForegroundColor White
    Write-Host "* (Green items are successful)" -ForegroundColor Green
    Write-Host "* (Red items have Errors)" -ForegroundColor Red
    Write-Host "* (Purple items are optional)" -ForegroundColor Magenta
    Write-Host "* (Blue Items are used for Day-to-Day ALM)" -ForegroundColor Cyan
    Write-Host ""
    if ($latestVersion -gt $global:devops_version) {
        Write-Host "There is a newer version available, please run Update-Module Microsoft.PowerPlatform.DevOps to update" -ForegroundColor Yellow
        Write-Host ""
    }
}

function Show-ConfigMenu {
    param (
        [string]$Title = 'Power Platform DevOps'
    )
    $devopsConfigMessage = "(ADO Org : $($global:devops_projectFile.ADOOrgName) | ADO Project : $($global:devops_projectFile.ADOProject) | git Repo : $($global:devops_projectFile.gitRepo))"
    $CICDConfigMessage = "(CI/CD Environment : $($global:devops_projectFile.CICDEnvironmentName) | CI/CD URL : $($global:devops_projectFile.CICDEnvironmentURL))"
    if ($global:devops_projectFile -eq "False") {
        $Title = "Power Platform DevOps (No Project Selected)"
    }
    else {
        $Title = "Power Platform DevOps ($($global:devops_projectFile.gitRepo))"
        $global:devops_projectTitle = $global:devops_projectFile.gitRepo
        Set-Location $global:devops_projectLocation
    }
    $global:devops_message = @"
 
    Power Platform DevOps Configuration.
     
     ver: $global:devops_version (latest available version : $latestVersion)
     project: $global:devops_projectTitle
     repo : $global:devops_gitRepo
     azure subscription : $global:devops_selectedSubscriptionName ($global:devops_selectedSubscription)
     azure keyvault : $global:devops_AzureKeyVault
     tenant id : $global:devops_TenantID
     client id : $global:devops_ClientID
     
"@

    [console]::ForegroundColor = "White"
    Clear-Host
    Write-Host $global:devops_logo -ForegroundColor Magenta
    Write-Host $global:devops_message -ForegroundColor White
    $Repeater = "=" * $Title.Length
    Write-Host "================ $Title ================" -ForegroundColor White
    
    Write-Host "G: Edit [G]lobal Config File" -ForegroundColor Yellow
    if ($global:devops_projectFile -ne "False") {

        Write-Host "P: Edit [P]roject Config File" -ForegroundColor Yellow
        Write-Host "A: Setup [A]zure KeyVault" -ForegroundColor Cyan
        Write-Host "C: [C]onfigure Service Principal" -ForegroundColor Cyan
    }    
    Write-Host "Q: Press 'Q' to return to Main Menu." -ForegroundColor White
    Write-Host "=================$Repeater=================" -ForegroundColor White
    Write-Host ""
    $configSelection = Read-Host "Selection"
    switch ($configSelection) {
        'G' {
            . $env:APPDATA\Microsoft.PowerPlatform.DevOps\devopsConfig.json
        } 'P' {
            . $global:devops_projectLocation\$global:devops_gitRepo.json
        } 'A' {
            Configure-AzureKeyVault
        } 'C' {
            Configure-ServicePrincipal
        }
    }
}

function Invoke-PowerPlatformDevOps {
    Param(
        [string] [Parameter(Mandatory = $false)] $PreLoadProjectPath = "",
        [string] [Parameter(Mandatory = $false)] $PreLoadedProjectName = "",
        [string] [Parameter(Mandatory = $false)] $PreSelection = ""
    )

    $global:devops_configFile = Get-Content ("$env:APPDATA\Microsoft.PowerPlatform.DevOps\devopsConfig.json") | ConvertFrom-Json
    [string]$getVersion = Get-ManifestValue (Join-Path $PSScriptRoot "\Microsoft.PowerPlatform.DevOps.psd1") -PropertyName 'ModuleVersion' -Passthru
    [version]$global:devops_version = $getVersion.Replace("'", "")
    $global:devops_projectFile = "False"
    $global:devops_gitRepo = ""
    $global:devops_projectLocation = ""
    $global:devops_continue = $true
    $global:devops_devMode = $false

    if ($global:devops_projectFile -eq "False") {
        $global:devops_projectTitle = "(No Project Selected)"
        $global:devops_gitRepo = "(No Project Selected)"
        $global:devops_selectedSubscriptionName = "(No Project Selected)"        
        $global:devops_AzureKeyVault = "(No Project Selected)"
        $global:devops_ClientID = "(No Project Selected)"
        $global:devops_TenantID = "(No Project Selected)"

    }
    else {
        $global:devops_projectTitle = $global:devops_projectFile.gitRepo
        $global:devops_selectedSubscription = $global:devops_projectFile.selectedSubscription
        $global:devops_selectedSubscriptionName = $global:devops_projectFile.selectedSubscriptionName
        $global:devops_AzureKeyVault = $global:devops_projectFile.AzureKeyVaultName
        $global:devops_ClientID = $global:devops_projectFile.ClientID
    }

    $regKey = "hklm:/software/microsoft/windows nt/currentversion"
    $global:devops_Core = (Get-ItemProperty $regKey).InstallationType -eq "Server Core"

    $global:devops_logo = @"
 ____ ____ _ _ __ ____ ___
| _ \ _____ _____ _ __ | _ \| | __ _| |_ / _| ___ _ __ _ __ ___ | _ \ _____ __/ _ \ _ __ ___
| |_) / _ \ \ /\ / / _ \ '__| | |_) | |/ _` | __| |_ / _ \| '__| '_ ` _ \ | | | |/ _ \ \ / / | | | '_ \/ __|
| __/ (_) \ V V / __/ | | __/| | (_| | |_| _| (_) | | | | | | | | | |_| | __/\ V /| |_| | |_) \__ \
|_| \___/ \_/\_/ \___|_| |_| |_|\__,_|\__|_| \___/|_| |_| |_| |_| |____/ \___| \_/ \___/| .__/|___/
                                                                                                  |_|
 
"@


    if ($PreLoadProjectPath -ne "") {
        if ((Test-Path -Path "$PreLoadProjectPath\$PreLoadedProjectName.json")) {
            $global:devops_projectLocation = "$PreLoadProjectPath"
    
            $global:devops_projectFile = Get-Content ("$PreLoadProjectPath\$PreLoadedProjectName.json") | ConvertFrom-Json
            $global:devops_gitRepo = $global:devops_projectFile.gitRepo
            Set-Location  $global:devops_projectLocation
        }  
    }
    Write-Host ""
    [console]::ForegroundColor = "White"
    do {
        $global:devops_configFile = Get-Content ("$env:APPDATA\Microsoft.PowerPlatform.DevOps\devopsConfig.json") | ConvertFrom-Json
        if ($global:devops_projectFile -ne "False") {
            Set-Location $global:devops_projectLocation
        }
        Show-Menu

        if ($PreSelection -ne "") {
            $selection = $PreSelection
            $global:devops_devMode = $true
            $PreSelection = ""
            Write-Host "Performing your Automated Selection ..."
            Start-Sleep -Seconds 2
        }
        else {
            $selection = Read-Host "Please make a selection"            
        }
        switch ($selection) {
            '1' {
                Install-PreReqs
            } '2' {
                Add-Project
            } '3' {
                Connect-AzureDevOps
            } '4' {
                Add-Solution
            } '5' {
                Add-CICDEnvironment
            } 'D' {
                Add-Solution
            } 'E' {
                Export-Solution
            } 'S' {
                Sync-Solution
            } 'V' {
                Show-SolutionChanges
            } 'T' {
                $CreateOrSelectEnv = Read-Host -Prompt "CI/CD Environment : Would you like to [P]rovision a new Power Platform Environment ? or [S]elect an Existing One (Default [S])"
                if ($CreateOrSelectEnv -eq "P") {
                    Add-Environment
                    Invoke-AddEnvironments
                }
                else {
                    Invoke-AddEnvironments
                }                
            } 'A' {
                Enable-AzureDeploy
            } 'F' {
                Enable-FunctionApp
            } 'U' {
                Write-Host "Checking for updated versions...."
                $latestVersion = (Find-Module Microsoft.PowerPlatform.DevOps -Repository "PSGallery").Version
                if ($latestVersion -gt $global:devops_version) {
                    Update-Module Microsoft.PowerPlatform.DevOps    
                    Write-Host "Updated to $latestVersion, please restart the tool for it to take effect."                
                }
            } 'O' {
                Invoke-OpenSolution
            } 'C' {
                Show-ConfigMenu
            }
        }
        Write-Host ""
    }
    until ($selection -eq 'q')
    Clear-Variable devops_* -Scope Global
    Clear-Host
    return    
    #Stop-Process -Id $PID
}

Export-ModuleMember -Function 'Invoke-PowerPlatformDevOps'
#, 'Add-CICDEnvironment', 'Add-Solution', 'Connect-AzureDevOps', 'Install-PreReqs'