OXYGENHUB
# cd437c4f5f8d oxygenhub / docs / backups.md
8.3 KiB · Markdown · last changed feat: keyless GCS backups, and TEST (closes #169) 22 days ago renderedsource blame history raw
6dbd999a Andy McCall ↶ 1
# Backups: per site, organisation, user and repository
2
35fea423 Andy McCall ↶ 3
Status: built 2026-10-13/14 (oxygenhub#169): user and organisation
4
settings, the hourly and daily schedule with "back up now" and TEST,
5
directory destinations and keyless GCS destinations, every repository's
6
and wiki's store byte for byte, the records as JSON, retention, failure
7
notices, and restore of the stores and repository rows
8
(`oxygenhub admin backup run|restore`). Next: restoring issues and
9
proposals from the records, Admin › Backups, per-repository settings.
6dbd999a Andy McCall ↶ 10
11
## 1. Why
12
13
A forge is where people's work lives. Before Andy moves his GitHub
14
repositories here, and before the hub moves to GCP, every owner must be
15
able to say "my repositories are copied, every hour, to a place I
16
control, and I can bring them back". GitHub leaves this to its users
17
(clone everything somewhere, and lose the issues and reviews); GitLab
18
backs up the whole instance or nothing. Here an owner backs up their own
19
repositories, with everything around them, to their own bucket, without a
20
key file anywhere.
21
22
## 2. What a backup holds
23
24
For each repository the owner has (a user's own, or an organisation's):
25
26
- **Its objects:** the store's content-addressed objects (changes,
27
  blobs, chunks, states, identities, signatures, seals, policies). An
28
  object never changes once written, so a run sends only the objects the
29
  destination does not already have: the first run copies everything,
30
  later ones only what arrived since. Nothing is re-encoded, so a
31
  restored repository is byte for byte the original, every signature
32
  still verifying.
33
- **Its refs at that moment:** bookmarks, tags and the trust policy's
34
  serial, as a small manifest per run. Each run's manifest is kept, so a
35
  restore can go back to any run that is still kept.
36
- **Its hub records, as JSON:** issues and their comments and events;
37
  proposals, their reviews, threads and approvals; labels, milestones,
38
  projects; releases and their assets (assets are blobs, sent like
39
  objects); the wiki (a repository of its own, backed up with it); and
40
  repository settings and webhooks. Secret values (webhook secrets, Flow
41
  secrets, tokens) are never written, only their names, so a restored
42
  repository asks for them again.
43
- **Its owner:** for an organisation, its members, teams and their
44
  permissions; for a user, their profile and linked identity keys
45
  (public keys only).
46
47
Not included: Flow runs' logs and artifacts, which belong to the run, not
48
the repository (a later choice); caches; anything of another owner.
49
50
## 3. Where it goes
51
52
A **destination** is one of:
53
54
- **A GCS bucket, keylessly.** The hub is already an OIDC issuer (spec
35fea423 Andy McCall ↶ 55
  §13.5). Built: the token lives 10 minutes, its audience is the
56
  provider's name over https (Google's default), the exchange asks for
57
  `devstorage.read_write`, and the access token is exchanged again five
58
  minutes before it runs out. A refusal says what to grant. For a backup it mints a short-lived token with
6dbd999a Andy McCall ↶ 59
  `sub = backup:OWNER` and audience the owner's workload identity
60
  provider. Google's STS exchanges it for a short-lived access token,
61
  which writes to the bucket. Nothing secret is stored: no service
62
  account key exists (Andy's organisation blocks them anyway), and if the
63
  hub leaks a token it lasts minutes and is scoped to that owner's
64
  prefix. Setup is §6.
65
- **A directory on the hub's host**, for the site administrator: the
66
  directories an owner may choose are listed in `hub.toml`
67
  (`[backups] directories = ["/srv/backups"]`), so nobody can write
68
  anywhere else on the host. Each owner gets `DIR/OWNER/`.
69
- **Later:** S3 (the same keyless exchange with AWS STS), and another hub.
70
71
Layout at a destination (`PREFIX` = the owner's chosen prefix):
72
73
```
74
PREFIX/objects/<repository id>/<object hash>    content-addressed, written once
75
PREFIX/runs/<run time>/<repository id>.json     the refs at that run
76
PREFIX/runs/<run time>/records/<repository id>.json
77
PREFIX/runs/<run time>/owner.json
78
PREFIX/runs/<run time>/DONE                     written last: the run is whole
79
```
80
81
A run without `DONE` was interrupted; a restore ignores it.
82
83
## 4. When, how long and how it went
84
85
- **How often:** hourly to begin with (off, hourly, daily); finer
86
  later. The hub's scheduler runs each due backup, at most two at once,
87
  at low priority, spread across the hour so owners' runs do not
88
  collide.
89
- **Back up now:** a button, and `POST /api/v1/{user|orgs/NAME}/backups/run`.
90
- **Kept:** runs' manifests for 30 days by default (7, 30 or 90).
91
  Objects no kept run refers to are pruned weekly. With GCS, the
92
  bucket's own versioning and lifecycle rules work too.
93
- **Status:** the last run's start, length, objects and bytes sent,
94
  repositories, and outcome (ok, partial with the repositories that
95
  failed and why, or failed with the reason, such as "GCS refused the
96
  token: check the provider's attribute condition"). The last 50 runs
97
  are listed. A failure notifies the owner (an organisation's owners)
98
  in the inbox, once per kind of failure until it succeeds again.
99
100
## 5. Who sets it, and where
101
102
- **A user:** Settings › Backups, for the repositories they own.
103
- **An organisation's owners:** the organisation's Settings › Backups.
104
- **The site administrator:** Admin › Backups, a table of every owner's
105
  backup (destination, schedule, last run, outcome) and the site's own
106
  backup. They may not read an owner's bucket, only see that it ran.
107
- **A repository** (later): opt a repository out of its owner's backup,
108
  or give it its own schedule.
109
110
The API mirrors the pages: `GET/PUT /api/v1/user/backups`,
111
`GET/PUT /api/v1/orgs/{name}/backups`, the run endpoint above, and
112
`GET …/backups/runs`. `oh backup status|run|restore` follows.
113
114
## 6. Setting up a GCS destination (for Andy, by hand)
115
116
Once per GCP project:
117
118
```
119
PROJECT=doomsdayone-oxy
120
NUM=$(gcloud projects describe $PROJECT --format='value(projectNumber)')
121
gcloud iam workload-identity-pools create oxygenhub \
122
  --project=$PROJECT --location=global --display-name="OxygenHub"
123
gcloud iam workload-identity-pools providers create-oidc oxygenhub-io \
124
  --project=$PROJECT --location=global --workload-identity-pool=oxygenhub \
125
  --issuer-uri=https://oxygenhub.io \
126
  --attribute-mapping=google.subject=assertion.sub \
127
  --attribute-condition="assertion.sub.startsWith('backup:')"
128
```
129
130
Once per owner and bucket (here `andymccall`):
131
132
```
133
gcloud storage buckets create gs://andymccall-oxygen-backup \
134
  --project=$PROJECT --location=europe-west2 \
135
  --uniform-bucket-level-access --public-access-prevention
136
gcloud storage buckets update gs://andymccall-oxygen-backup --versioning
137
gcloud storage buckets add-iam-policy-binding gs://andymccall-oxygen-backup \
138
  --role=roles/storage.objectAdmin \
139
  --member="principal://iam.googleapis.com/projects/$NUM/locations/global/workloadIdentityPools/oxygenhub/subject/backup:andymccall"
140
```
141
35fea423 Andy McCall ↶ 142
**Done for doomsdayone-oxy on 2026-10-13:** the pool `oxygenhub`, the
143
provider `oxygenhub-io` and the buckets `andymccall-oxygen-backup` and
144
`doomsdayone-oxygen-backup`, each granted to its own `backup:` subject.
145
6dbd999a Andy McCall ↶ 146
Then, in Settings › Backups: destination GCS, bucket
147
`andymccall-oxygen-backup`, provider
148
`//iam.googleapis.com/projects/NUM/locations/global/workloadIdentityPools/oxygenhub/providers/oxygenhub-io`,
149
and TEST, which exchanges a token and writes and deletes a probe object.
150
An organisation does the same with `backup:ORG`. Only that principal can
151
write the bucket; the hub never holds a key.
152
153
## 7. Restore
154
155
`oxygenhub admin restore --from gs://BUCKET/PREFIX [--run TIME] [--repo NAME]`
156
(or a directory) on any hub:
157
158
1. reads the run's manifests (the latest whole run by default);
159
2. creates each repository, or refuses an existing one unless `--into`
160
   names a new owner;
161
3. copies its objects, verifying every hash, and sets its refs;
162
4. imports its records, then reports what needs re-entering (secret
163
   values, webhook secrets).
164
165
A restore into a fresh hub, compared with the original, is the end-to-end
166
test (BK2), as is restoring one repository from a run before a bad push.
167
168
## 8. Order of work
169
170
1. **BK2:** settings for users and organisations, the scheduler and
171
   "back up now", objects, manifests and records to a directory
172
   destination, status and notifications, and restore with its
173
   end-to-end test.
174
2. **BK3:** the GCS destination with the keyless exchange (tested against
175
   a fake STS and a fake GCS), and TEST.
176
3. Admin › Backups, then per-repository settings, then S3.