sigs.k8s.io/cluster-api@v1.7.1/cmd/clusterctl/client/repository/template.go (about) 1 /* 2 Copyright 2020 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 repository 18 19 import ( 20 "fmt" 21 22 "github.com/pkg/errors" 23 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 24 "k8s.io/apimachinery/pkg/util/sets" 25 26 "sigs.k8s.io/cluster-api/cmd/clusterctl/client/config" 27 yaml "sigs.k8s.io/cluster-api/cmd/clusterctl/client/yamlprocessor" 28 utilyaml "sigs.k8s.io/cluster-api/util/yaml" 29 ) 30 31 // Template wraps a YAML file that defines the cluster objects (Cluster, Machines etc.). 32 // It is important to notice that clusterctl applies a set of processing steps to the “raw” cluster template YAML read 33 // from the provider repositories: 34 // 1. Checks for all the variables in the cluster template YAML file and replace with corresponding config values 35 // 2. Ensure all the cluster objects are deployed in the target namespace. 36 type Template interface { 37 // Variables used by the template. 38 // This value is derived from the template YAML. 39 Variables() []string 40 41 // VariableMap used by the template with their default values. If the value is `nil`, there is no 42 // default and the variable is required. 43 // This value is derived from the template YAML. 44 VariableMap() map[string]*string 45 46 // TargetNamespace where the template objects will be installed. 47 TargetNamespace() string 48 49 // Yaml returns yaml defining all the cluster template objects as a byte array. 50 Yaml() ([]byte, error) 51 52 // Objs returns the cluster template as a list of Unstructured objects. 53 Objs() []unstructured.Unstructured 54 } 55 56 // template implements Template. 57 type template struct { 58 variables []string 59 variableMap map[string]*string 60 targetNamespace string 61 objs []unstructured.Unstructured 62 } 63 64 // Ensures template implements the Template interface. 65 var _ Template = &template{} 66 67 func (t *template) Variables() []string { 68 return t.variables 69 } 70 71 func (t *template) VariableMap() map[string]*string { 72 return t.variableMap 73 } 74 75 func (t *template) TargetNamespace() string { 76 return t.targetNamespace 77 } 78 79 func (t *template) Objs() []unstructured.Unstructured { 80 return t.objs 81 } 82 83 func (t *template) Yaml() ([]byte, error) { 84 return utilyaml.FromUnstructured(t.objs) 85 } 86 87 // TemplateInput is an input struct for NewTemplate. 88 type TemplateInput struct { 89 RawArtifact []byte 90 ConfigVariablesClient config.VariablesClient 91 Processor yaml.Processor 92 TargetNamespace string 93 SkipTemplateProcess bool 94 } 95 96 // NewTemplate returns a new objects embedding a cluster template YAML file. 97 func NewTemplate(input TemplateInput) (Template, error) { 98 variables, err := input.Processor.GetVariables(input.RawArtifact) 99 if err != nil { 100 return nil, err 101 } 102 103 variableMap, err := input.Processor.GetVariableMap(input.RawArtifact) 104 if err != nil { 105 return nil, err 106 } 107 108 if input.SkipTemplateProcess { 109 return &template{ 110 variables: variables, 111 variableMap: variableMap, 112 targetNamespace: input.TargetNamespace, 113 }, nil 114 } 115 116 processedYaml, err := input.Processor.Process(input.RawArtifact, input.ConfigVariablesClient.Get) 117 if err != nil { 118 return nil, err 119 } 120 121 // Transform the yaml in a list of objects, so following transformation can work on typed objects (instead of working on a string/slice of bytes). 122 objs, err := utilyaml.ToUnstructured(processedYaml) 123 if err != nil { 124 return nil, errors.Wrap(err, "failed to parse yaml") 125 } 126 127 if input.TargetNamespace != "" { 128 // Ensures all the template components are deployed in the target namespace (applies only to namespaced objects) 129 // This is required in order to ensure a cluster and all the related objects are in a single namespace, that is a requirement for 130 // the clusterctl move operation (and also for many controller reconciliation loops). 131 objs, err = fixTargetNamespace(objs, input.TargetNamespace) 132 if err != nil { 133 return nil, errors.Wrap(err, "failed to set the TargetNamespace in the template") 134 } 135 } 136 137 return &template{ 138 variables: variables, 139 variableMap: variableMap, 140 targetNamespace: input.TargetNamespace, 141 objs: objs, 142 }, nil 143 } 144 145 // MergeTemplates merges the provided Templates into one Template. 146 // Notes on the merge operation: 147 // - The merge operation returns an error if all the templates do not have the same TargetNamespace. 148 // - The Variables of the resulting template is a union of all Variables in the templates. 149 // - The default value is picked from the first template that defines it. 150 // The defaults of the same variable in the subsequent templates will be ignored. 151 // (e.g when merging a cluster template and its ClusterClass, the default value from the template takes precedence) 152 // - The Objs of the final template will be a union of all the Objs in the templates. 153 func MergeTemplates(templates ...Template) (Template, error) { 154 templates = filterNilTemplates(templates...) 155 if len(templates) == 0 { 156 return nil, nil 157 } 158 159 merged := &template{ 160 variables: []string{}, 161 variableMap: map[string]*string{}, 162 objs: []unstructured.Unstructured{}, 163 targetNamespace: templates[0].TargetNamespace(), 164 } 165 166 for _, tmpl := range templates { 167 merged.variables = sets.List(sets.Set[string]{}.Insert(merged.variables...).Union(sets.Set[string]{}.Insert(tmpl.Variables()...))) 168 169 for key, val := range tmpl.VariableMap() { 170 if v, ok := merged.variableMap[key]; !ok || v == nil { 171 merged.variableMap[key] = val 172 } 173 } 174 175 if merged.targetNamespace != tmpl.TargetNamespace() { 176 return nil, fmt.Errorf("cannot merge templates with different targetNamespaces") 177 } 178 179 merged.objs = append(merged.objs, tmpl.Objs()...) 180 } 181 182 return merged, nil 183 } 184 185 func filterNilTemplates(templates ...Template) []Template { 186 res := []Template{} 187 for _, tmpl := range templates { 188 if tmpl != nil { 189 res = append(res, tmpl) 190 } 191 } 192 return res 193 }