Skip to content

Commit

Permalink
feat: support creating management.catte.io/v3 cluster
Browse files Browse the repository at this point in the history
Signed-off-by: Carlos Salas <carlos.salas@suse.com>
  • Loading branch information
salasberryfin committed Jan 11, 2024
1 parent f5ada58 commit 92a3bc4
Show file tree
Hide file tree
Showing 10 changed files with 1,252 additions and 285 deletions.
4 changes: 4 additions & 0 deletions feature/feature.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ const (
// RancherKubeSecretPatch is used to enable patching of the Rancher v2prov created kubeconfig
// secrets so that they can be used with CAPI 1.5.x.
RancherKubeSecretPatch featuregate.Feature = "rancher-kube-secret-patch" //nolint:gosec

// ManagementV3Cluster is used to enable the management.cattle.io/v3 cluster resource.
ManagementV3Cluster featuregate.Feature = "management-v3-cluster" //nolint:gosec
)

func init() {
Expand All @@ -33,4 +36,5 @@ func init() {

var defaultGates = map[featuregate.Feature]featuregate.FeatureSpec{
RancherKubeSecretPatch: {Default: false, PreRelease: featuregate.Beta},
ManagementV3Cluster: {Default: false, PreRelease: featuregate.Beta},
}
322 changes: 322 additions & 0 deletions internal/controllers/helpers.go
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

View workflow job for this annotation

GitHub Actions / lint

File is not `gci`-ed with --skip-generated -s standard -s blank -s dot -s default -s prefix(sigs.k8s.io/cluster-api) -s prefix(github.com/rancher-sandbox/rancher-turtles) --custom-order (gci)
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

View workflow job for this annotation

GitHub Actions / lint

File is not `gci`-ed with --skip-generated -s standard -s blank -s dot -s default -s prefix(sigs.k8s.io/cluster-api) -s prefix(github.com/rancher-sandbox/rancher-turtles) --custom-order (gci)
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

View workflow job for this annotation

GitHub Actions / lint

File is not `gci`-ed with --skip-generated -s standard -s blank -s dot -s default -s prefix(sigs.k8s.io/cluster-api) -s prefix(github.com/rancher-sandbox/rancher-turtles) --custom-order (gci)
)

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{

Check failure on line 71 in internal/controllers/helpers.go

View workflow job for this annotation

GitHub Actions / lint

if statements should only be cuddled with assignments (wsl)
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

Check failure on line 119 in internal/controllers/helpers.go

View workflow job for this annotation

GitHub Actions / lint

return statements should not be cuddled if block has more than two lines (wsl)
}

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
}
Loading

0 comments on commit 92a3bc4

Please sign in to comment.