RDG.Deployment.Utils.psm1

function Update-RDGDeploymentUtils {
  $moduleName = "RDG.Deployment.Utils"
  $repository = "PSGallery"

  # Check if the module is installed
  $installedModule = Get-Module -Name $moduleName -ListAvailable | Sort-Object Version -Descending | Select-Object -First 1

  if ($installedModule) {
    # Get information about the latest version of the module available in the repository
    $latestModule = Find-Module -Name $moduleName -Repository $repository | Sort-Object Version -Descending | Select-Object -First 1

    if ($latestModule.Version -gt $installedModule.Version) {
      # An update is available
      Write-Host "Updating $moduleName to version $($latestModule.Version)..."
      Update-Module -Name $moduleName -Force -Scope CurrentUser
      Write-Host "$moduleName has been updated to version $($latestModule.Version)."
    }
    else {
      Write-Host "$moduleName is already up to date (Version $($installedModule.Version))."
    }
  }
  else {
    # The module is not installed
    Write-Host "$moduleName is not installed. Installing it..."
    Install-Module -Name $moduleName -Repository $repository -Scope CurrentUser -Force
    Write-Host "$moduleName has been installed."
  }
}

function New-AzAuthentication {
  param(
    [string]$subscriptionId
  )


  Show-Title -title "Authenticate with Azure" -color "Yellow"
  if ($null -ne $env:AGENT_ID -and $null -ne $env:SYSTEM_TASKDEFINITIONSURI) {
    # Having the $env:AGENT_ID and $env:SYSTEM_TASKDEFINITIONSURI variables set means we are running in Azure Pipeline.
    # Authentication with service principal is done by the AzureCli task in Azure Pipeline
    Write-Host "Using service principal authentication"
  }
  else {
    # User login
    Write-Host "Using user authentication"
    $currentSubscriptionId = az account show --query "id" -o tsv
    if ($currentSubscriptionId -ne $subscriptionId) {
      az login | Out-Null
    }
  }

  az account set --subscription $subscriptionId | Out-Null
  
  $azAccountName = az account show --query 'name' --output tsv
  Set-AzContext -SubscriptionName $azAccountName
  
  Write-Host "Authenticated!" -ForegroundColor "Green"
}

function Get-Scripts {
  param(
    [bool]$delta,
    [string]$manifestPath,
    [string]$stack,
    [string]$environment
  )
  $manifest = Get-Content -Raw -Path $manifestPath | ConvertFrom-Json
  if ($delta) {
    $changedScripts = Get-Changed -stack $stack -environment $environment
    $scripts = $manifest.deploy.scripts | Where-Object { $_ -in $changedScripts }
  }
  else {
    $scripts = $manifest.deploy.scripts
  }

  return $scripts
}

function Get-Changed {
  param(
    [string]$type = 'deploy',
    [string]$stack,
    [string]$environment
  )
  # Get the root folder of your Git repository
  $repoRoot = git rev-parse --show-toplevel

  # Get the commit hashes for the current and previous commits
  #-----------------------------------------------------------------------------------

  $currentCommit = git rev-parse HEAD
  $previousCommit = git rev-parse HEAD^ # Get previous tag instead of commit -1

  # Define the prefix of the Git tag you want to search for
  $tagPrefix = "$stack-$environment*"
  # Get a list of all tags that start with the specified prefix
  $matchingTags = git tag --list $tagPrefix

  # If there are matching tags, find the most recent commit hash among them
  if ($matchingTags.Count -gt 0) {
    $previousCommit = $null
    $mostRecentCommitDate = [datetime]::MinValue

    foreach ($tag in $matchingTags) {
      $commitHash = git rev-list -n 1 $tag
      $commitDate = git show -s --format="%ci" $commitHash | Get-Date

      if ($commitDate -gt $mostRecentCommitDate) {
        $mostRecentCommitDate = $commitDate
        $previousCommit = $commitHash
      }
    }
  }
  else {
    Write-Host "No tags found with prefix '$tagPrefix' check if full deploy is needed."
    exit 1
  }

  #-----------------------------------------------------------------------------------
  # Get the list of modified files between the current and previous commits
  $changedFiles = git diff --name-only $previousCommit $currentCommit

  # Get the distinct folder paths from the changed files
  $changedFolders = $changedFiles | ForEach-Object { Split-Path -Parent $_ } | Get-Unique

  # Filter the changed folders to include only the ones containing deploy.ps1
  $folder = $changedFolders | Where-Object { Test-Path (Join-Path -Path $repoRoot -ChildPath ($_ + "\$type.ps1")) }

  # Get the relative paths of deploy.ps1 scripts within the deploy folders and their subfolders
  $changedScripts = $folder | Get-ChildItem -Recurse -Filter "$type.ps1" -File | ForEach-Object {
    $_.FullName.Substring($repoRoot.Length + 1)
  }

  if ($changedScripts) {
    # Normalize slashes in paths between operating systems
    $changedScripts = $changedScripts.Replace('\', '/')
  }

  return $changedScripts
}

function Get-LocationShort {
  param(
    [string]$location
  )

  $locations = @{
    westeurope = 'weu'
  }

  if (!$locations.ContainsKey($location)) {
    Write-Host "Location '$location' does not have a shortname available"
    exit 1
  }

  return $locations[$location];
}

function Show-Title {
  param(
    [string]$title = "",
    [string]$color = "Cyan",
    [string]$edge = "="
  )

  $line = $edge * 75
  Write-Host ""
  Write-Host $line -ForegroundColor $color
  Write-Host " $title" -ForegroundColor $color
  Write-Host $line -ForegroundColor $color
}

function Block-Start {
  param(
    [string]$title = "",
    [string]$color = "Yellow",
    [string]$edge = "="
  )

  $separator = $edge * 75
  Write-Host $separator
  Write-Host " $title" -ForegroundColor $color
  Write-Host ""
}

function Block-End {
  param(
    [string]$title = "",
    [string]$color = "Green",
    [string]$edge = "="
  )

  $separator = $edge * 75
  Write-Host ""
  Write-Host " $title" -ForegroundColor $color
  Write-Host $separator
}

function Invoke-Script {
  param(
    [string]$scriptPath,
    [string]$location,
    [string]$stack,
    [string]$environment,
    [string]$subscriptionId
  )

  Show-Title -title "Running $scriptPath ($environment, $stack, $location)" -color "Yellow"

  try {
    & $scriptPath -location $location -stack $stack -environment $environment -subscriptionId $subscriptionId
    Write-Host "Success $scriptPath" -ForegroundColor "Green"
  }
  catch {
    Write-Host "Failed $scriptPath with error: $_" -ForegroundColor "Red"
    throw $_
  }
}

function Set-GitTag {
  param(
    [string]$stack,
    [string]$environment
  )
  $commitHash = git rev-parse HEAD
  $timestamp = Get-Date -Format "yy-MM-dd_HH.mm.ss"
  $tagPrefix = "$stack-$environment"
  $tag = "$tagPrefix-$timestamp"

  # Check if a tag already exists on the specified commit
  $existingTags = git tag -l

  foreach ($existingTag in $existingTags) {
    if ($existingTag -like "$tagPrefix*") {
      # If a tag exists with the specified prefix, remove it
      git tag -d $existingTag
      git push --delete origin $existingTag
    }
  }

  # Add the new tag
  git tag -a $tag -m "Tagging version $tag" $commitHash
  git push origin $tag
}

function New-ParameterFile {
  param(
    [string]$filename = 'parameter.json',
    [string]$location = './',
    [Hashtable]$parametersList
  )

  $parametersObject = @{
    '$schema'        = "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#"
    "contentVersion" = "1.0.0.0"
    "parameters"     = @{}
  }

  foreach ($paramName in $parametersList.Keys) {
    $paramValue = $parametersList[$paramName]

    $parametersObject["parameters"][$paramName] = @{
      "value" = $paramValue
    }
  }
  
  $path = $location + '/' + $filename
  Write-Host 'Path: '$path
  $parametersObject | ConvertTo-Json -Depth 100 | Set-Content -Path $path

  return $path
}

function Invoke-MainDeploy {
  param(
    [bool]$delta,
    [Parameter(Mandatory = $true)]
    [string]$manifest,
    [Parameter(Mandatory = $true)]
    [string]$location,
    [Parameter(Mandatory = $true)]
    [string]$subscriptionId,
    [Parameter(Mandatory = $true)]
    [string]$hubSubscriptionId,
    [Parameter(Mandatory = $true)]
    [string]$stack,
    [Parameter(Mandatory = $true)]
    [string]$environment
  )

  $scripts = Get-Scripts -delta $delta -manifest $manifest -stack $stack -environment $environment

  Show-Title -title "Deploying to $location" -color "DarkBlue"
  Write-Host "Subscription: $subscriptionId"
  Write-Host "Hub Subscription: $hubSubscriptionId"
  Write-Host "Stack: $stack"
  Write-Host "Environment: $environment"
  Write-Host "Scripts to run: $scripts"

  New-AzAuthentication -SubscriptionId $subscriptionId

  foreach ($script in $scripts) {
    Invoke-Script -scriptPath $script `
      -location $location `
      -locationShort $locationShort `
      -environment $environment `
      -stack $stack `
      -subscriptionId $subscriptionId `
      -hubSubscriptionId $hubSubscriptionId
  }

  Set-GitTag -stack $stack -env $environment
}

function Push-FunctionCode {
  param(
    [string]$csprojAbsolutePath,
    [string]$csProj,
    [string]$netVersion = "7.0",
    [string]$subscriptionId,
    [string]$resourceGroup,
    [string]$functionName,
    [string]$scriptPath
  )
  
  $functionIsRunning = $false

  $startTime = Get-Date
  $timeout = New-TimeSpan -Seconds 300

  # Check if function app exist/reachable.
  do {
    $functionIsRunning = "Running" -eq (az functionapp show --resource-group $resourceGroup --name $functionName --subscription $subscriptionId --query "state" -o tsv)
    Write-Host "Function state: $functionIsRunning"
    if (-not $functionIsRunning) {
      Start-Sleep -Seconds 5
    }
    
    if (-not $functionIsRunning -and (Get-Date) -gt ($startTime + $timeout)) {
      Write-Host "Script execution timed out after 300 seconds."
      break
    }
  } while (-not $functionIsRunning) 

  if (-not $functionIsRunning) {
    Write-Host "Function App $functionName, not reachable."
    throw [System.Management.Automation.ItemNotFoundException]
  }

  $originLocation = Get-Location

  # Step 1: Build Adapter Code
  Set-Location $csprojAbsolutePath
  $output = dotnet build $csProj -c Release
 
  # Step 2: Zip all files
  $zipFile = Join-Path $scriptPath "$csProj.zip"
  
  Get-ChildItem -Path ".\bin\Release\net$netVersion\*" -Force | Compress-Archive -DestinationPath $zipFile -Force

  Set-Location $scriptPath

  # Step 3: Deploy Code to Function App
  az functionapp deployment source config-zip `
    --resource-group $resourceGroup `
    --name $functionName `
    --src $zipFile `
    --subscription $subscriptionId

  Remove-Item -Path $zipFile
  Set-Location $originLocation
}

function Get-Configuration {
  param(
    [string]$stack,
    [string]$environment,
    [string]$locationShort,
    [string]$cachedConfigFileLocation = "./configuration.json",
    [int]$cacheExpirationMinutes = 10
  )

  # Check if cached data exists, is not expired, and has the same stack and environment
  if (
  (Test-Path $cachedConfigFileLocation) -and
  ((Get-Date) - (Get-Item $cachedConfigFileLocation).LastWriteTime).TotalMinutes -lt $cacheExpirationMinutes -and
  ((Get-Content -Raw -Path $cachedConfigFileLocation | ConvertFrom-Json).stack -eq $stack) -and
  ((Get-Content -Raw -Path $cachedConfigFileLocation | ConvertFrom-Json).environment -eq $environment)
  ) {
    $cachedData = Get-Content -Path $cachedConfigFileLocation | ConvertFrom-Json -depth 100
    Write-Host "Using cached data"
    return $cachedData
  }

  if ($environment -eq "dv") {
    $apimName = "apim-rdg-iig-dv"
    $apimRgName = "rg-rpic-iig-rdg-integration-dv-weu"
    $url = "https://apim-rdg-iig-$environment.azure-api.net/api-rdg-tools-$stack-$environment-$locationShort/v1/GetConfiguration?retailer=$stack&environment=$environment"
  }
  else {
    $apimName = "apim-rpic-iig-$environment"
    $apimRgName = "rg-rpic-iig-integration-$environment-$locationShort"
    $url = "https://apim-rpic-iig-$environment.azure-api.net/api-rdg-tools-$stack-$environment-$locationShort/v1/GetConfiguration?retailer=$stack&environment=$environment"
  }

  $apimContext = New-AzApiManagementContext -ResourceGroupName $apimRgName -ServiceName $apimName
  $subscription = Get-AzApiManagementSubscriptionKey -Context $apimContext -SubscriptionId "subscription-rdg-tools-$stack-$environment"

  # Define headers
  $headers = @{
    "Ocp-Apim-Subscription-Key" = $subscription.PrimaryKey
  }

  $response = Invoke-RestMethod -Uri $url -Method Get -Body $requestBody -Headers $headers

  $response | ConvertTo-Json | Set-Content -Path $cachedConfigFileLocation

  return $response | ConvertFrom-Json
}