PowerShell / DSC

This repo is for the DSC v3 project
MIT License
196 stars 26 forks source link

Multi-string support #518

Closed Gijsreyn closed 3 weeks ago

Gijsreyn commented 1 month ago

Prerequisites

Summary

Problem statement

This morning, I was rambling on my keyboard and looking at some examples to migrate PSDSC configuration documents to DSC. Experimenting with the PSDesiredStateConfiguration/Script resource was like opening a Pandora's box, with odd behaviors emerging out of the shadows especially when using multi-strings.

That brought me to do some investigation and see where things are going wrong.

Investigation

The initial start of my investigation, was using the following document:

# powershell.script.dsc.config.yaml
$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json
resources:
  - type: Microsoft.Windows/WindowsPowerShell
    name: Run script
    properties: 
      resources:
        - name: Run script
          type: PSDesiredStateConfiguration/Script
          properties: 
            Ensure: "Present"
            GetScript: |
              $testFile = C:\Temp\test.txt
              @{result = $(Get-Content $testFile)}
            TestScript: |
                Throw
            SetScript: |
                Throw

Running the script, gave me the following error message:

image

The strange thing while executing the document (dsc config get --path powershell.script.dsc.config.yaml), is that a piece is cut off:

image

Now, forget about the fact on YAML. Let's dive into the JSON part and see why the error was thrown. What I would have expected, is the following JSON being passed through to Invoke-DscResource:

// Example 1
{"GetScript":"$var = \"C:\\temp\\test.txt\"; @{result = $(Get-Content $var)}","SetScript":"throw","TestScript":"throw","type":"PSDesiredStateConfiguration/Script"}

Unfortunately, that does not happen, nor does the following JSONs are being passed through:

// Example 2
$res = @'
{"GetScript":["$var = \"C:\\temp\\test.txt\", "@{result = $(Get-Content $var)}"],"SetScript":"throw","TestScript":"throw","type":"PSDesiredStateConfiguration/Script"}
'@ 

// Example 3
$$string = @'
{
    "GetScript": [
        {
            "$var": "C:\\Temp\\test.txt"
        },
        {
            "@{result}": "Get-Content $var"
        }        
    ],
  "SetScript": "throw",
  "TestScript": "throw",
  "type": "PSDesiredStateConfiguration/Script"
}
'@

That third one sticks out like a sore thumb to me too.

Thoughts and ideas

That brought me even deeper to look at the adapter code and a train of thoughts on how to potentially look at this.

Firstly, passing in example 2, which looks more naturally to me, only reveals the real error message when you change the following line and add the -ErrorAction Stop parameter:

$invokeResult = Invoke-DscResource -Method $Operation -ModuleName $cachedDscResourceInfo.ModuleName -Name $cachedDscResourceInfo.Name -Property $property -ErrorAction Stop

The error message that is returned:

image

That still doesn't really say what is happening behind the scenes, so you've to dump out the objects.

Example 2 vs 1:

image

image

While example 1 is running smoothly, but example 2 is a breeze to type. It raises the question of why it doesn’t flow the same way when you input YAML through DSC's core engine.

Still, I was curious why the object was a mismatch and dove into the code. Looking at how the property hashtable is built, you can spot the difference and I applied the following:

# morph the INPUT object into a hashtable named "property" for the cmdlet Invoke-DscResource
$DesiredState.properties.psobject.properties | ForEach-Object -Begin { $property = @{} } -Process { 
    if ($_.Value.GetType().Name -eq 'Object[]')
    {
        $property[$_.Name] = $_.Value -as [System.String]
    }

    $property[$_.Name] = $_.Value 
}

If you want me to raise a PR for the fix and create Pester tests around it, just let me know.

Steps to reproduce

See problem statement

Expected behavior

Apply multi-strings.

Actual behavior

Multi-strings are not passed through YAML.

Error details

No response

Environment data

Name                           Value
----                           -----
PSVersion                      5.1.22621.3880
PSEdition                      Desktop
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}
BuildVersion                   10.0.22621.3880
CLRVersion                     4.0.30319.42000
WSManStackVersion              3.0
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1

Version

dsc 3.0.0-preview.8

Visuals

No response

Gijsreyn commented 1 month ago

@michaeltlombardi and @anamnavi Do you mind having a look into this issue?

SteveL-MSFT commented 4 weeks ago

Running dsc -l trace config get with the example yaml shows:

2024-08-16T03:30:01.339224Z TRACE : dsc_lib::configure: 657: Invoke property expression for resources: [{"name":"Run script","type":"PSDesiredStateConfiguration/Script","properties":{"Ensure":"Present","GetScript":"$testFile = C:\\Temp\\test.txt\n@{result = $(Get-Content $testFile)}\n","TestScript":"Throw\n","SetScript":"Throw\n"}}]

So the YAML is directly converted to JSON which means each line of the script is a newline. I think the PSAdapter could just replace newlines with a semi-colon. There might be some edge cases where this won't work, but we can document this behavior.

Gijsreyn commented 4 weeks ago

Thanks for checking out the issue Steve. If you want me to take a look into it or create documentation around somewhere, just give me a heads-up.

SteveL-MSFT commented 4 weeks ago

Actually thinking about this further, we can't just automatically assume a multiline string is a script. It's possible there's a PS resource that takes a string and the user wants to preserve the newlines.

Ideally, the script resource would account for this, but we're not going to make a change to that legacy resource. So, it might be better in this case to special case the script resource and also document this behavior so that other PS resource authors handle it correctly within the resource.

SteveL-MSFT commented 4 weeks ago

Actually, just adding semi colons for a script won't work, for example:

$a = @"
this is text
"@
$a
SteveL-MSFT commented 4 weeks ago

Did some debugging, looks like the problem might be in the parser which currently isn't expected to handle multi-line strings. Working on a fix.

Gijsreyn commented 4 weeks ago

@SteveL-MSFT Legend. Thanks for checking it out. I was also thinking of another example were you use the > in YAML. Haven't tested that one out.