diff --git a/TODO.md b/TODO.md index ec3fe77..5e13fc6 100644 --- a/TODO.md +++ b/TODO.md @@ -39,12 +39,13 @@ compute of its own yet. 502 is no longer cached. Whatever raises its hand for the api host should raise it for this too. - [ ] **Nothing says which index a deployed site expects.** The site pins a - MegaMek version in its own source and fetches `/assets/helm//…`; - nothing here fails when that version has no index published. The check + MegaMek version in its own source and fetches `/assets/helm/…`; nothing + here fails when the pinned pair has no index published. The check belongs at deploy time, where the release id and the bucket are both in - reach. `megamek_index_version` is now a second place the same version is - written down — the api's, for the match report — so the check has two - values to answer for rather than one. + reach, and it now has a second reader to answer for: the api reads + `//units.json`, and an apply that + names a prefix helm never wrote leaves every match report with no + machine pictures until somebody looks at a log. - [ ] **Nothing prunes an old MegaMek's art.** Each release writes a prefix in the assets-megamek bucket and nothing removes one, deliberately — a match started under the old image still draws from it. 39MB a version is cheap enough diff --git a/envs/lance.blue/main.tf b/envs/lance.blue/main.tf index 2312ba1..a1d7d19 100644 --- a/envs/lance.blue/main.tf +++ b/envs/lance.blue/main.tf @@ -172,15 +172,27 @@ module "api_host" { # advertise api.lance.blue, which is the one thing the behaviour exists # to avoid. SHARE_ORIGIN = "https://${var.domain_name}" - - # helm's index of the unit library, read once at api startup: it is what - # says which of MegaMek's pictures each design is drawn with, so a match - # report shows the machines that fought rather than one silhouette per - # weight class. Served from the distribution below, beside the art it - # names. Empty when no version is set, which the api reads as "no index". - UNIT_INDEX_URL = var.megamek_index_version == "" ? "" : "https://${var.domain_name}/assets/helm/${var.megamek_index_version}/units.json" } + # helm's index of the unit library, read once at api startup: it is what + # says which of MegaMek's pictures each design is drawn with, so a match + # report shows the machines that fought rather than one silhouette per + # class. Served from the distribution below, beside the art it names. + # + # Not part of match_env, which rides in user_data and replaces the instance + # when it changes. A helm release is a routine deploy and must not cost a + # new box, so this goes the way the image and arena tags go: a parameter the + # host's poller watches, which restarts the container within ten seconds. + # + # Both halves of the path or none of it. helm keys its artifacts by its own + # release and then by the MegaMek they describe, so an address built from + # one of them alone names a file that was never written. + unit_index_url = ( + var.megamek_index_version == "" || var.releases.helm == "" + ? "" + : "https://${var.domain_name}/assets/helm/${var.releases.helm}/${var.megamek_index_version}/units.json" + ) + tags = { Component = "api", Environment = local.env } } @@ -198,7 +210,8 @@ module "assets_megamek" { # helm's generated index of MegaMek's unit library: what a force-building # screen filters on, keyed by the MegaMek release it describes so it cannot -# drift from the art beside it. scripts/assets-helm.sh publishes one. +# drift from the art beside it. helm's own scripts/deploy.sh publishes one, +# under a prefix named for the helm release that built it. module "assets_helm" { source = "../../modules/asset-bucket" diff --git a/envs/lance.blue/variables.tf b/envs/lance.blue/variables.tf index 240e43a..aa0d051 100644 --- a/envs/lance.blue/variables.tf +++ b/envs/lance.blue/variables.tf @@ -45,6 +45,9 @@ variable "releases" { arena - arena image tag a match task runs. site - release id headquarters pushed the built site under. CloudFront serves releases/ in the site bucket; empty serves the root. + helm - release id helm published its browser artifacts under. The + index and the wasm live at assets/helm//..., written by + helm's own scripts/deploy.sh, which also writes this value. A deploy is changing one of these and applying. EOT @@ -53,6 +56,7 @@ variable "releases" { api = string arena = string site = string + helm = string }) validation { @@ -172,7 +176,7 @@ variable "posthog_salt" { } variable "megamek_index_version" { - description = "The MegaMek release whose published unit index the api reads, e.g. \"0.51.0\". scripts/assets-helm.sh puts one at /assets/helm//units.json, and helm resolved the picture for every design in it; the api uses that to draw a machine on a match report as its own design rather than as its weight class. Empty means no index, and the reports draw silhouettes. Only a release whose index has actually been published belongs here - a version that was never synced is a report full of broken images." + description = "The MegaMek release the api reads an index for, e.g. \"0.51.0\". helm publishes its artifacts under assets/helm///units.json - two levels, because the index is a function of both helm's code and a particular MegaMek - and the api uses it to draw a machine on a match report as its own design rather than as its class. Must be a release helm has actually published an index for under the current releases.helm; empty means no index, and the reports draw no machine pictures at all." type = string default = "" } diff --git a/modules/api-host/main.tf b/modules/api-host/main.tf index b7d712e..d4c5196 100644 --- a/modules/api-host/main.tf +++ b/modules/api-host/main.tf @@ -143,6 +143,17 @@ resource "aws_ssm_parameter" "arena_tag" { value = var.arena_tag } +# The unit index the api reads. Same delivery as the tags above and for the +# same reason: helm publishes a release, this value moves, and the poller +# restarts the container - where a user_data change would replace the host. +resource "aws_ssm_parameter" "unit_index_url" { + name = "${local.ssm_prefix}/unit-index-url" + type = "String" + # SSM has no empty string. "none" is the absent value, and the host turns it + # back into an unset variable, which is what the api reads as "no index". + value = var.unit_index_url == "" ? "none" : var.unit_index_url +} + resource "aws_iam_instance_profile" "this" { name = "${var.name_prefix}-api-host-${var.environment}" role = aws_iam_role.this.name diff --git a/modules/api-host/user_data.sh.tpl b/modules/api-host/user_data.sh.tpl index 4516014..e5c3e62 100644 --- a/modules/api-host/user_data.sh.tpl +++ b/modules/api-host/user_data.sh.tpl @@ -39,9 +39,10 @@ mkdir -p /data/api /data/caddy docker network create web # Everything about how the api container runs, in one place. Idempotent: it -# exits immediately when the running container already matches the image-tag -# and arena-tag parameters. Secrets are fetched into shell variables and passed -# to docker as env - they never land on disk. +# exits immediately when the running container already matches every parameter +# it is converged on - the image tag, the arena tag and the unit index. +# Secrets are fetched into shell variables and passed to docker as env - they +# never land on disk. cat > /usr/local/bin/api-deploy <<'DEPLOY' #!/bin/bash set -euo pipefail @@ -55,12 +56,28 @@ image="${image_repo}:$tag" arena=$(aws ssm get-parameter --region ${region} \ --name "${ssm_prefix}/arena-tag" --query Parameter.Value --output text) +# helm's unit index, the same shape of value: env only, never pulled here, and +# read by the api once at startup - so a new helm release moves this and the +# container has to come back for it. "none" is how an absent index is spelt, +# because SSM has no empty string. +index=$(aws ssm get-parameter --region ${region} \ + --name "${ssm_prefix}/unit-index-url" --query Parameter.Value --output text) +if [ "$index" = "none" ]; then + index="" +fi + current=$(docker inspect -f '{{.Config.Image}}' api 2>/dev/null || true) -current_arena=$(docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' api 2>/dev/null \ - | sed -n 's/^ARENA_VERSION=//p' || true) -[ "$image" = "$current" ] && [ "$arena" = "$current_arena" ] && exit 0 +env_of() { + docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' api 2>/dev/null \ + | sed -n "s/^$1=//p" || true +} +current_arena=$(env_of ARENA_VERSION) +current_index=$(env_of UNIT_INDEX_URL) +[ "$image" = "$current" ] && [ "$arena" = "$current_arena" ] && + [ "$index" = "$current_index" ] && exit 0 echo "api-deploy: $current -> $image (arena $current_arena -> $arena)" +[ "$index" = "$current_index" ] || echo "api-deploy: unit index $current_index -> $index" aws ecr get-login-password --region ${region} \ | docker login --username AWS --password-stdin "${registry}" docker pull "$image" @@ -86,6 +103,7 @@ docker run -d --restart always --name api --network web \ -e WEB_ORIGIN="${web_origin}" \ -e DB_PATH=/data/api/headquarters.sqlite \ -e ARENA_VERSION="$arena" \ + -e UNIT_INDEX_URL="$index" \ %{ for k, v in match_env ~} -e ${k}="${v}" \ %{ endfor ~} diff --git a/modules/api-host/variables.tf b/modules/api-host/variables.tf index 514a093..6783aed 100644 --- a/modules/api-host/variables.tf +++ b/modules/api-host/variables.tf @@ -65,6 +65,12 @@ variable "match_policy_arns" { default = {} } +variable "unit_index_url" { + description = "Address of helm's unit index, which the api reads once at startup to draw each machine on a match report as its own design. Delivered as an SSM parameter rather than in user_data: a helm release changes this, and a release must not replace the instance. Empty means no index, which the api accepts and reports." + type = string + default = "" +} + variable "match_env" { description = "Extra environment for the api container: the match set (cluster, task family, subnets, task security group, artifacts bucket - empty disables match launching) plus standalone api settings like the cookie domain and the flare space." type = map(string)