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 2020/09/17 09:09:59 UTC

[camel-k] 04/21: kamelets: initial scaffolding of kamelets trait

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 9f0c5ec4370ad61751b18a2d05886f617601f2cf
Author: Nicola Ferraro <ni...@gmail.com>
AuthorDate: Wed Jun 24 10:37:39 2020 +0200

    kamelets: initial scaffolding of kamelets trait
---
 pkg/apis/camel/v1/integration_types.go           |   9 ++
 pkg/apis/camel/v1alpha1/kamelet_types.go         |   4 +-
 pkg/apis/camel/v1alpha1/zz_generated.deepcopy.go |  34 +++-
 pkg/trait/kamelets.go                            | 195 +++++++++++++++++++++++
 pkg/trait/kamelets_test.go                       |  84 ++++++++++
 pkg/trait/trait_register.go                      |   1 +
 pkg/util/digest/digest.go                        |  44 +++++
 7 files changed, 362 insertions(+), 9 deletions(-)

diff --git a/pkg/apis/camel/v1/integration_types.go b/pkg/apis/camel/v1/integration_types.go
index 1f473d5..7e985a2 100644
--- a/pkg/apis/camel/v1/integration_types.go
+++ b/pkg/apis/camel/v1/integration_types.go
@@ -125,8 +125,17 @@ type SourceSpec struct {
 	// Interceptors are optional identifiers the org.apache.camel.k.RoutesLoader
 	// uses to pre/post process sources
 	Interceptors []string `json:"interceptors,omitempty"`
+	// Type defines the kind of source described by this object
+	Type SourceType `json:"type,omitempty"`
 }
 
+type SourceType string
+
+const (
+	SourceTypeDefault SourceType = ""
+	SourceTypeKamelet SourceType = "kamelet"
+)
+
 // Language --
 type Language string
 
diff --git a/pkg/apis/camel/v1alpha1/kamelet_types.go b/pkg/apis/camel/v1alpha1/kamelet_types.go
index 0855489..fa53584 100644
--- a/pkg/apis/camel/v1alpha1/kamelet_types.go
+++ b/pkg/apis/camel/v1alpha1/kamelet_types.go
@@ -30,8 +30,8 @@ const (
 // KameletSpec defines the desired state of Kamelet
 type KameletSpec struct {
 	Definition    JSONSchemaProps             `json:"definition,omitempty"`
-	Sources       *camelv1.SourceSpec         `json:"sources,omitempty"`
-	Flow          *camelv1.Flow               `json:"flow,omitempty"`
+	Sources       []camelv1.SourceSpec        `json:"sources,omitempty"`
+	Flow          camelv1.Flow                `json:"flow,omitempty"`
 	Authorization AuthorizationSpec           `json:"authorization,omitempty"`
 	Types         map[EventSlot]EventTypeSpec `json:"types,omitempty"`
 	Dependencies  []string                    `json:"dependencies,omitempty"`
diff --git a/pkg/apis/camel/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/camel/v1alpha1/zz_generated.deepcopy.go
index b81d3ca..2f7512c 100644
--- a/pkg/apis/camel/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/apis/camel/v1alpha1/zz_generated.deepcopy.go
@@ -26,6 +26,27 @@ func (in *AuthorizationSpec) DeepCopy() *AuthorizationSpec {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *EventTypeSpec) DeepCopyInto(out *EventTypeSpec) {
+	*out = *in
+	if in.Schema != nil {
+		in, out := &in.Schema, &out.Schema
+		*out = new(JSONSchemaProps)
+		(*in).DeepCopyInto(*out)
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EventTypeSpec.
+func (in *EventTypeSpec) DeepCopy() *EventTypeSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(EventTypeSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ExternalDocumentation) DeepCopyInto(out *ExternalDocumentation) {
 	*out = *in
 	return
@@ -448,17 +469,16 @@ func (in *KameletSpec) DeepCopyInto(out *KameletSpec) {
 	in.Definition.DeepCopyInto(&out.Definition)
 	if in.Sources != nil {
 		in, out := &in.Sources, &out.Sources
-		*out = new(v1.SourceSpec)
-		(*in).DeepCopyInto(*out)
-	}
-	if in.Flow != nil {
-		in, out := &in.Flow, &out.Flow
-		*out = (*in).DeepCopy()
+		*out = make([]v1.SourceSpec, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
 	}
+	in.Flow.DeepCopyInto(&out.Flow)
 	out.Authorization = in.Authorization
 	if in.Types != nil {
 		in, out := &in.Types, &out.Types
-		*out = make(map[string]JSONSchemaProps, len(*in))
+		*out = make(map[EventSlot]EventTypeSpec, len(*in))
 		for key, val := range *in {
 			(*out)[key] = *val.DeepCopy()
 		}
diff --git a/pkg/trait/kamelets.go b/pkg/trait/kamelets.go
new file mode 100644
index 0000000..4ba9d67
--- /dev/null
+++ b/pkg/trait/kamelets.go
@@ -0,0 +1,195 @@
+/*
+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 trait
+
+import (
+	"fmt"
+	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/metadata"
+	"github.com/apache/camel-k/pkg/util"
+	"github.com/apache/camel-k/pkg/util/digest"
+	"github.com/apache/camel-k/pkg/util/flows"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"regexp"
+	"sort"
+	"strconv"
+	"strings"
+)
+
+// The kamelets trait is a platform trait used to inject Kamelets into the integration runtime.
+//
+// +camel-k:trait=kamelets
+type kameletsTrait struct {
+	BaseTrait `property:",squash"`
+	// Automatically inject all referenced Kamelets and their default configuration (enabled by default)
+	Auto *bool `property:"auto"`
+	// Comma separated list of Kamelet names to load into the current integration
+	List string `property:"list"`
+}
+
+var (
+	kameletNameRegexp = regexp.MustCompile("kamelet:(?://)?([a-z0-9-.]+)(?:$|[^a-z0-9-.].*)")
+)
+
+func newKameletsTrait() Trait {
+	return &kameletsTrait{
+		BaseTrait: NewBaseTrait("kamelets", 450),
+	}
+}
+
+func (t *kameletsTrait) Configure(e *Environment) (bool, error) {
+	if t.Enabled != nil && !*t.Enabled {
+		return false, nil
+	}
+
+	if t.Auto == nil || *t.Auto {
+		if t.List == "" {
+			var kamelets []string
+			metadata.Each(e.CamelCatalog, e.Integration.Sources(), func(_ int, meta metadata.IntegrationMetadata) bool {
+				util.StringSliceUniqueConcat(&kamelets, extractKamelets(meta.FromURIs))
+				util.StringSliceUniqueConcat(&kamelets, extractKamelets(meta.ToURIs))
+				return true
+			})
+			sort.Strings(kamelets)
+			t.List = strings.Join(kamelets, ",")
+		}
+
+	}
+
+	return t.List != "", nil
+}
+
+func (t *kameletsTrait) Apply(e *Environment) error {
+
+	return nil
+}
+
+// IsPlatformTrait overrides base class method
+func (t *kameletsTrait) IsPlatformTrait() bool {
+	return true
+}
+
+func (t *kameletsTrait) addKameletAsSource(e *Environment, kamelet *v1alpha1.Kamelet) error {
+	var sources []v1.SourceSpec
+
+	flowData, err := flows.Marshal([]v1.Flow{kamelet.Spec.Flow})
+	if err != nil {
+		return err
+	}
+	flowSource := v1.SourceSpec{
+		DataSpec: v1.DataSpec{
+			Name:    "flow.yaml",
+			Content: string(flowData),
+		},
+		Language: v1.LanguageYaml,
+		Type:     v1.SourceTypeKamelet,
+	}
+	flowSource, err = integrationSourceFromKameletSource(e, kamelet, flowSource, fmt.Sprintf("%s-kamelet-%s-flow", e.Integration.Name, kamelet.Name))
+	if err != nil {
+		return err
+	}
+	sources = append(sources, flowSource)
+
+	for idx, s := range kamelet.Spec.Sources {
+		intSource, err := integrationSourceFromKameletSource(e, kamelet, s, fmt.Sprintf("%s-kamelet-%s-source-%03d", e.Integration.Name, kamelet.Name, idx))
+		if err != nil {
+			return err
+		}
+		sources = append(sources, intSource)
+	}
+	return nil
+}
+
+func (t *kameletsTrait) getKamelets() []string {
+	answer := make([]string, 0)
+	for _, item := range strings.Split(t.List, ",") {
+		i := strings.Trim(item, " \t\"")
+		if i != "" {
+			answer = append(answer, i)
+		}
+	}
+	return answer
+}
+
+func integrationSourceFromKameletSource(e *Environment, kamelet *v1alpha1.Kamelet, source v1.SourceSpec, name string) (v1.SourceSpec, error) {
+	if source.DataSpec.ContentRef != "" {
+		return renameSource(kamelet, source), nil
+	}
+
+	// Create configmaps to avoid storing kamelet definitions in the integration CR
+
+	// Compute the input digest and store it along with the configmap
+	hash, err := digest.ComputeForSource(source)
+	if err != nil {
+		return v1.SourceSpec{}, err
+	}
+
+	cm := corev1.ConfigMap{
+		TypeMeta: metav1.TypeMeta{
+			Kind:       "ConfigMap",
+			APIVersion: "v1",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      name,
+			Namespace: e.Integration.Namespace,
+			Labels: map[string]string{
+				"camel.apache.org/integration": e.Integration.Name,
+				"camel.apache.org/kamelet":     kamelet.Name,
+			},
+			Annotations: map[string]string{
+				"camel.apache.org/source.language":    string(source.Language),
+				"camel.apache.org/source.name":        name,
+				"camel.apache.org/source.compression": strconv.FormatBool(source.Compression),
+				"camel.apache.org/source.generated":   "true",
+				"camel.apache.org/source.type":        string(source.Type),
+				"camel.apache.org/source.digest":      hash,
+			},
+		},
+		Data: map[string]string{
+			"content": source.Content,
+		},
+	}
+
+	e.Resources.Add(&cm)
+
+	target := renameSource(kamelet, source)
+	target.Content = ""
+	target.ContentRef = name
+	target.ContentKey = "content"
+	return target, nil
+}
+
+func renameSource(kamelet *v1alpha1.Kamelet, source v1.SourceSpec) v1.SourceSpec {
+	target := source.DeepCopy()
+	if !strings.HasPrefix(target.Name, fmt.Sprintf("kamelet-%s-", kamelet.Name)) {
+		target.Name = fmt.Sprintf("kamelet-%s-%s", kamelet.Name, target.Name)
+	}
+	return *target
+}
+
+func extractKamelets(uris []string) (kamelets []string) {
+	for _, uri := range uris {
+		matches := kameletNameRegexp.FindStringSubmatch(uri)
+		if len(matches) == 2 {
+			kamelets = append(kamelets, matches[1])
+		}
+	}
+	return
+}
diff --git a/pkg/trait/kamelets_test.go b/pkg/trait/kamelets_test.go
new file mode 100644
index 0000000..b7d4fe7
--- /dev/null
+++ b/pkg/trait/kamelets_test.go
@@ -0,0 +1,84 @@
+/*
+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 trait
+
+import (
+	"context"
+	"testing"
+
+	"github.com/apache/camel-k/pkg/util/camel"
+
+	"github.com/stretchr/testify/assert"
+
+	v1 "github.com/apache/camel-k/pkg/apis/camel/v1"
+	"github.com/apache/camel-k/pkg/util/kubernetes"
+	"github.com/apache/camel-k/pkg/util/test"
+)
+
+func TestKameletsFinding(t *testing.T) {
+	trait, environment := createKameletsTestEnvironment(`
+- from:
+    uri: kamelet:c1
+    steps:
+    - to: kamelet:c2
+    - to: telegram:bots
+    - to: kamelet://c0?prop=x
+    - to: kamelet://complex-.-.-1a?prop=x&prop2
+    - to: kamelet://complex-.-.-1b
+    - to: kamelet:complex-.-.-1b
+    - to: kamelet://complex-.-.-1b/a
+    - to: kamelet://complex-.-.-1c/b
+`)
+	enabled, err := trait.Configure(environment)
+	assert.NoError(t, err)
+	assert.True(t, enabled)
+	assert.Equal(t, []string{"c0", "c1", "c2", "complex-.-.-1a", "complex-.-.-1b", "complex-.-.-1c"}, trait.getKamelets())
+}
+
+func createKameletsTestEnvironment(flow string) (*kameletsTrait, *Environment) {
+	catalog, _ := camel.DefaultCatalog()
+
+	client, _ := test.NewFakeClient()
+	trait := newKameletsTrait().(*kameletsTrait)
+	trait.Ctx = context.TODO()
+	trait.Client = client
+
+	environment := &Environment{
+		Catalog:      NewCatalog(context.TODO(), nil),
+		CamelCatalog: catalog,
+		Integration: &v1.Integration{
+			Spec: v1.IntegrationSpec{
+				Sources: []v1.SourceSpec{
+					{
+						DataSpec: v1.DataSpec{
+							Name:    "flow.yaml",
+							Content: flow,
+						},
+						Language: v1.LanguageYaml,
+					},
+				},
+			},
+			Status: v1.IntegrationStatus{
+				Phase: v1.IntegrationPhaseInitialization,
+			},
+		},
+		Resources: kubernetes.NewCollection(),
+	}
+
+	return trait, environment
+}
diff --git a/pkg/trait/trait_register.go b/pkg/trait/trait_register.go
index 993efdf..30c4356 100644
--- a/pkg/trait/trait_register.go
+++ b/pkg/trait/trait_register.go
@@ -25,6 +25,7 @@ func init() {
 	AddToTraits(newCamelTrait)
 	AddToTraits(newOpenAPITrait)
 	AddToTraits(newKnativeTrait)
+	AddToTraits(newKameletsTrait)
 	AddToTraits(newDependenciesTrait)
 	AddToTraits(newBuilderTrait)
 	AddToTraits(newQuarkusTrait)
diff --git a/pkg/util/digest/digest.go b/pkg/util/digest/digest.go
index 097d08a..6ccd3ed 100644
--- a/pkg/util/digest/digest.go
+++ b/pkg/util/digest/digest.go
@@ -184,6 +184,50 @@ func ComputeForResource(res v1.ResourceSpec) (string, error) {
 	return digest, nil
 }
 
+// ComputeForSource returns a digest for the specific source
+func ComputeForSource(s v1.SourceSpec) (string, error) {
+	hash := sha256.New()
+	// Operator version is relevant
+	if _, err := hash.Write([]byte(defaults.Version)); err != nil {
+		return "", err
+	}
+
+	if _, err := hash.Write([]byte(s.Content)); err != nil {
+		return "", err
+	}
+	if _, err := hash.Write([]byte(s.Name)); err != nil {
+		return "", err
+	}
+	if _, err := hash.Write([]byte(s.Type)); err != nil {
+		return "", err
+	}
+	if _, err := hash.Write([]byte(s.Language)); err != nil {
+		return "", err
+	}
+	if _, err := hash.Write([]byte(s.ContentKey)); err != nil {
+		return "", err
+	}
+	if _, err := hash.Write([]byte(s.ContentRef)); err != nil {
+		return "", err
+	}
+	if _, err := hash.Write([]byte(s.Loader)); err != nil {
+		return "", err
+	}
+	for _, i := range s.Interceptors {
+		if _, err := hash.Write([]byte(i)); err != nil {
+			return "", err
+		}
+	}
+
+	if _, err := hash.Write([]byte(strconv.FormatBool(s.Compression))); err != nil {
+		return "", err
+	}
+
+	// Add a letter at the beginning and use URL safe encoding
+	digest := "v" + base64.RawURLEncoding.EncodeToString(hash.Sum(nil))
+	return digest, nil
+}
+
 func sortedTraitSpecMapKeys(m map[string]v1.TraitSpec) []string {
 	res := make([]string, len(m))
 	i := 0