BrazenCloud.ADK.psm1

<#
    Code in this file will be added to the beginning of the .psm1. For example,
    you should place any using statements here.
#>

# suppressing the warning from the scriptblock
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')]
param()
Function Get-BcAgentInstallPath {
    [OutputType([System.IO.DirectoryInfo], ParameterSetName = 'dirInfo')]
    [OutputType([System.String], ParameterSetName = 'str')]
    [cmdletbinding(
        DefaultParameterSetName = 'dirInfo'
    )]
    param (
        [Parameter(
            ParameterSetName = 'str'
        )]
        [switch]$AsString
    )
    if ($AsString.IsPresent) {
        Get-ChildItem 'C:\Program Files\Runway' | ForEach-Object { $_.FullName }
    } else {
        Get-ChildItem 'C:\Program Files\Runway'
    }
}
Function Get-BcUtilityExecutable {
    [cmdletbinding()]
    param ()
    $platform = if (Test-Path 'C:\Program Files (x86)') {
        'windows64'
    } else {
        'windows32'
    }
    Write-Information 'Downloading runway.exe...'
    Invoke-BcDownloadContentPublicFile -Key runway -Platform $platform -OutFile $PSScriptRoot\runway.exe
}
Function Invoke-BcAction {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '')]
    [cmdletbinding(
        SupportsShouldProcess,
        ConfirmImpact = 'High'
    )]
    param (
        [ValidateScript(
            {
                if (Test-Path $_ -PathType Leaf) {
                    $_ -like '*manifest.txt'
                } elseif (Test-Path $_ -PathType Container) {
                    Test-Path $_\manifest.txt -PathType Leaf
                }
            }
        )]
        [Parameter(
            Mandatory,
            HelpMessage = 'Path must be a manifest.txt file or a folder containing one.'
        )]
        [string]$Path,
        [string]$WorkingDirectory,
        [hashtable]$Settings,
        [switch]$PreserveWorkingDirectory,
        [switch]$IgnoreRequiredParameters,
        [switch]$SkipMissingParameters
    )
    $ip = $InformationPreference
    $InformationPreference = 'Continue'

    if (-not (Get-BcAdkNodeAgent).IsRunning) {
        Start-BcAdkNodeAgent
    }

    $agentPath = $NodeAgentPath.FullName
    $UtilityPath = "$($NodeAgentPath.FullName)\runway.exe"

    # If the path is a folder, append manifest.txt
    if (Test-Path $Path -PathType Container) {
        $sPath = "$path\settings.json"
        $Path = "$((Resolve-Path $Path).Path)\manifest.txt"
    } else {
        $sPath = "$(Split-Path $path)\settings.json"
    }

    $parametersPath = "$(Split-Path $Path)\parameters.json"
    $parameters = Get-Content $parametersPath | ConvertFrom-Json
    if ($PSBoundParameters.Keys -notcontains 'Settings') {
        $Settings = @{}
    }

    # Build settings with empty string parameters
    if (-not $SkipMissingParameters.IsPresent) {
        if (Test-Path $parametersPath) {
            foreach ($param in $parameters) {
                if ($Settings.Keys -notcontains $param.Name) {
                    if ($param.DefaultValue.Length -gt 0) {
                        $Settings[$param.Name] = $param.DefaultValue
                    } else {
                        $Settings[$param.Name] = ''
                    }
                }
            }
        }
    }

    foreach ($key in $Settings.Keys) {
        if ($parameters.Name -notcontains $key) {
            Write-Warning "Passed parameter '$key' is not a valid parameter."
        }
    }

    $splat = @{
        AgentPath  = $agentPath
        Parameters = $Settings
    }
    Join-BcSettingsHashtable @splat | ConvertTo-Json | Out-File $sPath

    # If no working dir is passed, use something in TEMP
    $actionRun = "Action_$(Get-Date -UFormat %s)"
    if ($PSBoundParameters.Keys -notcontains 'WorkingDirectory') {
        $WorkingDirectory = "$($env:TEMP)\$actionRun"
    }

    if (Test-Path $WorkingDirectory) {
        Write-Verbose 'The working directory already exists, clear it?'
        if ($PSCmdlet.ShouldProcess($WorkingDirectory, 'Remove-Item')) {
            Remove-Item $WorkingDirectory -Recurse -Force
        } else {
            $PSCmdlet.ShouldProcess
            return
        }
    }
    New-Item $WorkingDirectory -ItemType Directory | Out-Null

    $WorkingDirectory = (Resolve-Path $WorkingDirectory).Path

    if (Test-Path "$($env:TEMP)\action.app") {
        Remove-Item "$($env:TEMP)\action.app" -Force
    }

    if (-not $IgnoreRequiredParameters.IsPresent) {
        if (Test-Path $parametersPath) {
            $parameters | Where-Object { $_.PSObject.Properties.Name -contains 'IsOptional' } | Where-Object { $_.IsOptional.ToString() -eq 'false' } | ForEach-Object {
                if ($Settings.Keys -notcontains $_.Name) {
                    Throw "Mandatory parameter: '$($_.Name)' was not provided. Pass a value via -Settings or use -IgnoreRequiredParameters"
                }
            }
        }
    }

    # Build Action
    $buildSplat = @{
        Path                   = 'cmd.exe'
        ArgumentList           = "/C .\runway.exe -N build -i $Path -o $($env:TEMP)\action.app"
        WorkingDirectory       = (Split-Path $UtilityPath)
        WindowStyle            = 'Hidden'
        PassThru               = $true
        RedirectStandardError  = "$($env:TEMP)\buildstderr_$actionRun.txt"
        RedirectStandardOutput = "$($env:TEMP)\buildstdout_$actionRun.txt"
    }
    Write-Verbose 'Building the action...'
    $actionProc = Start-Process @buildSplat -Wait

    $buildStdErr = Get-Content "$($env:TEMP)\buildstderr_$actionRun.txt"
    if ($buildStdErr.Length -gt 0) {
        Throw "Error in build: $buildStdErr"
    }

    # Remove settings.json
    Remove-Item $sPath -Force

    # Run Action
    $runSplat = @{
        Path                   = 'cmd.exe'
        ArgumentList           = "/C .\runner.exe run --action_zip $($env:TEMP)\action.app --path $WorkingDirectory"
        WorkingDirectory       = $agentPath
        WindowStyle            = 'Hidden'
        PassThru               = $true
        RedirectStandardError  = "$($env:TEMP)\runstderr_$actionRun.txt"
        RedirectStandardOutput = "$($env:TEMP)\runstdout_$actionRun.txt"
    }
    Write-Verbose 'Running the action...'
    $actionProc = Start-Process @runSplat

    # Stream std.out
    While (-not (Test-Path $WorkingDirectory\std.out)) {
        $runStdErr = Get-Content "$($env:TEMP)\runstderr_$actionRun.txt"
        if ($runStdErr.Length -gt 0) {
            Throw "Error in run: $runStdErr"
        }
        Start-Sleep -Seconds 1
    }
    Write-Verbose 'The following output is stdout from executing the action:'
    $stream = [System.IO.File]::Open("$WorkingDirectory\std.out", [System.IO.FileMode]::OpenOrCreate, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)
    $reader = [System.IO.StreamReader]::new($stream)
    $stdOut = & {
        while ($null -ne ($line = $reader.ReadLine())) {
            $line
            Write-Information $line
        }
        while (-not $actionProc.HasExited) {
            while ($null -ne ($line = $reader.ReadLine())) {
                $line
                Write-Information $line
            }
            Start-Sleep -Milliseconds 100
        }
        while (-not $reader.EndOfStream) {
            while ($null -ne ($line = $reader.ReadLine())) {
                $line
                Write-Information $line
            }
            Start-Sleep -Milliseconds 100
        }
    }
    $reader.Close()

    # Collect results
    $resultPath = "$($env:TEMP)\Results_$actionRun.zip"
    if (Test-Path $resultPath) {
        Write-Verbose 'The results file already exists, overwrite?'
        if ($PSCmdlet.ShouldProcess($resultPath, 'Remove-Item')) {
            Remove-Item $resultPath -Recurse -Force
        }
    }
    if ((Get-ChildItem $WorkingDirectory\results).Count -gt 0) {
        Compress-Archive "$WorkingDirectory\results" -DestinationPath $resultPath
    } else {
        Write-Verbose 'No results to be collected.'
    }

    $global:out = [pscustomobject]@{
        Build   = @{
            StdOut = Get-Content "$($env:TEMP)\buildstdout_$actionRun.txt"
            StdErr = Get-Content "$($env:TEMP)\buildstderr_$actionRun.txt"
        }
        Run     = @{
            StdOut = Get-Content "$($env:TEMP)\runstdout_$actionRun.txt"
            StdErr = Get-Content "$($env:TEMP)\runstderr_$actionRun.txt"
        }
        Results = if (Test-Path $resultPath) { Get-Item $resultPath } else { $null }
        StdOut  = $stdOut
    }

    # Clean up redirects
    @("buildstdout_*.txt", "buildstderr_*.txt", "runstdout_*.txt", "runstderr_*.txt") | ForEach-Object {
        Remove-Item "$($env:TEMP)\$_" -ErrorAction SilentlyContinue -Force
    }

    # Clean up WorkingDirectory
    if (-not ($PreserveWorkingDirectory.IsPresent)) {
        Remove-Item $WorkingDirectory -Recurse -Force
    } else {
        $out | Add-Member -MemberType NoteProperty -Name 'WorkingDirectory' -Value (Get-Item $WorkingDirectory)
    }

    Out-BcActionInvokeReport -InvocationData $out
    $out
    $InformationPreference = $ip
}
Function Join-BcSettingsHashtable {
    [cmdletbinding()]
    param (
        [string]$AgentPath,
        [hashtable]$Parameters
    )
    $runnerSettings = Get-Content $AgentPath\runner_settings.json | ConvertFrom-Json

    $settings = [ordered]@{
        runner_identity      = $runnerSettings.identity
        host                 = $runnerSettings.host
        thread_id            = ''
        job_id               = ''
        action_instance_id   = ''
        repository_action_id = ''
        prodigal_object_id   = ''
        prodigal_asset_name  = $env:COMPUTERNAME
        atoken               = $runnerSettings.atoken
    }

    $x = 0
    foreach ($key in $Parameters.Keys) {
        $settings.Insert($x, $key, $Parameters[$key])
        $x++
    }

    $settings
}
Function New-BcAction {
    [cmdletbinding(SupportsShouldProcess)]
    param (
        [string]$TemplateName = 'PowerShellAction',
        [Parameter(
            Mandatory,
            HelpMessage = 'Parent path of the action.'
        )]
        [string]$Destination,
        [switch]$Force
    )
    if (-not (Get-Module Plaster -ListAvailable)) {
        if ($Force.IsPresent) {
            Install-Module Plaster -Repository PsGallery -Force
        } else {
            Write-Warning 'This command requires the Plaster module. Please install or use the -Force switch to automatically isntall.'
            return
        }
    }
    Invoke-Plaster -TemplatePath $PSScriptRoot\templates\$TemplateName -DestinationPath $Destination
}
Function Out-BcActionInvokeReport {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")]
    [cmdletbinding()]
    param (
        [psobject]$InvocationData
    )

    $str = @'
-------------------------------------------------------------
 ____ ____ _ _
| __ ) _ __ __ _ _______ _ __ / ___| | ___ _ _ __| |
| _ \| '__/ _` |_ / _ \ '_ \| | | |/ _ \| | | |/ _` |
| |_) | | | (_| |/ / __/ | | | |___| | (_) | |_| | (_| |
|____/|_| \__,_/___\___|_| |_|\____|_|\___/ \__,_|\__,_|
-------------------------------------------------------------
Action Invocation Report
-------------------------------------------------------------
'@


    Write-Host $str


    Write-Host "Build Process: " -NoNewline
    If ($InvocationData.Build.StdErr.Length -gt 0) {
        Write-Host "Errors. View with: '$Out.Build.StdErr'" -ForegroundColor Red
    } else {
        Write-Host "No Errors" -ForegroundColor Green
    }

    Write-Host "Run Process: " -NoNewline
    If ($InvocationData.Build.StdErr.Length -gt 0) {
        Write-Host "Errors. View with: '$Out.Run.StdErr'" -ForegroundColor Red
    } else {
        Write-Host "No Errors" -ForegroundColor Green
    }

    Write-Host "Action Output: " -NoNewline
    Write-Host "$($InvocationData.StdOut.Count) lines of stdout. View with '`$Out.StdOut'" -ForegroundColor Green

    Write-Host "Results: " -NoNewline
    If ($InvocationData.Results.Length -gt 0) {
        Write-Host $InvocationData.Results -ForegroundColor Green
    } else {
        Write-Host "No results." -ForegroundColor Yellow
    }
}
Function Get-BcAdkNodeAgent {
    [cmdletbinding()]
    param (

    )
    $ht = @{
        IsRunning = $false
        Process   = $NodeAgentProcess
        Path      = $NodeAgentPath
    }
    if (Get-Variable -Scope Script -Name NodeAgentProcess -ErrorAction SilentlyContinue) {
        if ( -not $NodeAgentProcess.HasExited) {
            $ht['IsRunning'] = $true
        }
    }
    New-Object psobject -Property $ht
}
Function Start-BcAdkNodeAgent {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')]
    [cmdletbinding(SupportsShouldProcess)]
    param (

    )
    if (-not (Get-BcAdkNodeAgent).IsRunning) {
        try {
            Get-BcAuthenticationCurrentUser -ErrorAction Stop | Out-Null
        } catch {
            Throw "Unauthorized. Authenticate using 'Connect-BrazenCloud' first."
        }
        $tokenSplat = @{
            Expiration = (Get-Date).AddMinutes(30)
            IsOneTime  = $false
            Type       = 'EnrollPersistentRunner'
            GroupId    = (Get-BcAuthenticationCurrentUser).HomeContainerId
        }
        $token = New-BcEnrollmentSession @tokenSplat
        if (-not (Test-Path $PSScriptRoot\runway.exe)) {
            Get-BcUtilityExecutable
        }
        Push-Location
        Set-Location $PSScriptRoot
        $script:BrazenCloudAdkNodeAgentId = (New-Guid).Guid
        $script:NodeAgentProcess = [System.Diagnostics.Process]::new()
        $NodeAgentProcess.StartInfo.RedirectStandardOutput = $true
        $NodeAgentProcess.StartInfo.RedirectStandardError = $true
        $NodeAgentProcess.StartInfo.Arguments = @('-S', $env:BrazenCloudDomain, 'node', '-t', $token.Token, '--customid', $BrazenCloudAdkNodeAgentId, '--new')
        $NodeAgentProcess.StartInfo.WindowStyle = 'Hidden'
        $NodeAgentProcess.StartInfo.WorkingDirectory = $PSScriptRoot
        $NodeAgentProcess.StartInfo.FileName = "runway.exe"
        $NodeAgentProcess.Start() | Out-Null
        Pop-Location | Out-Null

        $noDir = $true
        $x = 0
        While ($noDir) {
            Get-ChildItem -Directory | ForEach-Object {
                if ($_.Name -match '[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$') {
                    $script:NodeAgentPath = Get-Item $_.FullName
                    $noDir = $false
                }
            }
            Start-Sleep -Seconds 1
            $x++
            if (($x -ge 5 -or $NodeAgentProcess.HasExited) -and $noDir) {
                Throw 'Node failed to start.'
                $noDir = $false
            }
        }
        Write-Information "Node initiated at: '$($NodeAgentPath)'"
    }
}
<#
    Code in this file will be added to the end of the .psm1. For example,
    you should set variables or other environment settings here.
#>

# Argument completer for New-BcAction
$scriptBlock = {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)

    Get-ChildItem $PSScriptRoot\templates\$wordToComplete* | Select-Object -ExpandProperty Name
}
Register-ArgumentCompleter -CommandName New-BcAction -ParameterName TemplateName -ScriptBlock $scriptBlock