Skip to content

Commit 0b3c613

Browse files
committed
test/e2e: add virtualworkspace test
Signed-off-by: Dr. Stefan Schimanski <stefan.schimanski@gmail.com>
1 parent 4961162 commit 0b3c613

File tree

1 file changed

+219
-0
lines changed

1 file changed

+219
-0
lines changed

test/e2e/apiexport_test.go

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
/*
2+
Copyright 2025 The KCP 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+
"sync"
23+
"time"
24+
25+
"github.com/stretchr/testify/require"
26+
"golang.org/x/sync/errgroup"
27+
28+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
29+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
30+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
31+
"k8s.io/apimachinery/pkg/runtime"
32+
runtimeschema "k8s.io/apimachinery/pkg/runtime/schema"
33+
"k8s.io/apimachinery/pkg/util/sets"
34+
"k8s.io/apimachinery/pkg/util/wait"
35+
"k8s.io/client-go/rest"
36+
"sigs.k8s.io/controller-runtime/pkg/client"
37+
"sigs.k8s.io/controller-runtime/pkg/reconcile"
38+
"sigs.k8s.io/yaml"
39+
40+
apisv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1"
41+
"github.com/kcp-dev/kcp/sdk/apis/core"
42+
tenancyv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1"
43+
"github.com/kcp-dev/logicalcluster/v3"
44+
45+
mcbuilder "github.com/multicluster-runtime/multicluster-runtime/pkg/builder"
46+
mcmanager "github.com/multicluster-runtime/multicluster-runtime/pkg/manager"
47+
mcreconcile "github.com/multicluster-runtime/multicluster-runtime/pkg/reconcile"
48+
49+
clusterclient "github.com/kcp-dev/multicluster-provider/client"
50+
"github.com/kcp-dev/multicluster-provider/envtest"
51+
"github.com/kcp-dev/multicluster-provider/virtualworkspace"
52+
53+
. "github.com/onsi/ginkgo/v2"
54+
. "github.com/onsi/gomega"
55+
)
56+
57+
var _ = Describe("VirtualWorkspace Provider", Ordered, func() {
58+
var (
59+
cli clusterclient.ClusterClient
60+
provider, consumer logicalcluster.Path
61+
consumerWS *tenancyv1alpha1.Workspace
62+
mgr mcmanager.Manager
63+
)
64+
65+
BeforeAll(func(ctx context.Context) {
66+
var err error
67+
cli, err = clusterclient.New(kcpConfig, client.Options{})
68+
Expect(err).NotTo(HaveOccurred())
69+
70+
_, provider = envtest.NewWorkspaceFixture(GinkgoT(), cli, core.RootCluster.Path(), envtest.WithNamePrefix("provider"))
71+
consumerWS, consumer = envtest.NewWorkspaceFixture(GinkgoT(), cli, core.RootCluster.Path(), envtest.WithNamePrefix("consumer"))
72+
73+
By(fmt.Sprintf("creating a schema in the provider workspace %q", provider))
74+
schema := &apisv1alpha1.APIResourceSchema{
75+
ObjectMeta: metav1.ObjectMeta{
76+
Name: "v20250317.things.example.com",
77+
},
78+
Spec: apisv1alpha1.APIResourceSchemaSpec{
79+
Group: "example.com",
80+
Names: apiextensionsv1.CustomResourceDefinitionNames{
81+
Kind: "Thing",
82+
ListKind: "ThingList",
83+
Plural: "things",
84+
Singular: "thing",
85+
},
86+
Scope: apiextensionsv1.ClusterScoped,
87+
Versions: []apisv1alpha1.APIResourceVersion{{
88+
Name: "v1",
89+
Schema: runtime.RawExtension{
90+
Raw: []byte(`{"type":"object","properties":{"spec":{"type":"object","properties":{"message":{"type":"string"}}}}}`),
91+
},
92+
}},
93+
},
94+
}
95+
err = cli.Cluster(provider).Create(ctx, schema)
96+
Expect(err).NotTo(HaveOccurred())
97+
98+
By(fmt.Sprintf("creating an APIExport in the provider workspace %q", provider))
99+
export := &apisv1alpha1.APIExport{
100+
ObjectMeta: metav1.ObjectMeta{
101+
Name: "example.com",
102+
},
103+
Spec: apisv1alpha1.APIExportSpec{
104+
LatestResourceSchemas: []string{schema.Name},
105+
},
106+
}
107+
err = cli.Cluster(provider).Create(ctx, export)
108+
Expect(err).NotTo(HaveOccurred())
109+
110+
By(fmt.Sprintf("creating an APIExportEndpointSlice in the provider workspace %q", provider))
111+
endpoitns := &apisv1alpha1.APIExportEndpointSlice{
112+
ObjectMeta: metav1.ObjectMeta{
113+
Name: "example.com",
114+
},
115+
Spec: apisv1alpha1.APIExportEndpointSliceSpec{
116+
APIExport: apisv1alpha1.ExportBindingReference{
117+
Path: provider.String(),
118+
Name: export.Name,
119+
},
120+
},
121+
}
122+
err = cli.Cluster(provider).Create(ctx, endpoitns)
123+
Expect(err).NotTo(HaveOccurred())
124+
125+
By(fmt.Sprintf("creating an APIBinding in the consumer workspace %q", consumer))
126+
binding := &apisv1alpha1.APIBinding{
127+
ObjectMeta: metav1.ObjectMeta{
128+
Name: "example.com",
129+
},
130+
Spec: apisv1alpha1.APIBindingSpec{
131+
Reference: apisv1alpha1.BindingReference{
132+
Export: &apisv1alpha1.ExportBindingReference{
133+
Path: provider.String(),
134+
Name: export.Name,
135+
},
136+
},
137+
},
138+
}
139+
err = cli.Cluster(consumer).Create(ctx, binding)
140+
Expect(err).NotTo(HaveOccurred())
141+
142+
By(fmt.Sprintf("waiting until the APIExportEndpointSlice in the provider workspace %q to have endpoints", provider))
143+
envtest.Eventually(GinkgoT(), func() (bool, string) {
144+
endpoints := &apisv1alpha1.APIExportEndpointSlice{}
145+
err := cli.Cluster(provider).Get(ctx, client.ObjectKey{Name: "example.com"}, endpoints)
146+
if err != nil {
147+
return false, fmt.Sprintf("failed to get APIExportEndpointSlice in %s: %v", provider, err)
148+
}
149+
return len(endpoints.Status.APIExportEndpoints) > 0, toYAML(GinkgoT(), endpoints)
150+
}, wait.ForeverTestTimeout, time.Millisecond*100, "failed to see endpoints in APIExportEndpointSlice in %s", provider)
151+
152+
By(fmt.Sprintf("waiting until 'things' can be accessed in the consumer workspace %q", consumer))
153+
envtest.Eventually(GinkgoT(), func() (bool, string) {
154+
things := &unstructured.UnstructuredList{}
155+
things.SetGroupVersionKind(runtimeschema.GroupVersionKind{
156+
Group: "example.com",
157+
Version: "v1",
158+
Kind: "ThingList",
159+
})
160+
err := cli.Cluster(consumer).List(ctx, things)
161+
return err == nil, fmt.Sprintf("failed to access 'things' in %s: %v", consumer, err)
162+
}, wait.ForeverTestTimeout, time.Millisecond*100, "failed to access 'things' in %s", consumer)
163+
164+
By("waiting for the APIExport to have an endpoint")
165+
})
166+
167+
Describe("With a multicluster provider and manager", func(ctx context.Context) {
168+
var lock sync.RWMutex
169+
engaged := sets.NewString()
170+
171+
It("can start a multicluster manager", func() {
172+
p, err := virtualworkspace.New(kcpConfig, &apisv1alpha1.APIBinding{}, virtualworkspace.Options{})
173+
Expect(err).NotTo(HaveOccurred())
174+
175+
By("creating a manager against the provider workspace as host")
176+
rootConfig := rest.CopyConfig(kcpConfig)
177+
rootConfig.Host += provider.RequestPath()
178+
mgr, err = mcmanager.New(rootConfig, p, mcmanager.Options{})
179+
Expect(err).NotTo(HaveOccurred())
180+
181+
By("creating a reconciler for the APIBinding")
182+
err = mcbuilder.ControllerManagedBy(mgr).
183+
Named("things").
184+
For(&apisv1alpha1.APIBinding{}).
185+
Complete(mcreconcile.Func(func(ctx context.Context, request mcreconcile.Request) (reconcile.Result, error) {
186+
lock.Lock()
187+
defer lock.Unlock()
188+
engaged.Insert(request.ClusterName)
189+
return reconcile.Result{}, nil
190+
}))
191+
Expect(err).NotTo(HaveOccurred())
192+
193+
By("starting the provider and manager")
194+
g, ctx := errgroup.WithContext(ctx)
195+
g.Go(func() error {
196+
return p.Run(ctx, mgr)
197+
})
198+
g.Go(func() error {
199+
return mgr.Start(ctx)
200+
})
201+
err = g.Wait()
202+
Expect(err).NotTo(HaveOccurred())
203+
})
204+
205+
It("sees the consumer workspace %q as a cluster", func() {
206+
envtest.Eventually(GinkgoT(), func() (bool, string) {
207+
lock.RLock()
208+
defer lock.RUnlock()
209+
return engaged.Has(consumerWS.Spec.Cluster), fmt.Sprintf("failed to see the consumer workspace %q as a cluster: %v", consumerWS.Spec.Cluster, engaged.List())
210+
}, wait.ForeverTestTimeout, time.Millisecond*100, "failed to see the consumer workspace %q as a cluster", consumer)
211+
})
212+
})
213+
})
214+
215+
func toYAML(t require.TestingT, x any) string {
216+
y, err := yaml.Marshal(x)
217+
require.NoError(t, err)
218+
return string(y)
219+
}

0 commit comments

Comments
 (0)