Every PowerShell Desired State Configuration resource must have at least one Key
property that’s used to uniquely identify it within a single configuration. For the DSC Script
Resource the keys are the GetScript
, TestScript
, and SetScript
properties. Basically this means that each Script
resource can’t contain the same content. Makes sense on the surface, but when you consider variable substitution, and looping through collections in $ConfigurationData
, it’s easy to come up with a configuration that ends up with this error:
Add-NodeKeys : The key properties combination ‘your script here’ is duplicated for keys ‘GetScript,SetScript,TestScript’ of resource ‘Script’ in node ‘nodename’. Please make sure key properties are unique for each resource in a node.
This is quite annoying, but there are ways around it so you don’t have to resort to manually unrolling your loop.
Error Demonstration
Consider the following contrived sample configuration:
Assuming the configuration data for the node contains more than one entry in its values
key, this will cause the error above.
What’s going wrong?
The problem is that your loop is generating multiple Script
resources, and each of them appear to have the same values for the key properties, but the point of embedding the current iteration’s $value
in the script with $Using:value
is specifically to generate a unique set of [ScriptBlock]
’s for each iteration.
The problem is that the script blocks are being checked before the $Using
variables are replaced. If the check were not being done at all, the generated MOF would end up being correct.
[ScriptBlock]
vs [String]
and variable replacement
It has been pointed out that the *Script
properties on this resource are actually [String]
types. It’s useful to assign a [ScriptBlock]
because it makes it much easier to write the code in an editor: you get proper syntax highlighting and tab completion and intellisense. But if you wanted to you could just use a string.
That means you could use variable substitution and the values you want to be unique would be embedded. So you could do something like this:
You can see that this kind of sucks.
-
You get no source editing features
-
You have to be careful to escape special characters. If you miss one, you’ll be embedding a variable you didn’t intend to (which will probably end up being
$null
and it will be hard to find. The bigger the code, the worse this becomes (though arguably at that point you shouldn’t be using theScript
resource anymore). And it’s not just$
, you’ll also have to replace the backtick itself, double quotes, etc.
You could use single quotes with the-format
operator and use{0}
,{1}
, etc., but then you have to escape single quotes and curly braces. -
You have to be aware of the variable type
-
You can’t easily embed complex objects
This is not ideal.
Using the Power of $Using
If you take a look at a MOF file that’s generated from the use of a Script
resource with $Using
variables, you’ll be able to see the actual [String]
s that are generated by the process.
As an even smaller example let’s look at this GetScript
property:
In the MOF, you’ll end up with a script resource that looks something like this:
So essentially what’s happened here is that PowerShell serialized the object into CliXML (the same way it would if you had used Export-CliXml
) then embedded the deserialization call with the hardcoded XML string into the script block that gets put in the MOF. Pretty clever actually.
$Using
this to our advantage
If DSC’s validation of uniqueness is happening before the substitution, then we can do the substitution ourselves by serializing the object and doing the same substitution before we pass it to the Script
resource’s property.
I wrote a function that does just that. Here’s a gist so you can use git to clone it or just copy it, whichever is easiest:
Function Usage
That’s all there is to it! Write your script block like you normally would (with $Using
), but pass it to the Replace-Using
function either by parameter or by pipeline. All of the $Using
variables will be replaced by serialized representations (with deserialization code embedded).
Function Breakdown
This function takes a single [String]
parameter that contains the code (passing in a [ScriptBlock]
works because it gets cast).
I use a specific overload of [RegEx]::Replace()
that takes the string the search, the pattern to search for, and critically, a callback function to process the replace.
I create a script block within the function that acts as the callback function. The RegEx pattern I’m using looks for $Using:
and then captures the part after the colon, which is the variable name. Within the callback I retrieve the value of that variable with Get-Variable
and serialize it with the built-in serializer.
Then we just return the string that we want to use to replace the match (which is the entire $Using:variable
part of the code block). For the replacement, I make sure to embed the Deserialize
call, and I wrap the whole thing in $()
because that will help it work better in more situations (like if you embedded your variable in a string: "A powershell string containing $Using:myvalue is useful."
).
Caveats
I can’t be certain that what I’m doing will embed cleanly into every single place where you could shove a $Using
variable so you might want to just do something simple with it like assign your $Using
variables to local script variables and then use the local from then on. It’s easier to type and to debug that way anyway:
For now using this in a DSC Script Resource is the only time I can think of where it makes sense to do this manually, but I’d be interested to hear of other uses.