Skip to content

Commit 48f16b7

Browse files
committed
embeddable inmemory transport
1 parent aa1d072 commit 48f16b7

File tree

10 files changed

+1813
-22
lines changed

10 files changed

+1813
-22
lines changed

docs/embedding.md

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
# Embedding
2+
3+
The SpiceDB KubeAPI Proxy supports an embedded mode that allows you to integrate the proxy directly into your application without requiring TLS certificates or binding to network ports. Under the hood, it uses the [`pkg/inmemory`](./pkg/inmemory/) transport package for zero-overhead HTTP communication.
4+
5+
## Usage
6+
7+
### Basic Setup
8+
9+
```go
10+
package main
11+
12+
import (
13+
"context"
14+
"net/http"
15+
16+
"k8s.io/client-go/kubernetes"
17+
"k8s.io/client-go/rest"
18+
19+
"github.com/authzed/spicedb-kubeapi-proxy/pkg/proxy"
20+
)
21+
22+
func main() {
23+
ctx := context.Background()
24+
25+
// Create options with embedded mode enabled
26+
opts := proxy.NewOptions()
27+
opts.EmbeddedMode = true
28+
29+
// Configure your backend Kubernetes cluster
30+
opts.RestConfigFunc = func() (*rest.Config, http.RoundTripper, error) {
31+
// Return your cluster's REST config and transport
32+
return myClusterConfig, myTransport, nil
33+
}
34+
35+
// Configure SpiceDB (can use embedded SpiceDB too)
36+
opts.SpiceDBOptions.SpiceDBEndpoint = "embedded://"
37+
38+
// Set up your authorization rules
39+
opts.RuleConfigFile = "rules.yaml"
40+
41+
// Complete configuration
42+
if err := opts.Complete(ctx); err != nil {
43+
panic(err)
44+
}
45+
46+
// Create the proxy server
47+
proxySrv, err := proxy.NewServer(ctx, *opts)
48+
if err != nil {
49+
panic(err)
50+
}
51+
52+
// Get an HTTP client that connects directly to the embedded proxy
53+
embeddedClient := proxySrv.GetEmbeddedClient()
54+
55+
// Create a Kubernetes client that uses the embedded proxy
56+
k8sClient := createKubernetesClient(embeddedClient, "my-user", []string{"my-group"})
57+
58+
// Use the client normally - all requests go through SpiceDB authorization
59+
pods, err := k8sClient.CoreV1().Pods("default").List(ctx, metav1.ListOptions{})
60+
if err != nil {
61+
panic(err)
62+
}
63+
64+
fmt.Printf("Found %d pods\n", len(pods.Items))
65+
}
66+
67+
func createKubernetesClient(embeddedClient *http.Client, username string, groups []string) *kubernetes.Clientset {
68+
restConfig := &rest.Config{
69+
Host: "http://embedded", // Special URL for embedded mode
70+
Transport: embeddedClient.Transport,
71+
}
72+
73+
// Add authentication headers
74+
restConfig.WrapTransport = func(rt http.RoundTripper) http.RoundTripper {
75+
return &authTransport{
76+
username: username,
77+
groups: groups,
78+
rt: rt,
79+
}
80+
}
81+
82+
k8sClient, err := kubernetes.NewForConfig(restConfig)
83+
if err != nil {
84+
panic(err)
85+
}
86+
87+
return k8sClient
88+
}
89+
90+
type authTransport struct {
91+
username string
92+
groups []string
93+
rt http.RoundTripper
94+
}
95+
96+
func (t *authTransport) RoundTrip(req *http.Request) (*http.Response, error) {
97+
// Add authentication headers that embedded mode recognizes
98+
req.Header.Set("X-Remote-User", t.username)
99+
for _, group := range t.groups {
100+
req.Header.Add("X-Remote-Group", group)
101+
}
102+
return t.rt.RoundTrip(req)
103+
}
104+
```
105+
106+
### Configuration Options
107+
108+
When using embedded mode, you can set these options:
109+
110+
```go
111+
opts := proxy.NewOptions()
112+
113+
// Enable embedded mode
114+
opts.EmbeddedMode = true
115+
116+
// Backend Kubernetes cluster configuration
117+
opts.RestConfigFunc = func() (*rest.Config, http.RoundTripper, error) {
118+
// Your cluster configuration
119+
}
120+
121+
// SpiceDB configuration (can use embedded SpiceDB)
122+
opts.SpiceDBOptions.SpiceDBEndpoint = "embedded://" // or "localhost:50051"
123+
opts.SpiceDBOptions.SecureSpiceDBTokensBySpace = "your-token"
124+
125+
// Authorization rules
126+
opts.RuleConfigFile = "path/to/rules.yaml"
127+
```
128+
129+
### Authentication Headers
130+
131+
In embedded mode, authentication is handled via HTTP headers:
132+
133+
- `X-Remote-User`: The username (required)
134+
- `X-Remote-Group`: Group membership (can be specified multiple times)
135+
- `X-Remote-Extra-*`: Extra user attributes (e.g., `X-Remote-Extra-Department: engineering`)
136+
137+
Example:
138+
```
139+
X-Remote-User: alice
140+
X-Remote-Group: developers
141+
X-Remote-Group: admin
142+
X-Remote-Extra-Department: engineering
143+
X-Remote-Extra-Team: platform
144+
```
145+
146+
This is similar to kube's request header auth mode, but client cert configuration is not required (the requests are trusted because the server is embedded).

pkg/inmemory/README.md

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# In-Memory HTTP Transport
2+
3+
_note: written as if it will be split into its own package_
4+
5+
A high-performance, zero-network-overhead HTTP transport implementation that bypasses the network layer entirely by calling handlers directly using lazy evaluation.
6+
7+
## Overview
8+
9+
The `inmemory` package provides an `http.RoundTripper` implementation that directly invokes HTTP handlers in memory, eliminating all network serialization, parsing, and connection overhead. This is ideal for embedded http services or testing and development environments.
10+
11+
## Quick Start
12+
13+
```go
14+
package main
15+
16+
import (
17+
"fmt"
18+
"io"
19+
"net/http"
20+
21+
"github.com/authzed/spicedb-kubeapi-proxy/pkg/inmemory"
22+
)
23+
24+
func main() {
25+
// Create your HTTP handler
26+
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
27+
w.Header().Set("Content-Type", "application/json")
28+
w.WriteHeader(http.StatusOK)
29+
w.Write([]byte(`{"message": "Hello, World!"}`))
30+
})
31+
32+
// Create an HTTP client with in-memory transport
33+
client := inmemory.NewClient(handler)
34+
35+
// Make requests - no network involved!
36+
resp, err := client.Get("http://api.example.com/hello")
37+
if err != nil {
38+
panic(err)
39+
}
40+
defer resp.Body.Close()
41+
42+
// Headers and status are available after reading the body
43+
io.Copy(io.Discard, resp.Body) // Triggers handler execution
44+
fmt.Printf("Status: %d\n", resp.StatusCode)
45+
fmt.Printf("Content-Type: %s\n", resp.Header.Get("Content-Type"))
46+
}
47+
```
48+
49+
## API Reference
50+
51+
### `New(handler http.Handler) *Transport`
52+
53+
Creates a new in-memory transport that will invoke the provided handler directly.
54+
55+
```go
56+
transport := inmemory.New(myHandler)
57+
client := &http.Client{Transport: transport}
58+
```
59+
60+
### `NewClient(handler http.Handler) *http.Client`
61+
62+
Convenience function that creates an HTTP client with an in-memory transport.
63+
64+
```go
65+
client := inmemory.NewClient(myHandler)
66+
```
67+
68+
### Lazy Execution
69+
70+
The transport uses lazy evaluation - the handler is executed only when the response body is read:
71+
72+
```go
73+
resp, _ := client.Get("http://example.com/")
74+
// Handler not executed yet, headers/status not available
75+
76+
// Reading body triggers execution
77+
body, _ := io.ReadAll(resp.Body)
78+
// Now headers and status are available
79+
80+
// Or manually trigger execution without reading body
81+
if lazyBody, ok := resp.Body.(*inmemory.LazyResponseBody); ok {
82+
lazyBody.TriggerExecution() // Execute handler without reading body
83+
// Headers and status now available
84+
}
85+
```
86+
87+
## Examples
88+
89+
### Basic Usage
90+
91+
```go
92+
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
93+
w.WriteHeader(http.StatusOK)
94+
w.Write([]byte("Hello!"))
95+
})
96+
97+
client := inmemory.NewClient(handler)
98+
resp, _ := client.Get("http://example.com/")
99+
io.ReadAll(resp.Body) // Triggers execution
100+
// Response contains "Hello!" with status 200
101+
```
102+
103+
### With Request Bodies
104+
105+
```go
106+
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
107+
body, _ := io.ReadAll(r.Body)
108+
w.Write([]byte(fmt.Sprintf("Echo: %s", body)))
109+
})
110+
111+
client := inmemory.NewClient(handler)
112+
resp, _ := client.Post("http://example.com/echo", "text/plain",
113+
strings.NewReader("test data"))
114+
io.ReadAll(resp.Body) // Triggers execution
115+
// Response contains "Echo: test data"
116+
```
117+
118+
### Complex Handler
119+
120+
```go
121+
mux := http.NewServeMux()
122+
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
123+
w.WriteHeader(http.StatusOK)
124+
w.Write([]byte("healthy"))
125+
})
126+
mux.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) {
127+
w.Header().Set("Content-Type", "application/json")
128+
w.Write([]byte(`[{"id": 1, "name": "Alice"}]`))
129+
})
130+
131+
client := inmemory.NewClient(mux)
132+
133+
// Both endpoints work normally
134+
healthResp, _ := client.Get("http://api.com/health")
135+
usersResp, _ := client.Get("http://api.com/api/users")
136+
137+
// Read responses to trigger execution
138+
io.ReadAll(healthResp.Body)
139+
io.ReadAll(usersResp.Body)
140+
```

0 commit comments

Comments
 (0)