/* Copyright 2023 The Karmada 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 e2e import ( "context" "fmt" "github.com/onsi/ginkgo/v2" "github.com/onsi/gomega" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/rand" "k8s.io/client-go/kubernetes" "k8s.io/klog/v2" "k8s.io/utils/ptr" policyv1alpha1 "github.com/karmada-io/karmada/pkg/apis/policy/v1alpha1" workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2" pkgutil "github.com/karmada-io/karmada/pkg/util" "github.com/karmada-io/karmada/pkg/util/names" "github.com/karmada-io/karmada/test/e2e/framework" "github.com/karmada-io/karmada/test/helper" ) var _ = ginkgo.Describe("Seamless migration and rollback testing", func() { var member1 string var member1Client kubernetes.Interface ginkgo.BeforeEach(func() { member1 = framework.ClusterNames()[0] member1Client = framework.GetClusterClient(member1) }) // referring to related manual steps: https://github.com/karmada-io/karmada/pull/3821#issuecomment-1649238940 ginkgo.Context("Test migrate namespaced resource: Deployment", func() { var deployment *appsv1.Deployment var propagationPolicy *policyv1alpha1.PropagationPolicy var bindingName, workName, workNamespace string ginkgo.BeforeEach(func() { deployment = helper.NewDeployment(testNamespace, deploymentNamePrefix+rand.String(RandomStrLength)) propagationPolicy = helper.NewPropagationPolicy(deployment.Namespace, deployment.Name, []policyv1alpha1.ResourceSelector{ { APIVersion: deployment.APIVersion, Kind: deployment.Kind, Name: deployment.Name, }, }, policyv1alpha1.Placement{ ClusterAffinity: &policyv1alpha1.ClusterAffinity{ClusterNames: []string{member1}}, }) bindingName = names.GenerateBindingName(deployment.Kind, deployment.Name) workName = names.GenerateWorkName(deployment.Kind, deployment.Name, deployment.Namespace) workNamespace = names.GenerateExecutionSpaceName(member1) }) ginkgo.BeforeEach(func() { // Create Deployment in member1 cluster framework.CreateDeployment(member1Client, deployment) // Create Deployment in karmada control plane framework.CreateDeployment(kubeClient, deployment) // Create PropagationPolicy in karmada control plane without conflictResolution field framework.CreatePropagationPolicy(karmadaClient, propagationPolicy) ginkgo.DeferCleanup(func() { // Delete Deployment in member cluster framework.RemoveDeployment(member1Client, deployment.Namespace, deployment.Name) // Delete PropagationPolicy in karmada control plane framework.RemovePropagationPolicy(karmadaClient, propagationPolicy.Namespace, propagationPolicy.Name) }) }) ginkgo.It("Verify migrate a Deployment from member cluster", func() { // Step 1, Verify ResourceBinding got unHealthy for resource already exist ginkgo.By(fmt.Sprintf("Verify ResourceBinding %s got unApplied for resource already exist", bindingName), func() { klog.Infof("Waiting to verify ResourceBinding %s got unApplied for resource already exist", bindingName) gomega.Eventually(func() bool { binding, err := karmadaClient.WorkV1alpha2().ResourceBindings(deployment.Namespace).Get(context.TODO(), bindingName, metav1.GetOptions{}) if err != nil && apierrors.IsNotFound(err) { return false } gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) items := binding.Status.AggregatedStatus return len(items) > 0 && items[0].Applied == false && items[0].Health != workv1alpha2.ResourceHealthy }, pollTimeout, pollInterval).Should(gomega.Equal(true)) }) // Step 2, Update PropagationPolicy in karmada control plane with conflictResolution=Overwrite and preserveResourcesOnDeletion=true ginkgo.By(fmt.Sprintf("Update PropagationPolicy %s in karmada control plane with conflictResolution=Overwrite", propagationPolicy.Name), func() { propagationPolicy.Spec.ConflictResolution = policyv1alpha1.ConflictOverwrite propagationPolicy.Spec.PreserveResourcesOnDeletion = ptr.To[bool](true) framework.UpdatePropagationPolicyWithSpec(karmadaClient, propagationPolicy.Namespace, propagationPolicy.Name, propagationPolicy.Spec) }) // Step 3, Verify Deployment Replicas all ready and ResourceBinding got Healthy for overwriting conflict resource ginkgo.By(fmt.Sprintf("Verify Deployment Replicas all ready and ResourceBinding %s got Healthy for overwriting conflict resource", bindingName), func() { klog.Infof("Waiting for Deployment ready on Karmada control plane") framework.WaitDeploymentStatus(kubeClient, deployment, *deployment.Spec.Replicas) binding, err := karmadaClient.WorkV1alpha2().ResourceBindings(deployment.Namespace).Get(context.TODO(), bindingName, metav1.GetOptions{}) gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) items := binding.Status.AggregatedStatus gomega.Expect(len(items)).ShouldNot(gomega.Equal(0)) gomega.Expect(items[0].Applied).Should(gomega.BeTrue()) gomega.Expect(items[0].Health).Should(gomega.Equal(workv1alpha2.ResourceHealthy)) }) // Step 4, Delete resource template and check whether member cluster resource is preserved ginkgo.By("Delete resource template and check whether member cluster resource is preserved", func() { // Delete Deployment in karmada control plane framework.RemoveDeployment(kubeClient, deployment.Namespace, deployment.Name) // Wait for work deleted framework.WaitForWorkToDisappear(karmadaClient, workNamespace, workName) // Check member cluster resource is preserved framework.WaitDeploymentPresentOnClusterFitWith(member1, deployment.Namespace, deployment.Name, isResourceNotManagedByKarmada) }) }) }) ginkgo.Context("Test migrate cluster resources: ClusterRole", func() { var clusterRoleName string var clusterRole *rbacv1.ClusterRole var cpp *policyv1alpha1.ClusterPropagationPolicy var bindingName, workName, workNamespace string ginkgo.BeforeEach(func() { clusterRoleName = clusterRoleNamePrefix + rand.String(RandomStrLength) clusterRole = helper.NewClusterRole(clusterRoleName, []rbacv1.PolicyRule{ { APIGroups: []string{"cluster.karmada.io"}, Verbs: []string{"*"}, Resources: []string{"clusters/proxy"}, ResourceNames: []string{member1}, }, }) cpp = helper.NewClusterPropagationPolicy(clusterRole.Name, []policyv1alpha1.ResourceSelector{ { APIVersion: clusterRole.APIVersion, Kind: clusterRole.Kind, Name: clusterRole.Name, }, }, policyv1alpha1.Placement{ ClusterAffinity: &policyv1alpha1.ClusterAffinity{ClusterNames: []string{member1}}, }) cpp.Spec.ConflictResolution = policyv1alpha1.ConflictOverwrite cpp.Spec.PreserveResourcesOnDeletion = ptr.To[bool](true) bindingName = names.GenerateBindingName(clusterRole.Kind, clusterRole.Name) workName = names.GenerateWorkName(clusterRole.Kind, clusterRole.Name, clusterRole.Namespace) workNamespace = names.GenerateExecutionSpaceName(member1) }) ginkgo.BeforeEach(func() { // Create Deployment in member1 cluster framework.CreateClusterRole(member1Client, clusterRole) // Create Deployment in karmada control plane framework.CreateClusterRole(kubeClient, clusterRole) // Create PropagationPolicy in karmada control plane without conflictResolution field framework.CreateClusterPropagationPolicy(karmadaClient, cpp) ginkgo.DeferCleanup(func() { // Delete ClusterRole in member cluster framework.RemoveClusterRole(member1Client, clusterRoleName) // Delete ClusterPropagationPolicy in karmada control plane framework.RemoveClusterPropagationPolicy(karmadaClient, cpp.Name) }) }) ginkgo.It("Verify migrate a ClusterRole from member cluster", func() { ginkgo.By(fmt.Sprintf("Verify ClusterResourceBinding %s got Applied by overwriting conflict resource", bindingName), func() { klog.Infof("Waiting to verify ResourceBinding %s got Applied by overwriting conflict resource", bindingName) gomega.Eventually(func() bool { framework.WaitClusterRolePresentOnClusterFitWith(member1, clusterRoleName, func(*rbacv1.ClusterRole) bool { return true }) _, e1 := kubeClient.RbacV1().ClusterRoles().Get(context.TODO(), clusterRoleName, metav1.GetOptions{}) binding, e2 := karmadaClient.WorkV1alpha2().ClusterResourceBindings().Get(context.TODO(), bindingName, metav1.GetOptions{}) return e1 == nil && e2 == nil && len(binding.Status.AggregatedStatus) > 0 && binding.Status.AggregatedStatus[0].Applied }, pollTimeout, pollInterval).Should(gomega.Equal(true)) }) ginkgo.By("Delete resource template and check whether member cluster resource is preserved", func() { // Delete ClusterRole in karmada control plane framework.RemoveClusterRole(kubeClient, clusterRole.Name) // Wait for work deleted framework.WaitForWorkToDisappear(karmadaClient, workNamespace, workName) // Check member cluster resource is preserved framework.WaitClusterRolePresentOnClusterFitWith(member1, clusterRole.Name, isResourceNotManagedByKarmada) }) }) }) ginkgo.Context("Test migrate namespaced resource: Service (NodePort)", func() { var serviceName string var service *corev1.Service var pp *policyv1alpha1.PropagationPolicy var bindingName, workName, workNamespace string ginkgo.BeforeEach(func() { serviceName = serviceNamePrefix + rand.String(RandomStrLength) service = helper.NewService(testNamespace, serviceName, corev1.ServiceTypeNodePort) pp = helper.NewPropagationPolicy(testNamespace, service.Name, []policyv1alpha1.ResourceSelector{ { APIVersion: service.APIVersion, Kind: service.Kind, Name: service.Name, }, }, policyv1alpha1.Placement{ ClusterAffinity: &policyv1alpha1.ClusterAffinity{ClusterNames: []string{member1}}, }) pp.Spec.ConflictResolution = policyv1alpha1.ConflictOverwrite pp.Spec.PreserveResourcesOnDeletion = ptr.To[bool](true) bindingName = names.GenerateBindingName(service.Kind, service.Name) workName = names.GenerateWorkName(service.Kind, service.Name, service.Namespace) workNamespace = names.GenerateExecutionSpaceName(member1) }) ginkgo.BeforeEach(func() { // Create Deployment in member1 cluster framework.CreateService(member1Client, service) // Create Deployment in karmada control plane framework.CreateService(kubeClient, service) // Create PropagationPolicy in karmada control plane without conflictResolution field framework.CreatePropagationPolicy(karmadaClient, pp) ginkgo.DeferCleanup(func() { // Delete Service in member cluster framework.RemoveService(member1Client, testNamespace, serviceName) // Delete PropagationPolicy in karmada control plane framework.RemovePropagationPolicy(karmadaClient, testNamespace, pp.Name) }) }) ginkgo.It("Verify migrate a Service from member cluster", func() { ginkgo.By(fmt.Sprintf("Verify ResourceBinding %s got Applied by overwriting conflict resource", bindingName), func() { klog.Infof("Waiting to verify ResourceBinding %s got Applied by overwriting conflict resource", bindingName) gomega.Eventually(func() bool { framework.WaitServicePresentOnClusterFitWith(member1, testNamespace, serviceName, func(*corev1.Service) bool { return true }) _, e1 := kubeClient.CoreV1().Services(testNamespace).Get(context.TODO(), serviceName, metav1.GetOptions{}) binding, e2 := karmadaClient.WorkV1alpha2().ResourceBindings(testNamespace).Get(context.TODO(), bindingName, metav1.GetOptions{}) return e1 == nil && e2 == nil && len(binding.Status.AggregatedStatus) > 0 && binding.Status.AggregatedStatus[0].Applied }, pollTimeout, pollInterval).Should(gomega.Equal(true)) }) ginkgo.By("Delete resource template and check whether member cluster resource is preserved", func() { // Delete Service in karmada control plane framework.RemoveService(kubeClient, service.Namespace, service.Name) // Wait for work deleted framework.WaitForWorkToDisappear(karmadaClient, workNamespace, workName) // Check member cluster resource is preserved framework.WaitServicePresentOnClusterFitWith(member1, service.Namespace, service.Name, isResourceNotManagedByKarmada) }) }) }) ginkgo.Context("Test migrate dependent resource", func() { var secret *corev1.Secret var volume []corev1.Volume var deployment *appsv1.Deployment var propagationPolicy *policyv1alpha1.PropagationPolicy var bindingName, workName, workNamespace string ginkgo.BeforeEach(func() { secret = helper.NewSecret(testNamespace, secretNamePrefix+rand.String(RandomStrLength), map[string][]byte{"test": []byte("test")}) volume = []corev1.Volume{{ Name: secret.Name, VolumeSource: corev1.VolumeSource{ Secret: &corev1.SecretVolumeSource{ SecretName: secret.Name, }, }, }} deployment = helper.NewDeploymentWithVolumes(testNamespace, deploymentNamePrefix+rand.String(RandomStrLength), volume) propagationPolicy = helper.NewPropagationPolicy(deployment.Namespace, deployment.Name, []policyv1alpha1.ResourceSelector{ { APIVersion: deployment.APIVersion, Kind: deployment.Kind, Name: deployment.Name, }, }, policyv1alpha1.Placement{ ClusterAffinity: &policyv1alpha1.ClusterAffinity{ClusterNames: []string{member1}}, }) propagationPolicy.Spec.PropagateDeps = true propagationPolicy.Spec.ConflictResolution = policyv1alpha1.ConflictOverwrite propagationPolicy.Spec.PreserveResourcesOnDeletion = ptr.To[bool](true) bindingName = names.GenerateBindingName(secret.Kind, secret.Name) workName = names.GenerateWorkName(secret.Kind, secret.Name, secret.Namespace) workNamespace = names.GenerateExecutionSpaceName(member1) }) ginkgo.BeforeEach(func() { // Create Deployment in member1 cluster framework.CreateDeployment(member1Client, deployment) // Create Secret in member1 cluster framework.CreateSecret(member1Client, secret) // Create Deployment in karmada control plane framework.CreateDeployment(kubeClient, deployment) // Create Secret in karmada control plane framework.CreateSecret(kubeClient, secret) // Create PropagationPolicy in karmada control plane without conflictResolution field framework.CreatePropagationPolicy(karmadaClient, propagationPolicy) ginkgo.DeferCleanup(func() { // Delete Deployment in control plane and member cluster framework.RemoveDeployment(kubeClient, deployment.Namespace, deployment.Name) framework.RemoveDeployment(member1Client, deployment.Namespace, deployment.Name) // Delete Secret in member cluster framework.RemoveSecret(member1Client, secret.Namespace, secret.Name) // Delete PropagationPolicy in karmada control plane framework.RemovePropagationPolicy(karmadaClient, propagationPolicy.Namespace, propagationPolicy.Name) }) }) ginkgo.It("Verify migrate a dependent secret from member cluster", func() { ginkgo.By(fmt.Sprintf("Verify ResourceBinding %s got Applied by overwriting conflict resource", bindingName), func() { klog.Infof("Waiting to verify ResourceBinding %s got Applied by overwriting conflict resource", bindingName) gomega.Eventually(func() bool { framework.WaitSecretPresentOnClusterFitWith(member1, testNamespace, secret.Name, func(*corev1.Secret) bool { return true }) _, e1 := kubeClient.CoreV1().Secrets(testNamespace).Get(context.TODO(), secret.Name, metav1.GetOptions{}) binding, e2 := karmadaClient.WorkV1alpha2().ResourceBindings(testNamespace).Get(context.TODO(), bindingName, metav1.GetOptions{}) return e1 == nil && e2 == nil && len(binding.Status.AggregatedStatus) > 0 && binding.Status.AggregatedStatus[0].Applied }, pollTimeout, pollInterval).Should(gomega.Equal(true)) }) ginkgo.By("Delete dependent secret template and check whether member cluster secret is preserved", func() { // Delete dependent secret in karmada control plane framework.RemoveSecret(kubeClient, secret.Namespace, secret.Name) // Wait for work deleted framework.WaitForWorkToDisappear(karmadaClient, workNamespace, workName) // Check member cluster secret is preserved framework.WaitSecretPresentOnClusterFitWith(member1, secret.Namespace, secret.Name, isResourceNotManagedByKarmada) }) }) }) }) // isResourceNotManagedByKarmada checks if resource is missing all karmada managed labels/annotations // which indicates that it's not managed by Karmada. func isResourceNotManagedByKarmada[T metav1.Object](obj T) bool { for _, key := range pkgutil.ManagedResourceLabels { if _, exist := obj.GetLabels()[key]; exist { return false } } for _, key := range pkgutil.ManagedResourceAnnotations { if _, exist := obj.GetAnnotations()[key]; exist { return false } } return true }