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 }