From CLI to Chat command in 5 minutes

Patrick Van Stee
Operable News
Published in
6 min readFeb 17, 2017

--

I’m sure some of you heard about ChatOps and immediately thought, “That sounds really cool. But, our team already built a bunch of command line tools to manage our infrastructure. You mean we have to rewrite all of those just to run them from chat?” Well, for the most part, you’re not wrong. But, because Cog lets you write commands in any language and commands can specify a docker container to run in, you can quickly wrap your existing tooling in a simple bash script in less than five minutes. To prove it, I’ll take dig, the DNS lookup command we're all familiar, and turn it into a chat command.

Starting with the basics

Let’s start with a simple invocation that passes a single name argument for our query. Here’s what that looks like on the command line:

dig operable.io

and from chat we want to be able to run:

@cog dig operable.io

If you’ve written a command-line script before, writing a Cog command should feel familiar with two main differences: command invocations from chat are parsed by Cog and all arguments and options are passed down to the executable as environment variables, and STDOUT can be specified as JSON or left as raw text and automatically converted to a list of strings. For all the details of how Cog commands work, you can take a look at Cog’s command documentation, but that’s all you should need to know to get started.

So let’s write that command. We just want to take a single argument and pass that directly to dig as-is. To do that we'll need a simple wrapper bash script that takes the value of COG_ARGV_0 and passes it to dig. Open up dig.sh and paste the following:

#!/usr/bin/env bashdig "$COG_ARGV_0"

Now, to install and run this command, we need to define a bundle config. Create a file named config.yaml and add the following:

cog_bundle_version: 4
name: dig
version: "0.1.0"
description: DNS lookup utility
docker:
image: dig
tag: "0.1.0"
commands:
dig:
executable: "/dig.sh"
description: DNS lookup utility
rules:
- allow

This defines the dig bundle with a single command, also named dig. It also includes a docker image to run each command in. Let's build that next by creating a Dockerfile.

FROM alpine:latestRUN apk -U add bind-tools bash
COPY dig.sh /
RUN chmod +x /dig.sh

All we’re doing here is installing dig (via the bind-tools package) and bash, copying over our newly created bash script named dig.sh and making that executable. Now the only thing we have left is to build this image and use the tag specified in our bundle config.

docker build -t dig:0.1.0 .

Ok, we’re ready to install it with cogctl assuming you’ve already got Cog up and running. (If not, do that real quick by following our Installation Guide)

cogctl bundle install config.yaml --enable --relay-group default

To check that everything worked. You should be able to run the command and see the following response in chat.

Adding options

Ok now that we have the basics working, let’s add a couple of command options. We want to be able to set the name server and the type of record we’re querying for. You could use positional arguments instead but options give us a nice place to hang extra documentation and allow us to write rules against these values more easily (for instance we could only allow users with the admin permission to query for axfr records). To do this we’ll need to make changes to our script and bundle config.

Since Cog parses options for us, we don’t need to use getopts or anything, but we do need to transpose some environment variables into positional arguments that dig exepects; COG_OPT_SERVER to the first argument if it exists and COG_OPT_TYPE to the last argument if it exists. Our new dig.sh should look like this:

#!/usr/bin/env bashdeclare -a argumentsif [ "$COG_OPT_SERVER" != "" ]; then
arguments+=("@${COG_OPT_SERVER}")
fi
if [ "$COG_ARGV_0" != "" ]; then
arguments+=("$COG_ARGV_0")
fi
if [ "$COG_OPT_TYPE" != "" ]; then
arguments+=("$COG_OPT_TYPE")
fi
dig "${arguments[@]}"

We also need to tell Cog about these options in our bundle config.

...
commands:
dig:
executable: "/dig.sh"
description: DNS lookup utility
options:
server:
type: string
required: false
type:
type: string

required: false
rules:
- allow

I’d recommend you always include a description and short_flag attribute for options, but I've omitted them here to keep this short.

Now, we just need to reinstall our bundle and we can run our new command from chat.

cogctl bundle install config.yaml --force

Emitting JSON

To really make a Cog command useful, it needs to emit JSON data rather than strings. That way, you can use it in a pipeline and other commands can work with the data it outputs. For example, imagine you wanted to run a health check for all the IP addresses returned from a dig query. If dig returned JSON, we could pipe the output to a curlcommand to perform the healthcheck.

dig operable.io | curl --write-out "%{http_code}" "http://$ip"

Seems pretty useful, so let’s make our bash script print out some JSON. All we have to do is print “JSON” (and a newline) to STDOUT followed by a JSON list and Cog will do the rest. But, the output dig gives us is mostly tabular, which would be pretty annoying to parse, even if you've memorized the sed manpage. Instead let's limit the output dig gives us with some extra options and parse the remaining table of answer data with jq.

Oh and first, well need to add jq to our Dockerfile real quick.

RUN apk -U add bind-tools bash jq

And now for the command bits:

#!/usr/bin/env bashdeclare -a arugmentsarguments+=("+noall")
arguments+=("+answer")
if [ "$COG_OPT_SERVER" != "" ]; then
arguments+=("@${COG_OPT_SERVER}")
fi
if [ "$COG_ARGV_0" != "" ]; then
arguments+=("$COG_ARGV_0")
fi
if [ "$COG_OPT_TYPE" != "" ]; then
arguments+=("$COG_OPT_TYPE")
fi
read -r -d '' filter <<'EOF'
split("\n") |
.[0:-1] |
map(split("\t")) |
map({"name": .[0], "type": .[4], "ip": .[5]})
EOF
echo "JSON"dig "${arguments[@]}" |
jq --raw-input --slurp "$filter"

That jq filter is a little intense, but now when you run your command, you should see glorious JSON. And only after a pretty conservative 30 lines of bash, we have dig in chat.

Where to go from here

There are a few things I didn’t have time for in this post: logging from and debugging commands, applying rules to command options and arguments, and formatting JSON output with templates just to start. But, we cover those in detail in our full-featured command walkthrough. So, check that out and let us know what commands you’ve ported over to Cog, or even upload them to Warehouse so the community can give them a shot.

More ways to follow Cog:

Operable | Documentation | Twitter | Slack | Newsletter

--

--