Skip to content

Commit 90b4e2a

Browse files
authored
Merge pull request #937 from SteveL-MSFT/psscript
PSScript resource
2 parents c1b98e1 + 869d9fb commit 90b4e2a

File tree

7 files changed

+703
-1
lines changed

7 files changed

+703
-1
lines changed

build.ps1

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ $filesForWindowsPackage = @(
5353
'osinfo.dsc.resource.json',
5454
'powershell.dsc.resource.json',
5555
'psDscAdapter/',
56+
'psscript.ps1',
57+
'psscript.dsc.resource.json',
58+
'winpsscript.dsc.resource.json',
5659
'reboot_pending.dsc.resource.json',
5760
'reboot_pending.resource.ps1',
5861
'registry.dsc.resource.json',
@@ -87,6 +90,8 @@ $filesForLinuxPackage = @(
8790
'osinfo.dsc.resource.json',
8891
'powershell.dsc.resource.json',
8992
'psDscAdapter/',
93+
'psscript.ps1',
94+
'psscript.dsc.resource.json',
9095
'RunCommandOnSet.dsc.resource.json',
9196
'runcommandonset',
9297
'sshdconfig',
@@ -109,6 +114,8 @@ $filesForMacPackage = @(
109114
'osinfo.dsc.resource.json',
110115
'powershell.dsc.resource.json',
111116
'psDscAdapter/',
117+
'psscript.ps1',
118+
'psscript.dsc.resource.json',
112119
'RunCommandOnSet.dsc.resource.json',
113120
'runcommandonset',
114121
'sshdconfig',
@@ -300,6 +307,7 @@ if (!$SkipBuild) {
300307
"dscecho",
301308
"osinfo",
302309
"powershell-adapter",
310+
'resources/PSScript',
303311
"process",
304312
"runcommandonset",
305313
"sshdconfig",
@@ -422,7 +430,8 @@ if (!$SkipBuild) {
422430
Copy-Item "*.dsc.resource.json" $target -Force -ErrorAction Ignore
423431
}
424432
else { # don't copy WindowsPowerShell resource manifest
425-
Copy-Item "*.dsc.resource.json" $target -Exclude 'windowspowershell.dsc.resource.json' -Force -ErrorAction Ignore
433+
$exclude = @('windowspowershell.dsc.resource.json', 'winpsscript.dsc.resource.json')
434+
Copy-Item "*.dsc.resource.json" $target -Exclude $exclude -Force -ErrorAction Ignore
426435
}
427436

428437
# be sure that the files that should be executable are executable

dsc/examples/psscript.dsc.yaml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Example configuration using PowerShell script resource and using parameters and input
2+
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
3+
parameters:
4+
myName:
5+
type: string
6+
defaultValue: Steve
7+
myObject:
8+
type: object
9+
defaultValue:
10+
color: green
11+
number: 10
12+
resources:
13+
- name: Use PS script
14+
type: Microsoft.DSC.Transitional/PowerShellScript
15+
properties:
16+
input:
17+
- name: "[parameters('myName')]"
18+
- object: "[parameters('myObject')]"
19+
getScript: |
20+
param($inputArray)
21+
22+
Write-Warning "This is a warning message"
23+
# any output will be collected and returned
24+
"My name is " + $inputArray[0].name
25+
"My color is " + $inputArray[1].object.color

resources/PSScript/copy_files.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
./psscript.ps1
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
{
2+
"$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json",
3+
"type": "Microsoft.DSC.Transitional/PowerShellScript",
4+
"description": "Enable running PowerShell 7 scripts inline",
5+
"version": "0.1.0",
6+
"get": {
7+
"executable": "pwsh",
8+
"args": [
9+
"-NoLogo",
10+
"-NonInteractive",
11+
"-NoProfile",
12+
"-ExecutionPolicy",
13+
"Bypass",
14+
"-Command",
15+
"$input | ./psscript.ps1",
16+
"get"
17+
],
18+
"input": "stdin"
19+
},
20+
"set": {
21+
"executable": "pwsh",
22+
"args": [
23+
"-NoLogo",
24+
"-NonInteractive",
25+
"-NoProfile",
26+
"-ExecutionPolicy",
27+
"Bypass",
28+
"-Command",
29+
"$input | ./psscript.ps1",
30+
"set"
31+
],
32+
"implementsPretest": true,
33+
"input": "stdin",
34+
"return": "state"
35+
},
36+
"test": {
37+
"executable": "pwsh",
38+
"args": [
39+
"-NoLogo",
40+
"-NonInteractive",
41+
"-NoProfile",
42+
"-ExecutionPolicy",
43+
"Bypass",
44+
"-Command",
45+
"$input | ./psscript.ps1",
46+
"test"
47+
],
48+
"input": "stdin",
49+
"return": "state"
50+
},
51+
"exitCodes": {
52+
"0": "Success",
53+
"1": "PowerShell script execution failed",
54+
"2": "PowerShell exception occurred",
55+
"3": "Script had errors"
56+
},
57+
"schema": {
58+
"embedded": {
59+
"type": "object",
60+
"properties": {
61+
"getScript": {
62+
"type": ["string", "null"]
63+
},
64+
"setScript": {
65+
"type": ["string", "null"]
66+
},
67+
"testScript": {
68+
"type": ["string", "null"]
69+
},
70+
"input": {
71+
"type": ["string", "boolean", "integer", "object", "array", "null"]
72+
},
73+
"output": {
74+
"type": ["array", "null"]
75+
},
76+
"_inDesiredState": {
77+
"type": ["boolean", "null"],
78+
"default": null
79+
}
80+
}
81+
}
82+
}
83+
}

resources/PSScript/psscript.ps1

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
[CmdletBinding()]
4+
param(
5+
[Parameter(Mandatory = $true, Position = 0)]
6+
[ValidateSet('Get', 'Set', 'Test')]
7+
[string]$Operation,
8+
[Parameter(Mandatory = $true, Position = 1, ValueFromPipeline = $true)]
9+
[string]$jsonInput
10+
)
11+
12+
function Write-DscTrace {
13+
param(
14+
[Parameter(Mandatory = $true)]
15+
[ValidateSet('Error', 'Warn', 'Info', 'Debug', 'Trace')]
16+
[string]$Level,
17+
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
18+
[string]$Message,
19+
[switch]$Now
20+
)
21+
22+
$trace = @{$Level.ToLower() = $Message } | ConvertTo-Json -Compress
23+
24+
if ($Now) {
25+
$host.ui.WriteErrorLine($trace)
26+
} else {
27+
$traceQueue.Enqueue($trace)
28+
}
29+
}
30+
31+
$scriptObject = $jsonInput | ConvertFrom-Json
32+
33+
$script = switch ($Operation) {
34+
'Get' {
35+
$scriptObject.GetScript
36+
}
37+
'Set' {
38+
$scriptObject.SetScript
39+
}
40+
'Test' {
41+
$scriptObject.TestScript
42+
}
43+
}
44+
45+
if ($null -eq $script) {
46+
Write-DscTrace -Now -Level Info -Message "No script found for operation '$Operation'."
47+
if ($Operation -eq 'Test') {
48+
# if not implemented, we return it's in desired state
49+
@{ _inDesiredState = $true } | ConvertTo-Json -Compress
50+
exit 0
51+
}
52+
53+
# write an empty json object to stdout
54+
'{}'
55+
exit 0
56+
}
57+
58+
# use AST to see if script has param block, if any errors exit with error message
59+
$errors = $null
60+
$tokens = $null
61+
$ast = [System.Management.Automation.Language.Parser]::ParseInput($script, [ref]$tokens, [ref]$errors)
62+
if ($errors.Count -gt 0) {
63+
$errorMessage = $errors | ForEach-Object { $_.ToString() }
64+
Write-DscTrace -Now -Level Error -Message "Script has syntax errors: $errorMessage"
65+
exit 3
66+
}
67+
68+
$paramName = if ($null -ne $ast.ParamBlock) {
69+
# make sure it only specifies one parameter and get the name of that parameter
70+
if ($ast.ParamBlock.Parameters.Count -ne 1) {
71+
Write-DscTrace -Now -Level Error -Message 'Script must have exactly one parameter.'
72+
exit 3
73+
}
74+
$ast.ParamBlock.Parameters[0].Name.VariablePath.UserPath
75+
} else {
76+
$null
77+
}
78+
79+
$ps = [PowerShell]::Create().AddScript({
80+
$DebugPreference = 'Continue'
81+
$VerbosePreference = 'Continue'
82+
$ErrorActionPreference = 'Stop'
83+
}).AddStatement().AddScript($script)
84+
85+
if ($null -ne $scriptObject.input) {
86+
if ($null -eq $paramName) {
87+
Write-DscTrace -Now -Level Error -Message 'Input was provided but script does not have a parameter to accept input.'
88+
exit 3
89+
}
90+
$null = $ps.AddParameter($paramName, $scriptObject.input)
91+
} elseif ($null -ne $paramName) {
92+
Write-DscTrace -Now -Level Error -Message "Script has a parameter '$paramName' but no input was provided."
93+
exit 3
94+
}
95+
96+
$traceQueue = [System.Collections.Concurrent.ConcurrentQueue[object]]::new()
97+
98+
$null = Register-ObjectEvent -InputObject $ps.Streams.Error -EventName DataAdding -MessageData $traceQueue -Action {
99+
$traceQueue = $Event.MessageData
100+
# convert error to string since it's an ErrorRecord
101+
$traceQueue.Enqueue((@{ error = [string]$EventArgs.ItemAdded } | ConvertTo-Json -Compress))
102+
}
103+
$null = Register-ObjectEvent -InputObject $ps.Streams.Warning -EventName DataAdding -MessageData $traceQueue -Action {
104+
$traceQueue = $Event.MessageData
105+
$traceQueue.Enqueue((@{ warn = $EventArgs.ItemAdded.Message } | ConvertTo-Json -Compress))
106+
}
107+
$null = Register-ObjectEvent -InputObject $ps.Streams.Information -EventName DataAdding -MessageData $traceQueue -Action {
108+
$traceQueue = $Event.MessageData
109+
if ($null -ne $EventArgs.ItemAdded.MessageData) {
110+
if ($EventArgs.ItemAdded.Tags -contains 'PSHOST') {
111+
$traceQueue.Enqueue((@{ info = $EventArgs.ItemAdded.MessageData.ToString() } | ConvertTo-Json -Compress))
112+
} else {
113+
$traceQueue.Enqueue((@{ trace = $EventArgs.ItemAdded.MessageData.ToString() } | ConvertTo-Json -Compress))
114+
}
115+
return
116+
}
117+
}
118+
$null = Register-ObjectEvent -InputObject $ps.Streams.Verbose -EventName DataAdding -MessageData $traceQueue -Action {
119+
$traceQueue = $Event.MessageData
120+
$traceQueue.Enqueue((@{ info = $EventArgs.ItemAdded.Message } | ConvertTo-Json -Compress))
121+
}
122+
$null = Register-ObjectEvent -InputObject $ps.Streams.Debug -EventName DataAdding -MessageData $traceQueue -Action {
123+
$traceQueue = $Event.MessageData
124+
$traceQueue.Enqueue((@{ debug = $EventArgs.ItemAdded.Message } | ConvertTo-Json -Compress))
125+
}
126+
$outputObjects = [System.Collections.Generic.List[Object]]::new()
127+
128+
function Write-TraceQueue() {
129+
$trace = $null
130+
while (!$traceQueue.IsEmpty) {
131+
if ($traceQueue.TryDequeue([ref] $trace)) {
132+
$host.ui.WriteErrorLine($trace)
133+
}
134+
}
135+
}
136+
137+
try {
138+
$asyncResult = $ps.BeginInvoke()
139+
while (-not $asyncResult.IsCompleted) {
140+
Write-TraceQueue
141+
142+
Start-Sleep -Milliseconds 100
143+
}
144+
$outputCollection = $ps.EndInvoke($asyncResult)
145+
Write-TraceQueue
146+
147+
148+
if ($ps.HadErrors) {
149+
# If there are any errors, we will exit with an error code
150+
Write-DscTrace -Now -Level Error -Message 'Errors occurred during script execution.'
151+
exit 1
152+
}
153+
154+
foreach ($output in $outputCollection) {
155+
$outputObjects.Add($output)
156+
}
157+
}
158+
catch {
159+
Write-DscTrace -Now -Level Error -Message $_
160+
exit 1
161+
}
162+
finally {
163+
$ps.Dispose()
164+
Get-EventSubscriber | Unregister-Event
165+
}
166+
167+
# Test should return a single boolean value indicating if in the desired state
168+
if ($Operation -eq 'Test') {
169+
if ($outputObjects.Count -eq 1 -and $outputObjects[0] -is [bool]) {
170+
@{ _inDesiredState = $outputObjects[0] } | ConvertTo-Json -Compress
171+
} else {
172+
Write-DscTrace -Now -Level Error -Message 'Test operation did not return a single boolean value.'
173+
exit 1
174+
}
175+
} else {
176+
@{ output = $outputObjects } | ConvertTo-Json -Compress -Depth 10
177+
}

0 commit comments

Comments
 (0)