Add deploy script for scouting webserver

This patch lets folks do this to deploy the webserver:

    $ bazel run //scouting/deploy

It will copy the webserver to the scouting server, install it, and
start it. You can find a summary in the new README.md file.

Signed-off-by: Philipp Schrader <philipp.schrader@gmail.com>
Change-Id: I997738db483dc0e7d01a336ef4dde150ccfed070
diff --git a/WORKSPACE b/WORKSPACE
index 398202c..58fb14f 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -1057,3 +1057,20 @@
         "https://github.com/bazelbuild/buildtools/archive/refs/tags/4.2.4.tar.gz",
     ],
 )
+
+http_archive(
+    name = "rules_pkg",
+    patch_args = ["-p1"],
+    patches = [
+        "//third_party:rules_pkg/0001-Fix-tree-artifacts.patch",
+    ],
+    sha256 = "62eeb544ff1ef41d786e329e1536c1d541bb9bcad27ae984d57f18f314018e66",
+    urls = [
+        "https://mirror.bazel.build/github.com/bazelbuild/rules_pkg/releases/download/0.6.0/rules_pkg-0.6.0.tar.gz",
+        "https://github.com/bazelbuild/rules_pkg/releases/download/0.6.0/rules_pkg-0.6.0.tar.gz",
+    ],
+)
+
+load("@rules_pkg//:deps.bzl", "rules_pkg_dependencies")
+
+rules_pkg_dependencies()
diff --git a/scouting/BUILD b/scouting/BUILD
index 0ed540b..a769426 100644
--- a/scouting/BUILD
+++ b/scouting/BUILD
@@ -33,6 +33,7 @@
         "//scouting/www:index.html",
         "//scouting/www:zonejs_copy",
     ],
+    visibility = ["//scouting/deploy:__pkg__"],
 )
 
 protractor_ts_test(
diff --git a/scouting/deploy/BUILD b/scouting/deploy/BUILD
new file mode 100644
index 0000000..ed4b9cd
--- /dev/null
+++ b/scouting/deploy/BUILD
@@ -0,0 +1,60 @@
+load("@rules_pkg//pkg:pkg.bzl", "pkg_deb", "pkg_tar")
+load("@rules_pkg//pkg:mappings.bzl", "pkg_files")
+
+pkg_files(
+    name = "systemd_files",
+    srcs = [
+        "scouting.service",
+    ],
+    prefix = "etc/systemd/system",
+)
+
+pkg_tar(
+    name = "server_files",
+    srcs = [
+        "//scouting",
+    ],
+    include_runfiles = True,
+    package_dir = "opt/frc971/scouting_server",
+    strip_prefix = ".",
+)
+
+pkg_tar(
+    name = "deploy_tar",
+    srcs = [
+        ":systemd_files",
+    ],
+    deps = [
+        ":server_files",
+    ],
+)
+
+pkg_deb(
+    name = "frc971-scouting-server",
+    architecture = "amd64",
+    data = ":deploy_tar",
+    description = "The FRC971 scouting web server.",
+    # TODO(phil): What's a good email address for this?
+    maintainer = "frc971@frc971.org",
+    package = "frc971-scouting-server",
+    postinst = "postinst",
+    predepends = [
+        "systemd",
+    ],
+    prerm = "prerm",
+    version = "1",
+)
+
+py_binary(
+    name = "deploy",
+    srcs = [
+        "deploy.py",
+    ],
+    args = [
+        "--deb",
+        "$(location :frc971-scouting-server)",
+    ],
+    data = [
+        ":frc971-scouting-server",
+    ],
+)
diff --git a/scouting/deploy/README.md b/scouting/deploy/README.md
new file mode 100644
index 0000000..6d223da
--- /dev/null
+++ b/scouting/deploy/README.md
@@ -0,0 +1,37 @@
+Deploying the scouting application
+================================================================================
+The scouting application is deployed to `scouting.frc971.org` via `bazel`:
+```console
+$ bazel run //scouting/deploy
+(Reading database ... 119978 files and directories currently installed.)
+Preparing to unpack .../frc971-scouting-server_1_amd64.deb ...
+Removed /etc/systemd/system/multi-user.target.wants/scouting.service.
+Unpacking frc971-scouting-server (1) over (1) ...
+Setting up frc971-scouting-server (1) ...
+Created symlink /etc/systemd/system/multi-user.target.wants/scouting.service → /etc/systemd/system/scouting.service.
+Connection to scouting.frc971.org closed.
+```
+
+You will need SSH access to the scouting server. You can customize the SSH host
+with the `--host` argument.
+
+The Blue Alliance API key
+--------------------------------------------------------------------------------
+You need to set up an API key on the scouting server so that the scraping logic
+can use it. It needs to live in `/var/frc971/scouting/tba_config.json` and look
+as follows:
+```json
+{
+    "api_key": "..."
+}
+```
+
+Starting and stopping the application
+--------------------------------------------------------------------------------
+When you SSH into the scouting server, use `systemctl` to manage
+`scouting.service` like any other service.
+```console
+$ sudo systemctl stop scouting.service
+$ sudo systemctl start scouting.service
+$ sudo systemctl restart scouting.service
+```
diff --git a/scouting/deploy/deploy.py b/scouting/deploy/deploy.py
new file mode 100644
index 0000000..c9886fb
--- /dev/null
+++ b/scouting/deploy/deploy.py
@@ -0,0 +1,34 @@
+import argparse
+from pathlib import Path
+import subprocess
+import sys
+
+def main(argv):
+    """Installs the scouting application on the scouting server."""
+    parser = argparse.ArgumentParser()
+    parser.add_argument(
+        "--deb",
+        type=str,
+        required=True,
+        help="The .deb file to deploy.",
+    )
+    parser.add_argument(
+        "--host",
+        type=str,
+        default="scouting.frc971.org",
+        help="The SSH host to install the scouting web server to.",
+    )
+    args = parser.parse_args(argv[1:])
+    deb = Path(args.deb)
+
+    # Copy the .deb to the scouting server, install it, and delete it again.
+    subprocess.run(["rsync", "-L", args.deb, f"{args.host}:/tmp/{deb.name}"],
+                   check=True, stdin=sys.stdin)
+    subprocess.run(f"ssh -tt {args.host} sudo dpkg -i /tmp/{deb.name}",
+                   shell=True, check=True, stdin=sys.stdin)
+    subprocess.run(f"ssh {args.host} rm -f /tmp/{deb.name}",
+                   shell=True, check=True, stdin=sys.stdin)
+
+
+if __name__ == "__main__":
+    sys.exit(main(sys.argv))
diff --git a/scouting/deploy/postinst b/scouting/deploy/postinst
new file mode 100644
index 0000000..a7a8b16
--- /dev/null
+++ b/scouting/deploy/postinst
@@ -0,0 +1,24 @@
+#!/bin/bash
+
+# This script runs after the frc971-scouting-server package is installed. This
+# script is responsible for making sure the webserver has everything it needs,
+# then starts the webserver.
+
+set -o errexit
+set -o nounset
+set -o pipefail
+
+# Create a directory for the database to live in.
+mkdir -p /var/frc971/scouting/
+
+# Create an empty The Blue Alliance configuration file.
+if [[ ! -e /var/frc971/scouting/tba_config.json ]]; then
+    echo '{}' > /var/frc971/scouting/tba_config.json
+fi
+
+# Make sure it's all usable by the user.
+chown -R www-data:www-data /var/frc971/scouting/
+
+systemctl daemon-reload
+systemctl enable scouting.service
+systemctl start scouting.service || :
diff --git a/scouting/deploy/prerm b/scouting/deploy/prerm
new file mode 100644
index 0000000..31e3fbc
--- /dev/null
+++ b/scouting/deploy/prerm
@@ -0,0 +1,13 @@
+#!/bin/bash
+
+# This script gets run before the frc971-scouting-server package gets removed
+# or upgraded. This script is responsible for stopping the webserver before the
+# underlying files are removed by dpkg.
+
+set -o errexit
+set -o nounset
+set -o pipefail
+
+systemctl stop scouting.service
+systemctl disable scouting.service
+systemctl daemon-reload
diff --git a/scouting/deploy/scouting.service b/scouting/deploy/scouting.service
new file mode 100644
index 0000000..2990f42
--- /dev/null
+++ b/scouting/deploy/scouting.service
@@ -0,0 +1,17 @@
+[Unit]
+Description=FRC971 Scouting Server
+After=systemd-networkd-wait-online.service
+
+[Service]
+User=www-data
+Group=www-data
+Type=simple
+WorkingDirectory=/opt/frc971/scouting_server
+ExecStart=/opt/frc971/scouting_server/scouting/scouting \
+    -port 8080 \
+    -database /var/frc971/scouting/scouting.db \
+    -tba_config /var/frc971/scouting/tba_config.json
+Restart=always
+
+[Install]
+WantedBy=multi-user.target
diff --git a/third_party/rules_pkg/0001-Fix-tree-artifacts.patch b/third_party/rules_pkg/0001-Fix-tree-artifacts.patch
new file mode 100644
index 0000000..567aba7
--- /dev/null
+++ b/third_party/rules_pkg/0001-Fix-tree-artifacts.patch
@@ -0,0 +1,28 @@
+From d654cc64ae71366ea82ac492106e9b2c8fa532d5 Mon Sep 17 00:00:00 2001
+From: Philipp Schrader <philipp.schrader@gmail.com>
+Date: Thu, 10 Mar 2022 23:25:21 -0800
+Subject: [PATCH] Fix tree artifacts
+
+For some reason the upstream code strips the directory names from the
+`babel()` rule that we use. This patch makes it so the directory is
+not stripped.  This makes runfiles layout in the tarball match the
+runfiles layout in `bazel-bin`.
+---
+ pkg/pkg.bzl | 4 ++--
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+diff --git a/pkg/pkg.bzl b/pkg/pkg.bzl
+index d7adbbc..a241b26 100644
+--- a/pkg/pkg.bzl
++++ b/pkg/pkg.bzl
+@@ -157,8 +157,8 @@ def _pkg_tar_impl(ctx):
+                     # Tree artifacts need a name, but the name is never really
+                     # the important part. The likely behavior people want is
+                     # just the content, so we strip the directory name.
+-                    dest = "/".join(d_path.split("/")[0:-1])
+-                    add_tree_artifact(content_map, dest, f, src.label)
++                    #dest = "/".join(d_path.split("/")[0:-1])
++                    add_tree_artifact(content_map, d_path, f, src.label)
+                 else:
+                     # Note: This extra remap is the bottleneck preventing this
+                     # large block from being a utility method as shown below.