Native Commands in PowerShell – A New Approach – Part 2

Jim Truher

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 <resource>. 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

7 comments

Discussion is closed. Login to edit/delete existing comments.

  • tora 0

    Wrapping native executables is a really interesting approach. Comes to my mind that this could be useful with vagrant

  • Rob Cannon 0

    I don’t think that wrapping native commands is that great of an idea. If I need to look up how to do something with kubectl, for instance, I am going to get information how to use kubectl and not Update-KubeNamespace. What would be great (and somewhat magical) would be a way to register a shim that translate output of a given command so it can be consumed by the rest of the PowerShell pipeline.

    Scenario 1: I run ‘kubectl get namespaces –all-namespaces’, and it returns the normal output of the command.

    Scenario 2: I run ‘kubectl get namespaces –all-namespaces | ?{ $_.name -like myapp } | Select Age’
    1) Powershell would detect that the command is being run in a pipeline context and let the registered shim get control of the execution
    2) The shim could examine the parameters and determine this is going to return namespaces
    3) The shim would, invisibly, add the ‘–format=json’ argument to the execution so it can deal with the output
    4) It would translate the json into a pscustomobject so it can be dealt with in the rest of the pipeline

    Also, it would be great if the shim could handle error output and exit numbers so the errors get translated into powershell exceptions

  • Tony Ward 0

    Very interesting.

    Salesforce has a native cli called SFDX which uses the oclif standard and can return output in Json.

    I am a Salesforce Developer and PowerShell advocate, so I’ve written a PowerShell wrapper around SFDX called PSFDX.
    https://github.com/ZenInternet/psfdx

    It’s a very similar approach, except I use my PowerShell module on a daily basis… so instead of investing time dynamically generating wrappers I’ve hard coded the ones I need.

  • Kris Borowinski 0

    Please investigate git.exe, netsh.exe, robocopy.exe, diskpart.exe and dism.exe (not in that order necessarily)

  • Sergey Dedik 0

    With all respect and admiration to the work done, my opinion is that this is not the right approach. Personally, I would refrain from using auto-generated wrapper functions as much as possible in favor of using tools native.

    1. The whole reliance on structure, consistency, accuracy and completeness of tool’s integrated help is not reliable enough. There are many examples of tool’s switches and options being mutually exclusive. Those rules are hard to figure out programmatically. Often tool’s parameters are very sensitive to quotes and double-quotes, which is also hard to figure out precisely from help text and is always a PITA with all kinds of wrappers.
    2. The arguments that a person would rather google usage examples and query tool’s help directly than try to figure out how to use it via wrapper function has already been mentioned.
    3. General wrapper problems of our world: each wrapper reduce reliability and require more things for a person to learn. To figure out if my command doesn’t work because I messed up syntax or because wrapper has a problem might not be trivial. The closer I am to the tool itself – the better. Knowledge of how to manage K8 with native kubectl is transferable and universal to every K8 installation. K8 installation without kubectl is nonsense; K8 installation with kubectl, but without wrapper functions – very easy to imagine.
    4. Both native tool and wrapper function are aimed to solve the same problem – provide administrator friendly way of interacting with a system. Wrapper function gives very little value for the task, but introduces dependencies and more potential ways to fail.

  • Kirk Munro 0

    In general, I think investing in wrappers like this is the wrong approach.

    From my perspective is far more beneficial for both the scripter and the ecosystem to work from a single, consistent set of tools. PowerShell ends up being more welcoming to non-PowerShell users by being inclusive and respective of existing communities of users that work with native commands the way they were written than by wrapping those commands with a different syntax.

    The only place where I see investment value when it comes to the use of native commands in PowerShell is in providing a shim that converts output into objects so that you can then do additional things with them in PowerShell more easily.

  • 塩田 紳二 0

    Please consider character encodings that native command outputs. Various character codes are output in various language environments of Windows. For example, curl.exe outputs html text with various character codes depending on the URL.

Feedback usabilla icon