Skip to main content

DSC 3.0: running your own PowerShell with getScript, testScript and setScrip

Sooner or later you hit a setting that no DSC resource manages. A hosts file entry, a certificate with specific properties, some tweak that only exists as a PowerShell snippet on a wiki page. You know the PowerShell to fix it, but there is no resource to wrap it in.

One way is to reach for Microsoft.DSC.Transitional/RunCommandOnSet. It runs a command during the set operation, and that is all it does. There is no test, so DSC cannot tell you whether the machine is in the desired state. But it is a good fit as a temporary transitional resource while migrating DSCv3 resources.

There is another option: Microsoft.DSC.Transitional/PowerShellScript. It lets you bring your own getScript, testScript and setScript.

Note: If you are new to DSC 3.0 check out my blog series about it:

Why do we need this?

Every DSC resource implements the same operations: get, test and set. Most resources are written once and reused everywhere. With PowerShellScript you write those three operations yourself, inline in your configuration document.

It is a transitional resource, and the name says it all. It is a bridge between the declarative DSC model and the imperative scripts you already have. If you used the Script resource from PSDscResources in the past, the three properties will look familiar.

The three scripts

  • getScript: returns the current state of the resource. It runs when you invoke dsc config get.
  • testScript: returns $true when the machine is in the desired state and $false when it is not. It runs on dsc config test, and as the first step of dsc config set.
  • setScript: makes the change. It only runs when testScript returned $false.

The interesting part is the relation between the last two. testScript is the gatekeeper. If you get it right, setScript only touches the machine when it has to, and running the configuration twice is harmless.

Putting it into action

Let's make sure a hosts file entry exists. Not something you would do in production, but small enough to see all three scripts at work.

$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: Hosts entry for dsc.local
  type: Microsoft.DSC.Transitional/PowerShellScript
  properties:
    getScript: |
      $hosts = Join-Path $env:SystemRoot 'System32\drivers\etc\hosts'
      $entry = Get-Content $hosts | Where-Object { $_ -match '^\s*127\.0\.0\.1\s+dsc\.local\s*$' }
      return @{ present = [bool]$entry }
    testScript: |
      $hosts = Join-Path $env:SystemRoot 'System32\drivers\etc\hosts'
      return [bool](Get-Content $hosts | Where-Object { $_ -match '^\s*127\.0\.0\.1\s+dsc\.local\s*$' })
    setScript: |
      $hosts = Join-Path $env:SystemRoot 'System32\drivers\etc\hosts'
      Add-Content -Path $hosts -Value '127.0.0.1 dsc.local'

Let's see the three operations in action (from an elevated shell, the hosts file is protected):

# Runs getScript only
dsc config get --file hosts.dsc.yaml

# Runs testScript only, nothing is changed
dsc config test --file hosts.dsc.yaml

# Runs testScript first, and setScript only when the test fails
dsc config set --file hosts.dsc.yaml

# Run it again: testScript now returns true and setScript is skipped
dsc config set --file hosts.dsc.yaml

The second set is the one that matters. Nothing happens, because the machine already looks the way we described. That idempotency is not something DSC gives you for free here. It is the result of a good testScript.

Remark: PowerShellScript runs in PowerShell 7. If your script depends on modules or features that only exist in Windows PowerShell, use Microsoft.DSC.Transitional/WindowsPowerShellScript. Same properties, but it runs in 5.1.

Using the output of setScript

Whatever your setScript returns ends up in the result of the resource. Use --output-format pretty-json and look at afterState.output:

$result = dsc config set --file hosts.dsc.yaml --output-format pretty-json | ConvertFrom-Json -Depth 20
$result.results.result.afterState.output

This is useful when a script creates something you need later, like the thumbprint of a certificate, and you want to hand it to the next step in your pipeline.

When not to use it

If all you need is to run something once during set, and you do not care about get or test, RunCommandOnSet is simpler.

If you find yourself copying the same three scripts into every configuration, that is the signal to build a real resource. Inline scripts are hard to test and hard to reuse. Use them as glue, not as a foundation!

More information

Popular posts from this blog

Podman– Command execution failed with exit code 125

After updating WSL on one of the developer machines, Podman failed to work. When we took a look through Podman Desktop, we noticed that Podman had stopped running and returned the following error message: Error: Command execution failed with exit code 125 Here are the steps we tried to fix the issue: We started by running podman info to get some extra details on what could be wrong: >podman info OS: windows/amd64 provider: wsl version: 5.3.1 Cannot connect to Podman. Please verify your connection to the Linux system using `podman system connection list`, or try `podman machine init` and `podman machine start` to manage a new Linux VM Error: unable to connect to Podman socket: failed to connect: dial tcp 127.0.0.1:2655: connectex: No connection could be made because the target machine actively refused it. That makes sense as the podman VM was not running. Let’s check the VM: >podman machine list NAME         ...

Cache stampede: when our cache turned against us

While investigating some performance issues, we ran into an ASP.NET Core API that cached a fairly expensive aggregation query for 60 seconds. Under normal load, that was fine: one request rebuilds the cache, everyone else reads from it. Under peak load, dozens of requests would arrive in that same expiry window, all see a cache miss, and all fire the same expensive query in parallel. The database didn't like that. That was the moment when our caching layer stopped helping and started hurting. A burst of requests comes in at the same time, all miss the cache, and all go hammer the database or the downstream API at once. That's a cache stampede . The cache was supposed to protect our backend, and for a few hundred milliseconds it did the opposite. Why this happens IMemoryCache.GetOrCreate (and its async sibling) looks like it protects you, but it doesn't add any locking on its own. Look at the naive version: public async Task<Report> GetReportAsync(string key) ...

VS Code Planning mode

After the introduction of Plan mode in Visual Studio , it now also found its way into VS Code. Planning mode, or as I like to call it 'Hannibal mode', extends GitHub Copilot's Agent Mode capabilities to handle larger, multi-step coding tasks with a structured approach. Instead of jumping straight into code generation, Planning mode creates a detailed execution plan. If you want more details, have a look at my previous post . Putting plan mode into action VS Code takes a different approach compared to Visual Studio when using plan mode. Instead of a configuration setting that you can activate but have limited control over, planning is available as a separate chat mode/agent: I like this approach better than how Visual Studio does it as you have explicit control when plan mode is activated. Instead of immediately diving into execution, the plan agent creates a plan and asks some follow up questions: You can further edit the plan by clicking on ‘Open in Editor’: ...