about summary refs log tree commit diff
path: root/tools
diff options
context:
space:
mode:
Diffstat (limited to 'tools')
-rw-r--r--tools/aoc2019/default.nix22
-rw-r--r--tools/aoc2019/solution-day1.el28
-rw-r--r--tools/aoc2019/solution-day2.el53
-rw-r--r--tools/aoc2019/solution-day3.el58
-rwxr-xr-xtools/bin/__dispatch.sh40
l---------tools/bin/aoc20191
l---------tools/bin/blog_cli1
l---------tools/bin/kontemplate1
l---------tools/bin/pass1
l---------tools/bin/stern1
l---------tools/bin/terraform1
-rw-r--r--tools/blog_cli/README.md41
-rw-r--r--tools/blog_cli/default.nix9
-rw-r--r--tools/blog_cli/main.go209
-rw-r--r--tools/gotest/default.nix27
-rw-r--r--tools/gotest/lib.go11
-rw-r--r--tools/gotest/main.go16
-rw-r--r--tools/gotest/test.proto9
-rw-r--r--tools/kms_pass.nix60
19 files changed, 589 insertions, 0 deletions
diff --git a/tools/aoc2019/default.nix b/tools/aoc2019/default.nix
new file mode 100644
index 0000000000..a53586eea9
--- /dev/null
+++ b/tools/aoc2019/default.nix
@@ -0,0 +1,22 @@
+# Solutions for Advent of Code 2019, written in Emacs Lisp.
+#
+# For each day a new file is created as "solution-day$n.el".
+{ pkgs, ... }:
+
+let
+  inherit (builtins) attrNames filter head listToAttrs match readDir;
+  dir = readDir ./.;
+  matchSolution = match "solution-(.*)\.el";
+  isSolution = f: (matchSolution f) != null;
+  getDay = f: head (matchSolution f);
+
+  solutionFiles = filter (e: dir."${e}" == "regular" && isSolution e) (attrNames dir);
+  solutions = map (f: let day = getDay f; in {
+    name = day;
+    value = pkgs.writeElispBin {
+      name = "aoc2019";
+      deps = p: with p; [ dash s ht ];
+      src = ./. + ("/" + f);
+    };
+  }) solutionFiles;
+in listToAttrs solutions
diff --git a/tools/aoc2019/solution-day1.el b/tools/aoc2019/solution-day1.el
new file mode 100644
index 0000000000..d805c22ec8
--- /dev/null
+++ b/tools/aoc2019/solution-day1.el
@@ -0,0 +1,28 @@
+;; Advent of Code 2019 - Day 1
+(require 'dash)
+
+;; Puzzle 1:
+
+(defvar day-1/input
+  '(83285 96868 121640 51455 128067 128390 141809 52325 68310 140707 124520 149678
+          87961 52040 133133 52203 117483 85643 84414 86558 65402 122692 88565 61895
+          126271 128802 140363 109764 53600 114391 98973 124467 99574 69140 144856
+          56809 149944 138738 128823 82776 77557 51994 74322 64716 114506 124074
+          73096 97066 96731 149307 135626 121413 69575 98581 50570 60754 94843 72165
+          146504 53290 63491 50936 79644 119081 70218 85849 133228 114550 131943
+          67288 68499 80512 148872 99264 119723 68295 90348 146534 52661 99146 95993
+          130363 78956 126736 82065 77227 129950 97946 132345 107137 79623 148477
+          88928 118911 75277 97162 80664 149742 88983 74518))
+
+(defun calculate-fuel (mass)
+  (- (/ mass 3) 2))
+
+(message "Solution to day1/1: %d" (apply #'+ (-map #'calculate-fuel day-1/input)))
+
+;; Puzzle 2:
+(defun calculate-recursive-fuel (mass)
+  (let ((fuel (calculate-fuel mass)))
+    (if (< fuel 0) 0
+      (+ fuel (calculate-recursive-fuel fuel)))))
+
+(message "Solution to day1/2: %d" (apply #'+ (-map #'calculate-recursive-fuel day-1/input)))
diff --git a/tools/aoc2019/solution-day2.el b/tools/aoc2019/solution-day2.el
new file mode 100644
index 0000000000..6ecac1e201
--- /dev/null
+++ b/tools/aoc2019/solution-day2.el
@@ -0,0 +1,53 @@
+;; -*- lexical-binding: t; -*-
+;; Advent of Code 2019 - Day 2
+(require 'dash)
+(require 'ht)
+
+(defvar day2/input
+  [1 0 0 3 1 1 2 3 1 3 4 3 1 5 0 3 2 1 9 19 1 19 5 23 1 13 23 27 1 27 6 31
+     2 31 6 35 2 6 35 39 1 39 5 43 1 13 43 47 1 6 47 51 2 13 51 55 1 10 55
+     59 1 59 5 63 1 10 63 67 1 67 5 71 1 71 10 75 1 9 75 79 2 13 79 83 1 9
+     83 87 2 87 13 91 1 10 91 95 1 95 9 99 1 13 99 103 2 103 13 107 1 107 10
+     111 2 10 111 115 1 115 9 119 2 119 6 123 1 5 123 127 1 5 127 131 1 10
+     131 135 1 135 6 139 1 10 139 143 1 143 6 147 2 147 13 151 1 5 151 155 1
+     155 5 159 1 159 2 163 1 163 9 0 99 2 14 0 0])
+
+;; Puzzle 1
+
+(defun day2/single-op (f state idx)
+  (let* ((a (aref state (aref state (+ 1 idx))))
+         (b (aref state (aref state (+ 2 idx))))
+         (p (aref state (+ 3 idx)))
+         (result (funcall f a b)))
+    (aset state p (funcall f a b))))
+
+(defun day2/operate (state idx)
+  (pcase (aref state idx)
+    (99 (aref state 0))
+    (1 (day2/single-op #'+ state idx)
+       (day2/operate state (+ 4 idx)))
+    (2 (day2/single-op #'* state idx)
+       (day2/operate state (+ 4 idx)))
+    (other (error "Unknown opcode: %s" other))))
+
+(defun day2/program-with-inputs (noun verb)
+  (let* ((input (copy-tree day2/input t)))
+    (aset input 1 noun)
+    (aset input 2 verb)
+    (day2/operate input 0)))
+
+(message "Solution to day2/1: %s" (day2/program-with-inputs 12 2))
+
+;; Puzzle 2
+(let* ((used (ht))
+       (noun 0)
+       (verb 0)
+       (result (day2/program-with-inputs noun verb)))
+  (while (/= 19690720 result)
+    (setq noun (random 100))
+    (setq verb (random 100))
+    (unless (ht-get used (format "%d%d" noun verb))
+      (ht-set used (format "%d%d" noun verb) t)
+      (setq result (day2/program-with-inputs noun verb))))
+
+  (message "Solution to day2/2: %s%s" noun verb))
diff --git a/tools/aoc2019/solution-day3.el b/tools/aoc2019/solution-day3.el
new file mode 100644
index 0000000000..c0d2eb5ee6
--- /dev/null
+++ b/tools/aoc2019/solution-day3.el
@@ -0,0 +1,58 @@
+;; -*- lexical-binding: t; -*-
+;; Advent of Code 2019 - Day 3
+;;
+;; Note: Input was pre-processed with some Emacs shortcuts.
+(require 'cl)
+(require 'dash)
+(require 'ht)
+(require 's)
+
+(defvar day3/input/wire1
+  "R1010,D422,L354,U494,L686,U894,R212,U777,L216,U9,L374,U77,R947,U385,L170,U916,R492,D553,L992,D890,L531,U360,R128,U653,L362,U522,R817,U198,L126,D629,L569,U300,L241,U145,R889,D196,L450,D576,L319,D147,R985,U889,L941,U837,L608,D77,L864,U911,L270,D869,R771,U132,L249,U603,L36,D328,L597,U992,L733,D370,L947,D595,L308,U536,L145,U318,R55,D773,R175,D505,R483,D13,R780,U778,R445,D107,R490,U245,L587,U502,R446,U639,R150,U35,L455,D522,R866,U858,R394,D975,R513,D378,R58,D646,L374,D675,R209,U228,R530,U543,L480,U677,L912,D164,L573,U587,L784,D626,L994,U250,L215,U985,R684,D79,L877,U811,L766,U617,L665,D246,L408,U800,L360,D272,L436,U138,R240,U735,L681,U68,L608,D59,R532,D808,L104,U968,R887,U819,R346,U698,L317,U582,R516,U55,L303,U607,L457,U479,L510,D366,L583,U519,R878,D195,R970,D267,R842,U784,R9,D946,R833,D238,L232,D94,L860,D47,L346,U951,R491,D745,R849,U273,R263,U392,L341,D808,R696,U326,R886,D296,L865,U833,R241,U644,R729,D216,R661,D712,L466,D699,L738,U5,L556,D693,R912,D13,R48,U63,L877,U628,L689,D929,R74,U924,R612,U153,R417,U425,L879,D378,R79,D248,L3,U519,R366,U281,R439,D823,R149,D668,R326,D342,L213,D735,R504,U265,L718,D842,L565,U105,L214,U963,R518,D681,R642,U170,L111,U6,R697,U572,R18,U331,L618,D255,R534,D322,L399,U595,L246,U651,L836,U757,R417,D795,R291,U759,L568,U965,R828,D570,R350,U317,R338,D173,L74,D833,L650,D844,L70,U913,R594,U407,R674,D684,L481,D564,L128,D277,R851,D274,L435,D582,R469,U729,R387,D818,R443,U504,R414,U8,L842,U845,R275,U986,R53,U660,R661,D225,R614,U159,R477")
+
+(defvar day3/input/wire2
+  "L1010,D698,R442,U660,L719,U702,L456,D86,R938,D177,L835,D639,R166,D285,L694,U468,L569,D104,L234,D574,L669,U299,L124,D275,L179,D519,R617,U72,L985,D248,R257,D276,L759,D834,R490,U864,L406,U181,R911,U873,R261,D864,R260,U759,R648,U158,R308,D386,L835,D27,L745,U91,R840,U707,R275,U543,L663,U736,L617,D699,R924,U103,R225,U455,R708,U319,R569,U38,R315,D432,L179,D975,R519,D546,L295,U680,L685,U603,R262,D250,R7,U171,R261,U519,L832,U534,L471,U431,L474,U886,R10,D179,L79,D555,R452,U452,L832,U863,L367,U538,L237,D160,R441,U605,R942,U259,L811,D552,R646,D353,L225,D94,L35,D307,R752,U23,R698,U610,L379,D932,R698,D751,R178,D347,R325,D156,R471,D555,R558,D593,R773,U2,L955,U764,L735,U438,R364,D640,L757,U534,R919,U409,R361,U407,R336,D808,R877,D648,R610,U198,R340,U94,R795,D667,R811,U975,L965,D224,R565,D681,L64,U567,R621,U922,L665,U329,R242,U592,L727,D481,L339,U402,R213,D280,R656,U169,R976,D962,L294,D505,L251,D689,L497,U133,R230,D441,L90,D220,L896,D657,L500,U331,R502,U723,R762,D613,L447,D256,L226,U309,L935,U384,L740,D459,R309,D707,R952,D747,L304,D105,R977,D539,R941,D21,R291,U216,R132,D543,R515,U453,L854,D42,R982,U102,L469,D639,R559,D68,R302,U734,R980,D214,R107,D191,L730,D793,L63,U17,R807,U196,R412,D592,R330,D941,L87,D291,L44,D94,L272,D780,R968,U837,L712,D704,R163,U981,R537,U778,R220,D303,L196,D951,R163,D446,R11,D623,L72,D778,L158,U660,L189,D510,L247,D716,L89,U887,L115,U114,L36,U81,R927,U293,L265,U183,R331,D267,R745,D298,L561,D918,R299,U810,L322,U679,L739,D854,L581,U34,L862,D779,R23")
+
+;; Puzzle 1
+
+(defun wire-from (raw)
+  (-map (lambda (s)
+          (cons (substring s 0 1) (string-to-number (substring s 1))))
+        (s-split "," raw)))
+
+(defun day3/move (x y next)
+  (cl-flet ((steps (by op)
+                   (-map op (reverse (number-sequence 1 by)))))
+    (pcase next
+      (`("L" . ,by) (steps by (lambda (n) (cons (- x n) y))))
+      (`("R" . ,by) (steps by (lambda (n) (cons (+ x n) y))))
+      (`("U" . ,by) (steps by (lambda (n) (cons x (+ y n)))))
+      (`("D" . ,by) (steps by (lambda (n) (cons x (- y n))))))))
+
+(defun day3/wire-points (wire)
+  (let ((points (ht))
+        (point-list (-reduce-from
+                     (lambda (acc point)
+                       (-let* (((x . y) (car acc))
+                               (next (day3/move x y point)))
+                         (-concat next acc)))
+                     '((0 . 0)) wire)))
+    (-map (lambda (p) (ht-set! points p t)) point-list)
+    (ht-remove! points '(0 . 0))
+    points))
+
+(defun day3/closest-intersection (wire1 wire2)
+  (let* ((wire1-points (day3/wire-points (wire-from wire1)))
+         (wire2-points (day3/wire-points (wire-from wire2)))
+         (crossed-points (-filter (lambda (p) (ht-contains? wire1-points p))
+                                  (ht-keys wire2-points))))
+
+    (car (-sort #'<
+                (-map (-lambda ((x . y))
+                        (+ (abs x) (abs y)))
+                      crossed-points)))))
+
+(message "Solution form day3/1: %d"
+         (day3/closest-intersection day3/input/wire1
+                                    day3/input/wire2))
+
diff --git a/tools/bin/__dispatch.sh b/tools/bin/__dispatch.sh
new file mode 100755
index 0000000000..fb8a4d7791
--- /dev/null
+++ b/tools/bin/__dispatch.sh
@@ -0,0 +1,40 @@
+#!/usr/bin/env bash
+# This script dispatches invocations transparently to programs instantiated from
+# Nix.
+#
+# To add a new tool, insert it into the case statement below by setting `attr`
+# to the key in nixpkgs which represents the program you want to run.
+set -ueo pipefail
+
+readonly REPO_ROOT=$(git rev-parse --show-toplevel)
+readonly TARGET_TOOL=$(basename $0)
+
+case "${TARGET_TOOL}" in
+  terraform)
+    attr="third_party.terraform-gcp"
+    ;;
+  kontemplate)
+    attr="kontemplate"
+    ;;
+  blog_cli)
+    attr="tools.blog_cli"
+    ;;
+  stern)
+    attr="stern"
+    ;;
+  pass)
+    attr="tools.kms_pass"
+    ;;
+  aoc2019)
+    attr="tools.aoc2019.${1}"
+    ;;
+  *)
+    echo "The tool '${TARGET_TOOL}' is currently not installed in this repository."
+    exit 1
+    ;;
+esac
+
+result=$(nix-build --no-out-link --attr "${attr}" "${REPO_ROOT}")
+PATH="${result}/bin:$PATH"
+
+exec "${TARGET_TOOL}" "${@}"
diff --git a/tools/bin/aoc2019 b/tools/bin/aoc2019
new file mode 120000
index 0000000000..8390ec9c96
--- /dev/null
+++ b/tools/bin/aoc2019
@@ -0,0 +1 @@
+__dispatch.sh
\ No newline at end of file
diff --git a/tools/bin/blog_cli b/tools/bin/blog_cli
new file mode 120000
index 0000000000..8390ec9c96
--- /dev/null
+++ b/tools/bin/blog_cli
@@ -0,0 +1 @@
+__dispatch.sh
\ No newline at end of file
diff --git a/tools/bin/kontemplate b/tools/bin/kontemplate
new file mode 120000
index 0000000000..8390ec9c96
--- /dev/null
+++ b/tools/bin/kontemplate
@@ -0,0 +1 @@
+__dispatch.sh
\ No newline at end of file
diff --git a/tools/bin/pass b/tools/bin/pass
new file mode 120000
index 0000000000..8390ec9c96
--- /dev/null
+++ b/tools/bin/pass
@@ -0,0 +1 @@
+__dispatch.sh
\ No newline at end of file
diff --git a/tools/bin/stern b/tools/bin/stern
new file mode 120000
index 0000000000..8390ec9c96
--- /dev/null
+++ b/tools/bin/stern
@@ -0,0 +1 @@
+__dispatch.sh
\ No newline at end of file
diff --git a/tools/bin/terraform b/tools/bin/terraform
new file mode 120000
index 0000000000..8390ec9c96
--- /dev/null
+++ b/tools/bin/terraform
@@ -0,0 +1 @@
+__dispatch.sh
\ No newline at end of file
diff --git a/tools/blog_cli/README.md b/tools/blog_cli/README.md
new file mode 100644
index 0000000000..7afa0fe920
--- /dev/null
+++ b/tools/blog_cli/README.md
@@ -0,0 +1,41 @@
+tazblog CLI
+===========
+
+My blog stores its content in DNS, spread out over three types of `TXT` entries:
+
+* `TXT _posts.blog.tazj.in.`: A sorted list of posts, serialised as a JSON list of
+  strings (e.g. `["1486830338", "1476807384"]`)
+
+* `TXT _chunks.$postID.blog.tazj.in`: JSON chunks containing the blog post text
+
+* `TXT _meta.$postID.blog.tazj.in`: JSON blob with blog post metadata
+
+All JSON blobs are base64-encoded.
+
+This CLI tool helps to update those records.
+
+Each blog post data is a series of JSON-encoded structures which follow one of
+these formats:
+
+```
+struct metadata {
+    chunks: int
+    title: string
+    date: date
+}
+```
+
+Where `chunks` describes the number of chunks following this format:
+
+```
+struct chunk {
+    c: int
+    t: string
+}
+```
+
+Writing a blog post to DNS means taking its text and metadata, chunking it up
+and writing the chunks.
+
+Reading a blog post means retrieving all data, reading the metadata and then
+assembling the chunks in order.
diff --git a/tools/blog_cli/default.nix b/tools/blog_cli/default.nix
new file mode 100644
index 0000000000..c22e4c949b
--- /dev/null
+++ b/tools/blog_cli/default.nix
@@ -0,0 +1,9 @@
+{ pkgs, ... }:
+
+pkgs.buildGo.program {
+  name = "blog_cli";
+  srcs = [ ./main.go ];
+  deps = with pkgs.third_party; [
+    gopkgs."google.golang.org".api.dns.v1.gopkg
+  ];
+} // { meta.enableCI = true; }
diff --git a/tools/blog_cli/main.go b/tools/blog_cli/main.go
new file mode 100644
index 0000000000..db64f8378e
--- /dev/null
+++ b/tools/blog_cli/main.go
@@ -0,0 +1,209 @@
+// The tazblog CLI implements updating my blog records in DNS, see the
+// README in this folder for details.
+//
+// The post input format is a file with the title on one line,
+// followed by the date on a line, followed by an empty line, followed
+// by the post text.
+package main
+
+import (
+	"context"
+	"encoding/base64"
+	"encoding/json"
+	"flag"
+	"fmt"
+	"io/ioutil"
+	"log"
+	"time"
+
+	"google.golang.org/api/dns/v1"
+)
+
+var (
+	project = flag.String("project", "tazjins-infrastructure", "Target GCP project")
+	zone    = flag.String("zone", "blog-tazj-in", "Target Cloud DNS zone")
+	title   = flag.String("title", "", "Title of the blog post")
+	date    = flag.String("date", "", "Date the post was written on")
+	infile  = flag.String("text", "", "Text file containing the blog post")
+	id      = flag.String("id", "", "Post ID - will be generated if unset")
+)
+
+// Number of runes to include in a single chunk. If any chunks exceed
+// the limit of what can be encoded, the chunk size is reduced and we
+// try again.
+var chunkSize = 200
+
+type day time.Time
+
+func (d day) MarshalJSON() ([]byte, error) {
+	j := (time.Time(d)).Format(`"2006-01-02"`)
+	return []byte(j), nil
+}
+
+type metadata struct {
+	Chunks int    `json:"c"`
+	Title  string `json:"t"`
+	Date   day    `json:"d"`
+}
+
+type chunk struct {
+	Chunk int
+	Text  string
+}
+
+type post struct {
+	ID     string
+	Meta   metadata
+	Chunks []string
+}
+
+func (p *post) writeToDNS() error {
+	var additions []*dns.ResourceRecordSet
+	additions = append(additions, &dns.ResourceRecordSet{
+		Name: fmt.Sprintf("_meta.%s.blog.tazj.in.", p.ID),
+		Type: "TXT",
+		Ttl:  1200,
+		Rrdatas: []string{
+			encodeJSON(p.Meta),
+		},
+	})
+
+	for i, c := range p.Chunks {
+		additions = append(additions, &dns.ResourceRecordSet{
+			Name:    fmt.Sprintf("_%v.%s.blog.tazj.in.", i, p.ID),
+			Type:    "TXT",
+			Ttl:     1200,
+			Rrdatas: []string{c},
+		})
+	}
+
+	ctx := context.Background()
+	dnsSvc, err := dns.NewService(ctx)
+	if err != nil {
+		return err
+	}
+
+	change := dns.Change{
+		Additions: additions,
+	}
+
+	_, err = dnsSvc.Changes.Create(*project, *zone, &change).Do()
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// Encode given value as JSON and base64-encode it.
+func encodeJSON(v interface{}) string {
+	outer, err := json.Marshal(v)
+	if err != nil {
+		log.Fatalln("Failed to encode JSON", err)
+	}
+
+	return base64.RawStdEncoding.EncodeToString(outer)
+}
+
+// Encode a chunk and check whether it is too large
+func encodeChunk(c chunk) (string, bool) {
+	tooLarge := false
+	s := base64.RawStdEncoding.EncodeToString([]byte(c.Text))
+
+	if len(s) >= 255 {
+		tooLarge = true
+	}
+
+	return s, tooLarge
+}
+
+func createPost(id, title, text string, date day) post {
+	runes := []rune(text)
+	n := 0
+	tooLarge := false
+
+	var chunks []string
+
+	for chunkSize < len(runes) {
+		c, l := encodeChunk(chunk{
+			Chunk: n,
+			Text:  string(runes[0:chunkSize:chunkSize]),
+		})
+
+		tooLarge = tooLarge || l
+		chunks = append(chunks, c)
+		runes = runes[chunkSize:]
+		n++
+	}
+
+	if len(runes) > 0 {
+		c, l := encodeChunk(chunk{
+			Chunk: n,
+			Text:  string(runes),
+		})
+
+		tooLarge = tooLarge || l
+		chunks = append(chunks, c)
+		n++
+	}
+
+	if tooLarge {
+		log.Println("Too large at chunk size", chunkSize)
+		chunkSize -= 5
+		return createPost(id, title, text, date)
+	}
+
+	return post{
+		ID: id,
+		Meta: metadata{
+			Chunks: n,
+			Title:  title,
+			Date:   date,
+		},
+		Chunks: chunks,
+	}
+}
+
+func main() {
+	flag.Parse()
+
+	if *title == "" {
+		log.Fatalln("Post title must be set (-title)")
+	}
+
+	if *infile == "" {
+		log.Fatalln("Post text file must be set (-text)")
+	}
+
+	if *id == "" {
+		log.Fatalln("Post ID must be set (-id)")
+	}
+
+	var postDate day
+	if *date != "" {
+		t, err := time.Parse("2006-01-02", *date)
+		if err != nil {
+			log.Fatalln("Invalid post date", err)
+		}
+
+		postDate = day(t)
+	} else {
+		postDate = day(time.Now())
+	}
+
+	t, err := ioutil.ReadFile(*infile)
+	if err != nil {
+		log.Fatalln("Failed to read post:", err)
+	}
+
+	post := createPost(*id, *title, string(t), postDate)
+
+	log.Println("Writing post to DNS ...")
+	err = post.writeToDNS()
+
+	if err != nil {
+		log.Fatalln("Failed to write post:", err)
+	}
+
+	log.Println("Successfully wrote entries")
+}
diff --git a/tools/gotest/default.nix b/tools/gotest/default.nix
new file mode 100644
index 0000000000..168d15748e
--- /dev/null
+++ b/tools/gotest/default.nix
@@ -0,0 +1,27 @@
+# This file demonstrates how to make use of pkgs.buildGo.
+#
+# It introduces libraries and protobuf support, however gRPC support
+# is not yet included.
+#
+# From the root of this repository this example can be built with
+# `nix-build -A tools.gotest`
+{ pkgs, ... }:
+
+let
+  inherit (pkgs) buildGo;
+
+  somelib = buildGo.package {
+    name = "somelib";
+    srcs = [ ./lib.go ];
+  };
+
+  someproto = buildGo.proto {
+    name = "someproto";
+    proto = ./test.proto;
+  };
+
+in buildGo.program {
+  name = "gotest";
+  srcs = [ ./main.go ];
+  deps = [ somelib someproto ];
+} // { meta.enableCI = true; }
diff --git a/tools/gotest/lib.go b/tools/gotest/lib.go
new file mode 100644
index 0000000000..0aeebb2aea
--- /dev/null
+++ b/tools/gotest/lib.go
@@ -0,0 +1,11 @@
+package somelib
+
+import "fmt"
+
+func Name() string {
+	return "edef"
+}
+
+func Greet(s string) string {
+	return fmt.Sprintf("Hello %s", s)
+}
diff --git a/tools/gotest/main.go b/tools/gotest/main.go
new file mode 100644
index 0000000000..99218c0776
--- /dev/null
+++ b/tools/gotest/main.go
@@ -0,0 +1,16 @@
+// This program just exists to import some libraries and demonstrate
+// that the build works, it doesn't do anything useful.
+package main
+
+import (
+	"fmt"
+	"somelib"
+	"someproto"
+)
+
+func main() {
+	p := someproto.Person{
+		Name: somelib.Name(),
+	}
+	fmt.Println(somelib.Greet(fmt.Sprintf("%v", p)))
+}
diff --git a/tools/gotest/test.proto b/tools/gotest/test.proto
new file mode 100644
index 0000000000..76af63072b
--- /dev/null
+++ b/tools/gotest/test.proto
@@ -0,0 +1,9 @@
+syntax = "proto3";
+package someproto;
+
+import "google/protobuf/timestamp.proto";
+
+message Person {
+  string name = 1;
+  google.protobuf.Timestamp last_updated = 2;
+}
diff --git a/tools/kms_pass.nix b/tools/kms_pass.nix
new file mode 100644
index 0000000000..14989b392d
--- /dev/null
+++ b/tools/kms_pass.nix
@@ -0,0 +1,60 @@
+# This tool mimics a subset of the interface of 'pass', but uses
+# Google Cloud KMS for encryption.
+#
+# It is intended to be compatible with how 'kontemplate' invokes
+# 'pass.'
+#
+# Only the 'show' and 'insert' commands are supported.
+
+{ pkgs, kms, ... }:
+
+let inherit (pkgs.third_party) google-cloud-sdk tree writeShellScriptBin;
+in (writeShellScriptBin "pass" ''
+  set -eo pipefail
+
+  CMD="$1"
+  readonly SECRET=$2
+  readonly SECRET_PATH="$SECRETS_DIR/$SECRET"
+
+  function secret_check {
+    if [[ -z $SECRET ]]; then
+      echo 'Secret must be specified'
+      exit 1
+    fi
+  }
+
+  if [[ -z $CMD ]]; then
+    CMD="ls"
+  fi
+
+  case "$CMD" in
+    ls)
+       ${tree}/bin/tree $SECRETS_DIR
+       ;;
+    show)
+      secret_check
+      ${google-cloud-sdk}/bin/gcloud kms decrypt \
+        --project ${kms.project} \
+        --location ${kms.region} \
+        --keyring ${kms.keyring} \
+        --key ${kms.key} \
+        --ciphertext-file $SECRET_PATH \
+        --plaintext-file -
+      ;;
+    insert)
+      secret_check
+      ${google-cloud-sdk}/bin/gcloud kms encrypt \
+        --project ${kms.project} \
+        --location ${kms.region} \
+        --keyring ${kms.keyring} \
+        --key ${kms.key} \
+        --ciphertext-file $SECRET_PATH \
+        --plaintext-file -
+      echo "Inserted secret '$SECRET'"
+      ;;
+    *)
+      echo "Usage: pass show/insert <secret>"
+      exit 1
+      ;;
+  esac
+'') // { meta.enableCI = true; }