Skip to content

Commit 652450e

Browse files
committed
add integration test for embedded mode
since the existing e2e suite starts up a shared server for testing, I added a separate test just to validate the embedded mode
1 parent 8ad90c0 commit 652450e

File tree

14 files changed

+440
-65
lines changed

14 files changed

+440
-65
lines changed

docs/embedding.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,8 @@ func main() {
6969
}
7070

7171
func createKubernetesClient(embeddedClient *http.Client) *kubernetes.Clientset {
72-
restConfig := &rest.Config{
73-
Host: "http://embedded", // Special URL for embedded mode
74-
Transport: embeddedClient.Transport,
75-
}
72+
restConfig := rest.CopyConfig(proxy.EmbeddedRestConfig)
73+
restConfig.Transport = embeddedClient.Transport
7674

7775
k8sClient, err := kubernetes.NewForConfig(restConfig)
7876
if err != nil {

e2e/e2e_test.go

Lines changed: 3 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,12 @@ import (
1919
"os"
2020
"path"
2121
"path/filepath"
22-
goruntime "runtime"
22+
2323
"testing"
2424
"time"
2525

2626
. "github.com/onsi/ginkgo/v2"
2727
. "github.com/onsi/gomega"
28-
"github.com/spf13/afero"
2928
"k8s.io/apimachinery/pkg/runtime/schema"
3029
"k8s.io/client-go/discovery/cached/disk"
3130
"k8s.io/client-go/informers"
@@ -40,11 +39,6 @@ import (
4039
"k8s.io/kubernetes/pkg/controller/garbagecollector"
4140
"sigs.k8s.io/controller-runtime/pkg/envtest"
4241
"sigs.k8s.io/controller-runtime/pkg/log/zap"
43-
"sigs.k8s.io/controller-runtime/tools/setup-envtest/env"
44-
"sigs.k8s.io/controller-runtime/tools/setup-envtest/remote"
45-
"sigs.k8s.io/controller-runtime/tools/setup-envtest/store"
46-
"sigs.k8s.io/controller-runtime/tools/setup-envtest/versions"
47-
"sigs.k8s.io/controller-runtime/tools/setup-envtest/workflows"
4842

4943
"github.com/authzed/spicedb-kubeapi-proxy/pkg/authz/distributedtx"
5044
"github.com/authzed/spicedb-kubeapi-proxy/pkg/proxy"
@@ -153,39 +147,9 @@ var _ = SynchronizedBeforeSuite(func() []byte {
153147
func ConfigureApiserver() {
154148
log := zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))
155149

156-
e := &env.Env{
157-
Log: log,
158-
Client: &remote.HTTPClient{
159-
Log: log,
160-
IndexURL: remote.DefaultIndexURL,
161-
},
162-
Version: versions.Spec{
163-
Selector: versions.TildeSelector{},
164-
CheckLatest: false,
165-
},
166-
VerifySum: true,
167-
ForceDownload: false,
168-
Platform: versions.PlatformItem{
169-
Platform: versions.Platform{
170-
OS: goruntime.GOOS,
171-
Arch: goruntime.GOARCH,
172-
},
173-
},
174-
FS: afero.Afero{Fs: afero.NewOsFs()},
175-
Store: store.NewAt("../testbin"),
176-
Out: os.Stdout,
177-
}
178-
var err error
179-
e.Version, err = versions.FromExpr("~1.33.0")
180-
Expect(err).To(Succeed())
181-
182-
workflows.Use{
183-
UseEnv: true,
184-
PrintFormat: env.PrintOverview,
185-
AssetsPath: "../testbin",
186-
}.Do(e)
150+
assetsPath := setupEnvtest(log)
187151

188-
Expect(os.Setenv("KUBEBUILDER_ASSETS", fmt.Sprintf("../testbin/k8s/%s-%s-%s", e.Version.AsConcrete(), e.Platform.OS, e.Platform.Arch))).To(Succeed())
152+
Expect(os.Setenv("KUBEBUILDER_ASSETS", assetsPath)).To(Succeed())
189153
DeferCleanup(os.Unsetenv, "KUBEBUILDER_ASSETS")
190154
}
191155

e2e/embedded_integration_test.go

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
//go:build e2e
2+
3+
package e2e
4+
5+
import (
6+
"context"
7+
"errors"
8+
"fmt"
9+
"net/http"
10+
"testing"
11+
"time"
12+
13+
"github.com/stretchr/testify/assert"
14+
"github.com/stretchr/testify/require"
15+
corev1 "k8s.io/api/core/v1"
16+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
17+
18+
utilfeature "k8s.io/apiserver/pkg/util/feature"
19+
"k8s.io/client-go/kubernetes"
20+
"k8s.io/client-go/rest"
21+
logsv1 "k8s.io/component-base/logs/api/v1"
22+
"sigs.k8s.io/controller-runtime/pkg/envtest"
23+
"sigs.k8s.io/controller-runtime/pkg/log/zap"
24+
25+
"github.com/authzed/spicedb-kubeapi-proxy/pkg/config/proxyrule"
26+
"github.com/authzed/spicedb-kubeapi-proxy/pkg/proxy"
27+
"github.com/authzed/spicedb-kubeapi-proxy/pkg/rules"
28+
)
29+
30+
// TestEmbeddedModeIntegration tests the full integration of the embedded proxy mode
31+
// with a real Kubernetes API server.
32+
// Note: this is separate from the e2e tests because the setup/teardown is much less
33+
// involved in embedded mode.
34+
func TestEmbeddedModeIntegration(t *testing.T) {
35+
defer require.NoError(t, logsv1.ResetForTest(utilfeature.DefaultFeatureGate))
36+
37+
ctx, cancel := context.WithCancel(context.Background())
38+
t.Cleanup(cancel)
39+
40+
// Configure the test environment to download binaries if needed
41+
configureApiserver(t)
42+
43+
// Start a real Kubernetes API server using envtest
44+
testEnv := &envtest.Environment{
45+
ControlPlaneStopTimeout: 60 * time.Second,
46+
}
47+
48+
cfg, err := testEnv.Start()
49+
require.NoError(t, err)
50+
t.Cleanup(func() {
51+
require.NoError(t, testEnv.Stop())
52+
})
53+
54+
// Create custom bootstrap content for the test
55+
bootstrapContent := map[string][]byte{
56+
"bootstrap.yaml": []byte(`schema: |-
57+
definition cluster {}
58+
definition user {}
59+
definition namespace {
60+
relation cluster: cluster
61+
relation creator: user
62+
relation viewer: user
63+
64+
permission admin = creator
65+
permission edit = creator
66+
permission view = viewer + creator
67+
permission no_one_at_all = nil
68+
}
69+
definition pod {
70+
relation namespace: namespace
71+
relation creator: user
72+
relation viewer: user
73+
permission edit = creator
74+
permission view = viewer + creator
75+
}
76+
definition testresource {
77+
relation namespace: namespace
78+
relation creator: user
79+
relation viewer: user
80+
permission edit = creator
81+
permission view = viewer + creator
82+
}
83+
definition lock {
84+
relation workflow: workflow
85+
}
86+
definition workflow {}
87+
relationships: |
88+
`),
89+
}
90+
91+
// Create embedded proxy options with embedded mode enabled and custom bootstrap
92+
opts := proxy.NewOptions(proxy.WithEmbeddedProxy, proxy.WithEmbeddedSpiceDBBootstrap(bootstrapContent))
93+
94+
// Configure to use the real test API server
95+
opts.RestConfigFunc = func() (*rest.Config, http.RoundTripper, error) {
96+
transport, err := rest.TransportFor(cfg)
97+
if err != nil {
98+
return nil, nil, err
99+
}
100+
// Make a copy to avoid modifying the original
101+
configCopy := rest.CopyConfig(cfg)
102+
return configCopy, transport, nil
103+
}
104+
105+
// Create simple rules for namespace operations
106+
createNamespaceRule := proxyrule.Config{
107+
Spec: proxyrule.Spec{
108+
Matches: []proxyrule.Match{{
109+
GroupVersion: "v1",
110+
Resource: "namespaces",
111+
Verbs: []string{"create"},
112+
}},
113+
Update: proxyrule.Update{
114+
CreateRelationships: []proxyrule.StringOrTemplate{{
115+
Template: "namespace:{{name}}#creator@user:{{user.name}}",
116+
}},
117+
},
118+
},
119+
}
120+
121+
getNamespaceRule := proxyrule.Config{
122+
Spec: proxyrule.Spec{
123+
Matches: []proxyrule.Match{{
124+
GroupVersion: "v1",
125+
Resource: "namespaces",
126+
Verbs: []string{"get"},
127+
}},
128+
Checks: []proxyrule.StringOrTemplate{{
129+
Template: "namespace:{{name}}#creator@user:{{user.name}}",
130+
}},
131+
},
132+
}
133+
134+
matcher, err := rules.NewMapMatcher([]proxyrule.Config{
135+
createNamespaceRule,
136+
getNamespaceRule,
137+
})
138+
require.NoError(t, err)
139+
opts.Matcher = matcher
140+
141+
// Complete the configuration
142+
completedConfig, err := opts.Complete(ctx)
143+
require.NoError(t, err)
144+
145+
// Create the embedded proxy server
146+
proxySrv, err := proxy.NewServer(ctx, completedConfig)
147+
require.NoError(t, err)
148+
149+
// Start the proxy server
150+
go func() {
151+
err := proxySrv.Run(ctx)
152+
if err != nil && !errors.Is(err, context.Canceled) {
153+
t.Errorf("Proxy server failed: %v", err)
154+
}
155+
}()
156+
157+
// Wait for the server to be ready
158+
require.Eventually(t, func() bool {
159+
httpClient := proxySrv.GetEmbeddedClient(
160+
proxy.WithUser("testuser"),
161+
proxy.WithGroups("users"),
162+
)
163+
require.NotNil(t, httpClient)
164+
165+
kubeClient, err := kubernetes.NewForConfigAndClient(proxy.EmbeddedRestConfig, httpClient)
166+
require.NoError(t, err)
167+
168+
// if ns create works, the server is ready
169+
nsName := "test-namespace-" + fmt.Sprint(time.Now().UnixNano())
170+
namespace := &corev1.Namespace{
171+
ObjectMeta: metav1.ObjectMeta{
172+
Name: nsName,
173+
},
174+
}
175+
_, err = kubeClient.CoreV1().Namespaces().Create(ctx, namespace, metav1.CreateOptions{})
176+
return err == nil
177+
}, 10*time.Second, 100*time.Millisecond)
178+
179+
t.Run("namespace creation works with proper authorization", func(t *testing.T) {
180+
// Get embedded client for testuser
181+
httpClient := proxySrv.GetEmbeddedClient(
182+
proxy.WithUser("testuser"),
183+
proxy.WithGroups("users"),
184+
)
185+
require.NotNil(t, httpClient)
186+
187+
// Create a Kubernetes client using the embedded HTTP client
188+
kubeClient, err := kubernetes.NewForConfigAndClient(proxy.EmbeddedRestConfig, httpClient)
189+
require.NoError(t, err)
190+
191+
// Create a test namespace
192+
nsName := "test-namespace-" + fmt.Sprint(time.Now().UnixNano())
193+
namespace := &corev1.Namespace{
194+
ObjectMeta: metav1.ObjectMeta{
195+
Name: nsName,
196+
},
197+
}
198+
199+
// This should work with proper authorization rules
200+
createdNs, err := kubeClient.CoreV1().Namespaces().Create(ctx, namespace, metav1.CreateOptions{})
201+
require.NoError(t, err)
202+
203+
// Verify namespace was created correctly
204+
require.Equal(t, nsName, createdNs.Name)
205+
require.NotEmpty(t, createdNs.ResourceVersion)
206+
207+
// user can get the namespace they created
208+
retrievedNs, err := kubeClient.CoreV1().Namespaces().Get(ctx, nsName, metav1.GetOptions{})
209+
require.NoError(t, err)
210+
require.Equal(t, nsName, retrievedNs.Name)
211+
})
212+
213+
t.Run("different users get different clients", func(t *testing.T) {
214+
// Get embedded clients for different users
215+
adminClient := proxySrv.GetEmbeddedClient(
216+
proxy.WithUser("admin"),
217+
proxy.WithGroups("system:masters"),
218+
)
219+
userClient := proxySrv.GetEmbeddedClient(
220+
proxy.WithUser("testuser"),
221+
proxy.WithGroups("developers"),
222+
)
223+
224+
require.NotNil(t, adminClient)
225+
require.NotNil(t, userClient)
226+
227+
// Clients should be different instances
228+
assert.NotEqual(t, adminClient, userClient)
229+
})
230+
231+
t.Run("unauthenticated requests are rejected", func(t *testing.T) {
232+
// Get embedded client without authentication
233+
unauthHTTPClient := proxySrv.GetEmbeddedClient()
234+
require.NotNil(t, unauthHTTPClient)
235+
236+
// Create a Kubernetes client using the unauthenticated HTTP client
237+
kubeClient, err := kubernetes.NewForConfigAndClient(proxy.EmbeddedRestConfig, unauthHTTPClient)
238+
require.NoError(t, err)
239+
240+
// Try to list namespaces - should be unauthorized
241+
_, err = kubeClient.CoreV1().Namespaces().List(ctx, metav1.ListOptions{})
242+
require.Error(t, err)
243+
assert.Contains(t, err.Error(), "Unauthorized")
244+
})
245+
}
246+
247+
// configureApiserver sets up the test environment binaries for envtest
248+
func configureApiserver(t *testing.T) {
249+
t.Helper()
250+
251+
// Create a logger compatible with setup-envtest
252+
log := zap.New(zap.UseDevMode(true))
253+
254+
// Use the shared setupEnvtest function
255+
assetsPath := setupEnvtest(log)
256+
257+
// Set the KUBEBUILDER_ASSETS environment variable
258+
t.Setenv("KUBEBUILDER_ASSETS", assetsPath)
259+
}

e2e/go.mod

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,17 @@ require (
1212
github.com/onsi/gomega v1.37.0
1313
github.com/samber/lo v1.50.0
1414
github.com/spf13/afero v1.14.0
15+
github.com/stretchr/testify v1.10.0
1516
k8s.io/api v0.33.1
16-
k8s.io/apimachinery v0.34.0-alpha.1
17+
k8s.io/apimachinery v0.34.0-alpha.2
1718
k8s.io/apiserver v0.33.1
1819
k8s.io/client-go v0.33.1
20+
k8s.io/component-base v0.33.1
1921
k8s.io/controller-manager v0.33.1
2022
k8s.io/kubernetes v1.33.1
2123
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397
2224
sigs.k8s.io/controller-runtime v0.21.0
23-
sigs.k8s.io/controller-runtime/tools/setup-envtest v0.0.0-20250617162058-15c5d6129278
25+
sigs.k8s.io/controller-runtime/tools/setup-envtest v0.0.0-20250708091927-252af6420feb
2426
)
2527

2628
require (
@@ -222,7 +224,6 @@ require (
222224
github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect
223225
github.com/stoewer/go-strcase v1.3.0 // indirect
224226
github.com/stretchr/objx v0.5.2 // indirect
225-
github.com/stretchr/testify v1.10.0 // indirect
226227
github.com/subosito/gotenv v1.6.0 // indirect
227228
github.com/tilinna/z85 v1.0.0 // indirect
228229
github.com/warpstreamlabs/bento v1.8.2 // indirect
@@ -283,14 +284,13 @@ require (
283284
k8s.io/apiextensions-apiserver v0.33.0 // indirect
284285
k8s.io/cloud-provider v0.33.0 // indirect
285286
k8s.io/cluster-bootstrap v0.0.0 // indirect
286-
k8s.io/component-base v0.33.1 // indirect
287287
k8s.io/component-helpers v0.33.1 // indirect
288288
k8s.io/csi-translation-lib v0.0.0 // indirect
289289
k8s.io/dynamic-resource-allocation v0.0.0 // indirect
290290
k8s.io/klog/v2 v2.130.1 // indirect
291291
k8s.io/kms v0.33.1 // indirect
292292
k8s.io/kube-controller-manager v0.0.0 // indirect
293-
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
293+
k8s.io/kube-openapi v0.0.0-20250610211856-8b98d1ed966a // indirect
294294
k8s.io/kubelet v0.33.1 // indirect
295295
k8s.io/mount-utils v0.0.0 // indirect
296296
k8s.io/pod-security-admission v0.0.0 // indirect

0 commit comments

Comments
 (0)