Native Commands in PowerShell
A New Approach – Part 2

In my last post I went through some some strategies for executing native executable and having them participate more fully in the PowerShell environment. In this post, I’ll be going through a couple of experiments I’ve done with the kubernetes kubectl utility.

Is there a better way

It may be possible to create a framework that inspects the help of the application and automatically creates the code that calls the underlying application. This framework can also handle the output mapping to an object more suitable for the PowerShell environment.

Possibilities in wrapping

The aspect that makes this possible is that some commands have consistently structured help that describes how the application can be used. If this is the case, then we can iteratively call the help, parse it, and automatically construct much of the infrastructure needed to allow these native applications to be incorporated into the PowerShell environment.

First Experiment – Microsoft.PowerShell.Kubectl Module

I created a wrapper for to take the output of kubectl api-resources and create functions for each returned resource. This way, instead of running kubectl get pod; I could run Get-KubectlPod (a much more PowerShell-like experience). I also wanted to have the function return objects that I could then use with other PowerShell tools (Where-Object, ForEach-Object, etc). To do this, I needed a way to map the output (JSON) of the kubectl tool to PowerShell objects. I decided that it was reasonable to use a more declarative approach that maps the property in the JSON to a PowerShell class member.

There were some problems that I wanted to solve with this first experiment

  • wrap kubectl api-resources in a function
    • automatically create object output from kubectl api-resources
  • Auto-generate functions for each resource that could be retrieved (only resource get for now)
    • only support name as a parameter
  • Auto-generate the conversion of output to objects to look similar to the usual kubectl output

When it came to wrapping kubectl api-resources I took the static approach rather than auto generation. First, because it was my first attempt so I was still finding my feet. Second, because this is one of the kubectl commands that does not emit JSON. So, I took the path of parsing the output of kubectl api-resources -o wide. My concern is that I wasn’t sure whether the table changes width based on the screen width. I calculated column positions based on the fields I knew to be present and then parsed the line using the offsets. You can see the code in the function get-kuberesource and the constructor for the PowerShell class KubeResource. My plan was that these resources would drive the auto-generation of the Kubernetes resource functions.

Now that I have retrieved the resources, I can auto-generate specific resource function for calling the kubectl get . At the time, I wanted some flexibility in the creation of these proxy functions, so I provided a way to include a specific implementation, if desired (see the $proxyFunctions hashtable). I’m not sure that’s needed now, but we’ll get to that later. The problem is that while the resource data can be returned as JSON, that JSON has absolutely no relation to the way the data is represented in the kubectl get pod table. Of course, in PowerShell we can create formatting to present any member of an object (JSON or otherwise), but I like to be sure that the properties seen in a table are properties that I can use with Where-Object, etc. Since, I want to return the data as objects, I created classes for a couple resources by hand but thought there might be a better way.

I determined that when you get data from kubernetes, the table (both normal and wide) output is created on the server. This means the mapping of the properties of the JSON object to the table columns is defined in the server code. It’s possible to provide data as custom columns, but you need to provide the value for the column using a JSON path expression. So, it’s not possible to automatically generate those tables. However, I thought it might be possible to provide a configuration file that could be read to automatically generate a PowerShell class. The configuration file would need to define the mapping between the property in the table with the properties of the object. The file would include the name of the column and the expression to get the value for the object. This allows a user to retrieve the JSON object and construct their custom object without touching the programming logic of the module but a configuration file. I created the ResourceConfiguration.json file to encapsulate all the resources that I had access to and provide a way to customize the object members where desired.

here’s an example:

  {
    "TypeName": "namespaces",
    "Fields": [
      {
        "PropertyName": "NAME",
        "PropertyReference": "$o.metadata.NAME"
      },
      {
        "PropertyName": "STATUS",
        "PropertyReference": "$o.status.phase"
      },
      {
        "PropertyName": "AGE",
        "PropertyReference": "$o.metadata.creationTimeStamp"
      }
    ]
  },

This JSON is converted into a PowerShell class whose constructor takes the JSON object and assigns the values to the members, according to the PropertyReference. The module automatically attaches the original JSON to a hidden member originalObject so if you want to inspect all the data that’s available, you can. The module also automatically generates a proxy function so you can get the data:

function Get-KubeNamespace
{
  [CmdletBinding()]
  param ()
  (Invoke-KubeCtl -Verb get -resource namespaces).Foreach({[namespaces]::new($_)})
}

This function is then exported so it’s available in the module. When used, it behaves very close to the original:

PS> Get-KubeNamespace

Name                 Status Age
----                 ------ ---
default              Active 5/6/2020 6:13:07 PM
default-mem-example  Active 5/14/2020 8:14:45 PM
docker               Active 5/6/2020 6:14:25 PM
kube-node-lease      Active 5/6/2020 6:13:05 PM
kube-public          Active 5/6/2020 6:13:05 PM
kube-system          Active 5/6/2020 6:13:05 PM
kubernetes-dashboard Active 5/18/2020 8:44:01 PM
openfaas             Active 5/6/2020 6:51:22 PM
openfaas-fn          Active 5/6/2020 6:51:22 PM

PS> kubectl get namespaces --all-namespaces

NAME                   STATUS   AGE
default                Active   26d
default-mem-example    Active   18d
docker                 Active   26d
kube-node-lease        Active   26d
kube-public            Active   26d
kube-system            Active   26d
kubernetes-dashboard   Active   14d
openfaas               Active   26d
openfaas-fn            Active   26d

but importantly, I can use the output with Where-Object and ForEach-Object or change the format to list, etc.

PS> Get-KubeNamespace |? name -match "faas"

Name        Status Age
----        ------ ---
openfaas    Active 5/6/2020 6:51:22 PM
openfaas-fn Active 5/6/2020 6:51:22 PM

Second Experiment – Module KubectlHelpParser

I wanted to see if I could read any help content from kubectl that would enable me to auto-generate a complete proxy of the kubectl command that included general parameters, command specific parameters, and help. It turns out that kubectl help is regular enough that this is quite possible.

When retrieving help, kubectl provides subcommands that also have structured help. I created a recursive parser that allowed me to retrieve all of the help for all of the available kubectl commands. This means that if an additional command is provided in the future, and the help for that command follows the existing pattern for help, this parser will be able to generate a command for it.

PS> kubectl --help
kubectl controls the Kubernetes cluster manager.

 Find more information at: https://kubernetes.io/docs/reference/kubectl/overview/

Basic Commands (Beginner):
  create         Create a resource from a file or from stdin.
  expose         Take a replication controller, service, deployment or pod and expose it as a new Kubernetes Service
  run            Run a particular image on the cluster
  set            Set specific features on objects

Basic Commands (Intermediate):
  explain        Documentation of resources
  get            Display one or many resources
. . .

kubectl set --help

PS> kubectl set --help

Configure application resources

 These commands help you make changes to existing application resources.

Available Commands:
  env            Update environment variables on a pod template
  . . .
  subject        Update User, Group or ServiceAccount in a RoleBinding/ClusterRoleBinding

Usage:
  kubectl set SUBCOMMAND [options]

PS> kubectl set env --help

Update environment variables on a pod template.

 List environment variable definitions in one or more pods, pod templates. Add, update, or remove container environment
variable definitions in one or more pod templates (within replication controllers or deployment configurations). View or
modify the environment variable definitions on all containers in the specified pods or pod templates, or just those that
match a wildcard.

 If "--env -" is passed, environment variables can be read from STDIN using the standard env syntax.

 Possible resources include (case insensitive):

  pod (po), replicationcontroller (rc), deployment (deploy), daemonset (ds), job, replicaset (rs)

Examples:
  # Update deployment 'registry' with a new environment variable
  kubectl set env deployment/registry STORAGE_DIR=/local
  . . .
  # Set some of the local shell environment into a deployment config on the server
  env | grep RAILS_ | kubectl set env -e - deployment/registry

Options:
      --all=false: If true, select all resources in the namespace of the specified resource types
      --allow-missing-template-keys=true: If true, ignore any errors in templates when a field or map key is missing in
the template. Only applies to golang and jsonpath output formats.
  . . .
      --template='': Template string or path to template file to use when -o=go-template, -o=go-template-file. The
template format is golang templates [http://golang.org/pkg/text/template/#pkg-overview].

Usage:
  kubectl set env RESOURCE/NAME KEY_1=VAL_1 ... KEY_N=VAL_N [options]

Use "kubectl options" for a list of global command-line options (applies to all commands).

The main function of the module will recursively collect the help for all of the commands and construct an object representation that I hope can then be used to generate the proxy functions. This is still very much a work in progress, but it is definitely showing promise. Here’s an example of what it can already do.

PS> import-module ./KubeHelpParser.psm1
PS> $res = get-kubecommands
PS> $res.subcommands[3].subcommands[0]
Command             : set env
CommandElements     : {, set, env}
Description         : Update environment variables on a pod template.

                       List environment variable definitions in one or more pods, pod templates. Add, update, or remove container environment variable definitions in one or more pod templates (within replication controllers or deployment configurations). View or modify the environment variable definitions
                      on all containers in the specified pods or pod templates, or just those that match a wildcard.

                       If "--env -" is passed, environment variables can be read from STDIN using the standard env syntax.

                       Possible resources include (case insensitive):

                        pod (po), replicationcontroller (rc), deployment (deploy), daemonset (ds), job, replicaset (rs)
Usage               : kubectl set env RESOURCE/NAME KEY_1=VAL_1 ... KEY_N=VAL_N [options]
SubCommands         : {}
Parameters          : {[Parameter(Mandatory=$False)][switch]${All}, [Parameter(Mandatory=$False)][switch]${NoAllowMissingTemplateKeys}, [Parameter(Mandatory=$False)][System.String]${Containers} = "*", [Parameter(Mandatory=$False)][switch]${WhatIf}…}
MandatoryParameters : {}
Examples            : {kubectl set env deployment/registry STORAGE_DIR=/local, kubectl set env deployment/sample-build --list, kubectl set env pods --all --list, kubectl set env deployment/sample-build STORAGE_DIR=/data -o yaml…}
PS> $res.subcommands[3].subcommands[0].usage
Usage                                                               supportsFlags hasOptions
-----                                                               ------------- ----------
kubectl set env RESOURCE/NAME KEY_1=VAL_1 ... KEY_N=VAL_N [options]         False       True
PS> $res.subcommands[3].subcommands[0].examples
Description                                                   Command
-----------                                                   -------
Update deployment 'registry' with a new environment variable  kubectl set env deployment/registry STORAGE_DIR=/local
. . .

PS> $res.subcommands[3].subcommands[0].parameters.Foreach({$_.tostring()})

[Parameter(Mandatory=$False)][switch]${All}
[Parameter(Mandatory=$False)][switch]${NoAllowMissingTemplateKeys}
[Parameter(Mandatory=$False)][System.String]${Containers} = "*"
[Parameter(Mandatory=$False)][switch]${WhatIf}
. . .
[Parameter(Mandatory=$False)][System.String]${Selector}
[Parameter(Mandatory=$False)][System.String]${Template}

There are still a lot of open questions and details to work out here:

  • how are mandatory parameters determined?
  • how do we keep a map of used parameters?
  • does parameter order matter?
  • can reasonable debugging be provided?
  • do we have to “boil the ocean” to provide something useful?

I believe it may be possible to create a more generic framework which would allow a larger number native executables to be more fully incorporated into the PowerShell ecosystem. These are just the first steps in the investigation, but it looks very promising.

Call To Action

First, I’m really interested in knowing that having a framework that can auto-generate functions that wrap a native executable is useful. The obvious response might be “of course”, but how much of a solution is really needed to provide value? Second, I would really like to know if you would like us to investigate specific tools for this sort of treatment. If it is possible to make this a generic framework, I would love to have more examples of tools which would be beneficial to you and test our ability to handle.

James Truher
Software Engineer
PowerShell Team

The post Native Commands in PowerShell – A New Approach – Part 2 appeared first on PowerShell.