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