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 {