diff --git a/cmd/clusterctl/pkg/client/cluster/client.go b/cmd/clusterctl/pkg/client/cluster/client.go index 2ea1a5369010..27c4b6b72e0b 100644 --- a/cmd/clusterctl/pkg/client/cluster/client.go +++ b/cmd/clusterctl/pkg/client/cluster/client.go @@ -113,7 +113,7 @@ func (c *clusterClient) ProviderInstaller() ProviderInstaller { } func (c *clusterClient) ObjectMover() ObjectMover { - return newObjectMover(c.proxy) + return newObjectMover(c.proxy, c.ProviderInventory()) } func (c *clusterClient) ProviderUpgrader() ProviderUpgrader { diff --git a/cmd/clusterctl/pkg/client/cluster/mover.go b/cmd/clusterctl/pkg/client/cluster/mover.go index cdea98240e7b..0234f26e00be 100644 --- a/cmd/clusterctl/pkg/client/cluster/mover.go +++ b/cmd/clusterctl/pkg/client/cluster/mover.go @@ -28,6 +28,7 @@ import ( "k8s.io/apimachinery/pkg/types" kerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apimachinery/pkg/util/version" "k8s.io/apimachinery/pkg/util/wait" clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3" logf "sigs.k8s.io/cluster-api/cmd/clusterctl/pkg/log" @@ -42,7 +43,8 @@ type ObjectMover interface { // objectMover implements the ObjectMover interface. type objectMover struct { - fromProxy Proxy + fromProxy Proxy + fromProviderInventory InventoryClient } // ensure objectMover implements the ObjectMover interface. @@ -54,7 +56,10 @@ func (o *objectMover) Move(namespace string, toCluster Client) error { objectGraph := newObjectGraph(o.fromProxy) - //TODO: implement preflight checks ensuring the target cluster has all the required providers in place + // checks that all the required providers in place in the target cluster. + if err := o.checkTargetProviders(namespace, toCluster.ProviderInventory()); err != nil { + return err + } // Gets all the types defines by the CRDs installed by clusterctl plus the ConfigMap/Secret core types. types, err := objectGraph.getDiscoveryTypes() @@ -86,9 +91,10 @@ func (o *objectMover) Move(namespace string, toCluster Client) error { return nil } -func newObjectMover(fromProxy Proxy) *objectMover { +func newObjectMover(fromProxy Proxy, fromProviderInventory InventoryClient) *objectMover { return &objectMover{ - fromProxy: fromProxy, + fromProxy: fromProxy, + fromProviderInventory: fromProviderInventory, } } @@ -594,3 +600,75 @@ func retry(attempts int, interval time.Duration, action func() error) error { } return errors.Wrapf(errorToReturn, "action failed after %d attempts", attempts) } + +// checkTargetProviders checks that all the providers installed in the source cluster exists in the target cluster as well (with a version >= of the current version). +func (o *objectMover) checkTargetProviders(namespace string, toInventory InventoryClient) error { + // Gets the list of providers in the source/target cluster. + fromProviders, err := o.fromProviderInventory.List() + if err != nil { + return errors.Wrapf(err, "failed to get provider list from the source cluster") + } + + toProviders, err := toInventory.List() + if err != nil { + return errors.Wrapf(err, "failed to get provider list from the target cluster") + } + + // Checks all the providers installed in the source cluster + errList := []error{} + for _, sourceProvider := range fromProviders.Items { + // If we are moving objects in a namespace only, skip all the providers not watching such namespace. + if namespace != "" && !(sourceProvider.WatchedNamespace == "" || sourceProvider.WatchedNamespace == namespace) { + continue + } + + sourceVersion, err := version.ParseSemantic(sourceProvider.Version) + if err != nil { + return errors.Wrapf(err, "unable to parse version %q for the %s provider in the source cluster", sourceProvider.Version, sourceProvider.InstanceName()) + } + + // Check corresponding providers in the target cluster and gets the latest version installed. + var maxTargetVersion *version.Version + for _, targetProvider := range toProviders.Items { + // Skips other providers. + if sourceProvider.Name != targetProvider.Name { + continue + } + + // If we are moving objects in all the namespaces, skip all the providers with a different watching namespace. + // NB. This introduces a constraints for move all namespaces, that the configuration of source and target provider MUST match (except for the version); + // however this is acceptable because clusterctl supports only two models of multi-tenancy (n-Infra, n-Core). + if namespace == "" && !(targetProvider.WatchedNamespace == sourceProvider.WatchedNamespace) { + continue + } + + // If we are moving objects in a namespace only, skip all the providers not watching such namespace. + // NB. This means that when moving a single namespace, we use a lazy matching (the watching namespace MUST overlap; exact match is not required). + if namespace != "" && !(targetProvider.WatchedNamespace == "" || targetProvider.WatchedNamespace == namespace) { + continue + } + + targetVersion, err := version.ParseSemantic(targetProvider.Version) + if err != nil { + return errors.Wrapf(err, "unable to parse version %q for the %s provider in the target cluster", targetProvider.Version, targetProvider.InstanceName()) + } + if maxTargetVersion == nil || maxTargetVersion.LessThan(targetVersion) { + maxTargetVersion = targetVersion + } + } + if maxTargetVersion == nil { + watching := sourceProvider.WatchedNamespace + if namespace != "" { + watching = namespace + } + errList = append(errList, errors.Errorf("provider %s watching namespace %s not found in the target cluster", sourceProvider.Name, watching)) + continue + } + + if !maxTargetVersion.AtLeast(sourceVersion) { + errList = append(errList, errors.Errorf("provider %s in the target cluster is older than in the source cluster (source: %s, target: %s)", sourceProvider.Name, sourceVersion.String(), maxTargetVersion.String())) + } + } + + return kerrors.NewAggregate(errList) +} diff --git a/cmd/clusterctl/pkg/client/cluster/mover_test.go b/cmd/clusterctl/pkg/client/cluster/mover_test.go index 8795f40495cf..a3bf0b7b61c4 100644 --- a/cmd/clusterctl/pkg/client/cluster/mover_test.go +++ b/cmd/clusterctl/pkg/client/cluster/mover_test.go @@ -27,6 +27,7 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" 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/pkg/internal/test" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -670,3 +671,128 @@ func Test_objectMover_checkProvisioningCompleted(t *testing.T) { }) } } + +func Test_objectsMoverService_checkTargetProviders(t *testing.T) { + type fields struct { + fromProxy Proxy + } + type args struct { + toProxy Proxy + namespace string + } + tests := []struct { + name string + fields fields + args args + wantErr bool + }{ + { + name: "move objects in single namespace, all the providers in place (lazy matching)", + fields: fields{ + fromProxy: test.NewFakeProxy(). + WithProviderInventory("capi", clusterctlv1.CoreProviderType, "v1.0.0", "capi-system", ""). + WithProviderInventory("kubeadm", clusterctlv1.BootstrapProviderType, "v1.0.0", "cabpk-system", ""). + WithProviderInventory("capa", clusterctlv1.InfrastructureProviderType, "v1.0.0", "capa-system", ""), + }, + args: args{ + namespace: "ns1", // a single namespaces + toProxy: test.NewFakeProxy(). + WithProviderInventory("capi", clusterctlv1.CoreProviderType, "v1.0.0", "capi-system", "ns1"). + WithProviderInventory("kubeadm", clusterctlv1.BootstrapProviderType, "v1.0.0", "cabpk-system", "ns1"). + WithProviderInventory("capa", clusterctlv1.InfrastructureProviderType, "v1.0.0", "capa-system", "ns1"), + }, + wantErr: false, + }, + { + name: "move objects in single namespace, all the providers in place but with a newer version (lazy matching)", + fields: fields{ + fromProxy: test.NewFakeProxy(). + WithProviderInventory("capi", clusterctlv1.CoreProviderType, "v2.0.0", "capi-system", ""), + }, + args: args{ + namespace: "ns1", // a single namespaces + toProxy: test.NewFakeProxy(). + WithProviderInventory("capi", clusterctlv1.CoreProviderType, "v2.1.0", "capi-system", "ns1"), // Lazy matching + }, + wantErr: false, + }, + { + name: "move objects in all namespaces, all the providers in place (exact matching)", + fields: fields{ + fromProxy: test.NewFakeProxy(). + WithProviderInventory("capi", clusterctlv1.CoreProviderType, "v1.0.0", "capi-system", ""). + WithProviderInventory("kubeadm", clusterctlv1.BootstrapProviderType, "v1.0.0", "cabpk-system", ""). + WithProviderInventory("capa", clusterctlv1.InfrastructureProviderType, "v1.0.0", "capa-system", ""), + }, + args: args{ + namespace: "", // all namespaces + toProxy: test.NewFakeProxy(). + WithProviderInventory("capi", clusterctlv1.CoreProviderType, "v1.0.0", "capi-system", ""). + WithProviderInventory("kubeadm", clusterctlv1.BootstrapProviderType, "v1.0.0", "cabpk-system", ""). + WithProviderInventory("capa", clusterctlv1.InfrastructureProviderType, "v1.0.0", "capa-system", ""), + }, + wantErr: false, + }, + { + name: "move objects in all namespaces, all the providers in place but with a newer version (exact matching)", + fields: fields{ + fromProxy: test.NewFakeProxy(). + WithProviderInventory("capi", clusterctlv1.CoreProviderType, "v2.0.0", "capi-system", ""), + }, + args: args{ + namespace: "", // all namespaces + toProxy: test.NewFakeProxy(). + WithProviderInventory("capi", clusterctlv1.CoreProviderType, "v2.1.0", "capi-system", ""), + }, + wantErr: false, + }, + { + name: "move objects in all namespaces, not exact matching", + fields: fields{ + fromProxy: test.NewFakeProxy(). + WithProviderInventory("capi", clusterctlv1.CoreProviderType, "v2.0.0", "capi-system", ""), + }, + args: args{ + namespace: "", // all namespaces + toProxy: test.NewFakeProxy(). + WithProviderInventory("capi", clusterctlv1.CoreProviderType, "v2.1.0", "capi-system", "ns1"), // Lazy matching only + }, + wantErr: true, + }, + { + name: "fails if a provider is missing", + fields: fields{ + fromProxy: test.NewFakeProxy(). + WithProviderInventory("capi", clusterctlv1.CoreProviderType, "v2.0.0", "capi-system", ""), + }, + args: args{ + namespace: "", // all namespaces + toProxy: test.NewFakeProxy(), + }, + wantErr: true, + }, + { + name: "fails if a provider version is older than expected", + fields: fields{ + fromProxy: test.NewFakeProxy(). + WithProviderInventory("capi", clusterctlv1.CoreProviderType, "v2.0.0", "capi-system", ""), + }, + args: args{ + namespace: "", // all namespaces + toProxy: test.NewFakeProxy(). + WithProviderInventory("capi", clusterctlv1.CoreProviderType, "v1.0.0", "capi1-system", ""), + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + o := &objectMover{ + fromProviderInventory: newInventoryClient(tt.fields.fromProxy, nil), + } + if err := o.checkTargetProviders(tt.args.namespace, newInventoryClient(tt.args.toProxy, nil)); (err != nil) != tt.wantErr { + t.Errorf("error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +}