OXYGENHUB
# c65b0f0a196f oxygenhub / docs / backups.md
8.1 KiB · Markdown · last changed feat: backups for users and organisations, to a directory, with restore (refs #169) 22 days ago renderedsource blame history raw
1
# Backups: per site, organisation, user and repository
2
3
Status: BK2 built 2026-10-13 (oxygenhub#169): user and organisation
4
settings, the hourly and daily schedule with "back up now", directory
5
destinations, every repository's and wiki's store byte for byte, the
6
records as JSON, retention, failure notices and restore of the stores and
7
repository rows (`oxygenhub admin backup run|restore`). Next: the GCS
8
destination (BK3), then restoring issues and proposals from the records. Hourly backups configured by a
9
user and by an organisation come first; finer-grained ones (per
10
repository, more often than hourly) later. The site's own backup
11
(everything that runs a hub, #168) is an operations script today
12
(`~/backups/oxygenhub/backup.sh`); Admin › Backups shows it later.
13
14
## 1. Why
15
16
A forge is where people's work lives. Before Andy moves his GitHub
17
repositories here, and before the hub moves to GCP, every owner must be
18
able to say "my repositories are copied, every hour, to a place I
19
control, and I can bring them back". GitHub leaves this to its users
20
(clone everything somewhere, and lose the issues and reviews); GitLab
21
backs up the whole instance or nothing. Here an owner backs up their own
22
repositories, with everything around them, to their own bucket, without a
23
key file anywhere.
24
25
## 2. What a backup holds
26
27
For each repository the owner has (a user's own, or an organisation's):
28
29
- **Its objects:** the store's content-addressed objects (changes,
30
  blobs, chunks, states, identities, signatures, seals, policies). An
31
  object never changes once written, so a run sends only the objects the
32
  destination does not already have: the first run copies everything,
33
  later ones only what arrived since. Nothing is re-encoded, so a
34
  restored repository is byte for byte the original, every signature
35
  still verifying.
36
- **Its refs at that moment:** bookmarks, tags and the trust policy's
37
  serial, as a small manifest per run. Each run's manifest is kept, so a
38
  restore can go back to any run that is still kept.
39
- **Its hub records, as JSON:** issues and their comments and events;
40
  proposals, their reviews, threads and approvals; labels, milestones,
41
  projects; releases and their assets (assets are blobs, sent like
42
  objects); the wiki (a repository of its own, backed up with it); and
43
  repository settings and webhooks. Secret values (webhook secrets, Flow
44
  secrets, tokens) are never written, only their names, so a restored
45
  repository asks for them again.
46
- **Its owner:** for an organisation, its members, teams and their
47
  permissions; for a user, their profile and linked identity keys
48
  (public keys only).
49
50
Not included: Flow runs' logs and artifacts, which belong to the run, not
51
the repository (a later choice); caches; anything of another owner.
52
53
## 3. Where it goes
54
55
A **destination** is one of:
56
57
- **A GCS bucket, keylessly.** The hub is already an OIDC issuer (spec
58
  §13.5). For a backup it mints a short-lived token with
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
142
Then, in Settings › Backups: destination GCS, bucket
143
`andymccall-oxygen-backup`, provider
144
`//iam.googleapis.com/projects/NUM/locations/global/workloadIdentityPools/oxygenhub/providers/oxygenhub-io`,
145
and TEST, which exchanges a token and writes and deletes a probe object.
146
An organisation does the same with `backup:ORG`. Only that principal can
147
write the bucket; the hub never holds a key.
148
149
## 7. Restore
150
151
`oxygenhub admin restore --from gs://BUCKET/PREFIX [--run TIME] [--repo NAME]`
152
(or a directory) on any hub:
153
154
1. reads the run's manifests (the latest whole run by default);
155
2. creates each repository, or refuses an existing one unless `--into`
156
   names a new owner;
157
3. copies its objects, verifying every hash, and sets its refs;
158
4. imports its records, then reports what needs re-entering (secret
159
   values, webhook secrets).
160
161
A restore into a fresh hub, compared with the original, is the end-to-end
162
test (BK2), as is restoring one repository from a run before a bad push.
163
164
## 8. Order of work
165
166
1. **BK2:** settings for users and organisations, the scheduler and
167
   "back up now", objects, manifests and records to a directory
168
   destination, status and notifications, and restore with its
169
   end-to-end test.
170
2. **BK3:** the GCS destination with the keyless exchange (tested against
171
   a fake STS and a fake GCS), and TEST.
172
3. Admin › Backups, then per-repository settings, then S3.