Skip to content

Commit

Permalink
Merge pull request #5373 from ykakarap/clusterctl-better-errors
Browse files Browse the repository at this point in the history
✨clusterctl: generate returns better error message in case there is no management cluster available
  • Loading branch information
k8s-ci-robot committed Oct 7, 2021
2 parents c6d42a7 + a4fe42f commit 4613624
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 30 deletions.
47 changes: 17 additions & 30 deletions cmd/clusterctl/client/cluster/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,11 @@ import (
"time"

"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/rest"
"sigs.k8s.io/cluster-api/cmd/clusterctl/client/config"
"sigs.k8s.io/cluster-api/cmd/clusterctl/client/repository"
yaml "sigs.k8s.io/cluster-api/cmd/clusterctl/client/yamlprocessor"
logf "sigs.k8s.io/cluster-api/cmd/clusterctl/log"
"sigs.k8s.io/controller-runtime/pkg/client"
)

const (
Expand Down Expand Up @@ -61,15 +58,15 @@ type Client interface {
// Proxy return the Proxy used for operating objects in the management cluster.
Proxy() Proxy

// CertManager returns a CertManagerClient that can be user for
// CertManager returns a CertManagerClient that can be used for
// operating the cert-manager components in the cluster.
CertManager() CertManagerClient

// ProviderComponents returns a ComponentsClient object that can be user for
// ProviderComponents returns a ComponentsClient object that can be used for
// operating provider components objects in the management cluster (e.g. the CRDs, controllers, RBAC).
ProviderComponents() ComponentsClient

// ProviderInventory returns a InventoryClient object that can be user for
// ProviderInventory returns a InventoryClient object that can be used for
// operating provider inventory stored in the management cluster (e.g. the list of installed providers/versions).
ProviderInventory() InventoryClient

Expand Down Expand Up @@ -219,30 +216,6 @@ func newClusterClient(kubeconfig Kubeconfig, configClient config.Client, options
return client
}

// Proxy defines a client proxy interface.
type Proxy interface {
// GetConfig returns the rest.Config
GetConfig() (*rest.Config, error)

// CurrentNamespace returns the namespace from the current context in the kubeconfig file
CurrentNamespace() (string, error)

// ValidateKubernetesVersion returns an error if management cluster version less than minimumKubernetesVersion
ValidateKubernetesVersion() error

// NewClient returns a new controller runtime Client object for working on the management cluster
NewClient() (client.Client, error)

// ListResources returns all the Kubernetes objects with the given labels existing the listed namespaces.
ListResources(labels map[string]string, namespaces ...string) ([]unstructured.Unstructured, error)

// GetContexts returns the list of contexts in kubeconfig which begin with prefix.
GetContexts(prefix string) ([]string, error)

// GetResourceNames returns the list of resource names which begin with prefix.
GetResourceNames(groupVersion, kind string, options []client.ListOption, prefix string) ([]string, error)
}

// retryWithExponentialBackoff repeats an operation until it passes or the exponential backoff times out.
func retryWithExponentialBackoff(opts wait.Backoff, operation func() error) error {
log := logf.Log
Expand Down Expand Up @@ -291,6 +264,20 @@ func newConnectBackoff() wait.Backoff {
}
}

// newShortConnectBackoff creates a new API Machinery backoff parameter set suitable for use when clusterctl connect to a cluster.
// Preferred over newConnectBackoff() only when used to perform quick checks to check if a cluster is reachable.
func newShortConnectBackoff() wait.Backoff {
// Return a exponential backoff configuration which returns durations for a total time of ~5s.
// Example: 0, .25s, .6s, 1.2, 2.1s, 3.4s, 5.5s.
// Jitter is added as a random fraction of the duration multiplied by the jitter factor.
return wait.Backoff{
Duration: 250 * time.Millisecond,
Factor: 1.5,
Steps: 7,
Jitter: 0.1,
}
}

// newReadBackoff creates a new API Machinery backoff parameter set suitable for use with clusterctl read operations.
func newReadBackoff() wait.Backoff {
// Return a exponential backoff configuration which returns durations for a total time of ~15s.
Expand Down
48 changes: 48 additions & 0 deletions cmd/clusterctl/client/cluster/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,33 @@ var (
localScheme = scheme.Scheme
)

// Proxy defines a client proxy interface.
type Proxy interface {
// GetConfig returns the rest.Config
GetConfig() (*rest.Config, error)

// CurrentNamespace returns the namespace from the current context in the kubeconfig file.
CurrentNamespace() (string, error)

// ValidateKubernetesVersion returns an error if management cluster version less than minimumKubernetesVersion.
ValidateKubernetesVersion() error

// NewClient returns a new controller runtime Client object for working on the management cluster.
NewClient() (client.Client, error)

// CheckClusterAvailable checks if a a cluster is available and reachable.
CheckClusterAvailable() error

// ListResources returns all the Kubernetes objects with the given labels existing the listed namespaces.
ListResources(labels map[string]string, namespaces ...string) ([]unstructured.Unstructured, error)

// GetContexts returns the list of contexts in kubeconfig which begin with prefix.
GetContexts(prefix string) ([]string, error)

// GetResourceNames returns the list of resource names which begin with prefix.
GetResourceNames(groupVersion, kind string, options []client.ListOption, prefix string) ([]string, error)
}

type proxy struct {
kubeconfig Kubeconfig
timeout time.Duration
Expand Down Expand Up @@ -150,6 +177,27 @@ func (k *proxy) NewClient() (client.Client, error) {
return c, nil
}

func (k *proxy) CheckClusterAvailable() error {
// Check if the cluster is available by creating a client to the cluster.
// If creating the client times out and never established we assume that
// the cluster does not exist or is not reachable.
// For the purposes of clusterctl operations non-existent clusters and
// non-reachable clusters can be treated as the same.
config, err := k.GetConfig()
if err != nil {
return err
}

connectBackoff := newShortConnectBackoff()
if err := retryWithExponentialBackoff(connectBackoff, func() error {
_, err := client.New(config, client.Options{Scheme: localScheme})
return err
}); err != nil {
return err
}
return nil
}

func (k *proxy) ListResources(labels map[string]string, namespaces ...string) ([]unstructured.Unstructured, error) {
cs, err := k.newClientSet()
if err != nil {
Expand Down
9 changes: 9 additions & 0 deletions cmd/clusterctl/client/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,9 @@ func (c *clusterctlClient) GetClusterTemplate(options GetClusterTemplateOptions)

// If the option specifying the targetNamespace is empty, try to detect it.
if options.TargetNamespace == "" {
if err := clusterClient.Proxy().CheckClusterAvailable(); err != nil {
return nil, errors.Wrap(err, "management cluster not available. Cannot auto-discover target namespace. Please specify a target namespace")
}
currentNamespace, err := clusterClient.Proxy().CurrentNamespace()
if err != nil {
return nil, err
Expand Down Expand Up @@ -273,6 +276,9 @@ func (c *clusterctlClient) getTemplateFromRepository(cluster cluster.Client, opt
provider := source.InfrastructureProvider
ensureCustomResourceDefinitions := false
if provider == "" {
if err := cluster.Proxy().CheckClusterAvailable(); err != nil {
return nil, errors.Wrap(err, "management cluster not available. Cannot auto-discover default infrastructure provider. Please specify an infrastructure provider")
}
// ensure the custom resource definitions required by clusterctl are in place
if err := cluster.ProviderInventory().EnsureCustomResourceDefinitions(); err != nil {
return nil, errors.Wrapf(err, "provider custom resource definitions (CRDs) are not installed")
Expand All @@ -298,6 +304,9 @@ func (c *clusterctlClient) getTemplateFromRepository(cluster cluster.Client, opt

// If the version of the infrastructure provider to get templates from is empty, try to detect it.
if version == "" {
if err := cluster.Proxy().CheckClusterAvailable(); err != nil {
return nil, errors.Wrapf(err, "management cluster not available. Cannot auto-discover version for the provider %q automatically. Please specify a version", name)
}
// ensure the custom resource definitions required by clusterctl are in place (if not already done)
if !ensureCustomResourceDefinitions {
if err := cluster.ProviderInventory().EnsureCustomResourceDefinitions(); err != nil {
Expand Down
18 changes: 18 additions & 0 deletions cmd/clusterctl/internal/test/fake_proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@ limitations under the License.
package test

import (
"errors"

apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"k8s.io/utils/pointer"
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3"
fakebootstrap "sigs.k8s.io/cluster-api/cmd/clusterctl/internal/test/providers/bootstrap"
Expand All @@ -39,6 +42,7 @@ type FakeProxy struct {
cs client.Client
namespace string
objs []client.Object
available *bool
}

var (
Expand Down Expand Up @@ -79,6 +83,15 @@ func (f *FakeProxy) NewClient() (client.Client, error) {
return f.cs, nil
}

func (f *FakeProxy) CheckClusterAvailable() error {
// default to considering the cluster as available unless explicitly set to be
// unavailable.
if f.available == nil || *f.available {
return nil
}
return errors.New("cluster is not available")
}

// ListResources returns all the resources known by the FakeProxy.
func (f *FakeProxy) ListResources(labels map[string]string, namespaces ...string) ([]unstructured.Unstructured, error) {
var ret []unstructured.Unstructured //nolint
Expand Down Expand Up @@ -185,6 +198,11 @@ func (f *FakeProxy) WithFakeCAPISetup() *FakeProxy {
return f
}

func (f *FakeProxy) WithClusterAvailable(available bool) *FakeProxy {
f.available = pointer.Bool(available)
return f
}

// FakeCAPISetupObjects return required objects in order to make kubeadm pass checks
// ensuring that management cluster has a proper release of Cluster API installed.
func FakeCAPISetupObjects() []client.Object {
Expand Down

0 comments on commit 4613624

Please sign in to comment.