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 c23af418abefd8cbbd7c9a76ff617cc8867007af Author: Pasquale Congiusti <[email protected]> AuthorDate: Wed Jun 1 17:22:26 2022 +0200 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(+) diff --git a/pkg/cmd/promote.go b/pkg/cmd/promote.go new file mode 100644 index 000000000..5c65b82f2 --- /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 000000000..ef527248a --- /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 f148ab7e5..abdde0747 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 011d6eddb..ec0e493bd 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 {
