Skip to content

Commit fcd1693

Browse files
authored
Added support for external host clusters (#52)
* added support for external host clusters * fixed comments and result requeue
1 parent c0646f1 commit fcd1693

File tree

7 files changed

+186
-107
lines changed

7 files changed

+186
-107
lines changed

api/controlplane/v1alpha1/k3kcontrolplane_types.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,18 @@ import (
2525

2626
// K3kControlPlaneSpec defines the desired state of K3kControlPlane
2727
type K3kControlPlaneSpec struct {
28-
// HostKubeconfig is the location of the kubeconfig to the host cluster that the K3k cluster should install in.
29-
// Optional, if not supplied the K3k cluster will be made in the current cluster.
28+
// HostKubeconfig specifies the location of the Kubernetes secret containing the kubeconfig for the host cluster where the K3k cluster will be installed.
29+
// If not provided, the K3k cluster will be created in the current context's cluster.
30+
//
31+
// +optional
3032
HostKubeconfig *HostKubeconfigLocation `json:"hostKubeconfig,omitempty"`
3133

34+
// HostTargetNamespace is the host namespace where the virtual cluster will be installed.
35+
// If not provided a namespace with the k3k-<cluster_name> prefix will be created.
36+
//
37+
// +optional
38+
HostTargetNamespace string `json:"hostTargetNamespace,omitempty"`
39+
3240
// The following fields are copied from the github.com/rancher/k3k/pkg/apis/k3k.io/v1alpha1.ClusterSpec
3341

3442
// Servers is the number of K3s pods to run in server (controlplane) mode.

cmd/main.go

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,7 @@ import (
2525

2626
"github.com/go-logr/stdr"
2727
"k8s.io/apimachinery/pkg/runtime"
28-
"k8s.io/client-go/discovery/cached/memory"
29-
"k8s.io/client-go/kubernetes"
30-
"k8s.io/client-go/tools/clientcmd"
3128
"sigs.k8s.io/controller-runtime/pkg/healthz"
32-
"sigs.k8s.io/controller-runtime/pkg/manager"
3329
"sigs.k8s.io/controller-runtime/pkg/webhook"
3430

3531
upstream "github.com/rancher/k3k/pkg/apis/k3k.io/v1alpha1"
@@ -138,7 +134,7 @@ func main() {
138134
os.Exit(1)
139135
}
140136

141-
restClientGetter, err := newRestClientGetter(mgr)
137+
restClientGetter, err := helm.NewRESTClientGetter(mgr.GetConfig(), mgr.GetRESTMapper())
142138
if err != nil {
143139
setupLog.Error(err, "failed to set up REST client getter")
144140
os.Exit(1)
@@ -149,7 +145,7 @@ func main() {
149145
if err = (&controlplanecontroller.K3kControlPlaneReconciler{
150146
Client: mgr.GetClient(),
151147
Helm: helm.New(restClientGetter, "charts/k3k", "k3k", "k3k-system"),
152-
K3KVersion: k3kVersion,
148+
K3kVersion: k3kVersion,
153149
}).SetupWithManager(ctx, mgr); err != nil {
154150
setupLog.Error(err, "unable to create controller", "controller", "K3kControlPlane")
155151
os.Exit(1)
@@ -177,21 +173,3 @@ func main() {
177173
os.Exit(1)
178174
}
179175
}
180-
181-
func newRestClientGetter(mgr manager.Manager) (*helm.SimpleRESTClientGetter, error) {
182-
restConfig := mgr.GetConfig()
183-
k8s, err := kubernetes.NewForConfig(restConfig)
184-
if err != nil {
185-
return nil, fmt.Errorf("failed to get client set: %w", err)
186-
}
187-
restMapper := mgr.GetRESTMapper()
188-
cache := memory.NewMemCacheClient(k8s.Discovery())
189-
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
190-
kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &clientcmd.ConfigOverrides{})
191-
return &helm.SimpleRESTClientGetter{
192-
ClientConfig: kubeConfig,
193-
RESTConfig: restConfig,
194-
CachedDiscovery: cache,
195-
RESTMapper: restMapper,
196-
}, nil
197-
}

config/crd/bases/controlplane.cluster.x-k8s.io_k3kcontrolplanes.yaml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,8 @@ spec:
135135
type: object
136136
hostKubeconfig:
137137
description: |-
138-
HostKubeconfig is the location of the kubeconfig to the host cluster that the K3k cluster should install in.
139-
Optional, if not supplied the K3k cluster will be made in the current cluster.
138+
HostKubeconfig specifies the location of the Kubernetes secret containing the kubeconfig for the host cluster where the K3k cluster will be installed.
139+
If not provided, the K3k cluster will be created in the current context's cluster.
140140
properties:
141141
secretName:
142142
description: SecretName is the name of the secret containing the
@@ -150,6 +150,11 @@ spec:
150150
- secretName
151151
- secretNamespace
152152
type: object
153+
hostTargetNamespace:
154+
description: |-
155+
HostTargetNamespace is the host namespace where the virtual cluster will be installed.
156+
If not provided a namespace with the k3k-<cluster_name> prefix will be created.
157+
type: string
153158
persistence:
154159
description: |-
155160
Persistence contains options controlling how the etcd data of the virtual cluster is persisted. By default, no data

config/crd/bases/controlplane.cluster.x-k8s.io_k3kcontrolplanetemplates.yaml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,8 @@ spec:
192192
type: object
193193
hostKubeconfig:
194194
description: |-
195-
HostKubeconfig is the location of the kubeconfig to the host cluster that the K3k cluster should install in.
196-
Optional, if not supplied the K3k cluster will be made in the current cluster.
195+
HostKubeconfig specifies the location of the Kubernetes secret containing the kubeconfig for the host cluster where the K3k cluster will be installed.
196+
If not provided, the K3k cluster will be created in the current context's cluster.
197197
properties:
198198
secretName:
199199
description: SecretName is the name of the secret containing
@@ -207,6 +207,11 @@ spec:
207207
- secretName
208208
- secretNamespace
209209
type: object
210+
hostTargetNamespace:
211+
description: |-
212+
HostTargetNamespace is the host namespace where the virtual cluster will be installed.
213+
If not provided a namespace with the k3k-<cluster_name> prefix will be created.
214+
type: string
210215
persistence:
211216
description: |-
212217
Persistence contains options controlling how the etcd data of the virtual cluster is persisted. By default, no data

internal/controller/controlplane/k3kcontrolplane_controller.go

Lines changed: 101 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,9 @@ type scope struct {
6868
// K3kControlPlaneReconciler reconciles a K3kControlPlane object
6969
type K3kControlPlaneReconciler struct {
7070
client.Client
71+
Host client.Client
7172
Helm helm.Client
72-
K3KVersion string
73+
K3kVersion string
7374
}
7475

7576
// SetupWithManager sets up the controller with the Manager.
@@ -98,21 +99,58 @@ func (r *K3kControlPlaneReconciler) SetupWithManager(_ context.Context, mgr ctrl
9899
// Reconcile creates a K3k Upstream cluster based on the provided spec of the K3kControlPlane.
99100
func (r *K3kControlPlaneReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
100101
log := ctrl.LoggerFrom(ctx)
101-
if err := r.ensureUpstreamChart(ctx); err != nil {
102-
return ctrl.Result{}, fmt.Errorf("failed to ensure upstream K3K release is deployed: %w", err)
103-
} else {
104-
log.Info("The upstream K3K controller Helm release is deployed without errors.")
105-
}
106-
k3kControlPlane := &controlplanev1.K3kControlPlane{}
107-
err := r.Get(ctx, req.NamespacedName, k3kControlPlane)
108-
if err != nil {
102+
103+
var k3kControlPlane controlplanev1.K3kControlPlane
104+
if err := r.Get(ctx, req.NamespacedName, &k3kControlPlane); err != nil {
109105
if apiError.IsNotFound(err) {
110-
log.Error(err, "Couldn't find controlplane")
111-
return ctrl.Result{}, client.IgnoreNotFound(err)
106+
log.Error(err, "couldn't find controlplane")
107+
return ctrl.Result{}, nil
112108
}
113109
return ctrl.Result{}, fmt.Errorf("unable to get controlplane for request %w", err)
114110
}
115111

112+
r.Host = r.Client
113+
114+
if k3kControlPlane.Spec.HostKubeconfig != nil {
115+
log.Info("targeting external host cluster")
116+
117+
hostKubeconfigSecret := &v1.Secret{
118+
ObjectMeta: metav1.ObjectMeta{
119+
Name: k3kControlPlane.Spec.HostKubeconfig.SecretName,
120+
Namespace: k3kControlPlane.Spec.HostKubeconfig.SecretNamespace,
121+
},
122+
}
123+
124+
if err := r.Get(ctx, client.ObjectKeyFromObject(hostKubeconfigSecret), hostKubeconfigSecret); err != nil {
125+
return ctrl.Result{}, fmt.Errorf("failed to get host kubeconfig secret: %w", err)
126+
}
127+
128+
config, err := clientcmd.RESTConfigFromKubeConfig(hostKubeconfigSecret.Data["value"])
129+
if err != nil {
130+
return ctrl.Result{}, fmt.Errorf("failed to create RESTConfig from host kubeconfig secret: %w", err)
131+
}
132+
133+
hostClient, err := client.New(config, client.Options{Scheme: r.Scheme()})
134+
if err != nil {
135+
return ctrl.Result{}, fmt.Errorf("failed to create host client: %w", err)
136+
}
137+
138+
r.Host = hostClient
139+
140+
hostRESTClientGetter, err := helm.NewRESTClientGetter(config, hostClient.RESTMapper())
141+
if err != nil {
142+
return ctrl.Result{}, fmt.Errorf("failed to create host helm RESTClientGetter: %w", err)
143+
}
144+
145+
r.Helm = helm.New(hostRESTClientGetter, "charts/k3k", "k3k", "k3k-system")
146+
}
147+
148+
if err := r.ensureUpstreamChart(ctx); err != nil {
149+
return ctrl.Result{}, fmt.Errorf("failed to ensure upstream K3k release: %w", err)
150+
}
151+
152+
log.Info("upstream K3k controller Helm release deployed")
153+
116154
cluster, err := capiutil.GetOwnerCluster(ctx, r, k3kControlPlane.ObjectMeta)
117155
if err != nil {
118156
return ctrl.Result{}, fmt.Errorf("unable to get capi cluster owner: %w", err)
@@ -121,12 +159,12 @@ func (r *K3kControlPlaneReconciler) Reconcile(ctx context.Context, req ctrl.Requ
121159
if cluster == nil {
122160
// capi cluster owner may not be set immediately, but we don't want to process the cluster until it is
123161
log.Info("K3kControlPlane did not have a capi cluster owner", "controlPlane", k3kControlPlane.Name)
124-
return ctrl.Result{}, nil
162+
return ctrl.Result{}, fmt.Errorf("CAPI cluster owner not yet set for control plane %q", k3kControlPlane.Name)
125163
}
126164

127165
scope := &scope{
128166
Logger: log,
129-
k3kControlPlane: k3kControlPlane,
167+
k3kControlPlane: &k3kControlPlane,
130168
cluster: cluster,
131169
}
132170

@@ -138,15 +176,6 @@ func (r *K3kControlPlaneReconciler) Reconcile(ctx context.Context, req ctrl.Requ
138176
}
139177

140178
func (r *K3kControlPlaneReconciler) reconcileNormal(ctx context.Context, scope *scope) (ctrl.Result, error) {
141-
if !controllerutil.ContainsFinalizer(scope.k3kControlPlane, finalizer) {
142-
controllerutil.AddFinalizer(scope.k3kControlPlane, finalizer)
143-
err := r.Update(ctx, scope.k3kControlPlane)
144-
if err != nil {
145-
scope.Error(err, "Unable to add finalizer to k3k controlplane", "name", scope.k3kControlPlane.Name, "namespace", scope.k3kControlPlane.Namespace)
146-
return ctrl.Result{}, fmt.Errorf("unable to add finalizer")
147-
}
148-
}
149-
150179
upstreamCluster, err := r.reconcileUpstreamCluster(ctx, scope.k3kControlPlane)
151180
if err != nil {
152181
return ctrl.Result{}, fmt.Errorf("unable to reconcile k3k cluster: %w", err)
@@ -189,17 +218,25 @@ func (r *K3kControlPlaneReconciler) reconcileNormal(ctx context.Context, scope *
189218
scope.Error(err, "Unable to update capiCluster")
190219
return ctrl.Result{}, fmt.Errorf("unable to update capi cluster")
191220
}
192-
if !scope.k3kControlPlane.Status.Ready || scope.k3kControlPlane.Status.Initialized {
221+
222+
// Update status if not ready
223+
statusUpdated := !scope.k3kControlPlane.Status.Ready || !scope.k3kControlPlane.Status.Initialized
224+
if statusUpdated {
193225
scope.k3kControlPlane.Status.Ready = true
194226
scope.k3kControlPlane.Status.Initialized = true
195227
scope.k3kControlPlane.Status.ExternalManagedControlPlane = true
196228
scope.k3kControlPlane.Status.ClusterStatus = *upstreamCluster.Status.DeepCopy()
197-
err = r.Status().Update(ctx, scope.k3kControlPlane)
198-
if err != nil {
229+
230+
if err := r.Status().Update(ctx, scope.k3kControlPlane); err != nil {
199231
scope.Error(err, "unable to update status on controlPlane")
200232
return ctrl.Result{}, fmt.Errorf("unable to update status")
201233
}
202234
}
235+
236+
if controllerutil.AddFinalizer(scope.k3kControlPlane, finalizer) {
237+
return ctrl.Result{Requeue: true}, r.Update(ctx, scope.k3kControlPlane)
238+
}
239+
203240
return ctrl.Result{}, nil
204241
}
205242

@@ -215,24 +252,28 @@ func (r *K3kControlPlaneReconciler) reconcileDelete(ctx context.Context, scope *
215252
// ensureUpstreamChart tries to install a release of the upstream K3K chart or upgrade it if there is a new version.
216253
func (r *K3kControlPlaneReconciler) ensureUpstreamChart(ctx context.Context) error {
217254
log := ctrl.LoggerFrom(ctx)
218-
if err := r.Helm.ReleasePresent(ctx, r.K3KVersion); err != nil {
219-
values := map[string]any{}
255+
256+
release, err := r.Helm.GetRelease(ctx)
257+
if err != nil {
220258
if errors.Is(err, driver.ErrReleaseNotFound) {
221-
if err := r.Helm.DeployLocalChart(ctx, values); err != nil {
259+
if err := r.Helm.DeployLocalChart(ctx, nil); err != nil {
222260
return fmt.Errorf("failed to deploy a local K3K release: %w", err)
223261
}
224-
log.Info("Successfully deployed the upstream K3K release.")
225-
return nil
226-
}
227-
if errors.Is(err, helm.ErrReleaseOutdated) {
228-
if err := r.Helm.UpgradeLocalChart(ctx, values); err != nil {
229-
return fmt.Errorf("failed to upgrade a local K3K release: %w", err)
230-
}
231-
log.Info("Successfully upgraded the upstream K3K release.")
262+
263+
log.Info("successfully deployed the upstream K3K release.")
264+
232265
return nil
233266
}
234-
return fmt.Errorf("couldn't check for the presence of a K3K release: %w", err)
267+
268+
return fmt.Errorf("couldn't check for the presence of a K3k release: %w", err)
235269
}
270+
271+
log.Info("found K3k release version " + release.Chart.Metadata.Version)
272+
273+
if release.Chart.Metadata.Version != r.K3kVersion {
274+
log.Error(helm.ErrReleaseMismatch, "K3k release version is not matching the current build. Something could break.")
275+
}
276+
236277
return nil
237278
}
238279

@@ -245,6 +286,7 @@ func (r *K3kControlPlaneReconciler) reconcileUpstreamCluster(ctx context.Context
245286
if err != nil {
246287
return nil, fmt.Errorf("failed to get current clusters for controlPlane %s/%s: %w", controlPlane.Namespace, controlPlane.Name, err)
247288
}
289+
248290
spec := upstream.ClusterSpec{
249291
Version: controlPlane.Spec.Version,
250292
Servers: controlPlane.Spec.Servers,
@@ -259,6 +301,7 @@ func (r *K3kControlPlaneReconciler) reconcileUpstreamCluster(ctx context.Context
259301
Persistence: controlPlane.Spec.Persistence,
260302
Expose: controlPlane.Spec.Expose,
261303
}
304+
262305
if len(clusters) > 1 {
263306
var names []string
264307
for i := range clusters {
@@ -271,12 +314,17 @@ func (r *K3kControlPlaneReconciler) reconcileUpstreamCluster(ctx context.Context
271314
}
272315
return nil, fmt.Errorf("reprovisioning needed")
273316
}
317+
274318
if len(clusters) == 0 {
275-
// since the upstream cluster type isn't namespaced but we are, we can't use owner refs to control deletion of the cluster
319+
namespace := "k3k-" + controlPlane.Name
320+
if controlPlane.Spec.HostTargetNamespace != "" {
321+
namespace = controlPlane.Spec.HostTargetNamespace
322+
}
323+
276324
upstreamCluster := upstream.Cluster{
277325
ObjectMeta: metav1.ObjectMeta{
278326
GenerateName: controlPlane.Name + "-",
279-
Namespace: controlPlane.Namespace,
327+
Namespace: namespace,
280328
Labels: map[string]string{
281329
ownerNameLabel: controlPlane.Name,
282330
ownerNamespaceLabel: controlPlane.Namespace,
@@ -285,14 +333,20 @@ func (r *K3kControlPlaneReconciler) reconcileUpstreamCluster(ctx context.Context
285333
Spec: spec,
286334
}
287335

288-
if err := ctrl.SetControllerReference(controlPlane, &upstreamCluster, r.Scheme()); err != nil {
289-
return nil, fmt.Errorf("unable to create cluster for controlPlane %s: %w", controlPlane.Name, err)
336+
virtualClusterNamespace := &v1.Namespace{
337+
ObjectMeta: metav1.ObjectMeta{
338+
Name: namespace,
339+
},
290340
}
291341

292-
err = r.Create(ctx, &upstreamCluster)
293-
if err != nil {
342+
if err := r.Host.Create(ctx, virtualClusterNamespace); err != nil && !apiError.IsAlreadyExists(err) {
343+
return nil, fmt.Errorf("unable to create namespace for controlPlane %s: %w", controlPlane.Name, err)
344+
}
345+
346+
if err = r.Host.Create(ctx, &upstreamCluster); err != nil {
294347
return nil, fmt.Errorf("unable to create cluster for controlPlane %s: %w", controlPlane.Name, err)
295348
}
349+
296350
return &upstreamCluster, nil
297351
}
298352
// at this point we have exactly one cluster
@@ -301,7 +355,7 @@ func (r *K3kControlPlaneReconciler) reconcileUpstreamCluster(ctx context.Context
301355
return currentCluster, nil
302356
}
303357
currentCluster.Spec = spec
304-
err = r.Update(ctx, currentCluster)
358+
err = r.Host.Update(ctx, currentCluster)
305359
if err != nil {
306360
return nil, fmt.Errorf("unable to update cluster for controlPlane %s: %w", controlPlane.Name, err)
307361
}
@@ -318,7 +372,7 @@ func (r *K3kControlPlaneReconciler) deleteUpstreamClusters(ctx context.Context,
318372
}
319373
var deleteErrs []error
320374
for i := range clusters {
321-
if err := r.Delete(ctx, &clusters[i]); err != nil && !apiError.IsNotFound(err) {
375+
if err := r.Host.Delete(ctx, &clusters[i]); err != nil && !apiError.IsNotFound(err) {
322376
log.Error(err, "failed to delete upstream cluster "+clusters[i].Name)
323377
deleteErrs = append(deleteErrs, err)
324378
}
@@ -345,7 +399,7 @@ func (r *K3kControlPlaneReconciler) getUpstreamClusters(ctx context.Context, con
345399
return nil, fmt.Errorf("unable to form label for controlPlane %s: %w", controlPlane.Name, err)
346400
}
347401
selector := labels.NewSelector().Add(*ownerNameRequirement).Add(*ownerNamespaceRequirement)
348-
err = r.List(ctx, &currentClusters, &client.ListOptions{LabelSelector: selector})
402+
err = r.Host.List(ctx, &currentClusters, &client.ListOptions{LabelSelector: selector})
349403
if err != nil {
350404
return nil, fmt.Errorf("unable to list current clusters %w", err)
351405
}
@@ -367,7 +421,7 @@ func (r *K3kControlPlaneReconciler) getKubeconfig(ctx context.Context, upstreamC
367421
return nil, fmt.Errorf("unable to extract URL from specificed hostname: %s, %w", restConfig.Host, err)
368422
}
369423

370-
return cfg.Generate(ctx, r.Client, upstreamCluster, u.Hostname())
424+
return cfg.Generate(ctx, r.Host, upstreamCluster, u.Hostname())
371425
}
372426

373427
// createKubeconfigSecret stores the kubeconfig into a secret which can be retrieved by CAPI later on

0 commit comments

Comments
 (0)