Skip to content

Commit 3e96557

Browse files
committed
Add kcp e2e test
Signed-off-by: Nelo-T. Wallus <red.brush9525@fastmail.com> Signed-off-by: Nelo-T. Wallus <n.wallus@sap.com>
1 parent 8a2f10f commit 3e96557

5 files changed

Lines changed: 702 additions & 3 deletions

File tree

contrib/kcp/test/e2e/backend.go

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
/*
2+
Copyright 2025 The Kube Bind Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package e2e
18+
19+
import (
20+
"encoding/base64"
21+
"fmt"
22+
"net"
23+
"net/http"
24+
"os/exec"
25+
"testing"
26+
"time"
27+
28+
"github.com/gorilla/securecookie"
29+
kcpclientset "github.com/kcp-dev/kcp/sdk/client/clientset/versioned/cluster"
30+
kcptestinghelpers "github.com/kcp-dev/kcp/sdk/testing/helpers"
31+
kcptestingserver "github.com/kcp-dev/kcp/sdk/testing/server"
32+
"github.com/kcp-dev/logicalcluster/v3"
33+
"github.com/spf13/pflag"
34+
"github.com/stretchr/testify/assert"
35+
"github.com/stretchr/testify/require"
36+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
37+
"k8s.io/apimachinery/pkg/util/wait"
38+
39+
"github.com/kube-bind/kube-bind/backend"
40+
"github.com/kube-bind/kube-bind/backend/options"
41+
kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2"
42+
"github.com/kube-bind/kube-bind/test/e2e/framework"
43+
)
44+
45+
// This is just temporary as inprocess breaks probably due to dependency
46+
// issues. Prebuilt works with the pinned forks.
47+
const backendType = "prebuilt"
48+
49+
// startBackend is a copy of framework.StartBackend but skips the CRDs
50+
// (which clashes with the APIResourceSchemas installed by kcp-init.
51+
func startBackend(t *testing.T, args ...string) (string, *backend.Server) {
52+
signingKey := securecookie.GenerateRandomKey(32)
53+
require.NotEmpty(t, signingKey, "error creating signing key")
54+
encryptionKey := securecookie.GenerateRandomKey(32)
55+
require.NotEmpty(t, encryptionKey, "error creating encryption key")
56+
57+
args = append(
58+
[]string{
59+
"--oidc-issuer-url=http://127.0.0.1:5556/dex",
60+
"--cookie-signing-key=" + base64.StdEncoding.EncodeToString(signingKey),
61+
"--cookie-encryption-key=" + base64.StdEncoding.EncodeToString(encryptionKey),
62+
},
63+
args...,
64+
)
65+
66+
switch backendType {
67+
case "prebuilt":
68+
// Acquire a random free port
69+
listener, err := net.Listen("tcp", "localhost:0")
70+
require.NoError(t, err)
71+
addr := listener.Addr()
72+
require.NoError(t, listener.Close())
73+
74+
// Register client with dex
75+
dexId, dexSecret := framework.CreateDexClient(t, addr)
76+
77+
args = append(args,
78+
"--listen-address="+addr.String(),
79+
"--oidc-issuer-client-id="+dexId,
80+
"--oidc-issuer-client-secret="+dexSecret,
81+
"--oidc-callback-url=http://"+addr.String()+"/callback",
82+
)
83+
backendCmd := exec.CommandContext(t.Context(),
84+
"../../../../bin/backend",
85+
args...,
86+
)
87+
backendCmd.Stdout = newLogWriter("[backend stdout] ", t)
88+
backendCmd.Stderr = newLogWriter("[backend stderr] ", t)
89+
require.NoError(t, backendCmd.Start())
90+
t.Cleanup(func() {
91+
if backendCmd.Process != nil {
92+
t.Logf("Stopping backend (PID: %d)", backendCmd.Process.Pid)
93+
assert.NoError(t, backendCmd.Process.Kill())
94+
}
95+
})
96+
return addr.String(), nil
97+
case "inprocess":
98+
fs := pflag.NewFlagSet("backend", pflag.ContinueOnError)
99+
opts := options.NewOptions()
100+
opts.AddFlags(fs)
101+
err := fs.Parse(args)
102+
require.NoError(t, err)
103+
104+
t.Logf("starting backend with options: %#v", opts)
105+
106+
// use a random port via an explicit listener. Then add a kube-bind-<port> client to dex
107+
// with the callback URL set to the listener's address.
108+
opts.Serve.Listener, err = net.Listen("tcp", "localhost:0")
109+
require.NoError(t, err)
110+
addr := opts.Serve.Listener.Addr()
111+
112+
dexId, dexSecret := framework.CreateDexClient(t, addr)
113+
opts.OIDC.IssuerClientID = dexId
114+
opts.OIDC.IssuerClientSecret = dexSecret
115+
116+
opts.ExtraOptions.TestingSkipNameValidation = true
117+
opts.ExtraOptions.SchemaSource = options.CustomResourceDefinitionSource.String()
118+
119+
completed, err := opts.Complete()
120+
require.NoError(t, err)
121+
122+
config, err := backend.NewConfig(completed)
123+
require.NoError(t, err)
124+
125+
server, err := backend.NewServer(t.Context(), config)
126+
require.NoError(t, err)
127+
128+
err = server.Run(t.Context())
129+
require.NoError(t, err)
130+
t.Logf("backend listening on %s", addr)
131+
132+
return addr.String(), server
133+
default:
134+
require.Fail(t, "unknown backend type %q", backendType)
135+
return "", nil
136+
}
137+
}
138+
139+
func bootstrapBackend(t *testing.T, server kcptestingserver.RunningServer, scope kubebindv1alpha2.InformerScope) string {
140+
t.Helper()
141+
t.Log("Bootstrapping backend")
142+
143+
client, err := kcpclientset.NewForConfig(server.BaseConfig(t))
144+
require.NoError(t, err)
145+
146+
exportUrl := ""
147+
kcptestinghelpers.Eventually(t, func() (bool, string) {
148+
exportES, err := client.Cluster(logicalcluster.NewPath("root").Join("kube-bind")).
149+
ApisV1alpha1().
150+
APIExportEndpointSlices().
151+
Get(t.Context(), "kube-bind.io", metav1.GetOptions{})
152+
if err != nil {
153+
return false, fmt.Sprintf("Error getting APIExportEndpointSlice: %v", err)
154+
}
155+
if len(exportES.Status.APIExportEndpoints) == 0 {
156+
return false, "APIExportEndpoints is empty"
157+
}
158+
exportUrl = exportES.Status.APIExportEndpoints[0].URL
159+
return true, ""
160+
}, wait.ForeverTestTimeout, time.Millisecond*100)
161+
require.NotEmpty(t, exportUrl, "APIExportEndpointSlice URL is empty")
162+
163+
_, backendKubeconfig := wsConfig(t, server, logicalcluster.NewPath("root").Join("kube-bind"))
164+
165+
t.Log("Starting kube-bind backend for KCP")
166+
addr, _ := startBackend(t,
167+
"--kubeconfig="+backendKubeconfig,
168+
"--multicluster-runtime-provider=kcp",
169+
"--server-url="+exportUrl,
170+
"--pretty-name=BigCorp.com",
171+
"--namespace-prefix=kube-bind-",
172+
"--schema-source=apiresourceschemas",
173+
"--consumer-scope="+string(scope),
174+
)
175+
176+
t.Log("Wait for backend to be ready")
177+
req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, "http://"+addr+"/healthz", nil)
178+
require.NoError(t, err)
179+
kcptestinghelpers.Eventually(t, func() (bool, string) {
180+
resp, err := http.DefaultClient.Do(req)
181+
if err != nil {
182+
return false, ""
183+
}
184+
defer resp.Body.Close()
185+
return resp.StatusCode == http.StatusOK, ""
186+
}, wait.ForeverTestTimeout, time.Millisecond*100)
187+
t.Log("Backend is ready")
188+
189+
return addr
190+
}

contrib/kcp/test/e2e/browser.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
Copyright 2025 The Kube Bind Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package e2e
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"testing"
23+
"time"
24+
25+
"github.com/headzoo/surf"
26+
"github.com/stretchr/testify/require"
27+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
28+
"k8s.io/apimachinery/pkg/util/wait"
29+
"k8s.io/cli-runtime/pkg/genericclioptions"
30+
"k8s.io/client-go/rest"
31+
"sigs.k8s.io/yaml"
32+
33+
"github.com/kube-bind/kube-bind/test/e2e/framework"
34+
)
35+
36+
func performBindingWithBrowser(t *testing.T, backendAddr string, clusterID string, consumerCfg *rest.Config, consumerKubeconfig, resource string) {
37+
bindURL := fmt.Sprintf("http://%s/clusters/%s/exports", backendAddr, clusterID)
38+
t.Logf("Bind URL: %s", bindURL)
39+
40+
// Test binding dry run first (similar to happy-case test)
41+
t.Run("Service is bound dry run", func(t *testing.T) {
42+
authURLDryRunCh := make(chan string, 1)
43+
go simulateKCPBrowser(t, authURLDryRunCh, resource)
44+
45+
iostreams, _, bufOut, _ := genericclioptions.NewTestIOStreams()
46+
framework.Bind(t, iostreams, authURLDryRunCh, nil, bindURL, "--kubeconfig", consumerKubeconfig, "--dry-run")
47+
_, err := yaml.YAMLToJSON(bufOut.Bytes())
48+
require.NoError(t, err, "Generated output is not valid YAML")
49+
})
50+
51+
// Perform actual binding (similar to happy-case test)
52+
t.Run("Service is bound", func(t *testing.T) {
53+
authURLCh := make(chan string, 1)
54+
go simulateKCPBrowser(t, authURLCh, resource)
55+
56+
iostreams, _, _, _ := genericclioptions.NewTestIOStreams()
57+
invocations := make(chan framework.SubCommandInvocation, 1)
58+
framework.Bind(t, iostreams, authURLCh, invocations, bindURL, "--kubeconfig", consumerKubeconfig)
59+
inv := <-invocations
60+
61+
inv.Args = append(
62+
inv.Args,
63+
"--kubeconfig="+consumerKubeconfig,
64+
"--skip-konnector=true",
65+
"--no-banner",
66+
"-f=-", // api service export from stdin
67+
)
68+
69+
framework.BindAPIService(t, inv.Stdin, "", inv.Args...)
70+
71+
// Wait for CRD to be created on consumer side
72+
t.Logf("Waiting for %s CRD to be created on consumer side", resource)
73+
crdClient := framework.ApiextensionsClient(t, consumerCfg).ApiextensionsV1().CustomResourceDefinitions()
74+
require.Eventually(t, func() bool {
75+
_, err := crdClient.Get(context.Background(), resource+".wildwest.dev", metav1.GetOptions{})
76+
return err == nil
77+
}, wait.ForeverTestTimeout, time.Millisecond*100)
78+
})
79+
}
80+
81+
// simulateKCPBrowser simulates browser interaction for KCP binding.
82+
func simulateKCPBrowser(t *testing.T, authURLCh chan string, resource string) {
83+
browser := surf.NewBrowser()
84+
authURL := <-authURLCh
85+
86+
t.Logf("Browsing to auth URL: %s", authURL)
87+
err := browser.Open(authURL)
88+
require.NoError(t, err, "Failed to open auth URL")
89+
90+
t.Logf("Waiting for browser to be at /resources")
91+
framework.BrowserEventuallyAtPath(t, browser, "/resources")
92+
93+
t.Logf("Clicking %s resource", resource)
94+
err = browser.Click("a." + resource)
95+
require.NoError(t, err, "Failed to click resource link")
96+
97+
t.Logf("Waiting for browser to be forwarded to client")
98+
framework.BrowserEventuallyAtPath(t, browser, "/callback")
99+
}

0 commit comments

Comments
 (0)