sigs.k8s.io/cluster-api@v1.6.3/cmd/clusterctl/client/cluster/inventory.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  	"fmt"
    22  	"time"
    23  
    24  	"github.com/pkg/errors"
    25  	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    26  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    27  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    28  	"k8s.io/apimachinery/pkg/types"
    29  	kerrors "k8s.io/apimachinery/pkg/util/errors"
    30  	"k8s.io/apimachinery/pkg/util/sets"
    31  	"sigs.k8s.io/controller-runtime/pkg/client"
    32  
    33  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    34  	clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3"
    35  	"sigs.k8s.io/cluster-api/cmd/clusterctl/config"
    36  	logf "sigs.k8s.io/cluster-api/cmd/clusterctl/log"
    37  	utilyaml "sigs.k8s.io/cluster-api/util/yaml"
    38  )
    39  
    40  const (
    41  	waitInventoryCRDInterval = 250 * time.Millisecond
    42  	waitInventoryCRDTimeout  = 1 * time.Minute
    43  )
    44  
    45  // CheckCAPIContractOption is some configuration that modifies options for CheckCAPIContract.
    46  type CheckCAPIContractOption interface {
    47  	// Apply applies this configuration to the given CheckCAPIContractOptions.
    48  	Apply(*CheckCAPIContractOptions)
    49  }
    50  
    51  // CheckCAPIContractOptions contains options for CheckCAPIContract.
    52  type CheckCAPIContractOptions struct {
    53  	// AllowCAPINotInstalled instructs CheckCAPIContract to tolerate management clusters without Cluster API installed yet.
    54  	AllowCAPINotInstalled bool
    55  
    56  	// AllowCAPIContracts instructs CheckCAPIContract to tolerate management clusters with Cluster API with the given contract.
    57  	AllowCAPIContracts []string
    58  
    59  	// AllowCAPIAnyContract instructs CheckCAPIContract to tolerate management clusters with Cluster API installed with any contract.
    60  	AllowCAPIAnyContract bool
    61  }
    62  
    63  // AllowCAPINotInstalled instructs CheckCAPIContract to tolerate management clusters without Cluster API installed yet.
    64  // NOTE: This allows clusterctl init to run on empty management clusters.
    65  type AllowCAPINotInstalled struct{}
    66  
    67  // Apply applies this configuration to the given CheckCAPIContractOptions.
    68  func (t AllowCAPINotInstalled) Apply(in *CheckCAPIContractOptions) {
    69  	in.AllowCAPINotInstalled = true
    70  }
    71  
    72  // AllowCAPIAnyContract instructs CheckCAPIContract to tolerate management clusters with Cluster API with any contract.
    73  // NOTE: This allows clusterctl generate cluster with managed topologies to work properly by performing checks to see if CAPI is installed.
    74  type AllowCAPIAnyContract struct{}
    75  
    76  // Apply applies this configuration to the given CheckCAPIContractOptions.
    77  func (t AllowCAPIAnyContract) Apply(in *CheckCAPIContractOptions) {
    78  	in.AllowCAPIAnyContract = true
    79  }
    80  
    81  // AllowCAPIContract instructs CheckCAPIContract to tolerate management clusters with Cluster API with the given contract.
    82  // NOTE: This allows clusterctl upgrade to work on management clusters with old contract.
    83  type AllowCAPIContract struct {
    84  	Contract string
    85  }
    86  
    87  // Apply applies this configuration to the given CheckCAPIContractOptions.
    88  func (t AllowCAPIContract) Apply(in *CheckCAPIContractOptions) {
    89  	in.AllowCAPIContracts = append(in.AllowCAPIContracts, t.Contract)
    90  }
    91  
    92  // InventoryClient exposes methods to interface with a cluster's provider inventory.
    93  type InventoryClient interface {
    94  	// EnsureCustomResourceDefinitions installs the CRD required for creating inventory items, if necessary.
    95  	// Nb. In order to provide a simpler out-of-the box experience, the inventory CRD
    96  	// is embedded in the clusterctl binary.
    97  	EnsureCustomResourceDefinitions(ctx context.Context) error
    98  
    99  	// Create an inventory item for a provider instance installed in the cluster.
   100  	Create(context.Context, clusterctlv1.Provider) error
   101  
   102  	// List returns the inventory items for all the provider instances installed in the cluster.
   103  	List(ctx context.Context) (*clusterctlv1.ProviderList, error)
   104  
   105  	// GetDefaultProviderName returns the default provider for a given ProviderType.
   106  	// In case there is only a single provider for a given type, e.g. only the AWS infrastructure Provider, it returns
   107  	// this as the default provider; In case there are more provider of the same type, there is no default provider.
   108  	GetDefaultProviderName(ctx context.Context, providerType clusterctlv1.ProviderType) (string, error)
   109  
   110  	// GetProviderVersion returns the version for a given provider.
   111  	GetProviderVersion(ctx context.Context, provider string, providerType clusterctlv1.ProviderType) (string, error)
   112  
   113  	// GetProviderNamespace returns the namespace for a given provider.
   114  	GetProviderNamespace(ctx context.Context, provider string, providerType clusterctlv1.ProviderType) (string, error)
   115  
   116  	// CheckCAPIContract checks the Cluster API version installed in the management cluster, and fails if this version
   117  	// does not match the current one supported by clusterctl.
   118  	CheckCAPIContract(context.Context, ...CheckCAPIContractOption) error
   119  
   120  	// CheckCAPIInstalled checks if Cluster API is installed on the management cluster.
   121  	CheckCAPIInstalled(ctx context.Context) (bool, error)
   122  
   123  	// CheckSingleProviderInstance ensures that only one instance of a provider is running, returns error otherwise.
   124  	CheckSingleProviderInstance(ctx context.Context) error
   125  }
   126  
   127  // inventoryClient implements InventoryClient.
   128  type inventoryClient struct {
   129  	proxy               Proxy
   130  	pollImmediateWaiter PollImmediateWaiter
   131  }
   132  
   133  // ensure inventoryClient implements InventoryClient.
   134  var _ InventoryClient = &inventoryClient{}
   135  
   136  // newInventoryClient returns a inventoryClient.
   137  func newInventoryClient(proxy Proxy, pollImmediateWaiter PollImmediateWaiter) *inventoryClient {
   138  	return &inventoryClient{
   139  		proxy:               proxy,
   140  		pollImmediateWaiter: pollImmediateWaiter,
   141  	}
   142  }
   143  
   144  func (p *inventoryClient) EnsureCustomResourceDefinitions(ctx context.Context) error {
   145  	log := logf.Log
   146  
   147  	if err := p.proxy.ValidateKubernetesVersion(); err != nil {
   148  		return err
   149  	}
   150  
   151  	// Being this the first connection of many clusterctl operations, we want to fail fast if there is no
   152  	// connectivity to the cluster, so we try to get a client as a first thing.
   153  	// NB. NewClient has an internal retry loop that should mitigate temporary connection glitch; here we are
   154  	// trying to detect persistent connection problems (>10s) before entering in longer retry loops while executing
   155  	// clusterctl operations.
   156  	_, err := p.proxy.NewClient()
   157  	if err != nil {
   158  		return err
   159  	}
   160  
   161  	// Check the CRDs already exists, if yes, exit immediately.
   162  	// Nb. The operation is wrapped in a retry loop to make EnsureCustomResourceDefinitions more resilient to unexpected conditions.
   163  	var crdIsIstalled bool
   164  	listInventoryBackoff := newReadBackoff()
   165  	if err := retryWithExponentialBackoff(ctx, listInventoryBackoff, func(ctx context.Context) error {
   166  		var err error
   167  		crdIsIstalled, err = checkInventoryCRDs(ctx, p.proxy)
   168  		return err
   169  	}); err != nil {
   170  		return err
   171  	}
   172  	if crdIsIstalled {
   173  		return nil
   174  	}
   175  
   176  	log.V(1).Info("Installing the clusterctl inventory CRD")
   177  
   178  	// Transform the yaml in a list of objects.
   179  	objs, err := utilyaml.ToUnstructured(config.ClusterctlAPIManifest)
   180  	if err != nil {
   181  		return errors.Wrap(err, "failed to parse yaml for clusterctl inventory CRDs")
   182  	}
   183  
   184  	// Install the CRDs.
   185  	createInventoryObjectBackoff := newWriteBackoff()
   186  	for i := range objs {
   187  		o := objs[i]
   188  		log.V(5).Info("Creating", logf.UnstructuredToValues(o)...)
   189  
   190  		// Create the Kubernetes object.
   191  		// Nb. The operation is wrapped in a retry loop to make EnsureCustomResourceDefinitions more resilient to unexpected conditions.
   192  		if err := retryWithExponentialBackoff(ctx, createInventoryObjectBackoff, func(ctx context.Context) error {
   193  			return p.createObj(ctx, o)
   194  		}); err != nil {
   195  			return err
   196  		}
   197  
   198  		// If the object is a CRDs, waits for it being Established.
   199  		if apiextensionsv1.SchemeGroupVersion.WithKind("CustomResourceDefinition").GroupKind() == o.GroupVersionKind().GroupKind() {
   200  			crdKey := client.ObjectKeyFromObject(&o)
   201  			if err := p.pollImmediateWaiter(ctx, waitInventoryCRDInterval, waitInventoryCRDTimeout, func(ctx context.Context) (bool, error) {
   202  				c, err := p.proxy.NewClient()
   203  				if err != nil {
   204  					return false, err
   205  				}
   206  
   207  				crd := &apiextensionsv1.CustomResourceDefinition{}
   208  				if err := c.Get(ctx, crdKey, crd); err != nil {
   209  					return false, err
   210  				}
   211  
   212  				for _, c := range crd.Status.Conditions {
   213  					if c.Type == apiextensionsv1.Established && c.Status == apiextensionsv1.ConditionTrue {
   214  						return true, nil
   215  					}
   216  				}
   217  				return false, nil
   218  			}); err != nil {
   219  				return errors.Wrapf(err, "failed to scale deployment")
   220  			}
   221  		}
   222  	}
   223  
   224  	return nil
   225  }
   226  
   227  // checkInventoryCRDs checks if the inventory CRDs are installed in the cluster.
   228  func checkInventoryCRDs(ctx context.Context, proxy Proxy) (bool, error) {
   229  	c, err := proxy.NewClient()
   230  	if err != nil {
   231  		return false, err
   232  	}
   233  
   234  	crd := &apiextensionsv1.CustomResourceDefinition{}
   235  	if err := c.Get(ctx, client.ObjectKey{Name: fmt.Sprintf("providers.%s", clusterctlv1.GroupVersion.Group)}, crd); err != nil {
   236  		if apierrors.IsNotFound(err) {
   237  			return false, nil
   238  		}
   239  		return false, errors.Wrap(err, "failed to check if the clusterctl inventory CRD exists")
   240  	}
   241  
   242  	for _, version := range crd.Spec.Versions {
   243  		if version.Name == clusterctlv1.GroupVersion.Version {
   244  			return true, nil
   245  		}
   246  	}
   247  	return true, errors.Errorf("clusterctl inventory CRD does not defines the %s version", clusterctlv1.GroupVersion.Version)
   248  }
   249  
   250  func (p *inventoryClient) createObj(ctx context.Context, o unstructured.Unstructured) error {
   251  	c, err := p.proxy.NewClient()
   252  	if err != nil {
   253  		return err
   254  	}
   255  
   256  	labels := o.GetLabels()
   257  	if labels == nil {
   258  		labels = map[string]string{}
   259  	}
   260  	labels[clusterctlv1.ClusterctlCoreLabel] = clusterctlv1.ClusterctlCoreLabelInventoryValue
   261  	o.SetLabels(labels)
   262  
   263  	if err := c.Create(ctx, &o); err != nil {
   264  		if apierrors.IsAlreadyExists(err) {
   265  			return nil
   266  		}
   267  		return errors.Wrapf(err, "failed to create clusterctl inventory CRDs component: %s, %s/%s", o.GroupVersionKind(), o.GetNamespace(), o.GetName())
   268  	}
   269  	return nil
   270  }
   271  
   272  func (p *inventoryClient) Create(ctx context.Context, m clusterctlv1.Provider) error {
   273  	// Create the Kubernetes object.
   274  	createInventoryObjectBackoff := newWriteBackoff()
   275  	return retryWithExponentialBackoff(ctx, createInventoryObjectBackoff, func(ctx context.Context) error {
   276  		cl, err := p.proxy.NewClient()
   277  		if err != nil {
   278  			return err
   279  		}
   280  
   281  		currentProvider := &clusterctlv1.Provider{}
   282  		key := client.ObjectKey{
   283  			Namespace: m.Namespace,
   284  			Name:      m.Name,
   285  		}
   286  		if err := cl.Get(ctx, key, currentProvider); err != nil {
   287  			if !apierrors.IsNotFound(err) {
   288  				return errors.Wrapf(err, "failed to get current provider object")
   289  			}
   290  
   291  			// if it does not exists, create the provider object
   292  			if err := cl.Create(ctx, &m); err != nil {
   293  				return errors.Wrapf(err, "failed to create provider object")
   294  			}
   295  			return nil
   296  		}
   297  
   298  		// otherwise patch the provider object
   299  		// NB. we are using client.Merge PatchOption so the new objects gets compared with the current one server side
   300  		m.SetResourceVersion(currentProvider.GetResourceVersion())
   301  		if err := cl.Patch(ctx, &m, client.Merge); err != nil {
   302  			return errors.Wrapf(err, "failed to patch provider object")
   303  		}
   304  
   305  		return nil
   306  	})
   307  }
   308  
   309  func (p *inventoryClient) List(ctx context.Context) (*clusterctlv1.ProviderList, error) {
   310  	providerList := &clusterctlv1.ProviderList{}
   311  
   312  	listProvidersBackoff := newReadBackoff()
   313  	if err := retryWithExponentialBackoff(ctx, listProvidersBackoff, func(ctx context.Context) error {
   314  		return listProviders(ctx, p.proxy, providerList)
   315  	}); err != nil {
   316  		return nil, err
   317  	}
   318  
   319  	return providerList, nil
   320  }
   321  
   322  // listProviders retrieves the list of provider inventory objects.
   323  func listProviders(ctx context.Context, proxy Proxy, providerList *clusterctlv1.ProviderList) error {
   324  	cl, err := proxy.NewClient()
   325  	if err != nil {
   326  		return err
   327  	}
   328  
   329  	if err := cl.List(ctx, providerList); err != nil {
   330  		return errors.Wrap(err, "failed get providers")
   331  	}
   332  	return nil
   333  }
   334  
   335  func (p *inventoryClient) GetDefaultProviderName(ctx context.Context, providerType clusterctlv1.ProviderType) (string, error) {
   336  	providerList, err := p.List(ctx)
   337  	if err != nil {
   338  		return "", err
   339  	}
   340  
   341  	// Group the providers by name, because we consider more instance of the same provider not relevant for the answer.
   342  	names := sets.Set[string]{}
   343  	for _, p := range providerList.FilterByType(providerType) {
   344  		names.Insert(p.ProviderName)
   345  	}
   346  
   347  	// If there is only one provider, this is the default
   348  	if names.Len() == 1 {
   349  		return sets.List(names)[0], nil
   350  	}
   351  
   352  	// There is no provider or more than one provider of this type; in both cases, a default provider name cannot be decided.
   353  	return "", nil
   354  }
   355  
   356  func (p *inventoryClient) GetProviderVersion(ctx context.Context, provider string, providerType clusterctlv1.ProviderType) (string, error) {
   357  	providerList, err := p.List(ctx)
   358  	if err != nil {
   359  		return "", err
   360  	}
   361  
   362  	// Group the provider instances by version.
   363  	versions := sets.Set[string]{}
   364  	for _, p := range providerList.FilterByProviderNameAndType(provider, providerType) {
   365  		versions.Insert(p.Version)
   366  	}
   367  
   368  	if versions.Len() == 1 {
   369  		return sets.List(versions)[0], nil
   370  	}
   371  
   372  	// The default version for this provider cannot be decided.
   373  	return "", nil
   374  }
   375  
   376  func (p *inventoryClient) GetProviderNamespace(ctx context.Context, provider string, providerType clusterctlv1.ProviderType) (string, error) {
   377  	providerList, err := p.List(ctx)
   378  	if err != nil {
   379  		return "", err
   380  	}
   381  
   382  	// Group the providers by namespace
   383  	namespaces := sets.Set[string]{}
   384  	for _, p := range providerList.FilterByProviderNameAndType(provider, providerType) {
   385  		namespaces.Insert(p.Namespace)
   386  	}
   387  
   388  	if namespaces.Len() == 1 {
   389  		return sets.List(namespaces)[0], nil
   390  	}
   391  
   392  	// The default provider namespace cannot be decided.
   393  	return "", nil
   394  }
   395  
   396  func (p *inventoryClient) CheckCAPIContract(ctx context.Context, options ...CheckCAPIContractOption) error {
   397  	opt := &CheckCAPIContractOptions{}
   398  	for _, o := range options {
   399  		o.Apply(opt)
   400  	}
   401  
   402  	c, err := p.proxy.NewClient()
   403  	if err != nil {
   404  		return err
   405  	}
   406  
   407  	crd := &apiextensionsv1.CustomResourceDefinition{}
   408  	if err := c.Get(ctx, client.ObjectKey{Name: fmt.Sprintf("clusters.%s", clusterv1.GroupVersion.Group)}, crd); err != nil {
   409  		if opt.AllowCAPINotInstalled && apierrors.IsNotFound(err) {
   410  			return nil
   411  		}
   412  		return errors.Wrap(err, "failed to check Cluster API version")
   413  	}
   414  
   415  	if opt.AllowCAPIAnyContract {
   416  		return nil
   417  	}
   418  
   419  	for _, version := range crd.Spec.Versions {
   420  		if version.Storage {
   421  			if version.Name == clusterv1.GroupVersion.Version {
   422  				return nil
   423  			}
   424  			for _, allowedContract := range opt.AllowCAPIContracts {
   425  				if version.Name == allowedContract {
   426  					return nil
   427  				}
   428  			}
   429  			return errors.Errorf("this version of clusterctl could be used only with %q management clusters, %q detected", clusterv1.GroupVersion.Version, version.Name)
   430  		}
   431  	}
   432  	return errors.Errorf("failed to check Cluster API version")
   433  }
   434  
   435  func (p *inventoryClient) CheckCAPIInstalled(ctx context.Context) (bool, error) {
   436  	if err := p.CheckCAPIContract(ctx, AllowCAPIAnyContract{}); err != nil {
   437  		if apierrors.IsNotFound(err) {
   438  			// The expected CRDs are not installed on the management. This would mean that Cluster API is not installed on the cluster.
   439  			return false, nil
   440  		}
   441  		return false, err
   442  	}
   443  	return true, nil
   444  }
   445  
   446  func (p *inventoryClient) CheckSingleProviderInstance(ctx context.Context) error {
   447  	providers, err := p.List(ctx)
   448  	if err != nil {
   449  		return err
   450  	}
   451  
   452  	providerGroups := make(map[string][]string)
   453  	for _, p := range providers.Items {
   454  		namespacedName := types.NamespacedName{Namespace: p.Namespace, Name: p.Name}.String()
   455  		if providers, ok := providerGroups[p.ManifestLabel()]; ok {
   456  			providerGroups[p.ManifestLabel()] = append(providers, namespacedName)
   457  		} else {
   458  			providerGroups[p.ManifestLabel()] = []string{namespacedName}
   459  		}
   460  	}
   461  
   462  	var errs []error
   463  	for provider, providerInstances := range providerGroups {
   464  		if len(providerInstances) > 1 {
   465  			errs = append(errs, errors.Errorf("multiple instance of provider type %q found: %v", provider, providerInstances))
   466  		}
   467  	}
   468  
   469  	if len(errs) > 0 {
   470  		return errors.Wrap(kerrors.NewAggregate(errs), "detected multiple instances of the same provider, "+
   471  			"but clusterctl does not support this use case. See https://cluster-api.sigs.k8s.io/developer/architecture/controllers/support-multiple-instances.html for more details")
   472  	}
   473  
   474  	return nil
   475  }