Skip to content

Commit 31313e6

Browse files
authored
Merge pull request #32 from embik/add-error
✨ Add `multicluster.ErrClusterNotFound` for providers
2 parents 012c3ae + ff4b6dd commit 31313e6

File tree

9 files changed

+106
-8
lines changed

9 files changed

+106
-8
lines changed

pkg/builder/forked_controller.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ type TypedBuilder[request mcreconcile.ClusterAware[request]] struct {
7373
ctrlOptions controller.TypedOptions[request]
7474
name string
7575
newController func(name string, mgr mcmanager.Manager, options controller.TypedOptions[request]) (mccontroller.TypedController[request], error)
76+
77+
enableClusterNotFoundWrapper *bool
7678
}
7779

7880
// ControllerManagedBy returns a new controller builder that will be started by the provided Manager.
@@ -260,6 +262,15 @@ func (blder *TypedBuilder[request]) WithLogConstructor(logConstructor func(*requ
260262
return blder
261263
}
262264

265+
// WithClusterNotFoundWrapper enables or disables a reconciler that is wrapped around the original reconciler
266+
// added to this builder. [reconcile.ClusterNotFoundWrapper] will stop reconcile results with [multicluster.ErrClusterNotFound]
267+
// as error from requeuing by marking them as successfully reconciled. This wrapper is enabled by default
268+
// and can be disabled with this builder method by setting it to false.
269+
func (blder *TypedBuilder[request]) WithClusterNotFoundWrapper(enabled bool) *TypedBuilder[request] {
270+
blder.enableClusterNotFoundWrapper = ptr.To(enabled)
271+
return blder
272+
}
273+
263274
// Named sets the name of the controller to the given name. The name shows up
264275
// in metrics, among other things, and thus should be a prometheus compatible name
265276
// (underscores and alphanumeric characters only).
@@ -450,6 +461,11 @@ func (blder *TypedBuilder[request]) doController(r reconcile.TypedReconciler[req
450461
ctrlOptions.Reconciler = r
451462
}
452463

464+
// the ClusterNotFound wrapper is enabled by default, but can be disabled with WithClusterNotFoundWrapper(false).
465+
if ptr.Deref(blder.enableClusterNotFoundWrapper, true) {
466+
ctrlOptions.Reconciler = mcreconcile.NewClusterNotFoundWrapper(ctrlOptions.Reconciler)
467+
}
468+
453469
// Retrieve the GVK from the object we're reconciling
454470
// to pre-populate logger information, and to optionally generate a default name.
455471
var gvk schema.GroupVersionKind

pkg/multicluster/errors.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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 multicluster
18+
19+
import (
20+
"errors"
21+
)
22+
23+
var (
24+
// ErrClusterNotFound can be returned by provider implementations if the cluster requested
25+
// doesn't exist and cannot be constructed.
26+
ErrClusterNotFound = errClusterNotFound()
27+
)
28+
29+
func errClusterNotFound() error { return errors.New("cluster not found") }

pkg/multicluster/multicluster.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ type Provider interface {
5454
// Get returns a cluster for the given identifying cluster name. Get
5555
// returns an existing cluster if it has been created before.
5656
// If no cluster is known to the provider under the given cluster name,
57-
// an error should be returned.
57+
// ErrClusterNotFound should be returned.
5858
Get(ctx context.Context, clusterName string) (cluster.Cluster, error)
5959

6060
// IndexField indexes the given object by the given field on all engaged

pkg/reconcile/wrapper.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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 reconcile
18+
19+
import (
20+
"context"
21+
"errors"
22+
"fmt"
23+
24+
"sigs.k8s.io/controller-runtime/pkg/reconcile"
25+
26+
"sigs.k8s.io/multicluster-runtime/pkg/multicluster"
27+
)
28+
29+
// ClusterNotFoundWrapper wraps an existing [reconcile.TypedReconciler] and ignores [multicluster.ErrClusterNotFound] errors.
30+
type ClusterNotFoundWrapper[request comparable] struct {
31+
wrapped reconcile.TypedReconciler[request]
32+
}
33+
34+
// NewClusterNotFoundWrapper creates a new [ClusterNotFoundWrapper].
35+
func NewClusterNotFoundWrapper[request comparable](w reconcile.TypedReconciler[request]) reconcile.TypedReconciler[request] {
36+
return &ClusterNotFoundWrapper[request]{wrapped: w}
37+
}
38+
39+
// Reconcile implements [reconcile.TypedReconciler].
40+
func (r *ClusterNotFoundWrapper[request]) Reconcile(ctx context.Context, req request) (reconcile.Result, error) {
41+
res, err := r.wrapped.Reconcile(ctx, req)
42+
43+
// if the error returned by the reconciler is ErrClusterNotFound, we return without requeuing.
44+
if errors.Is(err, multicluster.ErrClusterNotFound) {
45+
return reconcile.Result{}, nil
46+
}
47+
48+
return res, err
49+
}
50+
51+
// String returns a string representation of the wrapped reconciler.
52+
func (r *ClusterNotFoundWrapper[request]) String() string {
53+
return fmt.Sprintf("%v", r.wrapped)
54+
}

providers/cluster-api/provider.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ func (p *Provider) Get(_ context.Context, clusterName string) (cluster.Cluster,
123123
return cl, nil
124124
}
125125

126-
return nil, fmt.Errorf("cluster %s not found", clusterName)
126+
return nil, multicluster.ErrClusterNotFound
127127
}
128128

129129
// Run starts the provider and blocks.

providers/kind/provider.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ func (p *Provider) Get(ctx context.Context, clusterName string) (cluster.Cluster
7373
return cl, nil
7474
}
7575

76-
return nil, fmt.Errorf("cluster %s not found", clusterName)
76+
return nil, multicluster.ErrClusterNotFound
7777
}
7878

7979
// Run starts the provider and blocks.

providers/namespace/provider.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,8 @@ func (p *Provider) Get(_ context.Context, clusterName string) (cluster.Cluster,
134134
if cl, ok := p.clusters[clusterName]; ok {
135135
return cl, nil
136136
}
137-
return nil, fmt.Errorf("cluster %s not found", clusterName)
137+
138+
return nil, multicluster.ErrClusterNotFound
138139
}
139140

140141
// IndexField indexes a field on all clusters.

providers/nop/provider.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ package namespace
1818

1919
import (
2020
"context"
21-
"fmt"
2221

2322
"sigs.k8s.io/controller-runtime/pkg/client"
2423
"sigs.k8s.io/controller-runtime/pkg/cluster"
@@ -45,7 +44,7 @@ func (p *Provider) Run(ctx context.Context, _ mcmanager.Manager) error {
4544

4645
// Get returns an error for any cluster name.
4746
func (p *Provider) Get(_ context.Context, clusterName string) (cluster.Cluster, error) {
48-
return nil, fmt.Errorf("cluster %s not found", clusterName)
47+
return nil, multicluster.ErrClusterNotFound
4948
}
5049

5150
// IndexField does nothing.

providers/single/provider.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ package namespace
1818

1919
import (
2020
"context"
21-
"fmt"
2221

2322
"sigs.k8s.io/controller-runtime/pkg/client"
2423
"sigs.k8s.io/controller-runtime/pkg/cluster"
@@ -55,7 +54,7 @@ func (p *Provider) Get(_ context.Context, clusterName string) (cluster.Cluster,
5554
if clusterName == p.name {
5655
return p.cl, nil
5756
}
58-
return nil, fmt.Errorf("cluster %s not found", clusterName)
57+
return nil, multicluster.ErrClusterNotFound
5958
}
6059

6160
// IndexField calls IndexField on the single cluster.

0 commit comments

Comments
 (0)