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
- automatically create object output from
- Auto-generate functions for each resource that could be retrieved (only resource get for now)
- only support
name
as a parameter
- only support
- 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
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.
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...
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....
Please investigate git.exe, netsh.exe, robocopy.exe, diskpart.exe and dism.exe (not in that order necessarily)
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.
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...
Wrapping native executables is a really interesting approach. Comes to my mind that this could be useful with vagrant