diff --git a/cmd/clusterctl/client/alias.go b/cmd/clusterctl/client/alias.go index c4c215ca255f..cbf244659dc8 100644 --- a/cmd/clusterctl/client/alias.go +++ b/cmd/clusterctl/client/alias.go @@ -36,3 +36,6 @@ type Template repository.Template // UpgradePlan defines a list of possible upgrade targets for a management group. type UpgradePlan cluster.UpgradePlan + +// Kubeconfig is a type that specifies inputs related to the actual kubeconfig. +type Kubeconfig cluster.Kubeconfig diff --git a/cmd/clusterctl/client/client.go b/cmd/clusterctl/client/client.go index b25bc51f1bf8..a851d1eb012c 100644 --- a/cmd/clusterctl/client/client.go +++ b/cmd/clusterctl/client/client.go @@ -23,52 +23,6 @@ import ( "sigs.k8s.io/cluster-api/cmd/clusterctl/client/repository" ) -// InitOptions carries the options supported by Init. -type InitOptions struct { - // Kubeconfig file to use for accessing the management cluster. If empty, default discovery rules apply. - Kubeconfig string - - // CoreProvider version (e.g. cluster-api:v0.3.0) to add to the management cluster. If unspecified, the - // cluster-api core provider's latest release is used. - CoreProvider string - - // BootstrapProviders and versions (e.g. kubeadm:v0.3.0) to add to the management cluster. - // If unspecified, the kubeadm bootstrap provider's latest release is used. - BootstrapProviders []string - - // InfrastructureProviders and versions (e.g. aws:v0.5.0) to add to the management cluster. - InfrastructureProviders []string - - // ControlPlaneProviders and versions (e.g. kubeadm:v0.3.0) to add to the management cluster. - // If unspecified, the kubeadm control plane provider latest release is used. - ControlPlaneProviders []string - - // TargetNamespace defines the namespace where the providers should be deployed. If unspecified, each provider - // will be installed in a provider's default namespace. - TargetNamespace string - - // WatchingNamespace defines the namespace the providers should watch to reconcile Cluster API objects. - // If unspecified, the providers watches for Cluster API objects across all namespaces. - WatchingNamespace string - - // LogUsageInstructions instructs the init command to print the usage instructions in case of first run. - LogUsageInstructions bool -} - -// MoveOptions carries the options supported by move. -type MoveOptions struct { - // FromKubeconfig defines the kubeconfig file to use for accessing the source management cluster. If empty, - // default rules for kubeconfig discovery will be used. - FromKubeconfig string - - // ToKubeconfig defines the path to the kubeconfig file to use for accessing the target management cluster. - ToKubeconfig string - - // Namespace where the objects describing the workload cluster exists. If unspecified, the current - // namespace will be used. - Namespace string -} - // Client is exposes the clusterctl high-level client library. type Client interface { // GetProvidersConfig returns the list of providers configured for this instance of clusterctl. @@ -111,7 +65,7 @@ type clusterctlClient struct { } type RepositoryClientFactory func(config.Provider) (repository.Client, error) -type ClusterClientFactory func(string) (cluster.Client, error) +type ClusterClientFactory func(Kubeconfig) (cluster.Client, error) // Ensure clusterctlClient implements Client. var _ Client = &clusterctlClient{} @@ -177,9 +131,10 @@ func newClusterctlClient(path string, options ...Option) (*clusterctlClient, err } // defaultClusterFactory is a ClusterClientFactory func the uses the default client provided by the cluster low level library. -func defaultClusterFactory(configClient config.Client) func(kubeconfig string) (cluster.Client, error) { - return func(kubeconfig string) (cluster.Client, error) { - return cluster.New(kubeconfig, configClient), nil +func defaultClusterFactory(configClient config.Client) func(kubeconfig Kubeconfig) (cluster.Client, error) { + return func(kubeconfig Kubeconfig) (cluster.Client, error) { + // Kubeconfig is a type alias to cluster.Kubeconfig + return cluster.New(cluster.Kubeconfig(kubeconfig), configClient), nil } } diff --git a/cmd/clusterctl/client/client_test.go b/cmd/clusterctl/client/client_test.go index c4b0fcbde41d..173a8ea7f228 100644 --- a/cmd/clusterctl/client/client_test.go +++ b/cmd/clusterctl/client/client_test.go @@ -49,7 +49,7 @@ func TestNewFakeClient(t *testing.T) { WithFile("v1.0", "components.yaml", []byte("content")) // create a fake cluster, eventually adding some existing runtime objects to it - cluster1 := newFakeCluster("cluster1", config1). + cluster1 := newFakeCluster(cluster.Kubeconfig{Path: "cluster1"}, config1). WithObjs() // create a new fakeClient that allows to execute tests on the fake config, the fake repositories and the fake cluster. @@ -59,8 +59,9 @@ func TestNewFakeClient(t *testing.T) { } type fakeClient struct { - configClient config.Client - clusters map[string]cluster.Client + configClient config.Client + // mapping between kubeconfigPath/context with cluster client + clusters map[cluster.Kubeconfig]cluster.Client repositories map[string]repository.Client internalClient *clusterctlClient } @@ -108,7 +109,7 @@ func (f fakeClient) ApplyUpgrade(options ApplyUpgradeOptions) error { func newFakeClient(configClient config.Client) *fakeClient { fake := &fakeClient{ - clusters: map[string]cluster.Client{}, + clusters: map[cluster.Kubeconfig]cluster.Client{}, repositories: map[string]repository.Client{}, } @@ -117,11 +118,13 @@ func newFakeClient(configClient config.Client) *fakeClient { fake.configClient = newFakeConfig() } - var clusterClientFactory = func(kubeconfig string) (cluster.Client, error) { - if _, ok := fake.clusters[kubeconfig]; !ok { - return nil, errors.Errorf("Cluster for kubeconfig %q does not exists.", kubeconfig) + var clusterClientFactory = func(i Kubeconfig) (cluster.Client, error) { + // converting the client.Kubeconfig to cluster.Kubeconfig alias + k := cluster.Kubeconfig(i) + if _, ok := fake.clusters[k]; !ok { + return nil, errors.Errorf("Cluster for kubeconfig %q and/or context %q does not exists.", i.Path, i.Context) } - return fake.clusters[kubeconfig], nil + return fake.clusters[k], nil } fake.internalClient, _ = newClusterctlClient("fake-config", @@ -139,7 +142,8 @@ func newFakeClient(configClient config.Client) *fakeClient { } func (f *fakeClient) WithCluster(clusterClient cluster.Client) *fakeClient { - f.clusters[clusterClient.Kubeconfig()] = clusterClient + input := clusterClient.Kubeconfig() + f.clusters[input] = clusterClient return f } @@ -154,7 +158,7 @@ func (f *fakeClient) WithRepository(repositoryClient repository.Client) *fakeCli // newFakeCluster returns a fakeClusterClient that // internally uses a FakeProxy (based on the controller-runtime FakeClient). // You can use WithObjs to pre-load a set of runtime objects in the cluster. -func newFakeCluster(kubeconfig string, configClient config.Client) *fakeClusterClient { +func newFakeCluster(kubeconfig cluster.Kubeconfig, configClient config.Client) *fakeClusterClient { fake := &fakeClusterClient{ kubeconfig: kubeconfig, repositories: map[string]repository.Client{}, @@ -165,7 +169,7 @@ func newFakeCluster(kubeconfig string, configClient config.Client) *fakeClusterC return nil } - fake.internalclient = cluster.New("", configClient, + fake.internalclient = cluster.New(kubeconfig, configClient, cluster.InjectProxy(fake.fakeProxy), cluster.InjectPollImmediateWaiter(pollImmediateWaiter), cluster.InjectRepositoryFactory(func(provider config.Provider, configClient config.Client, options ...repository.Option) (repository.Client, error) { @@ -195,15 +199,16 @@ func (p *fakeCertManagerClient) Images() ([]string, error) { } type fakeClusterClient struct { - kubeconfig string - fakeProxy *test.FakeProxy - repositories map[string]repository.Client - internalclient cluster.Client + kubeconfig cluster.Kubeconfig + fakeProxy *test.FakeProxy + fakeObjectMover cluster.ObjectMover + repositories map[string]repository.Client + internalclient cluster.Client } var _ cluster.Client = &fakeClusterClient{} -func (f fakeClusterClient) Kubeconfig() string { +func (f fakeClusterClient) Kubeconfig() cluster.Kubeconfig { return f.kubeconfig } @@ -228,7 +233,10 @@ func (f fakeClusterClient) ProviderInstaller() cluster.ProviderInstaller { } func (f *fakeClusterClient) ObjectMover() cluster.ObjectMover { - return f.internalclient.ObjectMover() + if f.fakeObjectMover == nil { + return f.internalclient.ObjectMover() + } + return f.fakeObjectMover } func (f *fakeClusterClient) ProviderUpgrader() cluster.ProviderUpgrader { @@ -254,6 +262,11 @@ func (f *fakeClusterClient) WithRepository(repositoryClient repository.Client) * return f } +func (f *fakeClusterClient) WithObjectMover(mover cluster.ObjectMover) *fakeClusterClient { + f.fakeObjectMover = mover + return f +} + // newFakeConfig return a fake implementation of the client for low-level config library. // The implementation uses a FakeReader that stores configuration settings in a map; you can use // the WithVar or WithProvider methods to set the map values. diff --git a/cmd/clusterctl/client/cluster/client.go b/cmd/clusterctl/client/cluster/client.go index a8e5f3e09fae..b4703743cab0 100644 --- a/cmd/clusterctl/client/cluster/client.go +++ b/cmd/clusterctl/client/cluster/client.go @@ -23,9 +23,9 @@ import ( "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" - "sigs.k8s.io/cluster-api/cmd/clusterctl/internal/test" logf "sigs.k8s.io/cluster-api/cmd/clusterctl/log" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -38,14 +38,24 @@ var ( ctx = context.TODO() ) +// Kubeconfig is a type that specifies inputs related to the actual +// kubeconfig. +type Kubeconfig struct { + // Path to the kubeconfig file + Path string + // Specify context within the kubeconfig file. If empty, cluster client + // will use the current context. + Context string +} + // Client is used to interact with a management cluster. // A management cluster contains following categories of objects: // - provider components (e.g. the CRDs, controllers, RBAC) // - provider inventory items (e.g. the list of installed providers/versions) // - provider objects (e.g. clusters, AWS clusters, machines etc.) type Client interface { - // Kubeconfig return the path to kubeconfig used to access to a management cluster. - Kubeconfig() string + // Kubeconfig returns the kubeconfig used to access to a management cluster. + Kubeconfig() Kubeconfig // Proxy return the Proxy used for operating objects in the management cluster. Proxy() Proxy @@ -83,7 +93,7 @@ type PollImmediateWaiter func(interval, timeout time.Duration, condition wait.Co // clusterClient implements Client. type clusterClient struct { configClient config.Client - kubeconfig string + kubeconfig Kubeconfig proxy Proxy repositoryClientFactory RepositoryClientFactory pollImmediateWaiter PollImmediateWaiter @@ -94,7 +104,7 @@ type RepositoryClientFactory func(provider config.Provider, configClient config. // ensure clusterClient implements Client. var _ Client = &clusterClient{} -func (c *clusterClient) Kubeconfig() string { +func (c *clusterClient) Kubeconfig() Kubeconfig { return c.kubeconfig } @@ -156,11 +166,11 @@ func InjectPollImmediateWaiter(pollImmediateWaiter PollImmediateWaiter) Option { } // New returns a cluster.Client. -func New(kubeconfig string, configClient config.Client, options ...Option) Client { +func New(kubeconfig Kubeconfig, configClient config.Client, options ...Option) Client { return newClusterClient(kubeconfig, configClient, options...) } -func newClusterClient(kubeconfig string, configClient config.Client, options ...Option) *clusterClient { +func newClusterClient(kubeconfig Kubeconfig, configClient config.Client, options ...Option) *clusterClient { client := &clusterClient{ configClient: configClient, kubeconfig: kubeconfig, @@ -171,7 +181,7 @@ func newClusterClient(kubeconfig string, configClient config.Client, options ... // if there is an injected proxy, use it, otherwise use a default one if client.proxy == nil { - client.proxy = newProxy(kubeconfig) + client.proxy = newProxy(client.kubeconfig) } // if there is an injected repositoryClientFactory, use it, otherwise use the default one @@ -188,6 +198,9 @@ func newClusterClient(kubeconfig string, configClient config.Client, options ... } 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) @@ -201,8 +214,6 @@ type Proxy interface { ListResources(labels map[string]string, namespaces ...string) ([]unstructured.Unstructured, error) } -var _ Proxy = &test.FakeProxy{} - // retryWithExponentialBackoff repeats an operation until it passes or the exponential backoff times out. func retryWithExponentialBackoff(opts wait.Backoff, operation func() error) error { //nolint:unparam log := logf.Log diff --git a/cmd/clusterctl/client/cluster/proxy.go b/cmd/clusterctl/client/cluster/proxy.go index 55f39921f1e4..92ef0879e781 100644 --- a/cmd/clusterctl/client/cluster/proxy.go +++ b/cmd/clusterctl/client/cluster/proxy.go @@ -19,6 +19,7 @@ package cluster import ( "fmt" "strings" + "time" "github.com/pkg/errors" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -38,24 +39,27 @@ var ( ) type proxy struct { - kubeconfig string + kubeconfig Kubeconfig + timeout time.Duration } var _ Proxy = &proxy{} func (k *proxy) CurrentNamespace() (string, error) { - config, err := clientcmd.LoadFromFile(k.kubeconfig) + config, err := clientcmd.LoadFromFile(k.kubeconfig.Path) if err != nil { - return "", errors.Wrapf(err, "failed to load Kubeconfig file from %q", k.kubeconfig) + return "", errors.Wrapf(err, "failed to load Kubeconfig file from %q", k.kubeconfig.Path) } - if config.CurrentContext == "" { - return "", errors.Wrapf(err, "failed to get current-context from %q", k.kubeconfig) + context := config.CurrentContext + // If a context is explicitly provided use that instead + if k.kubeconfig.Context != "" { + context = k.kubeconfig.Context } - v, ok := config.Contexts[config.CurrentContext] + v, ok := config.Contexts[context] if !ok { - return "", errors.Wrapf(err, "failed to get context %q from %q", config.CurrentContext, k.kubeconfig) + return "", errors.Errorf("failed to get context %q from %q", context, k.kubeconfig.Path) } if v.Namespace != "" { @@ -66,7 +70,7 @@ func (k *proxy) CurrentNamespace() (string, error) { } func (k *proxy) ValidateKubernetesVersion() error { - config, err := k.getConfig() + config, err := k.GetConfig() if err != nil { return err } @@ -89,8 +93,34 @@ func (k *proxy) ValidateKubernetesVersion() error { return nil } +func (k *proxy) GetConfig() (*rest.Config, error) { + config, err := clientcmd.LoadFromFile(k.kubeconfig.Path) + if err != nil { + return nil, errors.Wrapf(err, "failed to load Kubeconfig file from %q", k.kubeconfig.Path) + } + + configOverrides := &clientcmd.ConfigOverrides{ + CurrentContext: k.kubeconfig.Context, + Timeout: k.timeout.String(), + } + restConfig, err := clientcmd.NewDefaultClientConfig(*config, configOverrides).ClientConfig() + if err != nil { + if strings.HasPrefix(err.Error(), "invalid configuration:") { + 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)) + } + return nil, err + } + restConfig.UserAgent = fmt.Sprintf("clusterctl/%s (%s)", version.Get().GitVersion, version.Get().Platform) + + // Set QPS and Burst to a threshold that ensures the controller runtime client/client go does't generate throttling log messages + restConfig.QPS = 20 + restConfig.Burst = 100 + + return restConfig, nil +} + func (k *proxy) NewClient() (client.Client, error) { - config, err := k.getConfig() + config, err := k.GetConfig() if err != nil { return nil, err } @@ -175,40 +205,33 @@ func listObjByGVK(c client.Client, groupVersion, kind string, options []client.L return objList, nil } -func newProxy(kubeconfig string) Proxy { - // If a kubeconfig file isn't provided, find one in the standard locations. - if kubeconfig == "" { - kubeconfig = clientcmd.NewDefaultClientConfigLoadingRules().GetDefaultFilename() - } - return &proxy{ - kubeconfig: kubeconfig, +type ProxyOption func(p *proxy) + +func InjectProxyTimeout(t time.Duration) ProxyOption { + return func(p *proxy) { + p.timeout = t } } -func (k *proxy) getConfig() (*rest.Config, error) { - config, err := clientcmd.LoadFromFile(k.kubeconfig) - if err != nil { - return nil, errors.Wrapf(err, "failed to load Kubeconfig file from %q", k.kubeconfig) +func newProxy(kubeconfig Kubeconfig, opts ...ProxyOption) Proxy { + // If a kubeconfig file isn't provided, find one in the standard locations. + if kubeconfig.Path == "" { + kubeconfig.Path = clientcmd.NewDefaultClientConfigLoadingRules().GetDefaultFilename() } - - restConfig, err := clientcmd.NewDefaultClientConfig(*config, &clientcmd.ConfigOverrides{}).ClientConfig() - if err != nil { - if strings.HasPrefix(err.Error(), "invalid configuration:") { - 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)) - } - return nil, err + p := &proxy{ + kubeconfig: kubeconfig, + timeout: 30 * time.Second, } - restConfig.UserAgent = fmt.Sprintf("clusterctl/%s (%s)", version.Get().GitVersion, version.Get().Platform) - // Set QPS and Burst to a threshold that ensures the controller runtime client/client go does't generate throttling log messages - restConfig.QPS = 20 - restConfig.Burst = 100 + for _, o := range opts { + o(p) + } - return restConfig, nil + return p } func (k *proxy) newClientSet() (*kubernetes.Clientset, error) { - config, err := k.getConfig() + config, err := k.GetConfig() if err != nil { return nil, err } diff --git a/cmd/clusterctl/client/cluster/proxy_test.go b/cmd/clusterctl/client/cluster/proxy_test.go new file mode 100644 index 000000000000..160d58f6a90c --- /dev/null +++ b/cmd/clusterctl/client/cluster/proxy_test.go @@ -0,0 +1,218 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cluster + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "testing" + "time" + + . "github.com/onsi/gomega" + "sigs.k8s.io/cluster-api/cmd/clusterctl/internal/test" + "sigs.k8s.io/cluster-api/cmd/version" +) + +var _ Proxy = &test.FakeProxy{} + +func TestProxyGetConfig(t *testing.T) { + t.Run("GetConfig", func(t *testing.T) { + tests := []struct { + name string + context string + kubeconfigContents string + expectedHost string + expectErr bool + }{ + { + name: "defaults to the currentContext if none is specified", + kubeconfigContents: kubeconfig("management", "default"), + expectedHost: "https://management-server:1234", + expectErr: false, + }, + { + name: "returns host of cluster associated with the specified context even if currentContext is different", + kubeconfigContents: kubeconfig("management", "default"), + context: "workload", + expectedHost: "https://kind-server:38790", + expectErr: false, + }, + { + name: "returns error if cannot load the kubeconfig file", + expectErr: true, + kubeconfigContents: "bad contents", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + dir, err := ioutil.TempDir("", "clusterctl") + g.Expect(err).NotTo(HaveOccurred()) + defer os.RemoveAll(dir) + configFile := filepath.Join(dir, ".test-kubeconfig.yaml") + g.Expect(ioutil.WriteFile(configFile, []byte(tt.kubeconfigContents), 0640)).To(Succeed()) + + proxy := newProxy(Kubeconfig{Path: configFile, Context: tt.context}) + conf, err := proxy.GetConfig() + if tt.expectErr { + g.Expect(err).To(HaveOccurred()) + return + } + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(conf).ToNot(BeNil()) + // asserting on the host of the cluster associated with the + // context + g.Expect(conf.Host).To(Equal(tt.expectedHost)) + g.Expect(conf.UserAgent).To(Equal(fmt.Sprintf("clusterctl/%s (%s)", version.Get().GitVersion, version.Get().Platform))) + g.Expect(conf.QPS).To(BeEquivalentTo(20)) + g.Expect(conf.Burst).To(BeEquivalentTo(100)) + g.Expect(conf.Timeout.String()).To(Equal("30s")) + }) + } + }) + + t.Run("configure timeout", func(t *testing.T) { + g := NewWithT(t) + dir, err := ioutil.TempDir("", "clusterctl") + g.Expect(err).NotTo(HaveOccurred()) + defer os.RemoveAll(dir) + configFile := filepath.Join(dir, ".test-kubeconfig.yaml") + g.Expect(ioutil.WriteFile(configFile, []byte(kubeconfig("management", "default")), 0640)).To(Succeed()) + + proxy := newProxy(Kubeconfig{Path: configFile, Context: "management"}, InjectProxyTimeout(23*time.Second)) + conf, err := proxy.GetConfig() + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(conf.Timeout.String()).To(Equal("23s")) + }) +} + +func TestProxyCurrentNamespace(t *testing.T) { + tests := []struct { + name string + kubeconfigPath string + kubeconfigContents string + kubeconfigContext string + expectErr bool + expectedNamespace string + }{ + { + name: "return error for invalid kubeconfig path", + kubeconfigPath: "do-not-exist", + expectErr: true, + }, + { + name: "return error for bad kubeconfig contents", + kubeconfigContents: "management", + expectErr: true, + }, + { + name: "return default namespace if unspecified", + kubeconfigContents: kubeconfig("workload", ""), + expectErr: false, + expectedNamespace: "default", + }, + { + name: "return error when current-context is empty", + kubeconfigContents: kubeconfig("", ""), + expectErr: true, + }, + { + name: "return error when current-context is incorrect or does not exist", + kubeconfigContents: kubeconfig("does-not-exist", ""), + expectErr: true, + }, + { + name: "return specified namespace for the current context", + kubeconfigContents: kubeconfig("workload", "mykindns"), + expectErr: false, + expectedNamespace: "mykindns", + }, + { + name: "return the namespace of the specified context which is different from current context", + kubeconfigContents: kubeconfig("management", "workload-ns"), + expectErr: false, + kubeconfigContext: "workload", + expectedNamespace: "workload-ns", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + var configFile string + if len(tt.kubeconfigPath) != 0 { + configFile = tt.kubeconfigPath + } else { + dir, err := ioutil.TempDir("", "clusterctl") + g.Expect(err).NotTo(HaveOccurred()) + defer os.RemoveAll(dir) + configFile = filepath.Join(dir, ".test-kubeconfig.yaml") + g.Expect(ioutil.WriteFile(configFile, []byte(tt.kubeconfigContents), 0640)).To(Succeed()) + } + + proxy := newProxy(Kubeconfig{Path: configFile, Context: tt.kubeconfigContext}) + ns, err := proxy.CurrentNamespace() + if tt.expectErr { + g.Expect(err).To(HaveOccurred()) + return + } + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(ns).To(Equal(tt.expectedNamespace)) + }) + } +} + +func kubeconfig(currentContext, namespace string) string { + return fmt.Sprintf(`--- +apiVersion: v1 +clusters: +- cluster: + insecure-skip-tls-verify: true + server: https://management-server:1234 + name: management +- cluster: + insecure-skip-tls-verify: true + server: https://kind-server:38790 + name: workload +contexts: +- context: + cluster: management + user: management + namespace: management-ns + name: management +- context: + cluster: workload + user: workload + namespace: %s + name: workload +current-context: %s +kind: Config +preferences: {} +users: +- name: management + user: + client-certificate-data: c3R1ZmYK + client-key-data: c3R1ZmYK +- name: workload + user: + client-certificate-data: c3R1ZmYK + client-key-data: c3R1ZmYK +`, namespace, currentContext) + +} diff --git a/cmd/clusterctl/client/config.go b/cmd/clusterctl/client/config.go index 3bb71c9a03c1..be04264658c0 100644 --- a/cmd/clusterctl/client/config.go +++ b/cmd/clusterctl/client/config.go @@ -17,9 +17,10 @@ limitations under the License. package client import ( - "k8s.io/utils/pointer" "strconv" + "k8s.io/utils/pointer" + "github.com/pkg/errors" "k8s.io/apimachinery/pkg/util/version" clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3" @@ -51,8 +52,9 @@ func (c *clusterctlClient) GetProviderComponents(provider string, providerType c // GetClusterTemplateOptions carries the options supported by GetClusterTemplate. type GetClusterTemplateOptions struct { - // Kubeconfig file to use for accessing the management cluster. If empty, default discovery rules apply. - Kubeconfig string + // Kubeconfig defines the kubeconfig to use for accessing the management cluster. If empty, + // default rules for kubeconfig discovery will be used. + Kubeconfig Kubeconfig // ProviderRepositorySource to be used for reading the workload cluster template from a provider repository; // only one template source can be used at time; if not other source will be set, a ProviderRepositorySource diff --git a/cmd/clusterctl/client/config_test.go b/cmd/clusterctl/client/config_test.go index b6af7e15bfee..0565f85ff29b 100644 --- a/cmd/clusterctl/client/config_test.go +++ b/cmd/clusterctl/client/config_test.go @@ -19,16 +19,18 @@ package client import ( "fmt" "io/ioutil" - "k8s.io/utils/pointer" "os" "path/filepath" "testing" + "k8s.io/utils/pointer" + . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3" + "sigs.k8s.io/cluster-api/cmd/clusterctl/client/cluster" "sigs.k8s.io/cluster-api/cmd/clusterctl/client/config" ) @@ -391,7 +393,7 @@ func Test_clusterctlClient_GetClusterTemplate(t *testing.T) { WithDefaultVersion("v3.0.0"). WithFile("v3.0.0", "cluster-template.yaml", rawTemplate) - cluster1 := newFakeCluster("kubeconfig", config1). + cluster1 := newFakeCluster(cluster.Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, config1). WithProviderInventory(infraProviderConfig.Name(), infraProviderConfig.Type(), "v3.0.0", "foo", "bar"). WithObjs(configMap) @@ -419,7 +421,7 @@ func Test_clusterctlClient_GetClusterTemplate(t *testing.T) { name: "repository source - pass", args: args{ options: GetClusterTemplateOptions{ - Kubeconfig: "kubeconfig", + Kubeconfig: Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, ProviderRepositorySource: &ProviderRepositorySourceOptions{ InfrastructureProvider: "infra:v3.0.0", Flavor: "", @@ -439,7 +441,7 @@ func Test_clusterctlClient_GetClusterTemplate(t *testing.T) { name: "repository source - detects provider name/version if missing", args: args{ options: GetClusterTemplateOptions{ - Kubeconfig: "kubeconfig", + Kubeconfig: Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, ProviderRepositorySource: &ProviderRepositorySourceOptions{ InfrastructureProvider: "", // empty triggers auto-detection of the provider name/version Flavor: "", @@ -459,7 +461,7 @@ func Test_clusterctlClient_GetClusterTemplate(t *testing.T) { name: "repository source - use current namespace if targetNamespace is missing", args: args{ options: GetClusterTemplateOptions{ - Kubeconfig: "kubeconfig", + Kubeconfig: Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, ProviderRepositorySource: &ProviderRepositorySourceOptions{ InfrastructureProvider: "infra:v3.0.0", Flavor: "", @@ -479,7 +481,7 @@ func Test_clusterctlClient_GetClusterTemplate(t *testing.T) { name: "URL source - pass", args: args{ options: GetClusterTemplateOptions{ - Kubeconfig: "kubeconfig", + Kubeconfig: Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, URLSource: &URLSourceOptions{ URL: path, }, @@ -498,7 +500,7 @@ func Test_clusterctlClient_GetClusterTemplate(t *testing.T) { name: "ConfigMap source - pass", args: args{ options: GetClusterTemplateOptions{ - Kubeconfig: "kubeconfig", + Kubeconfig: Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, ConfigMapSource: &ConfigMapSourceOptions{ Namespace: "ns1", Name: "my-template", diff --git a/cmd/clusterctl/client/delete.go b/cmd/clusterctl/client/delete.go index 43676a2c5651..fcf8886c14a1 100644 --- a/cmd/clusterctl/client/delete.go +++ b/cmd/clusterctl/client/delete.go @@ -25,8 +25,9 @@ import ( // DeleteOptions carries the options supported by Delete. type DeleteOptions struct { - // Kubeconfig file to use for accessing the management cluster. If empty, default discovery rules apply. - Kubeconfig string + // Kubeconfig defines the kubeconfig to use for accessing the management cluster. If empty, + // default rules for kubeconfig discovery will be used. + Kubeconfig Kubeconfig // Namespace where the provider to be deleted lives. If unspecified, the namespace name will be inferred // from the current configuration. diff --git a/cmd/clusterctl/client/delete_test.go b/cmd/clusterctl/client/delete_test.go index a9732ea06d58..51c9c7cedb40 100644 --- a/cmd/clusterctl/client/delete_test.go +++ b/cmd/clusterctl/client/delete_test.go @@ -24,6 +24,7 @@ import ( "k8s.io/apimachinery/pkg/util/sets" clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3" + "sigs.k8s.io/cluster-api/cmd/clusterctl/client/cluster" ) func Test_clusterctlClient_Delete(t *testing.T) { @@ -47,7 +48,7 @@ func Test_clusterctlClient_Delete(t *testing.T) { }, args: args{ options: DeleteOptions{ - Kubeconfig: "kubeconfig", + Kubeconfig: Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, IncludeNamespace: false, IncludeCRDs: false, Namespace: "", @@ -68,7 +69,7 @@ func Test_clusterctlClient_Delete(t *testing.T) { }, args: args{ options: DeleteOptions{ - Kubeconfig: "kubeconfig", + Kubeconfig: Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, IncludeNamespace: false, IncludeCRDs: false, Namespace: "capbpk-system", @@ -89,7 +90,7 @@ func Test_clusterctlClient_Delete(t *testing.T) { }, args: args{ options: DeleteOptions{ - Kubeconfig: "kubeconfig", + Kubeconfig: Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, IncludeNamespace: false, IncludeCRDs: false, Namespace: "", // empty namespace triggers namespace auto detection @@ -115,7 +116,8 @@ func Test_clusterctlClient_Delete(t *testing.T) { } g.Expect(err).NotTo(HaveOccurred()) - proxy := tt.fields.client.clusters["kubeconfig"].Proxy() + input := cluster.Kubeconfig(tt.args.options.Kubeconfig) + proxy := tt.fields.client.clusters[input].Proxy() gotProviders := &clusterctlv1.ProviderList{} c, err := proxy.NewClient() @@ -150,7 +152,7 @@ func fakeClusterForDelete() *fakeClient { WithFile("v2.0.0", "components.yaml", componentsYAML("ns2")). WithFile("v2.1.0", "components.yaml", componentsYAML("ns2")) - cluster1 := newFakeCluster("kubeconfig", config1) + cluster1 := newFakeCluster(cluster.Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, config1) cluster1.fakeProxy.WithProviderInventory(capiProviderConfig.Name(), capiProviderConfig.Type(), "v1.0.0", "capi-system", "") cluster1.fakeProxy.WithProviderInventory(bootstrapProviderConfig.Name(), bootstrapProviderConfig.Type(), "v1.0.0", "capbpk-system", "") diff --git a/cmd/clusterctl/client/init.go b/cmd/clusterctl/client/init.go index c674ac24975e..12ac68402af7 100644 --- a/cmd/clusterctl/client/init.go +++ b/cmd/clusterctl/client/init.go @@ -28,6 +28,39 @@ import ( const NoopProvider = "-" +// InitOptions carries the options supported by Init. +type InitOptions struct { + // Kubeconfig defines the kubeconfig to use for accessing the management cluster. If empty, + // default rules for kubeconfig discovery will be used. + Kubeconfig Kubeconfig + + // CoreProvider version (e.g. cluster-api:v0.3.0) to add to the management cluster. If unspecified, the + // cluster-api core provider's latest release is used. + CoreProvider string + + // BootstrapProviders and versions (e.g. kubeadm:v0.3.0) to add to the management cluster. + // If unspecified, the kubeadm bootstrap provider's latest release is used. + BootstrapProviders []string + + // InfrastructureProviders and versions (e.g. aws:v0.5.0) to add to the management cluster. + InfrastructureProviders []string + + // ControlPlaneProviders and versions (e.g. kubeadm:v0.3.0) to add to the management cluster. + // If unspecified, the kubeadm control plane provider latest release is used. + ControlPlaneProviders []string + + // TargetNamespace defines the namespace where the providers should be deployed. If unspecified, each provider + // will be installed in a provider's default namespace. + TargetNamespace string + + // WatchingNamespace defines the namespace the providers should watch to reconcile Cluster API objects. + // If unspecified, the providers watches for Cluster API objects across all namespaces. + WatchingNamespace string + + // LogUsageInstructions instructs the init command to print the usage instructions in case of first run. + LogUsageInstructions bool +} + // Init initializes a management cluster by adding the requested list of providers. func (c *clusterctlClient) Init(options InitOptions) ([]Components, error) { log := logf.Log diff --git a/cmd/clusterctl/client/init_test.go b/cmd/clusterctl/client/init_test.go index 75ba15cca043..f814dcad5acc 100644 --- a/cmd/clusterctl/client/init_test.go +++ b/cmd/clusterctl/client/init_test.go @@ -23,11 +23,64 @@ import ( . "github.com/onsi/gomega" clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3" + "sigs.k8s.io/cluster-api/cmd/clusterctl/client/cluster" "sigs.k8s.io/cluster-api/cmd/clusterctl/client/config" "sigs.k8s.io/cluster-api/cmd/clusterctl/internal/test" "sigs.k8s.io/cluster-api/cmd/clusterctl/internal/util" ) +// TODO: Refactor to actually test InitImages +func Test_clusterctlClient_InitImages(t *testing.T) { + type field struct { + client *fakeClient + } + + type args struct { + coreProvider string + bootstrapProvider []string + controlPlaneProvider []string + infrastructureProvider []string + } + + tests := []struct { + name string + field field + args args + expectedImages []string + wantErr bool + }{ + { + name: "returns error if cannot find cluster client", + field: field{ + client: fakeEmptyCluster(), + }, + args: args{}, + expectedImages: []string{}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + got, err := tt.field.client.InitImages(InitOptions{ + Kubeconfig: Kubeconfig{Path: "kubeconfig", Context: "does-not-exist"}, + CoreProvider: tt.args.coreProvider, + BootstrapProviders: tt.args.bootstrapProvider, + ControlPlaneProviders: tt.args.controlPlaneProvider, + InfrastructureProviders: tt.args.infrastructureProvider, + }) + if tt.wantErr { + g.Expect(err).To(HaveOccurred()) + return + } + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(got).To(HaveLen(len(tt.expectedImages))) + }) + } +} + func Test_clusterctlClient_Init(t *testing.T) { type field struct { client *fakeClient @@ -326,11 +379,12 @@ func Test_clusterctlClient_Init(t *testing.T) { g := NewWithT(t) if tt.field.hasCRD { - g.Expect(tt.field.client.clusters["kubeconfig"].ProviderInventory().EnsureCustomResourceDefinitions()).To(Succeed()) + input := cluster.Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"} + g.Expect(tt.field.client.clusters[input].ProviderInventory().EnsureCustomResourceDefinitions()).To(Succeed()) } got, err := tt.field.client.Init(InitOptions{ - Kubeconfig: "kubeconfig", + Kubeconfig: Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, CoreProvider: tt.args.coreProvider, BootstrapProviders: tt.args.bootstrapProvider, ControlPlaneProviders: tt.args.controlPlaneProvider, @@ -435,7 +489,7 @@ func fakeEmptyCluster() *fakeClient { }). WithFile("v3.0.0", "cluster-template.yaml", templateYAML("ns4", "test")) - cluster1 := newFakeCluster("kubeconfig", config1). + cluster1 := newFakeCluster(cluster.Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, config1). // fake repository for capi, bootstrap and infra provider (matching provider's config) WithRepository(repository1). WithRepository(repository2). @@ -454,11 +508,17 @@ func fakeEmptyCluster() *fakeClient { return client } -// clusterctl client for an management cluster with capi installed (with repository setup for capi, bootstrap and infra provider) +// clusterctl client for a management cluster with capi installed (with repository setup for capi, bootstrap and infra provider) +// It references a cluster client that corresponds to the mgmt-context in the +// kubeconfig file. func fakeInitializedCluster() *fakeClient { client := fakeEmptyCluster() - p := client.clusters["kubeconfig"].Proxy() + input := cluster.Kubeconfig{ + Path: "kubeconfig", + Context: "mgmt-context", + } + p := client.clusters[input].Proxy() fp := p.(*test.FakeProxy) fp.WithProviderInventory(capiProviderConfig.Name(), capiProviderConfig.Type(), "v1.0.0", "capi-system", "") diff --git a/cmd/clusterctl/client/move.go b/cmd/clusterctl/client/move.go index dd89556a2fbf..efc26e982171 100644 --- a/cmd/clusterctl/client/move.go +++ b/cmd/clusterctl/client/move.go @@ -16,6 +16,21 @@ limitations under the License. package client +// MoveOptions carries the options supported by move. +type MoveOptions struct { + // FromKubeconfig defines the kubeconfig to use for accessing the source management cluster. If empty, + // default rules for kubeconfig discovery will be used. + FromKubeconfig Kubeconfig + + // ToKubeconfig defines the kubeconfig to use for accessing the target management cluster. If empty, + // default rules for kubeconfig discovery will be used. + ToKubeconfig Kubeconfig + + // Namespace where the objects describing the workload cluster exists. If unspecified, the current + // namespace will be used. + Namespace string +} + func (c *clusterctlClient) Move(options MoveOptions) error { // Get the client for interacting with the source management cluster. fromCluster, err := c.clusterClientFactory(options.FromKubeconfig) diff --git a/cmd/clusterctl/client/move_test.go b/cmd/clusterctl/client/move_test.go new file mode 100644 index 000000000000..278ec21c764a --- /dev/null +++ b/cmd/clusterctl/client/move_test.go @@ -0,0 +1,127 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package client + +import ( + "testing" + + . "github.com/onsi/gomega" + clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3" + "sigs.k8s.io/cluster-api/cmd/clusterctl/client/cluster" + "sigs.k8s.io/cluster-api/cmd/clusterctl/client/config" +) + +func Test_clusterctlClient_Move(t *testing.T) { + type fields struct { + client *fakeClient + } + type args struct { + options MoveOptions + } + tests := []struct { + name string + fields fields + args args + wantErr bool + }{ + { + name: "does not return error if cluster client is found", + fields: fields{ + client: fakeClientForMove(), // core v1.0.0 (v1.0.1 available), infra v2.0.0 (v2.0.1 available) + }, + args: args{ + options: MoveOptions{ + FromKubeconfig: Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, + ToKubeconfig: Kubeconfig{Path: "kubeconfig", Context: "worker-context"}, + }, + }, + wantErr: false, + }, + { + name: "returns an error if from cluster client is not found", + fields: fields{ + client: fakeClientForMove(), // core v1.0.0 (v1.0.1 available), infra v2.0.0 (v2.0.1 available) + }, + args: args{ + options: MoveOptions{ + FromKubeconfig: Kubeconfig{Path: "kubeconfig", Context: "does-not-exist"}, + ToKubeconfig: Kubeconfig{Path: "kubeconfig", Context: "worker-context"}, + }, + }, + wantErr: true, + }, + { + name: "returns an error if to cluster client is not found", + fields: fields{ + client: fakeClientForMove(), // core v1.0.0 (v1.0.1 available), infra v2.0.0 (v2.0.1 available) + }, + args: args{ + options: MoveOptions{ + FromKubeconfig: Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, + ToKubeconfig: Kubeconfig{Path: "kubeconfig", Context: "does-not-exist"}, + }, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + err := tt.fields.client.Move(tt.args.options) + if tt.wantErr { + g.Expect(err).To(HaveOccurred()) + return + } + g.Expect(err).NotTo(HaveOccurred()) + }) + } +} + +func fakeClientForMove() *fakeClient { + core := config.NewProvider("cluster-api", "https://somewhere.com", clusterctlv1.CoreProviderType) + infra := config.NewProvider("infra", "https://somewhere.com", clusterctlv1.InfrastructureProviderType) + + config1 := newFakeConfig(). + WithProvider(core). + WithProvider(infra) + + cluster1 := newFakeCluster(cluster.Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, config1). + WithProviderInventory(core.Name(), core.Type(), "v1.0.0", "cluster-api-system", ""). + WithProviderInventory(infra.Name(), infra.Type(), "v2.0.0", "infra-system", ""). + WithObjectMover(&fakeObjectMover{}) + + // Creating this cluster for move_test + cluster2 := newFakeCluster(cluster.Kubeconfig{Path: "kubeconfig", Context: "worker-context"}, config1). + WithProviderInventory(core.Name(), core.Type(), "v1.0.0", "cluster-api-system", ""). + WithProviderInventory(infra.Name(), infra.Type(), "v2.0.0", "infra-system", "") + + client := newFakeClient(config1). + WithCluster(cluster1). + WithCluster(cluster2) + + return client +} + +type fakeObjectMover struct { + moveErr error +} + +func (f *fakeObjectMover) Move(namespace string, toCluster cluster.Client) error { + return f.moveErr +} diff --git a/cmd/clusterctl/client/upgrade.go b/cmd/clusterctl/client/upgrade.go index 429baabc332a..81bbe8021b41 100644 --- a/cmd/clusterctl/client/upgrade.go +++ b/cmd/clusterctl/client/upgrade.go @@ -27,8 +27,8 @@ import ( // PlanUpgradeOptions carries the options supported by upgrade plan. type PlanUpgradeOptions struct { - // Kubeconfig file to use for accessing the management cluster. If empty, default discovery rules apply. - Kubeconfig string + // Kubeconfig defines the kubeconfig to use for accessing the management cluster. If empty, default discovery rules apply. + Kubeconfig Kubeconfig } func (c *clusterctlClient) PlanUpgrade(options PlanUpgradeOptions) ([]UpgradePlan, error) { @@ -43,14 +43,14 @@ func (c *clusterctlClient) PlanUpgrade(options PlanUpgradeOptions) ([]UpgradePla return nil, err } - upgradePlan, err := cluster.ProviderUpgrader().Plan() + upgradePlans, err := cluster.ProviderUpgrader().Plan() if err != nil { return nil, err } // UpgradePlan is an alias for cluster.UpgradePlan; this makes the conversion - aliasUpgradePlan := make([]UpgradePlan, len(upgradePlan)) - for i, plan := range upgradePlan { + aliasUpgradePlan := make([]UpgradePlan, len(upgradePlans)) + for i, plan := range upgradePlans { aliasUpgradePlan[i] = UpgradePlan{ Contract: plan.Contract, CoreProvider: plan.CoreProvider, @@ -63,8 +63,8 @@ func (c *clusterctlClient) PlanUpgrade(options PlanUpgradeOptions) ([]UpgradePla // ApplyUpgradeOptions carries the options supported by upgrade apply. type ApplyUpgradeOptions struct { - // Kubeconfig file to use for accessing the management cluster. If empty, default discovery rules apply. - Kubeconfig string + // Kubeconfig to use for accessing the management cluster. If empty, default discovery rules apply. + Kubeconfig Kubeconfig // ManagementGroup that should be upgraded (e.g. capi-system/cluster-api). ManagementGroup string diff --git a/cmd/clusterctl/client/upgrade_test.go b/cmd/clusterctl/client/upgrade_test.go index e4e325d532a8..8109b67c20fa 100644 --- a/cmd/clusterctl/client/upgrade_test.go +++ b/cmd/clusterctl/client/upgrade_test.go @@ -30,6 +30,59 @@ import ( "sigs.k8s.io/cluster-api/cmd/clusterctl/client/config" ) +func Test_clusterctlClient_PlanUpgrade(t *testing.T) { + type fields struct { + client *fakeClient + } + type args struct { + options PlanUpgradeOptions + } + tests := []struct { + name string + fields fields + args args + wantErr bool + }{ + { + name: "does not return error if cluster client is found", + fields: fields{ + client: fakeClientForUpgrade(), // core v1.0.0 (v1.0.1 available), infra v2.0.0 (v2.0.1 available) + }, + args: args{ + options: PlanUpgradeOptions{ + Kubeconfig: Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, + }, + }, + wantErr: false, + }, + { + name: "returns an error if cluster client is not found", + fields: fields{ + client: fakeClientForUpgrade(), // core v1.0.0 (v1.0.1 available), infra v2.0.0 (v2.0.1 available) + }, + args: args{ + options: PlanUpgradeOptions{ + Kubeconfig: Kubeconfig{Path: "kubeconfig", Context: "some-other-context"}, + }, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + _, err := tt.fields.client.PlanUpgrade(tt.args.options) + if tt.wantErr { + g.Expect(err).To(HaveOccurred()) + return + } + g.Expect(err).NotTo(HaveOccurred()) + }) + } +} + func Test_clusterctlClient_ApplyUpgrade(t *testing.T) { type fields struct { client *fakeClient @@ -47,11 +100,11 @@ func Test_clusterctlClient_ApplyUpgrade(t *testing.T) { { name: "apply a plan", fields: fields{ - client: fakeClientFoUpgrade(), // core v1.0.0 (v1.0.1 available), infra v2.0.0 (v2.0.1 available) + client: fakeClientForUpgrade(), // core v1.0.0 (v1.0.1 available), infra v2.0.0 (v2.0.1 available) }, args: args{ options: ApplyUpgradeOptions{ - Kubeconfig: "kubeconfig", + Kubeconfig: Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, ManagementGroup: "cluster-api-system/cluster-api", Contract: "v1alpha3", CoreProvider: "", @@ -76,11 +129,11 @@ func Test_clusterctlClient_ApplyUpgrade(t *testing.T) { { name: "apply a custom plan - core provider only", fields: fields{ - client: fakeClientFoUpgrade(), // core v1.0.0 (v1.0.1 available), infra v2.0.0 (v2.0.1 available) + client: fakeClientForUpgrade(), // core v1.0.0 (v1.0.1 available), infra v2.0.0 (v2.0.1 available) }, args: args{ options: ApplyUpgradeOptions{ - Kubeconfig: "kubeconfig", + Kubeconfig: Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, ManagementGroup: "cluster-api-system/cluster-api", Contract: "", CoreProvider: "cluster-api-system/cluster-api:v1.0.1", @@ -105,11 +158,11 @@ func Test_clusterctlClient_ApplyUpgrade(t *testing.T) { { name: "apply a custom plan - infra provider only", fields: fields{ - client: fakeClientFoUpgrade(), // core v1.0.0 (v1.0.1 available), infra v2.0.0 (v2.0.1 available) + client: fakeClientForUpgrade(), // core v1.0.0 (v1.0.1 available), infra v2.0.0 (v2.0.1 available) }, args: args{ options: ApplyUpgradeOptions{ - Kubeconfig: "kubeconfig", + Kubeconfig: Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, ManagementGroup: "cluster-api-system/cluster-api", Contract: "", CoreProvider: "", @@ -134,11 +187,11 @@ func Test_clusterctlClient_ApplyUpgrade(t *testing.T) { { name: "apply a custom plan - both providers", fields: fields{ - client: fakeClientFoUpgrade(), // core v1.0.0 (v1.0.1 available), infra v2.0.0 (v2.0.1 available) + client: fakeClientForUpgrade(), // core v1.0.0 (v1.0.1 available), infra v2.0.0 (v2.0.1 available) }, args: args{ options: ApplyUpgradeOptions{ - Kubeconfig: "kubeconfig", + Kubeconfig: Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, ManagementGroup: "cluster-api-system/cluster-api", Contract: "", CoreProvider: "cluster-api-system/cluster-api:v1.0.1", @@ -172,7 +225,9 @@ func Test_clusterctlClient_ApplyUpgrade(t *testing.T) { } g.Expect(err).NotTo(HaveOccurred()) - proxy := tt.fields.client.clusters["kubeconfig"].Proxy() + // converting between client and cluster alias for Kubeconfig + input := cluster.Kubeconfig(tt.args.options.Kubeconfig) + proxy := tt.fields.client.clusters[input].Proxy() gotProviders := &clusterctlv1.ProviderList{} c, err := proxy.NewClient() @@ -194,7 +249,7 @@ func Test_clusterctlClient_ApplyUpgrade(t *testing.T) { } } -func fakeClientFoUpgrade() *fakeClient { +func fakeClientForUpgrade() *fakeClient { core := config.NewProvider("cluster-api", "https://somewhere.com", clusterctlv1.CoreProviderType) infra := config.NewProvider("infra", "https://somewhere.com", clusterctlv1.InfrastructureProviderType) @@ -223,7 +278,7 @@ func fakeClientFoUpgrade() *fakeClient { }, }) - cluster1 := newFakeCluster("kubeconfig", config1). + cluster1 := newFakeCluster(cluster.Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, config1). WithRepository(repository1). WithRepository(repository2). WithProviderInventory(core.Name(), core.Type(), "v1.0.0", "cluster-api-system", ""). diff --git a/cmd/clusterctl/cmd/config_cluster.go b/cmd/clusterctl/cmd/config_cluster.go index 4666fd584ef5..68865b023a63 100644 --- a/cmd/clusterctl/cmd/config_cluster.go +++ b/cmd/clusterctl/cmd/config_cluster.go @@ -27,6 +27,7 @@ import ( type configClusterOptions struct { kubeconfig string + kubeconfigContext string flavor string infrastructureProvider string @@ -94,6 +95,8 @@ var configClusterClusterCmd = &cobra.Command{ func init() { configClusterClusterCmd.Flags().StringVar(&cc.kubeconfig, "kubeconfig", "", "Path to a kubeconfig file to use for the management cluster. If empty, default discovery rules apply.") + configClusterClusterCmd.Flags().StringVar(&cc.kubeconfigContext, "kubeconfig-context", "", + "Context to be used within the kubeconfig file. If empty, current context will be used.") // flags for the template variables configClusterClusterCmd.Flags().StringVarP(&cc.targetNamespace, "target-namespace", "n", "", @@ -137,7 +140,7 @@ func runGetClusterTemplate(cmd *cobra.Command, name string) error { } templateOptions := client.GetClusterTemplateOptions{ - Kubeconfig: cc.kubeconfig, + Kubeconfig: client.Kubeconfig{Path: cc.kubeconfig, Context: cc.kubeconfigContext}, ClusterName: name, TargetNamespace: cc.targetNamespace, KubernetesVersion: cc.kubernetesVersion, diff --git a/cmd/clusterctl/cmd/delete.go b/cmd/clusterctl/cmd/delete.go index 4e6ee0d092ef..c36b74e3f1b0 100644 --- a/cmd/clusterctl/cmd/delete.go +++ b/cmd/clusterctl/cmd/delete.go @@ -24,6 +24,7 @@ import ( type deleteOptions struct { kubeconfig string + kubeconfigContext string targetNamespace string coreProvider string bootstrapProviders []string @@ -84,6 +85,8 @@ var deleteCmd = &cobra.Command{ func init() { deleteCmd.Flags().StringVar(&dd.kubeconfig, "kubeconfig", "", "Path to the kubeconfig file to use for accessing the management cluster. If unspecified, default discovery rules apply.") + deleteCmd.Flags().StringVar(&dd.kubeconfigContext, "kubeconfig-context", "", + "Context to be used within the kubeconfig file. If empty, current context will be used.") deleteCmd.Flags().StringVar(&dd.targetNamespace, "namespace", "", "The namespace where the provider to be deleted lives. If unspecified, the namespace name will be inferred from the current configuration") deleteCmd.Flags().BoolVar(&dd.includeNamespace, "include-namespace", false, @@ -126,7 +129,7 @@ func runDelete() error { } if err := c.Delete(client.DeleteOptions{ - Kubeconfig: dd.kubeconfig, + Kubeconfig: client.Kubeconfig{Path: dd.kubeconfig, Context: dd.kubeconfigContext}, IncludeNamespace: dd.includeNamespace, IncludeCRDs: dd.includeCRDs, Namespace: dd.targetNamespace, diff --git a/cmd/clusterctl/cmd/init.go b/cmd/clusterctl/cmd/init.go index 66f6f8b05770..5c311f0908fb 100644 --- a/cmd/clusterctl/cmd/init.go +++ b/cmd/clusterctl/cmd/init.go @@ -25,6 +25,7 @@ import ( type initOptions struct { kubeconfig string + kubeconfigContext string coreProvider string bootstrapProviders []string controlPlaneProviders []string @@ -91,6 +92,8 @@ var initCmd = &cobra.Command{ func init() { initCmd.Flags().StringVar(&io.kubeconfig, "kubeconfig", "", "Path to the kubeconfig for the management cluster. If unspecified, default discovery rules apply.") + initCmd.Flags().StringVar(&io.kubeconfigContext, "kubeconfig-context", "", + "Context to be used within the kubeconfig file. If empty, current context will be used.") initCmd.Flags().StringVar(&io.coreProvider, "core", "", "Core provider version (e.g. cluster-api:v0.3.0) to add to the management cluster. If unspecified, Cluster API's latest release is used.") initCmd.Flags().StringSliceVarP(&io.infrastructureProviders, "infrastructure", "i", nil, @@ -118,7 +121,7 @@ func runInit() error { } options := client.InitOptions{ - Kubeconfig: io.kubeconfig, + Kubeconfig: client.Kubeconfig{Path: io.kubeconfig, Context: io.kubeconfigContext}, CoreProvider: io.coreProvider, BootstrapProviders: io.bootstrapProviders, ControlPlaneProviders: io.controlPlaneProviders, diff --git a/cmd/clusterctl/cmd/move.go b/cmd/clusterctl/cmd/move.go index e8bac515c156..2b96cd3e534c 100644 --- a/cmd/clusterctl/cmd/move.go +++ b/cmd/clusterctl/cmd/move.go @@ -23,9 +23,11 @@ import ( ) type moveOptions struct { - fromKubeconfig string - namespace string - toKubeconfig string + fromKubeconfig string + fromKubeconfigContext string + toKubeconfig string + toKubeconfigContext string + namespace string } var mo = &moveOptions{} @@ -52,6 +54,10 @@ func init() { "Path to the kubeconfig file for the source management cluster. If unspecified, default discovery rules apply.") moveCmd.Flags().StringVar(&mo.toKubeconfig, "to-kubeconfig", "", "Path to the kubeconfig file to use for the destination management cluster.") + moveCmd.Flags().StringVar(&mo.fromKubeconfigContext, "kubeconfig-context", "", + "Context to be used within the kubeconfig file for the source management cluster. If empty, current context will be used.") + moveCmd.Flags().StringVar(&mo.toKubeconfigContext, "to-kubeconfig-context", "", + "Context to be used within the kubeconfig file for the destination management cluster. If empty, current context will be used.") moveCmd.Flags().StringVarP(&mo.namespace, "namespace", "n", "", "The namespace where the workload cluster is hosted. If unspecified, the current context's namespace is used.") @@ -69,8 +75,8 @@ func runMove() error { } if err := c.Move(client.MoveOptions{ - FromKubeconfig: mo.fromKubeconfig, - ToKubeconfig: mo.toKubeconfig, + FromKubeconfig: client.Kubeconfig{Path: mo.fromKubeconfig, Context: mo.fromKubeconfigContext}, + ToKubeconfig: client.Kubeconfig{Path: mo.toKubeconfig, Context: mo.toKubeconfigContext}, Namespace: mo.namespace, }); err != nil { return err diff --git a/cmd/clusterctl/cmd/upgrade_apply.go b/cmd/clusterctl/cmd/upgrade_apply.go index ef0df09bb808..a4fe466c3ff4 100644 --- a/cmd/clusterctl/cmd/upgrade_apply.go +++ b/cmd/clusterctl/cmd/upgrade_apply.go @@ -25,6 +25,7 @@ import ( type upgradeApplyOptions struct { kubeconfig string + kubeconfigContext string managementGroup string contract string coreProvider string @@ -60,6 +61,8 @@ var upgradeApplyCmd = &cobra.Command{ func init() { upgradeApplyCmd.Flags().StringVar(&ua.kubeconfig, "kubeconfig", "", "Path to the kubeconfig file to use for accessing the management cluster. If unspecified, default discovery rules apply.") + upgradeApplyCmd.Flags().StringVar(&ua.kubeconfigContext, "kubeconfig-context", "", + "Context to be used within the kubeconfig file. If empty, current context will be used.") upgradeApplyCmd.Flags().StringVar(&ua.managementGroup, "management-group", "", "The management group that should be upgraded (e.g. capi-system/cluster-api)") upgradeApplyCmd.Flags().StringVar(&ua.contract, "contract", "", @@ -91,7 +94,7 @@ func runUpgradeApply() error { } if err := c.ApplyUpgrade(client.ApplyUpgradeOptions{ - Kubeconfig: ua.kubeconfig, + Kubeconfig: client.Kubeconfig{Path: ua.kubeconfig, Context: ua.kubeconfigContext}, ManagementGroup: ua.managementGroup, Contract: ua.contract, CoreProvider: ua.coreProvider, diff --git a/cmd/clusterctl/cmd/upgrade_plan.go b/cmd/clusterctl/cmd/upgrade_plan.go index b8dd922fb6af..828bda0af8be 100644 --- a/cmd/clusterctl/cmd/upgrade_plan.go +++ b/cmd/clusterctl/cmd/upgrade_plan.go @@ -26,7 +26,8 @@ import ( ) type upgradePlanOptions struct { - kubeconfig string + kubeconfig string + kubeconfigContext string } var up = &upgradePlanOptions{} @@ -56,6 +57,8 @@ var upgradePlanCmd = &cobra.Command{ func init() { upgradePlanCmd.Flags().StringVar(&up.kubeconfig, "kubeconfig", "", "Path to the kubeconfig file to use for accessing the management cluster. If empty, default discovery rules apply.") + upgradePlanCmd.Flags().StringVar(&up.kubeconfigContext, "kubeconfig-context", "", + "Context to be used within the kubeconfig file. If empty, current context will be used.") } func runUpgradePlan() error { @@ -65,7 +68,7 @@ func runUpgradePlan() error { } upgradePlans, err := c.PlanUpgrade(client.PlanUpgradeOptions{ - Kubeconfig: up.kubeconfig, + Kubeconfig: client.Kubeconfig{Path: up.kubeconfig, Context: up.kubeconfigContext}, }) if err != nil { return err diff --git a/cmd/clusterctl/internal/test/fake_proxy.go b/cmd/clusterctl/internal/test/fake_proxy.go index 2e3293586606..18b6c31776bc 100644 --- a/cmd/clusterctl/internal/test/fake_proxy.go +++ b/cmd/clusterctl/internal/test/fake_proxy.go @@ -22,6 +22,7 @@ import ( "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" clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3" clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3" fakebootstrap "sigs.k8s.io/cluster-api/cmd/clusterctl/internal/test/providers/bootstrap" @@ -61,6 +62,10 @@ func (f *FakeProxy) ValidateKubernetesVersion() error { return nil } +func (f *FakeProxy) GetConfig() (*rest.Config, error) { + return nil, nil +} + func (f *FakeProxy) NewClient() (client.Client, error) { if f.cs != nil { return f.cs, nil diff --git a/test/framework/clusterctl/client.go b/test/framework/clusterctl/client.go index 80c31d6b3d0d..70973f27ee40 100644 --- a/test/framework/clusterctl/client.go +++ b/test/framework/clusterctl/client.go @@ -45,7 +45,7 @@ const ( type InitInput struct { LogFolder string ClusterctlConfigPath string - KubeconfigPath string + Kubeconfig clusterctlclient.Kubeconfig CoreProvider string BootstrapProviders []string ControlPlaneProviders []string @@ -62,7 +62,7 @@ func Init(ctx context.Context, input InitInput) { ) initOpt := clusterctlclient.InitOptions{ - Kubeconfig: input.KubeconfigPath, + Kubeconfig: input.Kubeconfig, CoreProvider: input.CoreProvider, BootstrapProviders: input.BootstrapProviders, ControlPlaneProviders: input.ControlPlaneProviders, @@ -81,7 +81,7 @@ func Init(ctx context.Context, input InitInput) { type ConfigClusterInput struct { LogFolder string ClusterctlConfigPath string - KubeconfigPath string + Kubeconfig clusterctlclient.Kubeconfig InfrastructureProvider string Namespace string ClusterName string @@ -103,7 +103,7 @@ func ConfigCluster(ctx context.Context, input ConfigClusterInput) []byte { ) templateOptions := clusterctlclient.GetClusterTemplateOptions{ - Kubeconfig: input.KubeconfigPath, + Kubeconfig: input.Kubeconfig, ProviderRepositorySource: &clusterctlclient.ProviderRepositorySourceOptions{ InfrastructureProvider: input.InfrastructureProvider, Flavor: input.Flavor, @@ -133,8 +133,8 @@ func ConfigCluster(ctx context.Context, input ConfigClusterInput) []byte { type MoveInput struct { LogFolder string ClusterctlConfigPath string - FromKubeconfigPath string - ToKubeconfigPath string + FromKubeconfig clusterctlclient.Kubeconfig + ToKubeconfig clusterctlclient.Kubeconfig Namespace string } @@ -146,8 +146,8 @@ func Move(ctx context.Context, input MoveInput) { defer log.Close() options := clusterctlclient.MoveOptions{ - FromKubeconfig: input.FromKubeconfigPath, - ToKubeconfig: input.ToKubeconfigPath, + FromKubeconfig: input.FromKubeconfig, + ToKubeconfig: input.ToKubeconfig, Namespace: input.Namespace, }