Introduction to PowerShell Crescendo
Tags: powershell, crescendo
Published: 2020-12-14 10:35:01

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]( https://devblogs.microsoft.com/powershell/native-commands-in-powershell-a-new-approach/?WT.mc_id=AZ-MVP-5003620).
But recently Microsoft announced a new module, Crescendo, to better support these native commands, see this blog post [here]( https://devblogs.microsoft.com/powershell/announcing-powershell-crescendo-preview-1/?WT.mc_id=AZ-MVP-5003620).

This module, [Crescendo](https://github.com/PowerShell/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 

```powershell
Install-Module Microsoft.PowerShell.Crescendo
```
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](https://dev.to/omiossec/writing-kubectl-plugin-with-powershell-4c25), listing pods by namespace. 

The command is 

```bash
kubectl get pods -o json
```

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:

```bash
kubectl get pods -o json –namespace XXX 
```

The corresponding JSON configuration file look like: 
```json
{
"$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"
        }
    ]
}
``` 

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/<version>/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

```powershell
Get-verb
```
**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: 

```powershell
Export-CrescendoModule -ConfigurationFile ./pods.json -ModuleName pods.psm1
```
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 

```powershell
Import-Module ./pods1.psm1
```

You can now use the cmdlet

```powershell
Get-k8spods -namespace MyNameSpace
```

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. 

```json
{
    "$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
            "
            }
        ]
    }
```
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](https://dev-to-uploads.s3.amazonaws.com/i/a6zeojx4vj0wtax5fn1o.png)

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. 

```json
        "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"
                ]
            }
        ]
```
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. 

```json
{
    "$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
                "
                }
        ]
    }
``` 


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]( https://github.com/PowerShell/Crescendo)