You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by as...@apache.org on 2022/08/04 07:41:02 UTC

[camel-k] 01/02: re-deployment strategies #2256

This is an automated email from the ASF dual-hosted git repository.

astefanutti pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-k.git

commit 7eee5bc10e060d8776727ee4575d24461dc4c879
Author: Luca Burgazzoli <lb...@gmail.com>
AuthorDate: Wed Aug 3 15:56:21 2022 +0200

    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(-)

diff --git a/e2e/global/common/traits/deployment_test.go b/e2e/global/common/traits/deployment_test.go
new file mode 100644
index 000000000..9d9a23c22
--- /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 f862c66f9..c670a210f 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 d7a966d93..35a3eeada 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 015cbf7de..d0235e5af 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)