diff --git a/pkg/cmd/bind.go b/pkg/cmd/bind.go index 320ffb42ef..277189984b 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.=" or "sink.="`) + cmd.Flags().StringArrayP("property", "p", nil, `Add a binding property in the form of "source.=", "sink.=" or "step-.="`) 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].="`, prop) + return "", "", "", fmt.Errorf(`property %q does not follow format "[source|sink|step-].="`, prop) } keyParts := strings.SplitN(parts[0], ".", 2) if len(keyParts) != 2 { - return "", "", "", fmt.Errorf(`property key %q does not follow format "[source|sink]."`, parts[0]) + return "", "", "", fmt.Errorf(`property key %q does not follow format "[source|sink|step-]."`, 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-."`, 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 d3de5c7672..06a775b004 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 ebaddf4458..a847abbfd8 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 dc79cd59cf..5af887e061 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[a-z0-9-.]+)/)?(?P[a-z0-9-.]+)$`) - fullNameRegexp = regexp.MustCompile(`^(?:(?P(?:[a-z0-9-.]+/)?(?:[a-z0-9-.]+)):)?(?P[A-Za-z0-9-.]+):(?:(?P[a-z0-9-.]+)/)?(?P[a-z0-9-.]+)$`) + simpleNameRegexp = regexp.MustCompile(`^(?:(?P[a-z0-9-.]+)/)?(?P[a-z0-9-.]+)(?:$|[?].*$)`) + fullNameRegexp = regexp.MustCompile(`^(?:(?P(?:[a-z0-9-.]+/)?(?:[a-z0-9-.]+)):)?(?P[A-Za-z0-9-.]+):(?:(?P[a-z0-9-.]+)/)?(?P[a-z0-9-.]+)(?:$|[?].*$)`) + queryRegexp = regexp.MustCompile(`^[^?]*[?](?P.*)$`) 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 f13c5d8469..ca5be0bc75 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)