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  }