carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1#!/usr/bin/env bash
2# One-off cleanup for the empty-app errors chunks in the dev deployment bucket
3# (branch fix-empty-app-key). Run by hand, by Paul, from a laptop. Nothing in
4# the platform runs this, and it never runs on a box.
5#
6# What it cleans: objects under exactly apps/bab// in carlos-dev-640012953448
7# (eu-west-1). An app-less route on leo-climbing (proof.dev.carlosframework.com:
8# account bab, no app, an Addr upstream nobody listens on) had its refused 502s
9# filed under (bab, "") and shipped as apps/bab//errors/leo-climbing/... .
10# hostops.Apps refuses to walk a bucket holding that prefix. The writer fix
11# stops new ones, but the box's pruner no longer touches the prefix either, so
12# the ones already there stay until they are deleted.
13#
14# Usage:
15# 2026-10-16-empty-app-key-cleanup.bash dry run: list and count, delete nothing
16# 2026-10-16-empty-app-key-cleanup.bash --apply delete each current object by exact key
17# 2026-10-16-empty-app-key-cleanup.bash --apply --versions also delete every version and delete marker under the prefix, by key and version id
18#
19# The bucket is versioned. --apply alone deletes each current object with one
20# delete-object call per key, which leaves a delete marker: reversible, and
21# enough for hostops.Apps, which lists current objects only. --versions
22# deletes each listed version and delete marker by its version id, one call
23# each. That is permanent.
24#
25# Safety: the bucket, prefix, profile and region are fixed below and cannot
26# be overridden. The account is checked before anything else. Every key is
27# checked to start with exactly apps/bab// before it is acted on. There is no
28# --recursive and no prefix delete: each call names one key (and, with
29# --versions, one version id). The first failed call stops the run.
30#
31# Run it after the fixed agent has rolled to every box that uses this bucket,
32# or leo-climbing will write new ones (the old agent was still writing at
33# 2026-10-16 06:46Z). The listing and a log of every call go to a run
34# directory printed at the start.
35set -euo pipefail
36
37readonly BUCKET="carlos-dev-640012953448"
38readonly ACCOUNT="640012953448"
39readonly PREFIX="apps/bab//"
40readonly PROFILE="carloku-staging"
41readonly REGION="eu-west-1"
42
43apply=0
44versions=0
45for arg in "$@"; do
46 case "$arg" in
47 --apply) apply=1 ;;
48 --versions) versions=1 ;;
49 *)
50 echo "usage: $0 [--apply [--versions]]" >&2
51 exit 2
52 ;;
53 esac
54done
55if [ "$versions" = 1 ] && [ "$apply" = 0 ]; then
56 echo "--versions only means something with --apply; a dry run already lists every version" >&2
57 exit 2
58fi
59
60aws_() { aws --profile "$PROFILE" --region "$REGION" "$@"; }
61
62got_account="$(aws_ sts get-caller-identity --query Account --output text)"
63if [ "$got_account" != "$ACCOUNT" ]; then
64 echo "profile $PROFILE is account $got_account, want $ACCOUNT; stopping" >&2
65 exit 1
66fi
67
68run="$(mktemp -d "${TMPDIR:-/tmp}/empty-app-cleanup.XXXXXX")"
69echo "run directory: $run"
70
71# count prints "current versions markers" for the prefix.
72count() {
73 local cur ver
74 cur="$(aws_ s3api list-objects-v2 --bucket "$BUCKET" --prefix "$PREFIX" --output json |
75 jq '[.Contents[]?] | length')"
76 ver="$(aws_ s3api list-object-versions --bucket "$BUCKET" --prefix "$PREFIX" --output json |
77 jq -r '"\([.Versions[]?] | length) \([.DeleteMarkers[]?] | length)"')"
78 echo "$cur $ver"
79}
80
81# list writes the work list: one line per call, tab-separated
82# kind (object|version|marker), key, version id ("" for object).
83list() {
84 aws_ s3api list-objects-v2 --bucket "$BUCKET" --prefix "$PREFIX" --output json |
85 jq -r '.Contents[]? | ["object", .Key, ""] | @tsv' >"$run/objects.tsv"
86 aws_ s3api list-object-versions --bucket "$BUCKET" --prefix "$PREFIX" --output json |
87 jq -r '(.Versions[]? | ["version", .Key, .VersionId]), (.DeleteMarkers[]? | ["marker", .Key, .VersionId]) | @tsv' >"$run/versions.tsv"
88}
89
90# check refuses the whole run if any listed key is outside the prefix, or
91# carries a tab, newline or backslash (jq's @tsv escapes those with a
92# backslash, so the key read back would not be the key listed).
93check() {
94 local kind key vid
95 while IFS=$'\t' read -r kind key vid; do
96 if [[ "$key" == *\\* ]]; then
97 echo "listed key with an escaped character: $key; stopping, nothing deleted" >&2
98 exit 1
99 fi
100 if [[ "$key" != "$PREFIX"* ]]; then
101 echo "listed key outside $PREFIX: $key; stopping, nothing deleted" >&2
102 exit 1
103 fi
104 case "$kind" in object | version | marker) ;; *)
105 echo "unexpected row kind $kind for $key; stopping, nothing deleted" >&2
106 exit 1
107 ;;
108 esac
109 done <"$1"
110}
111
112read -r before_cur before_ver before_mark < <(count)
113echo "before: $before_cur current objects, $before_ver versions, $before_mark delete markers under $PREFIX" | tee "$run/counts.txt"
114
115list
116check "$run/objects.tsv"
117check "$run/versions.tsv"
118echo "listed: $(wc -l <"$run/objects.tsv") current objects ($run/objects.tsv), $(wc -l <"$run/versions.tsv") versions and markers ($run/versions.tsv)"
119
120if [ "$apply" = 0 ]; then
121 echo "dry run: nothing deleted. First rows of each list:"
122 head -5 "$run/objects.tsv"
123 head -5 "$run/versions.tsv"
124 echo "re-run with --apply (and --versions to remove every version too)"
125 exit 0
126fi
127
128work="$run/objects.tsv"
129if [ "$versions" = 1 ]; then
130 work="$run/versions.tsv"
131fi
132n=0
133while IFS=$'\t' read -r kind key vid; do
134 if [ "$kind" = object ]; then
135 aws_ s3api delete-object --bucket "$BUCKET" --key "$key" --output json >>"$run/calls.log"
136 else
137 aws_ s3api delete-object --bucket "$BUCKET" --key "$key" --version-id "$vid" --output json >>"$run/calls.log"
138 fi
139 printf '%s\t%s\t%s\n' "$kind" "$key" "$vid" >>"$run/deleted.tsv"
140 n=$((n + 1))
141done <"$work"
142echo "deleted: $n calls ($run/deleted.tsv)"
143
144read -r after_cur after_ver after_mark < <(count)
145echo "after: $after_cur current objects, $after_ver versions, $after_mark delete markers under $PREFIX" | tee -a "$run/counts.txt"
146