sigs.k8s.io/cluster-api@v1.7.1/cmd/clusterctl/client/cluster/proxy.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  	"os"
    23  	"strconv"
    24  	"strings"
    25  	"time"
    26  
    27  	"github.com/pkg/errors"
    28  	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    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/client-go/discovery"
    34  	"k8s.io/client-go/kubernetes"
    35  	"k8s.io/client-go/rest"
    36  	"k8s.io/client-go/tools/clientcmd"
    37  	"sigs.k8s.io/controller-runtime/pkg/client"
    38  
    39  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    40  	clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3"
    41  	"sigs.k8s.io/cluster-api/cmd/clusterctl/internal/scheme"
    42  	"sigs.k8s.io/cluster-api/version"
    43  )
    44  
    45  var (
    46  	localScheme = scheme.Scheme
    47  )
    48  
    49  // Proxy defines a client proxy interface.
    50  type Proxy interface {
    51  	// GetConfig returns the rest.Config
    52  	GetConfig() (*rest.Config, error)
    53  
    54  	// CurrentNamespace returns the namespace from the current context in the kubeconfig file.
    55  	CurrentNamespace() (string, error)
    56  
    57  	// ValidateKubernetesVersion returns an error if management cluster version less than MinimumKubernetesVersion.
    58  	ValidateKubernetesVersion() error
    59  
    60  	// NewClient returns a new controller runtime Client object for working on the management cluster.
    61  	NewClient(ctx context.Context) (client.Client, error)
    62  
    63  	// CheckClusterAvailable checks if a cluster is available and reachable.
    64  	CheckClusterAvailable(ctx context.Context) error
    65  
    66  	// ListResources lists namespaced and cluster-wide resources for a component matching the labels. Namespaced resources are only listed
    67  	// in the given namespaces.
    68  	// Please note that we are not returning resources for the component's CRD (e.g. we are not returning
    69  	// Certificates for cert-manager, Clusters for CAPI, AWSCluster for CAPA and so on).
    70  	// This is done to avoid errors when listing resources of providers which have already been deleted/scaled down to 0 replicas/with
    71  	// malfunctioning webhooks.
    72  	ListResources(ctx context.Context, labels map[string]string, namespaces ...string) ([]unstructured.Unstructured, error)
    73  
    74  	// GetContexts returns the list of contexts in kubeconfig which begin with prefix.
    75  	GetContexts(prefix string) ([]string, error)
    76  
    77  	// GetResourceNames returns the list of resource names which begin with prefix.
    78  	GetResourceNames(ctx context.Context, groupVersion, kind string, options []client.ListOption, prefix string) ([]string, error)
    79  }
    80  
    81  type proxy struct {
    82  	kubeconfig         Kubeconfig
    83  	timeout            time.Duration
    84  	configLoadingRules *clientcmd.ClientConfigLoadingRules
    85  }
    86  
    87  var _ Proxy = &proxy{}
    88  
    89  // CurrentNamespace returns the namespace for the specified context or the
    90  // first valid context as determined by the default config loading rules.
    91  func (k *proxy) CurrentNamespace() (string, error) {
    92  	config, err := k.configLoadingRules.Load()
    93  	if err != nil {
    94  		return "", errors.Wrap(err, "failed to load Kubeconfig")
    95  	}
    96  
    97  	context := config.CurrentContext
    98  	// If a context is explicitly provided use that instead
    99  	if k.kubeconfig.Context != "" {
   100  		context = k.kubeconfig.Context
   101  	}
   102  
   103  	v, ok := config.Contexts[context]
   104  	if !ok {
   105  		if k.kubeconfig.Path != "" {
   106  			return "", errors.Errorf("failed to get context %q from %q", context, k.configLoadingRules.GetExplicitFile())
   107  		}
   108  		return "", errors.Errorf("failed to get context %q from %q", context, k.configLoadingRules.GetLoadingPrecedence())
   109  	}
   110  
   111  	if v.Namespace != "" {
   112  		return v.Namespace, nil
   113  	}
   114  
   115  	return metav1.NamespaceDefault, nil
   116  }
   117  
   118  func (k *proxy) ValidateKubernetesVersion() error {
   119  	config, err := k.GetConfig()
   120  	if err != nil {
   121  		return err
   122  	}
   123  
   124  	minVer := version.MinimumKubernetesVersion
   125  	if clusterTopologyFeatureGate, _ := strconv.ParseBool(os.Getenv("CLUSTER_TOPOLOGY")); clusterTopologyFeatureGate {
   126  		minVer = version.MinimumKubernetesVersionClusterTopology
   127  	}
   128  
   129  	return version.CheckKubernetesVersion(config, minVer)
   130  }
   131  
   132  // GetConfig returns the config for a kubernetes client.
   133  func (k *proxy) GetConfig() (*rest.Config, error) {
   134  	config, err := k.configLoadingRules.Load()
   135  	if err != nil {
   136  		return nil, errors.Wrap(err, "failed to load Kubeconfig")
   137  	}
   138  
   139  	configOverrides := &clientcmd.ConfigOverrides{
   140  		CurrentContext: k.kubeconfig.Context,
   141  		Timeout:        k.timeout.String(),
   142  	}
   143  	restConfig, err := clientcmd.NewDefaultClientConfig(*config, configOverrides).ClientConfig()
   144  	if err != nil {
   145  		if strings.HasPrefix(err.Error(), "invalid configuration:") {
   146  			return nil, errors.New(strings.Replace(err.Error(), "invalid configuration:", "invalid kubeconfig file; clusterctl requires a valid kubeconfig file to connect to the management cluster:", 1))
   147  		}
   148  		return nil, err
   149  	}
   150  	restConfig.UserAgent = fmt.Sprintf("clusterctl/%s (%s)", version.Get().GitVersion, version.Get().Platform)
   151  
   152  	// Set QPS and Burst to a threshold that ensures the controller runtime client/client go doesn't generate throttling log messages
   153  	restConfig.QPS = 20
   154  	restConfig.Burst = 100
   155  
   156  	return restConfig, nil
   157  }
   158  
   159  func (k *proxy) NewClient(ctx context.Context) (client.Client, error) {
   160  	config, err := k.GetConfig()
   161  	if err != nil {
   162  		return nil, err
   163  	}
   164  
   165  	var c client.Client
   166  	// Nb. The operation is wrapped in a retry loop to make newClientSet more resilient to temporary connection problems.
   167  	connectBackoff := newConnectBackoff()
   168  	if err := retryWithExponentialBackoff(ctx, connectBackoff, func(_ context.Context) error {
   169  		var err error
   170  		c, err = client.New(config, client.Options{Scheme: localScheme})
   171  		if err != nil {
   172  			return err
   173  		}
   174  		return nil
   175  	}); err != nil {
   176  		return nil, errors.Wrap(err, "failed to connect to the management cluster")
   177  	}
   178  
   179  	return c, nil
   180  }
   181  
   182  func (k *proxy) CheckClusterAvailable(ctx context.Context) error {
   183  	// Check if the cluster is available by creating a client to the cluster.
   184  	// If creating the client times out and never established we assume that
   185  	// the cluster does not exist or is not reachable.
   186  	// For the purposes of clusterctl operations non-existent clusters and
   187  	// non-reachable clusters can be treated as the same.
   188  	config, err := k.GetConfig()
   189  	if err != nil {
   190  		return err
   191  	}
   192  
   193  	connectBackoff := newShortConnectBackoff()
   194  	return retryWithExponentialBackoff(ctx, connectBackoff, func(_ context.Context) error {
   195  		_, err := client.New(config, client.Options{Scheme: localScheme})
   196  		return err
   197  	})
   198  }
   199  
   200  // ListResources lists namespaced and cluster-wide resources for a component matching the labels. Namespaced resources are only listed
   201  // in the given namespaces.
   202  // Please note that we are not returning resources for the component's CRD (e.g. we are not returning
   203  // Certificates for cert-manager, Clusters for CAPI, AWSCluster for CAPA and so on).
   204  // This is done to avoid errors when listing resources of providers which have already been deleted/scaled down to 0 replicas/with
   205  // malfunctioning webhooks.
   206  // For example:
   207  //   - The AWS provider has already been deleted, but there are still cluster-wide resources of AWSClusterControllerIdentity.
   208  //   - The AWSClusterControllerIdentity resources are still stored in an older version (e.g. v1alpha4, when the preferred
   209  //     version is v1beta1)
   210  //   - If we now want to delete e.g. the kubeadm bootstrap provider, we cannot list AWSClusterControllerIdentity resources
   211  //     as the conversion would fail, because the AWS controller hosting the conversion webhook has already been deleted.
   212  //   - Thus we exclude resources of other providers if we detect that ListResources is called to list resources of a provider.
   213  func (k *proxy) ListResources(ctx context.Context, labels map[string]string, namespaces ...string) ([]unstructured.Unstructured, error) {
   214  	cs, err := k.newClientSet(ctx)
   215  	if err != nil {
   216  		return nil, err
   217  	}
   218  
   219  	c, err := k.NewClient(ctx)
   220  	if err != nil {
   221  		return nil, err
   222  	}
   223  
   224  	// Get all the API resources in the cluster.
   225  	resourceListBackoff := newReadBackoff()
   226  	var resourceList []*metav1.APIResourceList
   227  	if err := retryWithExponentialBackoff(ctx, resourceListBackoff, func(context.Context) error {
   228  		resourceList, err = cs.Discovery().ServerPreferredResources()
   229  		return err
   230  	}); err != nil {
   231  		return nil, errors.Wrap(err, "failed to list api resources")
   232  	}
   233  
   234  	// Exclude from discovery the objects from the cert-manager/provider's CRDs.
   235  	// Those objects are not part of the components, and they will eventually be removed when removing the CRD definition.
   236  	crdsToExclude := sets.Set[string]{}
   237  
   238  	crdList := &apiextensionsv1.CustomResourceDefinitionList{}
   239  	if err := retryWithExponentialBackoff(ctx, newReadBackoff(), func(ctx context.Context) error {
   240  		return c.List(ctx, crdList)
   241  	}); err != nil {
   242  		return nil, errors.Wrap(err, "failed to list CRDs")
   243  	}
   244  	for _, crd := range crdList.Items {
   245  		component, isCoreComponent := labels[clusterctlv1.ClusterctlCoreLabel]
   246  		_, isProviderResource := crd.Labels[clusterv1.ProviderNameLabel]
   247  		if (isCoreComponent && component == clusterctlv1.ClusterctlCoreLabelCertManagerValue) || isProviderResource {
   248  			for _, version := range crd.Spec.Versions {
   249  				crdsToExclude.Insert(metav1.GroupVersionKind{
   250  					Group:   crd.Spec.Group,
   251  					Version: version.Name,
   252  					Kind:    crd.Spec.Names.Kind,
   253  				}.String())
   254  			}
   255  		}
   256  	}
   257  
   258  	// Select resources with list and delete methods (list is required by this method, delete by the callers of this method)
   259  	resourceList = discovery.FilteredBy(discovery.SupportsAllVerbs{Verbs: []string{"list", "delete"}}, resourceList)
   260  
   261  	var ret []unstructured.Unstructured
   262  	for _, resourceGroup := range resourceList {
   263  		for _, resourceKind := range resourceGroup.APIResources {
   264  			// Discard the resourceKind that exists in two api groups (we are excluding one of the two groups arbitrarily).
   265  			if resourceGroup.GroupVersion == "extensions/v1beta1" &&
   266  				(resourceKind.Name == "daemonsets" || resourceKind.Name == "deployments" || resourceKind.Name == "replicasets" || resourceKind.Name == "networkpolicies" || resourceKind.Name == "ingresses") {
   267  				continue
   268  			}
   269  
   270  			// Continue if the resource is an excluded CRD.
   271  			gv, err := schema.ParseGroupVersion(resourceGroup.GroupVersion)
   272  			if err != nil {
   273  				return nil, errors.Wrapf(err, "failed to parse GroupVersion")
   274  			}
   275  			if crdsToExclude.Has(metav1.GroupVersionKind{
   276  				Group:   gv.Group,
   277  				Version: gv.Version,
   278  				Kind:    resourceKind.Kind,
   279  			}.String()) {
   280  				continue
   281  			}
   282  
   283  			// List all the object instances of this resourceKind with the given labels
   284  			if resourceKind.Namespaced {
   285  				for _, namespace := range namespaces {
   286  					objList, err := listObjByGVK(ctx, c, resourceGroup.GroupVersion, resourceKind.Kind, []client.ListOption{client.MatchingLabels(labels), client.InNamespace(namespace)})
   287  					if err != nil {
   288  						return nil, err
   289  					}
   290  					ret = append(ret, objList.Items...)
   291  				}
   292  			} else {
   293  				objList, err := listObjByGVK(ctx, c, resourceGroup.GroupVersion, resourceKind.Kind, []client.ListOption{client.MatchingLabels(labels)})
   294  				if err != nil {
   295  					return nil, err
   296  				}
   297  				ret = append(ret, objList.Items...)
   298  			}
   299  		}
   300  	}
   301  	return ret, nil
   302  }
   303  
   304  // GetContexts returns the list of contexts in kubeconfig which begin with prefix.
   305  func (k *proxy) GetContexts(prefix string) ([]string, error) {
   306  	config, err := k.configLoadingRules.Load()
   307  	if err != nil {
   308  		return nil, err
   309  	}
   310  
   311  	var comps []string
   312  	for name := range config.Contexts {
   313  		if strings.HasPrefix(name, prefix) {
   314  			comps = append(comps, name)
   315  		}
   316  	}
   317  
   318  	return comps, nil
   319  }
   320  
   321  // GetResourceNames returns the list of resource names which begin with prefix.
   322  func (k *proxy) GetResourceNames(ctx context.Context, groupVersion, kind string, options []client.ListOption, prefix string) ([]string, error) {
   323  	client, err := k.NewClient(ctx)
   324  	if err != nil {
   325  		return nil, err
   326  	}
   327  
   328  	objList, err := listObjByGVK(ctx, client, groupVersion, kind, options)
   329  	if err != nil {
   330  		return nil, err
   331  	}
   332  
   333  	var comps []string
   334  	for _, item := range objList.Items {
   335  		name := item.GetName()
   336  
   337  		if strings.HasPrefix(name, prefix) {
   338  			comps = append(comps, name)
   339  		}
   340  	}
   341  
   342  	return comps, nil
   343  }
   344  
   345  func listObjByGVK(ctx context.Context, c client.Client, groupVersion, kind string, options []client.ListOption) (*unstructured.UnstructuredList, error) {
   346  	objList := new(unstructured.UnstructuredList)
   347  	objList.SetAPIVersion(groupVersion)
   348  	objList.SetKind(kind)
   349  
   350  	resourceListBackoff := newReadBackoff()
   351  	if err := retryWithExponentialBackoff(ctx, resourceListBackoff, func(ctx context.Context) error {
   352  		return c.List(ctx, objList, options...)
   353  	}); err != nil {
   354  		return nil, errors.Wrapf(err, "failed to list objects for the %q GroupVersionKind", objList.GroupVersionKind())
   355  	}
   356  
   357  	return objList, nil
   358  }
   359  
   360  // ProxyOption defines a function that can change proxy options.
   361  type ProxyOption func(p *proxy)
   362  
   363  // InjectProxyTimeout sets the proxy timeout.
   364  func InjectProxyTimeout(t time.Duration) ProxyOption {
   365  	return func(p *proxy) {
   366  		p.timeout = t
   367  	}
   368  }
   369  
   370  // InjectKubeconfigPaths sets the kubeconfig paths loading rules.
   371  func InjectKubeconfigPaths(paths []string) ProxyOption {
   372  	return func(p *proxy) {
   373  		p.configLoadingRules.Precedence = paths
   374  	}
   375  }
   376  
   377  func newProxy(kubeconfig Kubeconfig, opts ...ProxyOption) Proxy {
   378  	// If a kubeconfig file isn't provided, find one in the standard locations.
   379  	rules := clientcmd.NewDefaultClientConfigLoadingRules()
   380  	if kubeconfig.Path != "" {
   381  		rules.ExplicitPath = kubeconfig.Path
   382  	}
   383  	p := &proxy{
   384  		kubeconfig:         kubeconfig,
   385  		timeout:            30 * time.Second,
   386  		configLoadingRules: rules,
   387  	}
   388  
   389  	for _, o := range opts {
   390  		o(p)
   391  	}
   392  
   393  	return p
   394  }
   395  
   396  func (k *proxy) newClientSet(ctx context.Context) (*kubernetes.Clientset, error) {
   397  	config, err := k.GetConfig()
   398  	if err != nil {
   399  		return nil, err
   400  	}
   401  
   402  	var cs *kubernetes.Clientset
   403  	// Nb. The operation is wrapped in a retry loop to make newClientSet more resilient to temporary connection problems.
   404  	connectBackoff := newConnectBackoff()
   405  	if err := retryWithExponentialBackoff(ctx, connectBackoff, func(_ context.Context) error {
   406  		var err error
   407  		cs, err = kubernetes.NewForConfig(config)
   408  		if err != nil {
   409  			return err
   410  		}
   411  		return nil
   412  	}); err != nil {
   413  		return nil, errors.Wrap(err, "failed to create the client-go client")
   414  	}
   415  
   416  	return cs, nil
   417  }