Skip to content

Commit f892291

Browse files
committed
embeddable inmemory transport
1 parent 98f1b32 commit f892291

File tree

9 files changed

+1377
-24
lines changed

9 files changed

+1377
-24
lines changed

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,34 @@ export KUBECONFIG=$(pwd)/dev.kubeconfig
139139
kubectx proxy
140140
kubectl --insecure-skip-tls-verify get namespace
141141
```
142+
143+
## Embedded Mode
144+
145+
The proxy supports an embedded mode that allows direct in-process connections without network overhead.
146+
This is useful for applications that want to embed the proxy functionality directly.
147+
148+
In embedded mode:
149+
- No TLS/network layer - requests go directly through handlers
150+
- Authentication via configurable HTTP headers (programmatic configuration only)
151+
- High performance with sub-microsecond latency
152+
- Compatible with standard HTTP clients and kubernetes client-go
153+
154+
Embedded mode is designed for programmatic use when embedding the proxy in Go applications:
155+
156+
```go
157+
// Basic embedded mode setup
158+
opts := proxy.NewOptions()
159+
opts.EmbeddedMode = true
160+
opts.SpiceDBOptions.SpiceDBEndpoint = "embedded://"
161+
162+
// Customize authentication headers (optional)
163+
opts.Authentication.Embedded.UsernameHeaders = []string{"Custom-User"}
164+
opts.Authentication.Embedded.GroupHeaders = []string{"Custom-Groups"}
165+
opts.Authentication.Embedded.ExtraHeaderPrefixes = []string{"Custom-Extra-"}
166+
167+
// Create and use the embedded proxy
168+
proxySrv, _ := proxy.NewServer(ctx, *opts)
169+
client := proxySrv.GetEmbeddedClient()
170+
```
171+
172+
See [docs/embedding.md](docs/embedding.md) for detailed usage examples.

docs/embedding.md

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

pkg/inmemory/README.md

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
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 in-process.
6+
7+
## Overview
8+
9+
The `inmemory` package provides an `http.RoundTripper` implementation that directly invokes HTTP handlers in memory during the RoundTrip call, eliminating all network serialization, parsing, and connection overhead.
10+
This is ideal for embedded http services or testing and development environments.
11+
12+
## Quick Start
13+
14+
```go
15+
package main
16+
17+
import (
18+
"fmt"
19+
"io"
20+
"net/http"
21+
22+
"github.com/authzed/spicedb-kubeapi-proxy/pkg/inmemory"
23+
)
24+
25+
func main() {
26+
// Create your HTTP handler
27+
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
28+
w.Header().Set("Content-Type", "application/json")
29+
w.WriteHeader(http.StatusOK)
30+
w.Write([]byte(`{"message": "Hello, World!"}`))
31+
})
32+
33+
// Create an HTTP client with in-memory transport
34+
client := inmemory.NewClient(handler)
35+
36+
// Make requests - no network involved!
37+
resp, err := client.Get("http://api.example.com/hello")
38+
if err != nil {
39+
panic(err)
40+
}
41+
defer resp.Body.Close()
42+
43+
// Headers and status are available immediately
44+
fmt.Printf("Status: %d\n", resp.StatusCode)
45+
fmt.Printf("Content-Type: %s\n", resp.Header.Get("Content-Type"))
46+
47+
// Read the response body
48+
io.Copy(io.Discard, resp.Body)
49+
}
50+
```
51+
52+
## API Reference
53+
54+
### `New(handler http.Handler) *Transport`
55+
56+
Creates a new in-memory transport that will invoke the provided handler directly during RoundTrip execution.
57+
58+
```go
59+
transport := inmemory.New(myHandler)
60+
client := &http.Client{Transport: transport}
61+
```
62+
63+
### `NewClient(handler http.Handler) *http.Client`
64+
65+
Convenience function that creates an HTTP client with an in-memory transport.
66+
67+
```go
68+
client := inmemory.NewClient(myHandler)
69+
```
70+
71+
## Examples
72+
73+
### Basic Usage
74+
75+
```go
76+
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
77+
w.WriteHeader(http.StatusOK)
78+
w.Write([]byte("Hello!"))
79+
})
80+
81+
client := inmemory.NewClient(handler)
82+
resp, _ := client.Get("http://example.com/")
83+
body, _ := io.ReadAll(resp.Body) // Response contains "Hello!"
84+
```
85+
86+
### With Request Bodies
87+
88+
```go
89+
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
90+
body, _ := io.ReadAll(r.Body)
91+
w.Write([]byte(fmt.Sprintf("Echo: %s", body)))
92+
})
93+
94+
client := inmemory.NewClient(handler)
95+
resp, _ := client.Post("http://example.com/echo", "text/plain",
96+
strings.NewReader("test data"))
97+
body, _ := io.ReadAll(resp.Body) // Response contains "Echo: test data"
98+
```
99+
100+
### Complex Handler
101+
102+
```go
103+
mux := http.NewServeMux()
104+
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
105+
w.WriteHeader(http.StatusOK)
106+
w.Write([]byte("healthy"))
107+
})
108+
mux.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) {
109+
w.Header().Set("Content-Type", "application/json")
110+
w.Write([]byte(`[{"id": 1, "name": "Alice"}]`))
111+
})
112+
113+
client := inmemory.NewClient(mux)
114+
115+
// Both endpoints work normally
116+
healthResp, _ := client.Get("http://api.com/health")
117+
usersResp, _ := client.Get("http://api.com/api/users")
118+
119+
// Handlers executed immediately, read responses
120+
io.ReadAll(healthResp.Body)
121+
io.ReadAll(usersResp.Body)
122+
```

0 commit comments

Comments
 (0)