Skip to content

Commit

Permalink
feat: add v2prov kubeconfig label patching
Browse files Browse the repository at this point in the history
This change adds a controller that watches for v2prov kubeconfig
secrets. It will then add the owned label (if it doesn't exist) as this
is required by CAPI 1.5.0 and higher.

It has been added as a feature that needs to be enabled so that we can
disable it in the future when the changes in Rancher have merged and are
generally available.

Signed-off-by: Richard Case <richard.case@suse.com>
  • Loading branch information
richardcase committed Oct 6, 2023
1 parent f9cf357 commit 3b24670
Show file tree
Hide file tree
Showing 6 changed files with 349 additions and 0 deletions.
20 changes: 20 additions & 0 deletions feature/feature.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package feature

import (
"k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/component-base/featuregate"
)

const (
// V2ProvKcfgPatch is used to enable patching of the v2prov created kubeconfig secrets so that they
// can be used with CAPI 1.5.x.
V2ProvKcfgPatch featuregate.Feature = "V2ProvKcfgPatch"
)

func init() {
runtime.Must(MutableGates.Add(defaultGates))
}

var defaultGates = map[featuregate.Feature]featuregate.FeatureSpec{
V2ProvKcfgPatch: {Default: false, PreRelease: featuregate.Beta},
}
18 changes: 18 additions & 0 deletions feature/gates.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package feature

import (
"k8s.io/component-base/featuregate"
"sigs.k8s.io/cluster-api/feature"
)

var (
// MutableGates is a mutable version of DefaultFeatureGate.
// Only top-level commands/options setup and the k8s.io/component-base/featuregate/testing package should make use of this.
// Tests that need to modify featuregate gates for the duration of their test should use:
// defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.<FeatureName>, <value>)()
MutableGates featuregate.MutableFeatureGate = feature.MutableGates

// Gates is a shared global FeatureGate.
// Top-level commands/options setup that needs to modify this featuregate gate should use DefaultMutableFeatureGate.
Gates featuregate.FeatureGate = MutableGates
)
173 changes: 173 additions & 0 deletions internal/controllers/patch_kcfg_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/*
Copyright 2023 SUSE.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package controllers

import (
"context"
"fmt"

provisioningv1 "github.com/rancher-sandbox/rancher-turtles/internal/rancher/provisioning/v1"
turtlespredicates "github.com/rancher-sandbox/rancher-turtles/util/predicates"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"
"k8s.io/client-go/util/retry"
capi "sigs.k8s.io/cluster-api/api/v1beta1"
"sigs.k8s.io/cluster-api/controllers/external"
"sigs.k8s.io/cluster-api/controllers/remote"
"sigs.k8s.io/cluster-api/util/predicates"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/log"
)

type V2ProvKcfgSecretReconciler struct {
Client client.Client
RancherClient client.Client
recorder record.EventRecorder
WatchFilterValue string
Scheme *runtime.Scheme

controller controller.Controller
externalTracker external.ObjectTracker
remoteClientGetter remote.ClusterClientGetter
}

func (r *V2ProvKcfgSecretReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, options controller.Options) error {
log := log.FromContext(ctx)

if r.remoteClientGetter == nil {
r.remoteClientGetter = remote.NewClusterClient
}

capiPredicates := predicates.All(log,
turtlespredicates.V2ProvClusterOwned(log),
turtlespredicates.NameHasSuffix(log, "-kubeconfig"),
)

c, err := ctrl.NewControllerManagedBy(mgr).
For(&corev1.Secret{}).
WithOptions(options).
WithEventFilter(capiPredicates).
Build(r)
if err != nil {
return fmt.Errorf("creating new controller: %w", err)
}

r.recorder = mgr.GetEventRecorderFor("rancher-turtles-v2prov")
r.controller = c
r.externalTracker = external.ObjectTracker{
Controller: c,
}

return nil
}

// +kubebuilder:rbac:groups="",resources=secrets;events,verbs=get;list;watch;create;update;patch
// +kubebuilder:rbac:groups=coordination.k8s.io,resources=leases,verbs=get;create;update
// +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=clusters;clusters/status,verbs=get;list;watch
// +kubebuilder:rbac:groups=provisioning.cattle.io,resources=clusters;clusters/status,verbs=get;list;watch

// Reconcile will patch v2prov created kubeconfig secrets to add the required owner label if its missing.
func (r *V2ProvKcfgSecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.Result, reterr error) {
log := log.FromContext(ctx)
log.Info("Reconciling v2prov cluster")

secret := &corev1.Secret{}
if err := r.Client.Get(ctx, req.NamespacedName, secret); err != nil {
if apierrors.IsNotFound(err) {
return ctrl.Result{Requeue: true}, nil
}

return ctrl.Result{Requeue: true}, err
}

_, ok := secret.Labels[capi.ClusterNameLabel]
if ok {
log.V(4).Info("kubeconfig secret %s/%s already has the capi cluster label", secret.Name, secret.Name)

return ctrl.Result{}, nil
}

clusterName, err := r.getClusterName(ctx, secret)
if err != nil {
return ctrl.Result{}, fmt.Errorf("getting cluster name from secret: %w", err)
}

if clusterName == "" {
log.Info("Could not determine cluster name from kubeconfig secret")

return ctrl.Result{}, nil
}

if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error {
secretCopy := secret.DeepCopy()
if secretCopy.Labels == nil {
secretCopy.Labels = map[string]string{}
}
secretCopy.Labels[capi.ClusterNameLabel] = clusterName

patchBase := client.MergeFromWithOptions(secret.DeepCopy(), client.MergeFromWithOptimisticLock{})

if err := r.Client.Patch(ctx, secret, patchBase); err != nil {
return fmt.Errorf("failed to patch secret: %w", err)
}

log.V(4).Info("patched kubeconfig secret", "name", secret.Name, "namespace", secret.Namespace, "cluster", clusterName)

return nil
}); err != nil {
return ctrl.Result{}, err
}

return ctrl.Result{}, nil
}

func (r *V2ProvKcfgSecretReconciler) getClusterName(ctx context.Context, secret *corev1.Secret) (string, error) {
v2ProvClusterName := ""

for _, ref := range secret.OwnerReferences {
if ref.Kind == provisioningv1.GroupVersion.Identifier() {
if ref.Kind != "Cluster" {
v2ProvClusterName = ref.Name

break
}
}
}

if v2ProvClusterName == "" {
return "", nil
}

v2ProvCluster := &provisioningv1.Cluster{}

if err := r.Client.Get(ctx, types.NamespacedName{Name: v2ProvClusterName, Namespace: secret.Namespace}, v2ProvCluster); err != nil {
return "", fmt.Errorf("Getting v2prov cluster: %w", err)
}

for _, ref := range v2ProvCluster.OwnerReferences {
if ref.Kind == "Cluster" && ref.Kind == capi.GroupVersion.Identifier() {
return ref.Name, nil
}
}

return "", nil
}
14 changes: 14 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import (

clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"

"github.com/rancher-sandbox/rancher-turtles/feature"
"github.com/rancher-sandbox/rancher-turtles/internal/controllers"
managementv3 "github.com/rancher-sandbox/rancher-turtles/internal/rancher/management/v3"
provisioningv1 "github.com/rancher-sandbox/rancher-turtles/internal/rancher/provisioning/v1"
Expand Down Expand Up @@ -185,6 +186,19 @@ func setupReconcilers(ctx context.Context, mgr ctrl.Manager) {
setupLog.Error(err, "unable to create capi controller")
os.Exit(1)
}

if feature.Gates.Enabled(feature.V2ProvKcfgPatch) {
setupLog.Info("enabling v2prov kubeconfig secret patching")

if err := (&controllers.V2ProvKcfgSecretReconciler{
Client: mgr.GetClient(),
RancherClient: rancherClient,
WatchFilterValue: watchFilterValue,
}).SetupWithManager(ctx, mgr, controller.Options{MaxConcurrentReconciles: concurrencyNumber}); err != nil {
setupLog.Error(err, "unable to create v2prov kcfg secret controller")
os.Exit(1)
}
}
}

// setupRancherClient can either create a client for an in-cluster installation (rancher and rancher-turtles in the same cluster)
Expand Down
59 changes: 59 additions & 0 deletions util/predicates/naming_redicates.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
Copyright 2023 SUSE.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package predicates

import (
"strings"

"github.com/go-logr/logr"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/predicate"
)

// NameHasSuffix returns a predicate that checks the name of the object has a specific suffix.
func NameHasSuffix(logger logr.Logger, suffix string) predicate.Funcs {
return predicate.Funcs{
UpdateFunc: func(e event.UpdateEvent) bool {
return processIfNameHasSuffix(logger.WithValues("predicate", "NameHasSuffix", "eventType", "update"), e.ObjectNew, suffix)
},
CreateFunc: func(e event.CreateEvent) bool {
return processIfNameHasSuffix(logger.WithValues("predicate", "NameHasSuffix", "eventType", "create"), e.Object, suffix)
},
DeleteFunc: func(e event.DeleteEvent) bool {
return processIfNameHasSuffix(logger.WithValues("predicate", "NameHasSuffix", "eventType", "delete"), e.Object, suffix)
},
GenericFunc: func(e event.GenericEvent) bool {
return processIfNameHasSuffix(logger.WithValues("predicate", "NameHasSuffix", "eventType", "generic"), e.Object, suffix)
},
}
}

func processIfNameHasSuffix(logger logr.Logger, obj client.Object, suffix string) bool {
kind := strings.ToLower(obj.GetObjectKind().GroupVersionKind().Kind)
log := logger.WithValues("namespace", obj.GetNamespace(), kind, obj.GetName())

if strings.HasSuffix(obj.GetName(), suffix) {
log.V(4).Info("Object name has suffix, will attempt to map", "object", obj)

return true
}

log.V(4).Info("Object name doesn't have suffix, will not map resource", "object", obj)

return false
}
65 changes: 65 additions & 0 deletions util/predicates/v2prov_predicates.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
Copyright 2023 SUSE.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package predicates

import (
"strings"

"github.com/go-logr/logr"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/predicate"

provisioningv1 "github.com/rancher-sandbox/rancher-turtles/internal/rancher/provisioning/v1"
)

// V2ProvClusterOwned returns a predicate that checks for a v2prov cluster owner reference.
func V2ProvClusterOwned(logger logr.Logger) predicate.Funcs {
return predicate.Funcs{
UpdateFunc: func(e event.UpdateEvent) bool {
return processIfV2ProvOwned(logger.WithValues("predicate", "V2ProvClusterOwned", "eventType", "update"), e.ObjectNew)
},
CreateFunc: func(e event.CreateEvent) bool {
return processIfV2ProvOwned(logger.WithValues("predicate", "V2ProvClusterOwned", "eventType", "create"), e.Object)
},
DeleteFunc: func(e event.DeleteEvent) bool {
return false
},
GenericFunc: func(e event.GenericEvent) bool {
return false
},
}
}

func processIfV2ProvOwned(logger logr.Logger, obj client.Object) bool {
kind := strings.ToLower(obj.GetObjectKind().GroupVersionKind().Kind)
log := logger.WithValues("namespace", obj.GetNamespace(), kind, obj.GetName())

ownerRefs := obj.GetOwnerReferences()
for _, ref := range ownerRefs {
if ref.Kind == provisioningv1.GroupVersion.Identifier() {
if ref.Kind != "Cluster" {
log.V(4).Info("Object is owned by v2prov cluster, will attempt to map", "object", obj)
return true
}
}
}

log.V(4).Info("No owner reference for v2prov cluster, will not map resource", "object", obj)

return false
}

0 comments on commit 3b24670

Please sign in to comment.