about summary refs log tree commit diff
path: root/tools/nixery
diff options
context:
space:
mode:
Diffstat (limited to 'tools/nixery')
-rw-r--r--tools/nixery/README.md2
-rw-r--r--tools/nixery/builder/archive.go4
-rw-r--r--tools/nixery/builder/builder.go99
-rw-r--r--tools/nixery/cmd/server/main.go (renamed from tools/nixery/main.go)17
-rw-r--r--tools/nixery/default.nix169
-rw-r--r--tools/nixery/docs/.gitignore1
-rw-r--r--tools/nixery/docs/book.toml8
-rw-r--r--tools/nixery/docs/default.nix26
-rw-r--r--tools/nixery/docs/src/SUMMARY.md8
-rw-r--r--tools/nixery/docs/src/caching.md69
-rw-r--r--tools/nixery/docs/src/nix-1p.md2
-rw-r--r--tools/nixery/docs/src/nix.md31
-rw-r--r--tools/nixery/docs/src/nixery.md80
-rw-r--r--tools/nixery/docs/src/run-your-own.md194
-rw-r--r--tools/nixery/docs/src/under-the-hood.md129
-rw-r--r--tools/nixery/docs/theme/favicon.pngbin16053 -> 0 bytes
-rw-r--r--tools/nixery/docs/theme/nixery.css3
-rw-r--r--tools/nixery/go.mod22
-rw-r--r--tools/nixery/go.sum154
-rw-r--r--tools/nixery/layers/layers.go (renamed from tools/nixery/builder/layers.go)43
-rw-r--r--tools/nixery/manifest/manifest.go4
-rw-r--r--tools/nixery/popcount/README.md2
-rw-r--r--tools/nixery/prepare-image/prepare-image.nix23
-rw-r--r--tools/nixery/web/index.html166
-rw-r--r--tools/nixery/web/nixery-logo.png (renamed from tools/nixery/docs/src/nixery-logo.png)bin194098 -> 194098 bytes
25 files changed, 472 insertions, 784 deletions
diff --git a/tools/nixery/README.md b/tools/nixery/README.md
index 03515939a9..a879d030b8 100644
--- a/tools/nixery/README.md
+++ b/tools/nixery/README.md
@@ -1,5 +1,5 @@
 <div align="center">
-  <img src="docs/src/nixery-logo.png">
+  <img src="https://nixery.dev/nixery-logo.png">
 </div>
 
 -----------------
diff --git a/tools/nixery/builder/archive.go b/tools/nixery/builder/archive.go
index 3bc02ab4d5..8763e4cb85 100644
--- a/tools/nixery/builder/archive.go
+++ b/tools/nixery/builder/archive.go
@@ -16,6 +16,8 @@ import (
 	"io"
 	"os"
 	"path/filepath"
+
+	"github.com/google/nixery/layers"
 )
 
 // Create a new compressed tarball from each of the paths in the list
@@ -23,7 +25,7 @@ import (
 //
 // The uncompressed tarball is hashed because image manifests must
 // contain both the hashes of compressed and uncompressed layers.
-func packStorePaths(l *layer, w io.Writer) (string, error) {
+func packStorePaths(l *layers.Layer, w io.Writer) (string, error) {
 	shasum := sha256.New()
 	gz := gzip.NewWriter(w)
 	multi := io.MultiWriter(shasum, gz)
diff --git a/tools/nixery/builder/builder.go b/tools/nixery/builder/builder.go
index 37c9b9fcb7..7f0bd7fffd 100644
--- a/tools/nixery/builder/builder.go
+++ b/tools/nixery/builder/builder.go
@@ -23,8 +23,10 @@ import (
 	"strings"
 
 	"github.com/google/nixery/config"
+	"github.com/google/nixery/layers"
 	"github.com/google/nixery/manifest"
 	"github.com/google/nixery/storage"
+	"github.com/im7mortal/kmutex"
 	log "github.com/sirupsen/logrus"
 )
 
@@ -36,10 +38,11 @@ const LayerBudget int = 94
 // State holds the runtime state that is carried around in Nixery and
 // passed to builder functions.
 type State struct {
-	Storage storage.Backend
-	Cache   *LocalCache
-	Cfg     config.Config
-	Pop     Popularity
+	Storage     storage.Backend
+	Cache       *LocalCache
+	Cfg         config.Config
+	Pop         layers.Popularity
+	UploadMutex *kmutex.Kmutex
 }
 
 // Architecture represents the possible CPU architectures for which
@@ -117,7 +120,7 @@ type ImageResult struct {
 	Pkgs  []string `json:"pkgs"`
 
 	// These fields are populated in case of success
-	Graph        runtimeGraph `json:"runtimeGraph"`
+	Graph        layers.RuntimeGraph `json:"runtimeGraph"`
 	SymlinkLayer struct {
 		Size    int    `json:"size"`
 		TarHash string `json:"tarHash"`
@@ -281,7 +284,7 @@ func prepareImage(s *State, image *Image) (*ImageResult, error) {
 // added only after successful uploads, which guarantees that entries
 // retrieved from the cache are present in the bucket.
 func prepareLayers(ctx context.Context, s *State, image *Image, result *ImageResult) ([]manifest.Entry, error) {
-	grouped := groupLayers(&result.Graph, &s.Pop, LayerBudget)
+	grouped := layers.GroupLayers(&result.Graph, &s.Pop, LayerBudget)
 
 	var entries []manifest.Entry
 
@@ -291,34 +294,23 @@ func prepareLayers(ctx context.Context, s *State, image *Image, result *ImageRes
 	// Missing layers are built and uploaded to the storage
 	// bucket.
 	for _, l := range grouped {
-		if entry, cached := layerFromCache(ctx, s, l.Hash()); cached {
-			entries = append(entries, *entry)
-		} else {
-			lh := l.Hash()
-
-			// While packing store paths, the SHA sum of
-			// the uncompressed layer is computed and
-			// written to `tarhash`.
-			//
-			// TODO(tazjin): Refactor this to make the
-			// flow of data cleaner.
-			var tarhash string
-			lw := func(w io.Writer) error {
-				var err error
-				tarhash, err = packStorePaths(&l, w)
-				return err
-			}
-
-			entry, err := uploadHashLayer(ctx, s, lh, lw)
+		lh := l.Hash()
+
+		// While packing store paths, the SHA sum of
+		// the uncompressed layer is computed and
+		// written to `tarhash`.
+		//
+		// TODO(tazjin): Refactor this to make the
+		// flow of data cleaner.
+		lw := func(w io.Writer) (string, error) {
+			tarhash, err := packStorePaths(&l, w)
 			if err != nil {
-				return nil, err
+				return "", err
 			}
-			entry.MergeRating = l.MergeRating
-			entry.TarHash = tarhash
 
 			var pkgs []string
 			for _, p := range l.Contents {
-				pkgs = append(pkgs, packageFromPath(p))
+				pkgs = append(pkgs, layers.PackageFromPath(p))
 			}
 
 			log.WithFields(log.Fields{
@@ -327,15 +319,21 @@ func prepareLayers(ctx context.Context, s *State, image *Image, result *ImageRes
 				"tarhash":  tarhash,
 			}).Info("created image layer")
 
-			go cacheLayer(ctx, s, l.Hash(), *entry)
-			entries = append(entries, *entry)
+			return tarhash, err
+		}
+
+		entry, err := uploadHashLayer(ctx, s, lh, l.MergeRating, lw)
+		if err != nil {
+			return nil, err
 		}
+
+		entries = append(entries, *entry)
 	}
 
 	// Symlink layer (built in the first Nix build) needs to be
 	// included here manually:
 	slkey := result.SymlinkLayer.TarHash
-	entry, err := uploadHashLayer(ctx, s, slkey, func(w io.Writer) error {
+	entry, err := uploadHashLayer(ctx, s, slkey, 0, func(w io.Writer) (string, error) {
 		f, err := os.Open(result.SymlinkLayer.Path)
 		if err != nil {
 			log.WithError(err).WithFields(log.Fields{
@@ -344,7 +342,7 @@ func prepareLayers(ctx context.Context, s *State, image *Image, result *ImageRes
 				"layer": slkey,
 			}).Error("failed to open symlink layer")
 
-			return err
+			return "", err
 		}
 		defer f.Close()
 
@@ -357,18 +355,16 @@ func prepareLayers(ctx context.Context, s *State, image *Image, result *ImageRes
 				"layer": slkey,
 			}).Error("failed to upload symlink layer")
 
-			return err
+			return "", err
 		}
 
-		return gz.Close()
+		return "sha256:" + slkey, gz.Close()
 	})
 
 	if err != nil {
 		return nil, err
 	}
 
-	entry.TarHash = "sha256:" + result.SymlinkLayer.TarHash
-	go cacheLayer(ctx, s, slkey, *entry)
 	entries = append(entries, *entry)
 
 	return entries, nil
@@ -379,7 +375,7 @@ func prepareLayers(ctx context.Context, s *State, image *Image, result *ImageRes
 //
 // This type exists to avoid duplication between the handling of
 // symlink layers and store path layers.
-type layerWriter func(w io.Writer) error
+type layerWriter func(w io.Writer) (string, error)
 
 // byteCounter is a special io.Writer that counts all bytes written to
 // it and does nothing else.
@@ -407,8 +403,16 @@ func (b *byteCounter) Write(p []byte) (n int, err error) {
 //
 // The return value is the layer's SHA256 hash, which is used in the
 // image manifest.
-func uploadHashLayer(ctx context.Context, s *State, key string, lw layerWriter) (*manifest.Entry, error) {
+func uploadHashLayer(ctx context.Context, s *State, key string, mrating uint64, lw layerWriter) (*manifest.Entry, error) {
+	s.UploadMutex.Lock(key)
+	defer s.UploadMutex.Unlock(key)
+
+	if entry, cached := layerFromCache(ctx, s, key); cached {
+		return entry, nil
+	}
+
 	path := "staging/" + key
+	var tarhash string
 	sha256sum, size, err := s.Storage.Persist(ctx, path, manifest.LayerType, func(sw io.Writer) (string, int64, error) {
 		// Sets up a "multiwriter" that simultaneously runs both hash
 		// algorithms and uploads to the storage backend.
@@ -416,7 +420,8 @@ func uploadHashLayer(ctx context.Context, s *State, key string, lw layerWriter)
 		counter := &byteCounter{}
 		multi := io.MultiWriter(sw, shasum, counter)
 
-		err := lw(multi)
+		var err error
+		tarhash, err = lw(multi)
 		sha256sum := fmt.Sprintf("%x", shasum.Sum([]byte{}))
 
 		return sha256sum, counter.count, err
@@ -448,10 +453,14 @@ func uploadHashLayer(ctx context.Context, s *State, key string, lw layerWriter)
 	}).Info("created and persisted layer")
 
 	entry := manifest.Entry{
-		Digest: "sha256:" + sha256sum,
-		Size:   size,
+		Digest:      "sha256:" + sha256sum,
+		Size:        size,
+		TarHash:     tarhash,
+		MergeRating: mrating,
 	}
 
+	cacheLayer(ctx, s, key, entry)
+
 	return &entry, nil
 }
 
@@ -492,13 +501,13 @@ func BuildImage(ctx context.Context, s *State, image *Image) (*BuildResult, erro
 	}
 	m, c := manifest.Manifest(image.Arch.imageArch, layers, cmd)
 
-	lw := func(w io.Writer) error {
+	lw := func(w io.Writer) (string, error) {
 		r := bytes.NewReader(c.Config)
 		_, err := io.Copy(w, r)
-		return err
+		return "", err
 	}
 
-	if _, err = uploadHashLayer(ctx, s, c.SHA256, lw); err != nil {
+	if _, err = uploadHashLayer(ctx, s, c.SHA256, 0, lw); err != nil {
 		log.WithError(err).WithFields(log.Fields{
 			"image": image.Name,
 			"tag":   image.Tag,
diff --git a/tools/nixery/main.go b/tools/nixery/cmd/server/main.go
index 2e633e0898..24aec6391c 100644
--- a/tools/nixery/main.go
+++ b/tools/nixery/cmd/server/main.go
@@ -26,9 +26,11 @@ import (
 
 	"github.com/google/nixery/builder"
 	"github.com/google/nixery/config"
+	"github.com/google/nixery/layers"
 	"github.com/google/nixery/logs"
 	mf "github.com/google/nixery/manifest"
 	"github.com/google/nixery/storage"
+	"github.com/im7mortal/kmutex"
 	log "github.com/sirupsen/logrus"
 )
 
@@ -52,7 +54,7 @@ var (
 
 // Downloads the popularity information for the package set from the
 // URL specified in Nixery's configuration.
-func downloadPopularity(url string) (builder.Popularity, error) {
+func downloadPopularity(url string) (layers.Popularity, error) {
 	resp, err := http.Get(url)
 	if err != nil {
 		return nil, err
@@ -67,7 +69,7 @@ func downloadPopularity(url string) (builder.Popularity, error) {
 		return nil, err
 	}
 
-	var pop builder.Popularity
+	var pop layers.Popularity
 	err = json.Unmarshal(j, &pop)
 	if err != nil {
 		return nil, err
@@ -246,7 +248,7 @@ func main() {
 		log.WithError(err).Fatal("failed to instantiate build cache")
 	}
 
-	var pop builder.Popularity
+	var pop layers.Popularity
 	if cfg.PopUrl != "" {
 		pop, err = downloadPopularity(cfg.PopUrl)
 		if err != nil {
@@ -256,10 +258,11 @@ func main() {
 	}
 
 	state := builder.State{
-		Cache:   &cache,
-		Cfg:     cfg,
-		Pop:     pop,
-		Storage: s,
+		Cache:       &cache,
+		Cfg:         cfg,
+		Pop:         pop,
+		Storage:     s,
+		UploadMutex: kmutex.New(),
 	}
 
 	log.WithFields(log.Fields{
diff --git a/tools/nixery/default.nix b/tools/nixery/default.nix
index b5575be507..91eabca960 100644
--- a/tools/nixery/default.nix
+++ b/tools/nixery/default.nix
@@ -19,106 +19,111 @@
 with pkgs;
 
 let
-  inherit (pkgs) buildGoModule;
+  inherit (pkgs) buildGoModule lib;
 
   # Avoid extracting this from git until we have a way to plumb
   # through revision numbers.
   nixery-commit-hash = "depot";
+in
+depot.nix.readTree.drvTargets rec {
+  # Implementation of the Nix image building logic
+  nixery-prepare-image = import ./prepare-image { inherit pkgs; };
+
+  # Include the Nixery website into the Nix store, unless its being
+  # overridden to something else. Nixery will serve this as its front
+  # page when visited from a browser.
+  nixery-web = ./web;
 
-  # Go implementation of the Nixery server which implements the
-  # container registry interface.
+  nixery-popcount = callPackage ./popcount { };
+
+  # Build Nixery's Go code, resulting in the binaries used for various
+  # bits of functionality.
   #
-  # Users should use the nixery-bin derivation below instead as it
-  # provides the paths of files needed at runtime.
-  nixery-server = buildGoModule rec {
-    name = "nixery-server";
+  # The server binary is wrapped to ensure that required environment
+  # variables are set at runtime.
+  nixery = buildGoModule rec {
+    name = "nixery";
     src = ./.;
     doCheck = true;
 
     # Needs to be updated after every modification of go.mod/go.sum
-    vendorSha256 = "1xnmyz2a5s5sck0fzhcz51nds4s80p0jw82dhkf4v2c4yzga83yk";
+    vendorHash = "sha256-io9NCeZmjCZPLmII3ajXIsBWbT40XiW8ncXOuUDabbo=";
 
-    buildFlagsArray = [
-      "-ldflags=-s -w -X main.version=${nixery-commit-hash}"
+    ldflags = [
+      "-s"
+      "-w"
+      "-X"
+      "main.version=${nixery-commit-hash}"
     ];
-  };
-in
-depot.nix.readTree.drvTargets rec {
-  # Implementation of the Nix image building logic
-  nixery-prepare-image = import ./prepare-image { inherit pkgs; };
 
-  # Use mdBook to build a static asset page which Nixery can then
-  # serve. This is primarily used for the public instance at
-  # nixery.dev.
-  nixery-book = callPackage ./docs { };
+    nativeBuildInputs = [ makeWrapper ];
+    postInstall = ''
+      wrapProgram $out/bin/server \
+        --set-default WEB_DIR "${nixery-web}" \
+        --prefix PATH : ${nixery-prepare-image}/bin
+    '';
+
+    # Nixery is mirrored to Github at tazjin/nixery; this is
+    # automatically updated from CI for canon builds.
+    passthru.meta.ci.extraSteps.github = depot.tools.releases.filteredGitPush {
+      filter = ":/tools/nixery";
+      remote = "git@github.com:tazjin/nixery.git";
+      ref = "refs/heads/master";
+    };
+  };
 
-  # Wrapper script running the Nixery server with the above two data
-  # dependencies configured.
+  # Wrapper script for the wrapper script (meta!) which configures
+  # the container environment appropriately.
   #
-  # In most cases, this will be the derivation a user wants if they
-  # are installing Nixery directly.
-  nixery-bin = writeShellScriptBin "nixery" ''
-    export WEB_DIR="${nixery-book}"
-    export PATH="${nixery-prepare-image}/bin:$PATH"
-    exec ${nixery-server}/bin/nixery
+  # Most importantly, sandboxing is disabled to avoid privilege
+  # issues in containers.
+  nixery-launch-script = writeShellScriptBin "nixery" ''
+    set -e
+    export PATH=${coreutils}/bin:$PATH
+    export NIX_SSL_CERT_FILE=/etc/ssl/certs/ca-bundle.crt
+    mkdir -p /tmp
+
+    # Create the build user/group required by Nix
+    echo 'nixbld:x:30000:nixbld' >> /etc/group
+    echo 'nixbld:x:30000:30000:nixbld:/tmp:/bin/bash' >> /etc/passwd
+    echo 'root:x:0:0:root:/root:/bin/bash' >> /etc/passwd
+    echo 'root:x:0:' >> /etc/group
+
+    # Disable sandboxing to avoid running into privilege issues
+    mkdir -p /etc/nix
+    echo 'sandbox = false' >> /etc/nix/nix.conf
+
+    # In some cases users building their own image might want to
+    # customise something on the inside (e.g. set up an environment
+    # for keys or whatever).
+    #
+    # This can be achieved by setting a 'preLaunch' script.
+    ${preLaunch}
+
+    exec ${nixery}/bin/server
   '';
 
-  nixery-popcount = callPackage ./popcount { };
-
   # Container image containing Nixery and Nix itself. This image can
   # be run on Kubernetes, published on AppEngine or whatever else is
   # desired.
-  nixery-image =
-    let
-      # Wrapper script for the wrapper script (meta!) which configures
-      # the container environment appropriately.
-      #
-      # Most importantly, sandboxing is disabled to avoid privilege
-      # issues in containers.
-      nixery-launch-script = writeShellScriptBin "nixery" ''
-        set -e
-        export PATH=${coreutils}/bin:$PATH
-        export NIX_SSL_CERT_FILE=/etc/ssl/certs/ca-bundle.crt
-        mkdir -p /tmp
-
-        # Create the build user/group required by Nix
-        echo 'nixbld:x:30000:nixbld' >> /etc/group
-        echo 'nixbld:x:30000:30000:nixbld:/tmp:/bin/bash' >> /etc/passwd
-        echo 'root:x:0:0:root:/root:/bin/bash' >> /etc/passwd
-        echo 'root:x:0:' >> /etc/group
-
-        # Disable sandboxing to avoid running into privilege issues
-        mkdir -p /etc/nix
-        echo 'sandbox = false' >> /etc/nix/nix.conf
-
-        # In some cases users building their own image might want to
-        # customise something on the inside (e.g. set up an environment
-        # for keys or whatever).
-        #
-        # This can be achieved by setting a 'preLaunch' script.
-        ${preLaunch}
-
-        exec ${nixery-bin}/bin/nixery
-      '';
-    in
-    dockerTools.buildLayeredImage {
-      name = "nixery";
-      config.Cmd = [ "${nixery-launch-script}/bin/nixery" ];
-
-      inherit maxLayers;
-      contents = [
-        bashInteractive
-        cacert
-        coreutils
-        git
-        gnutar
-        gzip
-        iana-etc
-        nix
-        nixery-prepare-image
-        nixery-launch-script
-        openssh
-        zlib
-      ] ++ extraPackages;
-    };
+  nixery-image = dockerTools.buildLayeredImage {
+    name = "nixery";
+    config.Cmd = [ "${nixery-launch-script}/bin/nixery" ];
+
+    inherit maxLayers;
+    contents = [
+      bashInteractive
+      cacert
+      coreutils
+      git
+      gnutar
+      gzip
+      iana-etc
+      nix
+      nixery-prepare-image
+      nixery-launch-script
+      openssh
+      zlib
+    ] ++ extraPackages;
+  };
 }
diff --git a/tools/nixery/docs/.gitignore b/tools/nixery/docs/.gitignore
deleted file mode 100644
index 7585238efe..0000000000
--- a/tools/nixery/docs/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-book
diff --git a/tools/nixery/docs/book.toml b/tools/nixery/docs/book.toml
deleted file mode 100644
index bf6ccbb27f..0000000000
--- a/tools/nixery/docs/book.toml
+++ /dev/null
@@ -1,8 +0,0 @@
-[book]
-authors = ["Vincent Ambo <tazjin@google.com>"]
-language = "en"
-multilingual = false
-src = "src"
-
-[output.html]
-additional-css = ["theme/nixery.css"]
diff --git a/tools/nixery/docs/default.nix b/tools/nixery/docs/default.nix
deleted file mode 100644
index 876a34dcf1..0000000000
--- a/tools/nixery/docs/default.nix
+++ /dev/null
@@ -1,26 +0,0 @@
-# Copyright 2022 The TVL Contributors
-# SPDX-License-Identifier: Apache-2.0
-
-# Builds the documentation page using the Rust project's 'mdBook'
-# tool.
-#
-# Some of the documentation is pulled in and included from other
-# sources.
-
-{ fetchFromGitHub, mdbook, runCommand, rustPlatform }:
-
-let
-  nix-1p = fetchFromGitHub {
-    owner = "tazjin";
-    repo = "nix-1p";
-    rev = "9f0baf5e270128d9101ba4446cf6844889e399a2";
-    sha256 = "1pf9i90gn98vz67h296w5lnwhssk62dc6pij983dff42dbci7lhj";
-  };
-in
-runCommand "nixery-book" { } ''
-  mkdir -p $out
-  cp -r ${./.}/* .
-  chmod -R a+w src
-  cp ${nix-1p}/README.md src/nix-1p.md
-  ${mdbook}/bin/mdbook build -d $out
-''
diff --git a/tools/nixery/docs/src/SUMMARY.md b/tools/nixery/docs/src/SUMMARY.md
deleted file mode 100644
index f1d68a3ac4..0000000000
--- a/tools/nixery/docs/src/SUMMARY.md
+++ /dev/null
@@ -1,8 +0,0 @@
-# Summary
-
-- [Nixery](./nixery.md)
-  - [Under the hood](./under-the-hood.md)
-  - [Caching](./caching.md)
-  - [Run your own Nixery](./run-your-own.md)
-- [Nix](./nix.md)
-  - [Nix, the language](./nix-1p.md)
diff --git a/tools/nixery/docs/src/caching.md b/tools/nixery/docs/src/caching.md
deleted file mode 100644
index 05ea68ef60..0000000000
--- a/tools/nixery/docs/src/caching.md
+++ /dev/null
@@ -1,69 +0,0 @@
-# Caching in Nixery
-
-This page gives a quick overview over the caching done by Nixery. All cache data
-is written to Nixery's storage bucket and is based on deterministic identifiers
-or content-addressing, meaning that cache entries under the same key *never
-change*.
-
-## Manifests
-
-Manifests of builds are cached at `$BUCKET/manifests/$KEY`. The effect of this
-cache is that multiple instances of Nixery do not need to rebuild the same
-manifest from scratch.
-
-Since the manifest cache is populated only *after* layers are uploaded, Nixery
-can immediately return the manifest to its clients without needing to check
-whether layers have been uploaded already.
-
-`$KEY` is generated by creating a SHA1 hash of the requested content of a
-manifest plus the package source specification.
-
-Manifests are *only* cached if the package source specification is *not* a
-moving target.
-
-Manifest caching *only* applies in the following cases:
-
-* package source specification is a specific git commit
-* package source specification is a specific NixOS/nixpkgs commit
-
-Manifest caching *never* applies in the following cases:
-
-* package source specification is a local file path (i.e. `NIXERY_PKGS_PATH`)
-* package source specification is a NixOS channel (e.g. `NIXERY_CHANNEL=nixos-20.09`)
-* package source specification is a git branch or tag (e.g. `staging`, `master` or `latest`)
-
-It is thus always preferable to request images from a fully-pinned package
-source.
-
-Manifests can be removed from the manifest cache without negative consequences.
-
-## Layer tarballs
-
-Layer tarballs are the files that Nixery clients retrieve from the storage
-bucket to download an image.
-
-They are stored content-addressably at `$BUCKET/layers/$SHA256HASH` and layer
-requests sent to Nixery will redirect directly to this storage location.
-
-The effect of this cache is that Nixery does not need to upload identical layers
-repeatedly. When Nixery notices that a layer already exists in GCS it will skip
-uploading this layer.
-
-Removing layers from the cache is *potentially problematic* if there are cached
-manifests or layer builds referencing those layers.
-
-To clean up layers, a user must ensure that no other cached resources still
-reference these layers.
-
-## Layer builds
-
-Layer builds are cached at `$BUCKET/builds/$HASH`, where `$HASH` is a SHA1 of
-the Nix store paths included in the layer.
-
-The content of the cached entries is a JSON-object that contains the SHA256
-hashes and sizes of the built layer.
-
-The effect of this cache is that different instances of Nixery will not build,
-hash and upload layers that have identical contents across different instances.
-
-Layer builds can be removed from the cache without negative consequences.
diff --git a/tools/nixery/docs/src/nix-1p.md b/tools/nixery/docs/src/nix-1p.md
deleted file mode 100644
index a21234150f..0000000000
--- a/tools/nixery/docs/src/nix-1p.md
+++ /dev/null
@@ -1,2 +0,0 @@
-This page is a placeholder. During the build process, it is replaced by the
-actual `nix-1p` guide from https://github.com/tazjin/nix-1p
diff --git a/tools/nixery/docs/src/nix.md b/tools/nixery/docs/src/nix.md
deleted file mode 100644
index 2bfd75a692..0000000000
--- a/tools/nixery/docs/src/nix.md
+++ /dev/null
@@ -1,31 +0,0 @@
-# Nix
-
-These sections are designed to give some background information on what Nix is.
-If you've never heard of Nix before looking at Nixery, this might just be the
-page for you!
-
-[Nix][] is a functional package-manager that comes with a number of advantages
-over traditional package managers, such as side-by-side installs of different
-package versions, atomic updates, easy customisability, simple binary caching
-and much more. Feel free to explore the [Nix website][Nix] for an overview of
-Nix itself.
-
-Nix uses a custom programming language also called Nix, which is explained here
-[on its own page][nix-1p].
-
-In addition to the package manager and language, the Nix project also maintains
-[NixOS][] - a Linux distribution built entirely on Nix. On NixOS, users can
-declaratively describe the *entire* configuration of their system and perform
-updates/rollbacks to other system configurations with ease.
-
-Most Nix packages are tracked in the [Nix package set][nixpkgs], usually simply
-referred to as `nixpkgs`. It contains tens of thousands of packages already!
-
-Nixery (which you are looking at!) provides an easy & simple way to get started
-with Nix, in fact you don't even need to know that you're using Nix to make use
-of Nixery.
-
-[Nix]: https://nixos.org/nix/
-[nix-1p]: nix-1p.html
-[NixOS]: https://nixos.org/
-[nixpkgs]: https://github.com/nixos/nixpkgs
diff --git a/tools/nixery/docs/src/nixery.md b/tools/nixery/docs/src/nixery.md
deleted file mode 100644
index d9ba179010..0000000000
--- a/tools/nixery/docs/src/nixery.md
+++ /dev/null
@@ -1,80 +0,0 @@
-![Nixery](./nixery-logo.png)
-
-------------
-
-Welcome to this instance of [Nixery][]. It provides ad-hoc container images that
-contain packages from the [Nix][] package manager. Images with arbitrary
-packages can be requested via the image name.
-
-Nix not only provides the packages to include in the images, but also builds the
-images themselves by using a special [layering strategy][] that optimises for
-cache efficiency.
-
-For general information on why using Nix makes sense for container images, check
-out [this blog post][layers].
-
-## Demo
-
-<script src="https://asciinema.org/a/262583.js" id="asciicast-262583" async data-autoplay="true" data-loop="true"></script>
-
-## Quick start
-
-Simply pull an image from this registry, separating each package you want
-included by a slash:
-
-    docker pull nixery.dev/shell/git/htop
-
-This gives you an image with `git`, `htop` and an interactively configured
-shell. You could run it like this:
-
-    docker run -ti nixery.dev/shell/git/htop bash
-
-Each path segment corresponds either to a key in the Nix package set, or a
-meta-package that automatically expands to several other packages.
-
-Meta-packages **must** be the first path component if they are used. Currently
-there are only two meta-packages:
-- `shell`, which provides a `bash`-shell with interactive configuration and 
-  standard tools like `coreutils`.
-- `arm64`, which provides ARM64 binaries.
-
-**Tip:** When pulling from a private Nixery instance, replace `nixery.dev` in
-the above examples with your registry address.
-
-## FAQ
-
-If you have a question that is not answered here, feel free to file an issue on
-Github so that we can get it included in this section. The volume of questions
-is quite low, thus by definition your question is already frequently asked.
-
-### Where is the source code for this?
-
-Over [on Github][Nixery]. It is licensed under the Apache 2.0 license. Consult
-the documentation entries in the sidebar for information on how to set up your
-own instance of Nixery.
-
-### Which revision of `nixpkgs` is used for the builds?
-
-The instance at `nixery.dev` tracks a recent NixOS channel, currently NixOS
-20.09. The channel is updated several times a day.
-
-Private registries might be configured to track a different channel (such as
-`nixos-unstable`) or even track a git repository with custom packages.
-
-### Should I depend on `nixery.dev` in production?
-
-While we appreciate the enthusiasm, if you would like to use Nixery in your
-production project we recommend setting up a private instance. The public Nixery
-at `nixery.dev` is run on a best-effort basis and we make no guarantees about
-availability.
-
-### Who made this?
-
-Nixery was written by [tazjin][], but many people have contributed to Nix over
-time, maybe you could become one of them?
-
-[Nixery]: https://github.com/tazjin/nixery
-[Nix]: https://nixos.org/nix
-[layering strategy]: https://storage.googleapis.com/nixdoc/nixery-layers.html
-[layers]: https://grahamc.com/blog/nix-and-layered-docker-images
-[tazjin]: https://tazj.in
diff --git a/tools/nixery/docs/src/run-your-own.md b/tools/nixery/docs/src/run-your-own.md
deleted file mode 100644
index 7ed8bdd0bc..0000000000
--- a/tools/nixery/docs/src/run-your-own.md
+++ /dev/null
@@ -1,194 +0,0 @@
-## Run your own Nixery
-
-<!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-refresh-toc -->
-
-- [0. Prerequisites](#0-prerequisites)
-- [1. Choose a package set](#1-choose-a-package-set)
-- [2. Build Nixery itself](#2-build-nixery-itself)
-- [3. Prepare configuration](#3-prepare-configuration)
-- [4. Deploy Nixery](#4-deploy-nixery)
-- [5. Productionise](#5-productionise)
-
-<!-- markdown-toc end -->
-
-
----------
-
-⚠ This page is still under construction! ⚠
-
---------
-
-Running your own Nixery is not difficult, but requires some setup. Follow the
-steps below to get up & running.
-
-*Note:* Nixery can be run inside of a [GKE][] cluster, providing a local service
-from which images can be requested. Documentation for how to set this up is
-forthcoming, please see [nixery#4][].
-
-## 0. Prerequisites
-
-To run Nixery, you must have:
-
-* [Nix][] (to build Nixery itself)
-* Somewhere to run it (your own server, Google AppEngine, a Kubernetes cluster,
-  whatever!)
-* *Either* a [Google Cloud Storage][gcs] bucket in which to store & serve layers,
-  *or* a comfortable amount of disk space
-
-Note that while the main Nixery process is a server written in Go,
-it invokes a script that itself relies on Nix to be available.
-You can compile the main Nixery daemon without Nix, but it won't
-work without Nix.
-
-(If you are completely new to Nix and don't know how to get
-started, check the [Nix installation documentation][nixinstall].)
-
-## 1. Choose a package set
-
-When running your own Nixery you need to decide which package set you want to
-serve. By default, Nixery builds packages from a recent NixOS channel which
-ensures that most packages are cached upstream and no expensive builds need to
-be performed for trivial things.
-
-However if you are running a private Nixery, chances are high that you intend to
-use it with your own packages. There are three options available:
-
-1. Specify an upstream Nix/NixOS channel[^1], such as `nixos-20.09` or
-   `nixos-unstable`.
-2. Specify your own git-repository with a custom package set[^2]. This makes it
-   possible to pull different tags, branches or commits by modifying the image
-   tag.
-3. Specify a local file path containing a Nix package set. Where this comes from
-   or what it contains is up to you.
-
-## 2. Build Nixery itself
-
-### 2.1. With a container image
-
-The easiest way to run Nixery is to build a container image. This
-section assumes that the container runtime used is Docker, please
-modify instructions accordingly if you are using something else.
-
-With a working Nix installation, you can clone and build the Nixery
-image like this:
-
-```
-git clone https://code.tvl.fyi/depot.git:/tools/nixery.git
-nix-build -A nixery-image
-```
-
-This will create a `result`-symlink which points to a tarball containing the
-image. In Docker, this tarball can be loaded by using `docker load -i result`.
-
-### 2.2. Without a container image
-
-*This method might be more convenient if you intend to work on
-the code of the Nixery server itself, because you won't have to
-rebuild (and reload) an image each time to test your changes.*
-
-You will need to run the two following commands at the root of the repo:
-
-* `go build` to build the `nixery` binary;
-* `nix-env --install --file prepare-image/default.nix` to build
-  the required helpers.
-
-## 3. Prepare configuration
-
-Nixery is configured via environment variables.
-
-You must set *all* of these:
-
-* `NIXERY_STORAGE_BACKEND` (must be set to `gcs` or `filesystem`)
-* `PORT`: HTTP port on which Nixery should listen
-* `WEB_DIR`: directory containing static files (see below)
-
-You must set *one* of these:
-
-* `NIXERY_CHANNEL`: The name of a [Nix/NixOS channel][nixchannel] to use for building,
-  for instance `nixos-21.05`
-* `NIXERY_PKGS_REPO`: URL of a git repository containing a package set (uses
-  locally configured SSH/git credentials)
-* `NIXERY_PKGS_PATH`: A local filesystem path containing a Nix package set to use
-  for building
-
-If `NIXERY_STORAGE_BACKEND` is set to `filesystem`, then `STORAGE_PATH`
-must be set to the directory that will hold the registry blobs.
-That directory must be located on a filesystem that supports extended
-attributes (which means that on most systems, `/tmp` won't work).
-
-If `NIXERY_STORAGE_BACKEND` is set to `gcs`, then `GCS_BUCKET`
-must be set to the [Google Cloud Storage][gcs] bucket that will be
-used to store & serve image layers.
-
-You may set *all* of these:
-
-* `NIX_TIMEOUT`: Number of seconds that any Nix builder is allowed to run
-  (defaults to 60)
-
-To authenticate to the configured GCS bucket, Nixery uses Google's [Application
-Default Credentials][ADC]. Depending on your environment this may require
-additional configuration.
-
-If the `GOOGLE_APPLICATION_CREDENTIALS` environment is configured, the service
-account's private key will be used to create [signed URLs for
-layers][signed-urls].
-
-## 4. Start Nixery
-
-Run the image that was built in step 2.1 with all the environment variables
-mentioned above. Alternatively, set all the environment variables and run
-the Nixery server that was built in step 2.2.
-
-Once Nixery is running you can immediately start requesting images from it.
-
-## 5. Productionise
-
-(⚠ Here be dragons! ⚠)
-
-Nixery is still an early project and has not yet been deployed in any production
-environments and some caveats apply.
-
-Notably, Nixery currently does not support any authentication methods, so anyone
-with network access to the registry can retrieve images.
-
-Running a Nixery inside of a fenced-off environment (such as internal to a
-Kubernetes cluster) should be fine, but you should consider to do all of the
-following:
-
-* Issue a TLS certificate for the hostname you are assigning to Nixery. In fact,
-  Docker will refuse to pull images from registries that do not use TLS (with
-  the exception of `.local` domains).
-* Configure signed GCS URLs to avoid having to make your bucket world-readable.
-* Configure request timeouts for Nixery if you have your own web server in front
-  of it. This will be natively supported by Nixery in the future.
-
-## 6. `WEB_DIR`
-
-All the URLs accessed by Docker registry clients start with `/v2/`.
-This means that it is possible to serve a static website from Nixery
-itself (as long as you don't want to serve anything starting with `/v2`).
-This is how, for instance, https://nixery.dev shows the website for Nixery,
-while it is also possible to e.g. `docker pull nixery.dev/shell`.
-
-When running Nixery, you must set the `WEB_DIR` environment variable.
-When Nixery receives requests that don't look like registry requests,
-it tries to serve them using files in the directory indicated by `WEB_DIR`.
-If the directory doesn't exist, Nixery will run fine but serve 404.
-
--------
-
-[^1]: Nixery will not work with Nix channels older than `nixos-19.03`.
-
-[^2]: This documentation will be updated with instructions on how to best set up
-    a custom Nix repository. Nixery expects custom package sets to be a superset
-    of `nixpkgs`, as it uses `lib` and other features from `nixpkgs`
-    extensively.
-
-[GKE]: https://cloud.google.com/kubernetes-engine/
-[nixery#4]: https://github.com/tazjin/nixery/issues/4
-[Nix]: https://nixos.org/nix
-[gcs]: https://cloud.google.com/storage/
-[signed-urls]: under-the-hood.html#5-image-layers-are-requested
-[ADC]: https://cloud.google.com/docs/authentication/production#finding_credentials_automatically
-[nixinstall]: https://nixos.org/manual/nix/stable/installation/installing-binary.html
-[nixchannel]: https://nixos.wiki/wiki/Nix_channels
diff --git a/tools/nixery/docs/src/under-the-hood.md b/tools/nixery/docs/src/under-the-hood.md
deleted file mode 100644
index 4b79830010..0000000000
--- a/tools/nixery/docs/src/under-the-hood.md
+++ /dev/null
@@ -1,129 +0,0 @@
-# Under the hood
-
-This page serves as a quick explanation of what happens under-the-hood when an
-image is requested from Nixery.
-
-<!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-refresh-toc -->
-
-- [1. The image manifest is requested](#1-the-image-manifest-is-requested)
-- [2. Nix fetches and prepares image content](#2-nix-fetches-and-prepares-image-content)
-- [3. Layers are grouped, created, hashed, and persisted](#3-layers-are-grouped-created-hashed-and-persisted)
-- [4. The manifest is assembled and returned to the client](#4-the-manifest-is-assembled-and-returned-to-the-client)
-- [5. Image layers are requested](#5-image-layers-are-requested)
-
-<!-- markdown-toc end -->
-
---------
-
-## 1. The image manifest is requested
-
-When container registry clients such as Docker pull an image, the first thing
-they do is ask for the image manifest. This is a JSON document describing which
-layers are contained in an image, as well as some additional auxiliary
-information.
-
-This request is of the form `GET /v2/$imageName/manifests/$imageTag`.
-
-Nixery receives this request and begins by splitting the image name into its
-path components and substituting meta-packages (such as `shell`) for their
-contents.
-
-For example, requesting `shell/htop/git` results in Nixery expanding the image
-name to `["bashInteractive", "coreutils", "htop", "git"]`.
-
-If Nixery is configured with a private Nix repository, it also looks at the
-image tag and substitutes `latest` with `master`.
-
-It then invokes Nix with three parameters:
-
-1. image contents (as above)
-2. image tag
-3. configured package set source
-
-## 2. Nix fetches and prepares image content
-
-Using the parameters above, Nix imports the package set and begins by mapping
-the image names to attributes in the package set.
-
-A special case during this process is packages with uppercase characters in
-their name, for example anything under `haskellPackages`. The registry protocol
-does not allow uppercase characters, so the Nix code will translate something
-like `haskellpackages` (lowercased) to the correct attribute name.
-
-After identifying all contents, Nix uses the `symlinkJoin` function to
-create a special layer with the "symlink farm" required to let the
-image function like a normal disk image.
-
-Nix then returns information about the image contents as well as the
-location of the special layer to Nixery.
-
-## 3. Layers are grouped, created, hashed, and persisted
-
-With the information received from Nix, Nixery determines the contents
-of each layer while optimising for the best possible cache efficiency
-(see the [layering design doc][] for details).
-
-With the grouped layers, Nixery then begins to create compressed
-tarballs with all required contents for each layer. As these tarballs
-are being created, they are simultaneously being hashed (as the image
-manifest must contain the content-hashes of all layers) and persisted
-to storage.
-
-Storage can be either a remote [Google Cloud Storage][gcs] bucket, or
-a local filesystem path.
-
-During this step, Nixery checks its build cache (see [Caching][]) to
-determine whether a layer needs to be built or is already cached from
-a previous build.
-
-*Note:* While this step is running (which can take some time in the case of
-large first-time image builds), the registry client is left hanging waiting for
-an HTTP response. Unfortunately the registry protocol does not allow for any
-feedback back to the user at this point, so from the user's perspective things
-just ... hang, for a moment.
-
-## 4. The manifest is assembled and returned to the client
-
-Once armed with the hashes of all required layers, Nixery assembles
-the OCI Container Image manifest which describes the structure of the
-built image and names all of its layers by their content hash.
-
-This manifest is returned to the client.
-
-## 5. Image layers are requested
-
-The client now inspects the manifest and determines which of the
-layers it is currently missing based on their content hashes. Note
-that different container runtimes will handle this differently, and in
-the case of certain engine and storage driver combinations (e.g.
-Docker with OverlayFS) layers might be downloaded again even if they
-are already present.
-
-For each of the missing layers, the client now issues a request to
-Nixery that looks like this:
-
-`GET /v2/${imageName}/blob/sha256:${layerHash}`
-
-Nixery receives these requests and handles them based on the
-configured storage backend.
-
-If the storage backend is GCS, it *redirects* them to Google Cloud
-Storage URLs, responding with an `HTTP 303 See Other` status code and
-the actual download URL of the layer.
-
-Nixery supports using private buckets which are not generally world-readable, in
-which case [signed URLs][] are constructed using a private key. These allow the
-registry client to download each layer without needing to care about how the
-underlying authentication works.
-
-If the storage backend is the local filesystem, Nixery will attempt to
-serve the layer back to the client from disk.
-
----------
-
-That's it. After these five steps the registry client has retrieved all it needs
-to run the image produced by Nixery.
-
-[gcs]: https://cloud.google.com/storage/
-[signed URLs]: https://cloud.google.com/storage/docs/access-control/signed-urls
-[layering design doc]: https://storage.googleapis.com/nixdoc/nixery-layers.html
diff --git a/tools/nixery/docs/theme/favicon.png b/tools/nixery/docs/theme/favicon.png
deleted file mode 100644
index f510bde197..0000000000
--- a/tools/nixery/docs/theme/favicon.png
+++ /dev/null
Binary files differdiff --git a/tools/nixery/docs/theme/nixery.css b/tools/nixery/docs/theme/nixery.css
deleted file mode 100644
index c240e693d5..0000000000
--- a/tools/nixery/docs/theme/nixery.css
+++ /dev/null
@@ -1,3 +0,0 @@
-h2, h3 {
-  margin-top: 1em;
-}
diff --git a/tools/nixery/go.mod b/tools/nixery/go.mod
index dfaeb72068..9e896ffb40 100644
--- a/tools/nixery/go.mod
+++ b/tools/nixery/go.mod
@@ -3,22 +3,12 @@ module github.com/google/nixery
 go 1.15
 
 require (
-	cloud.google.com/go/storage v1.18.2
-	github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect
-	github.com/cespare/xxhash/v2 v2.1.2 // indirect
-	github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 // indirect
-	github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1 // indirect
-	github.com/envoyproxy/go-control-plane v0.10.0 // indirect
-	github.com/envoyproxy/protoc-gen-validate v0.6.2 // indirect
+	cloud.google.com/go/storage v1.22.1
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
-	github.com/google/go-cmp v0.5.6
-	github.com/pkg/xattr v0.4.4
+	github.com/google/go-cmp v0.5.8
+	github.com/im7mortal/kmutex v1.0.1 // indirect
+	github.com/pkg/xattr v0.4.7
 	github.com/sirupsen/logrus v1.8.1
-	golang.org/x/net v0.0.0-20211029160332-540bb53d3b2e // indirect
-	golang.org/x/oauth2 v0.0.0-20211028175245-ba495a64dcb5
-	golang.org/x/sys v0.0.0-20211029162942-c1bf0bb051ef // indirect
-	gonum.org/v1/gonum v0.9.3
-	google.golang.org/api v0.60.0 // indirect
-	google.golang.org/genproto v0.0.0-20211029142109-e255c875f7c7 // indirect
-	google.golang.org/grpc v1.41.0 // indirect
+	golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401
+	gonum.org/v1/gonum v0.11.0
 )
diff --git a/tools/nixery/go.sum b/tools/nixery/go.sum
index 312cbfaa2e..5b6054fb60 100644
--- a/tools/nixery/go.sum
+++ b/tools/nixery/go.sum
@@ -24,16 +24,25 @@ cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWc
 cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=
 cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
 cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
-cloud.google.com/go v0.97.0 h1:3DXvAyifywvq64LfkKaMOmkWPS1CikIQdMe2lY9vxU8=
 cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
+cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
+cloud.google.com/go v0.100.2 h1:t9Iw5QH5v4XtlEQaCtUY7x6sCABps8sW0acw7e2WQ6Y=
+cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A=
 cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
 cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
 cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
 cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
 cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
 cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
+cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow=
+cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM=
+cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M=
+cloud.google.com/go/compute v1.6.0 h1:XdQIN5mdPTSBVwSIVDuY5e8ZzVAccsHvD3qTEz4zIps=
+cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s=
 cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
 cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
+cloud.google.com/go/iam v0.3.0 h1:exkAomrVUuzx9kWFI1wm3KI0uoDeUFPB4kKGzx6x+Gc=
+cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY=
 cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
 cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
 cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
@@ -43,24 +52,24 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
 cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
 cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
 cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
-cloud.google.com/go/storage v1.18.2 h1:5NQw6tOn3eMm0oE8vTkfjau18kjL79FlMjy/CHTpmoY=
-cloud.google.com/go/storage v1.18.2/go.mod h1:AiIj7BWXyhO5gGVmYJ+S8tbkCx3yb0IMjua8Aw4naVM=
+cloud.google.com/go/storage v1.22.1 h1:F6IlQJZrZM++apn9V5/VfS3gbTUYg98PS3EMQAzqtfg=
+cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8=
+git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY=
+github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk=
 github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
+github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM=
 github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
 github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
+github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
-github.com/census-instrumentation/opencensus-proto v0.3.0 h1:t/LhUZLVitR1Ow2YOnduCsavhwFUklBMoGVYUCqmCqk=
-github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
-github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
 github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
-github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@@ -68,13 +77,11 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
 github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
-github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 h1:hzAQntlaYRkVSFEfj9OTWlVV1H155FMD8BTKktLv0QI=
 github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
 github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
 github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
 github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
 github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
-github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1 h1:zH8ljVhhq7yC0MIeUL/IviMtY8hx2mK8cN9wEYb8ggw=
 github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -87,22 +94,23 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m
 github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
 github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
 github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
-github.com/envoyproxy/go-control-plane v0.10.0 h1:WVt4HEPbdRbRD/PKKPbPnIVavO6gk/h673jWyIJ016k=
-github.com/envoyproxy/go-control-plane v0.10.0/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ=
+github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
-github.com/envoyproxy/protoc-gen-validate v0.6.2 h1:JiO+kJTpmYGjEodY7O1Zk8oZcNz1+f30UtwtXoFUPzE=
-github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws=
 github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
 github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g=
 github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks=
 github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY=
+github.com/go-fonts/liberation v0.2.0/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY=
 github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY=
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U=
+github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk=
+github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M=
+github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M=
 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
 github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -152,8 +160,10 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
 github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
+github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
+github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
 github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
 github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@@ -180,44 +190,46 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
 github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
-github.com/googleapis/gax-go/v2 v2.1.1 h1:dp3bWCh+PPO1zjRRiCSczJav13sBvG4UhNyVTa1KqdU=
 github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
+github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM=
+github.com/googleapis/gax-go/v2 v2.3.0 h1:nRJtk3y8Fm770D42QV6T90ZnvFZyk7agSo3Q+Z9p3WI=
+github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM=
+github.com/googleapis/go-type-adapters v1.0.0 h1:9XdMn+d/G57qq1s8dNc5IesGCXHf6V2HZ2JwRxfA2tA=
+github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
 github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
-github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/im7mortal/kmutex v1.0.1 h1:zAACzjwD+OEknDqnLdvRa/BhzFM872EBwKijviGLc9Q=
+github.com/im7mortal/kmutex v1.0.1/go.mod h1:f71c/Ugk/+58OHRAgvgzPP3QEiWGUjK13fd8ozfKWdo=
 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
 github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
 github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
 github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
-github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w=
 github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY=
 github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
+github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
-github.com/pkg/xattr v0.4.4 h1:FSoblPdYobYoKCItkqASqcrKCxRn9Bgurz0sCBwzO5g=
-github.com/pkg/xattr v0.4.4/go.mod h1:sBD3RAqlr8Q+RC3FutZcikpT8nyDrIEEBw2J744gVWs=
+github.com/pkg/xattr v0.4.7 h1:XoA3KzmFvyPlH4RwX5eMcgtzcaGBaSvgt3IoFQfbrmQ=
+github.com/pkg/xattr v0.4.7/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
+github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk=
 github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
 github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
 github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
-github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
-github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
@@ -230,6 +242,7 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
 github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
 go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
 go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
@@ -242,7 +255,6 @@ go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqe
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -269,6 +281,10 @@ golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+o
 golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
+golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
+golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
+golang.org/x/image v0.0.0-20220302094943-723b81ca9867/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
 golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -292,7 +308,7 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
+golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -328,9 +344,11 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
 golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20211029160332-540bb53d3b2e h1:2lVrcCMRP9p7tfk4KUpV1ESqtf49jpihlUtYnSj67k4=
-golang.org/x/net v0.0.0-20211029160332-540bb53d3b2e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220325170049-de3da57026de h1:pZB1TWnKi+o4bENlbzAgLrEbY4RMYmUIRobMcSmfeYc=
+golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -346,9 +364,12 @@ golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ
 golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20211028175245-ba495a64dcb5 h1:v79phzBz03tsVCUTbvTBmmC3CUXF5mKYt7DA4ZVldpM=
-golang.org/x/oauth2 v0.0.0-20211028175245-ba495a64dcb5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
+golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
+golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
+golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401 h1:zwrSfklXn0gxyLRX/aR+q6cgHbV/ItVyzbPlbA+dkAw=
+golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -388,7 +409,6 @@ golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -403,17 +423,24 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211029162942-c1bf0bb051ef h1:1ZMK6QI8sz0q1ficx9/snrJ8E/PeRW7Oagamf+0xp10=
-golang.org/x/sys v0.0.0-20211029162942-c1bf0bb051ef/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f h1:8w7RhxzTVgUzw/AH/9mUV5q0vMgy40SQRursCcfmkCw=
+golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -481,19 +508,22 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f h1:GGU+dLjvlC3qDwqYgL6UgRmHXhOOgns0bZu2Ty5mm6U=
+golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
 gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
-gonum.org/v1/gonum v0.9.3 h1:DnoIG+QAMaF5NvxnGe/oKsgKcAc6PcUyl8q0VetfQ8s=
 gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0=
-gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0 h1:OE9mWmgKkjJyEmDAAtGMPjXu+YNeGvK9VTSHY6+Qihc=
+gonum.org/v1/gonum v0.11.0 h1:f1IJhK4Km5tBJmaiJXtk/PkL4cdVX6J+tGiM187uT5E=
+gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA=
 gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
 gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
 gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY=
+gonum.org/v1/plot v0.10.1/go.mod h1:VZW5OlhkL1mysU9vaqNHnsy86inf6Ot+jB3r+BczCEo=
 google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
 google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
 google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
@@ -523,9 +553,13 @@ google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6
 google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
 google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
 google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=
-google.golang.org/api v0.58.0/go.mod h1:cAbP2FsxoGVNwtgNAmmn3y5G1TWAiVYRmg4yku3lv+E=
-google.golang.org/api v0.60.0 h1:eq/zs5WPH4J9undYM9IP1O7dSr7Yh8Y0GtSCpzGzIUk=
-google.golang.org/api v0.60.0/go.mod h1:d7rl65NZAkEQ90JFzqBjcRq1TVeG5ZoGV3sSpEnnVb4=
+google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=
+google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo=
+google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g=
+google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA=
+google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8=
+google.golang.org/api v0.74.0 h1:ExR2D+5TYIrMphWgs5JCgwRhEDlPDXXrLwHHMgPHTXE=
+google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -573,6 +607,7 @@ google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6D
 google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
 google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
 google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
 google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
@@ -589,12 +624,22 @@ google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEc
 google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
 google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
 google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
-google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
 google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20211016002631-37fc39342514/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20211021150943-2b146023228c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20211029142109-e255c875f7c7 h1:aaSaYY/DIDJy3f/JLXWv6xJ1mBQSRnQ1s5JhAFTnzO4=
-google.golang.org/genproto v0.0.0-20211029142109-e255c875f7c7/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
+google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
+google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
+google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
+google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E=
+google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
+google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
+google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335 h1:2D0OT6tPVdrQTOnVe1VQjfJPTED6EZ7fdJ/f6Db6OsY=
+google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -620,8 +665,11 @@ google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQ
 google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
 google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
 google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
-google.golang.org/grpc v1.41.0 h1:f+PlOh7QV4iIJkPrx5NQ7qaNGFQ3OTse67yaDHfju4E=
-google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k=
+google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
+google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
+google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
+google.golang.org/grpc v1.46.0 h1:oCjezcn6g6A75TGoKYBPgKmVBLexhYLM6MebdrPApP8=
+google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
@@ -635,8 +683,9 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
 google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
 google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
+google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -652,6 +701,7 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
 honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=
 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
 rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
 rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
diff --git a/tools/nixery/builder/layers.go b/tools/nixery/layers/layers.go
index 5e37e62681..7251c61a84 100644
--- a/tools/nixery/builder/layers.go
+++ b/tools/nixery/layers/layers.go
@@ -11,10 +11,10 @@
 //
 // # Inputs
 //
-// * a graph of Nix runtime dependencies, generated via exportReferenceGraph
-// * popularity values of each package in the Nix package set (in the form of a
-//   direct reference count)
-// * a maximum number of layers to allocate for the image (the "layer budget")
+//   - a graph of Nix runtime dependencies, generated via exportReferenceGraph
+//   - popularity values of each package in the Nix package set (in the form of a
+//     direct reference count)
+//   - a maximum number of layers to allocate for the image (the "layer budget")
 //
 // # Algorithm
 //
@@ -30,14 +30,15 @@
 // │     │
 // │     v
 // └───> D ───> F
-//       │
-//       └────> G
+//
+//	│
+//	└────> G
 //
 // Each node (i.e. package) is then visited to determine how important
 // it is to separate this node into its own layer, specifically:
 //
-// 1. Is the node within a certain threshold percentile of absolute
-//    popularity within all of nixpkgs? (e.g. `glibc`, `openssl`)
+//  1. Is the node within a certain threshold percentile of absolute
+//     popularity within all of nixpkgs? (e.g. `glibc`, `openssl`)
 //
 // 2. Is the node's runtime closure above a threshold size? (e.g. 100MB)
 //
@@ -103,7 +104,7 @@
 //
 // Layer budget: 10
 // Layers: { E }, { D, F }, { A }, { B }, { C }
-package builder
+package layers
 
 import (
 	"crypto/sha1"
@@ -121,7 +122,7 @@ import (
 // dependencies of a derivation.
 //
 // This is generated in Nix by using the exportReferencesGraph feature.
-type runtimeGraph struct {
+type RuntimeGraph struct {
 	References struct {
 		Graph []string `json:"graph"`
 	} `json:"exportReferencesGraph"`
@@ -142,19 +143,19 @@ type Popularity = map[string]int
 
 // Layer represents the data returned for each layer that Nix should
 // build for the container image.
-type layer struct {
+type Layer struct {
 	Contents    []string `json:"contents"`
 	MergeRating uint64
 }
 
 // Hash the contents of a layer to create a deterministic identifier that can be
 // used for caching.
-func (l *layer) Hash() string {
+func (l *Layer) Hash() string {
 	sum := sha1.Sum([]byte(strings.Join(l.Contents, ":")))
 	return fmt.Sprintf("%x", sum)
 }
 
-func (a layer) merge(b layer) layer {
+func (a Layer) merge(b Layer) Layer {
 	a.Contents = append(a.Contents, b.Contents...)
 	a.MergeRating += b.MergeRating
 	return a
@@ -177,7 +178,7 @@ var nixRegexp = regexp.MustCompile(`^/nix/store/[a-z0-9]+-`)
 
 // PackageFromPath returns the name of a Nix package based on its
 // output store path.
-func packageFromPath(path string) string {
+func PackageFromPath(path string) string {
 	return nixRegexp.ReplaceAllString(path, "")
 }
 
@@ -185,7 +186,7 @@ func packageFromPath(path string) string {
 // the dot format used by GraphViz, into which the dependency graph
 // can be rendered.
 func (c *closure) DOTID() string {
-	return packageFromPath(c.Path)
+	return PackageFromPath(c.Path)
 }
 
 // bigOrPopular checks whether this closure should be considered for
@@ -228,7 +229,7 @@ func insertEdges(graph *simple.DirectedGraph, cmap *map[string]*closure, node *c
 }
 
 // Create a graph structure from the references supplied by Nix.
-func buildGraph(refs *runtimeGraph, pop *Popularity) *simple.DirectedGraph {
+func buildGraph(refs *RuntimeGraph, pop *Popularity) *simple.DirectedGraph {
 	cmap := make(map[string]*closure)
 	graph := simple.NewDirectedGraph()
 
@@ -288,7 +289,7 @@ func buildGraph(refs *runtimeGraph, pop *Popularity) *simple.DirectedGraph {
 // Extracts a subgraph starting at the specified root from the
 // dominator tree. The subgraph is converted into a flat list of
 // layers, each containing the store paths and merge rating.
-func groupLayer(dt *flow.DominatorTree, root *closure) layer {
+func groupLayer(dt *flow.DominatorTree, root *closure) Layer {
 	size := root.Size
 	contents := []string{root.Path}
 	children := dt.DominatedBy(root.ID())
@@ -305,7 +306,7 @@ func groupLayer(dt *flow.DominatorTree, root *closure) layer {
 	// Contents are sorted to ensure that hashing is consistent
 	sort.Strings(contents)
 
-	return layer{
+	return Layer{
 		Contents:    contents,
 		MergeRating: uint64(root.Popularity) * size,
 	}
@@ -316,10 +317,10 @@ func groupLayer(dt *flow.DominatorTree, root *closure) layer {
 //
 // Layers are merged together until they fit into the layer budget,
 // based on their merge rating.
-func dominate(budget int, graph *simple.DirectedGraph) []layer {
+func dominate(budget int, graph *simple.DirectedGraph) []Layer {
 	dt := flow.Dominators(graph.Node(0), graph)
 
-	var layers []layer
+	var layers []Layer
 	for _, n := range dt.DominatedBy(dt.Root().ID()) {
 		layers = append(layers, groupLayer(&dt, n.(*closure)))
 	}
@@ -347,7 +348,7 @@ func dominate(budget int, graph *simple.DirectedGraph) []layer {
 // groupLayers applies the algorithm described above the its input and returns a
 // list of layers, each consisting of a list of Nix store paths that it should
 // contain.
-func groupLayers(refs *runtimeGraph, pop *Popularity, budget int) []layer {
+func GroupLayers(refs *RuntimeGraph, pop *Popularity, budget int) []Layer {
 	graph := buildGraph(refs, pop)
 	return dominate(budget, graph)
 }
diff --git a/tools/nixery/manifest/manifest.go b/tools/nixery/manifest/manifest.go
index d61514d2f6..5638b576eb 100644
--- a/tools/nixery/manifest/manifest.go
+++ b/tools/nixery/manifest/manifest.go
@@ -54,8 +54,8 @@ type imageConfig struct {
 	} `json:"rootfs"`
 
 	Config struct {
-		Cmd []string `json:"cmd,omitempty"`
-		Env []string `json:"env,omitempty"`
+		Cmd []string `json:",omitempty"`
+		Env []string `json:",omitempty"`
 	} `json:"config"`
 }
 
diff --git a/tools/nixery/popcount/README.md b/tools/nixery/popcount/README.md
index 8485a4d30e..3e56f99d57 100644
--- a/tools/nixery/popcount/README.md
+++ b/tools/nixery/popcount/README.md
@@ -34,6 +34,6 @@ It currently does not evaluate nested attribute sets (such as
    ```
 
    In essence, this will trim Nix's store paths and hashes from the output,
-   count the occurences of each package and return the output as JSON. All
+   count the occurrences of each package and return the output as JSON. All
    packages that have no references other than themselves are removed from the
    output.
diff --git a/tools/nixery/prepare-image/prepare-image.nix b/tools/nixery/prepare-image/prepare-image.nix
index bb88983cf6..28022fe42f 100644
--- a/tools/nixery/prepare-image/prepare-image.nix
+++ b/tools/nixery/prepare-image/prepare-image.nix
@@ -13,7 +13,7 @@
 {
   # Description of the package set to be used (will be loaded by load-pkgs.nix)
   srcType ? "nixpkgs"
-, srcArgs ? "nixos-20.09"
+, srcArgs ? "nixos-unstable"
 , system ? "x86_64-linux"
 , importArgs ? { }
 , # Path to load-pkgs.nix
@@ -89,6 +89,19 @@ let
     in
     attrByPath path fetchLower s;
 
+  # Workaround for a workaround in nixpkgs: Unquoted language
+  # identifiers can not start with numbers in Nix, but some package
+  # names start with numbers (such as `1password`).
+  #
+  # In nixpkgs convention, these identifiers are prefixed with
+  # underscores (e.g. `_1password`), however this is not accepted by
+  # the Docker registry protocol.
+  #
+  # To make this work, we detect these kinds of packages and add the
+  # missing underscore.
+  needsUnderscore = pkg: (builtins.match "^[0-9].*" pkg) != null;
+  normalisedPackages = map (p: if needsUnderscore p then "_${p}" else p) (fromJSON packages);
+
   # allContents contains all packages successfully retrieved by name
   # from the package set, as well as any errors encountered while
   # attempting to fetch a package.
@@ -104,7 +117,7 @@ let
         then attrs // { errors = attrs.errors ++ [ res ]; }
         else attrs // { contents = attrs.contents ++ [ res ]; };
       init = { contents = [ ]; errors = [ ]; };
-      fetched = (map (deepFetch pkgs) (fromJSON packages));
+      fetched = (map (deepFetch pkgs) normalisedPackages);
     in
     foldl' splitter init fetched;
 
@@ -155,7 +168,7 @@ let
   # Metadata about the symlink layer which is required for serving it.
   # Two different hashes are computed for different usages (inclusion
   # in manifest vs. content-checking in the layer cache).
-  symlinkLayerMeta = fromJSON (readFile (runCommand "symlink-layer-meta.json"
+  symlinkLayerMeta = fromJSON (builtins.unsafeDiscardStringContext (readFile (runCommand "symlink-layer-meta.json"
     {
       buildInputs = [ coreutils jq openssl ];
     } ''
@@ -164,11 +177,11 @@ let
 
     jq -n -c --arg tarHash $tarHash --arg size $layerSize --arg path ${symlinkLayer} \
       '{ size: ($size | tonumber), tarHash: $tarHash, path: $path }' >> $out
-  ''));
+  '')));
 
   # Final output structure returned to Nixery if the build succeeded
   buildOutput = {
-    runtimeGraph = fromJSON (readFile runtimeGraph);
+    runtimeGraph = fromJSON (builtins.unsafeDiscardStringContext (readFile runtimeGraph));
     symlinkLayer = symlinkLayerMeta;
   };
 
diff --git a/tools/nixery/web/index.html b/tools/nixery/web/index.html
new file mode 100644
index 0000000000..354c4913b2
--- /dev/null
+++ b/tools/nixery/web/index.html
@@ -0,0 +1,166 @@
+<!DOCTYPE html>
+<head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <meta name="description" content="The Virus Lounge">
+  <link rel="stylesheet" type="text/css" href="https://static.tvl.fyi/latest/tvl.css" media="all">
+  <link rel="icon" type="image/webp" href="/favicon.webp">
+  <title>Nixery</title>
+</head>
+<body class="light">
+  <img src="./nixery-logo.png" alt="Nixery">
+  <hr>
+
+  <p>
+    Welcome to this instance of Nixery, an ad-hoc container image registry that provides
+    packages from the <a href="https://nixos.org/nix">Nix</a> package manager.
+  </p>
+
+  <p>
+    You can pull container images from this registry
+    at <code><span class="registry-hostname">nixery.dev</span></code> by appending any
+    packages that you need in the URL, separated by slashes.
+  </p>
+
+  <noscript>
+    <p class="cheddar-callout cheddar-tip">
+      <strong>NOTE:</strong> When pulling from a private Nixery instance,
+      replace <code>nixery.dev</code> in the above examples with your registry address.
+    </p>
+  </noscript>
+
+  <h2><a href="#demo" aria-hidden="true" class="anchor" id="demo"></a>Demo</h2>
+
+  <noscript>
+    <p>
+      The interactive demo needs Javascript to run, but you can just read the Usage
+      instructions below instead
+    </p>
+  </noscript>
+
+  <script src="https://asciinema.org/a/262583.js" id="asciicast-262583" async data-autoplay="true" data-loop="true"></script>
+
+  <h2><a href="#usage" aria-hidden="true" class="anchor" id="usage"></a>Usage</h2>
+
+  <p>
+    These usage examples assume that you use Docker, but should not be much different for
+    other OCI-compatible platforms.
+  </p>
+
+  <p>
+    Pull an image from this registry, separating each package you want included by a
+    slash:
+  </p>
+
+  <pre style="background-color:#f6f8fa;padding:16px;"><span style="color:#323232;">docker pull <span class="registry-hostname">nixery.dev</span>/shell/git/htop</span></pre>
+
+  <p>
+    This gives you an image with <code>git</code>, <code>htop</code> and an interactively
+    configured shell. You could run it like this:
+  </p>
+
+  <pre style="background-color:#f6f8fa;padding:16px;"><span style="color:#323232;">docker run -ti <span class="registry-hostname">nixery.dev</span>/shell/git/htop bash</span></pre>
+
+  <p>
+    Each path segment corresponds either to a key in the Nix package set, or a
+    meta-package that automatically expands to several other packages.
+  </p>
+
+  <p>
+    Meta-packages <strong>must</strong> be the first path component if they are used.
+    Currently there are only two meta-packages:
+  </p>
+
+  <ul>
+    <li>
+      <p>
+        <code>shell</code>, which provides a <code>bash</code>-shell with interactive
+        configuration and standard tools like <code>coreutils</code></p>
+    </li>
+    <li>
+      <p><code>arm64</code>, which provides ARM64 binaries</p>
+    </li>
+  </ul>
+
+  <h2><a href="#faq" aria-hidden="true" class="anchor" id="faq"></a>FAQ</h2>
+
+  <h3>
+    <a href="#how-does-this-work" aria-hidden="true" class="anchor" id="how-does-this-work"></a>
+    How does this work?
+  </h3>
+
+  <p>
+    The short version is that we use the Nix package manager and an optimised
+    <a href="https://tazj.in/blog/nixery-layers">layering strategy</a>.
+  </p>
+
+  <p>
+    Check out <a href="https://www.youtube.com/watch?v=pOI9H4oeXqA">the Nixery talk</a>
+    from NixCon 2019 for more information.
+  </p>
+
+  <h3>
+    <a href="#should-i-depend-on-nixerydev-in-production" aria-hidden="true" class="anchor" id="should-i-depend-on-nixerydev-in-production"></a>
+    Should I depend on <code>nixery.dev</code> in production?
+  </h3>
+
+  <p>
+    While we appreciate the enthusiasm, if you would like to use Nixery in your production
+    project we recommend setting up a private instance. The public Nixery
+    at <code>nixery.dev</code> is run on a best-effort basis and we make no guarantees
+    about availability.
+  </p>
+
+  <h3>
+    <a href="#who-made-this" aria-hidden="true" class="anchor" id="who-made-this"></a>
+    Who made this?
+  </h3>
+
+  <p>
+    Nixery was written by <a href="https://tazj.in">tazjin</a>, originally at Google.
+    These days Nixery is maintained by <a href="https://tvl.su">TVL</a>.
+  </p>
+  <p>
+    Nixery would not be possible without the many people that have contributed to Nix and
+    nixpkgs over time, maybe you could become one of them?
+  </p>
+
+  <h3>
+    <a href="#where-is-the-source-code-for-this" aria-hidden="true" class="anchor" id="where-is-the-source-code-for-this"></a>
+    Where is the source code for this?
+  </h3>
+
+  <p>
+    Nixery lives in the <a href="https://cs.tvl.fyi/depot/-/tree/tools/nixery">TVL
+      monorepo</a>. All development happens there and follows
+      the <a href="https://cs.tvl.fyi/depot/-/blob/docs/CONTRIBUTING.md">TVL contribution
+      guidelines</a>.
+  </p>
+
+  <p>
+    We <em>mirror</em> the source code <a href="https://github.com/tazjin/nixery">to
+    Github</a> but do not guarantee that anyone will look at PRs or issues there.
+  </p>
+
+  <hr>
+  <footer>
+    <p class="footer">
+      <a class="uncoloured-link" href="https://at.tvl.fyi/?q=//tools/nixery">code</a>
+      |
+      <a class="uncoloured-link" href="https://cl.tvl.fyi/q/file:%2522%255Etools/nixery/.*%2522">reviews</a>
+      |
+      <a class="uncoloured-link" href="https://b.tvl.fyi/">bugs</a>
+    </p>
+    <p class="lod">ಠ_ಠ</p>
+  </footer>
+
+  <script>
+    /* Replace the hostnames above with the one at which this page runs. */
+    let hostname = window.location.hostname;
+    if (hostname != '') {
+        for (span of document.getElementsByClassName("registry-hostname")) {
+            span.textContent = hostname;
+        }
+    }
+  </script>
+</body>
diff --git a/tools/nixery/docs/src/nixery-logo.png b/tools/nixery/web/nixery-logo.png
index fcf77df3d6..fcf77df3d6 100644
--- a/tools/nixery/docs/src/nixery-logo.png
+++ b/tools/nixery/web/nixery-logo.png
Binary files differ