sigs.k8s.io/cluster-api@v1.7.1/cmd/clusterctl/client/repository/components.go (about) 1 /* 2 Copyright 2019 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 "strings" 22 23 "github.com/pkg/errors" 24 admissionregistrationv1 "k8s.io/api/admissionregistration/v1" 25 admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1" 26 rbacv1 "k8s.io/api/rbac/v1" 27 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 28 apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 29 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 31 32 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 33 clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3" 34 "sigs.k8s.io/cluster-api/cmd/clusterctl/client/config" 35 yaml "sigs.k8s.io/cluster-api/cmd/clusterctl/client/yamlprocessor" 36 "sigs.k8s.io/cluster-api/cmd/clusterctl/internal/scheme" 37 "sigs.k8s.io/cluster-api/cmd/clusterctl/internal/util" 38 utilyaml "sigs.k8s.io/cluster-api/util/yaml" 39 ) 40 41 const ( 42 namespaceKind = "Namespace" 43 clusterRoleKind = "ClusterRole" 44 clusterRoleBindingKind = "ClusterRoleBinding" 45 roleBindingKind = "RoleBinding" 46 certificateKind = "Certificate" 47 mutatingWebhookConfigurationKind = "MutatingWebhookConfiguration" 48 validatingWebhookConfigurationKind = "ValidatingWebhookConfiguration" 49 customResourceDefinitionKind = "CustomResourceDefinition" 50 ) 51 52 // Components wraps a YAML file that defines the provider components 53 // to be installed in a management cluster (CRD, Controller, RBAC etc.) 54 // It is important to notice that clusterctl applies a set of processing steps to the “raw” component YAML read 55 // from the provider repositories: 56 // 1. Checks for all the variables in the component YAML file and replace with corresponding config values 57 // 2. Ensure all the provider components are deployed in the target namespace (apply only to namespaced objects) 58 // 3. Ensure all the ClusterRoleBinding which are referencing namespaced objects have the name prefixed with the namespace name 59 // 4. Adds labels to all the components in order to allow easy identification of the provider objects. 60 type Components interface { 61 // Provider holds configuration of the provider the provider components belong to. 62 config.Provider 63 64 // Version of the provider. 65 Version() string 66 67 // Variables required by the provider components. 68 // This value is derived by the component YAML. 69 Variables() []string 70 71 // Images required to install the provider components. 72 // This value is derived by the component YAML. 73 Images() []string 74 75 // TargetNamespace where the provider components will be installed. 76 // By default this value is derived by the component YAML, but it is possible to override it 77 // during the creation of the Components object. 78 TargetNamespace() string 79 80 // InventoryObject returns the clusterctl inventory object representing the provider that will be 81 // generated by this components. 82 InventoryObject() clusterctlv1.Provider 83 84 // Yaml return the provider components in the form of a YAML file. 85 Yaml() ([]byte, error) 86 87 // Objs return the components in the form of a list of Unstructured objects. 88 Objs() []unstructured.Unstructured 89 } 90 91 // components implement Components. 92 type components struct { 93 config.Provider 94 version string 95 variables []string 96 images []string 97 targetNamespace string 98 objs []unstructured.Unstructured 99 } 100 101 // ensure components implement Components. 102 var _ Components = &components{} 103 104 func (c *components) Version() string { 105 return c.version 106 } 107 108 func (c *components) Variables() []string { 109 return c.variables 110 } 111 112 func (c *components) Images() []string { 113 return c.images 114 } 115 116 func (c *components) TargetNamespace() string { 117 return c.targetNamespace 118 } 119 120 func (c *components) InventoryObject() clusterctlv1.Provider { 121 labels := getCommonLabels(c.Provider) 122 labels[clusterctlv1.ClusterctlCoreLabel] = clusterctlv1.ClusterctlCoreLabelInventoryValue 123 124 return clusterctlv1.Provider{ 125 TypeMeta: metav1.TypeMeta{ 126 APIVersion: clusterctlv1.GroupVersion.String(), 127 Kind: "Provider", 128 }, 129 ObjectMeta: metav1.ObjectMeta{ 130 Namespace: c.targetNamespace, 131 Name: c.ManifestLabel(), 132 Labels: labels, 133 }, 134 ProviderName: c.Name(), 135 Type: string(c.Type()), 136 Version: c.version, 137 } 138 } 139 140 func (c *components) Objs() []unstructured.Unstructured { 141 return c.objs 142 } 143 144 func (c *components) Yaml() ([]byte, error) { 145 return utilyaml.FromUnstructured(c.objs) 146 } 147 148 // ComponentsAlterFn defines the function that is used to alter the components.Objs(). 149 type ComponentsAlterFn func(objs []unstructured.Unstructured) ([]unstructured.Unstructured, error) 150 151 // AlterComponents provides a mechanism to alter the component.Objs from outside 152 // the repository module. 153 func AlterComponents(comps Components, alterFn ComponentsAlterFn) error { 154 c, ok := comps.(*components) 155 if !ok { 156 return errors.New("could not alter components as Components is not of the correct type") 157 } 158 159 alteredObjs, err := alterFn(c.Objs()) 160 if err != nil { 161 return err 162 } 163 c.objs = alteredObjs 164 return nil 165 } 166 167 // ComponentsOptions represents specific inputs that are passed in to 168 // clusterctl library. These are user specified inputs. 169 type ComponentsOptions struct { 170 Version string 171 TargetNamespace string 172 // SkipTemplateProcess allows for skipping the call to the template processor, including also variable replacement in the component YAML. 173 // NOTE this works only if the rawYaml is a valid yaml by itself, like e.g when using envsubst/the simple processor. 174 SkipTemplateProcess bool 175 } 176 177 // ComponentsInput represents all the inputs required by NewComponents. 178 type ComponentsInput struct { 179 Provider config.Provider 180 ConfigClient config.Client 181 Processor yaml.Processor 182 RawYaml []byte 183 Options ComponentsOptions 184 } 185 186 // NewComponents returns a new objects embedding a component YAML file 187 // 188 // It is important to notice that clusterctl applies a set of processing steps to the “raw” component YAML read 189 // from the provider repositories: 190 // 1. Checks for all the variables in the component YAML file and replace with corresponding config values 191 // 2. The variables replacement can be skipped using the SkipTemplateProcess flag in the input options 192 // 3. Ensure all the provider components are deployed in the target namespace (apply only to namespaced objects) 193 // 4. Ensure all the ClusterRoleBinding which are referencing namespaced objects have the name prefixed with the namespace name 194 // 5. Adds labels to all the components in order to allow easy identification of the provider objects. 195 func NewComponents(input ComponentsInput) (Components, error) { 196 variables, err := input.Processor.GetVariables(input.RawYaml) 197 if err != nil { 198 return nil, err 199 } 200 201 // If requested, we are skipping the call to the template processor; however, it is important to 202 // notice that this could work only if the rawYaml is a valid yaml by itself. 203 processedYaml := input.RawYaml 204 if !input.Options.SkipTemplateProcess { 205 processedYaml, err = input.Processor.Process(input.RawYaml, input.ConfigClient.Variables().Get) 206 if err != nil { 207 return nil, errors.Wrap(err, "failed to perform variable substitution") 208 } 209 } 210 211 // 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) 212 objs, err := utilyaml.ToUnstructured(processedYaml) 213 if err != nil { 214 return nil, errors.Wrap(err, "failed to parse yaml") 215 } 216 217 // Apply image overrides, if defined 218 objs, err = util.FixImages(objs, func(image string) (string, error) { 219 return input.ConfigClient.ImageMeta().AlterImage(input.Provider.ManifestLabel(), image) 220 }) 221 if err != nil { 222 return nil, errors.Wrap(err, "failed to apply image overrides") 223 } 224 225 // Inspect the list of objects for the images required by the provider component. 226 images, err := util.InspectImages(objs) 227 if err != nil { 228 return nil, errors.Wrap(err, "failed to detect required images") 229 } 230 231 // inspect the list of objects for the default target namespace 232 // the default target namespace is the namespace object defined in the component yaml read from the repository, if any 233 defaultTargetNamespace, err := inspectTargetNamespace(objs) 234 if err != nil { 235 return nil, errors.Wrap(err, "failed to detect default target namespace") 236 } 237 238 // Ensures all the provider components are deployed in the target namespace (apply only to namespaced objects) 239 // if targetNamespace is not specified, then defaultTargetNamespace is used. In case both targetNamespace and defaultTargetNamespace 240 // are empty, an error is returned 241 242 if input.Options.TargetNamespace == "" { 243 input.Options.TargetNamespace = defaultTargetNamespace 244 } 245 246 if input.Options.TargetNamespace == "" { 247 return nil, errors.New("target namespace can't be defaulted. Please specify a target namespace") 248 } 249 250 // add a Namespace object if missing (ensure the targetNamespace will be created) 251 objs = addNamespaceIfMissing(objs, input.Options.TargetNamespace) 252 253 // fix Namespace name in all the objects 254 objs, err = fixTargetNamespace(objs, input.Options.TargetNamespace) 255 if err != nil { 256 return nil, errors.Wrap(err, "failed to set the TargetNamespace on the components") 257 } 258 259 // Add common labels. 260 objs = addCommonLabels(objs, input.Provider) 261 262 return &components{ 263 Provider: input.Provider, 264 version: input.Options.Version, 265 variables: variables, 266 images: images, 267 targetNamespace: input.Options.TargetNamespace, 268 objs: objs, 269 }, nil 270 } 271 272 // inspectTargetNamespace identifies the name of the namespace object contained in the components YAML, if any. 273 // In case more than one Namespace object is identified, an error is returned. 274 func inspectTargetNamespace(objs []unstructured.Unstructured) (string, error) { 275 namespace := "" 276 for _, o := range objs { 277 // if the object has Kind Namespace 278 if o.GetKind() == namespaceKind { 279 // grab the name (or error if there is more than one Namespace object) 280 if namespace != "" { 281 return "", errors.New("Invalid manifest. There should be no more than one resource with Kind Namespace in the provider components yaml") 282 } 283 namespace = o.GetName() 284 } 285 } 286 return namespace, nil 287 } 288 289 // addNamespaceIfMissing adda a Namespace object if missing (this ensure the targetNamespace will be created). 290 func addNamespaceIfMissing(objs []unstructured.Unstructured, targetNamespace string) []unstructured.Unstructured { 291 namespaceObjectFound := false 292 for _, o := range objs { 293 // if the object has Kind Namespace, fix the namespace name 294 if o.GetKind() == namespaceKind { 295 namespaceObjectFound = true 296 } 297 } 298 299 // if there isn't an object with Kind Namespace, add it 300 if !namespaceObjectFound { 301 objs = append(objs, unstructured.Unstructured{ 302 Object: map[string]interface{}{ 303 "kind": namespaceKind, 304 "metadata": map[string]interface{}{ 305 "name": targetNamespace, 306 }, 307 }, 308 }) 309 } 310 311 return objs 312 } 313 314 // fixTargetNamespace ensures all the provider components are deployed in the target namespace (apply only to namespaced objects). 315 func fixTargetNamespace(objs []unstructured.Unstructured, targetNamespace string) ([]unstructured.Unstructured, error) { 316 for i := range objs { 317 o := objs[i] 318 319 // if the object has Kind Namespace, fix the namespace name 320 if o.GetKind() == namespaceKind { 321 o.SetName(targetNamespace) 322 } 323 324 originalNamespace := o.GetNamespace() 325 326 // if the object is namespaced, set the namespace name 327 if util.IsResourceNamespaced(o.GetKind()) { 328 o.SetNamespace(targetNamespace) 329 } 330 331 switch o.GetKind() { 332 case clusterRoleBindingKind: 333 // Convert Unstructured into a typed object 334 binding := &rbacv1.ClusterRoleBinding{} 335 if err := scheme.Scheme.Convert(&o, binding, nil); err != nil { 336 return nil, err 337 } 338 339 // ensure that namespaced subjects refers to targetNamespace 340 for s := range binding.Subjects { 341 if binding.Subjects[s].Namespace != "" { 342 binding.Subjects[s].Namespace = targetNamespace 343 } 344 } 345 346 // Convert ClusterRoleBinding back to Unstructured 347 if err := scheme.Scheme.Convert(binding, &o, nil); err != nil { 348 return nil, err 349 } 350 351 case roleBindingKind: 352 binding := &rbacv1.RoleBinding{} 353 if err := scheme.Scheme.Convert(&o, binding, nil); err != nil { 354 return nil, err 355 } 356 357 // ensure that namespaced subjects refers to targetNamespace 358 for k := range binding.Subjects { 359 if binding.Subjects[k].Namespace != "" { 360 binding.Subjects[k].Namespace = targetNamespace 361 } 362 } 363 364 // Convert RoleBinding back to Unstructured 365 if err := scheme.Scheme.Convert(binding, &o, nil); err != nil { 366 return nil, err 367 } 368 369 case mutatingWebhookConfigurationKind, validatingWebhookConfigurationKind, customResourceDefinitionKind: 370 var err error 371 o, err = fixWebhookNamespaceReferences(o, targetNamespace) 372 if err != nil { 373 return nil, err 374 } 375 376 case certificateKind: 377 var err error 378 o, err = fixCertificate(o, originalNamespace, targetNamespace) 379 if err != nil { 380 return nil, err 381 } 382 } 383 384 objs[i] = o 385 } 386 return objs, nil 387 } 388 389 func fixWebhookNamespaceReferences(o unstructured.Unstructured, targetNamespace string) (unstructured.Unstructured, error) { 390 annotations := o.GetAnnotations() 391 secretNamespacedName, ok := annotations["cert-manager.io/inject-ca-from"] 392 if ok { 393 secretNameSplit := strings.Split(secretNamespacedName, "/") 394 if len(secretNameSplit) != 2 { 395 return o, fmt.Errorf("object %s %s does not have a correct value for cert-manager.io/inject-ca-from", o.GetKind(), o.GetName()) 396 } 397 annotations["cert-manager.io/inject-ca-from"] = targetNamespace + "/" + secretNameSplit[1] 398 o.SetAnnotations(annotations) 399 } 400 401 switch o.GetKind() { 402 case mutatingWebhookConfigurationKind: 403 return fixMutatingWebhookNamespaceReferences(o, targetNamespace) 404 405 case validatingWebhookConfigurationKind: 406 return fixValidatingWebhookNamespaceReferences(o, targetNamespace) 407 408 case customResourceDefinitionKind: 409 return fixCRDWebhookNamespaceReference(o, targetNamespace) 410 } 411 412 return o, errors.Errorf("failed to patch %s %s version", o.GroupVersionKind().Version, o.GetKind()) 413 } 414 415 func fixMutatingWebhookNamespaceReferences(o unstructured.Unstructured, targetNamespace string) (unstructured.Unstructured, error) { 416 version := o.GroupVersionKind().Version 417 switch version { 418 case admissionregistrationv1beta1.SchemeGroupVersion.Version: 419 b := &admissionregistrationv1beta1.MutatingWebhookConfiguration{} 420 if err := scheme.Scheme.Convert(&o, b, nil); err != nil { 421 return o, err 422 } 423 for _, w := range b.Webhooks { 424 if w.ClientConfig.Service != nil { 425 w.ClientConfig.Service.Namespace = targetNamespace 426 } 427 } 428 return o, scheme.Scheme.Convert(b, &o, nil) 429 case admissionregistrationv1.SchemeGroupVersion.Version: 430 b := &admissionregistrationv1.MutatingWebhookConfiguration{} 431 if err := scheme.Scheme.Convert(&o, b, nil); err != nil { 432 return o, err 433 } 434 for _, w := range b.Webhooks { 435 if w.ClientConfig.Service != nil { 436 w.ClientConfig.Service.Namespace = targetNamespace 437 } 438 } 439 return o, scheme.Scheme.Convert(b, &o, nil) 440 } 441 return o, errors.Errorf("failed to patch %s MutatingWebhookConfiguration", version) 442 } 443 444 func fixValidatingWebhookNamespaceReferences(o unstructured.Unstructured, targetNamespace string) (unstructured.Unstructured, error) { 445 version := o.GroupVersionKind().Version 446 switch version { 447 case admissionregistrationv1beta1.SchemeGroupVersion.Version: 448 b := &admissionregistrationv1beta1.ValidatingWebhookConfiguration{} 449 if err := scheme.Scheme.Convert(&o, b, nil); err != nil { 450 return o, err 451 } 452 for _, w := range b.Webhooks { 453 if w.ClientConfig.Service != nil { 454 w.ClientConfig.Service.Namespace = targetNamespace 455 } 456 } 457 return o, scheme.Scheme.Convert(b, &o, nil) 458 case admissionregistrationv1.SchemeGroupVersion.Version: 459 b := &admissionregistrationv1.ValidatingWebhookConfiguration{} 460 if err := scheme.Scheme.Convert(&o, b, nil); err != nil { 461 return o, err 462 } 463 for _, w := range b.Webhooks { 464 if w.ClientConfig.Service != nil { 465 w.ClientConfig.Service.Namespace = targetNamespace 466 } 467 } 468 return o, scheme.Scheme.Convert(b, &o, nil) 469 } 470 return o, errors.Errorf("failed to patch %s ValidatingWebhookConfiguration", version) 471 } 472 473 func fixCRDWebhookNamespaceReference(o unstructured.Unstructured, targetNamespace string) (unstructured.Unstructured, error) { 474 version := o.GroupVersionKind().Version 475 switch version { 476 case apiextensionsv1beta1.SchemeGroupVersion.Version: 477 crd := &apiextensionsv1beta1.CustomResourceDefinition{} 478 if err := scheme.Scheme.Convert(&o, crd, nil); err != nil { 479 return o, err 480 } 481 if crd.Spec.Conversion != nil && crd.Spec.Conversion.WebhookClientConfig != nil && crd.Spec.Conversion.WebhookClientConfig.Service != nil { 482 crd.Spec.Conversion.WebhookClientConfig.Service.Namespace = targetNamespace 483 } 484 return o, scheme.Scheme.Convert(crd, &o, nil) 485 486 case apiextensionsv1.SchemeGroupVersion.Version: 487 crd := &apiextensionsv1.CustomResourceDefinition{} 488 if err := scheme.Scheme.Convert(&o, crd, nil); err != nil { 489 return o, err 490 } 491 if crd.Spec.Conversion != nil && crd.Spec.Conversion.Webhook != nil && crd.Spec.Conversion.Webhook.ClientConfig != nil && crd.Spec.Conversion.Webhook.ClientConfig.Service != nil { 492 crd.Spec.Conversion.Webhook.ClientConfig.Service.Namespace = targetNamespace 493 } 494 return o, scheme.Scheme.Convert(crd, &o, nil) 495 } 496 return o, errors.Errorf("failed to patch %s CustomResourceDefinition", version) 497 } 498 499 // fixCertificate fixes the dnsNames of cert-manager Certificates. The DNS names contain the dns names of the provider 500 // services (including the namespace) and thus have to be modified to use the target namespace instead. 501 func fixCertificate(o unstructured.Unstructured, originalNamespace, targetNamespace string) (unstructured.Unstructured, error) { 502 dnsNames, ok, err := unstructured.NestedStringSlice(o.UnstructuredContent(), "spec", "dnsNames") 503 if err != nil { 504 return o, errors.Wrapf(err, "failed to get .spec.dnsNames from Certificate %s/%s", o.GetNamespace(), o.GetName()) 505 } 506 // Return if we don't find .spec.dnsNames. 507 if !ok { 508 return o, nil 509 } 510 511 // Iterate through dnsNames and adjust the namespace. 512 // The dnsNames slice usually looks like this: 513 // - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc 514 // - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc.cluster.local 515 for i, dnsName := range dnsNames { 516 dnsNames[i] = strings.Replace(dnsName, fmt.Sprintf(".%s.", originalNamespace), fmt.Sprintf(".%s.", targetNamespace), 1) 517 } 518 519 if err := unstructured.SetNestedStringSlice(o.UnstructuredContent(), dnsNames, "spec", "dnsNames"); err != nil { 520 return o, errors.Wrapf(err, "failed to set .spec.dnsNames to Certificate %s/%s", o.GetNamespace(), o.GetName()) 521 } 522 523 return o, nil 524 } 525 526 // addCommonLabels ensures all the provider components have a consistent set of labels. 527 func addCommonLabels(objs []unstructured.Unstructured, provider config.Provider) []unstructured.Unstructured { 528 for _, o := range objs { 529 labels := o.GetLabels() 530 if labels == nil { 531 labels = map[string]string{} 532 } 533 for k, v := range getCommonLabels(provider) { 534 labels[k] = v 535 } 536 o.SetLabels(labels) 537 } 538 539 return objs 540 } 541 542 func getCommonLabels(provider config.Provider) map[string]string { 543 return map[string]string{ 544 clusterctlv1.ClusterctlLabel: "", 545 clusterv1.ProviderNameLabel: provider.ManifestLabel(), 546 } 547 }