Skip to content

Commit c5699dc

Browse files
committed
test/e2e: add virtualworkspace test
Signed-off-by: Dr. Stefan Schimanski <[email protected]>
1 parent 4961162 commit c5699dc

File tree

5 files changed

+249
-9
lines changed

5 files changed

+249
-9
lines changed

envtest/internal/controlplane/plane.go

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,8 @@ import (
2222

2323
kerrors "k8s.io/apimachinery/pkg/util/errors"
2424
"k8s.io/client-go/rest"
25-
26-
"github.com/kcp-dev/multicluster-provider/envtest/internal/certs"
2725
)
2826

29-
// NewTinyCA creates a new a tiny CA utility for provisioning serving certs and client certs FOR TESTING ONLY.
30-
// Don't use this for anything else!
31-
var NewTinyCA = certs.NewTinyCA
32-
3327
// Kcp is a struct that knows how to start your test kcp.
3428
//
3529
// Right now, that means one kcp shard. This is likely to increase in

envtest/internal/controlplane/shard.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@ func (s *Shard) defaultArgs() map[string][]string {
262262
"bind-address": {s.SecureServing.Address},
263263
"embedded-etcd-peer-port": {s.EmbeddedEtcd.PeerPort},
264264
"embedded-etcd-client-port": {s.EmbeddedEtcd.ClientPort},
265+
"external-hostname": {s.SecureServing.Address},
265266
}
266267
return args
267268
}

test/e2e/apiexport_test.go

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

test/e2e/suite_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,18 @@ import (
2121

2222
"github.com/stretchr/testify/require"
2323

24+
"k8s.io/apimachinery/pkg/util/runtime"
25+
"k8s.io/client-go/kubernetes/scheme"
2426
"k8s.io/client-go/rest"
2527
logf "sigs.k8s.io/controller-runtime/pkg/log"
2628
"sigs.k8s.io/controller-runtime/pkg/log/zap"
2729
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
2830

31+
apisv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1"
32+
corev1alpha1 "github.com/kcp-dev/kcp/sdk/apis/core/v1alpha1"
33+
tenancyv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1"
34+
topologyv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/topology/v1alpha1"
35+
2936
"github.com/kcp-dev/multicluster-provider/envtest"
3037

3138
. "github.com/onsi/ginkgo/v2"
@@ -37,6 +44,13 @@ var (
3744
kcpConfig *rest.Config
3845
)
3946

47+
func init() {
48+
runtime.Must(apisv1alpha1.AddToScheme(scheme.Scheme))
49+
runtime.Must(corev1alpha1.AddToScheme(scheme.Scheme))
50+
runtime.Must(tenancyv1alpha1.AddToScheme(scheme.Scheme))
51+
runtime.Must(topologyv1alpha1.AddToScheme(scheme.Scheme))
52+
}
53+
4054
func TestE2e(t *testing.T) {
4155
RegisterFailHandler(Fail)
4256

virtualworkspace/provider.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ func (p *Provider) Run(ctx context.Context, mgr mcmanager.Manager) error {
108108
// Watch logical clusters and engage them as clusters in multicluster-runtime.
109109
inf, err := p.cache.GetInformer(ctx, p.object, cache.BlockUntilSynced(false))
110110
if err != nil {
111-
return fmt.Errorf("failed to get logical cluster informer: %w", err)
111+
return fmt.Errorf("failed to get %T informer: %w", p.object, err)
112112
}
113113
shInf, _, _, err := p.cache.getSharedInformer(p.object)
114114
if err != nil {
@@ -206,8 +206,9 @@ func (p *Provider) Run(ctx context.Context, mgr mcmanager.Manager) error {
206206

207207
syncCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
208208
defer cancel()
209-
if !p.cache.WaitForCacheSync(syncCtx) {
210-
return fmt.Errorf("failed to sync wildcard cache")
209+
210+
if _, err := p.cache.GetInformer(syncCtx, p.object, cache.BlockUntilSynced(true)); err != nil {
211+
return fmt.Errorf("failed to sync %T informer: %w", p.object, err)
211212
}
212213

213214
return g.Wait()

0 commit comments

Comments
 (0)