diff --git a/pkg/trait/pod.go b/pkg/trait/pod.go new file mode 100644 index 0000000000..0ab8a5416e --- /dev/null +++ b/pkg/trait/pod.go @@ -0,0 +1,100 @@ +/* +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 trait + +import ( + "fmt" + v1 "github.com/apache/camel-k/pkg/apis/camel/v1" + "k8s.io/api/policy/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +// The PodDisruptionBudget trait is used to configure the pdb +// +// +camel-k:trait=pod +type podTrait struct { + BaseTrait `property:",squash"` + MaxUnavailable string `property:"max-unavailable" json:"maxUnavailable,omitempty"` + MinAvailable string `property:"min-available" json:"minAvailable,omitempty"` +} + +func newPodTrait() Trait { + return &podTrait{ + BaseTrait: NewBaseTrait("pod", 900), + } +} + +func (t *podTrait) Configure(e *Environment) (bool, error) { + if t.Enabled != nil && !*t.Enabled { + return false, nil + } + + return e.IntegrationInPhase( + v1.IntegrationPhaseDeploying, + v1.IntegrationPhaseRunning, + ), nil +} + +func (t *podTrait) Apply(e *Environment) error { + var err error + var pdb *v1beta1.PodDisruptionBudget + + if pdb, err = t.generatePodDisruptionBudget(e.Integration); pdb != nil { + e.Resources.Add(pdb) + } + return err +} + +func (t *podTrait) generatePodDisruptionBudget(integration *v1.Integration) (*v1beta1.PodDisruptionBudget, error) { + + if t.MaxUnavailable == "" && t.MinAvailable == "" { + return nil, nil + } + if t.MaxUnavailable != "" && t.MinAvailable != "" { + return nil, fmt.Errorf("both minAvailable and maxUnavailable can't be set simultaneously") + } + + spec := v1beta1.PodDisruptionBudgetSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + v1.IntegrationLabel: integration.Name, + }, + }, + } + + var min, max intstr.IntOrString + + if t.MaxUnavailable != "" { + max = intstr.Parse(t.MaxUnavailable) + spec.MaxUnavailable = &max + + } else { + min = intstr.Parse(t.MinAvailable) + spec.MinAvailable = &min + } + + return &v1beta1.PodDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: integration.Name, + Namespace: integration.Namespace, + Labels: integration.Labels, + }, + Spec: spec, + }, nil +} diff --git a/pkg/trait/pod_test.go b/pkg/trait/pod_test.go new file mode 100644 index 0000000000..e02b17ab5a --- /dev/null +++ b/pkg/trait/pod_test.go @@ -0,0 +1,121 @@ +/* +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 trait + +import ( + "k8s.io/api/policy/v1beta1" + "testing" + + "github.com/stretchr/testify/assert" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + v1 "github.com/apache/camel-k/pkg/apis/camel/v1" + "github.com/apache/camel-k/pkg/util/kubernetes" +) + +func TestConfigurePodTraitDoesSucceed(t *testing.T) { + podTrait, environment, _ := createPodTest() + configured, err := podTrait.Configure(environment) + + assert.True(t, configured) + assert.Nil(t, err) +} + +func TestPdbIsNotCreated(t *testing.T) { + podTrait, environment, _ := createPodTest() + err := podTrait.Apply(environment) + // no pdb params are set - do nothing + assert.Nil(t, err) + assert.Nil(t, findPdb(environment.Resources)) + + podTrait.MinAvailable = "1" + podTrait.MaxUnavailable = "2" + err = podTrait.Apply(environment) + assert.NotNil(t, err) + assert.Nil(t, findPdb(environment.Resources)) +} + +func TestPdbIsCreatedWithMaxUnavailable(t *testing.T) { + podTrait, environment, _ := createPodTest() + podTrait.MaxUnavailable = "1" + + pdb := refactorPdbIsCreated(podTrait, environment, t) + assert.Equal(t, int32(1), pdb.Spec.MaxUnavailable.IntVal) +} + +func TestPdbIsCreatedWithMinAvailable(t *testing.T) { + podTrait, environment, _ := createPodTest() + podTrait.MinAvailable = "2" + + pdb := refactorPdbIsCreated(podTrait, environment, t) + assert.Equal(t, int32(2), pdb.Spec.MinAvailable.IntVal) +} + +func refactorPdbIsCreated(podTrait *podTrait, environment *Environment, t *testing.T) *v1beta1.PodDisruptionBudget { + err := podTrait.Apply(environment) + assert.Nil(t, err) + pdb := findPdb(environment.Resources) + + assert.NotNil(t, pdb) + assert.Equal(t, environment.Integration.Name, pdb.Name) + assert.Equal(t, environment.Integration.Namespace, pdb.Namespace) + assert.Equal(t, environment.Integration.Labels, pdb.Labels) + return pdb +} + +func findPdb(resources *kubernetes.Collection) *v1beta1.PodDisruptionBudget { + for _, a := range resources.Items() { + // v1beta1 + if _, ok := a.(*v1beta1.PodDisruptionBudget); ok { + return a.(*v1beta1.PodDisruptionBudget) + } + } + return nil +} + +func createPodTest() (*podTrait, *Environment, *appsv1.Deployment) { + trait := newPodTrait().(*podTrait) + enabled := true + trait.Enabled = &enabled + + deployment := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "integration-name", + }, + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{}, + }, + } + + environment := &Environment{ + Integration: &v1.Integration{ + ObjectMeta: metav1.ObjectMeta{ + Name: "integration-name", + }, + Status: v1.IntegrationStatus{ + Phase: v1.IntegrationPhaseDeploying, + }, + }, + Resources: kubernetes.NewCollection(deployment), + } + + return trait, environment, deployment +} diff --git a/pkg/trait/trait_register.go b/pkg/trait/trait_register.go index 30c4356e5e..6de8cfac9f 100644 --- a/pkg/trait/trait_register.go +++ b/pkg/trait/trait_register.go @@ -46,4 +46,5 @@ func init() { AddToTraits(newIstioTrait) AddToTraits(newIngressTrait) AddToTraits(newOwnerTrait) + AddToTraits(newPodTrait) }