You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by pc...@apache.org on 2022/06/20 07:13:46 UTC

[camel-k] 01/04: feat(cli): kamel promote (or copy) command poc

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 <pa...@gmail.com>
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 {