mirror of
https://github.com/hanzoai/insights.git
synced 2026-10-30 16:15:31 +00:00
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.
358 lines
12 KiB
Bash
Executable File
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
|