samples/_default/.config-utils.ps1

function make-alias($moduleName, $path = $null, $workingDir = $null, $hooks) {
    if ($workingDir -eq $null) {
        $workingDir = $PSScriptRoot
    }
    $modulePath = $moduleName
    if ($path -ne $null) {
        $modulePath = "$moduleName/$path"
    }
    return @{
        get      = {
            param($path, $options)
            pushd $workingDir
            try {
                $r = & .\configure.ps1 -command get -module $modulePath -porcelain
                if ($hooks -and $hooks.get) {
                    $subResult = & $hooks.get -path $path -options $options
                    if ($subResult) {
                        foreach ($key in $subResult.keys) {
                            $r.$key = $subResult[$key]
                        }
                    }
                }

                return @{ Value = $r.Value; Active = $r.Active; IsValid = $r.IsValid }
            }
            finally {
                popd
            }
        }.GetNewClosure()
        set      = {
            param($path, $v, $optName)
            
            $r = $null
            pushd $workingDir
            try {
                
                . .\.configuration.map.ps1
                $module = $modules.$moduleName
                $moduleCommand = $module.set
                $r = Invoke-Command -ScriptBlock $moduleCommand -ArgumentList @($path, $v, $optName)
            }
            finally {
                popd
            }
            if ($hooks -and $hooks.set) {
                $subResult = & $hooks.set -path $path -value $v -key $optName
            }
            return $r
        }.GetNewClosure()
        validate = {
            param($path, $v, $optName)
            
            pushd $workingDir
            try {
                $r = & .\configure.ps1 -command validate -module $modulePath -porcelain

                return $r.IsValid
            }
            finally {
                popd
            }
        }.GetNewClosure()
        options  = {
            param($path)
            pushd $workingDir
            try {
                $r = & .\configure.ps1 -command options -module $modulePath -porcelain

                if ($hooks -and $hooks.options) {
                    $subResult = & $hooks.options -path $path -options $r
                    if ($subResult) {
                        return $subResult
                    }
                }
                
                return $r
            }
            finally {
                popd
            }
        }.GetNewClosure()
    }
}

function get-appsettings([Parameter(Mandatory = $true)]$file, $path = "") {
    $json = get-content $file | convertfrom-json

    $components = $path.split(":")
    $node = $json
    foreach ($component in $components) {
        $node = $node.$component
    }

    return $node
}

function set-appsettings(
    [Parameter(Mandatory = $true)] $file,
    [Parameter(Mandatory = $true)] $path,
    [Parameter(Mandatory = $true)] $value
) {
    $json = get-content $file | convertfrom-json -AsHashtable

    $components = $path.split(":")
    $node = $json
    for ($i = 0; $i -lt $components.Count - 1; $i++) {
        $component = $components[$i]
        if ($node.$component -eq $null) {
            $node.$component = @{}
        }
        $node = $node.$component
    }
    $leaf = $components[$components.Count - 1]
    
    $node.$($leaf) = $value
    $json | convertto-json -Depth 100 | set-content $file
}

function test-isSpecialValue($v) {
    return $v -and $v -is [string] -and ($v.StartsWith("user-secrets:") -or $v.StartsWith("keyvault:"));
}
function resolve-value($v) {
    $v = $value.$key
    if (!(test-isSpecialValue $v)) { return $v }

    if ($v -and $v -is [string] -and $v.StartsWith("user-secrets:")) {
        $secretName = $v.Substring("user-secrets:".Length)
        $secrets = dotnet user-secrets -p $dir list --json | ? { !$_.StartsWith("//") } | ConvertFrom-Json -AsHashtable
        if ($LASTEXITCODE -ne 0) {
            write-warning "cannot set '$secretName': failed to list user secrets for project in '$dir'. Do you have 'dotnet user-secrets' installed?"
            continue
        }
        if (!$secrets.ContainsKey($secretName)) {
            write-warning "secret '$secretName' not found in user secrets. Please run 'dotnet user-secrets -p $dir set $secretName <value>' to set the secret."
            continue
        }
        $v = $secrets[$secretName]
    }

    if ($v -and $v -is [string] -and $v.StartsWith("keyvault:")) {
        $splits = $v.Substring("keyvault:".Length).Split("/")
        $v = get-keyvaultSecret $splits[0] $splits[1]
    }

    return $v
}

function set-appsettingsobject(
    [Parameter(Mandatory = $true)]$file, 
    [Parameter(Mandatory = $true)]$value
) {
    $dir = split-path $file
    foreach ($key in $value.keys) {
        if ($key.StartsWith("__")) {
            continue
        }

        $v = resolve-value $value.$key

        set-appsettings $file -path $key -value $v
    }
}

function get-appsettingsObject(
    [Parameter(Mandatory = $true)][string]$file, 
    [Parameter(Mandatory = $true)]$options
) {
    foreach ($optionSet in $options.keys) {
        write-verbose "checking '$optionSet'"
        $option = $options[$optionSet]
        $isMatch = $true
        foreach ($key in $option.keys) {
            Write-Verbose "getting key $key=$v"
            $v = get-appsettings $file $key
            if ($v -is [string] -and $v -ne $option[$key] -and !(test-isSpecialValue $option[$key])) {
                Write-Verbose "key $key=$v does not match options[$optionSet].$key=$($option[$key])"
                $isMatch = $false
                # if any of the keys do not match, break out of the loop
                break
            }
        }
        if ($isMatch) {
            return @{ value = $optionSet; Active = $optionSet }
        }
    }
    
    return @{ value = "?"; }
}

function test-azureAccount($value) {
    $account = az account show | ConvertFrom-Json
    $targetSubscription = $value["__AzureSubscription"];
    if (!$targetSubscription) {
        write-warning "no '__AzureSubscription' subscription specified in $($value | ConvertTo-Json)"
        return $true
    }
    $targetTenant = $value["__AzureTenant"]

    if ($account) {
        if (($account.name -eq $targetSubscription -or $account.id -eq $targetSubscription)) {
            write-host "✅ active azure account: $($account.name) [$($account.id)]"
            write-host " active tenant: $($account.tenantId)"
            return $true
        }
        else {
            write-host "❌ active azure account: $($account.name) [$($account.id)] does not match target subscription '$targetSubscription'"
        }
    }
    else {
        write-host "no active azure account"
    }

    return $false
}
function set-azureAccount($value) {
    $targetSubscription = $value["__AzureSubscription"];
    $targetTenant = $value["__AzureTenant"]

    if (test-azureAccount $value) {
        return
    }

    Write-Host "Logging into azure..."
    $a = @()
    if ($targetTenant) {
        $a += "--tenant", $targetTenant
    }
    az login @a
    Write-Host "Selecting subscription $targetSubscription..."
    az account set --subscription $targetSubscription

}

function get-dockerContainer(
    [Parameter(Mandatory = $false)] $displayName, 
    [Parameter(ParameterSetName = "port", Mandatory = $true)] $port, 
    [Parameter(ParameterSetName = "name", Mandatory = $true)] $name 
) {
    $dockerVersionStr = docker -v
    $m = $dockerVersionStr -match "[0-9]+\.[0-9]+\.[0-9]+"
    $dockerVersion = [version]::Parse($matches[0])

    if ($dockerVersion.Major -lt 24) {
        return @{ Value = "? (Docker version >=24 required)" }
    }
    
    $containers = docker ps -a --format json | convertfrom-json 
    if ($port -ne $null) {
        $container = $containers | ? { $_.Ports.contains([string]$port) }
    }
    elseif ($name -ne $null) {
        if ($name.StartsWith("/")) {
            $regexp = [regex]::new($name.Trim("/"))
            $container = $containers | ? { $regexp.IsMatch($_.Image) -or $regexp.IsMatch($_.Names) }
        }
        else {
            $container = $containers | ? { $_.Image.StartsWith($name) -or $_.Names.StartsWith($name) }
        }
    }
    
    write-verbose "== all containers: =="
    docker -v | write-verbose
    docker compose version | write-verbose
    $containers | convertto-json | write-verbose
    write-verbose "== containers END =="

    return $container
}
function test-dockerContainer(
    [Parameter(Mandatory = $false)] $displayName, 
    [Parameter(ParameterSetName = "port", Mandatory = $true)] $port, 
    [Parameter(ParameterSetName = "name", Mandatory = $true)] $name) {
    $container = $null
    if ($port) { 
        $container = get-dockerContainer -displayName $displayName -port $port
        if (!$container) {
            write-host "❌ $displayName container at port $port not found"
            return $false
        }
    }
    if ($name) { 
        $container = get-dockerContainer -displayName $displayName -name $name
        if (!$container) {
            write-host "❌ $displayName container matching name '$name' not found"
            return $false
        }
    }

    
    $isvalid = $container -and $container.state -eq "running"

    if ($isvalid ) { write-host "✅ $displayName running" }
    else { write-host "❌ $displayName not running" }

    return $isvalid
}

function test-dockerDb($containerName, $dbName = "rls_lagerman") {
    write-verbose "checking if db is running..."
    $container = get-dockerContainer -name $containerName
    if (!$container) {
        throw "no containers found with name matching '$containerName'"
    }
    if (@($container).Length -gt 1) {
        throw "multiple containers found with name matching '$containerName'"
    }

    $containerName = $container.Names
    write-verbose "checking sql connection to $containerName..."
    
    $retries = 5
    $backoff = 5

    for ($i = $retries; $i -ge 0; $i++) {
        $result = docker exec $containerName "/opt/mssql-tools/bin/sqlcmd" -S localhost -U sa -P '12345678!aA#' -Q "print 'select from inisettings'; use [$dbName]; select top 10 * from inisitesettings;"
        $result

        if ($lastExitCode -eq 0 -and $result[0].StartsWith("select from inisettings")) {
            write-verbose "DB is running"
            break
        }
        else {
            write-verbose "DB is not running"
            if ($i -eq 0) {
                return $false
            }
            write-verbose "retrying in $backoff s..."
            Start-Sleep -Seconds $backoff
        }
    }

    write-verbose "DONE exitCode=$LastExitCode"
    return $true
}

function get-envsetting($file, $path) {
    if (!(test-path $file)) {
        return $null
    }
    $env = get-content $file | ConvertFrom-StringData
    return $env.$path
}

function set-envsetting($file, $path, $value) {
    $envData = @{}
    if (test-path $file) {
        $envData = get-content $file | ConvertFrom-StringData
    }
    $hash = [ordered]@{}
    $envData.keys | % {
        $hash[$_] = $envData.$_
    }
    $hash[$path] = $value
    $hash.GetEnumerator() | % {
        "$($_.key)=$($_.value)"
    } | Out-File $file
}

function set-envsettingsObject($file, $path, $value) {
    $dir = split-path $file
    foreach ($key in $value.keys) {
        if ($key.StartsWith("__")) {
            continue
        }

        $v = resolve-value $value.$key
        
        set-envsetting $file -path $key -value $v
    }   
}

function get-envsettingsObject(
    [Parameter(Mandatory = $true)][string]$file, 
    [Parameter(Mandatory = $true)]$options
) {
    if (!(test-path $file)) {
        return @{ value = $null; Active = $null }
    }
    $value = get-content $file | ConvertFrom-StringData
    
    foreach ($optionSet in $options.keys) {
        $option = $options.$optionSet
        $isMatch = $true
        foreach ($key in $option.keys) {
            $v = $value.key
            Write-Verbose "getting key $key=$v"
            if ($v -is [string] -and $v -ne $option.$key -and !(test-isSpecialValue $option.$key)) {
                Write-Verbose "key $key=$v does not match $option.$key"
                $isMatch = $false
                break
            }
        }
        if ($isMatch) {
            return @{ value = $value; Active = $optionSet }
        }
    }
    
    return @{ value = $value; Active = $null }
}

function get-keyvaultSecret($keyvault, $secret) {
    $secret = az keyvault secret show --vault-name $keyvault --name $secret | ConvertFrom-Json
    return $secret.value
}

Function Create-JWT(
    [Parameter(Mandatory = $true)]$headers,
    [Parameter(Mandatory = $true)]$payload,
    [Parameter(Mandatory = $true)]$secret
) {


    $headersJson = $headers | ConvertTo-Json -Compress
    $payloadJson = $payload | ConvertTo-Json -Compress
    $headersEncoded = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($headersJson), [Base64FormattingOptions]::None).TrimEnd('=')
    $payloadEncoded = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($payloadJson), [Base64FormattingOptions]::None).TrimEnd('=')

    $content = "$($headersEncoded).$($payloadEncoded)"

    $hmacsha = New-Object System.Security.Cryptography.HMACSHA256  
    $hmacsha.key = [Text.Encoding]::UTF8.GetBytes($secret)
    $bytesToSign = [Text.Encoding]::UTF8.GetBytes($content)
    $signatureByte = $hmacsha.ComputeHash($bytesToSign)

    $signature = [System.Convert]::ToBase64String($signatureByte, [Base64FormattingOptions]::None).Replace('+', '-').Replace('/', '_').Replace("=", "").TrimEnd('=')

    $jwt = "$($headersEncoded).$($payloadEncoded).$($signature)"

    return $jwt

}

Function Get-RezlynxToken {
    param(
        [Parameter(Mandatory = $true)]$signingKey,
        [Parameter(Mandatory = $true)]$siteId, 
        $groupId = "",
        $username = "SUPERVISOR",
        $audience = "GhpServices",
        $clientId = "RezlynxUi",
        $expireAfterMin = 60
    )

    # rlx uses symmetric key for signature
    $header = @{
        "alg" = "HS256"
        "typ" = "JWT"
    }

    # this is the format of Rlx auth token
    $payload = @{
        "aud"         = "$audience"
        "username"    = "$username"
        "gl_username" = "$username"
        "gl_group"    = "$groupId"
        "group"       = "$groupId"
        "siteId"      = "$siteId"
        "gl_site"     = "$siteId"
        "client_id"   = "$clientId"
        "nbf"         = 1677747257
        "exp"         = 1677748457
        "iat"         = 1677747257
        "iss"         = "rezlynx"
    }

    $epochNow = [int]([DateTime]::UtcNow - [DateTime]('1970,1,1')).TotalSeconds
    $epochExpiry = [int]([DateTime]::UtcNow.AddMinutes($expireAfterMin) - [DateTime]('1970,1,1')).TotalSeconds
    $payload.nbf = $epochNow
    $payload.iat = $epochNow
    $payload.exp = $epochExpiry


    $jwt = Create-JWT -headers $header -payload $payload -secret $signingKey

    return $jwt 
}

function get-xmlconfig($file, $path) {
    if (!(test-path $file)) {
        return $null
    }
    $config = [xml](get-content $file)

    $splits = $path.Split("/")
    $current = $config
    $currentPath = ""
    foreach ($split in $splits) {
        if ($current[$split] -eq $null) {
            throw "Could not find $split in $currentPath"
        }
        $current = $current[$split]
        $currentPath += "/$split"
       
    }
    
    return $current.InnerXml
}

function set-xmlconfig($file, $path, $value) {
    $fullPath = [System.IO.Path]::GetFullPath($file)
    if ((test-path $fullPath)) {
        $config = [xml](get-content $fullPath)
    }
    else {
        $config = [xml]""
    }

    $splits = $path.Split("/")
    $current = $config
    $currentPath = ""
    foreach ($split in $splits) {
        if ($current[$split] -eq $null) {
            $current.AppendChild($config.CreateElement($split))
        }
        $current = $current[$split]
        $currentPath += "/$split"
       
    }
    
    $current.InnerXml = $value
    
    $config.Save($fullPath)
}

function test-httpurl($url) {
    try {
        $result = Invoke-WebRequest -Uri $url -Method Get -UseBasicParsing -ErrorAction SilentlyContinue
        return ($result.statuscode -eq 200)

    }
    catch {
        if ($_.Exception.StatusCode -eq 404) {
            # 404 means the service is there and responding, so treat it as success
            return $true
        }
        return $false
    }
}