Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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