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/03/25 14:08:16 UTC

[camel-k] 01/03: Fix #2158: set a convention for object references

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 a134e488f7de828ea62cad32f02afdf438aa3ef2
Author: nicolaferraro <ni...@gmail.com>
AuthorDate: Wed Mar 24 17:03:39 2021 +0100

    Fix #2158: set a convention for object references
---
 pkg/cmd/run.go                       |   2 +-
 pkg/trait/service_binding.go         |  55 ++++++---------
 pkg/util/reference/reference.go      | 128 ++++++++++++++++++++++++++++++++++
 pkg/util/reference/reference_test.go | 131 +++++++++++++++++++++++++++++++++++
 4 files changed, 281 insertions(+), 35 deletions(-)

diff --git a/pkg/cmd/run.go b/pkg/cmd/run.go
index a77afa5..ca2c09f 100644
--- a/pkg/cmd/run.go
+++ b/pkg/cmd/run.go
@@ -73,7 +73,7 @@ func newCmdRun(rootCmdOptions *RootCmdOptions) (*cobra.Command, *runCmdOptions)
 	}
 
 	cmd.Flags().String("name", "", "The integration name")
-	cmd.Flags().StringArrayP("connect", "c", nil, "A ServiceBinding or Provisioned Service that the integration should bind to specified as KIND.VERSION.GROUP/NAME[/NAMESPACE]")
+	cmd.Flags().StringArrayP("connect", "c", nil, "A ServiceBinding or Provisioned Service that the integration should bind to, specified as [[apigroup/]version:]kind:[namespace/]name")
 	cmd.Flags().StringArrayP("dependency", "d", nil, "An external library that should be included. E.g. for Maven dependencies \"mvn:org.my/app:1.0\"")
 	cmd.Flags().BoolP("wait", "w", false, "Wait for the integration to be running")
 	cmd.Flags().StringP("kit", "k", "", "The kit used to run the integration")
diff --git a/pkg/trait/service_binding.go b/pkg/trait/service_binding.go
index d8ebb3a..10ff8ce 100644
--- a/pkg/trait/service_binding.go
+++ b/pkg/trait/service_binding.go
@@ -19,8 +19,8 @@ package trait
 
 import (
 	"fmt"
-	"strings"
 
+	"github.com/apache/camel-k/pkg/util/reference"
 	corev1 "k8s.io/api/core/v1"
 	k8serrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -38,7 +38,7 @@ import (
 // +camel-k:trait=service-binding
 type serviceBindingTrait struct {
 	BaseTrait `property:",squash"`
-	// List of Provisioned Services and ServiceBindings in the form KIND.VERSION.GROUP/NAME[/NAMESPACE]
+	// List of Provisioned Services and ServiceBindings in the form [[apigroup/]version:]kind:[namespace/]name
 	ServiceBindings []string `property:"service-bindings" json:"serviceBindings,omitempty"`
 }
 
@@ -173,33 +173,23 @@ func (t *serviceBindingTrait) getServiceBinding(e *Environment, name string) (sb
 
 func (t *serviceBindingTrait) parseProvisionedServices(e *Environment) ([]sb.Service, error) {
 	services := make([]sb.Service, 0)
+	converter := reference.NewConverter("")
 	for _, s := range t.ServiceBindings {
-		seg := strings.Split(s, "/")
-		if !(len(seg) == 3 || len(seg) == 2) {
-			return nil, fmt.Errorf("ServiceBinding: %s should be specified in the form KIND.VERSION.GROUP/NAME[/NAMESPACE]", s)
+		ref, err := converter.FromString(s)
+		if err != nil {
+			return services, err
 		}
-		gvk := seg[0]
-		index := strings.Index(gvk, ".")
-		kind := seg[0][0:index]
-		if kind == "ServiceBinding" {
-			continue
-		}
-		vg := seg[0][index+1 : len(gvk)]
-		index = strings.Index(vg, ".")
-		version := vg[0:index]
-		group := vg[index+1 : len(vg)]
-		name := seg[1]
 		namespace := e.Integration.Namespace
-		if len(seg) == 3 {
-			namespace = seg[2]
+		if ref.Namespace != "" {
+			namespace = ref.Namespace
 		}
 		service := sb.Service{
 			NamespacedRef: sb.NamespacedRef{
 				Ref: sb.Ref{
-					Group:   group,
-					Version: version,
-					Kind:    kind,
-					Name:    name,
+					Group:   ref.GroupVersionKind().Group,
+					Version: ref.GroupVersionKind().Version,
+					Kind:    ref.Kind,
+					Name:    ref.Name,
 				},
 				Namespace: &namespace,
 			},
@@ -211,23 +201,20 @@ func (t *serviceBindingTrait) parseProvisionedServices(e *Environment) ([]sb.Ser
 
 func (t *serviceBindingTrait) parseServiceBindings(e *Environment) ([]string, error) {
 	serviceBindings := make([]string, 0)
+	converter := reference.NewConverter("")
 	for _, s := range t.ServiceBindings {
-		seg := strings.Split(s, "/")
-		if !(len(seg) == 3 || len(seg) == 2) {
-			return nil, fmt.Errorf("ServiceBinding: %s should be specified in the form KIND.VERSION.GROUP/NAME[/NAMESPACE]", s)
+		ref, err := converter.FromString(s)
+		if err != nil {
+			return serviceBindings, err
 		}
-		gvk := seg[0]
-		index := strings.Index(gvk, ".")
-		kind := seg[0][0:index]
-		if kind == "ServiceBinding" {
-			vg := seg[0][index+1 : len(gvk)]
-			if vg != "v1alpha1.binding.operators.coreos.com" {
-				return nil, fmt.Errorf("ServiceBinding: %s VERSION.GROUP should be v1alpha1.binding.operators.coreos.com", s)
+		if ref.Kind == "ServiceBinding" {
+			if ref.GroupVersionKind().String() != sb.GroupVersion.String() {
+				return nil, fmt.Errorf("ServiceBinding: %q api version should be %q", s, sb.GroupVersion.String())
 			}
-			if len(seg) == 3 && seg[2] != e.Integration.Namespace {
+			if ref.Namespace != e.Integration.Namespace {
 				return nil, fmt.Errorf("ServiceBinding: %s should be in the same namespace %s as the integration", s, e.Integration.Namespace)
 			}
-			serviceBindings = append(serviceBindings, seg[1])
+			serviceBindings = append(serviceBindings, ref.Name)
 		}
 	}
 	return serviceBindings, nil
diff --git a/pkg/util/reference/reference.go b/pkg/util/reference/reference.go
new file mode 100644
index 0000000..0458a89
--- /dev/null
+++ b/pkg/util/reference/reference.go
@@ -0,0 +1,128 @@
+/*
+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 reference
+
+import (
+	"fmt"
+	"regexp"
+	"unicode"
+
+	camelv1alpha1 "github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
+	corev1 "k8s.io/api/core/v1"
+	eventingv1 "knative.dev/eventing/pkg/apis/eventing/v1"
+	messagingv1 "knative.dev/eventing/pkg/apis/messaging/v1"
+	servingv1 "knative.dev/serving/pkg/apis/serving/v1"
+)
+
+const (
+	KameletPrefix = "kamelet:"
+)
+
+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-.]+)$`)
+
+	templates = map[string]corev1.ObjectReference{
+		"kamelet": corev1.ObjectReference{
+			Kind:       "Kamelet",
+			APIVersion: camelv1alpha1.SchemeGroupVersion.String(),
+		},
+		"channel": corev1.ObjectReference{
+			Kind:       "Channel",
+			APIVersion: messagingv1.SchemeGroupVersion.String(),
+		},
+		"broker": corev1.ObjectReference{
+			Kind:       "Broker",
+			APIVersion: eventingv1.SchemeGroupVersion.String(),
+		},
+		"ksvc": corev1.ObjectReference{
+			Kind:       "Service",
+			APIVersion: servingv1.SchemeGroupVersion.String(),
+		},
+	}
+)
+
+type Converter struct {
+	defaultPrefix string
+}
+
+func NewConverter(defaultPrefix string) *Converter {
+	return &Converter{
+		defaultPrefix: defaultPrefix,
+	}
+}
+
+func (c *Converter) FromString(str string) (corev1.ObjectReference, error) {
+	ref, err := c.simpleDecodeString(str)
+	if err != nil {
+		return ref, err
+	}
+	c.expandReference(&ref)
+
+	if ref.Kind == "" || !unicode.IsUpper([]rune(ref.Kind)[0]) {
+		return corev1.ObjectReference{}, fmt.Errorf("invalid kind: %q", ref.Kind)
+	}
+	return ref, nil
+}
+
+func (c *Converter) expandReference(ref *corev1.ObjectReference) {
+	if template, ok := templates[ref.Kind]; ok {
+		if template.Kind != "" {
+			ref.Kind = template.Kind
+		}
+		if ref.APIVersion == "" && template.APIVersion != "" {
+			ref.APIVersion = template.APIVersion
+		}
+	}
+}
+
+func (c *Converter) simpleDecodeString(str string) (corev1.ObjectReference, error) {
+	fullName := str
+	if simpleNameRegexp.MatchString(str) {
+		fullName = c.defaultPrefix + str
+	}
+
+	if fullNameRegexp.MatchString(fullName) {
+		groupNames := fullNameRegexp.SubexpNames()
+		ref := corev1.ObjectReference{}
+		for _, match := range fullNameRegexp.FindAllStringSubmatch(fullName, -1) {
+			for idx, text := range match {
+				groupName := groupNames[idx]
+				switch groupName {
+				case "apiVersion":
+					ref.APIVersion = text
+				case "namespace":
+					ref.Namespace = text
+				case "kind":
+					ref.Kind = text
+				case "name":
+					ref.Name = text
+				}
+			}
+		}
+		return ref, nil
+	}
+	if c.defaultPrefix != "" {
+		return corev1.ObjectReference{}, fmt.Errorf(`name %q does not match either "[[apigroup/]version:]kind:[namespace/]name" or "[namespace/]name"`, str)
+	}
+	return corev1.ObjectReference{}, fmt.Errorf(`name %q does not match format "[[apigroup/]version:]kind:[namespace/]name"`, str)
+}
+
+func (c *Converter) ToString(ref corev1.ObjectReference) (string, error) {
+	return "", nil
+}
diff --git a/pkg/util/reference/reference_test.go b/pkg/util/reference/reference_test.go
new file mode 100644
index 0000000..5539602
--- /dev/null
+++ b/pkg/util/reference/reference_test.go
@@ -0,0 +1,131 @@
+/*
+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 reference
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	corev1 "k8s.io/api/core/v1"
+)
+
+func TestExpressions(t *testing.T) {
+	emptyPrefix := ""
+	tests := []struct {
+		defaultPrefix *string
+		name          string
+		error         bool
+		ref           corev1.ObjectReference
+	}{
+		{
+			name:  "lowercase:source",
+			error: true,
+		},
+		{
+			name:  "PostgreSQL/ns/name",
+			error: true,
+		},
+		{
+			defaultPrefix: &emptyPrefix,
+			name:          "source",
+			error:         true,
+		},
+		{
+			name: "source",
+			ref: corev1.ObjectReference{
+				Kind:       "Kamelet",
+				APIVersion: "camel.apache.org/v1alpha1",
+				Name:       "source",
+			},
+		},
+		{
+			name: "ns1/source",
+			ref: corev1.ObjectReference{
+				Kind:       "Kamelet",
+				APIVersion: "camel.apache.org/v1alpha1",
+				Namespace:  "ns1",
+				Name:       "source",
+			},
+		},
+		{
+			name: "ksvc:service",
+			ref: corev1.ObjectReference{
+				Kind:       "Service",
+				APIVersion: "serving.knative.dev/v1",
+				Name:       "service",
+			},
+		},
+		{
+			name: "channel:ns3/ch2",
+			ref: corev1.ObjectReference{
+				Kind:       "Channel",
+				APIVersion: "messaging.knative.dev/v1",
+				Namespace:  "ns3",
+				Name:       "ch2",
+			},
+		},
+		{
+			name: "broker:default",
+			ref: corev1.ObjectReference{
+				Kind:       "Broker",
+				APIVersion: "eventing.knative.dev/v1",
+				Name:       "default",
+			},
+		},
+		{
+			name: "PostgreSQL:ns1/db",
+			ref: corev1.ObjectReference{
+				Kind:      "PostgreSQL",
+				Namespace: "ns1",
+				Name:      "db",
+			},
+		},
+		{
+			name: "postgres.org/v1alpha1:PostgreSQL:ns1/db",
+			ref: corev1.ObjectReference{
+				APIVersion: "postgres.org/v1alpha1",
+				Kind:       "PostgreSQL",
+				Namespace:  "ns1",
+				Name:       "db",
+			},
+		},
+	}
+
+	for i, tc := range tests {
+		t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) {
+
+			var converter *Converter
+			if tc.defaultPrefix != nil {
+				converter = NewConverter(*tc.defaultPrefix)
+			} else {
+				// Using kamelet: prefix by default in the tests
+				converter = NewConverter(KameletPrefix)
+			}
+
+			ref, err := converter.FromString(tc.name)
+			if tc.error {
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+				assert.Equal(t, tc.ref, ref)
+			}
+		})
+	}
+
+}