forked from rancher/turtles
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: support creating management.catte.io/v3 cluster
Signed-off-by: Carlos Salas <carlos.salas@suse.com>
- Loading branch information
1 parent
f5ada58
commit 92a3bc4
Showing
10 changed files
with
1,252 additions
and
285 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
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,322 @@ | ||
/* | ||
Copyright © 2023 - 2024 SUSE LLC | ||
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 ( | ||
"bufio" | ||
"context" | ||
"crypto/tls" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"time" | ||
|
||
managementv3 "github.com/rancher-sandbox/rancher-turtles/internal/rancher/management/v3" | ||
Check failure on line 29 in internal/controllers/helpers.go GitHub Actions / lint
|
||
provisioningv1 "github.com/rancher-sandbox/rancher-turtles/internal/rancher/provisioning/v1" | ||
"github.com/rancher-sandbox/rancher-turtles/util" | ||
turtlesannotations "github.com/rancher-sandbox/rancher-turtles/util/annotations" | ||
turtlesnaming "github.com/rancher-sandbox/rancher-turtles/util/naming" | ||
corev1 "k8s.io/api/core/v1" | ||
apierrors "k8s.io/apimachinery/pkg/api/errors" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
yamlDecoder "k8s.io/apimachinery/pkg/util/yaml" | ||
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" | ||
Check failure on line 38 in internal/controllers/helpers.go GitHub Actions / lint
|
||
utilyaml "sigs.k8s.io/cluster-api/util/yaml" | ||
ctrl "sigs.k8s.io/controller-runtime" | ||
"sigs.k8s.io/controller-runtime/pkg/client" | ||
"sigs.k8s.io/controller-runtime/pkg/event" | ||
"sigs.k8s.io/controller-runtime/pkg/handler" | ||
"sigs.k8s.io/controller-runtime/pkg/log" | ||
"sigs.k8s.io/controller-runtime/pkg/predicate" | ||
Check failure on line 45 in internal/controllers/helpers.go GitHub Actions / lint
|
||
) | ||
|
||
const ( | ||
importLabelName = "cluster-api.cattle.io/rancher-auto-import" | ||
ownedLabelName = "cluster-api.cattle.io/owned" | ||
|
||
defaultRequeueDuration = 1 * time.Minute | ||
) | ||
|
||
// CAPIImportStrategy represents a strategy for importing clusters based on the type of recource created. | ||
type CAPIImportStrategy interface { | ||
// CreateClusterResource generates a new cluster resource. | ||
CreateClusterResource(context.Context, client.Client, *clusterv1.Cluster) error | ||
} | ||
|
||
// ProvisioningV1ImportStrategy represents the strategy for importing clusters based on creating provisioning.cattle.io/v1 cluster. | ||
type ProvisioningV1ImportStrategy struct{} | ||
|
||
// ManagementV3ImportStrategy represents the strategy for importing clusters based on creating management.cattle.io/v3 cluster. | ||
type ManagementV3ImportStrategy struct{} | ||
|
||
// CreateClusterResource generates a new provisioning.cattle.io/v1 cluster resource. | ||
func (s ProvisioningV1ImportStrategy) CreateClusterResource(ctx context.Context, r client.Client, capiCluster *clusterv1.Cluster) error { | ||
log := log.FromContext(ctx) | ||
log.Info("##### Creating provisioning.cattle.io/v1 cluster") | ||
if err := r.Create(ctx, &provisioningv1.Cluster{ | ||
ObjectMeta: metav1.ObjectMeta{ | ||
Name: turtlesnaming.Name(capiCluster.Name).ToRancherName(), | ||
Namespace: capiCluster.Namespace, | ||
OwnerReferences: []metav1.OwnerReference{{ | ||
APIVersion: clusterv1.GroupVersion.String(), | ||
Kind: clusterv1.ClusterKind, | ||
Name: capiCluster.Name, | ||
UID: capiCluster.UID, | ||
}}, | ||
Labels: map[string]string{ | ||
ownedLabelName: "", | ||
}, | ||
}, | ||
}); err != nil { | ||
return fmt.Errorf("provisioning.cattle.io/v1: %w", err) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// CreateClusterResource generates a new management.cattle.io/v3 cluster resource. | ||
func (s ManagementV3ImportStrategy) CreateClusterResource(ctx context.Context, r client.Client, capiCluster *clusterv1.Cluster) error { | ||
log := log.FromContext(ctx) | ||
log.Info("##### Creating management.cattle.io/v3 cluster") | ||
// TODO: review cluster resource creation params | ||
if err := r.Create(ctx, &managementv3.Cluster{ | ||
ObjectMeta: metav1.ObjectMeta{ | ||
Name: turtlesnaming.Name(capiCluster.Name).ToRancherName(), | ||
GenerateName: fmt.Sprintf("c-%s", turtlesnaming.Name(capiCluster.Name).ToRancherName()), | ||
Namespace: capiCluster.Namespace, | ||
OwnerReferences: []metav1.OwnerReference{{ | ||
APIVersion: clusterv1.GroupVersion.String(), | ||
Kind: clusterv1.ClusterKind, | ||
Name: capiCluster.Name, | ||
UID: capiCluster.UID, | ||
}}, | ||
Labels: map[string]string{ | ||
ownedLabelName: "", | ||
}, | ||
}, | ||
Spec: managementv3.ClusterSpec{ | ||
DisplayName: fmt.Sprintf("c-displayname-%s", capiCluster.Name), | ||
Description: "c-description", | ||
}, | ||
}); err != nil { | ||
return fmt.Errorf("management.cattle.io/v3: %w", err) | ||
} | ||
return nil | ||
} | ||
|
||
func reconcileDelete(ctx context.Context, capiCluster *clusterv1.Cluster) (ctrl.Result, error) { | ||
log := log.FromContext(ctx) | ||
log.Info("Reconciling rancher cluster deletion") | ||
|
||
// If the Rancher Cluster was already imported, then annotate the CAPI cluster so that we don't auto-import again. | ||
log.Info(fmt.Sprintf("Rancher cluster is being removed, annotating CAPI cluster %s with %s", | ||
capiCluster.Name, | ||
turtlesannotations.ClusterImportedAnnotation)) | ||
|
||
annotations := capiCluster.GetAnnotations() | ||
if annotations == nil { | ||
annotations = map[string]string{} | ||
} | ||
|
||
annotations[turtlesannotations.ClusterImportedAnnotation] = "true" | ||
capiCluster.SetAnnotations(annotations) | ||
|
||
return ctrl.Result{}, nil | ||
} | ||
|
||
func getClusterRegistrationManifest(ctx context.Context, clusterName, namespace string, rancherClient client.Client, insecureSkipVerify bool) (string, error) { | ||
log := log.FromContext(ctx) | ||
|
||
token := &managementv3.ClusterRegistrationToken{ | ||
ObjectMeta: metav1.ObjectMeta{ | ||
Name: clusterName, | ||
Namespace: namespace, | ||
}, | ||
Spec: managementv3.ClusterRegistrationTokenSpec{ | ||
ClusterName: clusterName, | ||
}, | ||
} | ||
err := rancherClient.Get(ctx, client.ObjectKeyFromObject(token), token) | ||
|
||
if client.IgnoreNotFound(err) != nil { | ||
return "", fmt.Errorf("error getting registration token for cluster %s: %w", clusterName, err) | ||
} else if err != nil { | ||
if err := rancherClient.Create(ctx, token); err != nil { | ||
return "", fmt.Errorf("failed to create cluster registration token for cluster %s: %w", clusterName, err) | ||
} | ||
} | ||
|
||
if token.Status.ManifestURL == "" { | ||
return "", nil | ||
} | ||
|
||
manifestData, err := downloadManifest(token.Status.ManifestURL, insecureSkipVerify) | ||
if err != nil { | ||
log.Error(err, "failed downloading import manifest") | ||
return "", err | ||
} | ||
|
||
return manifestData, nil | ||
} | ||
|
||
func rancherClusterToCapiCluster(ctx context.Context, clusterPredicate predicate.Funcs, rancherClient client.Client) handler.MapFunc { | ||
log := log.FromContext(ctx) | ||
|
||
return func(_ context.Context, o client.Object) []ctrl.Request { | ||
capiCluster := &clusterv1.Cluster{ObjectMeta: metav1.ObjectMeta{ | ||
Name: turtlesnaming.Name(o.GetName()).ToCapiName(), | ||
Namespace: o.GetNamespace(), | ||
}} | ||
if err := rancherClient.Get(ctx, client.ObjectKeyFromObject(capiCluster), capiCluster); err != nil { | ||
if !apierrors.IsNotFound(err) { | ||
log.Error(err, "getting capi cluster") | ||
} | ||
|
||
return nil | ||
} | ||
|
||
if !clusterPredicate.Generic(event.GenericEvent{Object: capiCluster}) { | ||
return nil | ||
} | ||
|
||
return []ctrl.Request{{NamespacedName: client.ObjectKey{Namespace: capiCluster.Namespace, Name: capiCluster.Name}}} | ||
} | ||
} | ||
|
||
func namespaceToCapiClusters(ctx context.Context, clusterPredicate predicate.Funcs, rancherClient client.Client) handler.MapFunc { | ||
log := log.FromContext(ctx) | ||
|
||
return func(_ context.Context, o client.Object) []ctrl.Request { | ||
ns, ok := o.(*corev1.Namespace) | ||
if !ok { | ||
log.Error(nil, fmt.Sprintf("Expected a Namespace but got a %T", o)) | ||
return nil | ||
} | ||
|
||
_, autoImport := util.ShouldImport(ns, importLabelName) | ||
if !autoImport { | ||
log.V(2).Info("Namespace doesn't have import annotation label with a true value, skipping") | ||
return nil | ||
} | ||
|
||
capiClusters := &clusterv1.ClusterList{} | ||
if err := rancherClient.List(ctx, capiClusters, client.InNamespace(o.GetNamespace())); err != nil { | ||
log.Error(err, "getting capi cluster") | ||
return nil | ||
} | ||
|
||
if len(capiClusters.Items) == 0 { | ||
log.V(2).Info("No CAPI clusters in namespace, no action") | ||
return nil | ||
} | ||
|
||
reqs := []ctrl.Request{} | ||
|
||
for _, cluster := range capiClusters.Items { | ||
cluster := cluster | ||
if !clusterPredicate.Generic(event.GenericEvent{Object: &cluster}) { | ||
continue | ||
} | ||
|
||
reqs = append(reqs, ctrl.Request{ | ||
NamespacedName: client.ObjectKey{ | ||
Namespace: cluster.Namespace, | ||
Name: cluster.Name, | ||
}, | ||
}) | ||
} | ||
|
||
return reqs | ||
} | ||
} | ||
|
||
func downloadManifest(url string, insecureSkipVerify bool) (string, error) { | ||
client := &http.Client{Transport: &http.Transport{ | ||
TLSClientConfig: &tls.Config{ | ||
InsecureSkipVerify: insecureSkipVerify, //nolint:gosec | ||
}, | ||
}} | ||
|
||
resp, err := client.Get(url) //nolint:gosec,noctx | ||
if err != nil { | ||
return "", fmt.Errorf("downloading manifest: %w", err) | ||
} | ||
defer resp.Body.Close() | ||
|
||
data, err := io.ReadAll(resp.Body) | ||
if err != nil { | ||
return "", fmt.Errorf("reading manifest: %w", err) | ||
} | ||
|
||
return string(data), err | ||
} | ||
|
||
func createImportManifest(ctx context.Context, remoteClient client.Client, in io.Reader) error { | ||
reader := yamlDecoder.NewYAMLReader(bufio.NewReaderSize(in, 4096)) | ||
|
||
for { | ||
raw, err := reader.Read() | ||
if errors.Is(err, io.EOF) { | ||
break | ||
} | ||
|
||
if err != nil { | ||
return err | ||
} | ||
|
||
if err := createRawManifest(ctx, remoteClient, raw); err != nil { | ||
return err | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func createRawManifest(ctx context.Context, remoteClient client.Client, bytes []byte) error { | ||
items, err := utilyaml.ToUnstructured(bytes) | ||
if err != nil { | ||
return fmt.Errorf("error unmarshalling bytes or empty object passed: %w", err) | ||
} | ||
|
||
for _, obj := range items { | ||
if err := createObject(ctx, remoteClient, obj.DeepCopy()); err != nil { | ||
return err | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func createObject(ctx context.Context, c client.Client, obj client.Object) error { | ||
log := log.FromContext(ctx) | ||
gvk := obj.GetObjectKind().GroupVersionKind() | ||
|
||
err := c.Create(ctx, obj) | ||
if apierrors.IsAlreadyExists(err) { | ||
log.V(4).Info("object already exists in remote cluster", "gvk", gvk, "name", obj.GetName(), "namespace", obj.GetNamespace()) | ||
return nil | ||
} | ||
|
||
if err != nil { | ||
return fmt.Errorf("creating object in remote cluster: %w", err) | ||
} | ||
|
||
log.V(4).Info("object was created", "gvk", gvk, "name", obj.GetName(), "namespace", obj.GetNamespace()) | ||
|
||
return nil | ||
} |
Oops, something went wrong.