Skip to content

feat(storage)!: per-keypair Tigris buckets, one per repo#5

Open
Xe wants to merge 4 commits into
mainfrom
Xe/per-repo-bucket
Open

feat(storage)!: per-keypair Tigris buckets, one per repo#5
Xe wants to merge 4 commits into
mainfrom
Xe/per-repo-bucket

Conversation

@Xe

@Xe Xe commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Summary

Repositories were served from one shared bucket chrooted by a raw, variable-depth path. This PR replaces that with a per-repo storage model:

  • Pluggable resolver seam (internal/repofs): every git request resolves to a billy.Filesystem via Resolver.Resolve(ctx, ref, cred, create), keyed on a validated {orgID}/{repoName} path (.git stripped). BucketResolver (chroot one bucket) stays for tests.
  • Production resolver (internal/tigrisfs): treats the HTTP Basic-auth pair as a Tigris keypair, builds and caches one storage.Client per keypair, and gives each repository its own bucket (objgit-<base36 sha256 of orgID/name>), created on the push path only.
  • Smart-HTTP now uses ServeMux wildcards (paths are fixed-depth), threads the credential into resolution, and logs every authorization decision (auth.Operation/Decision gained String()); s3fs.S3Client is exported so the resolver can cache the hardened client.

Fixes found bringing it up against real Tigris

  • fix(s3fs) directory markers: MkdirAll wrote a zero-byte marker at the exact key (refs/heads), which lists as a file; go-git's reference walker read it as a ref and failed the receive-pack advertisement with ref file is empty. Markers now end in a trailing slash (refs/heads/) so they list as directories. This was the actual push-blocking bug.
  • fix(s3fs) checksums: opt out of aws-sdk-go-v2's default RequestChecksumCalculation=when_supported (aws-chunked + trailing CRC32) in Harden, which some S3-compatible endpoints mishandle. Independent safeguard.

Breaking changes

  • Repository URLs must be {orgID}/{repoName}.
  • Repositories moved from one shared bucket to a bucket per repo; repos created under the old layout no longer resolve. -bucket is now system-only (SSH host key).

Testing

  • go build ./..., go vet ./..., go test ./... all green.
  • New unit/regression tests: repofs.Parse, tigrisfs (bucket naming, create-gating, credential→401), and s3fs init-at-bucket-root advertising refs + MkdirAll directory markers.
  • Verified end-to-end against real Tigris: git push to a fresh {org}/{repo}.git creates the bucket and a git clone round-trips the content.

Assisted-by: Claude Opus 4.8 via Claude Code

Xe added 4 commits June 26, 2026 13:26
Repositories were served from one shared bucket chrooted by a raw, variable-depth path. Every git request now resolves through a pluggable repofs.Resolver hook keyed on a validated {orgID}/{repoName} path (the .git suffix is stripped).

The production resolver (internal/tigrisfs) treats the HTTP Basic-auth pair as a Tigris keypair, builds and caches one storage.Client per keypair, and gives each repository its own bucket (objgit-<base36 sha256 of orgID/name>), created on the push path only.

Smart-HTTP now uses ServeMux wildcards (paths are fixed-depth), threads the credential into resolution, and logs every authorization decision; auth.Operation/Decision gain String() and s3fs.S3Client is exported so the resolver can cache the hardened client.

BREAKING CHANGE: repository URLs must be {orgID}/{repoName}; repositories moved from one shared bucket to a bucket per repo, so repositories created under the old layout no longer resolve.

Assisted-by: Claude Opus 4.8 via Claude Code
Signed-off-by: Xe Iaso <xe@tigrisdata.com>
aws-sdk-go-v2 (s3 >= v1.73) defaults RequestChecksumCalculation to "when supported", sending PutObject bodies with a trailing CRC32 via aws-chunked content encoding. Some S3-compatible endpoints mishandle that framing and store an empty or corrupt object. Force the legacy "when required" behavior in Harden so bodies are sent plain.

Assisted-by: Claude Opus 4.8 via Claude Code
Signed-off-by: Xe Iaso <xe@tigrisdata.com>
MkdirAll wrote a zero-byte marker at the exact key (e.g. "refs/heads"), which lists as a regular file. go-git's reference walker then read it as a ref and failed the receive-pack advertisement with "ref file is empty" — so a first push to a fresh per-repo bucket never completed.

Write the marker with a trailing separator ("refs/heads/") so it lists as a CommonPrefix (directory); listChildren already skips the trailing-slash self-marker. Also key the marker through fs3.key and short-circuit the bucket root.

Adds regression coverage that git.Init at the bucket root advertises references cleanly, and that MkdirAll yields a directory entry.

Assisted-by: Claude Opus 4.8 via Claude Code
Signed-off-by: Xe Iaso <xe@tigrisdata.com>
Captures the design for per-repo {orgID}/{repoName} filesystem dispatch and the per-keypair bucket-per-repo Tigris resolver.

Assisted-by: Claude Opus 4.8 via Claude Code
Signed-off-by: Xe Iaso <xe@tigrisdata.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant