You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by nf...@apache.org on 2021/04/12 13:56:45 UTC

[camel-k] 02/06: Fix #2083: add steps and query-like parameters

This is an automated email from the ASF dual-hosted git repository.

nferraro pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/camel-k.git

commit 0314d28c0435f54e9f848bc701792a1cce801db5
Author: nicolaferraro <ni...@gmail.com>
AuthorDate: Wed Apr 7 15:20:09 2021 +0200

    Fix #2083: add steps and query-like parameters
---
 pkg/cmd/bind.go                      | 68 +++++++++++++++++++++++++++++++-----
 pkg/cmd/root.go                      | 10 +++---
 pkg/cmd/run.go                       |  2 +-
 pkg/util/reference/reference.go      | 44 +++++++++++++++++++++--
 pkg/util/reference/reference_test.go | 36 +++++++++++++++++++
 5 files changed, 142 insertions(+), 18 deletions(-)

diff --git a/pkg/cmd/bind.go b/pkg/cmd/bind.go
index 320ffb4..2771899 100644
--- a/pkg/cmd/bind.go
+++ b/pkg/cmd/bind.go
@@ -57,15 +57,17 @@ func newCmdBind(rootCmdOptions *RootCmdOptions) (*cobra.Command, *bindCmdOptions
 
 	cmd.Flags().String("name", "", "Name for the binding")
 	cmd.Flags().StringP("output", "o", "", "Output format. One of: json|yaml")
-	cmd.Flags().StringArrayP("property", "p", nil, `Add a binding property in the form of "source.<key>=<value>" or "sink.<key>=<value>"`)
+	cmd.Flags().StringArrayP("property", "p", nil, `Add a binding property in the form of "source.<key>=<value>", "sink.<key>=<value>" or "step-<n>.<key>=<value>"`)
 	cmd.Flags().Bool("skip-checks", false, "Do not verify the binding for compliance with Kamelets and other Kubernetes resources")
+	cmd.Flags().StringArray("step", nil, `Add binding steps as Kubernetes resources, such as Kamelets. Endpoints are expected in the format "[[apigroup/]version:]kind:[namespace/]name" or plain Camel URIs.`)
 
 	return &cmd, &options
 }
 
 const (
-	sourceKey = "source"
-	sinkKey   = "sink"
+	sourceKey     = "source"
+	sinkKey       = "sink"
+	stepKeyPrefix = "step-"
 )
 
 type bindCmdOptions struct {
@@ -74,6 +76,7 @@ type bindCmdOptions struct {
 	OutputFormat string   `mapstructure:"output" yaml:",omitempty"`
 	Properties   []string `mapstructure:"properties" yaml:",omitempty"`
 	SkipChecks   bool     `mapstructure:"skip-checks" yaml:",omitempty"`
+	Steps        []string `mapstructure:"steps" yaml:",omitempty"`
 }
 
 func (o *bindCmdOptions) validate(cmd *cobra.Command, args []string) error {
@@ -105,6 +108,17 @@ func (o *bindCmdOptions) validate(cmd *cobra.Command, args []string) error {
 		if err := o.checkCompliance(cmd, sink); err != nil {
 			return err
 		}
+
+		for idx, stepDesc := range o.Steps {
+			stepKey := fmt.Sprintf("%s%d", stepKeyPrefix, idx)
+			step, err := o.decode(stepDesc, stepKey)
+			if err != nil {
+				return err
+			}
+			if err := o.checkCompliance(cmd, step); err != nil {
+				return err
+			}
+		}
 	}
 
 	return nil
@@ -134,6 +148,18 @@ func (o *bindCmdOptions) run(args []string) error {
 		},
 	}
 
+	if len(o.Steps) > 0 {
+		binding.Spec.Steps = make([]v1alpha1.Endpoint, 0)
+		for idx, stepDesc := range o.Steps {
+			stepKey := fmt.Sprintf("%s%d", stepKeyPrefix, idx)
+			step, err := o.decode(stepDesc, stepKey)
+			if err != nil {
+				return err
+			}
+			binding.Spec.Steps = append(binding.Spec.Steps, step)
+		}
+	}
+
 	switch o.OutputFormat {
 	case "":
 		// continue..
@@ -183,7 +209,8 @@ func (o *bindCmdOptions) run(args []string) error {
 func (o *bindCmdOptions) decode(res string, key string) (v1alpha1.Endpoint, error) {
 	refConverter := reference.NewConverter(reference.KameletPrefix)
 	endpoint := v1alpha1.Endpoint{}
-	props, err := o.asEndpointProperties(o.getProperties(key))
+	explicitProps := o.getProperties(key)
+	props, err := o.asEndpointProperties(explicitProps)
 	if err != nil {
 		return endpoint, err
 	}
@@ -201,6 +228,26 @@ func (o *bindCmdOptions) decode(res string, key string) (v1alpha1.Endpoint, erro
 	if endpoint.Ref.Namespace == "" {
 		endpoint.Ref.Namespace = o.Namespace
 	}
+	embeddedProps, err := refConverter.PropertiesFromString(res)
+	if err != nil {
+		return endpoint, err
+	}
+	if len(embeddedProps) > 0 {
+		allProps := make(map[string]string)
+		for k, v := range explicitProps {
+			allProps[k] = v
+		}
+		for k, v := range embeddedProps {
+			allProps[k] = v
+		}
+
+		props, err := o.asEndpointProperties(allProps)
+		if err != nil {
+			return endpoint, err
+		}
+		endpoint.Properties = props
+	}
+
 	return endpoint, nil
 }
 
@@ -254,14 +301,17 @@ func (o *bindCmdOptions) getProperties(refType string) map[string]string {
 func (o *bindCmdOptions) parseProperty(prop string) (string, string, string, error) {
 	parts := strings.SplitN(prop, "=", 2)
 	if len(parts) != 2 {
-		return "", "", "", fmt.Errorf(`property %q does not follow format "[source|sink].<key>=<value>"`, prop)
+		return "", "", "", fmt.Errorf(`property %q does not follow format "[source|sink|step-<n>].<key>=<value>"`, prop)
 	}
 	keyParts := strings.SplitN(parts[0], ".", 2)
 	if len(keyParts) != 2 {
-		return "", "", "", fmt.Errorf(`property key %q does not follow format "[source|sink].<key>"`, parts[0])
+		return "", "", "", fmt.Errorf(`property key %q does not follow format "[source|sink|step-<n>].<key>"`, parts[0])
 	}
-	if keyParts[0] != sourceKey && keyParts[0] != sinkKey {
-		return "", "", "", fmt.Errorf(`property key %q does not start with "source." or "sink."`, parts[0])
+	isSource := keyParts[0] == sourceKey
+	isSink := keyParts[0] == sinkKey
+	isStep := strings.HasPrefix(keyParts[0], stepKeyPrefix)
+	if !isSource && !isSink && !isStep {
+		return "", "", "", fmt.Errorf(`property key %q does not start with "source.", "sink." or "step-<n>."`, parts[0])
 	}
 	return keyParts[0], keyParts[1], parts[1], nil
 }
@@ -280,7 +330,7 @@ func (o *bindCmdOptions) checkCompliance(cmd *cobra.Command, endpoint v1alpha1.E
 		if err := c.Get(o.Context, key, &kamelet); err != nil {
 			if k8serrors.IsNotFound(err) {
 				// Kamelet may be in the operator namespace, but we currently don't have a way to determine it: we just warn
-				fmt.Fprintf(cmd.OutOrStderr(), "Warning: Kamelet %q not found in namespace %q\n", key.Name, key.Namespace)
+				fmt.Fprintf(cmd.ErrOrStderr(), "Warning: Kamelet %q not found in namespace %q\n", key.Name, key.Namespace)
 				return nil
 			}
 			return err
diff --git a/pkg/cmd/root.go b/pkg/cmd/root.go
index d3de5c7..06a775b 100644
--- a/pkg/cmd/root.go
+++ b/pkg/cmd/root.go
@@ -194,24 +194,24 @@ func (command *RootCmdOptions) preRun(cmd *cobra.Command, _ []string) error {
 		// Furthermore, there can be any incompatibilities, as the install command deploys
 		// the operator version it's compatible with.
 		if cmd.Use != installCommand && cmd.Use != operatorCommand {
-			checkAndShowCompatibilityWarning(command.Context, c, command.Namespace)
+			checkAndShowCompatibilityWarning(cmd, command.Context, c, command.Namespace)
 		}
 	}
 
 	return nil
 }
 
-func checkAndShowCompatibilityWarning(ctx context.Context, c client.Client, namespace string) {
+func checkAndShowCompatibilityWarning(cmd *cobra.Command, ctx context.Context, c client.Client, namespace string) {
 	operatorVersion, err := operatorVersion(ctx, c, namespace)
 	if err != nil {
 		if k8serrors.IsNotFound(err) {
-			fmt.Printf("No IntegrationPlatform resource in %s namespace\n", namespace)
+			fmt.Fprintf(cmd.ErrOrStderr(), "No IntegrationPlatform resource in %s namespace\n", namespace)
 		} else {
-			fmt.Printf("Unable to retrieve the operator version: %s\n", err.Error())
+			fmt.Fprintf(cmd.ErrOrStderr(), "Unable to retrieve the operator version: %s\n", err.Error())
 		}
 	} else {
 		if operatorVersion != "" && !compatibleVersions(operatorVersion, defaults.Version) {
-			fmt.Printf("You're using Camel K %s client with a %s cluster operator, it's recommended to use the same version to improve compatibility.\n\n", defaults.Version, operatorVersion)
+			fmt.Fprintf(cmd.ErrOrStderr(), "You're using Camel K %s client with a %s cluster operator, it's recommended to use the same version to improve compatibility.\n\n", defaults.Version, operatorVersion)
 		}
 	}
 }
diff --git a/pkg/cmd/run.go b/pkg/cmd/run.go
index 0b6bf6e..255feaf 100644
--- a/pkg/cmd/run.go
+++ b/pkg/cmd/run.go
@@ -414,7 +414,7 @@ func (o *runCmdOptions) syncIntegration(cmd *cobra.Command, c client.Client, sou
 				}
 			}()
 		} else {
-			fmt.Printf("Warning: the following URL will not be watched for changes: %s\n", s)
+			fmt.Fprintf(cmd.ErrOrStderr(), "Warning: the following URL will not be watched for changes: %s\n", s)
 		}
 	}
 
diff --git a/pkg/util/reference/reference.go b/pkg/util/reference/reference.go
index dc79cd5..5af887e 100644
--- a/pkg/util/reference/reference.go
+++ b/pkg/util/reference/reference.go
@@ -18,12 +18,14 @@ limitations under the License.
 package reference
 
 import (
-	"errors"
 	"fmt"
+	"net/url"
 	"regexp"
+	"strings"
 	"unicode"
 
 	camelv1alpha1 "github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
+	"github.com/pkg/errors"
 	corev1 "k8s.io/api/core/v1"
 	eventingv1 "knative.dev/eventing/pkg/apis/eventing/v1"
 	messagingv1 "knative.dev/eventing/pkg/apis/messaging/v1"
@@ -35,8 +37,9 @@ const (
 )
 
 var (
-	simpleNameRegexp = regexp.MustCompile(`^(?:(?P<namespace>[a-z0-9-.]+)/)?(?P<name>[a-z0-9-.]+)$`)
-	fullNameRegexp   = regexp.MustCompile(`^(?:(?P<apiVersion>(?:[a-z0-9-.]+/)?(?:[a-z0-9-.]+)):)?(?P<kind>[A-Za-z0-9-.]+):(?:(?P<namespace>[a-z0-9-.]+)/)?(?P<name>[a-z0-9-.]+)$`)
+	simpleNameRegexp = regexp.MustCompile(`^(?:(?P<namespace>[a-z0-9-.]+)/)?(?P<name>[a-z0-9-.]+)(?:$|[?].*$)`)
+	fullNameRegexp   = regexp.MustCompile(`^(?:(?P<apiVersion>(?:[a-z0-9-.]+/)?(?:[a-z0-9-.]+)):)?(?P<kind>[A-Za-z0-9-.]+):(?:(?P<namespace>[a-z0-9-.]+)/)?(?P<name>[a-z0-9-.]+)(?:$|[?].*$)`)
+	queryRegexp      = regexp.MustCompile(`^[^?]*[?](?P<query>.*)$`)
 
 	templates = map[string]corev1.ObjectReference{
 		"kamelet": corev1.ObjectReference{
@@ -81,6 +84,41 @@ func (c *Converter) FromString(str string) (corev1.ObjectReference, error) {
 	return ref, nil
 }
 
+func (c *Converter) PropertiesFromString(str string) (map[string]string, error) {
+	if queryRegexp.MatchString(str) {
+		groupNames := queryRegexp.SubexpNames()
+		res := make(map[string]string)
+		var query string
+		for _, match := range queryRegexp.FindAllStringSubmatch(str, -1) {
+			for idx, text := range match {
+				groupName := groupNames[idx]
+				switch groupName {
+				case "query":
+					query = text
+				}
+			}
+		}
+		parts := strings.Split(query, "&")
+		for _, part := range parts {
+			kv := strings.SplitN(part, "=", 2)
+			if len(kv) != 2 {
+				return nil, fmt.Errorf("invalid key=value format for string %q", part)
+			}
+			k, errkey := url.QueryUnescape(kv[0])
+			if errkey != nil {
+				return nil, errors.Wrapf(errkey, "cannot unescape key %q", kv[0])
+			}
+			v, errval := url.QueryUnescape(kv[1])
+			if errval != nil {
+				return nil, errors.Wrapf(errval, "cannot unescape value %q", kv[1])
+			}
+			res[k] = v
+		}
+		return res, nil
+	}
+	return nil, nil
+}
+
 func (c *Converter) expandReference(ref *corev1.ObjectReference) {
 	if template, ok := templates[ref.Kind]; ok {
 		if template.Kind != "" {
diff --git a/pkg/util/reference/reference_test.go b/pkg/util/reference/reference_test.go
index f13c5d8..ca5be0b 100644
--- a/pkg/util/reference/reference_test.go
+++ b/pkg/util/reference/reference_test.go
@@ -33,6 +33,7 @@ func TestExpressions(t *testing.T) {
 		error         bool
 		ref           corev1.ObjectReference
 		stringRef     string
+		properties    map[string]string
 	}{
 		{
 			name:  "lowercase:source",
@@ -123,6 +124,37 @@ func TestExpressions(t *testing.T) {
 			},
 			stringRef: "postgres.org/v1alpha1:PostgreSQL:ns1/db",
 		},
+		{
+			name: "postgres.org/v1alpha1:PostgreSQL:ns1/db?user=user1&password=pwd2&host=192.168.2.2&special=%201&special2=a=1",
+			ref: corev1.ObjectReference{
+				APIVersion: "postgres.org/v1alpha1",
+				Kind:       "PostgreSQL",
+				Namespace:  "ns1",
+				Name:       "db",
+			},
+			stringRef: "postgres.org/v1alpha1:PostgreSQL:ns1/db",
+			properties: map[string]string{
+				"user":     "user1",
+				"password": "pwd2",
+				"host":     "192.168.2.2",
+				"special":  " 1",
+				"special2": "a=1",
+			},
+		},
+		{
+			name: "source?a=b&b=c&d=e",
+			ref: corev1.ObjectReference{
+				Kind:       "Kamelet",
+				APIVersion: "camel.apache.org/v1alpha1",
+				Name:       "source",
+			},
+			stringRef: "camel.apache.org/v1alpha1:Kamelet:source",
+			properties: map[string]string{
+				"a": "b",
+				"b": "c",
+				"d": "e",
+			},
+		},
 	}
 
 	for i, tc := range tests {
@@ -143,6 +175,10 @@ func TestExpressions(t *testing.T) {
 				asString, err2 := converter.ToString(ref)
 				assert.NoError(t, err2)
 
+				props, err3 := converter.PropertiesFromString(tc.name)
+				assert.NoError(t, err3)
+				assert.Equal(t, tc.properties, props)
+
 				assert.NoError(t, err)
 				assert.Equal(t, tc.ref, ref)
 				assert.Equal(t, tc.stringRef, asString)