From 9359aa441ade02468c836fc68b253bc0a3bd17d9 Mon Sep 17 00:00:00 2001 From: Seongmin Lee Date: Sun, 9 Aug 2026 01:35:22 +0300 Subject: [PATCH] localinfra: add knot2 to localinfra Signed-off-by: Seongmin Lee --- docker-compose.yml | 49 +++++++++++++++++ knot2/Containerfile | 1 + localinfra/Caddyfile | 82 ++++++++++++----------------- localinfra/knot2.Dockerfile | 45 ++++++++++++++++ localinfra/scripts/init-accounts.sh | 1 + localinfra/scripts/init-data.sh | 55 +++++++++++++++---- localinfra/scripts/lib.sh | 10 ++++ 7 files changed, 185 insertions(+), 58 deletions(-) create mode 100644 localinfra/knot2.Dockerfile diff --git a/docker-compose.yml b/docker-compose.yml index 6a3415967..df95edf33 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -50,6 +50,7 @@ services: PDS_URL: http://pds:3000 OWNER_USER: alice KNOT_HOSTNAME: knot.tngl.boltless.dev + KNOT2_HOSTNAME: knot2.tngl.boltless.dev SPINDLE_HOSTNAME: spindle.tngl.boltless.dev volumes: - ./localinfra/scripts:/scripts:ro @@ -68,6 +69,8 @@ services: PDS_URL: http://pds:3000 KNOT_URL: http://knot:5555 KNOT_HOSTNAME: knot.tngl.boltless.dev + KNOT2_URL: http://knot2:5555 + KNOT2_HOSTNAME: knot2.tngl.boltless.dev volumes: - ./localinfra/scripts:/scripts:ro - init-state:/shared:ro @@ -75,6 +78,8 @@ services: depends_on: knot: condition: service_healthy + knot2: + condition: service_healthy networks: [tngl] plc: @@ -110,6 +115,48 @@ services: condition: service_healthy networks: [tngl] + knot2: + build: + context: . + dockerfile: localinfra/knot2.Dockerfile + restart: unless-stopped + environment: + KNOT_HOSTNAME: knot2.tngl.boltless.dev + KNOT_SCAN_PATH: /home/git/repositories + KNOT_SSH_HOST_KEY_FILE: /home/git/keys/host_key + KNOT_SEALED_KEY_FILE: /home/git/keys/sealed.bin + KNOT_MASTER_KEY_ENV: KNOT_MASTER_KEY + KNOT_MASTER_KEY: "9MfYcpOFaJYNzYJziRP3rrkM7DJdxQH/Hjey3LuwPF4=" # dev only + KNOT_PLC_DIRECTORY: https://plc.tngl.boltless.dev + KNOT_APPVIEW_ENDPOINT: https://tngl.boltless.dev + KNOT_EXTRA_CA_FILE: /etc/ssl/certs/tngl.crt + KNOT_XRPC_TRUSTED_PROXY_HEADER: x-forwarded-for + KNOT_XRPC_TRUSTED_PROXIES: 11.0.0.0/24 + KNOT_TLS_HTTP3: "false" + RUST_LOG: info + ports: + - "2223:2222" + volumes: + - knot2-data:/home/git + - init-state:/shared:ro + - ./localinfra/certs/root.crt:/etc/ssl/certs/tngl.crt:ro + healthcheck: + test: ["CMD", "curl", "-fsS", "-o", "/dev/null", "http://localhost:5555/"] + interval: 2s + timeout: 2s + retries: 60 + start_period: 30s + depends_on: + plc: + condition: service_started + pds: + condition: service_healthy + caddy: + condition: service_started + init-accounts: + condition: service_completed_successfully + networks: [tngl] + knot: build: context: . @@ -639,6 +686,7 @@ services: - david.pds.tngl.boltless.dev - jetstream.tngl.boltless.dev - knot.tngl.boltless.dev + - knot2.tngl.boltless.dev - spindle.tngl.boltless.dev - tngl.boltless.dev - mirror.tngl.boltless.dev @@ -713,6 +761,7 @@ volumes: jetstream-data: knot-data: knot-ssh-keys: + knot2-data: knotmirror-data: zoekt-index: spindle-data: diff --git a/knot2/Containerfile b/knot2/Containerfile index 4b5f6f10b..00b4b181b 100644 --- a/knot2/Containerfile +++ b/knot2/Containerfile @@ -8,6 +8,7 @@ COPY Cargo.toml Cargo.lock rust-toolchain.toml ./ COPY crates ./crates COPY bobbin/crates ./bobbin/crates COPY shuttle ./shuttle +COPY gitmirror ./gitmirror COPY lexicons ./lexicons COPY knot2/lexicons ./knot2/lexicons COPY knot2/crates ./knot2/crates diff --git a/localinfra/Caddyfile b/localinfra/Caddyfile index 1d269f212..78a7ec26d 100644 --- a/localinfra/Caddyfile +++ b/localinfra/Caddyfile @@ -13,6 +13,27 @@ } } +# permissive CORS for the vhosts web/ hits cross-origin from the browser, for the ones +# that serve no CORS headers of their own. prod puts the same header set at the edge in +# front of these services, so localinfra matches it here rather than in each service. +# import it only where the upstream is silent, since duplicate headers fail the check. +(cors) { + @cors_preflight method OPTIONS + handle @cors_preflight { + header Access-Control-Allow-Origin "*" + header Access-Control-Allow-Methods "GET, POST, OPTIONS" + header Access-Control-Allow-Headers "Content-Type, authorization, atproto-proxy" + header Access-Control-Max-Age "86400" + respond 204 + } + handle { + header Access-Control-Allow-Origin "*" + header Access-Control-Allow-Methods "GET, POST, OPTIONS" + header Access-Control-Allow-Headers "Content-Type, authorization, atproto-proxy" + reverse_proxy {args[0]} + } +} + # did-method-plc plc.tngl.boltless.dev { tls internal @@ -31,12 +52,20 @@ jetstream.tngl.boltless.dev { reverse_proxy jetstream:6008 } -# knot +# knot. no cors import: the go knot sets its own headers (knotserver/middleware.go), +# and a second set here would send duplicates, which browsers reject outright. knot.tngl.boltless.dev { tls internal reverse_proxy knot:5555 } +# knot2 serves no cors headers of its own, and web/ posts sh.tangled.git.keepCommit to +# whichever knot hosts the repo, straight from the browser. +knot2.tngl.boltless.dev { + tls internal + import cors knot2:5555 +} + # spindle http://spindle.tngl.boltless.dev { reverse_proxy spindle:6555 @@ -50,20 +79,7 @@ spindle.tngl.boltless.dev { # knotmirror mirror.tngl.boltless.dev { tls internal - @cors_preflight method OPTIONS - handle @cors_preflight { - header Access-Control-Allow-Origin "*" - header Access-Control-Allow-Methods "GET, POST, OPTIONS" - header Access-Control-Allow-Headers "Content-Type, authorization, atproto-proxy" - header Access-Control-Max-Age "86400" - respond 204 - } - handle { - header Access-Control-Allow-Origin "*" - header Access-Control-Allow-Methods "GET, POST, OPTIONS" - header Access-Control-Allow-Headers "Content-Type, authorization, atproto-proxy" - reverse_proxy knotmirror:7000 - } + import cors knotmirror:7000 } gitmirror.tngl.boltless.dev { @@ -108,41 +124,13 @@ avatar.tngl.boltless.dev { reverse_proxy avatar:8787 } -# bobbin (read appview / xrpc). permissive CORS so the web/ frontend in the -# browser can hit it cross-origin (bobbin serves no CORS headers itself). +# bobbin (read appview / xrpc) bobbin.tngl.boltless.dev { tls internal - @cors_preflight method OPTIONS - handle @cors_preflight { - header Access-Control-Allow-Origin "*" - header Access-Control-Allow-Methods "GET, POST, OPTIONS" - header Access-Control-Allow-Headers "Content-Type, authorization, atproto-proxy" - header Access-Control-Max-Age "86400" - respond 204 - } - handle { - header Access-Control-Allow-Origin "*" - header Access-Control-Allow-Methods "GET, POST, OPTIONS" - header Access-Control-Allow-Headers "Content-Type, authorization, atproto-proxy" - reverse_proxy bobbin:8090 - } + import cors bobbin:8090 } -# bobbin over plain http on :8090 with permissive CORS, for local web/ dev. -# bobbin serves no CORS headers itself (prod adds them at its reverse proxy). +# bobbin over plain http on :8090, for local web/ dev :8090 { - @cors_preflight method OPTIONS - handle @cors_preflight { - header Access-Control-Allow-Origin "*" - header Access-Control-Allow-Methods "GET, POST, OPTIONS" - header Access-Control-Allow-Headers "Content-Type, authorization, atproto-proxy" - header Access-Control-Max-Age "86400" - respond 204 - } - handle { - header Access-Control-Allow-Origin "*" - header Access-Control-Allow-Methods "GET, POST, OPTIONS" - header Access-Control-Allow-Headers "Content-Type, authorization, atproto-proxy" - reverse_proxy bobbin:8090 - } + import cors bobbin:8090 } diff --git a/localinfra/knot2.Dockerfile b/localinfra/knot2.Dockerfile new file mode 100644 index 000000000..b2e54d575 --- /dev/null +++ b/localinfra/knot2.Dockerfile @@ -0,0 +1,45 @@ +# Development only. Not for production use. +# Unlike knot2/Containerfile (distroless), this image keeps a shell: localinfra +# only learns alice's DID at bootstrap, so the entrypoint has to read it. + +FROM docker.io/library/rust:1.96-slim-trixie AS builder +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates build-essential cmake perl pkg-config clang mold \ + && rm -rf /var/lib/apt/lists/* +ENV RUSTFLAGS="-C linker=clang -C link-arg=-fuse-ld=mold" +WORKDIR /src +COPY Cargo.toml Cargo.lock rust-toolchain.toml ./ +COPY crates ./crates +COPY bobbin/crates ./bobbin/crates +COPY shuttle ./shuttle +COPY gitmirror ./gitmirror +COPY lexicons ./lexicons +COPY knot2/lexicons ./knot2/lexicons +COPY knot2/crates ./knot2/crates +COPY knot2/third_party ./knot2/third_party +RUN cargo build --release --package knot-server +RUN strip target/release/knot-server + +FROM debian:trixie-slim +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates curl \ + && rm -rf /var/lib/apt/lists/* +COPY --from=builder /src/target/release/knot-server /usr/local/bin/knot-server + +COPY <<'EOF' /usr/local/bin/knot-entrypoint.sh +#!/bin/sh +set -eu +[ -z "${KNOT_ADMINS:-}" ] && [ -r /shared/owner-did ] && \ + export KNOT_ADMINS="$(cat /shared/owner-did)" +: "${KNOT_ADMINS:?set via env or /shared/owner-did}" + +# the knot creates its host key and sealed store (parent dirs and all) on +# demand, but scan_path has to already exist before it will start +mkdir -p "${KNOT_SCAN_PATH:-/home/git/repositories}" + +exec /usr/local/bin/knot-server +EOF +RUN chmod +x /usr/local/bin/knot-entrypoint.sh + +EXPOSE 5555 2222 +ENTRYPOINT ["/usr/local/bin/knot-entrypoint.sh"] diff --git a/localinfra/scripts/init-accounts.sh b/localinfra/scripts/init-accounts.sh index 1b4c778d0..2da85ac96 100644 --- a/localinfra/scripts/init-accounts.sh +++ b/localinfra/scripts/init-accounts.sh @@ -139,6 +139,7 @@ printf '[env] wrote label-defaults, label-gfi\n' >&2 # service definitions (under OWNER_DID) put_record "at://$OWNER_DID/sh.tangled.knot/$KNOT_HOSTNAME" "{\"createdAt\": \"${CREATED_AT}\"}" >/dev/null +put_record "at://$OWNER_DID/sh.tangled.knot/$KNOT2_HOSTNAME" "{\"createdAt\": \"${CREATED_AT}\"}" >/dev/null put_record "at://$OWNER_DID/sh.tangled.spindle/$SPINDLE_HOSTNAME" "{\"createdAt\": \"${CREATED_AT}\"}" >/dev/null printf 'done.\n' >&2 diff --git a/localinfra/scripts/init-data.sh b/localinfra/scripts/init-data.sh index 5a6c61597..a84096348 100644 --- a/localinfra/scripts/init-data.sh +++ b/localinfra/scripts/init-data.sh @@ -3,6 +3,8 @@ set -eu : "${KNOT_URL:?KNOT_URL must be set}" : "${KNOT_HOSTNAME:?KNOT_HOSTNAME must be set}" +: "${KNOT2_URL:?KNOT2_URL must be set}" +: "${KNOT2_HOSTNAME:?KNOT2_HOSTNAME must be set}" : "${PDS_HOSTNAME:?PDS_HOSTNAME must be set}" SHARED_DIR="${SHARED_DIR:-/shared}" @@ -18,8 +20,12 @@ BOB_JWT=$(login "$BOB_DID") CHARLIE_DID=$(resolve_handle "charlie.${PDS_HOSTNAME}") DAVID_DID=$(resolve_handle "david.${PDS_HOSTNAME}") +CORE_SOURCE=https://tangled.org/boltless.me/core.archive +CORE_SOURCE_DIRECT=https://knot1.tangled.sh/did:plc:ioighzh4jnrhybayw65gfbia + # every record this script seeds, and who authors it REPO_CORE=at://$OWNER_DID/sh.tangled.repo/core +REPO_CORE_KNOT2=at://$OWNER_DID/sh.tangled.repo/core.knot2 REPO_EMPTY=at://$BOB_DID/sh.tangled.repo/empty-repo ISSUE_1=at://$OWNER_DID/sh.tangled.repo.issue/3lzg6f3aia222 @@ -59,22 +65,49 @@ VOUCH_CHARLIE_DAVID=at://$CHARLIE_DID/sh.tangled.graph.vouch/$DAVID_DID VOUCH_DAVID_ALICE=at://$DAVID_DID/sh.tangled.graph.vouch/$OWNER_DID VOUCH_BOB_DAVID=at://$BOB_DID/sh.tangled.graph.vouch/$DAVID_DID -# service_token JWT LXM → service auth JWT for the knot, on stdout -service_token() { - aud="did:web:$(printf '%s' "$KNOT_HOSTNAME" | sed 's/:/%3A/g')" - - curl -fsS -H "Authorization: Bearer $1" \ - "${PDS_URL}/xrpc/com.atproto.server.getServiceAuth?aud=${aud}&lxm=$2&exp=$(( $(date +%s) + 600 ))" \ - | jq -er '.token' -} +# knot2 +( + KNOT_HOSTNAME=$KNOT2_HOSTNAME + KNOT_URL=$KNOT2_URL + + # idempotent: the knot no-ops a subject that is already a member or an admin + for did in "$BOB_DID" "$CHARLIE_DID" "$DAVID_DID"; do + curl -fsS -o /dev/null -X POST \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $(service_token "$OWNER_JWT" sh.tangled.knot.addMember)" \ + -d "$(jq -nc --arg subject "$did" '{subject:$subject}')" \ + "${KNOT_URL}/xrpc/sh.tangled.knot.addMember" + printf '[knot2] member %s\n' "$did" >&2 + done + + # same upstream as the go knot's core, via $CORE_SOURCE_DIRECT for the reason noted + # there. a fork adopts the source's object format, so this lands as sha1 despite + # knot2 defaulting to sha256. + KNOT2_CORE_DID=$(curl -fsS \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $(service_token "$OWNER_JWT" sh.tangled.repo.create)" \ + -d "$(jq -nc --arg source "$CORE_SOURCE_DIRECT" \ + '{rkey:"core.knot2", name:"core.knot2", defaultBranch:"master", source:$source}')" \ + "${KNOT_URL}/xrpc/sh.tangled.repo.create" | jq -er '.repoDid') + + printf '[knot2] core.knot2 = %s\n' "$KNOT2_CORE_DID" >&2 + + put_record "$REPO_CORE_KNOT2" "$(jq -nc \ + --arg knot "$KNOT_HOSTNAME" \ + --arg repoDid "$KNOT2_CORE_DID" \ + --arg source "$CORE_SOURCE_DIRECT" \ + --arg createdAt "$CREATED_AT" \ + '{knot:$knot, name:"core.knot2", repoDid:$repoDid, source:$source, + createdAt:$createdAt}')" >/dev/null +) # --- repo fixtures --- REPO_DID=$(curl -fsS \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $(service_token "$OWNER_JWT" sh.tangled.repo.create)" \ - -d '{"rkey":"core", "name":"core", "defaultBranch":"master", - "source":"https://tangled.org/boltless.me/core.archive"}' \ + -d "$(jq -nc --arg source "$CORE_SOURCE" \ + '{rkey:"core", name:"core", defaultBranch:"master", source:$source}')" \ "${KNOT_URL}/xrpc/sh.tangled.repo.create" | jq -er '.repoDid') printf '[knot] core = %s\n' "$REPO_DID" >&2 @@ -82,7 +115,7 @@ printf '[knot] core = %s\n' "$REPO_DID" >&2 put_record "$REPO_CORE" "$(jq -nc \ --arg knot "$KNOT_HOSTNAME" \ --arg repoDid "$REPO_DID" \ - --arg source 'https://tangled.org/boltless.me/core.archive' \ + --arg source "$CORE_SOURCE" \ --arg createdAt "$CREATED_AT" \ '{knot:$knot, name:"core", repoDid:$repoDid, source:$source, createdAt:$createdAt}')" >/dev/null diff --git a/localinfra/scripts/lib.sh b/localinfra/scripts/lib.sh index 72536ed75..7e1f8ae76 100644 --- a/localinfra/scripts/lib.sh +++ b/localinfra/scripts/lib.sh @@ -26,6 +26,16 @@ login() { | jq -er '.accessJwt' } +# service_token JWT LXM → service auth JWT for the knot, on stdout. +service_token() { + : "${KNOT_HOSTNAME:?KNOT_HOSTNAME must be set}" + aud="did:web:$(printf '%s' "$KNOT_HOSTNAME" | sed 's/:/%3A/g')" + + curl -fsS -H "Authorization: Bearer $1" \ + "${PDS_URL}/xrpc/com.atproto.server.getServiceAuth?aud=${aud}&lxm=$2&exp=$(( $(date +%s) + 240 ))" \ + | jq -er '.token' +} + # put_record at://DID/COLLECTION/RKEY RECORD_JSON # → the record's cid on stdout, for use in a strongRef (comment subject, replyTo). # callers that don't need it can ignore stdout. -- 2.51.2