IPCR Forge: org-root image names and per-repository push credentials
Short names: the public repositories of one Gitea organisation (IPCR_ROOT_ORG, then the admin page; metadec on our boxes) are also published at the root of the forge's name, so ipcr.localhost:4767/ipns/metadec.eth/ipcr-hello:1.0.0 works. Collisions (a user/org of that name, another owner's folder) are skipped and explained. Push credentials: the staging registry now sits behind forge-gate (ipcr-forge-bridge gate), in the CI daemon's network namespace, with the registry on a unix socket and no network. Each publishing repository logs in as <owner>/<repo> with an IPCR_PUSH_TOKEN Actions secret the bridge sets, and may push its own name only; IPCR logs in with a credential it generates. Workflows need a docker/login-action step (example and page updated). ipcr 1.4.0: allow.aliases with per-pass alias sync; unpublish/retag on every path; IMPORT_AUTH_GENERATE; names this node publishes resolve via name/resolve --nocache (Kubo's resolve cache lagged, and the generic resolve ignores nocache). bridge 0.3.0: gate, push credentials (rotate in the admin page), root organisation setting, setup creates the org and the example in it; the Radicle description carries the Gitea path (identical documents gave the same RID); OAuth2 app updated only when hosts change, keeping the new secret. Admin page: tabs, one-line keys, name-check feedback, per-tag docker pull commands (IPFS, IPNS, ENS, short path first). Verified on holyhorse (docs/forge.md).
This commit is contained in:
@@ -35,7 +35,7 @@ services:
|
||||
# with the stock `rad` CLI against the node's own home. Serves the forge's page on :8080.
|
||||
# Its install step (forge-setup, below) mints its Gitea token and registers the webhook.
|
||||
ipcr-forge:
|
||||
image: ghcr.io/yundera/ipcr-forge-bridge:0.2.0
|
||||
image: ghcr.io/yundera/ipcr-forge-bridge:0.3.0
|
||||
container_name: ipcr-forge
|
||||
user: $PUID:$PGID
|
||||
read_only: true
|
||||
@@ -65,6 +65,9 @@ services:
|
||||
IPCR_ADMIN_TOKEN_FILE: /srv/ipcr-state/admin-token
|
||||
KUBO_KEYSTORE: /srv/ipcr-keystore
|
||||
IMPORT_PUBLISHER: ${IPCR_IMPORT_PUBLISHER:-forge}
|
||||
# The organisation published at the root of the forge's name (metadec/app → …/app), until
|
||||
# changed in the admin pages. Its members, through Gitea teams, are who can publish there.
|
||||
IPCR_ROOT_ORG: ${IPCR_ROOT_ORG:-}
|
||||
tmpfs:
|
||||
# HOME for git and rad.
|
||||
- /tmp
|
||||
@@ -271,11 +274,43 @@ services:
|
||||
networks:
|
||||
- ci-internal
|
||||
|
||||
# The staging registry, in the daemon's network namespace: `localhost:5000` to the daemon and to
|
||||
# every job (act runs jobs on the host network, which is this namespace), so a plain
|
||||
# `docker push` or buildx with `network=host` works without TLS or credentials — Docker allows
|
||||
# plain HTTP to localhost only. ipcr-gateway reaches it as forge-docker:5000. Nothing
|
||||
# outside ci-internal can.
|
||||
# The staging registry's front door: `localhost:5000` to the daemon and to every job (act runs
|
||||
# jobs on the host network, which is this namespace), forge-docker:5000 to ipcr-gateway. Plain
|
||||
# HTTP, which Docker allows to localhost only. It checks a login on every request: a job logs in
|
||||
# as its repository (<owner>/<repo>, the IPCR_PUSH_TOKEN secret the bridge sets on it) and may
|
||||
# push that name only; IPCR logs in as the importer (state/staging-auth, which it generates).
|
||||
# Same image as the bridge (`ipcr-forge-bridge gate`, bridge/gate.go). See rationale.md.
|
||||
forge-gate:
|
||||
image: ghcr.io/yundera/ipcr-forge-bridge:0.3.0
|
||||
container_name: forge-gate
|
||||
user: $PUID:$PGID
|
||||
read_only: true
|
||||
cap_drop: [ALL]
|
||||
security_opt: ["no-new-privileges:true"]
|
||||
restart: unless-stopped
|
||||
network_mode: service:forge-docker
|
||||
command: ["ipcr-forge-bridge", "gate"]
|
||||
cpu_shares: 20
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 64M
|
||||
environment:
|
||||
TZ: $TZ
|
||||
GATE_LISTEN: ":5000"
|
||||
GATE_UPSTREAM: unix:///run/staging/registry.sock
|
||||
GATE_CREDENTIALS: /gate/credentials.json
|
||||
volumes:
|
||||
# Hashes only, written by the bridge.
|
||||
- ${DATA_ROOT:-/DATA}/AppData/$AppID/bridge/state/gate:/gate:ro
|
||||
- ${DATA_ROOT:-/DATA}/AppData/$AppID/ci/registry-socket:/run/staging
|
||||
depends_on:
|
||||
- forge-docker
|
||||
- forge-registry
|
||||
|
||||
# The staging registry itself: no network at all. It listens on a unix socket in a folder only the
|
||||
# gate also mounts, so nothing reaches it but through the gate: not a job, which runs in the
|
||||
# daemon above and sees neither the folder nor a port.
|
||||
forge-registry:
|
||||
image: registry:3.0.0
|
||||
container_name: forge-registry
|
||||
@@ -283,7 +318,7 @@ services:
|
||||
cap_drop: [ALL]
|
||||
security_opt: ["no-new-privileges:true"]
|
||||
restart: unless-stopped
|
||||
network_mode: service:forge-docker
|
||||
network_mode: none
|
||||
# Garbage collection frees the blobs of deleted tags, and must not run while the registry
|
||||
# serves: so it runs at every start, before serving.
|
||||
entrypoint: ["sh", "-c", "registry garbage-collect --delete-untagged /etc/distribution/config.yml; exec registry serve /etc/distribution/config.yml"]
|
||||
@@ -294,15 +329,17 @@ services:
|
||||
memory: 128M
|
||||
environment:
|
||||
TZ: $TZ
|
||||
REGISTRY_HTTP_ADDR: 0.0.0.0:5000
|
||||
REGISTRY_HTTP_NET: unix
|
||||
REGISTRY_HTTP_ADDR: /run/staging/registry.sock
|
||||
# Upload URLs relative to the request, whatever Host the gate passes on.
|
||||
REGISTRY_HTTP_RELATIVEURLS: "true"
|
||||
# Staging only: IPCR has its own copy once imported, and deletes each tag it has published
|
||||
# (IMPORT_CLEANUP).
|
||||
REGISTRY_STORAGE_DELETE_ENABLED: "true"
|
||||
OTEL_TRACES_EXPORTER: none
|
||||
volumes:
|
||||
- ${DATA_ROOT:-/DATA}/AppData/$AppID/ci/registry:/var/lib/registry
|
||||
depends_on:
|
||||
- forge-docker
|
||||
- ${DATA_ROOT:-/DATA}/AppData/$AppID/ci/registry-socket:/run/staging
|
||||
|
||||
# ═══ Radicle — the public mirror ═══════════════════════════════════════════════
|
||||
# Verbatim from Apps/Radicle; only its data folder moves to $AppID/radicle/. The bridge
|
||||
@@ -438,7 +475,7 @@ services:
|
||||
# ipcrd: TLS front door on 127.0.0.1:4767. Passes /ipfs/<cid> through to the stock nerdctl
|
||||
# registry below, resolves /ipns/<name>:<tag> itself, and pins every image it serves.
|
||||
ipcr-gateway:
|
||||
image: ghcr.io/yundera/ipcr:1.3.0
|
||||
image: ghcr.io/yundera/ipcr:1.4.0
|
||||
container_name: ipcr-gateway
|
||||
# Root for one reason: it writes its CA into the host's Docker trust store (the narrow
|
||||
# bind below), which is root-owned. See rationale.md.
|
||||
@@ -462,6 +499,10 @@ services:
|
||||
# Forge: watch the CI's staging registry, over ci-internal. Internal, so no credentials.
|
||||
IMPORT_WATCH: ${IPCR_IMPORT_WATCH:-http://forge-docker:5000}
|
||||
IMPORT_AUTH: ${IPCR_IMPORT_AUTH:-}
|
||||
# Forge: IPCR's login at the staging registry's gate, generated on first start; the bridge
|
||||
# reads it (read-only ipcr/state) and gives it the importer's rights at the gate.
|
||||
IMPORT_AUTH_FILE: /data/state/staging-auth
|
||||
IMPORT_AUTH_GENERATE: "true"
|
||||
IMPORT_REPOS: ${IPCR_IMPORT_REPOS:-}
|
||||
# Forge: every repository this node builds is published under one IPNS key, "forge", as
|
||||
# /ipns/<forge key>/<repo>:<tag>. One name for the whole forge, which a DNSLink domain or an
|
||||
@@ -517,7 +558,7 @@ services:
|
||||
# ── Stock nerdctl IPFS registry ─────────────────────────────────────────────
|
||||
# `nerdctl ipfs registry serve`, unmodified: translates the registry API into IPFS reads.
|
||||
ipcr-registry:
|
||||
image: ghcr.io/yundera/ipcr:1.3.0
|
||||
image: ghcr.io/yundera/ipcr:1.4.0
|
||||
container_name: ipcr-registry
|
||||
# Root because nerdctl run as any other uid assumes rootless mode and refuses to start
|
||||
# without a rootless containerd — which `ipfs registry serve` never uses. The service has
|
||||
@@ -695,10 +736,13 @@ x-casaos:
|
||||
private later stops the copy, and what was published stays on the network. Private
|
||||
repositories are never copied.
|
||||
|
||||
**Who can publish images.** Only the names of public repositories that are switched on in the
|
||||
admin pages (all are, by default): `<owner>/<repo>`. A build in any repository can still
|
||||
push any name to the staging registry, so whoever can push code here can publish under an
|
||||
allowed name. Read a fork's workflow before approving its run.
|
||||
**Who can publish images.** Each public repository (switched on in the admin pages: all are,
|
||||
by default) publishes under its own name, `<owner>/<repo>`: its workflow logs in to the
|
||||
staging registry with the `IPCR_PUSH_TOKEN` secret the forge sets on it. The repositories of
|
||||
the root organisation (`IPCR_ROOT_ORG`, then the admin pages) are also published at the root,
|
||||
`<repo>`: its members decide who publishes there. Workflows from forks get no secrets.
|
||||
CI runs on a privileged Docker daemon: only give write access to people you trust with the
|
||||
server.
|
||||
fr_fr: |
|
||||
**Commencez ici : [la page de la forge](https://ipcr-forge-$APP_DOMAIN)** : publier, les
|
||||
images publiées, les copies Radicle.
|
||||
@@ -727,11 +771,13 @@ x-casaos:
|
||||
privé ensuite arrête la copie, et ce qui a été publié reste sur le réseau. Les dépôts privés
|
||||
ne sont jamais copiés.
|
||||
|
||||
**Qui peut publier des images.** Seuls les noms des dépôts publics activés dans
|
||||
l'administration (tous, par défaut) : `<propriétaire>/<dépôt>`. Un build de n'importe quel
|
||||
dépôt peut tout de même pousser n'importe quel nom vers le registre de transit : qui peut
|
||||
pousser du code ici peut publier sous un nom autorisé. Lisez le workflow d'un fork avant
|
||||
d'approuver son exécution.
|
||||
**Qui peut publier des images.** Chaque dépôt public (activé dans l'administration : tous,
|
||||
par défaut) publie sous son propre nom, `<propriétaire>/<dépôt>` : son workflow se connecte
|
||||
au registre de transit avec le secret `IPCR_PUSH_TOKEN` que la forge y dépose. Les dépôts de
|
||||
l'organisation racine (`IPCR_ROOT_ORG`, puis l'administration) sont aussi publiés à la
|
||||
racine, `<dépôt>` : ses membres décident qui y publie. Les workflows des forks n'ont pas de
|
||||
secrets. La CI tourne sur un démon Docker privilégié : ne donnez l'accès en écriture qu'à des
|
||||
personnes à qui vous confiez le serveur.
|
||||
index: /
|
||||
webui_port: 3000
|
||||
store_app_id: ipcr-forge
|
||||
@@ -747,6 +793,10 @@ x-compose-app:
|
||||
user: $PUID
|
||||
group: $PGID
|
||||
mode: "0700"
|
||||
# The gate's credentials (hashes), written by the bridge, read by the gate.
|
||||
- path: /DATA/AppData/$AppID/bridge/state/gate
|
||||
user: $PUID
|
||||
group: $PGID
|
||||
# ── Gitea ──
|
||||
- path: /DATA/AppData/$AppID/gitea/data
|
||||
user: $PUID
|
||||
@@ -782,6 +832,11 @@ x-compose-app:
|
||||
user: $PUID
|
||||
group: $PGID
|
||||
mode: "0700"
|
||||
# The staging registry's socket: the registry and the gate only.
|
||||
- path: /DATA/AppData/$AppID/ci/registry-socket
|
||||
user: $PUID
|
||||
group: $PGID
|
||||
mode: "0700"
|
||||
# ── IPCR ──
|
||||
- path: /DATA/AppData/$AppID/ipcr/kubo
|
||||
user: $PUID
|
||||
@@ -859,11 +914,12 @@ x-compose-app:
|
||||
mv /runner/token.new /runner/token
|
||||
chmod 600 /runner/token
|
||||
when: absent:/DATA/AppData/$AppID/gitea/runner/.runner
|
||||
# The bridge's Gitea token (public-only, read:repository), the webhook secret, the system
|
||||
# webhook, the admin pages' OAuth2 app, and the example repository. Idempotent, so on every start: it repairs the webhook.
|
||||
# The bridge's Gitea tokens (public-only read; and write:repository for push secrets), the
|
||||
# webhook secret, the system webhook, the admin pages' OAuth2 app, the root organisation and
|
||||
# the example repository (in that organisation). Idempotent, so on every start: it repairs the webhook.
|
||||
# The admin password is used here only, never stored.
|
||||
- name: forge-setup
|
||||
image: ghcr.io/yundera/ipcr-forge-bridge:0.2.0
|
||||
image: ghcr.io/yundera/ipcr-forge-bridge:0.3.0
|
||||
user: $PUID:$PGID
|
||||
phase: post_up
|
||||
network: $APP_NET
|
||||
@@ -872,7 +928,7 @@ x-compose-app:
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- GITEA_PASSWORD="$APP_DEFAULT_PASSWORD" GITEA_URL=http://gitea:3000 PUBLIC_HOSTS="ipcr-forge-$APP_DOMAIN,ipcr-forge-$APP_PUBLIC_IP_DASH.nip.io,ipcr-forge-$APP_PUBLIC_IP_DASH.sslip.io" ipcr-forge-bridge setup
|
||||
- GITEA_PASSWORD="$APP_DEFAULT_PASSWORD" GITEA_URL=http://gitea:3000 IPCR_ROOT_ORG="$IPCR_ROOT_ORG" PUBLIC_HOSTS="ipcr-forge-$APP_DOMAIN,ipcr-forge-$APP_PUBLIC_IP_DASH.nip.io,ipcr-forge-$APP_PUBLIC_IP_DASH.sslip.io" ipcr-forge-bridge setup
|
||||
when: always
|
||||
webui-host: gitea-${domain}
|
||||
webui-path: /
|
||||
|
||||
Reference in New Issue
Block a user