Skip to content
Open
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
56 changes: 56 additions & 0 deletions cmd/gateway/checkout_flags_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package main

import (
"testing"
"time"
)

// TestSplitNonEmpty pins the checkout-pools flag parsing: comma separation,
// whitespace trimming, and empty items dropped (so a trailing comma or a bare
// value never enables a pool named "").
func TestSplitNonEmpty(t *testing.T) {
cases := []struct {
in string
want []string
}{
{"", nil},
{" , ,", nil},
{"python", []string{"python"}},
{"python, node ,", []string{"python", "node"}},
}
for _, tc := range cases {
got := splitNonEmpty(tc.in)
if len(got) != len(tc.want) {
t.Fatalf("splitNonEmpty(%q) = %v, want %v", tc.in, got, tc.want)
}
for i := range got {
if got[i] != tc.want[i] {
t.Fatalf("splitNonEmpty(%q) = %v, want %v", tc.in, got, tc.want)
}
}
}
}

// TestEnvHelpersKeepDefaultsOnGarbage pins that a malformed env value keeps
// the compiled-in default instead of zeroing a limit.
func TestEnvHelpersKeepDefaultsOnGarbage(t *testing.T) {
t.Setenv("TEST_CHECKOUT_INT", "not-a-number")
if got := envInt("TEST_CHECKOUT_INT", 7); got != 7 {
t.Fatalf("envInt(garbage) = %d, want the default 7", got)
}
t.Setenv("TEST_CHECKOUT_INT", "3")
if got := envInt("TEST_CHECKOUT_INT", 7); got != 3 {
t.Fatalf("envInt(3) = %d, want 3", got)
}
t.Setenv("TEST_CHECKOUT_DUR", "soon")
if got := envDuration("TEST_CHECKOUT_DUR", time.Minute); got != time.Minute {
t.Fatalf("envDuration(garbage) = %s, want the default 1m", got)
}
t.Setenv("TEST_CHECKOUT_DUR", "90s")
if got := envDuration("TEST_CHECKOUT_DUR", time.Minute); got != 90*time.Second {
t.Fatalf("envDuration(90s) = %s, want 90s", got)
}
if got := envInt("TEST_CHECKOUT_UNSET_KEY", 5); got != 5 {
t.Fatalf("envInt(unset) = %d, want 5", got)
}
}
Comment on lines +53 to +56

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make the “unset” environment assertion deterministic.

envInt reads the process environment, so an externally defined TEST_CHECKOUT_UNSET_KEY can make this test fail. Clear the key explicitly before asserting the default.

Proposed fix
+	t.Setenv("TEST_CHECKOUT_UNSET_KEY", "")
 	if got := envInt("TEST_CHECKOUT_UNSET_KEY", 5); got != 5 {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if got := envInt("TEST_CHECKOUT_UNSET_KEY", 5); got != 5 {
t.Fatalf("envInt(unset) = %d, want 5", got)
}
}
t.Setenv("TEST_CHECKOUT_UNSET_KEY", "")
if got := envInt("TEST_CHECKOUT_UNSET_KEY", 5); got != 5 {
t.Fatalf("envInt(unset) = %d, want 5", got)
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/gateway/checkout_flags_test.go` around lines 53 - 56, Make the
unset-environment assertion in the envInt test deterministic by explicitly
clearing TEST_CHECKOUT_UNSET_KEY before calling envInt, using the test’s
cleanup-aware environment helper if available; then retain the assertion that
envInt returns 5.

63 changes: 61 additions & 2 deletions cmd/gateway/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import (
"net/http"
"os"
"os/signal"
"strconv"
"strings"
"sync/atomic"
"syscall"
"time"
Expand Down Expand Up @@ -75,7 +77,7 @@ func newScheme() *runtime.Scheme {
// the poll interval governs only the fail-open fallback loop. The client is
// returned alongside so main can wire the SAME client into the quota
// enforcer's live sandbox counter.
func newControlPlane(readyTimeout, readyPollInterval time.Duration, defaultPool, singleTenantNS string) (saas.ControlPlane, client.Client, error) {
func newControlPlane(readyTimeout, readyPollInterval time.Duration, defaultPool, singleTenantNS string, checkout controlplane.CheckoutConfig) (*controlplane.K8sControlPlane, client.Client, error) {
cfg, err := ctrl.GetConfig()
if err != nil {
return nil, nil, fmt.Errorf("load kubeconfig for the control-plane client: %w", err)
Expand All @@ -92,9 +94,44 @@ func newControlPlane(readyTimeout, readyPollInterval time.Duration, defaultPool,
opts = append(opts, controlplane.WithDefaultPool(defaultPool))
}
opts = append(opts, controlplane.WithSingleTenantNamespace(singleTenantNS))
if len(checkout.Pools) > 0 {
opts = append(opts, controlplane.WithCheckout(checkout))
}
return controlplane.New(c, opts...), c, nil
}

// splitNonEmpty splits a comma-separated flag value into trimmed, non-empty
// items.
func splitNonEmpty(s string) []string {
var out []string
for _, p := range strings.Split(s, ",") {
if p = strings.TrimSpace(p); p != "" {
out = append(out, p)
}
}
return out
}

// envInt and envDuration read an env default for a numeric flag; any parse
// problem keeps the compiled-in default.
func envInt(key string, def int) int {
if v := os.Getenv(key); v != "" {
if n, err := strconv.Atoi(v); err == nil {
return n
}
}
return def
}

func envDuration(key string, def time.Duration) time.Duration {
if v := os.Getenv(key); v != "" {
if d, err := time.ParseDuration(v); err == nil {
return d
}
}
return def
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
func main() {
addr := flag.String("addr", ":8080", "public listen address")
metricsAddr := flag.String("metrics-addr", ":9100", "Prometheus metrics listen address (a SEPARATE cluster-internal listener; /metrics is never mounted on the public mux). Empty disables the metrics listener.")
Expand All @@ -108,6 +145,10 @@ func main() {
databaseDSN := flag.String("database-dsn", "", "Postgres DSN for durable persistence (accounts, orgs, memberships, API keys). Falls back to the "+pgstore.EnvDSN+" env var. Empty means in-memory persistence (DEV ONLY). The value is a secret and is never logged.")
enforceQuota := flag.Bool("enforce-quota", true, "enforce per-organization quotas, rate limits, and the abuse kill-switch before forwarding. Default on (the hosted profile). Set to false only for a trusted single-tenant deployment; the bypass is logged at startup.")
trustedProxyHops := flag.Int("trusted-proxy-hops", 0, "number of trusted reverse-proxy hops in front of the gateway for client-IP resolution. 0 (the default) does NOT trust X-Forwarded-For and uses the connection RemoteAddr. Set to the count of trusted proxies (for example 1 behind a single ingress) so the per-IP rate limit keys on the real client; a too-short or spoofed X-Forwarded-For fails closed to RemoteAddr.")
checkoutPools := flag.String("checkout-pools", os.Getenv("MITOS_GATEWAY_CHECKOUT_POOLS"), "comma-separated pool names served by the pre-claimed checkout buffer (empty disables the feature); requires --single-tenant-namespace")
checkoutFloor := flag.Int("checkout-floor", envInt("MITOS_GATEWAY_CHECKOUT_FLOOR", 2), "buffered sandboxes to keep ready per checkout pool")
checkoutCap := flag.Int("checkout-cap", envInt("MITOS_GATEWAY_CHECKOUT_CAP", 4), "hard ceiling of buffered sandboxes per checkout pool")
checkoutMaxAge := flag.Duration("checkout-max-age", envDuration("MITOS_GATEWAY_CHECKOUT_MAX_AGE", 10*time.Minute), "buffered sandboxes older than this are recycled")
flag.Parse()

logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo}))
Expand Down Expand Up @@ -144,11 +185,29 @@ func main() {
logger.Warn("gateway running with the DEV stub control plane; no sandboxes are created (--allow-stub)")
cp = stubControlPlane{}
} else {
real, k8sClient, err := newControlPlane(*readyTimeout, *readyPollInterval, *defaultPool, *singleTenantNS)
checkoutCfg := controlplane.CheckoutConfig{
Pools: splitNonEmpty(*checkoutPools),
Floor: *checkoutFloor,
Cap: *checkoutCap,
MaxAge: *checkoutMaxAge,
}
real, k8sClient, err := newControlPlane(*readyTimeout, *readyPollInterval, *defaultPool, *singleTenantNS, checkoutCfg)
if err != nil {
log.Fatalf("build control plane: %v", err)
}
cp = real
// The checkout buffer's refill/janitor loop runs for the server's
// lifetime; the hot path only ever reads its cache. Not being silent
// about a half-configured feature: --checkout-pools without
// --single-tenant-namespace leaves the buffer off by design (see the
// spec's per-org-namespace migration note), and that must be loud.
switch {
case len(checkoutCfg.Pools) > 0 && *singleTenantNS == "":
logger.Warn("checkout pools configured but --single-tenant-namespace is unset; the pre-claimed checkout stays OFF", "pools", *checkoutPools)
case len(checkoutCfg.Pools) > 0:
real.StartCheckout(context.Background())
logger.Info("pre-claimed checkout enabled", "pools", *checkoutPools, "floor", *checkoutFloor, "cap", *checkoutCap, "max_age", checkoutMaxAge.String())
}
// The counter shares the control plane's client and namespace model
// (per-org, or the pinned single-tenant namespace) so it counts exactly
// where the control plane creates.
Expand Down
6 changes: 6 additions & 0 deletions deploy/charts/mitos/templates/gateway-rbac.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ rules:
- get
- list
- watch
# patch is the pre-claimed checkout's ONE hot-path write: the
# resourceVersion-guarded label patch that atomically drops the buffered
# label and stamps the org labels (attribution plus replica mutual
# exclusion). The gateway still gets no pod writes and no Secret writes;
# the controller propagates the org to the husk pod.
- patch
- delete
- apiGroups:
- mitos.run
Expand Down
6 changes: 6 additions & 0 deletions deploy/charts/mitos/templates/gateway.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ spec:
- --metrics-addr=:9100
- --enforce-quota={{ .Values.gateway.enforce.enabled }}
- --trusted-proxy-hops={{ int .Values.gateway.enforce.trustedProxyHops }}
{{- if .Values.gateway.checkout.pools }}
- --checkout-pools={{ join "," .Values.gateway.checkout.pools }}
- --checkout-floor={{ int .Values.gateway.checkout.floor }}
- --checkout-cap={{ int .Values.gateway.checkout.cap }}
- --checkout-max-age={{ .Values.gateway.checkout.maxAge }}
{{- end }}
{{- if or .Values.database.dsnSecretRef.name (and .Values.telemetry.enabled .Values.telemetry.endpoint) .Values.gateway.singleTenantNamespace .Values.saas.apiKeyPepperSecret.name }}
env:
{{- include "mitos.database.env" . | nindent 12 }}
Expand Down
28 changes: 28 additions & 0 deletions deploy/charts/mitos/values.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -938,6 +938,34 @@
"description": "When non-empty, pins all sandbox operations to this fixed namespace instead of per-org namespaces.",
"type": "string"
},
"checkout": {
"description": "Pre-claimed checkout: a buffer of already-activated sandboxes served to eligible creates with one attribution patch. Requires singleTenantNamespace; empty pools disables it.",
"type": "object",
"additionalProperties": false,
"properties": {
"pools": {
"description": "Pool names served by the checkout buffer; empty disables the feature.",
"type": "array",
"items": {
"type": "string"
}
},
"floor": {
"description": "Buffered sandboxes to keep ready per checkout pool.",
"type": "integer",
"minimum": 1
},
"cap": {
"description": "Hard ceiling of buffered sandboxes per checkout pool.",
"type": "integer",
"minimum": 1
},
"maxAge": {
"description": "Buffered sandboxes older than this are recycled (Go duration).",
"type": "string"
}
}
},
"ingress": {
"$ref": "#/definitions/ingress"
}
Expand Down
11 changes: 11 additions & 0 deletions deploy/charts/mitos/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -766,6 +766,17 @@ gateway:
# platform Secret or another tenant's object; GHSA-pgv2-9w24-j7wh). Use per-org
# tenancy to enable secretRef and workspaces safely.
singleTenantNamespace: ""
# Pre-claimed checkout: keep a small buffer of already-activated sandboxes
# per listed pool and serve eligible creates (no env, secrets, workspace,
# fan-out, or TTL) from it with a single attribution patch instead of the
# whole claim round trip. Requires singleTenantNamespace. Buffered sandboxes
# carry no org and bill nobody until claimed: deliberate platform cost,
# bounded by cap and maxAge. Default off (empty pools).
checkout:
pools: []
floor: 2
cap: 4
maxAge: 10m
ingress:
enabled: false
className: ""
Expand Down
Loading
Loading