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.ferr...@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) + } + }) + } + +}