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
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bin/*.wasm filter=lfs diff=lfs merge=lfs -text
3 changes: 3 additions & 0 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ jobs:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
# The .wasm files in bin/ are Git LFS objects. Without them, the
# tests and the image get the pointer files.
lfs: true

- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1

Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ jobs:
with:
persist-credentials: false
fetch-tags: true
# The .wasm files in bin/ are Git LFS objects. Without them, the
# tests and the image get the pointer files.
lfs: true

- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
Expand Down
57 changes: 33 additions & 24 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,34 +49,43 @@ Notes on the tests and on configuration:
flag to UPPER_SNAKE. `-allow-push` becomes `ALLOW_PUSH`, and `-bucket`
becomes `BUCKET`.
- `godotenv` loads a `.env` file from the working directory at startup.
- The `.wasm` files in `bin/` are Git LFS objects. Run `git lfs pull` before
you build. Without them, `TestRepoBin` fails. The image copies `bin/` to
`/usr/libexec/objgit/bin`. For a local run with hooks, set
`WASM_PATH=./bin`.
- `TestCluster` in `internal/kube` needs a real cluster. It skips itself
without the `OBJGIT_TEST_KUBE_*` variables.
- Tigris client credentials come from the standard AWS SDK chain, such as
`AWS_PROFILE`.

## Where the code lives

| Path | Purpose |
| --------------------------------------------------------- | --------------------------------------------------------------------------------- |
| `cmd/objgitd/main.go` | Builds the one `*daemon` and starts every listener. |
| `cmd/objgitd/git_protocol.go` | The git:// server. Also holds `operationFor` and `(*daemon).authorize`. |
| `cmd/objgitd/http.go` | Smart HTTP. `*daemon` is the `http.Handler` itself. |
| `cmd/objgitd/ssh.go` | The SSH server and its per-session dispatch. |
| `cmd/objgitd/shell.go` | The SSH `sh` command: an interactive shell in the hook sandbox. |
| `cmd/objgitd/receivepack.go` | The go-git fork that streams hook output, plus `writePack`. |
| `cmd/objgitd/hooks.go` | Ref diffing and the sandboxed hook run. |
| `cmd/objgitd/snapshots.go` | The erofs snapshot run after a push. |
| `cmd/objgitd/lfs.go` | The Git LFS HTTP handlers and `git-lfs-authenticate` over SSH. |
| `internal/auth` | The one authorization interface. |
| `internal/lfs` | Git LFS: protocol types, the bucket store, and the presigner. |
| `internal/repofs` | Maps a repository path to a `storage.Storer`. |
| `internal/storage/tigris` | Repository storage. A `storage.Storer` on the bucket. |
| `internal/bundler` | The async upload queue behind that storer. |
| `internal/s3fs` | Daemon-level state only, which is the SSH host key. |
| `internal/mountfs`, `internal/treefs`, `internal/kefkash` | The hook sandbox filesystem and shell wiring. |
| `internal/metrics` | Every Prometheus vector, plus thin helpers. |
| `internal/gittest` | `Isolate`, which keeps the real git client in tests away from this repository. |
| `internal/snapshot` | erofs images of git trees: `Ensure`, `Open`, and the `Store` interface. |
| `internal/slog.go` | JSON handler init. |
| `cmd/membench/` | Push memory benchmark harness. Not shipped; see `docs/usage/memory-benchmark.md`. |
| Path | Purpose |
| --------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `cmd/objgitd/main.go` | Builds the one `*daemon` and starts every listener. |
| `cmd/objgitd/git_protocol.go` | The git:// server. Also holds `operationFor` and `(*daemon).authorize`. |
| `cmd/objgitd/http.go` | Smart HTTP. `*daemon` is the `http.Handler` itself. |
| `cmd/objgitd/ssh.go` | The SSH server and its per-session dispatch. |
| `cmd/objgitd/shell.go` | The SSH `sh` command: an interactive shell in the hook sandbox. |
| `cmd/objgitd/receivepack.go` | The go-git fork that streams hook output, plus `writePack`. |
| `cmd/objgitd/hooks.go` | Ref diffing and the sandboxed hook run. |
| `cmd/objgitd/snapshots.go` | The erofs snapshot run after a push. |
| `cmd/objgitd/lfs.go` | The Git LFS HTTP handlers and `git-lfs-authenticate` over SSH. |
| `internal/auth` | The one authorization interface. |
| `internal/kube` | The in-cluster API client, `kube:apply`, and `tekton:pipelinerun`. |
| `internal/wasmbin` | Runs the WASI programs in `-wasm-path`, such as kustomize, as hook commands. |
| `internal/lfs` | Git LFS: protocol types, the bucket store, and the presigner. |
| `internal/repofs` | Maps a repository path to a `storage.Storer`. |
| `internal/storage/tigris` | Repository storage. A `storage.Storer` on the bucket. |
| `internal/bundler` | The async upload queue behind that storer. |
| `internal/s3fs` | Daemon-level state only, which is the SSH host key. |
| `internal/mountfs`, `internal/treefs`, `internal/kefkash` | The hook sandbox filesystem and shell wiring. |
| `internal/metrics` | Every Prometheus vector, plus thin helpers. |
| `internal/gittest` | `Isolate`, which keeps the real git client in tests away from this repository. |
| `internal/snapshot` | erofs images of git trees: `Ensure`, `Open`, and the `Store` interface. |
| `internal/slog.go` | JSON handler init. |
| `bin/` | WASI programs for hooks (Git LFS). The image copies them to `/usr/libexec/objgit/bin`. |
| `cmd/membench/` | Push memory benchmark harness. Not shipped; see `docs/usage/memory-benchmark.md`. |

## Architecture

Expand All @@ -87,7 +96,7 @@ describes the daemon and links to one page for each subsystem.
| ------------------------------------------------------ | ----------------------------------------------------------------------- |
| [transports.md](docs/architecture/transports.md) | Any transport. It holds two protocol points that are easy to get wrong. |
| [auth.md](docs/architecture/auth.md) | Credentials, decisions, or a new `Authorizer`. |
| [hooks.md](docs/architecture/hooks.md) | Push hooks, output streaming, or the sandbox. |
| [hooks.md](docs/architecture/hooks.md) | Push hooks, output streaming, the sandbox, or its Kubernetes commands. |
| [metrics.md](docs/architecture/metrics.md) | Any metric or instrumentation seam. |
| [snapshots.md](docs/architecture/snapshots.md) | Snapshot images, the snapshot cache, or `runSnapshots`. |
| [tigris-storer.md](docs/architecture/tigris-storer.md) | Object layout, refs, packs, the pack cache, or the upload path. |
Expand Down
12 changes: 12 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ RUN --mount=type=cache,target=/go/pkg/mod \

COPY . .

# The .wasm files in bin/ are Git LFS objects. A checkout without LFS has
# pointer files there, and the image would ship them. Stop here when a file
# does not start with the WebAssembly magic.
RUN for f in bin/*.wasm; do \
head -c 4 "$f" | od -An -tx1 | grep -q '00 61 73 6d' || \
{ echo "$f is not WebAssembly; run git lfs pull" >&2; exit 1; }; \
done

# Static, stripped binary. No cgo: objgitd is pure Go and answers the git
# protocol natively (no `git` binary at runtime).
RUN --mount=type=cache,target=/go/pkg/mod \
Expand All @@ -33,6 +41,10 @@ FROM gcr.io/distroless/static-debian12:nonroot

COPY --from=build /objgitd /objgitd

# WASI programs for hooks, such as kustomize. The default -wasm-path looks
# here after /app/wasm/bin, so a program mounted there replaces one of these.
COPY bin/ /usr/libexec/objgit/bin/

# Smart HTTP, git://, metrics. SSH (-ssh-bind) is opt-in; publish it yourself.
EXPOSE 8080 9418 9090

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Notable features:
- ssh, http, and git protocol support.
- [post-receive hooks](./docs/usage/hooks.md) powered by userspace sandboxed shells and [Kefka](https://xeiaso.net/blog/2026/dancing-mad-sandboxing/).
- SSH sandbox access to the environment hooks run in.
- [Kustomize, Kubernetes, and Tekton commands](./docs/usage/kubernetes-hooks.md) in hooks, so a push can start a PipelineRun.
- [Git LFS](./docs/usage/lfs.md) with presigned transfers, so large files move straight between the client and Tigris instead of through the daemon. LFS object bytes are deduplicated across every repository in the bucket.
- EROFS snapshots per tree (including LFS pointer resolution).
- basic prometheus metrics.
Expand Down
3 changes: 3 additions & 0 deletions bin/kustomize.wasm
Git LFS file not shown
11 changes: 11 additions & 0 deletions cmd/objgitd/git_protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ import (
"github.com/go-git/go-git/v6/plumbing/transport"
"github.com/go-git/go-git/v6/storage"
"github.com/tigrisdata/objgit/internal/auth"
"github.com/tigrisdata/objgit/internal/kube"
"github.com/tigrisdata/objgit/internal/metrics"
"github.com/tigrisdata/objgit/internal/repofs"
"github.com/tigrisdata/objgit/internal/wasmbin"
)

// handshakeTimeout bounds how long a client has to send its git-proto-request.
Expand Down Expand Up @@ -72,6 +74,15 @@ type daemon struct {
// registers no LFS route, so the feature reads as a 404 from outside
// instead of as a broken endpoint.
lfs *lfsService

// kube is the Kubernetes API client behind the kube:apply and
// tekton:pipelinerun hook commands, nil when -allow-kubernetes is unset.
// A nil kube registers stubs that name the flag.
kube *kube.Client

// bins is the WASI programs from -wasm-path, such as kustomize, that
// hooks and the SSH sh command can run. A nil bins adds none.
bins *wasmbin.Set
}

// storerFor reports whether a repository already exists at st, returning st
Expand Down
25 changes: 22 additions & 3 deletions cmd/objgitd/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@ import (
"github.com/go-git/go-git/v6/plumbing/transport"
"github.com/go-git/go-git/v6/storage"
"github.com/tigrisdata/objgit/internal/kefkash"
"github.com/tigrisdata/objgit/internal/kube"
"github.com/tigrisdata/objgit/internal/metrics"
"github.com/tigrisdata/objgit/internal/mountfs"
"github.com/tigrisdata/objgit/internal/pushevents"
"github.com/tigrisdata/objgit/internal/treefs"
"github.com/tigrisdata/objgit/internal/wasmbin"
"github.com/tigrisdata/objgit/internal/webhook"
"mvdan.cc/sh/v3/expand"
"mvdan.cc/sh/v3/interp"
Expand Down Expand Up @@ -164,7 +166,7 @@ func (d *daemon) runHook(repoPath, service string, st storage.Storer, u refUpdat
}

stdin := strings.NewReader(hookStdin(u))
sh, err := newHookShell(tree, changes, hookEnv(repoPath, service, u, changes), stdin, stdout, stderr)
sh, err := newHookShell(tree, changes, hookEnv(repoPath, service, u, changes), d.bins, d.kube, hookOrigin(repoPath, u), stdin, stdout, stderr)
if err != nil {
log.Error("hook: build shell", "err", err)
return
Expand Down Expand Up @@ -266,6 +268,18 @@ func hookEnv(repoPath, service string, u refUpdate, c hookChanges) []string {
}
}

// hookOrigin describes update u to the Kubernetes commands. It carries the
// same values as the OBJGIT_* variables, but a script cannot change it. The
// SSH sh shell can target a tag or a commit, so Branch is set only for a
// branch.
func hookOrigin(repoPath string, u refUpdate) kube.Origin {
o := kube.Origin{Repo: repoPath, Ref: u.Name.String(), Commit: u.New.String()}
if u.Name.IsBranch() {
o.Branch = u.Name.Short()
}
return o
}

// hookStdin mirrors git's post-receive stdin for update u: "<old> <new> <ref>\n".
func hookStdin(u refUpdate) string {
return u.Old.String() + " " + u.New.String() + " " + u.Name.String() + "\n"
Expand All @@ -274,8 +288,11 @@ func hookStdin(u refUpdate) string {
// newHookShell builds the kefka sandbox a hook runs in: /src is a lazy
// read-only view of tree, /tmp is writable scratch that holds hookChangesFile,
// and the shell starts in /src with env. Both push hooks and the SSH sh command
// use it, so the two environments cannot drift apart.
func newHookShell(tree *object.Tree, changes hookChanges, env []string, stdin io.Reader, stdout, stderr io.Writer) (*interp.Runner, error) {
// use it, so the two environments cannot drift apart. bins adds the WASI
// programs from -wasm-path, which can replace a kefka built-in. kc backs
// kube:apply and tekton:pipelinerun, which act for origin; nil registers stubs
// that say how to turn them on.
func newHookShell(tree *object.Tree, changes hookChanges, env []string, bins *wasmbin.Set, kc *kube.Client, origin kube.Origin, stdin io.Reader, stdout, stderr io.Writer) (*interp.Runner, error) {
fsys := mountfs.New(map[string]billy.Filesystem{
"src": treefs.New(tree),
"tmp": memfs.New(),
Expand All @@ -288,6 +305,8 @@ func newHookShell(tree *object.Tree, changes hookChanges, env []string, stdin io
coreutils.Register(reg)
wasmprog.Register(reg)
uutils.Register(reg)
bins.Register(reg)
kube.Register(reg, kc, origin)
if err := reg.Chdir(fsys, "/src"); err != nil {
return nil, fmt.Errorf("chdir /src: %w", err)
}
Expand Down
Loading
Loading