Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Phase G (ops) — **cloud storage backends** (first G cycle):
- New `internal/storage` leaf defining a backend-neutral `Store` interface (`Put`/`Get`/`Delete`/`List`/`Stat`, all context-aware) with two implementations: **local** (the previous filesystem layout, now behind the interface — temp-write+rename for atomic publish, keys map verbatim to file names so existing catalogs need no migration) and **s3** (S3 and S3-compatible services like MinIO/R2 via AWS SDK v2; streaming transfer-manager upload that is atomic on completion, path-style for custom endpoints).
- `internal/dumps.Catalog` refactored to hold a `Store` and address dumps by key (`<id>.dump`, `<id>.meta.json`) instead of filesystem paths; `Path`/`MetaPath`/`Root` removed. Catalog methods (`PutDump`/`OpenDump`/`ReadMeta`/`WriteMeta`/`List`/`Delete`/`ResolveChain`/`PruneDryRun`) are now context-aware. `backup`, `restore`, `verify`, and `dumps` stream through the store; SHA-256 integrity is computed app-side over `envelope ++ body`, so a dump verifies identically whether it lives locally or in S3.
- New `storage:` config block (`type: local|s3`, `bucket`, `prefix`, `region`, `endpoint`) with fail-fast validation; S3 credentials resolve from the standard AWS chain (never stored in config). See [docs/STORAGE.md](docs/STORAGE.md).
- A shared `RunStoreSuite` contract test (mirroring Phase D's `RunDriverSuite`) runs against every backend: the local backend runs it as a unit test, and the S3 backend runs it against MinIO via testcontainers under the `integration` tag, so the live object-store path executes in CI. GCS/Azure backends are a fast-follow.

- Phase A skeleton: Go module, Cobra CLI with placeholder subcommands, Bubble Tea TUI placeholder, Driver interface + registry, errs package, config stub, Makefile, golangci-lint with depguard, CI workflow.
- Phase B Postgres walking skeleton: `backup`, `restore`, `sync`, `verify`, `inspect`, `dumps`, and `profile` working end-to-end against PostgreSQL (shelling out to `pg_dump`/`pg_restore`, pgx for inspect), SHA-256 dump checksums with sidecar metadata, named profiles with `env:` secret refs, and POSIX exit-code taxonomy.
- Phase C interactive TUI dashboard: multi-panel Bubble Tea UI (profiles · dumps · jobs) with live job-progress subscription, backup/restore modal forms, an error overlay, and golden snapshot tests.
Expand Down
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ A single binary that turns the painful, error-prone sprawl of `pg_dump` → `pg_
---

> [!WARNING]
> **Pre-1.0 — active development.** Postgres, MySQL, and MariaDB backup/restore/sync/verify/inspect work end-to-end today (Phases B + E), and bare `siphon` opens an interactive multi-panel dashboard (Phase C). The driver layer is hardened with a shared cross-driver test harness, capability gating, and connection retry (Phase D). Advanced transfer is complete (Phase F): incremental backup/restore, cross-engine sync, and CDC continuous replication all work end-to-end. Ops features (cloud storage, secret backends, retention, telemetry) are on the [roadmap](#roadmap). APIs, flags, and the on-disk dump format may change before 1.0. Track progress via the milestone tags (`phase-a` … `phase-f`).
> **Pre-1.0 — active development.** Postgres, MySQL, and MariaDB backup/restore/sync/verify/inspect work end-to-end today (Phases B + E), and bare `siphon` opens an interactive multi-panel dashboard (Phase C). The driver layer is hardened with a shared cross-driver test harness, capability gating, and connection retry (Phase D). Advanced transfer is complete (Phase F): incremental backup/restore, cross-engine sync, and CDC continuous replication all work end-to-end. Ops features (Phase G) are underway — the dump catalog can now live in an S3 / S3-compatible bucket (`storage:` config block); secret backends, retention, and telemetry are still on the [roadmap](#roadmap). APIs, flags, and the on-disk dump format may change before 1.0. Track progress via the milestone tags (`phase-a` … `phase-f`).

## Table of contents

Expand Down Expand Up @@ -55,7 +55,7 @@ A single binary that turns the painful, error-prone sprawl of `pg_dump` → `pg_
| **D** — Driver hardening | Shared cross-driver test harness (`RunDriverSuite`), capability-gating helper (`RequireCapability`), Postgres connection-probe retry, and a `docs/DRIVERS.md` contributor guide | ✅ Complete |
| **E** — MySQL + MariaDB | Both drivers via a shared `_mysqlcommon` package (shared `Conn`, `mysqldump`/`mariadb-dump` backup, client-pipe restore), exercised by the Phase D `RunDriverSuite` harness | ✅ Complete |
| **F** — Advanced transfer | All four advanced-transfer modes work end-to-end: bounded-buffer streaming sync; **incremental** backup/restore (`backup --incremental --base <id>` captures a bounded change set via Postgres logical decoding / MySQL-MariaDB binlog, `restore` replays the base→incremental chain, Postgres orphan-slot sweep); **cross-engine** sync (`sync --cross-engine` — typed `SchemaInspector` introspection → canonical type-mapping, e.g. Postgres → MySQL); and **CDC** (`siphon cdc` / `sync --continuous` — unbounded change streaming with snapshot→stream handoff, resumable, same- and cross-engine). Live DB paths are integration-tested in CI — see [docs/INCREMENTAL.md](docs/INCREMENTAL.md), [docs/CROSS_ENGINE.md](docs/CROSS_ENGINE.md), [docs/CDC.md](docs/CDC.md) | ✅ Complete |
| **G** — Ops features | Cloud storage, secret backends, profile groups + 2FA, team mode, audit log, retention, telemetry | ⏳ Planned |
| **G** — Ops features | **Cloud storage** ships first: the dump catalog can live in an S3 / S3-compatible bucket via a pluggable `storage.Store` backend (local + S3 today; GCS/Azure are a fast-follow), selected by a `storage:` config block, with SHA-256 integrity preserved end-to-end — see [docs/STORAGE.md](docs/STORAGE.md). Secret backends, profile groups + 2FA, team mode, audit log, retention, and telemetry follow in later G cycles. | 🟡 In progress |
| **H** — Distribution | GoReleaser, Homebrew tap, Scoop bucket, install script, docs site | ⏳ Planned |

## Requirements
Expand Down Expand Up @@ -168,6 +168,19 @@ profiles:

Secret references (`env:VAR`) are resolved at runtime, so the config file is safe to commit. Plain values are also accepted.

By default the dump catalog lives on local disk at `defaults.dump_dir`. To store dumps in an S3 / S3-compatible bucket instead, add a `storage:` block:

```yaml
storage:
type: s3 # "local" (default) | "s3"
bucket: my-siphon-dumps # required for s3
prefix: prod # optional key prefix
region: us-east-1
endpoint: "" # optional: custom endpoint for MinIO / R2
```

Credentials are resolved from the standard AWS chain (env vars, `~/.aws`, instance role) — never stored in the config file. See [docs/STORAGE.md](docs/STORAGE.md) for details.

## Architecture

siphon is a strictly layered Go application; imports flow **downward only**, enforced at lint time by `golangci-lint`'s `depguard` (an upward import fails CI):
Expand Down
100 changes: 100 additions & 0 deletions docs/STORAGE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Storage backends

By default siphon keeps the dump catalog — every dump body plus its sidecar
metadata — in a local directory. Phase G adds a pluggable storage layer so the
catalog can instead live in an S3 (or S3-compatible) bucket, with `backup`,
`restore`, `verify`, and `dumps` all reading and writing through it transparently.

## Table of contents

- [How it works](#how-it-works)
- [Configuration](#configuration)
- [Integrity across backends](#integrity-across-backends)
- [Scope and limitations](#scope-and-limitations)

## How it works

The dump catalog (`internal/dumps`) holds a `storage.Store` and addresses
objects by opaque keys — `<id>.dump` for the envelope-prefixed dump body and
`<id>.meta.json` for the sidecar — never by filesystem path. The `Store`
interface is small and backend-neutral:

```go
type Store interface {
Put(ctx, key, io.Reader) error // durable, atomic-on-complete
Get(ctx, key) (io.ReadCloser, error) // one-shot forward stream
Delete(ctx, key) error // idempotent
List(ctx) ([]string, error)
Stat(ctx, key) (size int64, exists bool, err error)
}
```

Two backends ship today (`internal/storage`):

- **local** — a single directory. `Put` writes to a temp file and renames, so a
failed or cancelled write never leaves a partial dump under its final key.
Keys map verbatim to file names, so a pre-Phase-G local catalog keeps working
with no migration.
- **s3** — an S3 or S3-compatible bucket (AWS, MinIO, Cloudflare R2). `Put`
streams through the SDK's transfer manager, so the object only becomes
addressable once the upload completes — the same atomic-on-complete guarantee
the local backend gets from rename.

A backup stages the dump body to a local temp file (the dump tool needs a real
fd), then streams `envelope ++ body` into the store in a single `Put`, teeing
through SHA-256 as the bytes flow. Restore and verify open the dump with `Get`
and stream it straight into the envelope reader — no full local download.

## Configuration

Storage is selected by a top-level `storage:` block in the config file. Omitting
it (or `type: local`) uses the local filesystem at `defaults.dump_dir`.

```yaml
version: 1
defaults:
dump_dir: ~/.local/share/siphon/dumps # used by the local backend

storage:
type: s3 # "local" (default) | "s3"
bucket: my-siphon-dumps # required for s3
prefix: prod # optional key prefix within the bucket
region: us-east-1
endpoint: "" # optional: custom endpoint for S3-compatible services
```

For an S3-compatible service such as MinIO or R2, set `endpoint` to its URL
(path-style addressing is used automatically).

**Credentials are never stored in the config file.** The S3 backend resolves
them from the standard AWS chain — `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`
environment variables, the shared `~/.aws/config`, or an instance/role profile —
so the config stays safe to commit.

## Integrity across backends

The SHA-256 checksum is computed over the `envelope ++ body` stream at write
time and recomputed over the `Get` stream at verify time — both in siphon, not
in the backend. A dump written to S3 and a dump written locally therefore verify
identically, and `siphon verify` catches corruption regardless of where the dump
lives. siphon does not trust the backend's own ETag/MD5 (multipart uploads do
not expose a plain object MD5).

The live S3 path is integration-tested in CI against MinIO via testcontainers
(`internal/storage/s3_integration_test.go`) using the same `RunStoreSuite`
contract that the local backend runs, so the streaming upload, ranged read,
listing, and not-found mapping all execute against a real object store — not
just compile.

## Scope and limitations

- Backends covered: **local** and **s3** (incl. S3-compatible). **GCS and Azure
Blob are not implemented yet** — they are a fast-follow, and will get the full
correctness bar for free by running the same `RunStoreSuite` contract.
- `dumps list` over S3 issues one `ListObjectsV2` plus one read per metadata
object (N+1). This is fine at expected catalog sizes; no pagination/parallel
optimization is done yet.
- Each `Get` is a fresh one-shot forward stream — callers must not assume the
returned reader is seekable.
- Retention/lifecycle (chain-aware pruning over remote storage) remains a
separate Phase G concern; the `Store.Delete` it needs is delivered here.
20 changes: 20 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ module github.com/nixrajput/siphon
go 1.26.3

require (
github.com/aws/aws-sdk-go-v2 v1.42.0
github.com/aws/aws-sdk-go-v2/config v1.32.25
github.com/aws/aws-sdk-go-v2/credentials v1.19.24
github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.2.11
github.com/aws/aws-sdk-go-v2/service/s3 v1.104.0
github.com/aws/smithy-go v1.27.1
github.com/charmbracelet/bubbles v1.0.0
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/huh v1.0.0
Expand All @@ -16,6 +22,7 @@ require (
github.com/spf13/cobra v1.10.2
github.com/testcontainers/testcontainers-go v0.42.0
github.com/testcontainers/testcontainers-go/modules/mariadb v0.42.0
github.com/testcontainers/testcontainers-go/modules/minio v0.42.0
github.com/testcontainers/testcontainers-go/modules/mysql v0.42.0
github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0
gopkg.in/yaml.v3 v3.0.1
Expand All @@ -27,6 +34,19 @@ require (
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.22 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.29 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.2.0 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.31.3 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.43.3 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymanbagabas/go-udiff v0.3.1 // indirect
github.com/catppuccin/go v0.3.0 // indirect
Expand Down
Loading
Loading