diff --git a/controllers/operands/kubevirtConsolePlugin.go b/controllers/operands/kubevirtConsolePlugin.go index 123ec7336..2da605d45 100644 --- a/controllers/operands/kubevirtConsolePlugin.go +++ b/controllers/operands/kubevirtConsolePlugin.go @@ -105,6 +105,15 @@ func NewKvUIProxyDeployment(hc *hcov1beta1.HyperConverged) *appsv1.Deployment { func getKvUIDeployment(hc *hcov1beta1.HyperConverged, deploymentName string, image string, servingCertName string, servingCertPath string, port int32, componentName hcoutil.AppComponent) *appsv1.Deployment { labels := getLabels(hc, componentName) + infrastructureHighlyAvailable := hcoutil.GetClusterInfo().IsInfrastructureHighlyAvailable() + var replicas int32 + if infrastructureHighlyAvailable { + replicas = int32(2) + } else { + replicas = int32(1) + } + + affinity := getPodAntiAffinity(labels[hcoutil.AppLabelComponent], infrastructureHighlyAvailable) deployment := &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ @@ -113,7 +122,7 @@ func getKvUIDeployment(hc *hcov1beta1.HyperConverged, deploymentName string, ima Namespace: hc.Namespace, }, Spec: appsv1.DeploymentSpec{ - Replicas: ptr.To(int32(1)), + Replicas: ptr.To(replicas), Selector: &metav1.LabelSelector{ MatchLabels: labels, }, @@ -181,7 +190,7 @@ func getKvUIDeployment(hc *hcov1beta1.HyperConverged, deploymentName string, ima if hc.Spec.Infra.NodePlacement.Affinity != nil { deployment.Spec.Template.Spec.Affinity = hc.Spec.Infra.NodePlacement.Affinity.DeepCopy() } else { - deployment.Spec.Template.Spec.Affinity = nil + deployment.Spec.Template.Spec.Affinity = affinity } if hc.Spec.Infra.NodePlacement.Tolerations != nil { @@ -192,12 +201,37 @@ func getKvUIDeployment(hc *hcov1beta1.HyperConverged, deploymentName string, ima } } else { deployment.Spec.Template.Spec.NodeSelector = nil - deployment.Spec.Template.Spec.Affinity = nil + deployment.Spec.Template.Spec.Affinity = affinity deployment.Spec.Template.Spec.Tolerations = nil } return deployment } +func getPodAntiAffinity(componentLabel string, infrastructureHighlyAvailable bool) *corev1.Affinity { + if infrastructureHighlyAvailable { + return &corev1.Affinity{ + PodAntiAffinity: &corev1.PodAntiAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: []corev1.PodAffinityTerm{ + { + LabelSelector: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: hcoutil.AppLabelComponent, + Operator: metav1.LabelSelectorOpIn, + Values: []string{componentLabel}, + }, + }, + }, + TopologyKey: corev1.LabelHostname, + }, + }, + }, + } + } + + return nil +} + func NewKvUIPluginSvc(hc *hcov1beta1.HyperConverged) *corev1.Service { servicePorts := []corev1.ServicePort{ { diff --git a/controllers/operands/kubevirtConsolePlugin_test.go b/controllers/operands/kubevirtConsolePlugin_test.go index e7512bfbc..b0407fd1a 100644 --- a/controllers/operands/kubevirtConsolePlugin_test.go +++ b/controllers/operands/kubevirtConsolePlugin_test.go @@ -686,6 +686,94 @@ var _ = Describe("Kubevirt Console Plugin", func() { Entry("plugin deployment", hcoutil.AppComponentUIPlugin, NewKvUIPluginDeployment, newKvUIPluginDeploymentHandler), Entry("proxy deployment", hcoutil.AppComponentUIProxy, NewKvUIProxyDeployment, newKvUIProxyDeploymentHandler), ) + + DescribeTable("apply PodAntiAffinity and two replicas if HighlyAvailable", func(ctx context.Context, appComponent hcoutil.AppComponent, + deploymentManifestor func(converged *hcov1beta1.HyperConverged) *appsv1.Deployment, handlerFunc GetHandler) { + + originalGetClusterInfo := hcoutil.GetClusterInfo + hcoutil.GetClusterInfo = func() hcoutil.ClusterInfo { + return &commontestutils.ClusterInfoMock{} + } + + defer func() { + hcoutil.GetClusterInfo = originalGetClusterInfo + }() + + existingResource := deploymentManifestor(hco) + + hco.Spec.Infra.NodePlacement = nil + existingResource.Spec.Template.Spec.Affinity = nil + existingResource.Spec.Replicas = ptr.To(int32(1)) + + cl := commontestutils.InitClient([]client.Object{hco, existingResource}) + handlers, err := handlerFunc(logger, cl, commontestutils.GetScheme(), hco) + + Expect(err).ToNot(HaveOccurred()) + res := handlers[0].ensure(req) + Expect(res.Created).To(BeFalse()) + Expect(res.Updated).To(BeTrue()) + Expect(res.Overwritten).To(BeFalse()) + Expect(res.UpgradeDone).To(BeFalse()) + Expect(res.Err).ToNot(HaveOccurred()) + + foundResource := &appsv1.Deployment{} + Expect( + cl.Get(ctx, + types.NamespacedName{Name: existingResource.Name, Namespace: existingResource.Namespace}, + foundResource), + ).To(Succeed()) + + Expect(existingResource.Spec.Template.Spec.Affinity).To(BeNil()) + Expect(*existingResource.Spec.Replicas).To(Equal(int32(1))) + + expectedAffinity := expectedPodAntiAffinity(appComponent) + Expect(foundResource.Spec.Template.Spec.Affinity).To(BeEquivalentTo(expectedAffinity)) + Expect(*foundResource.Spec.Replicas).To(Equal(int32(2))) + }, + Entry("plugin deployment", hcoutil.AppComponentUIPlugin, NewKvUIPluginDeployment, newKvUIPluginDeploymentHandler), + Entry("proxy deployment", hcoutil.AppComponentUIProxy, NewKvUIProxyDeployment, newKvUIProxyDeploymentHandler), + ) + + DescribeTable("use one replica on SNO", func(ctx context.Context, appComponent hcoutil.AppComponent, + deploymentManifestor func(converged *hcov1beta1.HyperConverged) *appsv1.Deployment, handlerFunc GetHandler) { + + originalGetClusterInfo := hcoutil.GetClusterInfo + hcoutil.GetClusterInfo = func() hcoutil.ClusterInfo { + return &commontestutils.ClusterInfoSNOMock{} + } + + defer func() { + hcoutil.GetClusterInfo = originalGetClusterInfo + }() + + existingResource := deploymentManifestor(hco) + existingResource.Spec.Replicas = ptr.To(int32(3)) + + cl := commontestutils.InitClient([]client.Object{hco, existingResource}) + handlers, err := handlerFunc(logger, cl, commontestutils.GetScheme(), hco) + + Expect(err).ToNot(HaveOccurred()) + res := handlers[0].ensure(req) + Expect(res.Created).To(BeFalse()) + Expect(res.Updated).To(BeTrue()) + Expect(res.Overwritten).To(BeFalse()) + Expect(res.UpgradeDone).To(BeFalse()) + Expect(res.Err).ToNot(HaveOccurred()) + + foundResource := &appsv1.Deployment{} + Expect( + cl.Get(ctx, + types.NamespacedName{Name: existingResource.Name, Namespace: existingResource.Namespace}, + foundResource), + ).To(Succeed()) + + Expect(existingResource.Spec.Template.Spec.Affinity).To(BeNil()) + Expect(foundResource.Spec.Template.Spec.Affinity).To(BeNil()) + Expect(*foundResource.Spec.Replicas).To(Equal(int32(1))) + }, + Entry("plugin deployment", hcoutil.AppComponentUIPlugin, NewKvUIPluginDeployment, newKvUIPluginDeploymentHandler), + Entry("proxy deployment", hcoutil.AppComponentUIProxy, NewKvUIProxyDeployment, newKvUIProxyDeploymentHandler), + ) }) }) @@ -797,3 +885,24 @@ var _ = Describe("Kubevirt Console Plugin", func() { }) }) + +func expectedPodAntiAffinity(appComponent hcoutil.AppComponent) *v1.Affinity { + return &v1.Affinity{ + PodAntiAffinity: &v1.PodAntiAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: []v1.PodAffinityTerm{ + { + LabelSelector: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: hcoutil.AppLabelComponent, + Operator: metav1.LabelSelectorOpIn, + Values: []string{string(appComponent)}, + }, + }, + }, + TopologyKey: v1.LabelHostname, + }, + }, + }, + } +} diff --git a/tests/func-tests/console_plugin_test.go b/tests/func-tests/console_plugin_test.go index 1273b679e..36ea2762a 100644 --- a/tests/func-tests/console_plugin_test.go +++ b/tests/func-tests/console_plugin_test.go @@ -152,6 +152,50 @@ var _ = Describe("kubevirt console plugin", Label(tests.OpenshiftLabel), func() WithPolling(100 * time.Millisecond). Should(Succeed()) }) + + It("console-plugin and apiserver-proxy Deployments should have 2 replicas in Highly Available clusters", Label(tests.HighlyAvailableClusterLabel), func(ctx context.Context) { + Eventually(func(g Gomega, ctx context.Context) { + consoleUIDeployment, err := cli.AppsV1().Deployments(flags.KubeVirtInstallNamespace).Get(ctx, string(hcoutil.AppComponentUIPlugin), metav1.GetOptions{}) + g.Expect(err).ToNot(HaveOccurred()) + + g.Expect(consoleUIDeployment.Spec.Replicas).To(HaveValue(Equal(int32(2)))) + }).WithTimeout(1 * time.Minute). + WithPolling(100 * time.Millisecond). + WithContext(ctx). + Should(Succeed()) + + Eventually(func(g Gomega, ctx context.Context) { + proxyUIDeployment, err := cli.AppsV1().Deployments(flags.KubeVirtInstallNamespace).Get(ctx, string(hcoutil.AppComponentUIProxy), metav1.GetOptions{}) + g.Expect(err).ToNot(HaveOccurred()) + + g.Expect(proxyUIDeployment.Spec.Replicas).To(HaveValue(Equal(int32(2)))) + }).WithTimeout(1 * time.Minute). + WithPolling(100 * time.Millisecond). + WithContext(ctx). + Should(Succeed()) + }) + + It("console-plugin and apiserver-proxy Deployments should have 1 replica in single node clusters", Label(tests.SingleNodeLabel), func(ctx context.Context) { + Eventually(func(g Gomega, ctx context.Context) { + consoleUIDeployment, err := cli.AppsV1().Deployments(flags.KubeVirtInstallNamespace).Get(ctx, string(hcoutil.AppComponentUIPlugin), metav1.GetOptions{}) + g.Expect(err).ToNot(HaveOccurred()) + + g.Expect(consoleUIDeployment.Spec.Replicas).To(HaveValue(Equal(int32(1)))) + }).WithTimeout(1 * time.Minute). + WithPolling(100 * time.Millisecond). + WithContext(ctx). + Should(Succeed()) + + Eventually(func(g Gomega, ctx context.Context) { + proxyUIDeployment, err := cli.AppsV1().Deployments(flags.KubeVirtInstallNamespace).Get(ctx, string(hcoutil.AppComponentUIProxy), metav1.GetOptions{}) + g.Expect(err).ToNot(HaveOccurred()) + + g.Expect(proxyUIDeployment.Spec.Replicas).To(HaveValue(Equal(int32(1)))) + }).WithTimeout(1 * time.Minute). + WithPolling(100 * time.Millisecond). + WithContext(ctx). + Should(Succeed()) + }) }) func executeCommandOnPod(ctx context.Context, cli kubecli.KubevirtClient, pod *v1.Pod, command string) (string, string, error) {