Files
insights/bin/upgrade-objectstorage
zeekay 1b367e2b62 chore: rebase the tree onto upstream master
The tree was a debranded snapshot of upstream taken around 2026-02-20, and
upstream has moved about five months since. This replaces it with upstream
master, mechanically re-derived: bin/debrand does the rename, and the
pipeline and squashed migrations are restored from the shipped tree.

Not a merge. The previous import replaced the tree wholesale in a single
commit, so there is no shared content history to merge against. Reproducing
the rename on the new tree is what actually converges.

What survives: .hanzo/workflows, the Dockerfiles and bin/docker* entrypoints,
compose files, LLM.md, and insights/migrations. The squashed initial
migration is load-bearing — the live database records it by name, so
upstream's ~918 individual migrations cannot come back.

Nine products are dropped as unreferenced. Twenty-one more that we do not run
are kept anyway because the core app still imports them; unpicking those is a
refactor and does not belong in a rebase.

This base does not build yet, and the reasons are not in the rename:
upstream now wants Python 3.13.13 against our 3.12.12 and Django 5.2 against
our 4.2, and the dependency manifests still name upstream packages that we
republish under our own names. Those are decisions, not renames.
2026-08-01 15:44:40 -07:00

358 lines
12 KiB
Bash
Executable File

#!/usr/bin/env bash
set -e
set -o pipefail
# Image used to read the old MinIO data — must match the format the dev/hobby MinIO
# wrote (same RELEASE tag as docker-compose.hobby.yml).
MINIO_IMAGE="minio/minio:RELEASE.2025-04-22T22-12-26Z"
TEMP_CONTAINER="objectstorage-salvage-src"
S3_KEY="object_storage_root_user"
S3_SECRET="object_storage_root_password"
show_help() {
cat <<'EOF'
Objectstorage MinIO → SeaweedFS Salvage Script
⚠️ IMPORTANT: This script is for LOCAL DEVELOPMENT environments.
Insights dev objectstorage now runs SeaweedFS in a fresh named volume
(objectstorage-data). Your old MinIO objects live on the previous (now
orphaned) volume and are invisible to the new SeaweedFS — the on-disk formats
are not compatible, so there is no in-place conversion. This script salvages
them by copying object-by-object over S3.
The script automatically:
- Finds the orphaned MinIO objectstorage volume (looks for /data/.minio.sys)
- Spins up a temporary MinIO container reading that volume
- Copies every bucket into the running SeaweedFS objectstorage
- Removes the temporary container when done
The old MinIO volume is left untouched, so this is safe to run later if you
forgot — and safe to re-run (existing objects are skipped). Remove the old
volume once you've verified the data landed.
Usage:
insightscli deploy:upgrade-objectstorage Interactive mode (default)
insightscli deploy:upgrade-objectstorage --list List all MinIO volumes
./bin/upgrade-objectstorage <volume-id> Salvage a specific volume
Examples:
# Interactive: detect the orphaned volume and copy it over
insightscli deploy:upgrade-objectstorage
# Find orphaned MinIO volumes
insightscli deploy:upgrade-objectstorage --list
# Salvage a specific volume
./bin/upgrade-objectstorage 6e93806fad73d030739a55dd1c6594bf925833dd1a2ef59b3fb14b54bd6f5d73
EOF
}
# A MinIO data volume always has a .minio.sys directory at its root.
is_minio_volume() {
docker run --rm -v "$1:/data:ro" alpine test -d /data/.minio.sys 2>/dev/null
}
list_minio_volumes() {
echo "Scanning for MinIO objectstorage volumes..."
echo ""
docker volume ls -q | while read -r vol; do
if is_minio_volume "$vol"; then
SIZE=$(docker run --rm -v "$vol:/data:ro" alpine du -sh /data 2>/dev/null | awk '{print $1}' || echo "unknown")
IN_USE=$(docker ps -a --filter "volume=$vol" --format "{{.Names}}" 2>/dev/null)
if [ -n "$IN_USE" ]; then
echo "✅ $vol"
echo " Size: $SIZE | In use by: $IN_USE"
else
echo "⚠️ $vol"
echo " Size: $SIZE | ORPHANED (not mounted)"
fi
echo ""
fi
done
}
# The compose file in the directory you run this from is authoritative — whoever runs
# the command controls how containers come up. We deliberately do NOT read the running
# container's recorded compose path: the objectstorage container is shared across
# worktrees (same COMPOSE_PROJECT_NAME), so it may have last been started from another
# worktree still on MinIO. Trusting that label would force-recreate MinIO again instead
# of swapping to the SeaweedFS your freshly-pulled compose defines. So: pull, run, done.
detect_compose_file() {
if [ -f "docker-compose.dev.yml" ]; then
echo "docker-compose.dev.yml"
elif [ -f "docker-compose.yml" ]; then
echo "docker-compose.yml"
else
echo ""
fi
}
# Return the first volume that looks like MinIO data and isn't backing a running
# container (the live objectstorage is SeaweedFS, so it won't match anyway).
detect_minio_volume() {
docker volume ls -q | while read -r vol; do
if is_minio_volume "$vol"; then
echo "$vol"
return
fi
done
}
# Image of the running objectstorage container, empty if none is running.
running_objectstorage_image() {
local container
container=$(docker ps --filter "label=com.docker.compose.service=objectstorage" --format "{{.ID}}" | head -1)
[ -z "$container" ] && return
docker inspect "$container" --format '{{.Config.Image}}' 2>/dev/null || true
}
# The destination must be SeaweedFS. If the old MinIO objectstorage is still running
# (branch checked out but `insightscli up` not run yet), copying into it would silently
# no-op — the data is already there — and never populate SeaweedFS. Recreate it as
# SeaweedFS first; that orphans the old volume, which is exactly what we salvage from.
ensure_objectstorage_running() {
local COMPOSE_FILE="$1"
local image
image=$(running_objectstorage_image)
if [ -n "$image" ] && [[ "$image" != *minio* ]]; then
return
fi
if [ -n "$image" ]; then
echo "objectstorage is still the old MinIO — recreating it as SeaweedFS first..."
docker compose -f "$COMPOSE_FILE" up -d --force-recreate objectstorage
else
echo "Starting objectstorage (SeaweedFS) as the copy destination..."
docker compose -f "$COMPOSE_FILE" up -d objectstorage
fi
for _ in $(seq 1 30); do
image=$(running_objectstorage_image)
if [ -n "$image" ] && [[ "$image" != *minio* ]]; then
return
fi
sleep 1
done
echo "❌ objectstorage is not running as SeaweedFS. Run 'insightscli up' to swap it, then re-run."
exit 1
}
# Host port the running objectstorage publishes for S3 (defaults to 19000).
objectstorage_host_port() {
local container
container=$(docker ps --filter "label=com.docker.compose.service=objectstorage" --format "{{.ID}}" | head -1)
local mapping
mapping=$(docker port "$container" 19000 2>/dev/null | head -1)
if [ -n "$mapping" ]; then
echo "${mapping##*:}"
else
echo "19000"
fi
}
cleanup_temp_container() {
docker rm -f "$TEMP_CONTAINER" >/dev/null 2>&1 || true
}
# boto3 sends buffered (non-streaming) signed PUTs, which open-mode SeaweedFS
# accepts — unlike mc / aws-chunked streaming uploads, which it rejects.
copy_buckets() {
local SRC_ENDPOINT="$1"
local DST_ENDPOINT="$2"
if ! python3 -c "import boto3" >/dev/null 2>&1; then
echo "❌ boto3 is required. Run this inside the Insights dev environment (flox)."
exit 1
fi
python3 - "$SRC_ENDPOINT" "$DST_ENDPOINT" "$S3_KEY" "$S3_SECRET" <<'PY'
import sys, time, boto3
from botocore.config import Config
from botocore.exceptions import ClientError
src_ep, dst_ep, key, secret = sys.argv[1:5]
def client(ep):
return boto3.client(
"s3", endpoint_url=ep, aws_access_key_id=key, aws_secret_access_key=secret,
region_name="us-east-1",
config=Config(s3={"addressing_style": "path"}, signature_version="s3v4"),
)
src, dst = client(src_ep), client(dst_ep)
for name, c in (("source", src), ("destination", dst)):
for _ in range(60):
try:
c.list_buckets()
break
except Exception:
time.sleep(1)
else:
print(f"❌ {name} object storage never became reachable")
sys.exit(1)
def exists(c, bucket, key):
try:
c.head_object(Bucket=bucket, Key=key)
return True
except ClientError:
return False
buckets = [b["Name"] for b in src.list_buckets().get("Buckets", [])]
if not buckets:
print("No buckets on the old volume — nothing to copy.")
sys.exit(0)
copied = skipped = 0
for bucket in buckets:
try:
dst.create_bucket(Bucket=bucket)
except ClientError:
pass
print(f"→ {bucket}")
for page in src.get_paginator("list_objects_v2").paginate(Bucket=bucket):
for obj in page.get("Contents", []):
obj_key = obj["Key"]
if exists(dst, bucket, obj_key):
skipped += 1
continue
o = src.get_object(Bucket=bucket, Key=obj_key)
body = o["Body"].read()
kw = {"Bucket": bucket, "Key": obj_key, "Body": body}
if o.get("ContentType"):
kw["ContentType"] = o["ContentType"]
dst.put_object(**kw)
copied += 1
print(f"✅ Copied {copied} objects ({skipped} already present, skipped)")
PY
}
migrate_volume() {
local SOURCE_VOLUME="$1"
local COMPOSE_FILE="$2"
if ! is_minio_volume "$SOURCE_VOLUME"; then
echo "❌ Error: Volume $SOURCE_VOLUME does not look like MinIO data (no .minio.sys)"
exit 1
fi
SIZE=$(docker run --rm -v "$SOURCE_VOLUME:/data:ro" alpine du -sh /data 2>/dev/null | awk '{print $1}' || echo "unknown")
echo "Found MinIO objectstorage volume: $SOURCE_VOLUME ($SIZE)"
echo ""
read -r -p "Copy this data into the running SeaweedFS objectstorage? [y/N] " confirm
if [[ ! "$confirm" =~ ^([yY][eE][sS]|[yY])$ ]]; then
echo "Cancelled."
exit 0
fi
ensure_objectstorage_running "$COMPOSE_FILE"
DST_PORT=$(objectstorage_host_port)
cleanup_temp_container
trap cleanup_temp_container EXIT
echo ""
echo "Step 1: Starting temporary MinIO reading the old volume..."
docker run -d \
--name "$TEMP_CONTAINER" \
-p "127.0.0.1::19000" \
-v "$SOURCE_VOLUME:/data" \
-e MINIO_ROOT_USER="$S3_KEY" \
-e MINIO_ROOT_PASSWORD="$S3_SECRET" \
"$MINIO_IMAGE" \
server --address ":19000" /data >/dev/null
SRC_MAPPING=$(docker port "$TEMP_CONTAINER" 19000 | head -1)
SRC_PORT="${SRC_MAPPING##*:}"
echo ""
echo "Step 2: Copying every bucket into SeaweedFS..."
copy_buckets "http://127.0.0.1:$SRC_PORT" "http://127.0.0.1:$DST_PORT"
echo ""
echo "Step 3: Removing temporary container..."
cleanup_temp_container
trap - EXIT
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "✅ Objectstorage salvage complete!"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "Verify your data, then remove the old MinIO volume to reclaim disk:"
echo " docker volume rm $SOURCE_VOLUME"
echo ""
}
case "${1:-}" in
--help|-h)
show_help
exit 0
;;
--list)
list_minio_volumes
exit 0
;;
"")
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🪣 Objectstorage MinIO → SeaweedFS salvage"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
COMPOSE_FILE=$(detect_compose_file)
if [ -z "$COMPOSE_FILE" ]; then
echo "❌ Error: No docker-compose file found"
exit 1
fi
echo "Using compose file: $COMPOSE_FILE"
echo ""
DETECTED_VOLUME=$(detect_minio_volume)
if [ -n "$DETECTED_VOLUME" ]; then
migrate_volume "$DETECTED_VOLUME" "$COMPOSE_FILE"
else
echo "No MinIO objectstorage data detected — nothing to salvage."
echo ""
echo "If you have data in an orphaned MinIO volume:"
echo " insightscli deploy:upgrade-objectstorage --list # List all MinIO volumes"
echo " ./bin/upgrade-objectstorage <volume-id> # Salvage a specific volume"
echo ""
fi
;;
*)
VOLUME_ID="$1"
COMPOSE_FILE=$(detect_compose_file)
if [ -z "$COMPOSE_FILE" ]; then
echo "❌ Error: No docker-compose file found"
exit 1
fi
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🪣 Objectstorage MinIO → SeaweedFS salvage"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "Using compose file: $COMPOSE_FILE"
echo ""
migrate_volume "$VOLUME_ID" "$COMPOSE_FILE"
;;
esac