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 |