Skip to content

Commit db62549

Browse files
committed
multicluster changes for kcp
1 parent 6eed6de commit db62549

File tree

12 files changed

+297
-50
lines changed

12 files changed

+297
-50
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,5 @@ go.work.sum
2323

2424
# env file
2525
.env
26+
hack/tools/apigen
27+
bin/*

Makefile

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,8 @@ build: manifests generate fmt vet ## Build manager binary.
9696
go build -o bin/manager cmd/main.go
9797

9898
.PHONY: run
99-
run: manifests generate fmt vet ## Run a controller from your host.
100-
go run ./cmd/main.go
99+
run: manifests generate kcp-generate fmt vet ## Run a controller from your host.
100+
go run ./cmd/main.go --server=$$(kubectl get apiexport apis.contrib.kcp.io -o jsonpath="{.status.virtualWorkspaces[0].url}")
101101

102102
# If you wish to build the manager image targeting other platforms you can use the --platform flag.
103103
# (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it.
@@ -222,3 +222,30 @@ mv $(1) $(1)-$(3) ;\
222222
} ;\
223223
ln -sf $(1)-$(3) $(1)
224224
endef
225+
226+
# MUTLICLUSTER-PROVIDER KCP SPECIFIC:
227+
TOOLS_DIR=hack/tools
228+
ARCH := $(shell go env GOARCH)
229+
OS := $(shell go env GOOS)
230+
231+
KCP_APIGEN_BIN := apigen
232+
KCP_APIGEN_VERSION ?= 0.27.0-rc.1
233+
# Construct the download URL.
234+
DOWNLOAD_URL = https://github.com/kcp-dev/kcp/releases/download/v$(KCP_APIGEN_VERSION)/apigen_$(KCP_APIGEN_VERSION)_$(OS)_$(ARCH).tar.gz
235+
KCP_APIGEN_GEN := $(TOOLS_DIR)/$(KCP_APIGEN_BIN)
236+
export KCP_APIGEN_GEN # so hack scripts can use it
237+
238+
# TODO: move to binary to avoid depeendencies
239+
$(KCP_APIGEN_GEN):
240+
# Create a temporary directory for download and extraction
241+
TMP_DIR=$$(mktemp -d) ; \
242+
curl -L -o $$TMP_DIR/apigen.tar.gz $(DOWNLOAD_URL) ; \
243+
tar -xzf $$TMP_DIR/apigen.tar.gz -C $$TMP_DIR ; \
244+
mv $$TMP_DIR/bin/apigen $(KCP_APIGEN_GEN) ; \
245+
rm -rf $$TMP_DIR
246+
247+
kcp-tools: $(KCP_APIGEN_GEN)
248+
.PHONY: kcp-tools
249+
250+
kcp-generate:
251+
$(TOOLS_DIR)/apigen --input-dir ./config/crd/bases --output-dir ./config/kcp

README.md

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,79 @@
11
# multicluster-provider-kubebuilder-example
2-
A full kcp multicluster-provider example using kubebuilder
2+
3+
A full kcp multicluster-provider example using kubebuilder.
4+
5+
This example demonstrates how to lift&shift a kubebuilder project to a multicluster-provider project for kcp.
6+
7+
There should be 2 commits in this repository at all times:
8+
9+
1. The initial commit with the kubebuilder project.
10+
11+
Skeleton created by running [kubebuilder](https://book.kubebuilder.io/quick-start.html) with the following command:
12+
13+
```sh
14+
$ kubebuilder init --domain contrib.kcp.io --repo github.com/kcp-dev/multicluster-provider/examples/crd
15+
$ kubebuilder create api --group apis --version v1alpha1 --kind Application
16+
```
17+
18+
2. The commit with the kubebuilder project lifted&shifted to a multicluster-provider project to work with kcp.
19+
20+
Please refer to main repositories for more information:
21+
22+
- [multicluster-provider for kcp](https://github.com/kcp-dev/multicluster-provider)
23+
- [multicluster-runtime](https://github.com/multicluster-runtime/multicluster-runtime)
24+
- [kcp](https://github.com/kcp-dev/kcp)
25+
26+
27+
## Customizations & Run
28+
29+
### Prerequisites
30+
31+
- [kcp](https://github.com/kcp-dev/kcp) installed and running
32+
- [kcp krew plugins](https://docs.kcp.io/kcp/v0.26/setup/kubectl-plugin/) installed
33+
34+
1. Make file targets to install kcp specific tooling and generate kcp specific manifests:
35+
36+
```sh
37+
# install tooling
38+
make kcp-tools
39+
# generate kcp manifests
40+
make kcp-generate
41+
```
42+
43+
After than content of `cmd/main.go` was updated with multicluster extension.
44+
45+
It can be tested by applying the necessary manifests from the respective folder while connected to the `root` workspace of a kcp instance:
46+
47+
export KUBECONFIG=admin.kubeconfig
48+
49+
```sh
50+
$ kubectl ws create provider --enter
51+
$ kubectl apply -f ./config/kcp/apiresourceschema-applications.apis.contrib.kcp.io.yaml
52+
$ kubectl apply -f ./config/kcp/apiexport-apis.contrib.kcp.io.yaml
53+
54+
# Consumer 1
55+
$ kubectl ws use :root
56+
$ kubectl ws create examples-crd-multicluster-1 --enter
57+
$ kubectl kcp bind apiexport root:provider:apis.contrib.kcp.io
58+
$ kubectl apply -f ./config/samples/apis_v1alpha1_application.yaml
59+
60+
# Consumer 2
61+
$ kubectl ws use :root
62+
$ kubectl ws create examples-crd-multicluster-2 --enter
63+
$ kubectl kcp bind apiexport root:provider:apis.contrib.kcp.io
64+
$ kubectl apply -f ./config/samples/apis_v1alpha1_application.yaml
65+
```
66+
67+
Then, start the example controller by passing the virtual workspace URL to it:
68+
69+
70+
```sh
71+
$ kubectl ws use :root:provider
72+
$ go run . --server=$(kubectl get apiexport apis.contrib.kcp.io -o jsonpath="{.status.virtualWorkspaces[0].url}")
73+
```
74+
75+
Observe the controller reconciling the ` application-sample` Aplication in the workspaces:
76+
77+
```sh
78+
2025-03-11T13:04:52+02:00 INFO Reconciling Application {"controller": "kcp-applications-controller", "controllerGroup": "apis.contrib.kcp.io", "controllerKind": "Application", "reconcileID": "babfc696-50cc-4851-ab35-d1d956a6c120", "cluster": "1058d5hgzdd3ask6"}
79+
```

bin/.gitkeep

Whitespace-only changes.

bin/controller-gen

Lines changed: 0 additions & 1 deletion
This file was deleted.

bin/controller-gen-v0.17.2

-23.5 MB
Binary file not shown.

cmd/main.go

Lines changed: 86 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,27 +17,39 @@ limitations under the License.
1717
package main
1818

1919
import (
20+
"context"
2021
"crypto/tls"
2122
"flag"
23+
"fmt"
2224
"os"
2325
"path/filepath"
2426

2527
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
2628
// to ensure that exec-entrypoint and run can make use of them.
2729
_ "k8s.io/client-go/plugin/pkg/client/auth"
30+
"k8s.io/client-go/rest"
31+
32+
apisv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1"
33+
"github.com/kcp-dev/multicluster-provider/virtualworkspace"
34+
mcbuilder "github.com/multicluster-runtime/multicluster-runtime/pkg/builder"
35+
mcmanager "github.com/multicluster-runtime/multicluster-runtime/pkg/manager"
36+
mcreconcile "github.com/multicluster-runtime/multicluster-runtime/pkg/reconcile"
2837

2938
"k8s.io/apimachinery/pkg/runtime"
3039
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
3140
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
3241
ctrl "sigs.k8s.io/controller-runtime"
3342
"sigs.k8s.io/controller-runtime/pkg/certwatcher"
3443
"sigs.k8s.io/controller-runtime/pkg/healthz"
44+
"sigs.k8s.io/controller-runtime/pkg/log"
3545
"sigs.k8s.io/controller-runtime/pkg/log/zap"
46+
"sigs.k8s.io/controller-runtime/pkg/manager/signals"
3647
"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
3748
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
49+
"sigs.k8s.io/controller-runtime/pkg/reconcile"
3850
"sigs.k8s.io/controller-runtime/pkg/webhook"
3951

40-
apisv1alpha1 "github.com/kcp-dev/multicluster-provider/examples/crd/api/v1alpha1"
52+
applicationapisv1alpha1 "github.com/kcp-dev/multicluster-provider/examples/crd/api/v1alpha1"
4153
"github.com/kcp-dev/multicluster-provider/examples/crd/internal/controller"
4254
// +kubebuilder:scaffold:imports
4355
)
@@ -49,7 +61,8 @@ var (
4961

5062
func init() {
5163
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
52-
64+
// MULTICLUSTER: This is where it differ from the default scaffold.
65+
utilruntime.Must(applicationapisv1alpha1.AddToScheme(scheme))
5366
utilruntime.Must(apisv1alpha1.AddToScheme(scheme))
5467
// +kubebuilder:scaffold:scheme
5568
}
@@ -63,6 +76,7 @@ func main() {
6376
var probeAddr string
6477
var secureMetrics bool
6578
var enableHTTP2 bool
79+
var server string
6680
var tlsOpts []func(*tls.Config)
6781
flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
6882
"Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.")
@@ -81,6 +95,8 @@ func main() {
8195
flag.StringVar(&metricsCertKey, "metrics-cert-key", "tls.key", "The name of the metrics server key file.")
8296
flag.BoolVar(&enableHTTP2, "enable-http2", false,
8397
"If set, HTTP/2 will be enabled for the metrics and webhook servers")
98+
// MULTICLUSTER: This is where it differ from the default scaffold.
99+
flag.StringVar(&server, "server", "", "Override for kubeconfig server URL")
84100
opts := zap.Options{
85101
Development: true,
86102
}
@@ -178,7 +194,25 @@ func main() {
178194
})
179195
}
180196

181-
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
197+
// MULTICLUSTER: This is where it differ from the default scaffold.
198+
ctx := signals.SetupSignalHandler()
199+
200+
cfg := ctrl.GetConfigOrDie()
201+
cfg = rest.CopyConfig(cfg)
202+
if server != "" {
203+
cfg.Host = server
204+
}
205+
206+
var err error
207+
provider, err := virtualworkspace.New(cfg, &apisv1alpha1.APIBinding{}, virtualworkspace.Options{
208+
Scheme: scheme,
209+
})
210+
if err != nil {
211+
setupLog.Error(err, "unable to construct cluster provider")
212+
os.Exit(1)
213+
}
214+
215+
mgr, err := mcmanager.New(cfg, provider, ctrl.Options{
182216
Scheme: scheme,
183217
Metrics: metricsServerOptions,
184218
WebhookServer: webhookServer,
@@ -202,30 +236,48 @@ func main() {
202236
os.Exit(1)
203237
}
204238

205-
if err = (&controller.ApplicationReconciler{
206-
Client: mgr.GetClient(),
207-
Scheme: mgr.GetScheme(),
208-
}).SetupWithManager(mgr); err != nil {
239+
if err := mcbuilder.ControllerManagedBy(mgr).
240+
Named("kcp-applications-controller").
241+
For(&applicationapisv1alpha1.Application{}).
242+
Complete(mcreconcile.Func(
243+
func(ctx context.Context, req mcreconcile.Request) (ctrl.Result, error) {
244+
log := log.FromContext(ctx).WithValues("cluster", req.ClusterName)
245+
log.Info("Reconciling Application")
246+
247+
cl, err := mgr.GetCluster(ctx, req.ClusterName)
248+
if err != nil {
249+
return reconcile.Result{}, fmt.Errorf("failed to get cluster: %w", err)
250+
}
251+
client := cl.GetClient()
252+
253+
reconciler := &controller.ApplicationReconciler{
254+
Client: client,
255+
Scheme: cl.GetScheme(),
256+
}
257+
return reconciler.Reconcile(ctx, reconcile.Request{NamespacedName: req.NamespacedName})
258+
},
259+
)); err != nil {
209260
setupLog.Error(err, "unable to create controller", "controller", "Application")
210261
os.Exit(1)
211262
}
212263
// +kubebuilder:scaffold:builder
213264

214-
if metricsCertWatcher != nil {
215-
setupLog.Info("Adding metrics certificate watcher to manager")
216-
if err := mgr.Add(metricsCertWatcher); err != nil {
217-
setupLog.Error(err, "unable to add metrics certificate watcher to manager")
218-
os.Exit(1)
219-
}
220-
}
221-
222-
if webhookCertWatcher != nil {
223-
setupLog.Info("Adding webhook certificate watcher to manager")
224-
if err := mgr.Add(webhookCertWatcher); err != nil {
225-
setupLog.Error(err, "unable to add webhook certificate watcher to manager")
226-
os.Exit(1)
227-
}
228-
}
265+
// TODO(mjudeikis): This needs to be implemented in mcmanager.
266+
// if metricsCertWatcher != nil {
267+
// setupLog.Info("Adding metrics certificate watcher to manager")
268+
// if err := mgr.Add(metricsCertWatcher); err != nil {
269+
// setupLog.Error(err, "unable to add metrics certificate watcher to manager")
270+
// os.Exit(1)
271+
// }
272+
// }
273+
//
274+
// if webhookCertWatcher != nil {
275+
// setupLog.Info("Adding webhook certificate watcher to manager")
276+
// if err := mgr.Add(webhookCertWatcher); err != nil {
277+
// setupLog.Error(err, "unable to add webhook certificate watcher to manager")
278+
// os.Exit(1)
279+
// }
280+
// }
229281

230282
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
231283
setupLog.Error(err, "unable to set up health check")
@@ -237,7 +289,18 @@ func main() {
237289
}
238290

239291
setupLog.Info("starting manager")
240-
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
292+
if provider != nil {
293+
setupLog.Info("Starting provider")
294+
go func() {
295+
if err := provider.Run(ctx, mgr); err != nil {
296+
setupLog.Error(err, "unable to run provider")
297+
os.Exit(1)
298+
}
299+
}()
300+
}
301+
302+
setupLog.Info("starting manager", "server", server)
303+
if err := mgr.Start(ctx); err != nil {
241304
setupLog.Error(err, "problem running manager")
242305
os.Exit(1)
243306
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
apiVersion: apis.kcp.io/v1alpha1
2+
kind: APIExport
3+
metadata:
4+
creationTimestamp: null
5+
name: apis.contrib.kcp.io
6+
spec:
7+
latestResourceSchemas:
8+
- v250316-6eed6de.applications.apis.contrib.kcp.io
9+
status: {}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
apiVersion: apis.kcp.io/v1alpha1
2+
kind: APIResourceSchema
3+
metadata:
4+
creationTimestamp: null
5+
name: v250316-6eed6de.applications.apis.contrib.kcp.io
6+
spec:
7+
group: apis.contrib.kcp.io
8+
names:
9+
kind: Application
10+
listKind: ApplicationList
11+
plural: applications
12+
singular: application
13+
scope: Namespaced
14+
versions:
15+
- name: v1alpha1
16+
schema:
17+
description: Application is the Schema for the applications API.
18+
properties:
19+
apiVersion:
20+
description: |-
21+
APIVersion defines the versioned schema of this representation of an object.
22+
Servers should convert recognized schemas to the latest internal value, and
23+
may reject unrecognized values.
24+
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
25+
type: string
26+
kind:
27+
description: |-
28+
Kind is a string value representing the REST resource this object represents.
29+
Servers may infer this from the endpoint the client submits requests to.
30+
Cannot be updated.
31+
In CamelCase.
32+
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
33+
type: string
34+
metadata:
35+
type: object
36+
spec:
37+
description: ApplicationSpec defines the desired state of Application.
38+
properties:
39+
foo:
40+
description: Foo is an example field of Application. Edit application_types.go
41+
to remove/update
42+
type: string
43+
type: object
44+
status:
45+
description: ApplicationStatus defines the observed state of Application.
46+
type: object
47+
type: object
48+
served: true
49+
storage: true
50+
subresources:
51+
status: {}

0 commit comments

Comments
 (0)