Skip to content

PSScript resource #937

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ $filesForWindowsPackage = @(
'osinfo.dsc.resource.json',
'powershell.dsc.resource.json',
'psDscAdapter/',
'psscript.ps1',
'psscript.dsc.resource.json',
'winpsscript.dsc.resource.json',
'reboot_pending.dsc.resource.json',
'reboot_pending.resource.ps1',
'registry.dsc.resource.json',
Expand Down Expand Up @@ -84,6 +87,8 @@ $filesForLinuxPackage = @(
'osinfo.dsc.resource.json',
'powershell.dsc.resource.json',
'psDscAdapter/',
'psscript.ps1',
'psscript.dsc.resource.json',
'RunCommandOnSet.dsc.resource.json',
'runcommandonset',
'sshdconfig',
Expand All @@ -106,6 +111,8 @@ $filesForMacPackage = @(
'osinfo.dsc.resource.json',
'powershell.dsc.resource.json',
'psDscAdapter/',
'psscript.ps1',
'psscript.dsc.resource.json',
'RunCommandOnSet.dsc.resource.json',
'runcommandonset',
'sshdconfig',
Expand Down Expand Up @@ -276,6 +283,7 @@ if (!$SkipBuild) {
"dscecho",
"osinfo",
"powershell-adapter",
'resources/PSScript',
"process",
"runcommandonset",
"sshdconfig",
Expand Down Expand Up @@ -398,7 +406,8 @@ if (!$SkipBuild) {
Copy-Item "*.dsc.resource.json" $target -Force -ErrorAction Ignore
}
else { # don't copy WindowsPowerShell resource manifest
Copy-Item "*.dsc.resource.json" $target -Exclude 'windowspowershell.dsc.resource.json' -Force -ErrorAction Ignore
$exclude = @('windowspowershell.dsc.resource.json', 'winpsscript.dsc.resource.json')
Copy-Item "*.dsc.resource.json" $target -Exclude $exclude -Force -ErrorAction Ignore
}

# be sure that the files that should be executable are executable
Expand Down
25 changes: 25 additions & 0 deletions dsc/examples/psscript.dsc.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Example configuration using PowerShell script resource and using parameters and input
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
parameters:
myName:
type: string
defaultValue: Steve
myObject:
type: object
defaultValue:
color: green
number: 10
resources:
- name: Use PS script
type: Microsoft.DSC.Transitional/PowerShellScript
properties:
input:
- name: "[parameters('myName')]"
- object: "[parameters('myObject')]"
getScript: |
param($inputArray)

Write-Warning "This is a warning message"
# any output will be collected and returned
"My name is " + $inputArray[0].name
"My color is " + $inputArray[1].object.color
1 change: 1 addition & 0 deletions resources/PSScript/copy_files.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
./psscript.ps1
83 changes: 83 additions & 0 deletions resources/PSScript/psscript.dsc.resource.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
{
"$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json",
"type": "Microsoft.DSC.Transitional/PowerShellScript",
"description": "Enable running PowerShell 7 scripts inline",
"version": "0.1.0",
"get": {
"executable": "pwsh",
"args": [
"-NoLogo",
"-NonInteractive",
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-Command",
"$input | ./psscript.ps1",
"get"
],
"input": "stdin"
},
"set": {
"executable": "pwsh",
"args": [
"-NoLogo",
"-NonInteractive",
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-Command",
"$input | ./psscript.ps1",
"set"
],
"implementsPretest": true,
"input": "stdin",
"return": "state"
},
"test": {
"executable": "pwsh",
"args": [
"-NoLogo",
"-NonInteractive",
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-Command",
"$input | ./psscript.ps1",
"test"
],
"input": "stdin",
"return": "state"
},
"exitCodes": {
"0": "Success",
"1": "PowerShell script execution failed",
"2": "PowerShell exception occurred",
"3": "Script had errors"
},
"schema": {
"embedded": {
"type": "object",
"properties": {
"getScript": {
"type": ["string", "null"]
},
"setScript": {
"type": ["string", "null"]
},
"testScript": {
"type": ["string", "null"]
},
"input": {
"type": ["string", "boolean", "integer", "object", "array", "null"]
},
"output": {
"type": ["array", "null"]
},
"_inDesiredState": {
"type": ["boolean", "null"],
"default": null
}
}
}
}
}
166 changes: 166 additions & 0 deletions resources/PSScript/psscript.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, Position = 0)]
[ValidateSet('Get', 'Set', 'Test')]
[string]$Operation,
[Parameter(Mandatory = $true, Position = 1, ValueFromPipeline = $true)]
[string]$jsonInput
)

$traceQueue = [System.Collections.Concurrent.ConcurrentQueue[object]]::new()

function Write-DscTrace {
param(
[Parameter(Mandatory = $true)]
[ValidateSet('Error', 'Warn', 'Info', 'Debug', 'Trace')]
[string]$Level,
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[string]$Message,
[switch]$Now
)

$trace = @{$Level.ToLower() = $Message } | ConvertTo-Json -Compress

if ($Now) {
$host.ui.WriteErrorLine($trace)
} else {
$traceQueue.Enqueue($trace)
}
}

$scriptObject = $jsonInput | ConvertFrom-Json

$script = switch ($Operation) {
'Get' {
$scriptObject.GetScript
}
'Set' {
$scriptObject.SetScript
}
'Test' {
$scriptObject.TestScript
}
}

if ($null -eq $script) {
Write-DscTrace -Now -Level Info -Message "No script found for operation '$Operation'."
if ($Operation -eq 'Test') {
# if not implemented, we return it's in desired state
@{ _inDesiredState = $true } | ConvertTo-Json -Compress
exit 0
}

# write an empty json object to stdout
'{}'
exit 0
}

# use AST to see if script has param block, if any errors exit with error message
$errors = $null
$tokens = $null
$ast = [System.Management.Automation.Language.Parser]::ParseInput($script, [ref]$tokens, [ref]$errors)
if ($errors.Count -gt 0) {
$errorMessage = $errors | ForEach-Object { $_.ToString() }
Write-DscTrace -Now -Level Error -Message "Script has syntax errors: $errorMessage"
exit 3
}

$paramName = if ($ast.ParamBlock -ne $null) {
# make sure it only specifies one parameter and get the name of that parameter
if ($ast.ParamBlock.Parameters.Count -ne 1) {
Write-DscTrace -Now -Level Error -Message 'Script must have exactly one parameter.'
exit 3
}
$ast.ParamBlock.Parameters[0].Name.VariablePath.UserPath
} else {
$null
}

$ps = [PowerShell]::Create().AddScript({
$DebugPreference = 'Continue'
$VerbosePreference = 'Continue'
$ErrorActionPreference = 'Stop'
}).AddScript($script)

if ($null -ne $scriptObject.input) {
if ($null -eq $paramName) {
Write-DscTrace -Now -Level Error -Message 'Input was provided but script does not have a parameter to accept input.'
exit 3
}
$null = $ps.AddParameter($paramName, $scriptObject.input)
} elseif ($null -ne $paramName) {
Write-DscTrace -Now -Level Error -Message "Script has a parameter '$paramName' but no input was provided."
exit 3
}

$ps.Streams.Error.add_DataAdded({
param($sender, $args)
Write-DscTrace -Level Error -Message $sender.Message
})
$ps.Streams.Warning.add_DataAdded({
param($sender, $args)
Write-DscTrace -Level Warn -Message $sender.Message
})
$ps.Streams.Information.add_DataAdded({
param($sender, $args)
Write-DscTrace -Level Trace -Message $sender.MessageData.ToString()
})
$ps.Streams.Verbose.add_DataAdded({
param($sender, $args)
Write-DscTrace -Level Info -Message $sender.Message
})
$ps.Streams.Debug.add_DataAdded({
param($sender, $args)
Write-DscTrace -Level Debug -Message $sender.Message
})
$outputObjects = [System.Collections.Generic.List[Object]]::new()

function write-traces() {
$trace = $null
while (!$traceQueue.IsEmpty) {
if ($traceQueue.TryDequeue([ref] $trace)) {
$host.ui.WriteErrorLine($trace)
}
}
}

try {
$asyncResult = $ps.BeginInvoke()
while (-not $asyncResult.IsCompleted) {
write-traces
Start-Sleep -Milliseconds 100
}
$outputCollection = $ps.EndInvoke($asyncResult)
write-traces

if ($ps.HadErrors) {
# If there are any errors, we will exit with an error code
Write-DscTrace -Now -Level Error -Message 'Errors occurred during script execution.'
exit 1
}

foreach ($output in $outputCollection) {
$outputObjects.Add($output)
}
}
catch {
Write-DscTrace -Now -Level Error -Message $_.Exception.Message
exit 1
}
finally {
$ps.Dispose()
}

# Test should return a single boolean value indicating if in the desired state
if ($Operation -eq 'Test') {
if ($outputObjects.Count -eq 1 -and $outputObjects[0] -is [bool]) {
@{ _inDesiredState = $outputObjects[0] } | ConvertTo-Json -Compress
} else {
Write-DscTrace -Now -Level Error -Message 'Test operation did not return a single boolean value.'
exit 1
}
} else {
@{ output = $outputObjects } | ConvertTo-Json -Compress -Depth 10
}
Loading
Loading