# 09 · Storage round-trip

Proves the control plane can mint working presigned URLs and temporary
credentials against a real bucket through a logical storage handle.

## Exercises

- `lakeshore storage add` — backend registration, and optionally
  `CreateBucket` via `--provision`.
- `lakeshore storage presign` — the control plane's presigned-URL mint
  path, both GET and PUT.
- `lakeshore storage credentials` — the STS `GetSessionToken` path.
- AWS / R2 credential resolution from a Lakeshore secret.

## Requires

- An S3 (or R2 / MinIO) bucket, or permission to create one.
- Credentials for it registered as an `aws_keypair` secret.
- For AWS, the bucket in the region you declare on the storage entry.

> **Warning:** `lakeshore storage` has no `put` / `get` / `ls`. The object surface is
> the presigned URL — use `curl` (or any S3 SDK, with
> `storage credentials`) against it. `add`, `list`, `show`, `update`,
> `remove`, `presign`, and `credentials` are the complete verb set.

## Run

```bash
# 1. Register the credentials, then the storage entry.
#    An aws_keypair secret is either colon-delimited
#    <access_key_id>:<secret_access_key> or JSON {accessKeyId, secretAccessKey}.
printf 'AKIAEXAMPLE:wJalr...' | lakeshore secrets add aws-prod-keys --kind aws_keypair

lakeshore storage add my-bucket \
  --kind s3 \
  --bucket lakeshore-test-bucket \
  --region us-east-1 \
  --creds aws-prod-keys

lakeshore storage show my-bucket

# 2. Upload through a presigned PUT.
echo hi > /tmp/hello.txt
PUT_URL=$(lakeshore storage presign my-bucket hello.txt --put --expires-in 600)
curl -sS -X PUT --upload-file /tmp/hello.txt "$PUT_URL"

# 3. Download through a presigned GET.
curl -sS "$(lakeshore storage presign my-bucket hello.txt)"

# 4. List with temporary credentials.
eval "$(lakeshore storage credentials my-bucket --env)"
aws s3 ls s3://lakeshore-test-bucket/
```

To have the control plane create the bucket for you, add `--provision`
on step 1 — it resolves the `aws_keypair` secret and calls
`CreateBucket`, idempotently if the bucket already exists under that
account.

## Expected output

Step 3 prints `hi`. Step 4 lists `hello.txt`. `storage credentials
--env` emits eval-able `export AWS_...` lines with a default lifetime
of 3600 seconds (range 900–129600).

## If it fails

| Symptom | Likely cause |
| ------- | ------------ |
| `error: unknown command 'put'` / `'get'` / `'ls'` | Those verbs don't exist — see the callout above. |
| `403 Forbidden` from S3 on the presigned PUT | The credential secret is wrong, or the IAM policy lacks `s3:PutObject` / `s3:GetObject`. |
| `301 Moved Permanently` | Region mismatch — the bucket isn't in the region you declared. |
| The storage entry has a surprise config field | `storage add` allows unknown options and folds them into the config. A typo'd flag becomes a config key. Check `storage show`. |
| `storage credentials` errors | The secret isn't an `aws_keypair`, or STS isn't reachable from the control plane. |

## Status

Manual.

## Next

→ [10 · Code mount](/python-sdk/happy-paths/10-code-mount.md)
