Skip to content

Commit 7205cf6

Browse files
committed
providers/namespace: add test
Signed-off-by: Dr. Stefan Schimanski <[email protected]>
1 parent cddc819 commit 7205cf6

File tree

4 files changed

+310
-14
lines changed

4 files changed

+310
-14
lines changed

examples/namespace/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ func main() {
139139

140140
// Retrieve the service account from the namespace.
141141
cm := &corev1.ConfigMap{}
142-
if err := client.Get(ctx, req.Request.NamespacedName, cm); err != nil {
142+
if err := client.Get(ctx, req.NamespacedName, cm); err != nil {
143143
return reconcile.Result{}, err
144144
}
145145
log.Info("Reconciling configmap", "cluster", req.ClusterName, "ns", req.Request.Namespace, "name", cm.Name, "uuid", cm.UID)

providers/namespace/client.go

Lines changed: 94 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@ import (
2121

2222
apierrors "k8s.io/apimachinery/pkg/api/errors"
2323
"k8s.io/apimachinery/pkg/api/meta"
24+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2425
"k8s.io/apimachinery/pkg/runtime"
2526
"k8s.io/apimachinery/pkg/runtime/schema"
27+
"k8s.io/apimachinery/pkg/util/validation/field"
2628
"sigs.k8s.io/controller-runtime/pkg/client"
2729
)
2830

@@ -36,14 +38,16 @@ type NamespacedClient struct {
3638

3739
// Get returns a single object.
3840
func (n *NamespacedClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
39-
if key.Namespace != "default" {
40-
return apierrors.NewNotFound(schema.GroupResource{}, key.Name)
41+
if ns := key.Namespace; ns != metav1.NamespaceDefault {
42+
return apierrors.NewInvalid(obj.GetObjectKind().GroupVersionKind().GroupKind(), obj.GetName(),
43+
field.ErrorList{field.Invalid(field.NewPath("metadata", "namespace"), ns, "must be 'default'")})
4144
}
4245
key.Namespace = n.clusterName
43-
if err := n.Client.Get(ctx, key, obj, opts...); err != nil {
46+
err := n.Client.Get(ctx, key, obj, opts...)
47+
if err != nil {
4448
return err
4549
}
46-
obj.SetNamespace("default")
50+
obj.SetNamespace(metav1.NamespaceDefault)
4751
return nil
4852
}
4953

@@ -53,49 +57,126 @@ func (n *NamespacedClient) List(ctx context.Context, list client.ObjectList, opt
5357
for _, o := range opts {
5458
o.ApplyToList(&copts)
5559
}
56-
if copts.Namespace != "default" {
60+
if copts.Namespace != metav1.NamespaceDefault {
5761
return apierrors.NewNotFound(schema.GroupResource{}, copts.Namespace)
5862
}
5963
if err := n.Client.List(ctx, list, append(opts, client.InNamespace(n.clusterName))...); err != nil {
6064
return err
6165
}
6266
return meta.EachListItem(list, func(obj runtime.Object) error {
63-
obj.(client.Object).SetNamespace("default")
67+
obj.(client.Object).SetNamespace(metav1.NamespaceDefault)
6468
return nil
6569
})
6670
}
6771

6872
// Create creates a new object.
6973
func (n *NamespacedClient) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error {
70-
panic("implement me")
74+
if ns := obj.GetNamespace(); ns != metav1.NamespaceDefault {
75+
return apierrors.NewInvalid(obj.GetObjectKind().GroupVersionKind().GroupKind(), obj.GetName(),
76+
field.ErrorList{field.Invalid(field.NewPath("metadata", "namespace"), ns, "must be 'default'")})
77+
}
78+
obj.SetNamespace(n.clusterName)
79+
defer obj.SetNamespace(metav1.NamespaceDefault)
80+
return n.Client.Create(ctx, obj, opts...)
7181
}
7282

7383
// Delete deletes an object.
7484
func (n *NamespacedClient) Delete(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error {
75-
panic("implement me")
85+
if ns := obj.GetNamespace(); ns != metav1.NamespaceDefault {
86+
return apierrors.NewInvalid(obj.GetObjectKind().GroupVersionKind().GroupKind(), obj.GetName(),
87+
field.ErrorList{field.Invalid(field.NewPath("metadata", "namespace"), ns, "must be 'default'")})
88+
}
89+
obj.SetNamespace(n.clusterName)
90+
defer obj.SetNamespace(metav1.NamespaceDefault)
91+
return n.Client.Delete(ctx, obj, opts...)
7692
}
7793

7894
// Update updates an object.
7995
func (n *NamespacedClient) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error {
80-
panic("implement me")
96+
if ns := obj.GetNamespace(); ns != metav1.NamespaceDefault {
97+
return apierrors.NewInvalid(obj.GetObjectKind().GroupVersionKind().GroupKind(), obj.GetName(),
98+
field.ErrorList{field.Invalid(field.NewPath("metadata", "namespace"), ns, "must be 'default'")})
99+
}
100+
obj.SetNamespace(n.clusterName)
101+
defer obj.SetNamespace(metav1.NamespaceDefault)
102+
return n.Client.Update(ctx, obj, opts...)
81103
}
82104

83105
// Patch patches an object.
84106
func (n *NamespacedClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error {
85-
panic("implement me")
107+
// TODO(sttts): this is not thas easy here. We likely have to support all the different patch types.
108+
// But other than that, this is just an example provider, so ¯\_(ツ)_/¯.
109+
panic("implement the three patch types")
86110
}
87111

88112
// DeleteAllOf deletes all objects of the given type.
89113
func (n *NamespacedClient) DeleteAllOf(ctx context.Context, obj client.Object, opts ...client.DeleteAllOfOption) error {
90-
panic("implement me")
114+
if ns := obj.GetNamespace(); ns != metav1.NamespaceDefault {
115+
return apierrors.NewInvalid(obj.GetObjectKind().GroupVersionKind().GroupKind(), obj.GetName(),
116+
field.ErrorList{field.Invalid(field.NewPath("metadata", "namespace"), ns, "must be 'default'")})
117+
}
118+
obj.SetNamespace(n.clusterName)
119+
defer obj.SetNamespace(metav1.NamespaceDefault)
120+
return n.Client.DeleteAllOf(ctx, obj, opts...)
91121
}
92122

93123
// Status returns a subresource writer.
94124
func (n *NamespacedClient) Status() client.SubResourceWriter {
95-
panic("implement me")
125+
return &SubResourceNamespacedClient{clusterName: n.clusterName, client: n.Client.SubResource("status")}
96126
}
97127

98128
// SubResource returns a subresource client.
99129
func (n *NamespacedClient) SubResource(subResource string) client.SubResourceClient {
100-
panic("implement me")
130+
return &SubResourceNamespacedClient{clusterName: n.clusterName, client: n.Client.SubResource(subResource)}
131+
}
132+
133+
var _ client.SubResourceClient = &SubResourceNamespacedClient{}
134+
135+
// SubResourceNamespacedClient is a client that operates on a specific namespace
136+
// and subresource.
137+
type SubResourceNamespacedClient struct {
138+
clusterName string
139+
client client.SubResourceClient
140+
}
141+
142+
// Get returns a single object from a subresource.
143+
func (s SubResourceNamespacedClient) Get(ctx context.Context, obj client.Object, subResource client.Object, opts ...client.SubResourceGetOption) error {
144+
if ns := obj.GetNamespace(); ns != metav1.NamespaceDefault {
145+
return apierrors.NewInvalid(obj.GetObjectKind().GroupVersionKind().GroupKind(), obj.GetName(),
146+
field.ErrorList{field.Invalid(field.NewPath("metadata", "namespace"), ns, "must be 'default'")})
147+
}
148+
obj.SetNamespace(s.clusterName)
149+
defer obj.SetNamespace(metav1.NamespaceDefault)
150+
defer subResource.SetNamespace(metav1.NamespaceDefault)
151+
return s.client.Get(ctx, obj, subResource, opts...)
152+
}
153+
154+
// Create creates a new object in a subresource.
155+
func (s SubResourceNamespacedClient) Create(ctx context.Context, obj client.Object, subResource client.Object, opts ...client.SubResourceCreateOption) error {
156+
if ns := obj.GetNamespace(); ns != metav1.NamespaceDefault {
157+
return apierrors.NewInvalid(obj.GetObjectKind().GroupVersionKind().GroupKind(), obj.GetName(),
158+
field.ErrorList{field.Invalid(field.NewPath("metadata", "namespace"), ns, "must be 'default'")})
159+
}
160+
obj.SetNamespace(s.clusterName)
161+
defer obj.SetNamespace(metav1.NamespaceDefault)
162+
defer subResource.SetNamespace(metav1.NamespaceDefault)
163+
return s.client.Create(ctx, obj, subResource, opts...)
164+
}
165+
166+
// Update updates an object in a subresource.
167+
func (s SubResourceNamespacedClient) Update(ctx context.Context, obj client.Object, opts ...client.SubResourceUpdateOption) error {
168+
if ns := obj.GetNamespace(); ns != metav1.NamespaceDefault {
169+
return apierrors.NewInvalid(obj.GetObjectKind().GroupVersionKind().GroupKind(), obj.GetName(),
170+
field.ErrorList{field.Invalid(field.NewPath("metadata", "namespace"), ns, "must be 'default'")})
171+
}
172+
obj.SetNamespace(s.clusterName)
173+
defer obj.SetNamespace(metav1.NamespaceDefault)
174+
return s.client.Update(ctx, obj, opts...)
175+
}
176+
177+
// Patch patches an object in a subresource.
178+
func (s SubResourceNamespacedClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.SubResourcePatchOption) error {
179+
// TODO(sttts): this is not thas easy here. We likely have to support all the different patch types.
180+
// But other than that, this is just an example provider, so ¯\_(ツ)_/¯.
181+
panic("implement the three patch types")
101182
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
Copyright 2025 The Kubernetes 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 namespace
18+
19+
import (
20+
"testing"
21+
22+
. "github.com/onsi/ginkgo/v2"
23+
. "github.com/onsi/gomega"
24+
"k8s.io/client-go/rest"
25+
26+
"sigs.k8s.io/controller-runtime/pkg/envtest"
27+
logf "sigs.k8s.io/controller-runtime/pkg/log"
28+
"sigs.k8s.io/controller-runtime/pkg/log/zap"
29+
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
30+
)
31+
32+
func TestBuilder(t *testing.T) {
33+
RegisterFailHandler(Fail)
34+
RunSpecs(t, "Namespace Provider Suite")
35+
}
36+
37+
var testenv *envtest.Environment
38+
var cfg *rest.Config
39+
40+
var _ = BeforeSuite(func() {
41+
logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
42+
43+
testenv = &envtest.Environment{}
44+
45+
var err error
46+
cfg, err = testenv.Start()
47+
Expect(err).NotTo(HaveOccurred())
48+
49+
// Prevent the metrics listener being created
50+
metricsserver.DefaultBindAddress = "0"
51+
})
52+
53+
var _ = AfterSuite(func() {
54+
if testenv != nil {
55+
Expect(testenv.Stop()).To(Succeed())
56+
}
57+
58+
// Put the DefaultBindAddress back
59+
metricsserver.DefaultBindAddress = ":8080"
60+
})

providers/namespace/provider_test.go

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/*
2+
Copyright 2025 The Kubernetes 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 namespace
18+
19+
import (
20+
"context"
21+
"errors"
22+
23+
. "github.com/onsi/ginkgo/v2"
24+
. "github.com/onsi/gomega"
25+
"golang.org/x/sync/errgroup"
26+
27+
corev1 "k8s.io/api/core/v1"
28+
apierrors "k8s.io/apimachinery/pkg/api/errors"
29+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
30+
"k8s.io/apimachinery/pkg/labels"
31+
"k8s.io/apimachinery/pkg/util/runtime"
32+
"k8s.io/client-go/rest"
33+
34+
ctrl "sigs.k8s.io/controller-runtime"
35+
"sigs.k8s.io/controller-runtime/pkg/cache"
36+
"sigs.k8s.io/controller-runtime/pkg/client"
37+
"sigs.k8s.io/controller-runtime/pkg/cluster"
38+
"sigs.k8s.io/controller-runtime/pkg/log"
39+
"sigs.k8s.io/controller-runtime/pkg/manager"
40+
"sigs.k8s.io/controller-runtime/pkg/reconcile"
41+
42+
mcbuilder "github.com/multicluster-runtime/multicluster-runtime/pkg/builder"
43+
mcmanager "github.com/multicluster-runtime/multicluster-runtime/pkg/manager"
44+
mcreconcile "github.com/multicluster-runtime/multicluster-runtime/pkg/reconcile"
45+
)
46+
47+
var _ = Describe("Provider Namespace", func() {
48+
Describe("New", func() {
49+
It("should return success if given valid objects", func(ctx context.Context) {
50+
cli, err := client.New(cfg, client.Options{})
51+
Expect(err).NotTo(HaveOccurred())
52+
53+
By("Creating Namespace and ConfigMap objects")
54+
runtime.Must(client.IgnoreAlreadyExists(cli.Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "zoo"}})))
55+
runtime.Must(client.IgnoreAlreadyExists(cli.Create(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: "zoo", Name: "elephant", Labels: map[string]string{"type": "animal"}}})))
56+
runtime.Must(client.IgnoreAlreadyExists(cli.Create(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: "zoo", Name: "lion", Labels: map[string]string{"type": "animal"}}})))
57+
runtime.Must(client.IgnoreAlreadyExists(cli.Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "jungle"}})))
58+
runtime.Must(client.IgnoreAlreadyExists(cli.Create(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: "jungle", Name: "monkey", Labels: map[string]string{"type": "animal"}}})))
59+
runtime.Must(client.IgnoreAlreadyExists(cli.Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "island"}})))
60+
runtime.Must(client.IgnoreAlreadyExists(cli.Create(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: "island", Name: "bird", Labels: map[string]string{"type": "animal"}}})))
61+
62+
By("Setting up the provider")
63+
cl, err := cluster.New(cfg, WithClusterNameIndex(), func(options *cluster.Options) {
64+
options.Cache.ByObject = map[client.Object]cache.ByObject{
65+
&corev1.ConfigMap{}: {
66+
Label: labels.Set{"type": "animal"}.AsSelector(),
67+
},
68+
}
69+
})
70+
Expect(err).NotTo(HaveOccurred())
71+
provider := New(cl)
72+
73+
By("Setting up the cluster-aware manager, with the provider to lookup clusters")
74+
mgr, err := mcmanager.New(cfg, provider, manager.Options{
75+
NewCache: func(config *rest.Config, opts cache.Options) (cache.Cache, error) {
76+
// wrap cache to turn IndexField calls into cluster-scoped indexes.
77+
return &NamespaceScopeableCache{Cache: cl.GetCache()}, nil
78+
},
79+
})
80+
Expect(err).NotTo(HaveOccurred())
81+
82+
By("Setting up the controller")
83+
err = mcbuilder.ControllerManagedBy(mgr).
84+
Named("fleet-ns-configmap-controller").
85+
For(&corev1.ConfigMap{}).
86+
Complete(mcreconcile.Func(
87+
func(ctx context.Context, req mcreconcile.Request) (ctrl.Result, error) {
88+
log := log.FromContext(ctx).WithValues("request", req.String())
89+
log.Info("Reconciling ConfigMap")
90+
91+
cl, err := mgr.GetCluster(ctx, req.ClusterName)
92+
if err != nil {
93+
return reconcile.Result{}, err
94+
}
95+
96+
// Feed the animal.
97+
cm := &corev1.ConfigMap{}
98+
if err := cl.GetClient().Get(ctx, req.NamespacedName, cm); err != nil {
99+
if apierrors.IsNotFound(err) {
100+
return reconcile.Result{}, nil
101+
}
102+
return reconcile.Result{}, err
103+
}
104+
cm.Data = map[string]string{"stomach": "food"}
105+
if err := cl.GetClient().Update(ctx, cm); err != nil {
106+
return reconcile.Result{}, err
107+
}
108+
109+
return ctrl.Result{}, nil
110+
},
111+
))
112+
Expect(err).NotTo(HaveOccurred())
113+
114+
By("Starting provider")
115+
ctx, cancel := context.WithCancel(ctx)
116+
g, ctx := errgroup.WithContext(ctx)
117+
defer func() {
118+
cancel()
119+
By("Waiting for all components to finish")
120+
err = g.Wait()
121+
Expect(err).NotTo(HaveOccurred())
122+
}()
123+
g.Go(func() error {
124+
return ignoreCanceled(provider.Run(ctx, mgr))
125+
})
126+
127+
By("Starting cluster")
128+
g.Go(func() error {
129+
return ignoreCanceled(cl.Start(ctx))
130+
})
131+
132+
By("Starting cluster-aware manager")
133+
g.Go(func() error {
134+
return ignoreCanceled(mgr.Start(ctx))
135+
})
136+
137+
err = cli.Create(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: "zoo", Name: "tiger", Labels: map[string]string{"type": "animal"}}})
138+
Expect(err).NotTo(HaveOccurred())
139+
140+
Eventually(func() string {
141+
cm := &corev1.ConfigMap{}
142+
err := cli.Get(ctx, client.ObjectKey{Namespace: "zoo", Name: "tiger"}, cm)
143+
Expect(err).NotTo(HaveOccurred())
144+
return cm.Data["stomach"]
145+
}, "10s").Should(Equal("food"))
146+
})
147+
})
148+
})
149+
150+
func ignoreCanceled(err error) error {
151+
if errors.Is(err, context.Canceled) {
152+
return nil
153+
}
154+
return err
155+
}

0 commit comments

Comments
 (0)