| 1 |
# Oxygen Strata: keeping a repository on your own storage |
| 2 |
|
| 3 |
A repository can be pushed to, cloned from and pulled from a bucket in |
| 4 |
your own cloud account, or a directory on a shared drive, with no server |
| 5 |
in between. The bucket is just another remote: |
| 6 |
|
| 7 |
``` |
| 8 |
oxy remote add cloud s3://my-bucket/repos/project --profile work |
| 9 |
oxy remote check cloud |
| 10 |
oxy push cloud |
| 11 |
oxy clone s3://my-bucket/repos/project?profile=work |
| 12 |
oxy pull |
| 13 |
``` |
| 14 |
|
| 15 |
It works because Oxygen's objects never change and are named by their |
| 16 |
hash, so any store that can keep write-once objects and update one small |
| 17 |
record by compare-and-swap can hold a complete repository. Nothing in the |
| 18 |
store is trusted: every object is re-hashed as it is read, changes are |
| 19 |
signed, and a bookmark's attestation is verified under your policy, as |
| 20 |
for any remote. The data stays in your account, in your region, under |
| 21 |
your keys; an operator of a hub need never see it. |
| 22 |
|
| 23 |
Supported stores: |
| 24 |
|
| 25 |
|
| 26 |
|
| 27 |
| `s3://BUCKET/PREFIX` | Amazon S3, and S3-compatible services with `--endpoint` | |
| 28 |
| `gs://BUCKET/PREFIX` | Google Cloud Storage | |
| 29 |
| `az://ACCOUNT/CONTAINER/PREFIX` | Azure Blob Storage | |
| 30 |
| `dir://PATH` | a directory: a USB stick, a network share, or for trying things out | |
| 31 |
|
| 32 |
A store holds one repository per prefix. The first push binds the prefix |
| 33 |
to the repository; a push of another repository to the same prefix is |
| 34 |
refused. Use a prefix per repository, as `repos/project` above. |
| 35 |
|
| 36 |
## Setting up a bucket |
| 37 |
|
| 38 |
### Amazon S3 |
| 39 |
|
| 40 |
1. Create a bucket in the region you want the data kept in (for example |
| 41 |
`eu-west-2`, London), with public access blocked. |
| 42 |
2. Give the identity oxy will use these permissions, and no more: |
| 43 |
|
| 44 |
```json |
| 45 |
{ |
| 46 |
"Version": "2012-10-17", |
| 47 |
"Statement": [ |
| 48 |
{ "Effect": "Allow", "Action": "s3:ListBucket", |
| 49 |
"Resource": "arn:aws:s3:::my-bucket" }, |
| 50 |
{ "Effect": "Allow", |
| 51 |
"Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"], |
| 52 |
"Resource": "arn:aws:s3:::my-bucket/repos/*" } |
| 53 |
] |
| 54 |
} |
| 55 |
``` |
| 56 |
|
| 57 |
A read-only identity (`s3:ListBucket` and `s3:GetObject`) gives a |
| 58 |
working clone and pull. |
| 59 |
3. Credentials come from the places the AWS tools use, in this order: |
| 60 |
`AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` in the environment; the |
| 61 |
profile named by `--profile` (or `AWS_PROFILE`, else `default`) in |
| 62 |
`~/.aws/credentials`; what the AWS CLI can produce for that profile |
| 63 |
(`aws sso login`, `aws login`, assumed roles, credential processes), so |
| 64 |
anything that works for `aws` works for `oxy`; and the instance's or |
| 65 |
container's role. |
| 66 |
4. The region is taken from `--region`, `AWS_REGION`, the profile, or |
| 67 |
asked of S3. `--endpoint http://…` points at an S3-compatible service. |
| 68 |
|
| 69 |
### Google Cloud Storage |
| 70 |
|
| 71 |
1. Create a bucket in the region you want (for example `europe-west2`, |
| 72 |
London), with uniform access and public access prevention. |
| 73 |
2. Create a service account and grant it **Storage Object Admin** on that |
| 74 |
bucket only (**Storage Object Viewer** for a read-only identity), or |
| 75 |
use your own account. |
| 76 |
3. Credentials: a service account key file named by `--credentials` or |
| 77 |
`GOOGLE_APPLICATION_CREDENTIALS`; else the application default |
| 78 |
credentials that `gcloud auth application-default login` writes; else |
| 79 |
the instance's service account. |
| 80 |
|
| 81 |
### Azure Blob Storage |
| 82 |
|
| 83 |
1. Create a storage account in the region you want (for example |
| 84 |
`UK South`), with public blob access off, and a container in it. |
| 85 |
2. Create a service principal (`az ad sp create-for-rbac`) with |
| 86 |
**Storage Blob Data Contributor** scoped to that container (**Storage |
| 87 |
Blob Data Reader** for read-only), or use your own sign-in. |
| 88 |
3. Credentials: `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET` and |
| 89 |
`AZURE_TENANT_ID` for a service principal; `AZURE_STORAGE_ACCOUNT_KEY` |
| 90 |
or `AZURE_STORAGE_SAS_TOKEN`; else what `az login` left behind; else |
| 91 |
the machine's managed identity. |
| 92 |
|
| 93 |
### A directory |
| 94 |
|
| 95 |
`dir:///media/usb/project` needs nothing. Several people on one machine, |
| 96 |
or one person from several checkouts, can share it; a reference is |
| 97 |
updated under a lock file beside it. |
| 98 |
|
| 99 |
## Using a store remote |
| 100 |
|
| 101 |
`oxy remote add NAME URL` stores the URL; `--profile`, `--region`, |
| 102 |
`--endpoint` and `--credentials` are kept on it as query parameters, so a |
| 103 |
clone can name them too: `oxy clone "gs://bucket/project?credentials=key.json"`. |
| 104 |
|
| 105 |
`oxy remote check NAME` reaches the store with the credentials in hand |
| 106 |
and says how long that took, what the prefix holds (this repository, |
| 107 |
another one, or nothing yet), and then puts the store through the checks |
| 108 |
a repository depends on: object round trips, create-only writes, listing, |
| 109 |
deletion and compare-and-swap on a reference. It writes under a probe |
| 110 |
prefix of its own and leaves nothing behind. |
| 111 |
|
| 112 |
Then `push`, `pull`, `fetch` and `clone` work as for any remote. Every |
| 113 |
push uploads one bundle holding what the store lacks, and moves the |
| 114 |
record of bookmarks and tags from the version it read. Two people |
| 115 |
pushing at once are safe: whoever lands second is applied over what |
| 116 |
landed, or told to pull first when the same bookmark moved. |
| 117 |
|
| 118 |
`oxy gc --remote NAME` folds the bundles into one, so a fresh clone |
| 119 |
downloads one file. The superseded bundles are removed only seven days |
| 120 |
later (`--now` removes them at once), so a clone that was listing them at |
| 121 |
the time still completes. |
| 122 |
|
| 123 |
## A repository kept on a store |
| 124 |
|
| 125 |
A store can also hold a repository itself rather than a copy of it. |
| 126 |
Start one with `--store`: |
| 127 |
|
| 128 |
```sh |
| 129 |
oxy init myproject --store s3://my-bucket/repos/myproject?profile=work |
| 130 |
``` |
| 131 |
|
| 132 |
- **Where things live:** the objects and the operation log are in the |
| 133 |
store. `.oxy/cache` keeps the objects this machine has used, up to |
| 134 |
`storage.cache` (2 GiB unless you set it, for example |
| 135 |
`oxy config storage.cache 10G`). |
| 136 |
- **The cache:** you can delete `.oxy/cache` at any time. Objects come |
| 137 |
back from the store as they are needed. |
| 138 |
- **One repository per prefix:** `oxy init --store` refuses a prefix |
| 139 |
that already holds a repository's operation log. |
| 140 |
- **Speed:** each command that records something makes a few requests to |
| 141 |
the store, so a commit takes about half a second to a second. |
| 142 |
- **Choose at init:** set `storage.url` only when the repository is |
| 143 |
created. Changing it later does not move the objects. |
| 144 |
- **Signatures and seals:** each one is stored under a key of its own, |
| 145 |
as in any repository. A bucket can only find those keys with a LIST |
| 146 |
request, so the store also keeps a journal of them (`pack-entries`) |
| 147 |
and lists of them (`pack-<id>.entries`). These are read when the |
| 148 |
repository is opened and again every 30 seconds (spec §13.7). After |
| 149 |
every client that writes to the bucket runs this version of oxy, run |
| 150 |
`oxy gc` once. From then on, looking up a signature or a seal sends no |
| 151 |
LIST request, and another client's new signature is seen within 30 |
| 152 |
seconds. A client still on oxy 0.4.6 or older writes signatures that |
| 153 |
the others miss until the next `oxy gc`, so upgrade every client |
| 154 |
first. |
| 155 |
|
| 156 |
A repository kept on a store holds: |
| 157 |
|
| 158 |
``` |
| 159 |
PREFIX/ |
| 160 |
pack-<id>.pack, .idx, .dict packs, their indexes and dictionaries |
| 161 |
pack-retired what repacks and folds replaced, deleted an hour later |
| 162 |
pack-entries journal: signatures and index entries since the last fold |
| 163 |
pack-<id>.entries every signature and index entry, as the last gc found them |
| 164 |
pack-<id>.entries-part journal lines folded out once there were more than 1000 |
| 165 |
pack-legacy-folded the older one-list keys have been folded |
| 166 |
loose/ nocompress/ objects not yet packed; chunks kept raw |
| 167 |
sig/ seal/ receipt/ approval/ cert/ accepted/ |
| 168 |
one key per signature or index entry |
| 169 |
ops/head the operation head |
| 170 |
``` |
| 171 |
|
| 172 |
## What a store holds |
| 173 |
|
| 174 |
``` |
| 175 |
PREFIX/ |
| 176 |
oxygen.toml format version and repository id, written once |
| 177 |
bundles/00000001-<hash>.bundle one per push: changes, signatures, seals, objects |
| 178 |
bundles/00000002-<hash>.bundle |
| 179 |
refs bookmarks with heads and attestations, tags, policy |
| 180 |
gc bundles a gc superseded, with when |
| 181 |
``` |
| 182 |
|
| 183 |
Each client keeps a mirror of the store under `.oxy/remotes/NAME/strata/`, |
| 184 |
brought level before every use, and runs the same checks on a push that a |
| 185 |
hub runs. The mirror is a cache: delete it and the next use rebuilds it. |
| 186 |
|
| 187 |
## Troubleshooting |
| 188 |
|
| 189 |
Every error says what to do next. The common ones: |
| 190 |
|
| 191 |
- **access denied** — the identity cannot do this on the bucket: check |
| 192 |
its policy covers `ListBucket` on the bucket and `GetObject`, |
| 193 |
`PutObject` and `DeleteObject` under the prefix (or the equivalents on |
| 194 |
GCS and Azure). |
| 195 |
- **no usable credentials** — nothing in the chain above produced any: |
| 196 |
set a profile, sign in with the provider's tool, or set the variables. |
| 197 |
- **profile "x" is not in ~/.aws/config or ~/.aws/credentials** — the |
| 198 |
name on the URL is wrong, or the files live elsewhere |
| 199 |
(`AWS_CONFIG_FILE`, `AWS_SHARED_CREDENTIALS_FILE`). |
| 200 |
- **no region was given and S3 did not say** — add `--region`, set |
| 201 |
`AWS_REGION`, or put `region` in the profile. |
| 202 |
- **holds a different repository** — the prefix is taken: use another. |
| 203 |
- **a push may be in progress, so try again** — a bundle another push |
| 204 |
uploaded is not yet named in the record; a moment later it is. If it |
| 205 |
never is, the store is missing changes: `oxy remote check` says what |
| 206 |
it holds. |
| 207 |
- **the remote kept moving while pushing** — several pushes landed while |
| 208 |
yours was retried; `oxy pull`, then push again. |
| 209 |
|
| 210 |
`oxy remote check NAME` is the first thing to run when something is |
| 211 |
wrong: it separates reachability, permissions and behaviour. |