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 <[email protected]> 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
