sigs.k8s.io/cluster-api@v1.7.1/cmd/clusterctl/client/cluster/installer.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 cluster
    18  
    19  import (
    20  	"context"
    21  	"sort"
    22  	"strings"
    23  	"time"
    24  
    25  	"github.com/pkg/errors"
    26  	appsv1 "k8s.io/api/apps/v1"
    27  	corev1 "k8s.io/api/core/v1"
    28  	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    29  	apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
    30  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    31  	"k8s.io/apimachinery/pkg/runtime/schema"
    32  	"k8s.io/apimachinery/pkg/util/sets"
    33  	"k8s.io/apimachinery/pkg/util/version"
    34  	"k8s.io/apimachinery/pkg/util/wait"
    35  	"sigs.k8s.io/controller-runtime/pkg/client"
    36  
    37  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    38  	clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3"
    39  	"sigs.k8s.io/cluster-api/cmd/clusterctl/client/config"
    40  	"sigs.k8s.io/cluster-api/cmd/clusterctl/client/repository"
    41  	"sigs.k8s.io/cluster-api/cmd/clusterctl/internal/scheme"
    42  	"sigs.k8s.io/cluster-api/cmd/clusterctl/internal/util"
    43  	logf "sigs.k8s.io/cluster-api/cmd/clusterctl/log"
    44  	"sigs.k8s.io/cluster-api/util/contract"
    45  )
    46  
    47  // ProviderInstaller defines methods for enforcing consistency rules for provider installation.
    48  type ProviderInstaller interface {
    49  	// Add adds a provider to the install queue.
    50  	// NB. By deferring the installation, the installer service can perform validation of the target state of the management cluster
    51  	// before actually starting the installation of new providers.
    52  	Add(repository.Components)
    53  
    54  	// Install performs the installation of the providers ready in the install queue.
    55  	Install(context.Context, InstallOptions) ([]repository.Components, error)
    56  
    57  	// Validate performs steps to validate a management cluster by looking at the current state and the providers in the queue.
    58  	// The following checks are performed in order to ensure a fully operational cluster:
    59  	// - There must be only one instance of the same provider
    60  	// - All the providers in must support the same API Version of Cluster API (contract)
    61  	// - All provider CRDs that are referenced in core Cluster API CRDs must comply with the CRD naming scheme,
    62  	//   otherwise a warning is logged.
    63  	Validate(context.Context) error
    64  
    65  	// Images returns the list of images required for installing the providers ready in the install queue.
    66  	Images() []string
    67  }
    68  
    69  // InstallOptions defines the options used to configure installation.
    70  type InstallOptions struct {
    71  	WaitProviders       bool
    72  	WaitProviderTimeout time.Duration
    73  }
    74  
    75  // providerInstaller implements ProviderInstaller.
    76  type providerInstaller struct {
    77  	configClient            config.Client
    78  	repositoryClientFactory RepositoryClientFactory
    79  	proxy                   Proxy
    80  	providerComponents      ComponentsClient
    81  	providerInventory       InventoryClient
    82  	installQueue            []repository.Components
    83  }
    84  
    85  var _ ProviderInstaller = &providerInstaller{}
    86  
    87  func (i *providerInstaller) Add(components repository.Components) {
    88  	i.installQueue = append(i.installQueue, components)
    89  
    90  	// Ensure Providers are installed in the following order: Core, Bootstrap, ControlPlane, Infrastructure.
    91  	sort.Slice(i.installQueue, func(a, b int) bool {
    92  		return i.installQueue[a].Type().Order() < i.installQueue[b].Type().Order()
    93  	})
    94  }
    95  
    96  func (i *providerInstaller) Install(ctx context.Context, opts InstallOptions) ([]repository.Components, error) {
    97  	ret := make([]repository.Components, 0, len(i.installQueue))
    98  	for _, components := range i.installQueue {
    99  		if err := installComponentsAndUpdateInventory(ctx, components, i.providerComponents, i.providerInventory); err != nil {
   100  			return nil, err
   101  		}
   102  
   103  		ret = append(ret, components)
   104  	}
   105  
   106  	return ret, waitForProvidersReady(ctx, opts, i.installQueue, i.proxy)
   107  }
   108  
   109  func installComponentsAndUpdateInventory(ctx context.Context, components repository.Components, providerComponents ComponentsClient, providerInventory InventoryClient) error {
   110  	log := logf.Log
   111  	log.Info("Installing", "Provider", components.ManifestLabel(), "Version", components.Version(), "TargetNamespace", components.TargetNamespace())
   112  
   113  	inventoryObject := components.InventoryObject()
   114  
   115  	log.V(1).Info("Creating objects", "Provider", components.ManifestLabel(), "Version", components.Version(), "TargetNamespace", components.TargetNamespace())
   116  	if err := providerComponents.Create(ctx, components.Objs()); err != nil {
   117  		return err
   118  	}
   119  
   120  	log.V(1).Info("Creating inventory entry", "Provider", components.ManifestLabel(), "Version", components.Version(), "TargetNamespace", components.TargetNamespace())
   121  	return providerInventory.Create(ctx, inventoryObject)
   122  }
   123  
   124  // waitForProvidersReady waits till the installed components are ready.
   125  func waitForProvidersReady(ctx context.Context, opts InstallOptions, installQueue []repository.Components, proxy Proxy) error {
   126  	// If we dont have to wait for providers to be installed
   127  	// return early.
   128  	if !opts.WaitProviders {
   129  		return nil
   130  	}
   131  
   132  	log := logf.Log
   133  	log.Info("Waiting for providers to be available...")
   134  
   135  	return waitManagerDeploymentsReady(ctx, opts, installQueue, proxy)
   136  }
   137  
   138  // waitManagerDeploymentsReady waits till the installed manager deployments are ready.
   139  func waitManagerDeploymentsReady(ctx context.Context, opts InstallOptions, installQueue []repository.Components, proxy Proxy) error {
   140  	for _, components := range installQueue {
   141  		for _, obj := range components.Objs() {
   142  			if util.IsDeploymentWithManager(obj) {
   143  				if err := waitDeploymentReady(ctx, obj, opts.WaitProviderTimeout, proxy); err != nil {
   144  					return errors.Wrapf(err, "deployment %q is not ready after %s", obj.GetName(), opts.WaitProviderTimeout)
   145  				}
   146  			}
   147  		}
   148  	}
   149  	return nil
   150  }
   151  
   152  func waitDeploymentReady(ctx context.Context, deployment unstructured.Unstructured, timeout time.Duration, proxy Proxy) error {
   153  	return wait.PollUntilContextTimeout(ctx, 100*time.Millisecond, timeout, false, func(ctx context.Context) (bool, error) {
   154  		c, err := proxy.NewClient(ctx)
   155  		if err != nil {
   156  			return false, err
   157  		}
   158  		key := client.ObjectKey{
   159  			Namespace: deployment.GetNamespace(),
   160  			Name:      deployment.GetName(),
   161  		}
   162  		dep := &appsv1.Deployment{}
   163  		if err := c.Get(ctx, key, dep); err != nil {
   164  			return false, err
   165  		}
   166  		for _, c := range dep.Status.Conditions {
   167  			if c.Type == appsv1.DeploymentAvailable && c.Status == corev1.ConditionTrue {
   168  				return true, nil
   169  			}
   170  		}
   171  		return false, nil
   172  	})
   173  }
   174  
   175  func (i *providerInstaller) Validate(ctx context.Context) error {
   176  	// Get the list of providers currently in the cluster.
   177  	providerList, err := i.providerInventory.List(ctx)
   178  	if err != nil {
   179  		return err
   180  	}
   181  
   182  	// Starts simulating what will be the resulting management cluster by adding to the list the providers in the installQueue.
   183  	// During this operation following checks are performed:
   184  	// - There must be only one instance of the same provider
   185  	for _, components := range i.installQueue {
   186  		if providerList, err = simulateInstall(providerList, components); err != nil {
   187  			return errors.Wrapf(err, "installing provider %q can lead to a non functioning management cluster", components.ManifestLabel())
   188  		}
   189  	}
   190  
   191  	// Gets the API Version of Cluster API (contract) all the providers in the management cluster must support,
   192  	// which is the same of the core provider.
   193  	providerInstanceContracts := map[string]string{}
   194  
   195  	coreProviders := providerList.FilterCore()
   196  	if len(coreProviders) != 1 {
   197  		return errors.Errorf("invalid management cluster: there should a core provider, found %d", len(coreProviders))
   198  	}
   199  	coreProvider := coreProviders[0]
   200  
   201  	managementClusterContract, err := i.getProviderContract(ctx, providerInstanceContracts, coreProvider)
   202  	if err != nil {
   203  		return err
   204  	}
   205  
   206  	// Checks if all the providers supports the same API Version of Cluster API (contract).
   207  	for _, components := range i.installQueue {
   208  		provider := components.InventoryObject()
   209  
   210  		// Gets the API Version of Cluster API (contract) the provider support and compare it with the management cluster contract.
   211  		providerContract, err := i.getProviderContract(ctx, providerInstanceContracts, provider)
   212  		if err != nil {
   213  			return err
   214  		}
   215  		if providerContract != managementClusterContract {
   216  			return errors.Errorf("installing provider %q can lead to a non functioning management cluster: the target version for the provider supports the %s API Version of Cluster API (contract), while the management cluster is using %s", components.ManifestLabel(), providerContract, managementClusterContract)
   217  		}
   218  	}
   219  
   220  	// Validate if provider CRDs comply with the naming scheme.
   221  	for _, components := range i.installQueue {
   222  		componentObjects := components.Objs()
   223  
   224  		for _, obj := range componentObjects {
   225  			// Continue if object is not a CRD.
   226  			if obj.GetKind() != customResourceDefinitionKind {
   227  				continue
   228  			}
   229  
   230  			gk, err := getCRDGroupKind(obj)
   231  			if err != nil {
   232  				return errors.Wrap(err, "failed to read group and kind from CustomResourceDefinition")
   233  			}
   234  
   235  			if err := validateCRDName(obj, gk); err != nil {
   236  				return err
   237  			}
   238  		}
   239  	}
   240  
   241  	return nil
   242  }
   243  
   244  func getCRDGroupKind(obj unstructured.Unstructured) (*schema.GroupKind, error) {
   245  	var group string
   246  	var kind string
   247  	version := obj.GroupVersionKind().Version
   248  	switch version {
   249  	case apiextensionsv1beta1.SchemeGroupVersion.Version:
   250  		crd := &apiextensionsv1beta1.CustomResourceDefinition{}
   251  		if err := scheme.Scheme.Convert(&obj, crd, nil); err != nil {
   252  			return nil, errors.Wrapf(err, "failed to convert %s CustomResourceDefinition %q", version, obj.GetName())
   253  		}
   254  		group = crd.Spec.Group
   255  		kind = crd.Spec.Names.Kind
   256  	case apiextensionsv1.SchemeGroupVersion.Version:
   257  		crd := &apiextensionsv1.CustomResourceDefinition{}
   258  		if err := scheme.Scheme.Convert(&obj, crd, nil); err != nil {
   259  			return nil, errors.Wrapf(err, "failed to convert %s CustomResourceDefinition %q", version, obj.GetName())
   260  		}
   261  		group = crd.Spec.Group
   262  		kind = crd.Spec.Names.Kind
   263  	default:
   264  		return nil, errors.Errorf("failed to read %s CustomResourceDefinition %q", version, obj.GetName())
   265  	}
   266  	return &schema.GroupKind{Group: group, Kind: kind}, nil
   267  }
   268  
   269  func validateCRDName(obj unstructured.Unstructured, gk *schema.GroupKind) error {
   270  	// Return if CRD has skip CRD name preflight check annotation.
   271  	if _, ok := obj.GetAnnotations()[clusterctlv1.SkipCRDNamePreflightCheckAnnotation]; ok {
   272  		return nil
   273  	}
   274  
   275  	correctCRDName := contract.CalculateCRDName(gk.Group, gk.Kind)
   276  
   277  	// Return if name is correct.
   278  	if obj.GetName() == correctCRDName {
   279  		return nil
   280  	}
   281  
   282  	return errors.Errorf("ERROR: CRD name %q is invalid for a CRD referenced in a core Cluster API CRD,"+
   283  		"it should be %q. Support for CRDs that are referenced in core Cluster API resources with invalid names has been "+
   284  		"dropped. Note: Please check if this CRD is actually referenced in core Cluster API "+
   285  		"CRDs. If not, this warning can be hidden by setting the %q' annotation.", obj.GetName(), correctCRDName, clusterctlv1.SkipCRDNamePreflightCheckAnnotation)
   286  }
   287  
   288  // getProviderContract returns the API Version of Cluster API (contract) for a provider instance.
   289  func (i *providerInstaller) getProviderContract(ctx context.Context, providerInstanceContracts map[string]string, provider clusterctlv1.Provider) (string, error) {
   290  	// If the contract for the provider instance is already known, return it.
   291  	if contract, ok := providerInstanceContracts[provider.InstanceName()]; ok {
   292  		return contract, nil
   293  	}
   294  
   295  	// Otherwise get the contract for the providers instance.
   296  
   297  	// Gets the providers metadata.
   298  	configRepository, err := i.configClient.Providers().Get(provider.ProviderName, provider.GetProviderType())
   299  	if err != nil {
   300  		return "", err
   301  	}
   302  
   303  	providerRepository, err := i.repositoryClientFactory(ctx, configRepository, i.configClient)
   304  	if err != nil {
   305  		return "", err
   306  	}
   307  
   308  	latestMetadata, err := providerRepository.Metadata(provider.Version).Get(ctx)
   309  	if err != nil {
   310  		return "", err
   311  	}
   312  
   313  	// Gets the contract for the current release.
   314  	currentVersion, err := version.ParseSemantic(provider.Version)
   315  	if err != nil {
   316  		return "", errors.Wrapf(err, "failed to parse current version for the %s provider", provider.InstanceName())
   317  	}
   318  
   319  	releaseSeries := latestMetadata.GetReleaseSeriesForVersion(currentVersion)
   320  	if releaseSeries == nil {
   321  		return "", errors.Errorf("invalid provider metadata: version %s for the provider %s does not match any release series", provider.Version, provider.InstanceName())
   322  	}
   323  
   324  	if releaseSeries.Contract != clusterv1.GroupVersion.Version {
   325  		return "", errors.Errorf("current version of clusterctl is only compatible with %s providers, detected %s for provider %s", clusterv1.GroupVersion.Version, releaseSeries.Contract, provider.ManifestLabel())
   326  	}
   327  
   328  	providerInstanceContracts[provider.InstanceName()] = releaseSeries.Contract
   329  	return releaseSeries.Contract, nil
   330  }
   331  
   332  // simulateInstall adds a provider to the list of providers in a cluster (without installing it).
   333  func simulateInstall(providerList *clusterctlv1.ProviderList, components repository.Components) (*clusterctlv1.ProviderList, error) {
   334  	provider := components.InventoryObject()
   335  
   336  	existingInstances := providerList.FilterByProviderNameAndType(provider.ProviderName, provider.GetProviderType())
   337  	if len(existingInstances) > 0 {
   338  		namespaces := func() string {
   339  			var namespaces []string
   340  			for _, provider := range existingInstances {
   341  				namespaces = append(namespaces, provider.Namespace)
   342  			}
   343  			return strings.Join(namespaces, ", ")
   344  		}()
   345  		return providerList, errors.Errorf("there is already an instance of the %q provider installed in the %q namespace", provider.ManifestLabel(), namespaces)
   346  	}
   347  
   348  	providerList.Items = append(providerList.Items, provider)
   349  	return providerList, nil
   350  }
   351  
   352  func (i *providerInstaller) Images() []string {
   353  	ret := sets.Set[string]{}
   354  	for _, components := range i.installQueue {
   355  		ret = ret.Insert(components.Images()...)
   356  	}
   357  	return sets.List(ret)
   358  }
   359  
   360  func newProviderInstaller(configClient config.Client, repositoryClientFactory RepositoryClientFactory, proxy Proxy, providerMetadata InventoryClient, providerComponents ComponentsClient) *providerInstaller {
   361  	return &providerInstaller{
   362  		configClient:            configClient,
   363  		repositoryClientFactory: repositoryClientFactory,
   364  		proxy:                   proxy,
   365  		providerComponents:      providerComponents,
   366  		providerInventory:       providerMetadata,
   367  	}
   368  }