From 14b63f2e01e7286a622eca9bd326c4cb5b9a620e Mon Sep 17 00:00:00 2001 From: Pasquale Congiusti Date: Wed, 1 Jun 2022 17:22:26 +0200 Subject: [PATCH 1/4] feat(cli): kamel promote (or copy) command poc * Check compatibility version between source and dest operators * Copy the Integration spec from namespace source to ns dest * Set container.image trait on destination to reuse image from the source Integration --- pkg/cmd/promote.go | 288 ++++++++++++++++++++++++++++++++++++++++ pkg/cmd/promote_test.go | 53 ++++++++ pkg/cmd/root.go | 1 + pkg/cmd/version.go | 1 + 4 files changed, 343 insertions(+) create mode 100644 pkg/cmd/promote.go create mode 100644 pkg/cmd/promote_test.go diff --git a/pkg/cmd/promote.go b/pkg/cmd/promote.go new file mode 100644 index 0000000000..5c65b82f20 --- /dev/null +++ b/pkg/cmd/promote.go @@ -0,0 +1,288 @@ +/* +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 cmd + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "strings" + + v1 "github.com/apache/camel-k/pkg/apis/camel/v1" + "github.com/apache/camel-k/pkg/apis/camel/v1alpha1" + "github.com/apache/camel-k/pkg/client" + "github.com/apache/camel-k/pkg/metadata" + "github.com/apache/camel-k/pkg/util" + "github.com/apache/camel-k/pkg/util/camel" + "github.com/apache/camel-k/pkg/util/kubernetes" + "github.com/apache/camel-k/pkg/util/source" + "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" + k8sclient "sigs.k8s.io/controller-runtime/pkg/client" +) + +// newCmdPromote --. +func newCmdPromote(rootCmdOptions *RootCmdOptions) (*cobra.Command, *promoteCmdOptions) { + options := promoteCmdOptions{ + RootCmdOptions: rootCmdOptions, + } + cmd := cobra.Command{ + 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") + + return &cmd, &options +} + +type promoteCmdOptions struct { + *RootCmdOptions + To string `mapstructure:"to" yaml:",omitempty"` +} + +func (o *promoteCmdOptions) validate(_ *cobra.Command, args []string) error { + if len(args) != 1 { + return errors.New("promote expects an integration name argument") + } + + return nil +} + +func (o *promoteCmdOptions) run(cmd *cobra.Command, args []string) error { + it := args[0] + c, err := o.GetCmdClient() + if err != nil { + return err + } + + opSource, err := operatorInfo(o.Context, c, o.Namespace) + if err != nil { + return fmt.Errorf("could not retrieve info for Camel K operator source") + } + opDest, err := operatorInfo(o.Context, c, o.To) + if err != nil { + return fmt.Errorf("could not retrieve info for Camel K operator source") + } + + checkOpsCompatibility(cmd, opSource, opDest) + + sourceIntegration, err := o.getIntegration(c, it) + o.validateDestResources(c, sourceIntegration) + //destIntegration := o.editIntegration(sourceIntegration) + + //return c.Create(o.Context, destIntegration) + return nil +} + +func checkOpsCompatibility(cmd *cobra.Command, source, dest map[string]string) { + 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"])) + } + 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"])) + } + 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"])) + } +} + +func (o *promoteCmdOptions) getIntegration(c client.Client, name string) (*v1.Integration, error) { + it := v1.NewIntegration(o.Namespace, name) + key := k8sclient.ObjectKey{ + Name: name, + Namespace: o.Namespace, + } + if err := c.Get(o.Context, key, &it); err != nil { + return nil, fmt.Errorf("could not find integration %s in namespace %s", it.Name, o.Namespace) + } + + return &it, nil +} + +func (o *promoteCmdOptions) validateDestResources(c client.Client, it *v1.Integration) { + var traits map[string][]string + var configmaps []string + var secrets []string + var pvcs []string + var kamelets []string + // Mount trait + mounts := it.Spec.Traits["mount"] + json.Unmarshal(mounts.Configuration.RawMessage, &traits) + for t, v := range traits { + if t == "configs" || t == "resources" { + for _, c := range v { + //TODO proper parse resources, now it does not account for complex parsing + if strings.HasPrefix(c, "configmap:") { + configmaps = append(configmaps, strings.Split(c, ":")[1]) + } + if strings.HasPrefix(c, "secret:") { + secrets = append(secrets, strings.Split(c, ":")[1]) + } + } + } else if t == "volumes" { + for _, c := range v { + pvcs = append(pvcs, strings.Split(c, ":")[0]) + } + } + } + // Openapi trait + openapis := it.Spec.Traits["openapi"] + json.Unmarshal(openapis.Configuration.RawMessage, &traits) + for k, v := range traits { + for _, c := range v { + if k == "configmaps" { + configmaps = append(configmaps, c) + } + } + } + // Kamelet trait + kamelets = o.listKamelets(c, it) + + anyError := false + 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) + } + } + 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) + } + } + 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) + } + } + 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) + } + } + + if anyError { + os.Exit(1) + } +} + +func (o *promoteCmdOptions) listKamelets(c client.Client, it *v1.Integration) []string { + // TODO collect any kamelets which may be coming into the kamelet trait as well + var kamelets []string + + sources, _ := kubernetes.ResolveIntegrationSources(o.Context, c, it, &kubernetes.Collection{}) + catalog, _ := camel.DefaultCatalog() + metadata.Each(catalog, sources, func(_ int, meta metadata.IntegrationMetadata) bool { + util.StringSliceUniqueConcat(&kamelets, meta.Kamelets) + return true + }) + + // Check if a Kamelet is configured as default error handler URI + defaultErrorHandlerURI := it.Spec.GetConfigurationProperty(v1alpha1.ErrorHandlerAppPropertiesPrefix + ".deadLetterUri") + if defaultErrorHandlerURI != "" { + if strings.HasPrefix(defaultErrorHandlerURI, "kamelet:") { + kamelets = append(kamelets, source.ExtractKamelet(defaultErrorHandlerURI)) + } + } + + return kamelets +} + +func existsCm(ctx context.Context, c client.Client, name string, namespace string) bool { + var obj corev1.ConfigMap + key := k8sclient.ObjectKey{ + Name: name, + Namespace: namespace, + } + if err := c.Get(ctx, key, &obj); err != nil { + return false + } + + return true +} + +func existsSecret(ctx context.Context, c client.Client, name string, namespace string) bool { + var obj corev1.Secret + key := k8sclient.ObjectKey{ + Name: name, + Namespace: namespace, + } + if err := c.Get(ctx, key, &obj); err != nil { + return false + } + + return true +} + +func existsPv(ctx context.Context, c client.Client, name string, namespace string) bool { + var obj corev1.PersistentVolume + key := k8sclient.ObjectKey{ + Name: name, + Namespace: namespace, + } + if err := c.Get(ctx, key, &obj); err != nil { + return false + } + + return true +} + +func existsKamelet(ctx context.Context, c client.Client, name string, namespace string) bool { + var obj v1alpha1.Kamelet + key := k8sclient.ObjectKey{ + Name: name, + Namespace: namespace, + } + if err := c.Get(ctx, key, &obj); err != nil { + return false + } + + return true +} + +func (o *promoteCmdOptions) editIntegration(it *v1.Integration) *v1.Integration { + 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, + }), + } + + return &dst +} + +// 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 +} diff --git a/pkg/cmd/promote_test.go b/pkg/cmd/promote_test.go new file mode 100644 index 0000000000..ef527248ac --- /dev/null +++ b/pkg/cmd/promote_test.go @@ -0,0 +1,53 @@ +/* +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 cmd + +import ( + "testing" + + "github.com/apache/camel-k/pkg/util/test" + "github.com/spf13/cobra" +) + +const cmdPromote = "promote" + +// nolint: unparam +func initializePromoteCmdOptions(t *testing.T) (*promoteCmdOptions, *cobra.Command, RootCmdOptions) { + t.Helper() + + options, rootCmd := kamelTestPreAddCommandInit() + promoteCmdOptions := addTestPromoteCmd(*options, rootCmd) + kamelTestPostAddCommandInit(t, rootCmd) + + return promoteCmdOptions, rootCmd, *options +} + +// 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 +} diff --git a/pkg/cmd/root.go b/pkg/cmd/root.go index f148ab7e58..abdde07476 100644 --- a/pkg/cmd/root.go +++ b/pkg/cmd/root.go @@ -150,6 +150,7 @@ func addKamelSubcommands(cmd *cobra.Command, options *RootCmdOptions) { cmd.AddCommand(cmdOnly(newCmdDump(options))) cmd.AddCommand(newCmdLocal(options)) cmd.AddCommand(cmdOnly(newCmdBind(options))) + cmd.AddCommand(cmdOnly(newCmdPromote(options))) cmd.AddCommand(newCmdKamelet(options)) } diff --git a/pkg/cmd/version.go b/pkg/cmd/version.go index 011d6eddb3..ec0e493bd4 100644 --- a/pkg/cmd/version.go +++ b/pkg/cmd/version.go @@ -143,6 +143,7 @@ func operatorInfo(ctx context.Context, c client.Client, namespace string) (map[s infos["version"] = platform.Status.Version infos["publishStrategy"] = string(platform.Status.Build.PublishStrategy) infos["runtimeVersion"] = platform.Status.Build.RuntimeVersion + infos["registryAddress"] = platform.Status.Build.Registry.Address if platform.Status.Info != nil { for k, v := range platform.Status.Info { From 4877cea58c976bfae5020ae7cd18f658b2f2fc34 Mon Sep 17 00:00:00 2001 From: Pasquale Congiusti Date: Wed, 15 Jun 2022 09:57:47 +0200 Subject: [PATCH 2/4] 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(-) create mode 100644 e2e/common/cli/files/promote-route.groovy create mode 100644 e2e/common/cli/promote_test.go diff --git a/e2e/common/cli/files/promote-route.groovy b/e2e/common/cli/files/promote-route.groovy new file mode 100644 index 0000000000..943a4ab915 --- /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 0000000000..3f78401655 --- /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 5c65b82f20..b24eafd808 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 ef527248ac..3fe2331b5b 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"]) } From dde508e8c7c2da3ab3b96ef0537c5e6865579e78 Mon Sep 17 00:00:00 2001 From: Pasquale Congiusti Date: Thu, 16 Jun 2022 17:24:30 +0200 Subject: [PATCH 3/4] chore(cli): polished promote feature --- pkg/cmd/promote.go | 91 +++++++++++++++++++++++++-------------- pkg/trait/kamelets.go | 24 ++--------- pkg/trait/mount.go | 2 +- pkg/util/kamelets/util.go | 56 ++++++++++++++++++++++++ 4 files changed, 119 insertions(+), 54 deletions(-) create mode 100644 pkg/util/kamelets/util.go diff --git a/pkg/cmd/promote.go b/pkg/cmd/promote.go index b24eafd808..28be8b28bf 100644 --- a/pkg/cmd/promote.go +++ b/pkg/cmd/promote.go @@ -27,11 +27,10 @@ import ( v1 "github.com/apache/camel-k/pkg/apis/camel/v1" "github.com/apache/camel-k/pkg/apis/camel/v1alpha1" "github.com/apache/camel-k/pkg/client" - "github.com/apache/camel-k/pkg/metadata" - "github.com/apache/camel-k/pkg/util" "github.com/apache/camel-k/pkg/util/camel" + "github.com/apache/camel-k/pkg/util/kamelets" "github.com/apache/camel-k/pkg/util/kubernetes" - "github.com/apache/camel-k/pkg/util/source" + "github.com/apache/camel-k/pkg/util/resource" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" k8sclient "sigs.k8s.io/controller-runtime/pkg/client" @@ -120,7 +119,7 @@ func checkOpsCompatibility(cmd *cobra.Command, source, dest map[string]string) e if !compatibleVersions(source["Runtime Version"], dest["Runtime Version"], cmd) { 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"] { + if 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"]) } @@ -148,27 +147,52 @@ func (o *promoteCmdOptions) validateDestResources(c client.Client, it *v1.Integr var kamelets []string // Mount trait mounts := it.Spec.Traits["mount"] - json.Unmarshal(mounts.Configuration.RawMessage, &traits) + if err := json.Unmarshal(mounts.Configuration.RawMessage, &traits); err != nil { + return err + } for t, v := range traits { - if t == "configs" || t == "resources" { + switch t { + case "configs": for _, c := range v { - //TODO proper parse resources, now it does not account for complex parsing - if strings.HasPrefix(c, "configmap:") { - configmaps = append(configmaps, strings.Split(c, ":")[1]) + if conf, parseErr := resource.ParseConfig(c); parseErr == nil { + if conf.StorageType() == resource.StorageTypeConfigmap { + configmaps = append(configmaps, conf.Name()) + } else if conf.StorageType() == resource.StorageTypeSecret { + secrets = append(secrets, conf.Name()) + } + } else { + return parseErr } - if strings.HasPrefix(c, "secret:") { - secrets = append(secrets, strings.Split(c, ":")[1]) + } + case "resources": + for _, c := range v { + if conf, parseErr := resource.ParseResource(c); parseErr == nil { + if conf.StorageType() == resource.StorageTypeConfigmap { + configmaps = append(configmaps, conf.Name()) + } else if conf.StorageType() == resource.StorageTypeSecret { + secrets = append(secrets, conf.Name()) + } + } else { + return parseErr } } - } else if t == "volumes" { + case "volumes": for _, c := range v { - pvcs = append(pvcs, strings.Split(c, ":")[0]) + if conf, parseErr := resource.ParseVolume(c); parseErr == nil { + if conf.StorageType() == resource.StorageTypePVC { + pvcs = append(pvcs, conf.Name()) + } + } else { + return parseErr + } } } } // Openapi trait openapis := it.Spec.Traits["openapi"] - json.Unmarshal(openapis.Configuration.RawMessage, &traits) + if err := json.Unmarshal(openapis.Configuration.RawMessage, &traits); err != nil { + return err + } for k, v := range traits { for _, c := range v { if k == "configmaps" { @@ -177,7 +201,17 @@ func (o *promoteCmdOptions) validateDestResources(c client.Client, it *v1.Integr } } // Kamelet trait - kamelets = o.listKamelets(c, it) + kameletTrait := it.Spec.Traits["kamelets"] + var kameletListTrait map[string]string + if err := json.Unmarshal(kameletTrait.Configuration.RawMessage, &kameletListTrait); err != nil { + return err + } + kamelets = strings.Split(kameletListTrait["list"], ",") + sourceKamelets, err := o.listKamelets(c, it) + if err != nil { + return err + } + kamelets = append(kamelets, sourceKamelets...) anyError := false var errorTrace string @@ -213,23 +247,14 @@ func (o *promoteCmdOptions) validateDestResources(c client.Client, it *v1.Integr return nil } -func (o *promoteCmdOptions) listKamelets(c client.Client, it *v1.Integration) []string { - // TODO collect any kamelets which may be coming into the kamelet trait as well - var kamelets []string - - sources, _ := kubernetes.ResolveIntegrationSources(o.Context, c, it, &kubernetes.Collection{}) - catalog, _ := camel.DefaultCatalog() - metadata.Each(catalog, sources, func(_ int, meta metadata.IntegrationMetadata) bool { - util.StringSliceUniqueConcat(&kamelets, meta.Kamelets) - return true - }) - - // Check if a Kamelet is configured as default error handler URI - defaultErrorHandlerURI := it.Spec.GetConfigurationProperty(v1alpha1.ErrorHandlerAppPropertiesPrefix + ".deadLetterUri") - if defaultErrorHandlerURI != "" { - if strings.HasPrefix(defaultErrorHandlerURI, "kamelet:") { - kamelets = append(kamelets, source.ExtractKamelet(defaultErrorHandlerURI)) - } +func (o *promoteCmdOptions) listKamelets(c client.Client, it *v1.Integration) ([]string, error) { + catalog, err := camel.DefaultCatalog() + if err != nil { + return nil, err + } + kamelets, err := kamelets.ExtractKameletFromSources(o.Context, c, catalog, &kubernetes.Collection{}, it) + if err != nil { + return nil, err } // We must remove any default source/sink @@ -240,7 +265,7 @@ func (o *promoteCmdOptions) listKamelets(c client.Client, it *v1.Integration) [] } } - return filtered + return filtered, nil } func existsCm(ctx context.Context, c client.Client, name string, namespace string) bool { diff --git a/pkg/trait/kamelets.go b/pkg/trait/kamelets.go index a5c1c936be..f8873cd7d7 100644 --- a/pkg/trait/kamelets.go +++ b/pkg/trait/kamelets.go @@ -31,13 +31,11 @@ import ( "github.com/apache/camel-k/pkg/apis/camel/v1alpha1" kameletutils "github.com/apache/camel-k/pkg/kamelet" "github.com/apache/camel-k/pkg/kamelet/repository" - "github.com/apache/camel-k/pkg/metadata" "github.com/apache/camel-k/pkg/platform" "github.com/apache/camel-k/pkg/util" "github.com/apache/camel-k/pkg/util/digest" "github.com/apache/camel-k/pkg/util/dsl" - "github.com/apache/camel-k/pkg/util/kubernetes" - "github.com/apache/camel-k/pkg/util/source" + "github.com/apache/camel-k/pkg/util/kamelets" ) // The kamelets trait is a platform trait used to inject Kamelets into the integration runtime. @@ -91,23 +89,9 @@ func (t *kameletsTrait) Configure(e *Environment) (bool, error) { } if IsNilOrTrue(t.Auto) { - var kamelets []string - if t.List == "" { - sources, err := kubernetes.ResolveIntegrationSources(e.Ctx, e.Client, e.Integration, e.Resources) - if err != nil { - return false, err - } - metadata.Each(e.CamelCatalog, sources, func(_ int, meta metadata.IntegrationMetadata) bool { - util.StringSliceUniqueConcat(&kamelets, meta.Kamelets) - return true - }) - } - // Check if a Kamelet is configured as default error handler URI - defaultErrorHandlerURI := e.Integration.Spec.GetConfigurationProperty(v1alpha1.ErrorHandlerAppPropertiesPrefix + ".deadLetterUri") - if defaultErrorHandlerURI != "" { - if strings.HasPrefix(defaultErrorHandlerURI, "kamelet:") { - kamelets = append(kamelets, source.ExtractKamelet(defaultErrorHandlerURI)) - } + kamelets, err := kamelets.ExtractKameletFromSources(e.Ctx, e.Client, e.CamelCatalog, e.Resources, e.Integration) + if err != nil { + return false, err } if len(kamelets) > 0 { diff --git a/pkg/trait/mount.go b/pkg/trait/mount.go index 6c8976a856..81d58c50f1 100644 --- a/pkg/trait/mount.go +++ b/pkg/trait/mount.go @@ -43,7 +43,7 @@ type mountTrait struct { // A list of configuration pointing to configmap/secret. // The configuration are expected to be UTF-8 resources as they are processed by runtime Camel Context and tried to be parsed as property files. // They are also made available on the classpath in order to ease their usage directly from the Route. - // Syntax: [configmap|secret]:name[key], where name represents the resource name and key optionally represents the resource key to be filtered + // Syntax: [configmap|secret]:name[/key], where name represents the resource name and key optionally represents the resource key to be filtered Configs []string `property:"configs" json:"configs,omitempty"` // A list of resources (text or binary content) pointing to configmap/secret. // The resources are expected to be any resource type (text or binary content). diff --git a/pkg/util/kamelets/util.go b/pkg/util/kamelets/util.go new file mode 100644 index 0000000000..2db03c7fc9 --- /dev/null +++ b/pkg/util/kamelets/util.go @@ -0,0 +1,56 @@ +/* +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 kamelets + +import ( + "context" + "strings" + + v1 "github.com/apache/camel-k/pkg/apis/camel/v1" + "github.com/apache/camel-k/pkg/apis/camel/v1alpha1" + "github.com/apache/camel-k/pkg/client" + "github.com/apache/camel-k/pkg/metadata" + "github.com/apache/camel-k/pkg/util" + "github.com/apache/camel-k/pkg/util/camel" + "github.com/apache/camel-k/pkg/util/kubernetes" + "github.com/apache/camel-k/pkg/util/source" +) + +// ExtractKameletFromSources provide a list of Kamelets referred into the Integration sources. +func ExtractKameletFromSources(context context.Context, c client.Client, catalog *camel.RuntimeCatalog, resources *kubernetes.Collection, it *v1.Integration) ([]string, error) { + var kamelets []string + + sources, err := kubernetes.ResolveIntegrationSources(context, c, it, resources) + if err != nil { + return nil, err + } + metadata.Each(catalog, sources, func(_ int, meta metadata.IntegrationMetadata) bool { + util.StringSliceUniqueConcat(&kamelets, meta.Kamelets) + return true + }) + + // Check if a Kamelet is configured as default error handler URI + defaultErrorHandlerURI := it.Spec.GetConfigurationProperty(v1alpha1.ErrorHandlerAppPropertiesPrefix + ".deadLetterUri") + if defaultErrorHandlerURI != "" { + if strings.HasPrefix(defaultErrorHandlerURI, "kamelet:") { + kamelets = append(kamelets, source.ExtractKamelet(defaultErrorHandlerURI)) + } + } + + return kamelets, nil +} From 533f662e96d1be4f48f63c57e9d75467167b837b Mon Sep 17 00:00:00 2001 From: Pasquale Congiusti Date: Fri, 17 Jun 2022 16:17:19 +0200 Subject: [PATCH 4/4] feat(cli): promote kameletbinding support --- e2e/common/cli/promote_test.go | 28 ++++++++-- pkg/cmd/promote.go | 98 ++++++++++++++++++++++++++++------ 2 files changed, 107 insertions(+), 19 deletions(-) diff --git a/e2e/common/cli/promote_test.go b/e2e/common/cli/promote_test.go index 3f78401655..c2b7182c78 100644 --- a/e2e/common/cli/promote_test.go +++ b/e2e/common/cli/promote_test.go @@ -46,7 +46,7 @@ func TestKamelCLIPromote(t *testing.T) { secData["my-secret-key"] = "very top secret development" NewPlainTextSecret(nsDev, "my-sec", secData) - t.Run("plain integration", func(t *testing.T) { + t.Run("plain integration dev", func(t *testing.T) { Expect(Kamel("run", "-n", nsDev, "./files/promote-route.groovy", "--config", "configmap:my-cm", "--config", "secret:my-sec", @@ -57,13 +57,20 @@ func TestKamelCLIPromote(t *testing.T) { Eventually(IntegrationLogs(nsDev, "promote-route"), TestTimeoutShort).Should(ContainSubstring("very top secret development")) }) - t.Run("kamelet integration", func(t *testing.T) { + t.Run("kamelet integration dev", 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")) }) + t.Run("kamelet binding dev", func(t *testing.T) { + Expect(CreateTimerKamelet(nsDev, "kb-timer-source")()).To(Succeed()) + Expect(Kamel("bind", "kb-timer-source", "log:info", "-p", "message=my-kamelet-binding-rocks", "-n", nsDev).Execute()).To(Succeed()) + Eventually(IntegrationPodPhase(nsDev, "kb-timer-source-to-log"), TestTimeoutMedium).Should(Equal(corev1.PodRunning)) + Eventually(IntegrationLogs(nsDev, "kb-timer-source-to-log"), TestTimeoutShort).Should(ContainSubstring("my-kamelet-binding-rocks")) + }) + // Prod environment namespace WithNewTestNamespace(t, func(nsProd string) { Expect(Kamel("install", "-n", nsProd).Execute()).To(Succeed()) @@ -85,7 +92,7 @@ func TestKamelCLIPromote(t *testing.T) { secData["my-secret-key"] = "very top secret production" NewPlainTextSecret(nsProd, "my-sec", secData) - t.Run("Production integration", func(t *testing.T) { + t.Run("plain integration promotion", 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)) @@ -99,7 +106,7 @@ func TestKamelCLIPromote(t *testing.T) { Expect(Kamel("promote", "-n", nsDev, "timer-kamelet-usage", "--to", nsProd).Execute()).NotTo(Succeed()) }) - t.Run("kamelet integration", func(t *testing.T) { + t.Run("kamelet integration promotion", 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)) @@ -107,6 +114,19 @@ func TestKamelCLIPromote(t *testing.T) { // They must use the same image Expect(IntegrationPodImage(nsProd, "timer-kamelet-usage")()).Should(Equal(IntegrationPodImage(nsDev, "timer-kamelet-usage")())) }) + + t.Run("no kamelet for kameletbinding in destination", func(t *testing.T) { + Expect(Kamel("promote", "-n", nsDev, "kb-timer-source", "--to", nsProd).Execute()).NotTo(Succeed()) + }) + + t.Run("kamelet binding promotion", func(t *testing.T) { + Expect(CreateTimerKamelet(nsProd, "kb-timer-source")()).To(Succeed()) + Expect(Kamel("promote", "-n", nsDev, "kb-timer-source-to-log", "--to", nsProd).Execute()).To(Succeed()) + Eventually(IntegrationPodPhase(nsProd, "kb-timer-source-to-log"), TestTimeoutMedium).Should(Equal(corev1.PodRunning)) + Eventually(IntegrationLogs(nsProd, "kb-timer-source-to-log"), TestTimeoutShort).Should(ContainSubstring("my-kamelet-binding-rocks")) + // They must use the same image + Expect(IntegrationPodImage(nsProd, "kb-timer-source-to-log")()).Should(Equal(IntegrationPodImage(nsDev, "kb-timer-source-to-log")())) + }) }) }) } diff --git a/pkg/cmd/promote.go b/pkg/cmd/promote.go index 28be8b28bf..6e15c6cc3b 100644 --- a/pkg/cmd/promote.go +++ b/pkg/cmd/promote.go @@ -20,10 +20,11 @@ package cmd import ( "context" "encoding/json" - "errors" "fmt" "strings" + "github.com/pkg/errors" + v1 "github.com/apache/camel-k/pkg/apis/camel/v1" "github.com/apache/camel-k/pkg/apis/camel/v1alpha1" "github.com/apache/camel-k/pkg/client" @@ -33,6 +34,7 @@ import ( "github.com/apache/camel-k/pkg/util/resource" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" k8sclient "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -43,8 +45,8 @@ func newCmdPromote(rootCmdOptions *RootCmdOptions) (*cobra.Command, *promoteCmdO } cmd := cobra.Command{ 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", + Short: "Promote an Integration/KameletBinding from an environment to another", + Long: "Promote an Integration/KameletBinding from an environment to another, for example from a Development environment to a Production environment", PreRunE: decode(&options), RunE: options.run, } @@ -61,7 +63,7 @@ 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/KameletBinding name argument") } if o.To == "" { return errors.New("promote expects a destination namespace as --to argument") @@ -74,39 +76,61 @@ func (o *promoteCmdOptions) run(cmd *cobra.Command, args []string) error { return err } - it := args[0] + name := args[0] c, err := o.GetCmdClient() if err != nil { - return err + return errors.Wrap(err, "could not retrieve cluster client") } opSource, err := operatorInfo(o.Context, c, o.Namespace) if err != nil { - return fmt.Errorf("could not retrieve info for Camel K operator source") + return errors.Wrap(err, "could not retrieve info for Camel K operator source") } opDest, err := operatorInfo(o.Context, c, o.To) if err != nil { - return fmt.Errorf("could not retrieve info for Camel K operator source") + return errors.Wrap(err, "could not retrieve info for Camel K operator destination") } err = checkOpsCompatibility(cmd, opSource, opDest) if err != nil { - return err + return errors.Wrap(err, "could not verify operators compatibility") + } + promoteKameletBinding := false + var sourceIntegration *v1.Integration + // We first look if a KameletBinding with the name exists + sourceKameletBinding, err := o.getKameletBinding(c, name) + if err != nil && !k8serrors.IsNotFound(err) { + return errors.Wrap(err, "problems looking for KameletBinding "+name) + } + if sourceKameletBinding != nil { + promoteKameletBinding = true } - sourceIntegration, err := o.getIntegration(c, it) + sourceIntegration, err = o.getIntegration(c, name) if err != nil { - return err + return errors.Wrap(err, "could not get Integration "+name) } if sourceIntegration.Status.Phase != v1.IntegrationPhaseRunning { - return fmt.Errorf("could not promote an integration in %s status", sourceIntegration.Status.Phase) + return fmt.Errorf("could not promote an Integration in %s status", sourceIntegration.Status.Phase) } err = o.validateDestResources(c, sourceIntegration) if err != nil { - return err + return errors.Wrap(err, "could not validate destination resources") + } + if promoteKameletBinding { + // KameletBinding promotion + destKameletBinding, err := o.editKameletBinding(sourceKameletBinding, sourceIntegration) + if err != nil { + return errors.Wrap(err, "could not edit KameletBinding "+name) + } + + return c.Create(o.Context, destKameletBinding) } + // Plain Integration promotion destIntegration, err := o.editIntegration(sourceIntegration) if err != nil { - return err + if err != nil { + return errors.Wrap(err, "could not edit Integration "+name) + } } return c.Create(o.Context, destIntegration) @@ -126,6 +150,19 @@ func checkOpsCompatibility(cmd *cobra.Command, source, dest map[string]string) e return nil } +func (o *promoteCmdOptions) getKameletBinding(c client.Client, name string) (*v1alpha1.KameletBinding, error) { + it := v1alpha1.NewKameletBinding(o.Namespace, name) + key := k8sclient.ObjectKey{ + Name: name, + Namespace: o.Namespace, + } + if err := c.Get(o.Context, key, &it); err != nil { + return nil, err + } + + return &it, nil +} + func (o *promoteCmdOptions) getIntegration(c client.Client, name string) (*v1.Integration, error) { it := v1.NewIntegration(o.Namespace, name) key := k8sclient.ObjectKey{ @@ -133,7 +170,7 @@ func (o *promoteCmdOptions) getIntegration(c client.Client, name string) (*v1.In Namespace: o.Namespace, } if err := c.Get(o.Context, key, &it); err != nil { - return nil, fmt.Errorf("could not find integration %s in namespace %s", it.Name, o.Namespace) + return nil, err } return &it, nil @@ -145,6 +182,9 @@ func (o *promoteCmdOptions) validateDestResources(c client.Client, it *v1.Integr var secrets []string var pvcs []string var kamelets []string + if it.Spec.Traits == nil { + return nil + } // Mount trait mounts := it.Spec.Traits["mount"] if err := json.Unmarshal(mounts.Configuration.RawMessage, &traits); err != nil { @@ -332,6 +372,34 @@ func (o *promoteCmdOptions) editIntegration(it *v1.Integration) (*v1.Integration return &dst, err } +func (o *promoteCmdOptions) editKameletBinding(kb *v1alpha1.KameletBinding, it *v1.Integration) (*v1alpha1.KameletBinding, error) { + dst := v1alpha1.NewKameletBinding(o.To, kb.Name) + dst.Spec = *kb.Spec.DeepCopy() + contImage := it.Status.Image + if dst.Spec.Integration == nil { + dst.Spec.Integration = &v1.IntegrationSpec{} + } + if dst.Spec.Integration.Traits == nil { + dst.Spec.Integration.Traits = map[string]v1.TraitSpec{} + } + editedContTrait, err := editContainerImage(dst.Spec.Integration.Traits["container"], contImage) + dst.Spec.Integration.Traits["container"] = editedContTrait + if dst.Spec.Source.Ref != nil { + dst.Spec.Source.Ref.Namespace = o.To + } + if dst.Spec.Sink.Ref != nil { + dst.Spec.Sink.Ref.Namespace = o.To + } + if dst.Spec.Steps != nil { + for _, step := range dst.Spec.Steps { + if step.Ref != nil { + step.Ref.Namespace = o.To + } + } + } + return &dst, err +} + func editContainerImage(contTrait v1.TraitSpec, image string) (v1.TraitSpec, error) { var editedTrait v1.TraitSpec m := make(map[string]map[string]interface{})