IPCR Forge admin: name, health, unpublish, allowlist, key backup
ipcr 1.3.0 (gateway/): - admin API on ADMIN_LISTEN (Bearer token in STATE_DIR/admin-token): status (key, node, storage, IPNS record here and on the network via delegated routing, ENS/DNSLink name check), providers/announce, peers, images, unpublish (tombstone + unpin unused objects), retag, reimport, config (name, allowlist), republish, key import (restore with sequence = max(network, local) + 1; previous key kept, kept copies reused) - watcher refactored into shared state; config.json with an allowlist (IMPORT_ALLOW_REQUIRED); staging cleanup after publish (IMPORT_CLEANUP) - IPNS records published with IPNS_LIFETIME / IPNS_TTL; superseded trees unpinned - encrypted publisher-key backups (PBKDF2 + AES-256-GCM), ipcrd key decrypt bridge 0.2.0 (bridge/): - /admin: Gitea OAuth2 login (PKCE, site admins only), signed session, same-origin + X-IPCR-Admin on writes; setup registers the OAuth2 app - admin page: publisher name and ENS/DNS check, health, published images, per-repository publish toggles (pushed to IPCR as its allowlist), key export (from Kubo's keystore, encrypted) and restore - public page pull lines use the verified name Compose: ipcr-admin internal network, keystore mount, 7-day IPNS lifetime, staging GC at registry start. Verified on holyhorse (docs/forge.md).
This commit is contained in:
+51
-2
@@ -30,6 +30,15 @@
|
||||
// IPCR_PUBLISHED IPCR's published.json, served as /images.json
|
||||
// (default /srv/ipcr-state/published.json)
|
||||
// RECONCILE full pass interval (default 10m)
|
||||
//
|
||||
// Admin pages (/admin, auth.go and admin.go), on when IPCR_ADMIN is set:
|
||||
//
|
||||
// PUBLIC_HOSTS the hosts the page is served on (ipcr-forge-<domain>, …), comma separated;
|
||||
// setup registers one OAuth2 redirect URI per host
|
||||
// IPCR_ADMIN ipcrd's admin API (e.g. http://ipcr-gateway:4769)
|
||||
// IPCR_ADMIN_TOKEN_FILE its token (default /srv/ipcr-state/admin-token)
|
||||
// KUBO_KEYSTORE Kubo's keystore, read-only, for key backups (default /srv/ipcr-keystore)
|
||||
// IMPORT_PUBLISHER the publisher key's name (default forge)
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -37,6 +46,7 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -104,7 +114,6 @@ func serve() error {
|
||||
}
|
||||
q := newQueue()
|
||||
go q.run(m.sync)
|
||||
go reconcileLoop(m, q, every)
|
||||
|
||||
hook := &hookHandler{secretFile: secrets + "/hook-secret", enqueue: q.add}
|
||||
hmux := http.NewServeMux()
|
||||
@@ -113,8 +122,45 @@ func serve() error {
|
||||
log.Fatal(http.ListenAndServe(env("HOOK_LISTEN", ":8081"), hmux))
|
||||
}()
|
||||
|
||||
mux := webHandler(st, env("IPCR_PUBLISHED", "/srv/ipcr-state/published.json"))
|
||||
if ipcr := os.Getenv("IPCR_ADMIN"); ipcr != "" {
|
||||
au, err := newAuth(splitList(env("PUBLIC_HOSTS", "")), m.gitea.base, secrets, stateDir+"/session-key")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
adm := &adminAPI{
|
||||
ipcr: strings.TrimRight(ipcr, "/"), tokenFile: env("IPCR_ADMIN_TOKEN_FILE", "/srv/ipcr-state/admin-token"),
|
||||
keystore: env("KUBO_KEYSTORE", "/srv/ipcr-keystore"), publisher: env("IMPORT_PUBLISHER", "forge"),
|
||||
settings: stateDir + "/admin.json", gitea: m.gitea, http: &http.Client{Timeout: 3 * time.Minute},
|
||||
}
|
||||
adminRoutes(mux, au, adm)
|
||||
// IPCR publishes nothing until it has the list (IMPORT_ALLOW_REQUIRED): send it as soon as
|
||||
// both ends are up, then after every reconcile.
|
||||
go func() {
|
||||
for {
|
||||
if !m.gitea.hasToken() {
|
||||
time.Sleep(15 * time.Second)
|
||||
continue
|
||||
}
|
||||
err := adm.pushAllowlist()
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
log.Printf("allowlist: %v (retrying)", err)
|
||||
time.Sleep(30 * time.Second)
|
||||
}
|
||||
}()
|
||||
m.onReconcile = func() {
|
||||
if err := adm.pushAllowlist(); err != nil {
|
||||
log.Printf("allowlist: %v", err)
|
||||
}
|
||||
}
|
||||
log.Printf("bridge: admin pages on, IPCR admin API %s", ipcr)
|
||||
}
|
||||
go reconcileLoop(m, q, every)
|
||||
|
||||
log.Printf("bridge: %s → Radicle (%s), reconcile every %s", m.gitea.base, m.radHome, every)
|
||||
return http.ListenAndServe(env("WEB_LISTEN", ":8080"), webHandler(st, env("IPCR_PUBLISHED", "/srv/ipcr-state/published.json")))
|
||||
return http.ListenAndServe(env("WEB_LISTEN", ":8080"), mux)
|
||||
}
|
||||
|
||||
// reconcileLoop queues every repository Gitea shows as public, plus every one already known (so one
|
||||
@@ -144,6 +190,9 @@ func reconcileLoop(m *mirror, q *queue, every time.Duration) {
|
||||
q.add(id)
|
||||
}
|
||||
}
|
||||
if m.onReconcile != nil {
|
||||
m.onReconcile()
|
||||
}
|
||||
time.Sleep(every)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user