Mirrors the canonical NRP public-* datasets to Source Cooperative
for discoverability, the way catalog/sync/k8s/sync-public-*.yaml mirrors them to MinIO.
NRP S3 stays canonical; source.coop is a downstream public copy.
A bucket/collection is mirrored only if both hold:
- Catalogued — reachable from the STAC root catalog (
public-data/stac/catalog.json). Raw/uncatalogued buckets are not datasets and are not published. - License-clear — the upstream license permits redistribution by a third party. NC / share-alike are fine (we label them); no-redistribution licenses are not.
Per-collection license verdicts live in license-inventory.md
(OK · OK-NC · NO · HOLD). That file + the REPOS/EXCLUDES arrays in
gen-source-sync.sh are the single source of truth for scope.
Repo name = NRP bucket minus public- (1:1). Two get a new repo while the older
differently-versioned legacy repo is left untouched:
| NRP bucket | source.coop repo | legacy repo kept as-is |
|---|---|---|
public-padus (PAD-US 4.1) |
cboettig/padus |
cboettig/pad-us-3 |
public-rivers |
cboettig/rivers |
cboettig/us-rivers |
Two buckets are mirrored partially — HOLD sub-collections (license unconfirmed)
are excluded via per-repo --exclude until terms are confirmed:
| repo | excluded sub-paths | reason |
|---|---|---|
rivers |
american-rivers/{campaigns,ira-watersheds,roo-cjest} |
American Rivers org layers, no clear license |
high-seas |
mpa-candidates |
candidate-MPA source/license unidentified |
- License prohibits redistribution → NRP-only:
hydrobasins(HydroSHEDS v1c: "no stand-alone redistribution"; re-import v2 on release — issue #223),wdpa(+ WD-OECM),icca(UNEP-WCMC Protected Planet),iucn(IUCN Red List — incl. derived richness/ranges). - Not a catalogued dataset / WIP:
datacenters,im3(raw files, no STAC);landfire(WIP, issue #203);ca30x30,wlfw,working-lands(real data but not yet in the STAC catalog — catalogue them first, then add here). tpl— misfiled state-agency data, no clear-license remainder: the only license-clear collection inpublic-tplwaswcb-approved-projects(CDFW BIOS ds672, CA Wildlife Conservation Board) — a CDFW state-agency product, not a Trust for Public Land one. It is being relocated to the plannedpublic-cdfwbucket (geo-agent-ops #19; data-workflows #228), after which mirror it ascboettig/cdfw. The rest ofpublic-tpl(Conservation Almanac, LandVote) is HOLD pending TPL terms, socboettig/tplwas never created.ca-wolves— live product, not a license issue: license is clear (CC0-1.0), but it is a real-time updated product (wolf_*_latest.geojson+snapshots/); a static mirror would be a stale point-in-time copy presented as current. Held until we decide whether to publish only the datedsnapshots/(not the_latestfeeds).- Infra/internal:
public-{test,output,requests,boettiger-lab,data,grids};public-tncempty.
carbon, gfw, mobi, mappinginequality, high-seas hydrothermal-vents (CC-BY-NC),
rfmo (CC-BY-SA), meow (CC-BY-NC), and the GBIF aggregate. Plus pre-existing source.coop
repos fishbase (CC-BY-NC) and carbon. The source.coop product page / STAC for these MUST
state the NC/SA license — we are a non-commercial project and downstream users must know.
- Confirm it's catalogued and license-clear (add a row to
license-inventory.md). - Add the bucket to
REPOSingen-source-sync.sh(andEXCLUDES/MODE/new-repos.mdif needed). ./gen-source-sync.sh→ emits../k8s/source-sync-<repo>.yamland regenerates the scheduled-backup scope (../k8s/source-sync-cron-config.yaml).- Create the
cboettig/<repo>product in the source.coop web UI (it's not automatable — see below). ./dry-run-local.sh <repo>then./run-source-sync.sh <repo>.- Roll the new repo into the weekly backup:
kubectl apply -f ../k8s/source-sync-cron-config.yaml(thesource-syncCronJob picks up the new line on its next run — see Scheduled weekly backup below).
The source rclone remote (k8s rclone-config secret) is a long-term AWS key with access to
the entire source.coop account (every tenant in the shared bucket). Every job hard-codes the
source:us-west-2.opendata.source.coop/cboettig/<repo> dest and refuses to run against any
other path, so a typo can't rclone sync (delete-extras) the bucket root or another account.
Always dry-run-local.sh before the first real sync.
Create repos (manual, web UI): the create API (POST /api/v1/products/{account_id}) is
501 Not implemented in production (the documented /repositories/ API is stale), so create
each repo in the web UI first (visibility: public). The 23 still-to-create repos (id / title /
description, with NC + partial-mirror notes) are in new-repos.md; 7 already
exist (carbon, cpad, fire, gbif, mappinginequality, mobi, social-vulnerability) and only refresh.
Generate jobs: ./gen-source-sync.sh (30 jobs; minio recipe exactly — --transfers 2 --checkers 4 --bwlimit 50M --tpslimit 5 --retries 5, opportunistic, 2 cpu / 4 Gi, one pod/bucket).
Preview then sync (sequentially):
./dry-run-local.sh rivers # preview adds/updates/DELETES + excludes (no writes, no cluster)
./run-source-sync.sh rivers # apply one job, wait for completion
./run-source-sync.sh # all repos, smallest -> largest (gbif ~1.2 TB last)Run sequentially (the runner does this): 30 jobs at --bwlimit 50M in parallel ≈ 1.5 GiB/s
of NRP egress — the opposite of gentle. Monitor: kubectl -n biodiversity get jobs | grep source-sync.
Mirror-with-delete: rclone sync makes the dest an exact copy, deleting stale source.coop
files. For the pre-existing repos this replaces older structure with NRP-canonical content —
dry-run first. This is the riskiest step: several pre-existing repos were populated from an
older layout of their NRP bucket that has since been restructured/re-versioned, so a blind
sync deletes the old-vintage files. That is usually intended (the data lives on NRP under new
paths), but it breaks any citable cboettig/<repo>/… URL to the old paths, so confirm before
running. The 2026-06 refresh dry-ran all 7 and split them:
- Safe (0–2 benign deletions):
social-vulnerability,mappinginequality,cpad— synced. carbon(~445 GiB),fire(~6 GiB): fullsync; oldcogs/*_2010|2018.tif/ 2022 CALFIRE vintages and build cruft removed, replaced by current NRP layout (decision: match NRP).gbif(~1.2 TB): dry-run separately before refreshing (the long pole).
copy vs sync (per-repo MODE in gen-source-sync.sh): a repo holding content that exists
only on source.coop (not a stale version of any NRP file) must NOT be mirror-with-deleted. Set
MODE[repo]="copy" so the job uses rclone copy (additive, never deletes). Currently:
| repo | mode | why |
|---|---|---|
mobi |
copy |
A 27k-tile tiles/** XYZ pyramid, a whole range-size-rarity-all/ layer, the original SpeciesRichness_All/RSR_All source rasters, and LICENSE.txt live only on source.coop (NRP public-mobi has just the reprocessed COG + hex). A sync would wipe them. |
| (all others) | sync |
mirror-with-delete (default) |
Once the one-time backfill above is done, the source-sync CronJob keeps every in-scope
repo current automatically — this is the original goal of #158. ./gen-source-sync.sh emits two
extra files alongside the per-repo jobs:
../k8s/source-sync-cron-config.yaml— asource-sync-scopeConfigMap holdingrepos.txt(one line per repo:<repo> <verb> [exclude globs…]), generated from the sameREPOS/MODE/EXCLUDESarrays. This is the policy/scope — regenerate + re-apply it whenever scope changes.../k8s/source-sync-cron.yaml— the CronJob itself (mechanism, ~static). One pod loopsrepos.txtsequentially at the same gentle 50 MB/s recipe, continue-on-error (one bad repo doesn't block the rest; the Job still exits non-zero so a failure shows up). Schedule: Sundays 08:00 UTC (0 8 * * 0),concurrencyPolicy: Forbid(a still-running weekly backup is never overlapped), opportunistic priority, 3-run history.
Keeping scope in the generated ConfigMap (not baked into the CronJob) is deliberate: the per-repo jobs and the weekly cron read the same source of truth and can't drift.
kubectl apply -f ../k8s/source-sync-cron-config.yaml -f ../k8s/source-sync-cron.yaml # install/update
kubectl -n biodiversity get cronjob source-sync # status
# one-off run now (real sync):
kubectl -n biodiversity create job --from=cronjob/source-sync source-sync-manual
# dry-run a one-off (lists changes, writes nothing): create the job then flip env DRYRUN=true,
# or just run ./run-source-sync.sh <repos…> which honors DRYRUN.
kubectl -n biodiversity logs -f job/source-sync-manualThe per-repo source-sync-<repo>.yaml jobs + run-source-sync.sh remain for manual/backfill
use (a single repo, a fresh repo's first sync, or a targeted re-run); the CronJob is the standing
backup. The same DEST safety guard (refuse any non-cboettig/<repo> path) runs per repo inside the
loop.
As mirrored, the STAC *.json files carry NRP hrefs. rewrite-stac-hrefs.py makes them
self-referential for everything that exists on source.coop:
- Rewrites any href
https://s3-west.nrp-nautilus.io/public-<X>/<path>→https://data.source.coop/cboettig/<X>/<path>iff<X>is a mirrored repo (read fromsource-sync-cron-config.yamlrepos.txt — the same single source of truth). Coversself/child/describedby/ all asset hrefs, including cross-bucket references. - Leaves
root/parentpointing at the NRP canonical root (public-data/stac/catalog.jsonis the global catalog spanning non-mirrored buckets; it is not mirrored). source.coop collections stay navigable up to the canonical root. (Decision in #158; a source.coop root catalog is a possible future enhancement.) - Drops dangling
childlinks to the excluded HOLD sub-paths that were never mirrored (rivers/american-rivers/{campaigns,ira-watersheds,roo-cjest},high-seas/mpa-candidates). - Licenses are unchanged (already correct SPDX on NRP, carried over by the mirror).
Idempotent (a 2nd run is a no-op — an already-source.coop href won't re-match) and
topology-agnostic (does not assume a fixed root path; e.g. rivers has no top-level
stac-collection.json). Writes to source.coop ONLY (refuses any non-cboettig/<repo> path); never
touches NRP. It reads/writes via rclone (cat/rcat --s3-no-check-bucket), so it works
identically on a laptop and in-pod.
./rewrite-stac-hrefs.py --dry-run # report changes, write nothing
./rewrite-stac-hrefs.py # rewrite in place on source.coop
./rewrite-stac-hrefs.py --repos rivers high-seas # limit to some reposWhy it's wired into the weekly CronJob: a plain rclone sync makes source.coop an exact copy of
NRP, so the weekly data mirror would clobber the rewrite back to NRP-pointing hrefs. The
source-sync CronJob therefore runs this script after each sync (fetched from main, scope
from the mounted ConfigMap), re-applying it after each sync. So source.coop self-consistency is
maintained automatically — no manual re-run needed.
Phase 2 makes each mirrored collection self-consistent, but nothing ties them together into a
navigable catalog. gen-root-catalog.py publishes the root: cboettig/glen/{catalog.json, README.md} — the source.coop analog of NRP's public-data/stac/catalog.json.
- Child list = the NRP root's top-level
childlinks, filtered to the mirror scope, hrefs rewritten to source.coop. The STAC catalog is a tree: this root links only the curated top-level collections; nested/cross-collection nodes (e.g.gfwunderhigh-seas, the CAL FIRE leaves underfire-perimeters) are reached by descending. The scope (repos.txt) is the license gate, so restricted collections (wdpa/iucn/hydrobasins/icca/tpl/…) can never leak in — enforced by construction. It reuses phase-2'smirrored_repos()+rewrite_string()(imported fromrewrite-stac-hrefs.py). - Navigation is downward-only by design: mirrored collections keep their
root/parentpointing at canonical NRP (#158). glen is an entry point you descend from. - Fails hard (won't publish) if the NRP root can't be fetched or yields fewer than
--min-children(default 15) in-scope collections — a circuit-breaker against a truncated catalog. - One-time owner step: register
cboettig/glenon source.coop (web UI — the create-repo API is disabled). Writes fail until the repo exists; the account-wide-cred guard already scopes writes tocboettig/.
SCOPE_FILE=/config/repos.txt ./gen-root-catalog.py --dry-run --readme glen-README.md # preview
SCOPE_FILE=/config/repos.txt ./gen-root-catalog.py --readme glen-README.md # publish
./gen-root-catalog.py --dry-run --nrp-root /tmp/cached-root.json # test offline / when Ceph is downAudited all pre-existing cboettig/* (+ berkeley-dse, espm-288) repos: none contain
non-redistributable data (no WDPA/IUCN/ICCA/HydroBASINS). Several are NC (fishbase,
carbon, mobi, mappinginequality, gbif) — keep, but ensure NC labels. Several hold older
versions than NRP (pad-us-3, old ca30x30/cpad/fire) — stale, not a license issue.
Total in current scope ≈ 2.5 TB / ~44 k objects (gbif alone ≈ 1.2 TB).
Anything we can't publish to source.coop still needs an off-NRP backup — that's MinIO
(catalog/sync/k8s/sync-public-*.yaml). Status:
- License-prohibited (source.coop impossible — MinIO is the ONLY off-NRP copy):
wdpa,icca,iucn,hydrobasins— ✅ all have MinIO sync jobs. Keep these. - Already MinIO-backed (uncatalogued/clipped, no source.coop):
wyoming,datacenters,im3,ca30x30(wyoming/ca30x30 restructuring tracked in #225). - NOT backed up yet — and shouldn't be until fixed upstream (don't back up a mess):
landfire— partial import; complete it first (#203), then catalogue + back up.wlfw— migrate intopublic-working-landsfirst (#226), don't back up standalone.working-lands— back up after the wlfw consolidation + proper STAC (#226).
- Redundant-backup candidates (space optimization, not urgent): large public
datasets ALSO mirrored to source.coop (
gbif≈1.2 TB,carbon,wetlands,rap, …) now have an off-NRP copy on source.coop, so their MinIO copy is redundant and could be dropped to save private-system space. Not needed yet ("so far we are fine") — revisit if MinIO fills up.