From bdaa4fb8a7a1c057dee3633b283cc4301c929f15 Mon Sep 17 00:00:00 2001 From: Luca Burgazzoli Date: Wed, 3 Aug 2022 15:56:21 +0200 Subject: [PATCH] re-deployment strategies #2256 --- e2e/global/common/traits/deployment_test.go | 98 +++++++++++++++++++++ pkg/apis/camel/v1/trait/deployment.go | 19 ++++ pkg/trait/deployment.go | 34 ++++++- pkg/trait/deployment_test.go | 88 ++++++++++++++++++ 4 files changed, 238 insertions(+), 1 deletion(-) create mode 100644 e2e/global/common/traits/deployment_test.go diff --git a/e2e/global/common/traits/deployment_test.go b/e2e/global/common/traits/deployment_test.go new file mode 100644 index 0000000000..9d9a23c22a --- /dev/null +++ b/e2e/global/common/traits/deployment_test.go @@ -0,0 +1,98 @@ +//go:build integration +// +build integration + +// To enable compilation of this file in Goland, go to "Settings -> Go -> Vendoring & Build Tags -> Custom Tags" and add "integration" + +/* +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You 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 traits + +import ( + appsv1 "k8s.io/api/apps/v1" + "testing" + + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gstruct" + + corev1 "k8s.io/api/core/v1" + + . "github.com/apache/camel-k/e2e/support" + v1 "github.com/apache/camel-k/pkg/apis/camel/v1" +) + +func TestRecreateDeploymentStrategyTrait(t *testing.T) { + WithNewTestNamespace(t, func(ns string) { + operatorID := "camel-k-trait-jolokia" + Expect(KamelInstallWithID(operatorID, ns).Execute()).To(Succeed()) + + t.Run("Run with Recreate Deployment Strategy", func(t *testing.T) { + Expect(KamelRunWithID(operatorID, ns, "files/Java.java", + "-t", "deployment.strategy="+string(appsv1.RecreateDeploymentStrategyType)). + Execute()).To(Succeed()) + + Eventually(IntegrationPodPhase(ns, "java"), TestTimeoutLong).Should(Equal(corev1.PodRunning)) + Eventually(IntegrationConditionStatus(ns, "java", v1.IntegrationConditionReady), TestTimeoutShort).Should(Equal(corev1.ConditionTrue)) + Eventually(IntegrationLogs(ns, "java"), TestTimeoutShort).Should(ContainSubstring("Magicstring!")) + + Eventually(Deployment(ns, "java"), TestTimeoutMedium).Should(PointTo(MatchFields(IgnoreExtras, + Fields{ + "Spec": MatchFields(IgnoreExtras, + Fields{ + "Strategy": MatchFields(IgnoreExtras, + Fields{ + "Type": Equal(appsv1.RecreateDeploymentStrategyType), + }), + }), + }), + )) + + Expect(Kamel("delete", "--all", "-n", ns).Execute()).To(Succeed()) + }) + }) +} + +func TestRollingUpdateDeploymentStrategyTrait(t *testing.T) { + WithNewTestNamespace(t, func(ns string) { + operatorID := "camel-k-trait-jolokia" + Expect(KamelInstallWithID(operatorID, ns).Execute()).To(Succeed()) + + t.Run("Run with RollingUpdate Deployment Strategy", func(t *testing.T) { + Expect(KamelRunWithID(operatorID, ns, "files/Java.java", + "-t", "deployment.strategy="+string(appsv1.RollingUpdateDeploymentStrategyType)). + Execute()).To(Succeed()) + + Eventually(IntegrationPodPhase(ns, "java"), TestTimeoutLong).Should(Equal(corev1.PodRunning)) + Eventually(IntegrationConditionStatus(ns, "java", v1.IntegrationConditionReady), TestTimeoutShort).Should(Equal(corev1.ConditionTrue)) + Eventually(IntegrationLogs(ns, "java"), TestTimeoutShort).Should(ContainSubstring("Magicstring!")) + + Eventually(Deployment(ns, "java"), TestTimeoutMedium).Should(PointTo(MatchFields(IgnoreExtras, + Fields{ + "Spec": MatchFields(IgnoreExtras, + Fields{ + "Strategy": MatchFields(IgnoreExtras, + Fields{ + "Type": Equal(appsv1.RollingUpdateDeploymentStrategyType), + }), + }), + }), + )) + + Expect(Kamel("delete", "--all", "-n", ns).Execute()).To(Succeed()) + }) + }) +} diff --git a/pkg/apis/camel/v1/trait/deployment.go b/pkg/apis/camel/v1/trait/deployment.go index f862c66f92..c670a210f0 100644 --- a/pkg/apis/camel/v1/trait/deployment.go +++ b/pkg/apis/camel/v1/trait/deployment.go @@ -17,6 +17,10 @@ limitations under the License. package trait +import ( + appsv1 "k8s.io/api/apps/v1" +) + // The Deployment trait is responsible for generating the Kubernetes deployment that will make sure // the integration will run in the cluster. // @@ -26,4 +30,19 @@ type DeploymentTrait struct { // The maximum time in seconds for the deployment to make progress before it // is considered to be failed. It defaults to 60s. ProgressDeadlineSeconds *int32 `property:"progress-deadline-seconds" json:"progressDeadlineSeconds,omitempty"` + // The deployment strategy to use to replace existing pods with new ones. + Strategy appsv1.DeploymentStrategyType `property:"strategy" json:"strategy,omitempty"` + // The maximum number of pods that can be unavailable during the update. + // Value can be an absolute number (ex: 5) or a percentage of desired pods (ex: 10%). + // Absolute number is calculated from percentage by rounding down. + // This can not be 0 if MaxSurge is 0. + // Defaults to 25%. + RollingUpdateMaxUnavailable *int `property:"rolling-update-max-unavailable" json:"rollingUpdateMaxUnavailable,omitempty"` + // The maximum number of pods that can be scheduled above the desired number of + // pods. + // Value can be an absolute number (ex: 5) or a percentage of desired pods (ex: 10%). + // This can not be 0 if MaxUnavailable is 0. + // Absolute number is calculated from percentage by rounding up. + // Defaults to 25%. + RollingUpdateMaxSurge *int `property:"rolling-update-max-surge" json:"rollingUpdateMaxSurge,omitempty"` } diff --git a/pkg/trait/deployment.go b/pkg/trait/deployment.go index d7a966d934..35a3eeada3 100644 --- a/pkg/trait/deployment.go +++ b/pkg/trait/deployment.go @@ -20,10 +20,12 @@ package trait import ( "fmt" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/utils/pointer" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/utils/pointer" v1 "github.com/apache/camel-k/pkg/apis/camel/v1" traitv1 "github.com/apache/camel-k/pkg/apis/camel/v1/trait" @@ -165,6 +167,36 @@ func (t *deploymentTrait) getDeploymentFor(e *Environment) *appsv1.Deployment { }, } + switch t.Strategy { + case appsv1.RecreateDeploymentStrategyType: + deployment.Spec.Strategy = appsv1.DeploymentStrategy{ + Type: t.Strategy, + } + case appsv1.RollingUpdateDeploymentStrategyType: + deployment.Spec.Strategy = appsv1.DeploymentStrategy{ + Type: t.Strategy, + } + + if t.RollingUpdateMaxSurge != nil || t.RollingUpdateMaxUnavailable != nil { + var maxSurge *intstr.IntOrString + var maxUnavailable *intstr.IntOrString + + if t.RollingUpdateMaxSurge != nil { + v := intstr.FromInt(*t.RollingUpdateMaxSurge) + maxSurge = &v + } + if t.RollingUpdateMaxUnavailable != nil { + v := intstr.FromInt(*t.RollingUpdateMaxUnavailable) + maxUnavailable = &v + } + + deployment.Spec.Strategy.RollingUpdate = &appsv1.RollingUpdateDeployment{ + MaxSurge: maxSurge, + MaxUnavailable: maxUnavailable, + } + } + } + // Reconcile the deployment replicas replicas := e.Integration.Spec.Replicas // Deployment replicas defaults to 1, so we avoid forcing diff --git a/pkg/trait/deployment_test.go b/pkg/trait/deployment_test.go index 015cbf7dea..d0235e5af5 100644 --- a/pkg/trait/deployment_test.go +++ b/pkg/trait/deployment_test.go @@ -147,6 +147,94 @@ func TestApplyDeploymentTraitWithProgressDeadline(t *testing.T) { assert.Equal(t, int32(120), *deployment.Spec.ProgressDeadlineSeconds) } +func TestApplyDeploymentTraitWitRecresteStrategy(t *testing.T) { + deploymentTrait, environment := createNominalDeploymentTest() + maxSurge := 10 + + deploymentTrait.Strategy = appsv1.RecreateDeploymentStrategyType + deploymentTrait.RollingUpdateMaxSurge = &maxSurge + + environment.Integration.Status.Phase = v1.IntegrationPhaseRunning + + err := deploymentTrait.Apply(environment) + + assert.Nil(t, err) + + deployment := environment.Resources.GetDeployment(func(deployment *appsv1.Deployment) bool { return true }) + assert.NotNil(t, deployment) + assert.Equal(t, "integration-name", deployment.Name) + assert.Equal(t, appsv1.RecreateDeploymentStrategyType, deployment.Spec.Strategy.Type) + assert.Nil(t, deployment.Spec.Strategy.RollingUpdate) +} + +func TestApplyDeploymentTraitWitRollingUpdateStrategy(t *testing.T) { + + t.Run("with defaults", func(t *testing.T) { + deploymentTrait, environment := createNominalDeploymentTest() + + deploymentTrait.Strategy = appsv1.RollingUpdateDeploymentStrategyType + environment.Integration.Status.Phase = v1.IntegrationPhaseRunning + + err := deploymentTrait.Apply(environment) + + assert.Nil(t, err) + + deployment := environment.Resources.GetDeployment(func(deployment *appsv1.Deployment) bool { return true }) + assert.NotNil(t, deployment) + assert.Equal(t, "integration-name", deployment.Name) + assert.Equal(t, appsv1.RollingUpdateDeploymentStrategyType, deployment.Spec.Strategy.Type) + assert.Nil(t, deployment.Spec.Strategy.RollingUpdate) + }) + + t.Run("with surge", func(t *testing.T) { + deploymentTrait, environment := createNominalDeploymentTest() + + maxSurge := 10 + + deploymentTrait.Strategy = appsv1.RollingUpdateDeploymentStrategyType + deploymentTrait.RollingUpdateMaxSurge = &maxSurge + + environment.Integration.Status.Phase = v1.IntegrationPhaseRunning + + err := deploymentTrait.Apply(environment) + + assert.Nil(t, err) + + deployment := environment.Resources.GetDeployment(func(deployment *appsv1.Deployment) bool { return true }) + assert.NotNil(t, deployment) + assert.Equal(t, "integration-name", deployment.Name) + assert.Equal(t, appsv1.RollingUpdateDeploymentStrategyType, deployment.Spec.Strategy.Type) + assert.NotNil(t, deployment.Spec.Strategy.RollingUpdate) + assert.Nil(t, deployment.Spec.Strategy.RollingUpdate.MaxUnavailable) + assert.Equal(t, maxSurge, deployment.Spec.Strategy.RollingUpdate.MaxSurge.IntValue()) + }) + + t.Run("with surge and unavailable", func(t *testing.T) { + deploymentTrait, environment := createNominalDeploymentTest() + + maxSurge := 10 + maxUnavailable := 11 + + deploymentTrait.Strategy = appsv1.RollingUpdateDeploymentStrategyType + deploymentTrait.RollingUpdateMaxSurge = &maxSurge + deploymentTrait.RollingUpdateMaxUnavailable = &maxUnavailable + + environment.Integration.Status.Phase = v1.IntegrationPhaseRunning + + err := deploymentTrait.Apply(environment) + + assert.Nil(t, err) + + deployment := environment.Resources.GetDeployment(func(deployment *appsv1.Deployment) bool { return true }) + assert.NotNil(t, deployment) + assert.Equal(t, "integration-name", deployment.Name) + assert.Equal(t, appsv1.RollingUpdateDeploymentStrategyType, deployment.Spec.Strategy.Type) + assert.NotNil(t, deployment.Spec.Strategy.RollingUpdate) + assert.Equal(t, maxUnavailable, deployment.Spec.Strategy.RollingUpdate.MaxUnavailable.IntValue()) + assert.Equal(t, maxSurge, deployment.Spec.Strategy.RollingUpdate.MaxSurge.IntValue()) + }) +} + func createNominalDeploymentTest() (*deploymentTrait, *Environment) { trait, _ := newDeploymentTrait().(*deploymentTrait) trait.Enabled = pointer.Bool(true)