Skip to content

Commit be4e966

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 558f658 commit be4e966

5 files changed

Lines changed: 662 additions & 3 deletions

File tree

contrib/kcp/test/e2e/backend.go

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
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+
"github.com/kube-bind/kube-bind/test/e2e/framework"
42+
)
43+
44+
// This is just temporary as inprocess breaks probably due to dependency
45+
// issues. Prebuilt works with the pinned forks.
46+
const backendType = "prebuilt"
47+
48+
// startBackend is a copy of framework.StartBackend but skips the CRDs
49+
// (which clashes with the APIResourceSchemas installed by kcp-init.
50+
func startBackend(t *testing.T, args ...string) (string, *backend.Server) {
51+
signingKey := securecookie.GenerateRandomKey(32)
52+
require.NotEmpty(t, signingKey, "error creating signing key")
53+
encryptionKey := securecookie.GenerateRandomKey(32)
54+
require.NotEmpty(t, encryptionKey, "error creating encryption key")
55+
56+
args = append(
57+
[]string{
58+
"--oidc-issuer-client-secret=ZXhhbXBsZS1hcHAtc2VjcmV0",
59+
"--oidc-issuer-client-id=kube-bind",
60+
"--oidc-issuer-url=http://127.0.0.1:5556/dex",
61+
"--cookie-signing-key=" + base64.StdEncoding.EncodeToString(signingKey),
62+
"--cookie-encryption-key=" + base64.StdEncoding.EncodeToString(encryptionKey),
63+
},
64+
args...,
65+
)
66+
67+
switch backendType {
68+
case "prebuilt":
69+
addr := "127.0.0.1:8080"
70+
args = append(args,
71+
"--listen-address="+addr,
72+
"--oidc-callback-url=http://"+addr+"/callback",
73+
)
74+
backendCmd := exec.CommandContext(t.Context(),
75+
"../../../../bin/backend",
76+
args...,
77+
)
78+
backendCmd.Stdout = newLogWriter("[backend stdout] ", t)
79+
backendCmd.Stderr = newLogWriter("[backend stderr] ", t)
80+
require.NoError(t, backendCmd.Start())
81+
t.Cleanup(func() {
82+
if backendCmd.Process != nil {
83+
t.Logf("Stopping dex (PID: %d)", backendCmd.Process.Pid)
84+
assert.NoError(t, backendCmd.Process.Kill())
85+
}
86+
})
87+
return addr, nil
88+
case "inprocess":
89+
args = append(
90+
[]string{
91+
"--oidc-issuer-client-secret=ZXhhbXBsZS1hcHAtc2VjcmV0",
92+
"--oidc-issuer-client-id=kube-bind",
93+
"--oidc-issuer-url=http://127.0.0.1:5556/dex",
94+
"--cookie-signing-key=" + base64.StdEncoding.EncodeToString(signingKey),
95+
"--cookie-encryption-key=" + base64.StdEncoding.EncodeToString(encryptionKey),
96+
},
97+
args...,
98+
)
99+
100+
fs := pflag.NewFlagSet("backend", pflag.ContinueOnError)
101+
opts := options.NewOptions()
102+
opts.AddFlags(fs)
103+
err := fs.Parse(args)
104+
require.NoError(t, err)
105+
106+
t.Logf("starting backend with options: %#v", opts)
107+
108+
// use a random port via an explicit listener. Then add a kube-bind-<port> client to dex
109+
// with the callback URL set to the listener's address.
110+
opts.Serve.Listener, err = net.Listen("tcp", "localhost:0")
111+
require.NoError(t, err)
112+
addr := opts.Serve.Listener.Addr()
113+
_, port, err := net.SplitHostPort(addr.String())
114+
require.NoError(t, err)
115+
116+
opts.OIDC.IssuerClientID = "kube-bind-" + port
117+
framework.CreateDexClient(t, addr)
118+
119+
opts.ExtraOptions.TestingSkipNameValidation = true
120+
opts.ExtraOptions.SchemaSource = options.CustomResourceDefinitionSource.String()
121+
122+
completed, err := opts.Complete()
123+
require.NoError(t, err)
124+
125+
config, err := backend.NewConfig(completed)
126+
require.NoError(t, err)
127+
128+
server, err := backend.NewServer(t.Context(), config)
129+
require.NoError(t, err)
130+
131+
err = server.Run(t.Context())
132+
require.NoError(t, err)
133+
t.Logf("backend listening on %s", addr)
134+
135+
return addr.String(), server
136+
default:
137+
require.Fail(t, "unknown backend type %q", backendType)
138+
return "", nil
139+
}
140+
}
141+
142+
func bootstrapBackend(t *testing.T, server kcptestingserver.RunningServer) string {
143+
t.Helper()
144+
t.Log("Bootstrapping backend")
145+
146+
client, err := kcpclientset.NewForConfig(server.BaseConfig(t))
147+
require.NoError(t, err)
148+
149+
exportUrl := ""
150+
kcptestinghelpers.Eventually(t, func() (bool, string) {
151+
exportES, err := client.Cluster(logicalcluster.NewPath("root").Join("kube-bind")).
152+
ApisV1alpha1().
153+
APIExportEndpointSlices().
154+
Get(t.Context(), "kube-bind.io", metav1.GetOptions{})
155+
if err != nil {
156+
return false, fmt.Sprintf("Error getting APIExportEndpointSlice: %v", err)
157+
}
158+
if len(exportES.Status.APIExportEndpoints) == 0 {
159+
return false, "APIExportEndpoints is empty"
160+
}
161+
exportUrl = exportES.Status.APIExportEndpoints[0].URL
162+
return true, ""
163+
}, wait.ForeverTestTimeout, time.Millisecond*100)
164+
require.NotEmpty(t, exportUrl, "APIExportEndpointSlice URL is empty")
165+
166+
_, backendKubeconfig := wsConfig(t, server, logicalcluster.NewPath("root").Join("kube-bind"))
167+
168+
t.Log("Starting kube-bind backend for KCP")
169+
addr, _ := startBackend(t,
170+
"--kubeconfig="+backendKubeconfig,
171+
"--multicluster-runtime-provider=kcp",
172+
"--server-url="+exportUrl,
173+
"--pretty-name=BigCorp.com",
174+
"--namespace-prefix=kube-bind-",
175+
"--schema-source=apiresourceschemas",
176+
"--consumer-scope=cluster", // TODO configure to test both modes
177+
)
178+
179+
t.Log("Wait for backend to be ready")
180+
req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, "http://"+addr+"/healthz", nil)
181+
require.NoError(t, err)
182+
kcptestinghelpers.Eventually(t, func() (bool, string) {
183+
resp, err := http.DefaultClient.Do(req)
184+
if err != nil {
185+
return false, ""
186+
}
187+
defer resp.Body.Close()
188+
return resp.StatusCode == http.StatusOK, ""
189+
}, wait.ForeverTestTimeout, time.Millisecond*100)
190+
t.Log("Backend is ready")
191+
192+
return addr
193+
}

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 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, "cowboys")
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, "cowboys")
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 cowboy CRD to be created on consumer side")
73+
crdClient := framework.ApiextensionsClient(t, consumerCfg).ApiextensionsV1().CustomResourceDefinitions()
74+
require.Eventually(t, func() bool {
75+
_, err := crdClient.Get(context.Background(), "cowboys.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)