Skip to content

Commit 8ad90c0

Browse files
committed
add functional options for embedded mode and embedding spicedb
1 parent d3d418c commit 8ad90c0

File tree

9 files changed

+116
-51
lines changed

9 files changed

+116
-51
lines changed

README.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,9 +155,7 @@ Embedded mode is designed for programmatic use when embedding the proxy in Go ap
155155

156156
```go
157157
// Basic embedded mode setup
158-
opts := proxy.NewOptions()
159-
opts.EmbeddedMode = true
160-
opts.SpiceDBOptions.SpiceDBEndpoint = "embedded://"
158+
opts := proxy.NewOptions(proxy.WithEmbeddedProxy, proxy.WithEmbeddedSpiceDBEndpoint)
161159

162160
// Complete configuration
163161
completedConfig, _ := opts.Complete(ctx)

docs/embedding.md

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,15 @@ func main() {
2424
ctx := context.Background()
2525

2626
// Create options with embedded mode enabled
27-
opts := proxy.NewOptions()
28-
opts.EmbeddedMode = true
27+
opts := proxy.NewOptions(proxy.WithEmbeddedProxy, proxy.WithEmbeddedSpiceDBEndpoint)
2928

3029
// Configure your backend Kubernetes cluster
3130
opts.RestConfigFunc = func() (*rest.Config, http.RoundTripper, error) {
3231
// Return your cluster's REST config and transport
3332
return myClusterConfig, myTransport, nil
3433
}
3534

36-
// Configure SpiceDB (can use embedded SpiceDB too)
37-
opts.SpiceDBOptions.SpiceDBEndpoint = "embedded://"
35+
// SpiceDB is already configured for embedded mode via WithEmbeddedSpiceDBEndpoint
3836

3937
// Set up your authorization rules
4038
opts.RuleConfigFile = "rules.yaml"
@@ -87,22 +85,43 @@ func createKubernetesClient(embeddedClient *http.Client) *kubernetes.Clientset {
8785

8886
### Configuration Options
8987

90-
When using embedded mode, you can set these options:
88+
## Configuration Options
9189

90+
You can configure the proxy with different combinations of embedded options:
91+
92+
### Full Embedded Mode (Proxy + SpiceDB)
9293
```go
93-
opts := proxy.NewOptions()
94+
// Both proxy and SpiceDB run embedded
95+
opts := proxy.NewOptions(proxy.WithEmbeddedProxy, proxy.WithEmbeddedSpiceDBEndpoint)
96+
```
9497

95-
// Enable embedded mode
96-
opts.EmbeddedMode = true
98+
### Embedded Proxy with Remote SpiceDB
99+
```go
100+
// Proxy runs embedded, but connects to remote SpiceDB
101+
opts := proxy.NewOptions(proxy.WithEmbeddedProxy)
102+
opts.SpiceDBOptions.SpiceDBEndpoint = "localhost:50051"
103+
opts.SpiceDBOptions.SecureSpiceDBTokensBySpace = "your-token"
104+
```
105+
106+
### Regular Proxy with Embedded SpiceDB
107+
```go
108+
// Proxy runs with TLS termination, but uses embedded SpiceDB
109+
opts := proxy.NewOptions(proxy.WithEmbeddedSpiceDBEndpoint)
110+
```
111+
112+
### Example Configuration
113+
```go
114+
opts := proxy.NewOptions(proxy.WithEmbeddedProxy, proxy.WithEmbeddedSpiceDBEndpoint)
97115

98116
// Backend Kubernetes cluster configuration
99117
opts.RestConfigFunc = func() (*rest.Config, http.RoundTripper, error) {
100118
// Your cluster configuration
101119
}
102120

103-
// SpiceDB configuration (can use embedded SpiceDB)
104-
opts.SpiceDBOptions.SpiceDBEndpoint = "embedded://" // or "localhost:50051"
105-
opts.SpiceDBOptions.SecureSpiceDBTokensBySpace = "your-token"
121+
// SpiceDB configuration is already set to embedded via WithEmbeddedSpiceDBEndpoint
122+
// For remote SpiceDB, you would instead use:
123+
// opts.SpiceDBOptions.SpiceDBEndpoint = "localhost:50051"
124+
// opts.SpiceDBOptions.SecureSpiceDBTokensBySpace = "your-token"
106125

107126
// Authorization rules
108127
opts.RuleConfigFile = "path/to/rules.yaml"
@@ -136,8 +155,7 @@ X-Remote-Extra-Team: platform
136155
**Example with custom headers (programmatic configuration):**
137156

138157
```go
139-
opts := proxy.NewOptions()
140-
opts.EmbeddedMode = true
158+
opts := proxy.NewOptions(proxy.WithEmbeddedProxy, proxy.WithEmbeddedSpiceDBEndpoint)
141159

142160
// Configure custom header names
143161
opts.Authentication.Embedded.UsernameHeaders = []string{"Custom-User"}

e2e/e2e_test.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ var _ = SynchronizedBeforeSuite(func() []byte {
118118
Expect(err).To(Succeed())
119119
clientCA = GenerateClientCA(port)
120120

121-
opts := proxy.NewOptions()
121+
opts := proxy.NewOptions(proxy.WithEmbeddedSpiceDBEndpoint)
122122
opts.RestConfigFunc = func() (*rest.Config, http.RoundTripper, error) {
123123
conf, err := clientcmd.NewDefaultClientConfig(*backendCfg, nil).ClientConfig()
124124
if err != nil {
@@ -130,7 +130,6 @@ var _ = SynchronizedBeforeSuite(func() []byte {
130130
}
131131
opts.RuleConfigFile = "rules.yaml"
132132
opts.SecureServing.BindPort = port
133-
opts.SpiceDBOptions.SpiceDBEndpoint = proxy.EmbeddedSpiceDBEndpoint
134133
opts.SecureServing.BindAddress = net.ParseIP("127.0.0.1")
135134
opts.Authentication.BuiltInOptions.ClientCert.ClientCA = clientCA.Path()
136135

go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ module github.com/authzed/spicedb-kubeapi-proxy
22

33
go 1.24.4
44

5+
tool github.com/ecordell/optgen
6+
57
require (
68
github.com/authzed/authzed-go v1.4.0
79
github.com/authzed/grpcutil v0.0.0-20240123194739-2ea1e3d2d98b
@@ -78,6 +80,7 @@ require (
7880
github.com/coreos/go-semver v0.3.1 // indirect
7981
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
8082
github.com/dalzilio/rudd v1.1.1-0.20230806153452-9e08a6ea8170 // indirect
83+
github.com/dave/jennifer v1.6.1 // indirect
8184
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
8285
github.com/distribution/reference v0.6.0 // indirect
8386
github.com/dlmiddlecote/sqlstats v1.0.2 // indirect
@@ -86,6 +89,7 @@ require (
8689
github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect
8790
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
8891
github.com/fatih/color v1.18.0 // indirect
92+
github.com/fatih/structtag v1.2.0 // indirect
8993
github.com/felixge/httpsnoop v1.0.4 // indirect
9094
github.com/fsnotify/fsnotify v1.7.0 // indirect
9195
github.com/fxamacker/cbor/v2 v2.7.0 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -752,6 +752,8 @@ github.com/cschleiden/go-workflows v0.16.3-0.20230928210702-d72004e1fdf2 h1:N2oz
752752
github.com/cschleiden/go-workflows v0.16.3-0.20230928210702-d72004e1fdf2/go.mod h1:a2TcOFW/byjgukUjo2DAD/Cuqdj/ISgh/PB39r1bdH8=
753753
github.com/dalzilio/rudd v1.1.1-0.20230806153452-9e08a6ea8170 h1:bHEN1z3EOO/IXHTQ8ZcmGoW4gTJt+mSrH2Sd458uo0E=
754754
github.com/dalzilio/rudd v1.1.1-0.20230806153452-9e08a6ea8170/go.mod h1:IxPC4Bdi3WqUwyGBMgLrWWGx67aRtUAZmOZrkIr7qaM=
755+
github.com/dave/jennifer v1.6.1 h1:T4T/67t6RAA5AIV6+NP8Uk/BIsXgDoqEowgycdQQLuk=
756+
github.com/dave/jennifer v1.6.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc=
755757
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
756758
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
757759
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@@ -811,6 +813,8 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
811813
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
812814
github.com/fatih/set v0.2.1 h1:nn2CaJyknWE/6txyUDGwysr3G5QC6xWB/PtVjPBbeaA=
813815
github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI=
816+
github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
817+
github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
814818
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
815819
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
816820
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=

pkg/proxy/authn_test.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ func runProxyRequest(t testing.TB, ctx context.Context, headers map[string][]str
9595
AllowedNames: []string{"service"},
9696
}
9797

98-
opts := NewOptions()
98+
opts := NewOptions(WithEmbeddedSpiceDBEndpoint)
9999
opts.RestConfigFunc = func() (*rest.Config, http.RoundTripper, error) {
100100
ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
101101
w.Header().Set("Content-Type", "application/json")
@@ -164,7 +164,6 @@ func runProxyRequest(t testing.TB, ctx context.Context, headers map[string][]str
164164

165165
return rc, transport, nil
166166
}
167-
opts.SpiceDBOptions.SpiceDBEndpoint = EmbeddedSpiceDBEndpoint
168167
opts.SecureServing.ServerCert.CertKey = certStore.servingCertKey
169168
opts.SecureServing.BindAddress = net.ParseIP("127.0.0.1")
170169
opts.SecureServing.BindPort = port

pkg/proxy/embedded_test.go

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -203,10 +203,8 @@ func TestEmbeddedModeDefaults(t *testing.T) {
203203
t.Cleanup(cancel)
204204

205205
// Create embedded proxy with no explicit header configuration to test defaults
206-
opts := NewOptions()
207-
opts.EmbeddedMode = true
206+
opts := NewOptions(WithEmbeddedProxy, WithEmbeddedSpiceDBEndpoint)
208207
opts.Authentication.Embedded.Enabled = true
209-
opts.SpiceDBOptions.SpiceDBEndpoint = EmbeddedSpiceDBEndpoint
210208

211209
// Configure mock upstream server
212210
opts.RestConfigFunc = func() (*rest.Config, http.RoundTripper, error) {
@@ -403,13 +401,9 @@ func (t *testTransport) RoundTrip(req *http.Request) (*http.Response, error) {
403401
func createEmbeddedTestOptions(t *testing.T) *Options {
404402
t.Helper()
405403

406-
opts := NewOptions()
407-
opts.EmbeddedMode = true
404+
opts := NewOptions(WithEmbeddedProxy, WithEmbeddedSpiceDBEndpoint)
408405
opts.Authentication.Embedded.Enabled = true
409406

410-
// Use embedded SpiceDB for testing
411-
opts.SpiceDBOptions.SpiceDBEndpoint = EmbeddedSpiceDBEndpoint
412-
413407
// Configure mock upstream server
414408
opts.RestConfigFunc = func() (*rest.Config, http.RoundTripper, error) {
415409
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

pkg/proxy/options.go

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,6 @@ const (
4141
defaultDialerTimeout = 5 * time.Second
4242
)
4343

44-
//go:generate go run github.com/ecordell/optgen -output zz_spicedb_options.go . SpiceDBOptions
45-
4644
type Options struct {
4745
SecureServing apiserveroptions.SecureServingOptionsWithLoopback `debugmap:"hidden"`
4846
Authentication Authentication `debugmap:"hidden"`
@@ -77,6 +75,7 @@ type Options struct {
7775
PermissionsClient v1.PermissionsServiceClient `debugmap:"hidden"`
7876
}
7977

78+
//go:generate go run github.com/ecordell/optgen -output zz_spicedb_options.go . SpiceDBOptions
8079
type SpiceDBOptions struct {
8180
SpiceDBEndpoint string `debugmap:"visible"`
8281
EmbeddedSpiceDB server.RunnableServer `debugmap:"hidden"`
@@ -106,7 +105,9 @@ func (so *SpiceDBOptions) AddFlags(fs *pflag.FlagSet) {
106105

107106
const tlsCertificatePairName = "tls"
108107

109-
func NewOptions() *Options {
108+
type setOpt func(*Options)
109+
110+
func NewOptions(opts ...setOpt) *Options {
110111
o := &Options{
111112
SecureServing: *apiserveroptions.NewSecureServingOptions().WithLoopback(),
112113
Authentication: *NewAuthentication(),
@@ -116,9 +117,28 @@ func NewOptions() *Options {
116117
o.Logs.Verbosity = logsv1.VerbosityLevel(3)
117118
o.SecureServing.BindPort = 443
118119
o.SecureServing.ServerCert.PairName = tlsCertificatePairName
120+
121+
for _, opt := range opts {
122+
opt(o)
123+
}
124+
119125
return o
120126
}
121127

128+
// WithEmbeddedProxy configures the proxy to run in embedded mode.
129+
// In embedded mode, the proxy runs as an HTTP server without TLS termination,
130+
// suitable for use behind a load balancer or ingress controller.
131+
func WithEmbeddedProxy(o *Options) {
132+
o.EmbeddedMode = true
133+
}
134+
135+
// WithEmbeddedSpiceDBEndpoint configures the proxy to use an embedded SpiceDB instance.
136+
// This creates an in-memory SpiceDB instance that runs within the proxy process.
137+
// Use this for development, testing, or single-node deployments.
138+
func WithEmbeddedSpiceDBEndpoint(o *Options) {
139+
o.SpiceDBOptions.SpiceDBEndpoint = EmbeddedSpiceDBEndpoint
140+
}
141+
122142
func (o *Options) FromRESTConfig(restConfig *rest.Config) *Options {
123143
o.OverrideUpstream = false
124144
o.UseInClusterConfig = false

pkg/proxy/options_test.go

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@ import (
2525
func TestKubeConfig(t *testing.T) {
2626
defer require.NoError(t, logsv1.ResetForTest(utilfeature.DefaultFeatureGate))
2727

28-
opts := optionsForTesting(t)
29-
opts.SpiceDBOptions.SpiceDBEndpoint = EmbeddedSpiceDBEndpoint
28+
opts := optionsForTesting(t, WithEmbeddedSpiceDBEndpoint)
3029
require.Empty(t, opts.Validate())
3130

3231
c, err := opts.Complete(context.Background())
@@ -46,8 +45,7 @@ func TestKubeConfig(t *testing.T) {
4645
func TestInClusterConfig(t *testing.T) {
4746
defer require.NoError(t, logsv1.ResetForTest(utilfeature.DefaultFeatureGate))
4847

49-
opts := optionsForTesting(t)
50-
opts.SpiceDBOptions.SpiceDBEndpoint = EmbeddedSpiceDBEndpoint
48+
opts := optionsForTesting(t, WithEmbeddedSpiceDBEndpoint)
5149
opts.BackendKubeconfigPath = ""
5250
opts.UseInClusterConfig = true
5351
require.Empty(t, opts.Validate())
@@ -62,8 +60,7 @@ func TestInClusterConfig(t *testing.T) {
6260
}
6361

6462
func TestEmbeddedSpiceDB(t *testing.T) {
65-
opts := optionsForTesting(t)
66-
opts.SpiceDBOptions.SpiceDBEndpoint = EmbeddedSpiceDBEndpoint
63+
opts := optionsForTesting(t, WithEmbeddedSpiceDBEndpoint)
6764
require.Empty(t, opts.Validate())
6865

6966
c, err := opts.Complete(context.Background())
@@ -116,8 +113,7 @@ func TestRemoteSpiceDBCerts(t *testing.T) {
116113
}
117114

118115
func TestRuleConfig(t *testing.T) {
119-
opts := optionsForTesting(t)
120-
opts.SpiceDBOptions.SpiceDBEndpoint = EmbeddedSpiceDBEndpoint
116+
opts := optionsForTesting(t, WithEmbeddedSpiceDBEndpoint)
121117
require.Empty(t, opts.Validate())
122118

123119
c, err := opts.Complete(context.Background())
@@ -139,7 +135,7 @@ func TestRuleConfig(t *testing.T) {
139135
errConfigBytes := []byte(`
140136
apiVersion: authzed.com/v1alpha1
141137
kind: ProxyRule
142-
lock: Pessimistic
138+
lock: Pessimistic
143139
match:
144140
- apiVersion: authzed.com/v1alpha1
145141
resource: spicedbclusters
@@ -151,26 +147,59 @@ prefilter:
151147
`)
152148
errConfigFile := path.Join(t.TempDir(), "rulesbad.yaml")
153149
require.NoError(t, os.WriteFile(errConfigFile, errConfigBytes, 0o600))
154-
opts = optionsForTesting(t)
155-
opts.SpiceDBOptions.SpiceDBEndpoint = EmbeddedSpiceDBEndpoint
150+
opts = optionsForTesting(t, WithEmbeddedSpiceDBEndpoint)
156151
opts.RuleConfigFile = errConfigFile
157152
require.Empty(t, opts.Validate())
158153

159154
_, err = opts.Complete(context.Background())
160155
require.ErrorContains(t, err, "expected")
161156
}
162157

163-
func optionsForTesting(t *testing.T) *Options {
158+
func optionsForTesting(t *testing.T, opts ...setOpt) *Options {
164159
t.Helper()
165160

166161
require.NoError(t, logsv1.ResetForTest(utilfeature.DefaultFeatureGate))
162+
options := NewOptions(opts...)
163+
options.SecureServing.BindPort = getFreePort(t, "127.0.0.1")
164+
options.SecureServing.BindAddress = net.ParseIP("127.0.0.1")
165+
options.BackendKubeconfigPath = kubeConfigForTest(t)
166+
options.RuleConfigFile = ruleConfigForTest(t)
167+
require.Empty(t, options.Validate())
168+
return options
169+
}
170+
171+
func TestWithEmbeddedProxy(t *testing.T) {
172+
opts := NewOptions(WithEmbeddedProxy)
173+
require.True(t, opts.EmbeddedMode)
174+
}
175+
176+
func TestWithEmbeddedSpiceDBEndpoint(t *testing.T) {
177+
opts := NewOptions(WithEmbeddedSpiceDBEndpoint)
178+
require.Equal(t, EmbeddedSpiceDBEndpoint, opts.SpiceDBOptions.SpiceDBEndpoint)
179+
}
180+
181+
func TestWithBothEmbeddedOptions(t *testing.T) {
182+
opts := NewOptions(WithEmbeddedProxy, WithEmbeddedSpiceDBEndpoint)
183+
require.True(t, opts.EmbeddedMode)
184+
require.Equal(t, EmbeddedSpiceDBEndpoint, opts.SpiceDBOptions.SpiceDBEndpoint)
185+
}
186+
187+
func TestWithEmbeddedProxyOnly(t *testing.T) {
188+
opts := NewOptions(WithEmbeddedProxy)
189+
require.True(t, opts.EmbeddedMode)
190+
require.NotEqual(t, EmbeddedSpiceDBEndpoint, opts.SpiceDBOptions.SpiceDBEndpoint)
191+
}
192+
193+
func TestWithEmbeddedSpiceDBEndpointOnly(t *testing.T) {
194+
opts := NewOptions(WithEmbeddedSpiceDBEndpoint)
195+
require.False(t, opts.EmbeddedMode)
196+
require.Equal(t, EmbeddedSpiceDBEndpoint, opts.SpiceDBOptions.SpiceDBEndpoint)
197+
}
198+
199+
func TestNewOptionsWithoutEmbedded(t *testing.T) {
167200
opts := NewOptions()
168-
opts.SecureServing.BindPort = getFreePort(t, "127.0.0.1")
169-
opts.SecureServing.BindAddress = net.ParseIP("127.0.0.1")
170-
opts.BackendKubeconfigPath = kubeConfigForTest(t)
171-
opts.RuleConfigFile = ruleConfigForTest(t)
172-
require.Empty(t, opts.Validate())
173-
return opts
201+
require.False(t, opts.EmbeddedMode)
202+
require.NotEqual(t, EmbeddedSpiceDBEndpoint, opts.SpiceDBOptions.SpiceDBEndpoint)
174203
}
175204

176205
func getFreePort(t *testing.T, listenAddr string) int {
@@ -238,10 +267,10 @@ func ruleConfigForTest(t *testing.T) string {
238267
configBytes := []byte(`
239268
apiVersion: authzed.com/v1alpha1
240269
kind: ProxyRule
241-
lock: Pessimistic
270+
lock: Pessimistic
242271
match:
243272
- apiVersion: authzed.com/v1alpha1
244-
resource: spicedbclusters
273+
resource: spicedbclusters
245274
verbs: ["list"]
246275
prefilter:
247276
- fromObjectIDNameExpr: "{{request.name}}"

0 commit comments

Comments
 (0)