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..3e7b614c4 100644 --- a/tests/func-tests/console_plugin_test.go +++ b/tests/func-tests/console_plugin_test.go @@ -23,6 +23,7 @@ import ( hcoutil "github.com/kubevirt/hyperconverged-cluster-operator/pkg/util" tests "github.com/kubevirt/hyperconverged-cluster-operator/tests/func-tests" + "github.com/kubevirt/hyperconverged-cluster-operator/tests/vendor/sigs.k8s.io/controller-runtime/pkg/client" ) const ( @@ -152,6 +153,70 @@ 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 := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: string(hcoutil.AppComponentUIPlugin), + Namespace: tests.InstallNamespace, + }, + } + + g.Expect(cli.Get(ctx, client.ObjectKeyFromObject(consoleUIDeployment), consoleUIDeployment)).To(Succeed()) + + 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 := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: string(hcoutil.AppComponentUIProxy), + Namespace: tests.InstallNamespace, + }, + } + g.Expect(cli.Get(ctx, client.ObjectKeyFromObject(proxyUIDeployment), proxyUIDeployment)).To(Succeed()) + 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 := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: string(hcoutil.AppComponentUIPlugin), + Namespace: tests.InstallNamespace, + }, + } + + g.Expect(cli.Get(ctx, client.ObjectKeyFromObject(consoleUIDeployment), consoleUIDeployment)).To(Succeed()) + + 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 := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: string(hcoutil.AppComponentUIProxy), + Namespace: tests.InstallNamespace, + }, + } + g.Expect(cli.Get(ctx, client.ObjectKeyFromObject(proxyUIDeployment), proxyUIDeployment)).To(Succeed()) + 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) { diff --git a/tests/go.mod b/tests/go.mod index 7ddd6018a..638a74193 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -35,6 +35,7 @@ replace ( ) require ( + github.com/evanphx/json-patch/v5 v5.9.0 github.com/gertd/go-pluralize v0.2.1 github.com/kubevirt/cluster-network-addons-operator v0.93.0 github.com/kubevirt/hyperconverged-cluster-operator v0.0.0-00010101000000-000000000000 @@ -44,6 +45,7 @@ require ( kubevirt.io/application-aware-quota v1.2.3 kubevirt.io/controller-lifecycle-operator-sdk/api v0.2.4 kubevirt.io/ssp-operator/api v0.20.0 + sigs.k8s.io/controller-runtime v0.17.3 ) require ( @@ -53,7 +55,6 @@ require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a // indirect github.com/emicklei/go-restful/v3 v3.12.0 // indirect - github.com/evanphx/json-patch/v5 v5.9.0 // indirect github.com/fatih/color v1.16.0 // indirect github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 // indirect github.com/go-kit/kit v0.13.0 // indirect @@ -120,7 +121,6 @@ require ( k8s.io/kube-aggregator v0.27.4 // indirect k8s.io/kube-openapi v0.0.0-20240403164606-bc84c2ddaf99 // indirect k8s.io/kubectl v0.29.3 // indirect - sigs.k8s.io/controller-runtime v0.17.3 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect sigs.k8s.io/yaml v1.4.0 // indirect