Skip to content

Instantly share code, notes, and snippets.

@nilium
Created August 11, 2019 07:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nilium/fce51f20bd98832868c5bcb9f4220d94 to your computer and use it in GitHub Desktop.
Save nilium/fce51f20bd98832868c5bcb9f4220d94 to your computer and use it in GitHub Desktop.
Bash script and jq module to make it easier to make small adjustments to Nomad jobs
#!/usr/bin/env bash
# Copyright Noel Cower 2019.
# Distributed under the Boost Software License, Version 1.0.
# (See accompanying file LICENSE_1_0.txt or copy at
# https://www.boost.org/LICENSE_1_0.txt)
# Usage: nomad-tweak <job> [options...] <jq-script>
#
# Options:
# -A, --apply
# Apply changes made to the job by sending an update request.
# -D, --dryrun (default)
# Do not apply changes made to the job and instead print the updated JSON.
# -r, -c, -S, --raw, --compact, ...
# Most jq arguments.
# VAR=VAL
# Shorthand for --arg VAR VAL. If VAL can be parsed as JSON, it is passed
# using --argjson instead.
# VAR=@FILE
# Same as VAR=VAL, but VAL is loaded from FILE.
#
# The jq-script passed is always prefixed with 'include "nomad";', and as such,
# all nomad.jq functions are available in the global namespace.
#
# Example: simple script to scale a Nomad job.
# This script could use more filtering of arguments, such as to avoid someone
# passing a '--' ahead of the script, but works as an example.
#
# #!/bin/bash
# # nomad-scale JOB GROUP COUNT [-a|--dry-run|...]
# set -e
# job="$1"
# group="$2"
# count="$3"
# shift 3
# exec nomad-tweak "$job" \
# group="$group" \
# count="$count" \
# "$@" -- \
# 'group($group; .Count = $count)'
set -e
job="$(mktemp)"
trap 'rm "${job}"' EXIT
id="$(urlcode "$1")"
shift
args=()
pusharg() {
for it; do
args[${#args[@]}]="$it"
done
}
apply=0
script='include "nomad"; . as $job'
scriptfile=
for arg; do
if [ $# -eq 1 ]; then
break
fi
case "$arg" in
--)
shift
break
;;
-apply|--apply|-A)
apply=1
;;
-dryrun|-dry-run|--dry-run|--dryrun|-D)
apply=0
;;
-[rcS]|--raw|--compact|--tab|--sort-keys)
pusharg "$arg"
;;
-f|-from-filename|--from-filename)
extra="${extra:+| }$(cat "${arg#-f}")"
shift
;;
-L)
pusharg "$arg" "$2"
shift
;;
-L?*)
pusharg "$arg"
;;
--arg|--argjson|--slurpfile)
pusharg "$arg" "$2" "$3"
shift 2
;;
-arg|-argjson|-slurpfile)
pusharg "-$arg" "$2" "$3"
shift 2
;;
?*=@*)
argname="${arg%%=*}"
arg="$(cat "${arg#*=}")"
if json="$(printf '%s' "$arg" | jq -c . 2>/dev/null)"; then
pusharg --argjson "$argname" "$arg"
else
pusharg --arg "$argname" "$arg"
fi
;;
?*=*)
argname="${arg%%=*}"
arg="${arg#*=}"
if json="$(printf '%s' "$arg" | jq -c . 2>/dev/null)"; then
pusharg --argjson "$argname" "$arg"
else
pusharg --arg "$argname" "$arg"
fi
;;
*)
break
;;
esac
shift
done
# Create script argument
pusharg "$script ${extra:-| .} $(printf '| %s' "$@")"
# Request job JSON
curl --silent --get --header 'Accept: application/json' "$NOMAD_ADDR/v1/job/$id" > "$job"
# Modify the job, then copy the JobModifyIndex (from the unmodified JSON) and set EnforceIndex
jq "${args[@]}" "$job" |
jq --argjson index "$(jq .JobModifyIndex "$job")" '{ JobModifyIndex: $index, EnforceIndex: true, Job: . }' |
case "$apply" in
0)
jq .
;;
1)
curl --silent --request POST --data-binary @- --header 'Content-Type: application/json' "$NOMAD_ADDR/v1/job/$id" |
jq .
;;
*)
echo "Unexpected state: apply=$apply" >&2
exit 1
;;
esac
# Copyright Noel Cower 2019.
# Distributed under the Boost Software License, Version 1.0.
# (See accompanying file LICENSE_1_0.txt or copy at
# https://www.boost.org/LICENSE_1_0.txt)
module "nomad";
## Auxiliary
# mapif/2 updates an entity in an array with the given sel if it matches
# filter. If it doesn't match, the nomatch selector is used.
def mapif(filter; sel; nomatch):
map(if filter then sel else nomatch end);
# mapif/2 updates an entity in an array with the given sel if it matches
# filter.
def mapif(filter; sel):
map(if filter then sel else . end);
# zip/1 combines two arrays into one by zipping their values together into
# arrays of of [[lhs[0], rhs[0]], [lhs[1], rhs[1]], ...].
#
# The input is considered the left-hand side and the argument to the function
# the right-hand side for readability (i.e., lhs | zip(rhs)).
#
# Any elements not present on one side of the zip are null.
def zip($rhs):
. as $lhs |
([($lhs | length), ($rhs | length)] | max) as $len |
reduce range(0; $len) as $it ([]; . + [[$rhs[$it], $lhs[$it]]]);
# rzip/1 combines two arrays into one by zipping their values together into
# arrays of of [[lhs[0], rhs[0]], [lhs[1], rhs[1]], ...].
#
# This is inverted from zip/1 and exists for convenience.
def rzip($lhs):
. as $rhs |
$lhs | zip($rhs);
# named/3 updates an entity with the given name using sel, otherwise it
# updates it with nomatch.
def named($name; sel; nomatch):
mapif(.Name == $name; sel; .);
# named/2 updates an entity with the given name using sel.
def named($name; sel):
named($name; sel; .);
# named/1 looks up the first entity with the given name.
def named($name):
map(select(name($name))) | first;
# name/1 returns true if an object's Name field is $name.
# This is a convenience function.
def name($name):
.Name == $name;
## Groups
# Read
# group/1 returns the first group with the given name.
def group($name):
.TaskGroups | named($name);
# Mutate
# group/2 applies sel to all groups with the given name.
def group($name; sel):
.TaskGroups |= named($name; sel);
# Filter
# groups/1 filters task groups and returns an array of groups that yield
# a non-false value for filter.
def groups(filter):
.TaskGroups | map(select(filter));
## Tasks
# Read
# task/1 returns the first task with the given name.
def task($name):
.Tasks | named($name);
def task($group; $name):
group($group) | task($name);
# Mutate
# task/2 applies sel to all tasks with the given name.
def task(taskFilter; sel):
.Tasks |= named($name; sel);
def task($group; $name; sel):
group($group; task($name; sel));
# Filter
# tasks/1 filters tasks and returns an array of tasks that yield a non-false
# value for filter.
def tasks(filter):
.Tasks | map(select(filter));
def tasks($group; filter):
group($group) | tasks(filter);
## Services
# Read
# service/1 returns the first service with the given name.
def service($name):
.Services | named($name);
def service($task; $name):
task($task) | service($name);
def service($group; $task; $name):
group($group) | task($task) | service($name);
# Mutate
# service/2 applies sel to all services with the given name.
def service($name; sel):
.Services |= named($name; sel);
def service($task; $name; sel):
group($group; task($task; service($name; sel)));
def service($group; $task; $name; sel):
group($group; task($task; service($name; sel)));
# Filter
# services/1 filters services and returns an array of services that yield
# a non-false value for filter.
def services(filter):
.Services | map(select(filter));
def services($task; filter):
task($task) | services(filter);
def services($group; $task; filter):
group($group) | task($task) | services(filter);
## Checks
# Read
# check/1 returns the first service check with the given name.
def check($name):
.Checks | named($name);
def check($service; $name):
service($service) | check($name);
def check($task; $service; $name):
task($task) | service($service) | check($name);
def check($group; $task; $service; $name):
group($group) | task($task) | service($service) | check($name);
# Mutate
def check($name; sel):
.Checks |= named($name; sel);
def check($service; $name; sel):
service($servie; check($name; sel));
def check($task; $service; $name; sel):
task($task; service($servie; check($name; sel)));
def check($group; $task; $service; $name; sel):
group($group; task($task; service($servie; check($name; sel))));
# Filter
# checks/1 filters service checks and returns an array of checks that yield
# a non-false value for filter.
def checks(filter):
.Checks | map(select(filter));
def checks($service; filter):
service($service) | checks(filter);
def checks($task; $service; filter):
task($task) | service($service) | checks(filter);
def checks($group; $task; $service; filter):
group($group) | task($task) | service($service) | checks(filter);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment