This is an automated email from the ASF dual-hosted git repository. pcongiusti pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel-k.git
commit 0cad7974633a54cef58e7f0e0cf9d000d6787d1c Author: Pasquale Congiusti <[email protected]> AuthorDate: Wed Jun 15 09:57:47 2022 +0200 feat(e2e): promote integration test --- e2e/common/cli/files/promote-route.groovy | 25 +++++++ e2e/common/cli/promote_test.go | 112 ++++++++++++++++++++++++++++ pkg/cmd/promote.go | 119 ++++++++++++++++++++---------- pkg/cmd/promote_test.go | 53 +++++++------ 4 files changed, 249 insertions(+), 60 deletions(-) diff --git a/e2e/common/cli/files/promote-route.groovy b/e2e/common/cli/files/promote-route.groovy new file mode 100644 index 000000000..943a4ab91 --- /dev/null +++ b/e2e/common/cli/files/promote-route.groovy @@ -0,0 +1,25 @@ +// camel-k: language=groovy +/* + * 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. + */ + +from('timer:configmap') + .setBody() + .simple("resource:classpath:my-configmap-key") + .log('configmap: ${body}') + .setBody() + .simple("resource:classpath:my-secret-key") + .log('secret: ${body}') \ No newline at end of file diff --git a/e2e/common/cli/promote_test.go b/e2e/common/cli/promote_test.go new file mode 100644 index 000000000..3f7840165 --- /dev/null +++ b/e2e/common/cli/promote_test.go @@ -0,0 +1,112 @@ +//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 common + +import ( + "testing" + + corev1 "k8s.io/api/core/v1" + + . "github.com/onsi/gomega" + + . "github.com/apache/camel-k/e2e/support" + v1 "github.com/apache/camel-k/pkg/apis/camel/v1" +) + +func TestKamelCLIPromote(t *testing.T) { + // Dev environment namespace + WithNewTestNamespace(t, func(nsDev string) { + Expect(Kamel("install", "-n", nsDev).Execute()).To(Succeed()) + // Dev content configmap + var cmData = make(map[string]string) + cmData["my-configmap-key"] = "I am development configmap!" + NewPlainTextConfigmap(nsDev, "my-cm", cmData) + // Dev secret + var secData = make(map[string]string) + secData["my-secret-key"] = "very top secret development" + NewPlainTextSecret(nsDev, "my-sec", secData) + + t.Run("plain integration", func(t *testing.T) { + Expect(Kamel("run", "-n", nsDev, "./files/promote-route.groovy", + "--config", "configmap:my-cm", + "--config", "secret:my-sec", + ).Execute()).To(Succeed()) + Eventually(IntegrationPodPhase(nsDev, "promote-route"), TestTimeoutMedium).Should(Equal(corev1.PodRunning)) + Eventually(IntegrationConditionStatus(nsDev, "promote-route", v1.IntegrationConditionReady), TestTimeoutShort).Should(Equal(corev1.ConditionTrue)) + Eventually(IntegrationLogs(nsDev, "promote-route"), TestTimeoutShort).Should(ContainSubstring("I am development configmap!")) + Eventually(IntegrationLogs(nsDev, "promote-route"), TestTimeoutShort).Should(ContainSubstring("very top secret development")) + }) + + t.Run("kamelet integration", func(t *testing.T) { + Expect(CreateTimerKamelet(nsDev, "my-own-timer-source")()).To(Succeed()) + Expect(Kamel("run", "-n", nsDev, "files/timer-kamelet-usage.groovy").Execute()).To(Succeed()) + Eventually(IntegrationPodPhase(nsDev, "timer-kamelet-usage"), TestTimeoutMedium).Should(Equal(corev1.PodRunning)) + Eventually(IntegrationLogs(nsDev, "timer-kamelet-usage"), TestTimeoutShort).Should(ContainSubstring("Hello world")) + }) + + // Prod environment namespace + WithNewTestNamespace(t, func(nsProd string) { + Expect(Kamel("install", "-n", nsProd).Execute()).To(Succeed()) + + t.Run("no configmap in destination", func(t *testing.T) { + Expect(Kamel("promote", "-n", nsDev, "promote-route", "--to", nsProd).Execute()).NotTo(Succeed()) + }) + // Prod content configmap + var cmData = make(map[string]string) + cmData["my-configmap-key"] = "I am production!" + NewPlainTextConfigmap(nsProd, "my-cm", cmData) + + t.Run("no secret in destination", func(t *testing.T) { + Expect(Kamel("promote", "-n", nsDev, "promote-route", "--to", nsProd).Execute()).NotTo(Succeed()) + }) + + // Prod secret + var secData = make(map[string]string) + secData["my-secret-key"] = "very top secret production" + NewPlainTextSecret(nsProd, "my-sec", secData) + + t.Run("Production integration", func(t *testing.T) { + Expect(Kamel("promote", "-n", nsDev, "promote-route", "--to", nsProd).Execute()).To(Succeed()) + Eventually(IntegrationPodPhase(nsProd, "promote-route"), TestTimeoutMedium).Should(Equal(corev1.PodRunning)) + Eventually(IntegrationConditionStatus(nsProd, "promote-route", v1.IntegrationConditionReady), TestTimeoutShort).Should(Equal(corev1.ConditionTrue)) + Eventually(IntegrationLogs(nsProd, "promote-route"), TestTimeoutShort).Should(ContainSubstring("I am production!")) + Eventually(IntegrationLogs(nsProd, "promote-route"), TestTimeoutShort).Should(ContainSubstring("very top secret production")) + // They must use the same image + Expect(IntegrationPodImage(nsProd, "promote-route")()).Should(Equal(IntegrationPodImage(nsDev, "promote-route")())) + }) + + t.Run("no kamelet in destination", func(t *testing.T) { + Expect(Kamel("promote", "-n", nsDev, "timer-kamelet-usage", "--to", nsProd).Execute()).NotTo(Succeed()) + }) + + t.Run("kamelet integration", func(t *testing.T) { + Expect(CreateTimerKamelet(nsProd, "my-own-timer-source")()).To(Succeed()) + Expect(Kamel("promote", "-n", nsDev, "timer-kamelet-usage", "--to", nsProd).Execute()).To(Succeed()) + Eventually(IntegrationPodPhase(nsProd, "timer-kamelet-usage"), TestTimeoutMedium).Should(Equal(corev1.PodRunning)) + Eventually(IntegrationLogs(nsProd, "timer-kamelet-usage"), TestTimeoutShort).Should(ContainSubstring("Hello world")) + // They must use the same image + Expect(IntegrationPodImage(nsProd, "timer-kamelet-usage")()).Should(Equal(IntegrationPodImage(nsDev, "timer-kamelet-usage")())) + }) + }) + }) +} diff --git a/pkg/cmd/promote.go b/pkg/cmd/promote.go index 5c65b82f2..b24eafd80 100644 --- a/pkg/cmd/promote.go +++ b/pkg/cmd/promote.go @@ -22,7 +22,6 @@ import ( "encoding/json" "errors" "fmt" - "os" "strings" v1 "github.com/apache/camel-k/pkg/apis/camel/v1" @@ -44,16 +43,14 @@ func newCmdPromote(rootCmdOptions *RootCmdOptions) (*cobra.Command, *promoteCmdO RootCmdOptions: rootCmdOptions, } cmd := cobra.Command{ - Use: "promote integration -to [namespace] ...", + Use: "promote integration --to [namespace] ...", Short: "Promote an Integration from an environment to another", Long: "Promote an Integration from an environment to another, for example from a Development environment to a Production environment", - Aliases: []string{"cp", "mv"}, - Args: options.validate, PreRunE: decode(&options), RunE: options.run, } - cmd.Flags().StringP("to", "", "", "The namespace where to promote the Integration") + cmd.Flags().String("to", "", "The namespace where to promote the Integration") return &cmd, &options } @@ -65,13 +62,19 @@ type promoteCmdOptions struct { func (o *promoteCmdOptions) validate(_ *cobra.Command, args []string) error { if len(args) != 1 { - return errors.New("promote expects an integration name argument") + return errors.New("promote expects an Integration name argument") + } + if o.To == "" { + return errors.New("promote expects a destination namespace as --to argument") } - return nil } func (o *promoteCmdOptions) run(cmd *cobra.Command, args []string) error { + if err := o.validate(cmd, args); err != nil { + return err + } + it := args[0] c, err := o.GetCmdClient() if err != nil { @@ -87,26 +90,41 @@ func (o *promoteCmdOptions) run(cmd *cobra.Command, args []string) error { return fmt.Errorf("could not retrieve info for Camel K operator source") } - checkOpsCompatibility(cmd, opSource, opDest) - + err = checkOpsCompatibility(cmd, opSource, opDest) + if err != nil { + return err + } sourceIntegration, err := o.getIntegration(c, it) - o.validateDestResources(c, sourceIntegration) - //destIntegration := o.editIntegration(sourceIntegration) + if err != nil { + return err + } + if sourceIntegration.Status.Phase != v1.IntegrationPhaseRunning { + return fmt.Errorf("could not promote an integration in %s status", sourceIntegration.Status.Phase) + } + err = o.validateDestResources(c, sourceIntegration) + if err != nil { + return err + } + destIntegration, err := o.editIntegration(sourceIntegration) + if err != nil { + return err + } - //return c.Create(o.Context, destIntegration) - return nil + return c.Create(o.Context, destIntegration) } -func checkOpsCompatibility(cmd *cobra.Command, source, dest map[string]string) { +func checkOpsCompatibility(cmd *cobra.Command, source, dest map[string]string) error { if !compatibleVersions(source["Version"], dest["Version"], cmd) { - panic(fmt.Sprintf("source (%s) and destination (%s) Camel K operator versions are not compatible", source["version"], dest["version"])) + return fmt.Errorf("source (%s) and destination (%s) Camel K operator versions are not compatible", source["Version"], dest["Version"]) } if !compatibleVersions(source["Runtime Version"], dest["Runtime Version"], cmd) { - panic(fmt.Sprintf("source (%s) and destination (%s) Camel K runtime versions are not compatible", source["runtime version"], dest["runtime version"])) + return fmt.Errorf("source (%s) and destination (%s) Camel K runtime versions are not compatible", source["Runtime Version"], dest["Runtime Version"]) } if source["Registry Address"] != source["Registry Address"] { - panic(fmt.Sprintf("source (%s) and destination (%s) Camel K container images registries are not the same", source["registry address"], dest["registry address"])) + return fmt.Errorf("source (%s) and destination (%s) Camel K container images registries are not the same", source["Registry Address"], dest["Registry Address"]) } + + return nil } func (o *promoteCmdOptions) getIntegration(c client.Client, name string) (*v1.Integration, error) { @@ -122,7 +140,7 @@ func (o *promoteCmdOptions) getIntegration(c client.Client, name string) (*v1.In return &it, nil } -func (o *promoteCmdOptions) validateDestResources(c client.Client, it *v1.Integration) { +func (o *promoteCmdOptions) validateDestResources(c client.Client, it *v1.Integration) error { var traits map[string][]string var configmaps []string var secrets []string @@ -162,34 +180,37 @@ func (o *promoteCmdOptions) validateDestResources(c client.Client, it *v1.Integr kamelets = o.listKamelets(c, it) anyError := false + var errorTrace string for _, name := range configmaps { if !existsCm(o.Context, c, name, o.To) { anyError = true - fmt.Printf("Configmap %s is missing from %s namespace\n", name, o.To) + errorTrace += fmt.Sprintf("Configmap %s is missing from %s namespace\n", name, o.To) } } for _, name := range secrets { if !existsSecret(o.Context, c, name, o.To) { anyError = true - fmt.Printf("Secret %s is missing from %s namespace\n", name, o.To) + errorTrace += fmt.Sprintf("Secret %s is missing from %s namespace\n", name, o.To) } } for _, name := range pvcs { if !existsPv(o.Context, c, name, o.To) { anyError = true - fmt.Printf("PersistentVolume %s is missing from %s namespace\n", name, o.To) + errorTrace += fmt.Sprintf("PersistentVolume %s is missing from %s namespace\n", name, o.To) } } for _, name := range kamelets { if !existsKamelet(o.Context, c, name, o.To) { anyError = true - fmt.Printf("Kamelet %s is missing from %s namespace\n", name, o.To) + errorTrace += fmt.Sprintf("Kamelet %s is missing from %s namespace\n", name, o.To) } } if anyError { - os.Exit(1) + return fmt.Errorf(errorTrace) } + + return nil } func (o *promoteCmdOptions) listKamelets(c client.Client, it *v1.Integration) []string { @@ -211,7 +232,15 @@ func (o *promoteCmdOptions) listKamelets(c client.Client, it *v1.Integration) [] } } - return kamelets + // We must remove any default source/sink + var filtered []string + for _, k := range kamelets { + if k != "source" && k != "sink" { + filtered = append(filtered, k) + } + } + + return filtered } func existsCm(ctx context.Context, c client.Client, name string, namespace string) bool { @@ -266,23 +295,39 @@ func existsKamelet(ctx context.Context, c client.Client, name string, namespace return true } -func (o *promoteCmdOptions) editIntegration(it *v1.Integration) *v1.Integration { +func (o *promoteCmdOptions) editIntegration(it *v1.Integration) (*v1.Integration, error) { dst := v1.NewIntegration(o.To, it.Name) contImage := it.Status.Image dst.Spec = *it.Spec.DeepCopy() - dst.Spec.Traits = map[string]v1.TraitSpec{ - "container": traitSpecFromMap(map[string]interface{}{ - "image": contImage, - }), + if dst.Spec.Traits == nil { + dst.Spec.Traits = map[string]v1.TraitSpec{} } - - return &dst + editedContTrait, err := editContainerImage(dst.Spec.Traits["container"], contImage) + dst.Spec.Traits["container"] = editedContTrait + return &dst, err } -// TODO refactor properly -func traitSpecFromMap(spec map[string]interface{}) v1.TraitSpec { - var trait v1.TraitSpec - data, _ := json.Marshal(spec) - _ = json.Unmarshal(data, &trait.Configuration) - return trait +func editContainerImage(contTrait v1.TraitSpec, image string) (v1.TraitSpec, error) { + var editedTrait v1.TraitSpec + m := make(map[string]map[string]interface{}) + data, err := json.Marshal(contTrait) + if err != nil { + return editedTrait, err + } + err = json.Unmarshal(data, &m) + if err != nil { + return editedTrait, err + } + // We must initialize, if it was not initialized so far + if m["configuration"] == nil { + m["configuration"] = make(map[string]interface{}) + } + m["configuration"]["image"] = image + newData, err := json.Marshal(m) + if err != nil { + return editedTrait, err + } + err = json.Unmarshal(newData, &editedTrait) + + return editedTrait, err } diff --git a/pkg/cmd/promote_test.go b/pkg/cmd/promote_test.go index ef527248a..3fe2331b5 100644 --- a/pkg/cmd/promote_test.go +++ b/pkg/cmd/promote_test.go @@ -18,36 +18,43 @@ limitations under the License. package cmd import ( + "encoding/json" "testing" - "github.com/apache/camel-k/pkg/util/test" - "github.com/spf13/cobra" + v1 "github.com/apache/camel-k/pkg/apis/camel/v1" + "github.com/stretchr/testify/assert" ) -const cmdPromote = "promote" +func TestEditContainerTrait(t *testing.T) { + var containerTrait v1.TraitSpec + m := make(map[string]interface{}) + m["configuration"] = map[string]interface{}{ + "name": "myName", + "image": "myImage", + } + data, _ := json.Marshal(m) + _ = json.Unmarshal(data, &containerTrait) -// nolint: unparam -func initializePromoteCmdOptions(t *testing.T) (*promoteCmdOptions, *cobra.Command, RootCmdOptions) { - t.Helper() + editedContainerTrait, err := editContainerImage(containerTrait, "editedImage") + assert.Nil(t, err) - options, rootCmd := kamelTestPreAddCommandInit() - promoteCmdOptions := addTestPromoteCmd(*options, rootCmd) - kamelTestPostAddCommandInit(t, rootCmd) + mappedTrait := make(map[string]map[string]interface{}) + newData, _ := json.Marshal(editedContainerTrait) + _ = json.Unmarshal(newData, &mappedTrait) - return promoteCmdOptions, rootCmd, *options + assert.Equal(t, "myName", mappedTrait["configuration"]["name"]) + assert.Equal(t, "editedImage", mappedTrait["configuration"]["image"]) } -// nolint: unparam -func addTestPromoteCmd(options RootCmdOptions, rootCmd *cobra.Command) *promoteCmdOptions { - // add a testing version of operator Command - operatorCmd, promoteOptions := newCmdPromote(&options) - operatorCmd.RunE = func(c *cobra.Command, args []string) error { - return nil - } - operatorCmd.PostRunE = func(c *cobra.Command, args []string) error { - return nil - } - operatorCmd.Args = test.ArbitraryArgs - rootCmd.AddCommand(operatorCmd) - return promoteOptions +func TestEditMissingContainerTrait(t *testing.T) { + var containerTrait v1.TraitSpec + + editedContainerTrait, err := editContainerImage(containerTrait, "editedImage") + assert.Nil(t, err) + + mappedTrait := make(map[string]map[string]interface{}) + newData, _ := json.Marshal(editedContainerTrait) + _ = json.Unmarshal(newData, &mappedTrait) + + assert.Equal(t, "editedImage", mappedTrait["configuration"]["image"]) }
