-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add v2prov kubeconfig label patching
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
1 parent
f9cf357
commit 3b24670
Showing
6 changed files
with
349 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |