From 2997f18a4a6f64bbc23c12691ab4d8ec00294b99 Mon Sep 17 00:00:00 2001 From: fabriziopandini Date: Fri, 19 Mar 2021 17:48:13 +0100 Subject: [PATCH] clusterctl v1alpha3 should block on v1alpha4 clusters --- cmd/clusterctl/client/cluster/inventory.go | 90 ++++++++++- .../client/cluster/inventory_test.go | 149 +++++++++++++++++- cmd/clusterctl/client/config.go | 5 + cmd/clusterctl/client/config_test.go | 4 +- cmd/clusterctl/client/delete.go | 6 + cmd/clusterctl/client/delete_test.go | 2 +- cmd/clusterctl/client/describe.go | 5 + cmd/clusterctl/client/get_kubeconfig.go | 9 +- cmd/clusterctl/client/get_kubeconfig_test.go | 7 +- cmd/clusterctl/client/init.go | 28 ++-- cmd/clusterctl/client/move.go | 10 ++ cmd/clusterctl/client/move_test.go | 7 +- .../client/repository/metadata_client_test.go | 24 +-- cmd/clusterctl/client/upgrade.go | 17 +- cmd/clusterctl/client/upgrade_test.go | 5 +- cmd/clusterctl/internal/test/contracts.go | 6 +- cmd/clusterctl/internal/test/fake_proxy.go | 37 ++++- 17 files changed, 359 insertions(+), 52 deletions(-) diff --git a/cmd/clusterctl/client/cluster/inventory.go b/cmd/clusterctl/client/cluster/inventory.go index 3b6a0ae7bf99..4a85aac3a8e0 100644 --- a/cmd/clusterctl/client/cluster/inventory.go +++ b/cmd/clusterctl/client/cluster/inventory.go @@ -17,14 +17,15 @@ limitations under the License. package cluster import ( + "fmt" "time" "github.com/pkg/errors" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" - apimeta "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/util/sets" + clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3" clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3" "sigs.k8s.io/cluster-api/cmd/clusterctl/config" logf "sigs.k8s.io/cluster-api/cmd/clusterctl/log" @@ -39,6 +40,41 @@ const ( waitInventoryCRDTimeout = 1 * time.Minute ) +// CheckCAPIContractOption is some configuration that modifies options for CheckCAPIContract. +type CheckCAPIContractOption interface { + // Apply applies this configuration to the given CheckCAPIContractOptions. + Apply(*CheckCAPIContractOptions) +} + +// CheckCAPIContractOptions contains options for CheckCAPIContract. +type CheckCAPIContractOptions struct { + // AllowCAPINotInstalled instructs CheckCAPIContract to tolerate management clusters without Cluster API installed yet. + AllowCAPINotInstalled bool + + // AllowCAPIContract instructs CheckCAPIContract to tolerate management clusters with Cluster API with the given contract. + AllowCAPIContract string +} + +// AllowCAPINotInstalled instructs CheckCAPIContract to tolerate management clusters without Cluster API installed yet. +// NOTE: This allows clusterctl init to run on empty management clusters. +type AllowCAPINotInstalled struct{} + +// Apply applies this configuration to the given CheckCAPIContractOptions. +func (t AllowCAPINotInstalled) Apply(in *CheckCAPIContractOptions) { + in.AllowCAPINotInstalled = true +} + +// AllowCAPIContract instructs CheckCAPIContract to tolerate management clusters with Cluster API with the given contract. +// NOTE: This allows clusterctl upgrade to work on management clusters with old contract. +type AllowCAPIContract struct { + Contract string +} + +// Apply applies this configuration to the given CheckCAPIContractOptions. +func (t AllowCAPIContract) Apply(in *CheckCAPIContractOptions) { + in.AllowCAPIContract = t.Contract +} + // InventoryClient exposes methods to interface with a cluster's provider inventory. type InventoryClient interface { // EnsureCustomResourceDefinitions installs the CRD required for creating inventory items, if necessary. @@ -69,6 +105,10 @@ type InventoryClient interface { // GetManagementGroups returns the list of management groups defined in the management cluster. GetManagementGroups() (ManagementGroupList, error) + + // CheckCAPIContract checks the Cluster API version installed in the management cluster, and fails if this version + // does not match the current one supported by clusterctl. + CheckCAPIContract(...CheckCAPIContractOption) error } // inventoryClient implements InventoryClient. @@ -188,14 +228,20 @@ func checkInventoryCRDs(proxy Proxy) (bool, error) { return false, err } - l := &clusterctlv1.ProviderList{} - if err = c.List(ctx, l); err == nil { - return true, nil - } - if !apimeta.IsNoMatchError(err) { + crd := &apiextensionsv1.CustomResourceDefinition{} + if err := c.Get(ctx, client.ObjectKey{Name: fmt.Sprintf("providers.%s", clusterctlv1.GroupVersion.Group)}, crd); err != nil { + if apierrors.IsNotFound(err) { + return false, nil + } return false, errors.Wrap(err, "failed to check if the clusterctl inventory CRD exists") } - return false, nil + + for _, version := range crd.Spec.Versions { + if version.Name == clusterctlv1.GroupVersion.Version { + return true, nil + } + } + return true, errors.Errorf("clusterctl inventory CRD does not defines the %s version", clusterctlv1.GroupVersion.Version) } func (p *inventoryClient) createObj(o unstructured.Unstructured) error { @@ -343,3 +389,33 @@ func (p *inventoryClient) GetDefaultProviderNamespace(provider string, providerT // There is no provider or more than one namespace for this provider; in both cases, a default provider namespace cannot be decided. return "", nil } + +func (p *inventoryClient) CheckCAPIContract(options ...CheckCAPIContractOption) error { + opt := &CheckCAPIContractOptions{} + for _, o := range options { + o.Apply(opt) + } + + c, err := p.proxy.NewClient() + if err != nil { + return err + } + + crd := &apiextensionsv1.CustomResourceDefinition{} + if err := c.Get(ctx, client.ObjectKey{Name: fmt.Sprintf("clusters.%s", clusterv1.GroupVersion.Group)}, crd); err != nil { + if opt.AllowCAPINotInstalled && apierrors.IsNotFound(err) { + return nil + } + return errors.Wrap(err, "failed to check Cluster API version") + } + + for _, version := range crd.Spec.Versions { + if version.Storage { + if version.Name == clusterv1.GroupVersion.Version || version.Name == opt.AllowCAPIContract { + return nil + } + return errors.Errorf("this version of clusterctl could be used only with %q management clusters, %q detected", clusterv1.GroupVersion.Version, version.Name) + } + } + return errors.Errorf("failed to check Cluster API version") +} diff --git a/cmd/clusterctl/client/cluster/inventory_test.go b/cmd/clusterctl/client/cluster/inventory_test.go index 9155f0993b58..26f22c623504 100644 --- a/cmd/clusterctl/client/cluster/inventory_test.go +++ b/cmd/clusterctl/client/cluster/inventory_test.go @@ -21,7 +21,7 @@ import ( "time" . "github.com/onsi/gomega" - + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/wait" @@ -33,13 +33,14 @@ func fakePollImmediateWaiter(interval, timeout time.Duration, condition wait.Con return nil } -func Test_inventoryClient_EnsureCustomResourceDefinitions(t *testing.T) { +func Test_inventoryClient_CheckInventoryCRDs(t *testing.T) { type fields struct { alreadyHasCRD bool } tests := []struct { name string fields fields + want bool wantErr bool }{ { @@ -47,6 +48,7 @@ func Test_inventoryClient_EnsureCustomResourceDefinitions(t *testing.T) { fields: fields{ alreadyHasCRD: false, }, + want: false, wantErr: false, }, { @@ -54,6 +56,7 @@ func Test_inventoryClient_EnsureCustomResourceDefinitions(t *testing.T) { fields: fields{ alreadyHasCRD: true, }, + want: true, wantErr: false, }, } @@ -61,13 +64,15 @@ func Test_inventoryClient_EnsureCustomResourceDefinitions(t *testing.T) { t.Run(tt.name, func(t *testing.T) { g := NewWithT(t) - p := newInventoryClient(test.NewFakeProxy(), fakePollImmediateWaiter) + proxy := test.NewFakeProxy() + p := newInventoryClient(proxy, fakePollImmediateWaiter) if tt.fields.alreadyHasCRD { //forcing creation of metadata before test g.Expect(p.EnsureCustomResourceDefinitions()).To(Succeed()) } - err := p.EnsureCustomResourceDefinitions() + res, err := checkInventoryCRDs(proxy) + g.Expect(res).To(Equal(tt.want)) if tt.wantErr { g.Expect(err).To(HaveOccurred()) } else { @@ -194,3 +199,139 @@ func Test_inventoryClient_Create(t *testing.T) { }) } } + +func Test_CheckCAPIContract(t *testing.T) { + type args struct { + options []CheckCAPIContractOption + } + type fields struct { + proxy Proxy + } + + tests := []struct { + name string + fields fields + args args + wantErr bool + }{ + { + name: "Fails if Cluster API is not installed", + fields: fields{ + proxy: test.NewFakeProxy().WithObjs(), + }, + args: args{}, + wantErr: true, + }, + { + name: "Pass if Cluster API is not installed, but this is explicitly tolerated", + fields: fields{ + proxy: test.NewFakeProxy().WithObjs(), + }, + args: args{ + options: []CheckCAPIContractOption{AllowCAPINotInstalled{}}, + }, + wantErr: false, + }, + { + name: "Pass when Cluster API with current contract is installed", + fields: fields{ + proxy: test.NewFakeProxy().WithObjs(&apiextensionsv1.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{Name: "clusters.cluster.x-k8s.io"}, + Spec: apiextensionsv1.CustomResourceDefinitionSpec{ + Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ + { + Name: test.PreviousCAPIContractNotSupported, + }, + { + Name: test.CurrentCAPIContract, + Storage: true, + }, + }, + }, + }), + }, + args: args{}, + wantErr: false, + }, + { + name: "Fails when Cluster API with previous contract is installed", + fields: fields{ + proxy: test.NewFakeProxy().WithObjs(&apiextensionsv1.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{Name: "clusters.cluster.x-k8s.io"}, + Spec: apiextensionsv1.CustomResourceDefinitionSpec{ + Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ + { + Name: test.PreviousCAPIContractNotSupported, + Storage: true, + }, + { + Name: test.CurrentCAPIContract, + }, + }, + }, + }), + }, + args: args{}, + wantErr: true, + }, + { + name: "Pass when Cluster API with previous contract is installed, but this is explicitly tolerated", + fields: fields{ + proxy: test.NewFakeProxy().WithObjs(&apiextensionsv1.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{Name: "clusters.cluster.x-k8s.io"}, + Spec: apiextensionsv1.CustomResourceDefinitionSpec{ + Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ + { + Name: test.PreviousCAPIContractNotSupported, + Storage: true, + }, + { + Name: test.CurrentCAPIContract, + }, + }, + }, + }), + }, + args: args{ + options: []CheckCAPIContractOption{AllowCAPIContract{Contract: test.PreviousCAPIContractNotSupported}}, + }, + wantErr: false, + }, + { + name: "Fails when Cluster API with next contract is installed", + fields: fields{ + proxy: test.NewFakeProxy().WithObjs(&apiextensionsv1.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{Name: "clusters.cluster.x-k8s.io"}, + Spec: apiextensionsv1.CustomResourceDefinitionSpec{ + Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ + { + Name: test.CurrentCAPIContract, + }, + { + Name: test.NextCAPIContractNotSupported, + Storage: true, + }, + }, + }, + }), + }, + args: args{}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + p := &inventoryClient{ + proxy: tt.fields.proxy, + } + err := p.CheckCAPIContract(tt.args.options...) + if tt.wantErr { + g.Expect(err).To(HaveOccurred()) + return + } + g.Expect(err).NotTo(HaveOccurred()) + }) + } +} diff --git a/cmd/clusterctl/client/config.go b/cmd/clusterctl/client/config.go index eb87d20aad04..a74365f52260 100644 --- a/cmd/clusterctl/client/config.go +++ b/cmd/clusterctl/client/config.go @@ -229,6 +229,11 @@ func (c *clusterctlClient) GetClusterTemplate(options GetClusterTemplateOptions) return nil, err } + // Ensure this command only runs against management clusters with the current Cluster API contract. + if err := cluster.ProviderInventory().CheckCAPIContract(); err != nil { + return nil, err + } + // If the option specifying the targetNamespace is empty, try to detect it. if options.TargetNamespace == "" { currentNamespace, err := cluster.Proxy().CurrentNamespace() diff --git a/cmd/clusterctl/client/config_test.go b/cmd/clusterctl/client/config_test.go index 06b2d10bd3f6..f0ca1dda1de8 100644 --- a/cmd/clusterctl/client/config_test.go +++ b/cmd/clusterctl/client/config_test.go @@ -26,6 +26,7 @@ import ( . "github.com/onsi/gomega" "github.com/pkg/errors" + "sigs.k8s.io/cluster-api/cmd/clusterctl/internal/test" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -469,7 +470,8 @@ func Test_clusterctlClient_GetClusterTemplate(t *testing.T) { cluster1 := newFakeCluster(cluster.Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"}, config1). WithProviderInventory(infraProviderConfig.Name(), infraProviderConfig.Type(), "v3.0.0", "foo", "bar"). - WithObjs(configMap) + WithObjs(configMap). + WithObjs(test.FakeCAPISetupObjects()...) client := newFakeClient(config1). WithCluster(cluster1). diff --git a/cmd/clusterctl/client/delete.go b/cmd/clusterctl/client/delete.go index 40ddfdf0afae..2da4487c7671 100644 --- a/cmd/clusterctl/client/delete.go +++ b/cmd/clusterctl/client/delete.go @@ -66,6 +66,12 @@ func (c *clusterctlClient) Delete(options DeleteOptions) error { return err } + // Ensure this command only runs against management clusters with the current Cluster API contract. + if err := clusterClient.ProviderInventory().CheckCAPIContract(); err != nil { + return err + } + + // Ensure the custom resource definitions required by clusterctl are in place. if err := clusterClient.ProviderInventory().EnsureCustomResourceDefinitions(); err != nil { return err } diff --git a/cmd/clusterctl/client/delete_test.go b/cmd/clusterctl/client/delete_test.go index fd0c7aa4ef0a..8afbf2096fd1 100644 --- a/cmd/clusterctl/client/delete_test.go +++ b/cmd/clusterctl/client/delete_test.go @@ -21,7 +21,6 @@ import ( "testing" . "github.com/onsi/gomega" - "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" @@ -207,6 +206,7 @@ func fakeClusterForDelete() *fakeClient { cluster1.fakeProxy.WithProviderInventory(bootstrapProviderConfig.Name(), bootstrapProviderConfig.Type(), "v1.0.0", "capbpk-system", "") cluster1.fakeProxy.WithProviderInventory(controlPlaneProviderConfig.Name(), controlPlaneProviderConfig.Type(), "v1.0.0", namespace, "") cluster1.fakeProxy.WithProviderInventory(infraProviderConfig.Name(), infraProviderConfig.Type(), "v1.0.0", namespace, "") + cluster1.fakeProxy.WithFakeCAPISetup() client := newFakeClient(config1). // fake repository for capi, bootstrap, controlplane and infra provider (matching provider's config) diff --git a/cmd/clusterctl/client/describe.go b/cmd/clusterctl/client/describe.go index 78bea56c6a45..72e56492ac60 100644 --- a/cmd/clusterctl/client/describe.go +++ b/cmd/clusterctl/client/describe.go @@ -55,6 +55,11 @@ func (c *clusterctlClient) DescribeCluster(options DescribeClusterOptions) (*tre return nil, err } + // Ensure this command only runs against management clusters with the current Cluster API contract. + if err := cluster.ProviderInventory().CheckCAPIContract(); err != nil { + return nil, err + } + // If the option specifying the Namespace is empty, try to detect it. if options.Namespace == "" { currentNamespace, err := cluster.Proxy().CurrentNamespace() diff --git a/cmd/clusterctl/client/get_kubeconfig.go b/cmd/clusterctl/client/get_kubeconfig.go index 6e06fdd8a393..eb6a76f8f94d 100644 --- a/cmd/clusterctl/client/get_kubeconfig.go +++ b/cmd/clusterctl/client/get_kubeconfig.go @@ -16,7 +16,9 @@ limitations under the License. package client -import "github.com/pkg/errors" +import ( + "github.com/pkg/errors" +) //GetKubeconfigOptions carries all the options supported by GetKubeconfig type GetKubeconfigOptions struct { @@ -38,6 +40,11 @@ func (c *clusterctlClient) GetKubeconfig(options GetKubeconfigOptions) (string, return "", err } + // Ensure this command only runs against management clusters with the current Cluster API contract. + if err := clusterClient.ProviderInventory().CheckCAPIContract(); err != nil { + return "", err + } + if options.Namespace == "" { currentNamespace, err := clusterClient.Proxy().CurrentNamespace() if err != nil { diff --git a/cmd/clusterctl/client/get_kubeconfig_test.go b/cmd/clusterctl/client/get_kubeconfig_test.go index ac519f7221e5..dc5cbeb38e96 100644 --- a/cmd/clusterctl/client/get_kubeconfig_test.go +++ b/cmd/clusterctl/client/get_kubeconfig_test.go @@ -28,11 +28,10 @@ func Test_clusterctlClient_GetKubeconfig(t *testing.T) { configClient := newFakeConfig() kubeconfig := cluster.Kubeconfig{Path: "kubeconfig", Context: "mgmt-context"} - clusterClient := &fakeClusterClient{ - kubeconfig: kubeconfig, - } + clusterClient := newFakeCluster(cluster.Kubeconfig{Path: "cluster1"}, configClient) + // create a clusterctl client where the proxy returns an empty namespace - clusterClient.fakeProxy = test.NewFakeProxy().WithNamespace("") + clusterClient.fakeProxy = test.NewFakeProxy().WithNamespace("").WithFakeCAPISetup() badClient := newFakeClient(configClient).WithCluster(clusterClient) tests := []struct { diff --git a/cmd/clusterctl/client/init.go b/cmd/clusterctl/client/init.go index 8a1507453db3..7adb90fdacb3 100644 --- a/cmd/clusterctl/client/init.go +++ b/cmd/clusterctl/client/init.go @@ -71,13 +71,18 @@ func (c *clusterctlClient) Init(options InitOptions) ([]Components, error) { log := logf.Log // gets access to the management cluster - cluster, err := c.clusterClientFactory(ClusterClientFactoryInput{Kubeconfig: options.Kubeconfig}) + clusterClient, err := c.clusterClientFactory(ClusterClientFactoryInput{Kubeconfig: options.Kubeconfig}) if err != nil { return nil, err } // ensure the custom resource definitions required by clusterctl are in place - if err := cluster.ProviderInventory().EnsureCustomResourceDefinitions(); err != nil { + if err := clusterClient.ProviderInventory().EnsureCustomResourceDefinitions(); err != nil { + return nil, err + } + + // Ensure this command only runs against v1alpha4 management clusters + if err := clusterClient.ProviderInventory().CheckCAPIContract(cluster.AllowCAPINotInstalled{}); err != nil { return nil, err } @@ -85,11 +90,11 @@ func (c *clusterctlClient) Init(options InitOptions) ([]Components, error) { // if not we consider this the first time init is executed, and thus we enforce the installation of a core provider, // a bootstrap provider and a control-plane provider (if not already explicitly requested by the user) log.Info("Fetching providers") - firstRun := c.addDefaultProviders(cluster, &options) + firstRun := c.addDefaultProviders(clusterClient, &options) // create an installer service, add the requested providers to the install queue and then perform validation // of the target state of the management cluster before starting the installation. - installer, err := c.setupInstaller(cluster, options) + installer, err := c.setupInstaller(clusterClient, options) if err != nil { return nil, err } @@ -105,7 +110,7 @@ func (c *clusterctlClient) Init(options InitOptions) ([]Components, error) { } // Before installing the providers, ensure the cert-manager Webhook is in place. - if err := cluster.CertManager().EnsureInstalled(); err != nil { + if err := clusterClient.CertManager().EnsureInstalled(); err != nil { return nil, err } @@ -136,28 +141,33 @@ func (c *clusterctlClient) Init(options InitOptions) ([]Components, error) { // Init returns the list of images required for init. func (c *clusterctlClient) InitImages(options InitOptions) ([]string, error) { // gets access to the management cluster - cluster, err := c.clusterClientFactory(ClusterClientFactoryInput{Kubeconfig: options.Kubeconfig}) + clusterClient, err := c.clusterClientFactory(ClusterClientFactoryInput{Kubeconfig: options.Kubeconfig}) if err != nil { return nil, err } + // Ensure this command only runs against empty management clusters or v1alpha4 management clusters. + if err := clusterClient.ProviderInventory().CheckCAPIContract(cluster.AllowCAPINotInstalled{}); err != nil { + return nil, err + } + // checks if the cluster already contains a Core provider. // if not we consider this the first time init is executed, and thus we enforce the installation of a core provider, // a bootstrap provider and a control-plane provider (if not already explicitly requested by the user) - c.addDefaultProviders(cluster, &options) + c.addDefaultProviders(clusterClient, &options) // skip variable parsing when listing images options.skipVariables = true // create an installer service, add the requested providers to the install queue and then perform validation // of the target state of the management cluster before starting the installation. - installer, err := c.setupInstaller(cluster, options) + installer, err := c.setupInstaller(clusterClient, options) if err != nil { return nil, err } // Gets the list of container images required for the cert-manager (if not already installed). - images, err := cluster.CertManager().Images() + images, err := clusterClient.CertManager().Images() if err != nil { return nil, err } diff --git a/cmd/clusterctl/client/move.go b/cmd/clusterctl/client/move.go index 0df6d189db44..0e8ede6bae86 100644 --- a/cmd/clusterctl/client/move.go +++ b/cmd/clusterctl/client/move.go @@ -45,6 +45,11 @@ func (c *clusterctlClient) Move(options MoveOptions) error { return err } + // Ensure this command only runs against management clusters with the current Cluster API contract. + if err := fromCluster.ProviderInventory().CheckCAPIContract(); err != nil { + return err + } + // Ensures the custom resource definitions required by clusterctl are in place. if err := fromCluster.ProviderInventory().EnsureCustomResourceDefinitions(); err != nil { return err @@ -58,6 +63,11 @@ func (c *clusterctlClient) Move(options MoveOptions) error { return err } + // Ensure this command only runs against management clusters with the current Cluster API contract. + if err := toCluster.ProviderInventory().CheckCAPIContract(); err != nil { + return err + } + // Ensures the custom resource definitions required by clusterctl are in place if err := toCluster.ProviderInventory().EnsureCustomResourceDefinitions(); err != nil { return err diff --git a/cmd/clusterctl/client/move_test.go b/cmd/clusterctl/client/move_test.go index f20392191a0a..6ce483d2a843 100644 --- a/cmd/clusterctl/client/move_test.go +++ b/cmd/clusterctl/client/move_test.go @@ -23,6 +23,7 @@ import ( 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" ) func Test_clusterctlClient_Move(t *testing.T) { @@ -104,12 +105,14 @@ func fakeClientForMove() *fakeClient { 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{}) + WithObjectMover(&fakeObjectMover{}). + WithObjs(test.FakeCAPISetupObjects()...) // 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", "") + WithProviderInventory(infra.Name(), infra.Type(), "v2.0.0", "infra-system", ""). + WithObjs(test.FakeCAPISetupObjects()...) client := newFakeClient(config1). WithCluster(cluster1). diff --git a/cmd/clusterctl/client/repository/metadata_client_test.go b/cmd/clusterctl/client/repository/metadata_client_test.go index dca83641d226..c0588c7d8daa 100644 --- a/cmd/clusterctl/client/repository/metadata_client_test.go +++ b/cmd/clusterctl/client/repository/metadata_client_test.go @@ -27,14 +27,6 @@ import ( "sigs.k8s.io/cluster-api/cmd/clusterctl/internal/test" ) -var metadataYaml = []byte("apiVersion: clusterctl.cluster.x-k8s.io/v1alpha3\n" + - "kind: Metadata\n" + - "releaseSeries:\n" + - " - major: 1\n" + - " minor: 2\n" + - " contract: v1alpha3\n" + - "") - func Test_metadataClient_Get(t *testing.T) { type fields struct { provider config.Provider @@ -55,18 +47,22 @@ func Test_metadataClient_Get(t *testing.T) { repository: test.NewFakeRepository(). WithPaths("root", ""). WithDefaultVersion("v1.0.0"). - WithFile("v1.0.0", "metadata.yaml", metadataYaml), + WithMetadata("v1.0.0", &clusterctlv1.Metadata{ + ReleaseSeries: []clusterctlv1.ReleaseSeries{ + {Major: 1, Minor: 2, Contract: test.CurrentCAPIContract}, + }, + }), }, want: &clusterctlv1.Metadata{ TypeMeta: metav1.TypeMeta{ - APIVersion: "clusterctl.cluster.x-k8s.io/v1alpha3", + APIVersion: clusterctlv1.GroupVersion.String(), Kind: "Metadata", }, ReleaseSeries: []clusterctlv1.ReleaseSeries{ { Major: 1, Minor: 2, - Contract: "v1alpha3", + Contract: test.CurrentCAPIContract, }, }, }, @@ -113,7 +109,11 @@ func Test_metadataClient_Get(t *testing.T) { repository: test.NewFakeRepository(). WithPaths("root", ""). WithDefaultVersion("v2.0.0"). - WithFile("v2.0.0", "metadata.yaml", metadataYaml), // metadata file exists for version 2.0.0, while we are checking metadata for v1.0.0 + WithMetadata("v2.0.0", &clusterctlv1.Metadata{ // metadata file exists for version 2.0.0, while we are checking metadata for v1.0.0 + ReleaseSeries: []clusterctlv1.ReleaseSeries{ + {Major: 1, Minor: 2, Contract: test.CurrentCAPIContract}, + }, + }), }, want: nil, wantErr: true, diff --git a/cmd/clusterctl/client/upgrade.go b/cmd/clusterctl/client/upgrade.go index 5d6bcf58f5cd..24907343e51b 100644 --- a/cmd/clusterctl/client/upgrade.go +++ b/cmd/clusterctl/client/upgrade.go @@ -21,6 +21,7 @@ import ( "github.com/pkg/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + clusterv1old "sigs.k8s.io/cluster-api/api/v1alpha3" clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3" "sigs.k8s.io/cluster-api/cmd/clusterctl/client/cluster" ) @@ -44,17 +45,22 @@ func (c *clusterctlClient) PlanCertManagerUpgrade(options PlanUpgradeOptions) (C func (c *clusterctlClient) PlanUpgrade(options PlanUpgradeOptions) ([]UpgradePlan, error) { // Get the client for interacting with the management cluster. - cluster, err := c.clusterClientFactory(ClusterClientFactoryInput{Kubeconfig: options.Kubeconfig}) + clusterClient, err := c.clusterClientFactory(ClusterClientFactoryInput{Kubeconfig: options.Kubeconfig}) if err != nil { return nil, err } + // Ensure this command only runs against management clusters with the current Cluster API contract (default) or the previous one. + if err := clusterClient.ProviderInventory().CheckCAPIContract(cluster.AllowCAPIContract{Contract: clusterv1old.GroupVersion.Version}); err != nil { + return nil, err + } + // Ensures the custom resource definitions required by clusterctl are in place. - if err := cluster.ProviderInventory().EnsureCustomResourceDefinitions(); err != nil { + if err := clusterClient.ProviderInventory().EnsureCustomResourceDefinitions(); err != nil { return nil, err } - upgradePlans, err := cluster.ProviderUpgrader().Plan() + upgradePlans, err := clusterClient.ProviderUpgrader().Plan() if err != nil { return nil, err } @@ -105,6 +111,11 @@ func (c *clusterctlClient) ApplyUpgrade(options ApplyUpgradeOptions) error { return err } + // Ensure this command only runs against management clusters with the current Cluster API contract (default) or the previous one. + if err := clusterClient.ProviderInventory().CheckCAPIContract(cluster.AllowCAPIContract{Contract: clusterv1old.GroupVersion.Version}); err != nil { + return err + } + // Ensures the custom resource definitions required by clusterctl are in place. if err := clusterClient.ProviderInventory().EnsureCustomResourceDefinitions(); err != nil { return err diff --git a/cmd/clusterctl/client/upgrade_test.go b/cmd/clusterctl/client/upgrade_test.go index 396848b23828..d2fded094dc3 100644 --- a/cmd/clusterctl/client/upgrade_test.go +++ b/cmd/clusterctl/client/upgrade_test.go @@ -22,12 +22,12 @@ import ( "testing" . "github.com/onsi/gomega" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3" 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" ) func Test_clusterctlClient_PlanCertUpgrade(t *testing.T) { @@ -342,7 +342,8 @@ func fakeClientForUpgrade() *fakeClient { WithRepository(repository1). WithRepository(repository2). WithProviderInventory(core.Name(), core.Type(), "v1.0.0", "cluster-api-system", "watchingNS"). - WithProviderInventory(infra.Name(), infra.Type(), "v2.0.0", "infra-system", "watchingNS") + WithProviderInventory(infra.Name(), infra.Type(), "v2.0.0", "infra-system", "watchingNS"). + WithObjs(test.FakeCAPISetupObjects()...) client := newFakeClient(config1). WithRepository(repository1). diff --git a/cmd/clusterctl/internal/test/contracts.go b/cmd/clusterctl/internal/test/contracts.go index ff9e794efd44..a9dd7a88e402 100644 --- a/cmd/clusterctl/internal/test/contracts.go +++ b/cmd/clusterctl/internal/test/contracts.go @@ -16,8 +16,10 @@ limitations under the License. package test -import clusterv1old "sigs.k8s.io/cluster-api/api/v1alpha2" -import clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3" +import ( + clusterv1old "sigs.k8s.io/cluster-api/api/v1alpha2" + clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3" +) // PreviousCAPIContractNotSupported define the previous Cluster API contract, not supported by this release of clusterctl. var PreviousCAPIContractNotSupported = clusterv1old.GroupVersion.Version diff --git a/cmd/clusterctl/internal/test/fake_proxy.go b/cmd/clusterctl/internal/test/fake_proxy.go index 1149e0404218..abbf2d15a776 100644 --- a/cmd/clusterctl/internal/test/fake_proxy.go +++ b/cmd/clusterctl/internal/test/fake_proxy.go @@ -17,7 +17,7 @@ limitations under the License. package test import ( - apiextensionslv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + 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" @@ -29,7 +29,7 @@ import ( fakecontrolplane "sigs.k8s.io/cluster-api/cmd/clusterctl/internal/test/providers/controlplane" fakeexternal "sigs.k8s.io/cluster-api/cmd/clusterctl/internal/test/providers/external" fakeinfrastructure "sigs.k8s.io/cluster-api/cmd/clusterctl/internal/test/providers/infrastructure" - addonsv1alpha3 "sigs.k8s.io/cluster-api/exp/addons/api/v1alpha3" + addonsv1 "sigs.k8s.io/cluster-api/exp/addons/api/v1alpha3" expv1 "sigs.k8s.io/cluster-api/exp/api/v1alpha3" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" @@ -50,8 +50,8 @@ func init() { _ = clusterctlv1.AddToScheme(FakeScheme) _ = clusterv1.AddToScheme(FakeScheme) _ = expv1.AddToScheme(FakeScheme) - _ = addonsv1alpha3.AddToScheme(FakeScheme) - _ = apiextensionslv1.AddToScheme(FakeScheme) + _ = addonsv1.AddToScheme(FakeScheme) + _ = apiextensionsv1.AddToScheme(FakeScheme) _ = fakebootstrap.AddToScheme(FakeScheme) _ = fakecontrolplane.AddToScheme(FakeScheme) @@ -166,3 +166,32 @@ func (f *FakeProxy) WithProviderInventory(name string, providerType clusterctlv1 return f } + +// WithFakeCAPISetup adds required objects in order to make kubeadm pass checks +// ensuring that management cluster has a proper release of Cluster API installed. +// NOTE: When using the fake client it is not required to install CRDs, given that type information are +// derived from the schema. However, CheckCAPIContract looks for CRDs to be installed, so this +// helper provide a way to get around to this difference between fake client and a real API server. +func (f *FakeProxy) WithFakeCAPISetup() *FakeProxy { + f.objs = append(f.objs, FakeCAPISetupObjects()...) + + 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() []runtime.Object { + return []runtime.Object{ + &apiextensionsv1.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{Name: "clusters.cluster.x-k8s.io"}, + Spec: apiextensionsv1.CustomResourceDefinitionSpec{ + Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ + { + Name: clusterv1.GroupVersion.Version, // Current Cluster API contract + Storage: true, + }, + }, + }, + }, + } +}