sigs.k8s.io/cluster-api@v1.7.1/cmd/clusterctl/client/clusterclass.go (about)

     1  /*
     2  Copyright 2021 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package client
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  
    23  	"github.com/pkg/errors"
    24  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    25  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    26  	"sigs.k8s.io/controller-runtime/pkg/client"
    27  
    28  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    29  	"sigs.k8s.io/cluster-api/cmd/clusterctl/client/cluster"
    30  	"sigs.k8s.io/cluster-api/cmd/clusterctl/client/repository"
    31  	"sigs.k8s.io/cluster-api/cmd/clusterctl/internal/scheme"
    32  )
    33  
    34  // addClusterClassIfMissing returns a Template that includes the base template and adds any cluster class definitions that
    35  // are references in the template. If the cluster class referenced already exists in the cluster it is not added to the
    36  // template.
    37  func addClusterClassIfMissing(ctx context.Context, template Template, clusterClassClient repository.ClusterClassClient, clusterClient cluster.Client, targetNamespace string, listVariablesOnly bool) (Template, error) {
    38  	classes, err := clusterClassNamesFromTemplate(template)
    39  	if err != nil {
    40  		return nil, err
    41  	}
    42  	// If the template does not reference any ClusterClass, return early.
    43  	if len(classes) == 0 {
    44  		return template, nil
    45  	}
    46  
    47  	clusterClassesTemplate, err := fetchMissingClusterClassTemplates(ctx, clusterClassClient, clusterClient, classes, targetNamespace, listVariablesOnly)
    48  	if err != nil {
    49  		return nil, err
    50  	}
    51  
    52  	// We intentionally render the ClusterClass before the Cluster resource, as the Cluster
    53  	// is depending on the ClusterClass.
    54  	mergedTemplate, err := repository.MergeTemplates(clusterClassesTemplate, template)
    55  	if err != nil {
    56  		return nil, err
    57  	}
    58  
    59  	return mergedTemplate, nil
    60  }
    61  
    62  // clusterClassNamesFromTemplate returns the list of ClusterClasses referenced
    63  // by clusters defined in the template. If not clusters are defined in the template
    64  // or if no cluster uses a cluster class it returns an empty list.
    65  func clusterClassNamesFromTemplate(template Template) ([]string, error) {
    66  	classes := []string{}
    67  
    68  	// loop through all the objects and if the object is a cluster
    69  	// check and see if cluster.spec.topology.class is defined.
    70  	// If defined, add value to the result.
    71  	for i := range template.Objs() {
    72  		obj := template.Objs()[i]
    73  		if obj.GroupVersionKind().GroupKind() != clusterv1.GroupVersion.WithKind("Cluster").GroupKind() {
    74  			continue
    75  		}
    76  		cluster := &clusterv1.Cluster{}
    77  		if err := scheme.Scheme.Convert(&obj, cluster, nil); err != nil {
    78  			return nil, errors.Wrap(err, "failed to convert object to Cluster")
    79  		}
    80  		if cluster.Spec.Topology == nil {
    81  			continue
    82  		}
    83  		classes = append(classes, cluster.Spec.Topology.Class)
    84  	}
    85  	return classes, nil
    86  }
    87  
    88  // fetchMissingClusterClassTemplates returns a list of templates for ClusterClasses that do not yet exist
    89  // in the cluster. If the cluster is not initialized, all the ClusterClasses are added.
    90  func fetchMissingClusterClassTemplates(ctx context.Context, clusterClassClient repository.ClusterClassClient, clusterClient cluster.Client, classes []string, targetNamespace string, listVariablesOnly bool) (Template, error) {
    91  	// first check if the cluster is initialized.
    92  	// If it is initialized:
    93  	//    For every ClusterClass check if it already exists in the cluster.
    94  	//    If the ClusterClass already exists there is nothing further to do.
    95  	//    If not, get the ClusterClass from the repository
    96  	// If it is not initialized:
    97  	//    For every ClusterClass fetch the class definition from the repository.
    98  
    99  	// Check if the cluster is initialized
   100  	clusterInitialized := false
   101  	var err error
   102  	if err := clusterClient.Proxy().CheckClusterAvailable(ctx); err == nil {
   103  		clusterInitialized, err = clusterClient.ProviderInventory().CheckCAPIInstalled(ctx)
   104  		if err != nil {
   105  			return nil, errors.Wrap(err, "failed to check if the cluster is initialized")
   106  		}
   107  	}
   108  	var c client.Client
   109  	if clusterInitialized {
   110  		c, err = clusterClient.Proxy().NewClient(ctx)
   111  		if err != nil {
   112  			return nil, err
   113  		}
   114  	}
   115  
   116  	// Get the templates for all ClusterClasses and associated objects if the target
   117  	// ClusterClass does not exist in the cluster.
   118  	templates := []repository.Template{}
   119  	for _, class := range classes {
   120  		if clusterInitialized {
   121  			exists, err := clusterClassExists(ctx, c, class, targetNamespace)
   122  			if err != nil {
   123  				return nil, err
   124  			}
   125  			if exists {
   126  				continue
   127  			}
   128  		}
   129  		// The cluster is either not initialized or the ClusterClass does not yet exist in the cluster.
   130  		// Fetch the cluster class to install.
   131  		clusterClassTemplate, err := clusterClassClient.Get(ctx, class, targetNamespace, listVariablesOnly)
   132  		if err != nil {
   133  			return nil, errors.Wrapf(err, "failed to get the cluster class template for %q", class)
   134  		}
   135  
   136  		// If any of the objects in the ClusterClass template already exist in the cluster then
   137  		// we should error out.
   138  		// We do this to avoid adding partial items from the template in the output YAML. This ensures
   139  		// that we do not add a ClusterClass (and associated objects) who definition is unknown.
   140  		if clusterInitialized {
   141  			for _, obj := range clusterClassTemplate.Objs() {
   142  				if exists, err := objExists(ctx, c, obj); err != nil {
   143  					return nil, err
   144  				} else if exists {
   145  					return nil, fmt.Errorf("%s(%s) already exists in the cluster", obj.GetName(), obj.GetObjectKind().GroupVersionKind())
   146  				}
   147  			}
   148  		}
   149  		templates = append(templates, clusterClassTemplate)
   150  	}
   151  
   152  	merged, err := repository.MergeTemplates(templates...)
   153  	if err != nil {
   154  		return nil, err
   155  	}
   156  
   157  	return merged, nil
   158  }
   159  
   160  func clusterClassExists(ctx context.Context, c client.Client, class, targetNamespace string) (bool, error) {
   161  	clusterClass := &clusterv1.ClusterClass{}
   162  	if err := c.Get(ctx, client.ObjectKey{Name: class, Namespace: targetNamespace}, clusterClass); err != nil {
   163  		if apierrors.IsNotFound(err) {
   164  			return false, nil
   165  		}
   166  		return false, errors.Wrapf(err, "failed to check if ClusterClass %q exists in the cluster", class)
   167  	}
   168  	return true, nil
   169  }
   170  
   171  func objExists(ctx context.Context, c client.Client, obj unstructured.Unstructured) (bool, error) {
   172  	o := obj.DeepCopy()
   173  	if err := c.Get(ctx, client.ObjectKeyFromObject(o), o); err != nil {
   174  		if apierrors.IsNotFound(err) {
   175  			return false, nil
   176  		}
   177  		return false, err
   178  	}
   179  	return true, nil
   180  }