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
22 changes: 14 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@

A production-minded Layer 7 gateway in Go, designed around protocol correctness, safe configuration rollouts, and predictable failure behavior.

**Go:** 1.26.8 · **Contracts:** Protobuf · **CI:** [GitHub Actions](https://github.com/SShogun/GateHouse/actions/workflows/ci.yml)
**Go:** 1.26.9 · **Contracts:** Protobuf · **CI:** [GitHub Actions](https://github.com/SShogun/GateHouse/actions/workflows/ci.yml)

</div>

> **Project state: M0 — repository, contracts, and CI foundation.** The current code has process lifecycle scaffolding and generated Protobuf bindings. It does **not** yet accept network traffic, route requests, store configuration, or publish runtime configuration. This README separates the intended system from what runs today.
> **Project state: M2 implementation present — static routing, clusters, and health.** The gateway can compile in-memory listener, route, and cluster configuration, match requests deterministically, select eligible endpoints, and run optional HTTP health checks. The `gatehouse-data` command still builds a single default route and cluster from environment variables. M3 configuration storage and dynamic publication/reload are not implemented.

[What exists](#what-works-today) · [Run checks](#try-the-current-foundation) · [Target architecture](#target-architecture) · [Roadmap](#roadmap) · [Docs](#repository-map)

Expand All @@ -30,16 +30,18 @@ Its eventual boundary is deliberately split:

## What works today

The current milestone is intentionally small. The repository provides:
The current implementation provides:

- separate Go command entry points for `gatehouse-control` and `gatehouse-data`;
- SIGINT/SIGTERM handling, root cancellation, and bounded shutdown scaffolding;
- a static HTTP reverse proxy and compiled in-memory route/cluster runtime, with bounded server defaults and graceful shutdown;
- deterministic route matching, endpoint selection, and optional scheduled HTTP health checks;
- SIGINT/SIGTERM handling, root cancellation, and bounded shutdown coordination;
- a `gatehouse.v1` Protobuf workspace with committed Go and TypeScript generated output;
- Buf linting, compatibility checks, and generated-code drift checks;
- an executable compatibility fixture that proves Buf rejects a removed field and accepts a compatible addition;
- separate CI jobs for contracts, static analysis, unit tests, race tests, and vulnerability scanning.

The command binaries currently wait for a shutdown signal. They do not open listeners or implement gateway behavior yet. The Protobuf package is a contract workspace, not a completed application API.
The data-plane binary listens on `127.0.0.1:8080` and routes requests to `http://127.0.0.1:8081` by default. Set `GATEHOUSE_LISTEN_ADDR` and `GATEHOUSE_UPSTREAM_URL` to override the listener and the sole endpoint configured by this command. The gateway/config packages support richer static in-memory route and cluster configurations, but the command does not load them from a file or control plane. The control-plane binary remains a lifecycle scaffold. The Protobuf package is a contract workspace, not a completed application API.

## Target architecture

Expand Down Expand Up @@ -71,7 +73,7 @@ The critical separation: the control plane owns durable configuration and public
### Requirements

- Git
- Go 1.26.8 (the `go.mod` toolchain directive is authoritative)
- Go 1.26.9 (the `go.mod` Go version is authoritative)
- Buf CLI 1.73.0
- `make`
- Network access on the first verification run to download pinned analysis tools and Buf generators
Expand All @@ -86,13 +88,17 @@ make verify

`make verify` runs formatting checks, `go vet`, Staticcheck, Buf lint and breaking checks, the negative compatibility fixture, generated-code drift detection, module verification, unit tests, race tests, and `govulncheck`.

### Run the lifecycle scaffold
### Run the static data-plane proxy

```bash
GATEHOUSE_LISTEN_ADDR=127.0.0.1:8080 \
GATEHOUSE_UPSTREAM_URL=http://127.0.0.1:8081 \
go run ./cmd/gatehouse-data
```

The process waits for SIGINT (`Ctrl+C`) or SIGTERM, then follows the lifecycle shutdown path. It does not bind a port or proxy requests yet. The control-plane scaffold can be started with `go run ./cmd/gatehouse-control` and behaves the same way at this milestone.
The default command configuration matches all requests to one cluster with one upstream endpoint. The gateway runtime also supports deterministic static route matching and cluster endpoint selection, with optional scheduled HTTP health checks when configured in memory. The proxy preserves the request method, path, end-to-end headers, and body; forwards response status, end-to-end headers, and body; strips hop-by-hop headers; propagates request cancellation; and streams response chunks without waiting for the full upstream body. Retries and dynamic configuration publication/reload are not implemented. SIGINT (`Ctrl+C`) and SIGTERM stop new requests and drain in-flight requests within the shutdown timeout. The control-plane scaffold can be started with `go run ./cmd/gatehouse-control` and currently waits for a shutdown signal.

The development command accepts only literal loopback listener addresses, such as `127.0.0.1:8080` or `[::1]:8080`; wildcard addresses, non-loopback IPs, and hostnames are rejected. Authentication and TLS remain M6 work. Static defaults admit at most 128 concurrent HTTP handlers per listener, bound individual request-body read waits to 30 seconds, and bound upstream response-header waits to 30 seconds and headers to 1 MiB. Excess requests receive HTTP 503 without a queue. Active uploads and response streams retain streaming behavior; total request deadlines and route policies remain M5 work. See the [PR #5 review record](docs/Gatehouse_PR5_REVIEW.md) for dispositions and verification evidence.

## The proof bar

Expand Down
64 changes: 54 additions & 10 deletions cmd/gatehouse-data/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,21 @@ package main

import (
"context"
"errors"
"fmt"
"log/slog"
"net/url"
"os"
"os/signal"
"syscall"
"time"

"github.com/SShogun/GateHouse/internal/cluster"
"github.com/SShogun/GateHouse/internal/config"
"github.com/SShogun/GateHouse/internal/dataplane"
"github.com/SShogun/GateHouse/internal/gateway"
"github.com/SShogun/GateHouse/internal/lifecycle"
"github.com/SShogun/GateHouse/internal/router"
)

const shutdownTimeout = 10 * time.Second
Expand All @@ -19,14 +29,48 @@ func main() {
}

func run() error {
return lifecycle.RunWithSignals(shutdownTimeout,
func(ctx context.Context) error {
<-ctx.Done()
return nil
},
func(context.Context) error {
// The data plane owns no listener or upstream pool until later milestones.
return nil
},
)
listenAddress := envOrDefault("GATEHOUSE_LISTEN_ADDR", "127.0.0.1:8080")
if err := dataplane.ValidateDevelopmentListenAddress(listenAddress); err != nil {
return fmt.Errorf("validate development listener: %w", err)
}
signalCtx, stopSignals := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stopSignals()

logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
upstreamAddress := envOrDefault("GATEHOUSE_UPSTREAM_URL", "http://127.0.0.1:8081")
upstream, err := url.Parse(upstreamAddress)
if err != nil {
return fmt.Errorf("parse GATEHOUSE_UPSTREAM_URL: %w", err)
}
clusterID, endpointID, routeID := "default", "default", "default"
runtime, err := gateway.New(config.Config{
Listener: config.Listener{Address: listenAddress},
Routes: []router.Route{{ID: routeID, Path: "/", PathType: router.PathPrefix, ClusterID: clusterID}},
Clusters: []config.Cluster{{ID: clusterID, Policy: cluster.RoundRobin, Endpoints: []cluster.Endpoint{{ID: endpointID, Address: upstream.String(), Weight: 1}}}},
}, logger)
if err != nil {
return fmt.Errorf("configure data-plane gateway: %w", err)
}
server, err := dataplane.NewServer(runtime, config.Listener{Address: listenAddress})
if err != nil {
_ = runtime.Close()
return fmt.Errorf("configure data-plane listener: %w", err)
}
if err := server.Start(); err != nil {
_ = runtime.Close()
return err
}
logger.Info("data plane listening", "address", server.Addr(), "upstream", upstreamAddress)
return lifecycle.Run(signalCtx, shutdownTimeout, server.Wait, func(ctx context.Context) error {
serverErr := server.Shutdown(ctx)
gatewayErr := runtime.Close()
return errors.Join(serverErr, gatewayErr)
})
}

func envOrDefault(key, fallback string) string {
if value := os.Getenv(key); value != "" {
return value
}
return fallback
}
15 changes: 15 additions & 0 deletions cmd/gatehouse-data/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package main

import (
"strings"
"testing"
)

func TestRunRejectsExternallyReachableDevelopmentListenerBeforeStarting(t *testing.T) {
t.Setenv("GATEHOUSE_LISTEN_ADDR", "0.0.0.0:8080")
t.Setenv("GATEHOUSE_UPSTREAM_URL", "not a URL")
err := run()
if err == nil || !strings.Contains(err.Error(), "loopback") {
t.Fatalf("run() error = %v, want loopback listener rejection before upstream setup", err)
}
}
4 changes: 2 additions & 2 deletions docs/Gatehouse_DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,6 @@ An unauthenticated developer mode may bind loopback only and must be explicit.

The Go module path follows the canonical repository identity: `github.com/SShogun/GateHouse`.

Since `gatehouse.v1` currently defines no application messages and the project is pre-release (M0), the `go_package` option in `gatehouse.v1` has been updated directly to match the new canonical repository path.
Since `gatehouse.v1` defined no application messages and the project was pre-release (M0), the `go_package` option in `gatehouse.v1` was updated directly to match the new canonical repository path.

To satisfy Buf's strict `FILE` compatibility rules during this zero-consumer migration, a narrowly scoped `breaking.ignore_only` exception for `FILE_SAME_GO_PACKAGE` on `gatehouse.proto` is temporarily applied in `buf.yaml`. This exception must be removed immediately after merging, once the new main commit becomes the compatibility baseline.
The zero-consumer migration completed in M0. The temporary `FILE_SAME_GO_PACKAGE` exception was removed in commit `0d366eb`; `buf.yaml` now enforces strict `FILE` compatibility. No temporary exception remains.
31 changes: 17 additions & 14 deletions docs/Gatehouse_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,13 +224,14 @@ Do not add retries yet.
## Gate M1

```text
[ ] HTTP proxy semantic test GREEN
[ ] cancel reaches upstream GREEN
[ ] streaming-first-byte test GREEN
[ ] huge streamed response does not scale memory with response size
[ ] hop-by-hop header tests GREEN
[ ] graceful shutdown unary request test GREEN
[ ] race + leak checks GREEN
[x] HTTP proxy semantic test GREEN
[x] cancel reaches upstream GREEN
[x] streaming-first-byte test GREEN
[x] huge streamed response does not scale memory with response size (sampled peak HeapAlloc: 8 MiB → 128 MiB body, growth under 24 MiB)
[x] hop-by-hop header tests GREEN
[x] structured failure outcome logged when an upstream response aborts
[x] graceful shutdown in-flight request test GREEN
[x] race + leak checks GREEN (16 concurrent cancellations; active count 16→0 and goroutines return within baseline +4)
```

---
Expand Down Expand Up @@ -286,15 +287,17 @@ Implement:
## Gate M2

```text
[ ] full route precedence matrix GREEN
[ ] fuzz smoke GREEN
[ ] health scheduler add/remove leak test GREEN
[ ] endpoint selection never chooses known-unhealthy endpoint when healthy choice exists
[ ] config with empty/missing cluster rejected
[ ] 10k route-match benchmark recorded
[ ] race GREEN under concurrent requests + health transitions
[x] full route precedence matrix GREEN
[x] fuzz smoke GREEN
[x] health scheduler add/remove leak test GREEN
[x] endpoint selection never chooses known-unhealthy endpoint when healthy choice exists
[x] config with empty/missing cluster rejected
[x] 10k route-match benchmark recorded
[x] race GREEN under concurrent requests + health transitions
```

M2 acceptance evidence is recorded in `Gatehouse_TESTING_CI.md`.

---

# 7. Milestone 3 — Control plane, revisions, and atomic hot reload
Expand Down
Loading
Loading