DEV Community

Olivier Miossec
Olivier Miossec

Posted on • Edited on

Introduction to PowerShell Crescendo

There are a lot of administrative tasks you cannot do with PowerShell alone, especially on Unix platforms like macOS or Linux. For day-to-day tasks, it is not a problem, you will use native applications and return to PowerShell when needed. But what if you need to integrate these native applications in a more complex PowerShell solution.
There are several approaches for that, check this blog post.
But recently Microsoft announced a new module, Crescendo, to better support these native commands, see this blog post here.

This module, Crescendo, is a framework. It let you create a cmdlet with a Verb-Noum naming convention, PowerShell parameters mapped to the command parameters, and output the result as an object.

To install the module, you must use PowerShell 7.X

Install-Module Microsoft.PowerShell.Crescendo
Enter fullscreen mode Exit fullscreen mode

Now that the module is installed you can start to create your first cmdlet with it. For that, you need a JSON configuration file to describe how to use the target command, parameters, output, …
Let's take the same example I used to illustrate how to create a Kubectl plugin with PowerShell, listing pods by namespace.

The command is

kubectl get pods -o json
Enter fullscreen mode Exit fullscreen mode

It lists pods information of the Kubernetes cluster you are connected to.

On this command, you need to add a parameter to select a specific namespace, and if the parameter is not present, list pods from the default namespace.

The command looks like this:

kubectl get pods -o json –namespace XXX 
Enter fullscreen mode Exit fullscreen mode

The corresponding JSON configuration file look like:

{
"$schema": "https://raw.githubusercontent.com/PowerShell/Crescendo/master/Microsoft.PowerShell.Crescendo/src/Microsoft.PowerShell.Crescendo.Schema.json",
"Verb": "Get",
    "Noun":"k8spods", 
    "OriginalName": "kubectl", 
    "OriginalCommandElements": [
        "get",
        "pods",
        "-o",
        "json"
    ],
    "Parameters": [  
        {
            "Name":"namespace",
            "OriginalName": "--namespace",
            "ParameterType": "string",
            "Description": "Namespace name",
            "DefaultValue": "default"
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

The $Schema section is optional, but it will help you to write your configuration file in Visual Studio Code. You can use the local file (on macOs, $HOME/.local/share/powershell/Modules/Microsoft.PowerShell.Crescendo//Microsoft.PowerShell.Crescendo.Schema.json) or you can use the online version in GitHub (https://raw.githubusercontent.com/PowerShell/Crescendo/master/Microsoft.PowerShell.Crescendo/src/Microsoft.PowerShell.Crescendo.Schema.json).

The verb section will design your cmdlet Verb. You need to choose a valid PowerShell verb. You can use this command to get the list of valid verbs

Get-verb
Enter fullscreen mode Exit fullscreen mode

Noun is the second part of the cmdlet you want to create; you can choose anything you want.

The originalName is the name (or the path) to the command without any parameter.

If the command needs some default parameters to work, you will need to use the OriginalCommandElements. These parameters do not need to be part of your cmdlet but must be present during the execution of the command. It's an array.

And finally, parameters, it's an array of JSON objects. Each object will define a parameter in the cmdlet. You will need to provide a name, the name of the parameter in the PowerShell cmdlet, originalName, the corresponding name for the native command.
Other properties can help you to design the parameter:

  • Description, it will be added to the comment-based help of the function
  • DefaultValue, a default value for the parameter
  • ParameterType, any PowerShell value (String, Int, …) or Switch if the parameter doesn't need a value.
  • Mandatory, true, or false if you want to make the parameter mandatory

This configuration file is enough to create a cmdlet. It will create a function with parameters from the Parameters section and create a wraper for the native command.

To create the cmdlet:

Export-CrescendoModule -ConfigurationFile ./pods.json -ModuleName pods.psm1
Enter fullscreen mode Exit fullscreen mode

This will create the module file in the current directory (if the file exists, the cmdlet will raise an error).

You can use the cmdlet by using

Import-Module ./pods1.psm1
Enter fullscreen mode Exit fullscreen mode

You can now use the cmdlet

Get-k8spods -namespace MyNameSpace
Enter fullscreen mode Exit fullscreen mode

But if you use it you will have the direct output of the kubectl command. Can we do better and output a custom PowerShell object instead?

In the original post, the output from kubectl was converted from JSON data and retrieve only pod name, status (or phase), restart count, start DateTime, image, namespace node name, and node type.
You need to modify the output of the cmdlet. Until now, the configuration file is only related to how to run the command and create the cmdlet inputs. To modify the output, you need to add an outputHandlers property. It's an array of objects where each element contains two required properties, ParameterSetName, the name of the parameterSet associated with the output, and Handler the code to execute for the output.

The Handler property contains the code to execute against the native command output. The output as an argument. You can use the first element of the $args array ($args[0]) or use the param keyword with any parameter name.
Even if JSON doesn't support multiline string by default, PowerShell supports it, but it's not the case with Visual Studio Code and you can get some warnings you can ignore while editing your configuration file.

{
    "$schema": "https://raw.githubusercontent.com/PowerShell/Crescendo/master/Microsoft.PowerShell.Crescendo/src/Microsoft.PowerShell.Crescendo.Schema.json",
    "Verb": "Get",
    "Noun":"pods2", 
    "OriginalName": "kubectl", 
        "OriginalCommandElements": [
            "get",
            "pods",
            "-o",
            "json"
        ],
        "Parameters": [  
            {
                "Name":"namespace",
                "OriginalName": "--namespace",
                "ParameterType": "string",
                "Description": "Namespace name",
                "DefaultValue": "default",
                "Mandatory": true
            }
        ],
        "OutputHandlers": [
            {
            "ParameterSetName": "Default",
            "Handler":"param ( $kubectlOutput )
                $Podslist = $kubectlOutput | convertfrom-Json
                $PodsArrayList = [System.Collections.ArrayList]::new()
                foreach ($pod in $Podslist.items) {
                    [void]$PodsArrayList.Add([pscustomobject]@{ 
                        PodName                 =   $pod.metadata.name
                        Status                  =   $pod.status.phase
                        restartCount            =   $pod.status.containerStatuses[0].restartCount
                        StartTime               =   $pod.status.startTime
                        image                   =   $pod.status.containerStatuses[0].image
                        Node                    =   $pod.spec.nodeName 
                        NodeType                =   $pod.spec.nodeSelector
                        NameSpace               =   $pod.metadata.namespace
                    })
                }
                $PodsArrayList
            "
            }
        ]
    }
Enter fullscreen mode Exit fullscreen mode

The code in the Handler property converts the output from kubectl to a PowerShell object. A loop extract needed values and add them to a custom object before returning this object.

The result
Alt Text

The cmdlet is almost complete, but the kubectl command also accepts a --all-namespace. This switch excludes the --namespace parameter.
You need to add the parameter in the parameters section. To exclude the --namespace in the cmdlet you will need to add a parameterSetName property to each parameter.

        "Parameters": [  
            {
                "Name":"namespace",
                "OriginalName": "--namespace",
                "ParameterType": "string",
                "Description": "Namespace name",
                "DefaultValue": "default",
                "ParameterSetName": [
                    "default"
                ]
            },
            {
                "Name":"allNameSpace",
                "OriginalName": "--all-namespace",
                "ParameterType": "switch",
                "Description": "to get all namespace",
                "ParameterSetName": [
                    "allnamespace"
                ]
            }
        ]
Enter fullscreen mode Exit fullscreen mode

The first parameter, namespace, has been modified to add a parameter set name, default. The second parameter, allnamespace, is bind to the –all-namespaces switch, the parameter type is switch and the parameter is added to the allnamespace parameter set.

There is another thing to do. In the handler section, each object must have a parameterSetName property. This implies we need to create another item for the allnamespace paramterSet.

{
    "$schema": "https://raw.githubusercontent.com/PowerShell/Crescendo/master/Microsoft.PowerShell.Crescendo/src/Microsoft.PowerShell.Crescendo.Schema.json",
    "Verb": "Get",
    "Noun":"pods3", 
    "OriginalName": "kubectl", 
        "OriginalCommandElements": [
            "get",
            "pods",
            "-o",
            "json"
        ],
        "Parameters": [  
            {
                "Name":"namespace",
                "OriginalName": "--namespace",
                "ParameterType": "string",
                "Description": "Namespace name",
                "DefaultValue": "default",
                "ParameterSetName": [
                    "default"
                ]
            },
            {
                "Name":"allnamespace",
                "OriginalName": "--all-namespaces",
                "ParameterType": "switch",
                "Description": "to get all namespace",
                "ParameterSetName": [
                    "allnamespace"
                ]
            }
        ],
        "OutputHandlers": [
            {
            "ParameterSetName": "Default",
            "Handler":"param ( $kubectlOutput )
                $Podslist = $kubectlOutput | convertfrom-Json
                $PodsArrayList = [System.Collections.ArrayList]::new()
                foreach ($pod in $Podslist.items) {
                    [void]$PodsArrayList.Add([pscustomobject]@{ 
                        PodName                 =   $pod.metadata.name
                        Status                  =   $pod.status.phase
                        restartCount            =   $pod.status.containerStatuses[0].restartCount
                        StartTime               =   $pod.status.startTime
                        image                   =   $pod.status.containerStatuses[0].image
                        Node                    =   $pod.spec.nodeName 
                        NodeType                =   $pod.spec.nodeSelector
                        NameSpace               =   $pod.metadata.namespace
                    })
                }
                $PodsArrayList
            "
            },
            {
                "ParameterSetName": "allnamespace",
                "Handler":"param ( $kubectlOutput )
                    $Podslist = $kubectlOutput | convertfrom-Json
                    $PodsArrayList = [System.Collections.ArrayList]::new()
                    foreach ($pod in $Podslist.items) {
                        [void]$PodsArrayList.Add([pscustomobject]@{ 
                            PodName                 =   $pod.metadata.name
                            Status                  =   $pod.status.phase
                            restartCount            =   $pod.status.containerStatuses[0].restartCount
                            StartTime               =   $pod.status.startTime
                            image                   =   $pod.status.containerStatuses[0].image
                            Node                    =   $pod.spec.nodeName 
                            NodeType                =   $pod.spec.nodeSelector
                            NameSpace               =   $pod.metadata.namespace
                        })
                    }
                    $PodsArrayList
                "
                }
        ]
    }
Enter fullscreen mode Exit fullscreen mode

The module created by the Crescendo framework is only a PSM1 file with one function. You may want to create a full module with it or integrate the created function into your project.

The Crescendo framework is still in preview and you can find bugs and limitations. You can view the project and open issue on the project's GitHub pages

Top comments (1)

Collapse
 
antdimot profile image
Antonio Di Motta

Very interesting. I saw a demo of crescendo I had immediately thought to kubectl use case.