diff --git a/addons/keda/keda_test.go b/addons/keda/keda_test.go index b017df262a..4b4be7d7e5 100644 --- a/addons/keda/keda_test.go +++ b/addons/keda/keda_test.go @@ -129,21 +129,23 @@ func TestKameletAutoDetection(t *testing.T) { }, }, Spec: camelv1.KameletSpec{ - Definition: &camelv1.JSONSchemaProps{ - Properties: map[string]camelv1.JSONSchemaProp{ - "a": { - XDescriptors: []string{ - "urn:keda:metadata:a", + KameletSpecBase: camelv1.KameletSpecBase{ + Definition: &camelv1.JSONSchemaProps{ + Properties: map[string]camelv1.JSONSchemaProp{ + "a": { + XDescriptors: []string{ + "urn:keda:metadata:a", + }, }, - }, - "b": { - XDescriptors: []string{ - "urn:keda:metadata:bb", + "b": { + XDescriptors: []string{ + "urn:keda:metadata:bb", + }, }, - }, - "c": { - XDescriptors: []string{ - "urn:keda:authentication:cc", + "c": { + XDescriptors: []string{ + "urn:keda:authentication:cc", + }, }, }, }, @@ -245,21 +247,23 @@ func TestPipeAutoDetection(t *testing.T) { }, }, Spec: camelv1.KameletSpec{ - Definition: &camelv1.JSONSchemaProps{ - Properties: map[string]camelv1.JSONSchemaProp{ - "a": { - XDescriptors: []string{ - "urn:keda:metadata:a", + KameletSpecBase: camelv1.KameletSpecBase{ + Definition: &camelv1.JSONSchemaProps{ + Properties: map[string]camelv1.JSONSchemaProp{ + "a": { + XDescriptors: []string{ + "urn:keda:metadata:a", + }, }, - }, - "b": { - XDescriptors: []string{ - "urn:keda:metadata:bb", + "b": { + XDescriptors: []string{ + "urn:keda:metadata:bb", + }, }, - }, - "c": { - XDescriptors: []string{ - "urn:keda:authentication:cc", + "c": { + XDescriptors: []string{ + "urn:keda:authentication:cc", + }, }, }, }, diff --git a/docs/modules/ROOT/pages/kamelets/kamelets-dev.adoc b/docs/modules/ROOT/pages/kamelets/kamelets-dev.adoc index ab5e533e62..c617c47806 100644 --- a/docs/modules/ROOT/pages/kamelets/kamelets-dev.adoc +++ b/docs/modules/ROOT/pages/kamelets/kamelets-dev.adoc @@ -331,6 +331,52 @@ If everything goes right, you should get some tweets in the logs after the integ Refer to the xref:kamelets/kamelets-user.adoc[Kamelets User Guide] for more information on how to use it in different contexts (like Knative, Kafka, etc.). +== Kamelet versions + +The catalog containing a set of Kamelets is generally developed in order to be used with a given Camel version (see the Apache Camel Kamelets catalog). However, when publishing the Kamelet to the cluster you may want to maintain more than one version for any reason (ie, to use a different dependency and be able to support multiple runtimes). You can therefore use the `.spec.versions` parameter to optionally maintain a set of alternative versions beside the main (and default) one. + +.my-timer-source.yaml +[source,yaml] +---- +apiVersion: camel.apache.org/v1 +kind: Kamelet +metadata: + name: my-timer-source +spec: + definition: + title: "Timer Example" + types: + out: + mediaType: text/plain + template: + from: + uri: timer:tick + steps: + - setBody: + constant: "Kamelet Main" + - to: "kamelet:sink" + versions: + v2: + definition: + title: "Timer Example 2" + types: + out: + mediaType: text/plain + template: + from: + uri: timer:tick + steps: + - setBody: + constant: "Kamelet V2" + - to: "kamelet:sink" +---- + +NOTE: make sure the overall content fits into 1 MiB, which is the storage limit for a Custom Resource. + +This is a way to handle multiple version on Kubernetes and may not be supported out of the box by Camel core. If the Integration will require specifically to use `kamelet:my-timer-source?kameletVersion=v2`, then, the operator will mount properly the specification on the running application. + +The `.spec.versions` field may not be necessarily supported by the core as it's meant to provide a way to handle versioning on the cluster only. The runtime must be provided with a materialized Kamelet file with the chosen spec (the operator is in charge of that). + == Kamelet data types A Kamelet usually encapsulates a specific functionality and serves a very opinionated use case with well-defined input parameters and outcome. diff --git a/docs/modules/ROOT/pages/kamelets/kamelets-user.adoc b/docs/modules/ROOT/pages/kamelets/kamelets-user.adoc index ae27db0230..d39814ced0 100644 --- a/docs/modules/ROOT/pages/kamelets/kamelets-user.adoc +++ b/docs/modules/ROOT/pages/kamelets/kamelets-user.adoc @@ -218,6 +218,21 @@ You can now write an integration that uses the Kamelet with the named configurat You can run this integration without specifying other parameters, the Kamelet endpoint will be implicitly configured by the Camel K operator that will automatically mount the secret into the integration Pod. +=== Kamelet versioning + +Kamelets provided in a catalog are generally meant to work with a given runtime version (the same for which they are released). However, when you create a Kamelet and publish to a cluster, you may want to store and use different versions. If the Kamelet is provided with more than the `main` version, then, you can specify which version to use in your Integration by adding the version parameter. For instance: + +[source,yaml] +.kamlet-namedconfig-route.yaml +---- +- from: + uri: "timer:tick?kameletVersion=v2" + steps: + - to: "log:info" +---- + +The operator will be able to automatically pick the right version and use it at runtime. If no version is specified, then you will use the default one. + [[kamelets-usage-binding]] == Binding Kamelets in Pipes diff --git a/docs/modules/ROOT/partials/apis/camel-k-crds.adoc b/docs/modules/ROOT/partials/apis/camel-k-crds.adoc index 3b4e6e41b0..4761b27477 100644 --- a/docs/modules/ROOT/partials/apis/camel-k-crds.adoc +++ b/docs/modules/ROOT/partials/apis/camel-k-crds.adoc @@ -1756,7 +1756,7 @@ the expected schema for the data type *Appears on:* -* <<#_camel_apache_org_v1_KameletSpec, KameletSpec>> +* <<#_camel_apache_org_v1_KameletSpecBase, KameletSpecBase>> DataTypesSpec represents the specification for a set of data types. @@ -2005,7 +2005,7 @@ ErrorHandlerType a type of error handler (ie, sink). *Appears on:* -* <<#_camel_apache_org_v1_KameletSpec, KameletSpec>> +* <<#_camel_apache_org_v1_KameletSpecBase, KameletSpecBase>> EventTypeSpec represents a specification for an event type. Deprecated: In favor of using DataTypeSpec. @@ -3864,7 +3864,7 @@ XDescriptors is a list of extended properties that trigger a custom behavior in * <<#_camel_apache_org_v1_DataTypeSpec, DataTypeSpec>> * <<#_camel_apache_org_v1_EventTypeSpec, EventTypeSpec>> -* <<#_camel_apache_org_v1_KameletSpec, KameletSpec>> +* <<#_camel_apache_org_v1_KameletSpecBase, KameletSpecBase>> JSONSchemaProps is a JSON-Schema following Specification Draft 4 (http://json-schema.org/). @@ -4129,6 +4129,39 @@ KameletSpec specifies the configuration required to execute a Kamelet. |Field |Description +|`KameletSpecBase` + +*xref:#_camel_apache_org_v1_KameletSpecBase[KameletSpecBase]* +|(Members of `KameletSpecBase` are embedded into this type.) + + + + +|`versions` + +*xref:#_camel_apache_org_v1_KameletSpecBase[map[string\]github.com/apache/camel-k/v2/pkg/apis/camel/v1.KameletSpecBase]* +| + + +the optional versions available for this Kamelet. This field may not be taken in account by Camel core and is meant to support +any user defined versioning model on cluster only. If the user wants to use any given version, she must materialize a file with the given version spec +as the `main` Kamelet spec on the runtime. + + +|=== + +[#_camel_apache_org_v1_KameletSpecBase] +=== KameletSpecBase + +*Appears on:* + +* <<#_camel_apache_org_v1_KameletSpec, KameletSpec>> + +KameletSpecBase specifies the base configuration of a Kamelet. + +[cols="2,2a",options="header"] +|=== +|Field +|Description + |`definition` + *xref:#_camel_apache_org_v1_JSONSchemaProps[JSONSchemaProps]* | @@ -5474,7 +5507,7 @@ string * <<#_camel_apache_org_v1_IntegrationKitSpec, IntegrationKitSpec>> * <<#_camel_apache_org_v1_IntegrationSpec, IntegrationSpec>> * <<#_camel_apache_org_v1_IntegrationStatus, IntegrationStatus>> -* <<#_camel_apache_org_v1_KameletSpec, KameletSpec>> +* <<#_camel_apache_org_v1_KameletSpecBase, KameletSpecBase>> SourceSpec defines the configuration for one or more routes to be executed in a certain Camel DSL language. @@ -5687,7 +5720,7 @@ a JibTask, for Jib strategy *Appears on:* -* <<#_camel_apache_org_v1_KameletSpec, KameletSpec>> +* <<#_camel_apache_org_v1_KameletSpecBase, KameletSpecBase>> Template is an unstructured object representing a Kamelet template in YAML/JSON DSL. diff --git a/e2e/common/traits/files/.camel-jbang/camel-jbang-run.properties b/e2e/common/traits/files/.camel-jbang/camel-jbang-run.properties new file mode 100644 index 0000000000..8b86781dca --- /dev/null +++ b/e2e/common/traits/files/.camel-jbang/camel-jbang-run.properties @@ -0,0 +1,25 @@ +loggingLevel=info +loggingColor=true +loggingJson=false +camel.jbang.localKameletDir=file\:. +camel.main.modeline=true +camel.jbang.compileWorkDir=.camel-jbang/compile +camel.jbang.health=false +camel.jbang.metrics=false +camel.jbang.console=false +camel.jbang.verbose=false +camel.jbang.camel-version=4.7.0 +camel.jbang.springBootVersion=3.3.1 +camel.jbang.quarkusVersion=3.12.0 +camel.jbang.kameletsVersion=4.7.0 +camel.main.name=kamelet-it-v1 +camel.main.routesIncludePattern=file\:kamelet-it-v1.yaml +dependency=mvn\:org.apache.camel\:camel-kamelet\:4.7.0 +dependency=mvn\:org.apache.camel\:camel-yaml-dsl\:4.7.0 +dependency=mvn\:org.apache.camel\:camel-core-languages\:4.7.0 +dependency=mvn\:org.apache.camel\:camel-rest\:4.7.0 +dependency=mvn\:org.apache.camel\:camel-rest-openapi\:4.7.0 +dependency=mvn\:org.apache.camel\:camel-platform-http\:4.7.0 +camel.jbang.platform-http.port=8080 +kamelet=my-timer-source +dependency=mvn\:org.apache.camel\:camel-timer\:4.7.0 diff --git a/e2e/common/traits/files/kamelet-it-main.yaml b/e2e/common/traits/files/kamelet-it-main.yaml new file mode 100644 index 0000000000..6dc2d08f2e --- /dev/null +++ b/e2e/common/traits/files/kamelet-it-main.yaml @@ -0,0 +1,23 @@ +# camel-k: language=yaml + +# --------------------------------------------------------------------------- +# 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. +# --------------------------------------------------------------------------- + +- from: + uri: "kamelet:my-timer-source" + steps: + - log: "${body}" \ No newline at end of file diff --git a/e2e/common/traits/files/kamelet-it-v1.yaml b/e2e/common/traits/files/kamelet-it-v1.yaml new file mode 100644 index 0000000000..a9a824ce9a --- /dev/null +++ b/e2e/common/traits/files/kamelet-it-v1.yaml @@ -0,0 +1,23 @@ +# camel-k: language=yaml + +# --------------------------------------------------------------------------- +# 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. +# --------------------------------------------------------------------------- + +- from: + uri: "kamelet:my-timer-source?kameletVersion=v1" + steps: + - log: "${body}" \ No newline at end of file diff --git a/e2e/common/traits/files/my-timer-source.kamelet.yaml b/e2e/common/traits/files/my-timer-source.kamelet.yaml new file mode 100644 index 0000000000..c389d36487 --- /dev/null +++ b/e2e/common/traits/files/my-timer-source.kamelet.yaml @@ -0,0 +1,66 @@ +# --------------------------------------------------------------------------- +# 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. +# --------------------------------------------------------------------------- + +apiVersion: camel.apache.org/v1 +kind: Kamelet +metadata: + name: my-timer-source + labels: + camel.apache.org/kamelet.type: "source" +spec: + definition: + title: "Timer Example" + description: "Produces periodic events with a custom payload" + types: + out: + mediaType: text/plain + template: + from: + uri: timer:tick + steps: + - setBody: + constant: "Kamelet Main" + - to: "kamelet:sink" + versions: + v1: + definition: + title: "Timer Example 1" + description: "Produces periodic events with a custom payload" + types: + out: + mediaType: text/plain + template: + from: + uri: timer:tick + steps: + - setBody: + constant: "Kamelet V1" + - to: "kamelet:sink" + v2: + definition: + title: "Timer Example 2" + description: "Produces periodic events with a custom payload" + types: + out: + mediaType: text/plain + template: + from: + uri: timer:tick + steps: + - setBody: + constant: "Kamelet V2" + - to: "kamelet:sink" diff --git a/e2e/common/traits/kamelet_test.go b/e2e/common/traits/kamelet_test.go index 72f636b8dd..257566471d 100644 --- a/e2e/common/traits/kamelet_test.go +++ b/e2e/common/traits/kamelet_test.go @@ -34,7 +34,7 @@ import ( v1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1" ) -func TestKameletTrait(t *testing.T) { +func TestKameletDiscoverCapabilities(t *testing.T) { t.Parallel() WithNewTestNamespace(t, func(ctx context.Context, g *WithT, ns string) { t.Run("discover kamelet capabilities", func(t *testing.T) { @@ -52,8 +52,8 @@ func TestKameletTrait(t *testing.T) { name := RandomizedSuffixName("webhook") g.Expect(KamelRun(t, ctx, ns, "files/webhook.yaml", "--name", name).Execute()).To(Succeed()) - g.Eventually(IntegrationPodPhase(t, ctx, ns, name), TestTimeoutLong).Should(Equal(corev1.PodRunning)) - g.Eventually(IntegrationConditionStatus(t, ctx, ns, name, v1.IntegrationConditionReady), TestTimeoutShort).Should(Equal(corev1.ConditionTrue)) + g.Eventually(IntegrationConditionStatus(t, ctx, ns, name, v1.IntegrationConditionReady), TestTimeoutMedium).Should(Equal(corev1.ConditionTrue)) + g.Eventually(IntegrationPodPhase(t, ctx, ns, name), TestTimeoutShort).Should(Equal(corev1.PodRunning)) g.Eventually(IntegrationLogs(t, ctx, ns, name), TestTimeoutShort).Should(ContainSubstring("Started route1 (kamelet://capabilities-webhook-source)")) // Verify Integration capabilities g.Eventually(IntegrationStatusCapabilities(t, ctx, ns, name), TestTimeoutShort).Should(ContainElements("platform-http")) @@ -63,3 +63,22 @@ func TestKameletTrait(t *testing.T) { }) }) } + +func TestKameletMultiVersions(t *testing.T) { + t.Parallel() + WithNewTestNamespace(t, func(ctx context.Context, g *WithT, ns string) { + t.Run("multiple kamelet versions", func(t *testing.T) { + ExpectExecSucceed(t, g, Kubectl("apply", "-f", "files/my-timer-source.kamelet.yaml", "-n", ns)) + name := RandomizedSuffixName("multiversions") + g.Expect(KamelRun(t, ctx, ns, "files/kamelet-it-main.yaml", "--name", name).Execute()).To(Succeed()) + g.Eventually(IntegrationConditionStatus(t, ctx, ns, name, v1.IntegrationConditionReady), TestTimeoutMedium).Should(Equal(corev1.ConditionTrue)) + g.Eventually(IntegrationPodPhase(t, ctx, ns, name), TestTimeoutShort).Should(Equal(corev1.PodRunning)) + g.Eventually(IntegrationLogs(t, ctx, ns, name), TestTimeoutShort).Should(ContainSubstring("Kamelet Main")) + // Switch to Kamelet V1 + g.Expect(KamelRun(t, ctx, ns, "files/kamelet-it-v1.yaml", "--name", name).Execute()).To(Succeed()) + g.Eventually(IntegrationConditionStatus(t, ctx, ns, name, v1.IntegrationConditionReady), TestTimeoutMedium).Should(Equal(corev1.ConditionTrue)) + g.Eventually(IntegrationPodPhase(t, ctx, ns, name), TestTimeoutShort).Should(Equal(corev1.PodRunning)) + g.Eventually(IntegrationLogs(t, ctx, ns, name), TestTimeoutShort).Should(ContainSubstring("Kamelet V1")) + }) + }) +} diff --git a/e2e/support/test_support.go b/e2e/support/test_support.go index 48cab1b691..ba8d6e9f05 100644 --- a/e2e/support/test_support.go +++ b/e2e/support/test_support.go @@ -2666,10 +2666,12 @@ func CreateKameletWithID(t *testing.T, operatorID string, ctx context.Context, n Labels: labels, }, Spec: v1.KameletSpec{ - Definition: &v1.JSONSchemaProps{ - Properties: properties, + KameletSpecBase: v1.KameletSpecBase{ + Definition: &v1.JSONSchemaProps{ + Properties: properties, + }, + Template: asTemplate(t, template), }, - Template: asTemplate(t, template), }, } diff --git a/helm/camel-k/crds/camel-k-crds.yaml b/helm/camel-k/crds/camel-k-crds.yaml index c3a49865e0..632193ad51 100644 --- a/helm/camel-k/crds/camel-k-crds.yaml +++ b/helm/camel-k/crds/camel-k-crds.yaml @@ -29257,6 +29257,599 @@ spec: data specification types for the events consumed/produced by the Kamelet Deprecated: In favor of using DataTypes type: object + versions: + additionalProperties: + description: KameletSpecBase specifies the base configuration of + a Kamelet. + properties: + dataTypes: + additionalProperties: + description: DataTypesSpec represents the specification for + a set of data types. + properties: + default: + description: the default data type for this Kamelet + type: string + headers: + additionalProperties: + description: HeaderSpec represents the specification + for a header used in the Kamelet. + properties: + default: + type: string + description: + type: string + required: + type: boolean + title: + type: string + type: + type: string + type: object + description: one to many header specifications + type: object + types: + additionalProperties: + description: DataTypeSpec represents the specification + for a data type. + properties: + dependencies: + description: the list of Camel or Maven dependencies + required by the data type + items: + type: string + type: array + description: + description: optional description + type: string + format: + description: the data type format name + type: string + headers: + additionalProperties: + description: HeaderSpec represents the specification + for a header used in the Kamelet. + properties: + default: + type: string + description: + type: string + required: + type: boolean + title: + type: string + type: + type: string + type: object + description: one to many header specifications + type: object + mediaType: + description: media type as expected for HTTP media + types (ie, application/json) + type: string + schema: + description: the expected schema for the data type + properties: + $schema: + description: JSONSchemaURL represents a schema + url. + type: string + description: + type: string + example: + description: |- + JSON represents any valid JSON value. + These types are supported: bool, int64, float64, string, []interface{}, map[string]interface{} and nil. + x-kubernetes-preserve-unknown-fields: true + externalDocs: + description: ExternalDocumentation allows referencing + an external resource for extended documentation. + properties: + description: + type: string + url: + type: string + type: object + id: + type: string + properties: + additionalProperties: + properties: + default: + description: default is a default value + for undefined object fields. + x-kubernetes-preserve-unknown-fields: true + deprecated: + type: boolean + description: + type: string + enum: + items: + description: |- + JSON represents any valid JSON value. + These types are supported: bool, int64, float64, string, []interface{}, map[string]interface{} and nil. + x-kubernetes-preserve-unknown-fields: true + type: array + example: + description: |- + JSON represents any valid JSON value. + These types are supported: bool, int64, float64, string, []interface{}, map[string]interface{} and nil. + x-kubernetes-preserve-unknown-fields: true + exclusiveMaximum: + type: boolean + exclusiveMinimum: + type: boolean + format: + description: |- + format is an OpenAPI v3 format string. Unknown formats are ignored. The following formats are validated: + + + - bsonobjectid: a bson object ID, i.e. a 24 characters hex string + - uri: an URI as parsed by Golang net/url.ParseRequestURI + - email: an email address as parsed by Golang net/mail.ParseAddress + - hostname: a valid representation for an Internet host name, as defined by RFC 1034, section 3.1 [RFC1034]. + - ipv4: an IPv4 IP as parsed by Golang net.ParseIP + - ipv6: an IPv6 IP as parsed by Golang net.ParseIP + - cidr: a CIDR as parsed by Golang net.ParseCIDR + - mac: a MAC address as parsed by Golang net.ParseMAC + - uuid: an UUID that allows uppercase defined by the regex (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}$ + - uuid3: an UUID3 that allows uppercase defined by the regex (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?3[0-9a-f]{3}-?[0-9a-f]{4}-?[0-9a-f]{12}$ + - uuid4: an UUID4 that allows uppercase defined by the regex (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?4[0-9a-f]{3}-?[89ab][0-9a-f]{3}-?[0-9a-f]{12}$ + - uuid5: an UUID5 that allows uppercase defined by the regex (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?5[0-9a-f]{3}-?[89ab][0-9a-f]{3}-?[0-9a-f]{12}$ + - isbn: an ISBN10 or ISBN13 number string like "0321751043" or "978-0321751041" + - isbn10: an ISBN10 number string like "0321751043" + - isbn13: an ISBN13 number string like "978-0321751041" + - creditcard: a credit card number defined by the regex ^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\\d{3})\\d{11})$ with any non digit characters mixed in + - ssn: a U.S. social security number following the regex ^\\d{3}[- ]?\\d{2}[- ]?\\d{4}$ + - hexcolor: an hexadecimal color code like "#FFFFFF" following the regex ^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$ + - rgbcolor: an RGB color code like rgb like "rgb(255,255,255)" + - byte: base64 encoded binary data + - password: any kind of string + - date: a date string like "2006-01-02" as defined by full-date in RFC3339 + - duration: a duration string like "22 ns" as parsed by Golang time.ParseDuration or compatible with Scala duration format + - datetime: a date time string like "2014-12-15T19:30:20.000Z" as defined by date-time in RFC3339. + type: string + id: + type: string + maxItems: + format: int64 + type: integer + maxLength: + format: int64 + type: integer + maxProperties: + format: int64 + type: integer + maximum: + description: A Number represents a JSON + number literal. + type: string + minItems: + format: int64 + type: integer + minLength: + format: int64 + type: integer + minProperties: + format: int64 + type: integer + minimum: + description: A Number represents a JSON + number literal. + type: string + multipleOf: + description: A Number represents a JSON + number literal. + type: string + nullable: + type: boolean + pattern: + type: string + title: + type: string + type: + type: string + uniqueItems: + type: boolean + x-descriptors: + description: XDescriptors is a list of + extended properties that trigger a custom + behavior in external systems + items: + type: string + type: array + type: object + type: object + required: + items: + type: string + type: array + title: + type: string + type: + type: string + type: object + scheme: + description: the data type component scheme + type: string + type: object + description: one to many data type specifications + type: object + type: object + description: data specification types for the events consumed/produced + by the Kamelet + type: object + definition: + description: defines the formal configuration of the Kamelet + properties: + $schema: + description: JSONSchemaURL represents a schema url. + type: string + description: + type: string + example: + description: |- + JSON represents any valid JSON value. + These types are supported: bool, int64, float64, string, []interface{}, map[string]interface{} and nil. + x-kubernetes-preserve-unknown-fields: true + externalDocs: + description: ExternalDocumentation allows referencing an + external resource for extended documentation. + properties: + description: + type: string + url: + type: string + type: object + id: + type: string + properties: + additionalProperties: + properties: + default: + description: default is a default value for undefined + object fields. + x-kubernetes-preserve-unknown-fields: true + deprecated: + type: boolean + description: + type: string + enum: + items: + description: |- + JSON represents any valid JSON value. + These types are supported: bool, int64, float64, string, []interface{}, map[string]interface{} and nil. + x-kubernetes-preserve-unknown-fields: true + type: array + example: + description: |- + JSON represents any valid JSON value. + These types are supported: bool, int64, float64, string, []interface{}, map[string]interface{} and nil. + x-kubernetes-preserve-unknown-fields: true + exclusiveMaximum: + type: boolean + exclusiveMinimum: + type: boolean + format: + description: |- + format is an OpenAPI v3 format string. Unknown formats are ignored. The following formats are validated: + + + - bsonobjectid: a bson object ID, i.e. a 24 characters hex string + - uri: an URI as parsed by Golang net/url.ParseRequestURI + - email: an email address as parsed by Golang net/mail.ParseAddress + - hostname: a valid representation for an Internet host name, as defined by RFC 1034, section 3.1 [RFC1034]. + - ipv4: an IPv4 IP as parsed by Golang net.ParseIP + - ipv6: an IPv6 IP as parsed by Golang net.ParseIP + - cidr: a CIDR as parsed by Golang net.ParseCIDR + - mac: a MAC address as parsed by Golang net.ParseMAC + - uuid: an UUID that allows uppercase defined by the regex (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}$ + - uuid3: an UUID3 that allows uppercase defined by the regex (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?3[0-9a-f]{3}-?[0-9a-f]{4}-?[0-9a-f]{12}$ + - uuid4: an UUID4 that allows uppercase defined by the regex (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?4[0-9a-f]{3}-?[89ab][0-9a-f]{3}-?[0-9a-f]{12}$ + - uuid5: an UUID5 that allows uppercase defined by the regex (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?5[0-9a-f]{3}-?[89ab][0-9a-f]{3}-?[0-9a-f]{12}$ + - isbn: an ISBN10 or ISBN13 number string like "0321751043" or "978-0321751041" + - isbn10: an ISBN10 number string like "0321751043" + - isbn13: an ISBN13 number string like "978-0321751041" + - creditcard: a credit card number defined by the regex ^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\\d{3})\\d{11})$ with any non digit characters mixed in + - ssn: a U.S. social security number following the regex ^\\d{3}[- ]?\\d{2}[- ]?\\d{4}$ + - hexcolor: an hexadecimal color code like "#FFFFFF" following the regex ^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$ + - rgbcolor: an RGB color code like rgb like "rgb(255,255,255)" + - byte: base64 encoded binary data + - password: any kind of string + - date: a date string like "2006-01-02" as defined by full-date in RFC3339 + - duration: a duration string like "22 ns" as parsed by Golang time.ParseDuration or compatible with Scala duration format + - datetime: a date time string like "2014-12-15T19:30:20.000Z" as defined by date-time in RFC3339. + type: string + id: + type: string + maxItems: + format: int64 + type: integer + maxLength: + format: int64 + type: integer + maxProperties: + format: int64 + type: integer + maximum: + description: A Number represents a JSON number literal. + type: string + minItems: + format: int64 + type: integer + minLength: + format: int64 + type: integer + minProperties: + format: int64 + type: integer + minimum: + description: A Number represents a JSON number literal. + type: string + multipleOf: + description: A Number represents a JSON number literal. + type: string + nullable: + type: boolean + pattern: + type: string + title: + type: string + type: + type: string + uniqueItems: + type: boolean + x-descriptors: + description: XDescriptors is a list of extended properties + that trigger a custom behavior in external systems + items: + type: string + type: array + type: object + type: object + required: + items: + type: string + type: array + title: + type: string + type: + type: string + type: object + dependencies: + description: Camel dependencies needed by the Kamelet + items: + type: string + type: array + sources: + description: sources in any Camel DSL supported + items: + description: SourceSpec defines the configuration for one + or more routes to be executed in a certain Camel DSL language. + properties: + compression: + description: if the content is compressed (base64 encrypted) + type: boolean + content: + description: the source code (plain text) + type: string + contentKey: + description: the confimap key holding the source content + type: string + contentRef: + description: the confimap reference holding the source + content + type: string + contentType: + description: the content type (tipically text or binary) + type: string + from-kamelet: + description: True if the spec is generated from a Kamelet + type: boolean + interceptors: + description: |- + Interceptors are optional identifiers the org.apache.camel.k.RoutesLoader + uses to pre/post process sources + items: + type: string + type: array + language: + description: specify which is the language (Camel DSL) + used to interpret this source code + type: string + loader: + description: |- + Loader is an optional id of the org.apache.camel.k.RoutesLoader that will + interpret this source at runtime + type: string + name: + description: the name of the specification + type: string + path: + description: the path where the file is stored + type: string + property-names: + description: List of property names defined in the source + (e.g. if type is "template") + items: + type: string + type: array + rawContent: + description: the source code (binary) + format: byte + type: string + type: + description: Type defines the kind of source described + by this object + type: string + type: object + type: array + template: + description: the main source in YAML DSL + type: object + x-kubernetes-preserve-unknown-fields: true + types: + additionalProperties: + description: |- + EventTypeSpec represents a specification for an event type. + Deprecated: In favor of using DataTypeSpec. + properties: + mediaType: + description: media type as expected for HTTP media types + (ie, application/json) + type: string + schema: + description: the expected schema for the event + properties: + $schema: + description: JSONSchemaURL represents a schema url. + type: string + description: + type: string + example: + description: |- + JSON represents any valid JSON value. + These types are supported: bool, int64, float64, string, []interface{}, map[string]interface{} and nil. + x-kubernetes-preserve-unknown-fields: true + externalDocs: + description: ExternalDocumentation allows referencing + an external resource for extended documentation. + properties: + description: + type: string + url: + type: string + type: object + id: + type: string + properties: + additionalProperties: + properties: + default: + description: default is a default value for + undefined object fields. + x-kubernetes-preserve-unknown-fields: true + deprecated: + type: boolean + description: + type: string + enum: + items: + description: |- + JSON represents any valid JSON value. + These types are supported: bool, int64, float64, string, []interface{}, map[string]interface{} and nil. + x-kubernetes-preserve-unknown-fields: true + type: array + example: + description: |- + JSON represents any valid JSON value. + These types are supported: bool, int64, float64, string, []interface{}, map[string]interface{} and nil. + x-kubernetes-preserve-unknown-fields: true + exclusiveMaximum: + type: boolean + exclusiveMinimum: + type: boolean + format: + description: |- + format is an OpenAPI v3 format string. Unknown formats are ignored. The following formats are validated: + + + - bsonobjectid: a bson object ID, i.e. a 24 characters hex string + - uri: an URI as parsed by Golang net/url.ParseRequestURI + - email: an email address as parsed by Golang net/mail.ParseAddress + - hostname: a valid representation for an Internet host name, as defined by RFC 1034, section 3.1 [RFC1034]. + - ipv4: an IPv4 IP as parsed by Golang net.ParseIP + - ipv6: an IPv6 IP as parsed by Golang net.ParseIP + - cidr: a CIDR as parsed by Golang net.ParseCIDR + - mac: a MAC address as parsed by Golang net.ParseMAC + - uuid: an UUID that allows uppercase defined by the regex (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}$ + - uuid3: an UUID3 that allows uppercase defined by the regex (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?3[0-9a-f]{3}-?[0-9a-f]{4}-?[0-9a-f]{12}$ + - uuid4: an UUID4 that allows uppercase defined by the regex (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?4[0-9a-f]{3}-?[89ab][0-9a-f]{3}-?[0-9a-f]{12}$ + - uuid5: an UUID5 that allows uppercase defined by the regex (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?5[0-9a-f]{3}-?[89ab][0-9a-f]{3}-?[0-9a-f]{12}$ + - isbn: an ISBN10 or ISBN13 number string like "0321751043" or "978-0321751041" + - isbn10: an ISBN10 number string like "0321751043" + - isbn13: an ISBN13 number string like "978-0321751041" + - creditcard: a credit card number defined by the regex ^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\\d{3})\\d{11})$ with any non digit characters mixed in + - ssn: a U.S. social security number following the regex ^\\d{3}[- ]?\\d{2}[- ]?\\d{4}$ + - hexcolor: an hexadecimal color code like "#FFFFFF" following the regex ^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$ + - rgbcolor: an RGB color code like rgb like "rgb(255,255,255)" + - byte: base64 encoded binary data + - password: any kind of string + - date: a date string like "2006-01-02" as defined by full-date in RFC3339 + - duration: a duration string like "22 ns" as parsed by Golang time.ParseDuration or compatible with Scala duration format + - datetime: a date time string like "2014-12-15T19:30:20.000Z" as defined by date-time in RFC3339. + type: string + id: + type: string + maxItems: + format: int64 + type: integer + maxLength: + format: int64 + type: integer + maxProperties: + format: int64 + type: integer + maximum: + description: A Number represents a JSON number + literal. + type: string + minItems: + format: int64 + type: integer + minLength: + format: int64 + type: integer + minProperties: + format: int64 + type: integer + minimum: + description: A Number represents a JSON number + literal. + type: string + multipleOf: + description: A Number represents a JSON number + literal. + type: string + nullable: + type: boolean + pattern: + type: string + title: + type: string + type: + type: string + uniqueItems: + type: boolean + x-descriptors: + description: XDescriptors is a list of extended + properties that trigger a custom behavior + in external systems + items: + type: string + type: array + type: object + type: object + required: + items: + type: string + type: array + title: + type: string + type: + type: string + type: object + type: object + description: |- + data specification types for the events consumed/produced by the Kamelet + Deprecated: In favor of using DataTypes + type: object + type: object + description: |- + the optional versions available for this Kamelet. This field may not be taken in account by Camel core and is meant to support + any user defined versioning model on cluster only. If the user wants to use any given version, she must materialize a file with the given version spec + as the `main` Kamelet spec on the runtime. + type: object type: object status: default: diff --git a/pkg/apis/camel/v1/kamelet_types.go b/pkg/apis/camel/v1/kamelet_types.go index 43a8404fe4..3a571d4621 100644 --- a/pkg/apis/camel/v1/kamelet_types.go +++ b/pkg/apis/camel/v1/kamelet_types.go @@ -49,6 +49,8 @@ var ( reservedKameletNames = map[string]bool{"source": true, "sink": true} // KameletIDProperty used to identify. KameletIDProperty = "id" + // KameletVersionProperty used to specify the version to use. + KameletVersionProperty = "kameletVersion" ) // +genclient @@ -76,6 +78,15 @@ type Kamelet struct { // KameletSpec specifies the configuration required to execute a Kamelet. type KameletSpec struct { + KameletSpecBase `json:",inline"` + // the optional versions available for this Kamelet. This field may not be taken in account by Camel core and is meant to support + // any user defined versioning model on cluster only. If the user wants to use any given version, she must materialize a file with the given version spec + // as the `main` Kamelet spec on the runtime. + Versions map[string]KameletSpecBase `json:"versions,omitempty"` +} + +// KameletSpecBase specifies the base configuration of a Kamelet. +type KameletSpecBase struct { // defines the formal configuration of the Kamelet Definition *JSONSchemaProps `json:"definition,omitempty"` // sources in any Camel DSL supported diff --git a/pkg/apis/camel/v1/kamelet_types_support.go b/pkg/apis/camel/v1/kamelet_types_support.go index e8818cb9ef..bb22c998e8 100644 --- a/pkg/apis/camel/v1/kamelet_types_support.go +++ b/pkg/apis/camel/v1/kamelet_types_support.go @@ -18,6 +18,7 @@ limitations under the License. package v1 import ( + "fmt" "sort" corev1 "k8s.io/api/core/v1" @@ -181,16 +182,6 @@ func ValidKameletName(name string) bool { return !reservedKameletNames[name] } -func ValidKameletProperties(kamelet *Kamelet) bool { - if kamelet == nil || kamelet.Spec.Definition == nil || kamelet.Spec.Definition.Properties == nil { - return true - } - if _, idPresent := kamelet.Spec.Definition.Properties[KameletIDProperty]; idPresent { - return false - } - return true -} - // NewKamelet creates a new Kamelet. func NewKamelet(namespace string, name string) Kamelet { return Kamelet{ @@ -219,3 +210,20 @@ func NewKameletList() KameletList { func (k *Kamelet) SetOperatorID(operatorID string) { SetAnnotation(&k.ObjectMeta, OperatorIDAnnotation, operatorID) } + +// CloneWithVersion clones a Kamelet and set the main specification with any version provided. +// It also changes the name adding a suffix with the version provided. +func (k *Kamelet) CloneWithVersion(version string) (*Kamelet, error) { + clone := k.DeepCopy() + if version != "" { + kameletVersionSpec, ok := k.Spec.Versions[version] + if !ok { + return nil, fmt.Errorf("could not find version %s for Kamelet %s/%s", version, k.Namespace, k.Name) + } + clone.Spec.KameletSpecBase = kameletVersionSpec + } + // Remove any existing version + clone.Spec.Versions = nil + + return clone, nil +} diff --git a/pkg/apis/camel/v1/zz_generated.deepcopy.go b/pkg/apis/camel/v1/zz_generated.deepcopy.go index 181d1c91c1..275a98449f 100644 --- a/pkg/apis/camel/v1/zz_generated.deepcopy.go +++ b/pkg/apis/camel/v1/zz_generated.deepcopy.go @@ -2157,6 +2157,29 @@ func (in *KameletRepositorySpec) DeepCopy() *KameletRepositorySpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *KameletSpec) DeepCopyInto(out *KameletSpec) { + *out = *in + in.KameletSpecBase.DeepCopyInto(&out.KameletSpecBase) + if in.Versions != nil { + in, out := &in.Versions, &out.Versions + *out = make(map[string]KameletSpecBase, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KameletSpec. +func (in *KameletSpec) DeepCopy() *KameletSpec { + if in == nil { + return nil + } + out := new(KameletSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KameletSpecBase) DeepCopyInto(out *KameletSpecBase) { *out = *in if in.Definition != nil { in, out := &in.Definition, &out.Definition @@ -2196,12 +2219,12 @@ func (in *KameletSpec) DeepCopyInto(out *KameletSpec) { } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KameletSpec. -func (in *KameletSpec) DeepCopy() *KameletSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KameletSpecBase. +func (in *KameletSpecBase) DeepCopy() *KameletSpecBase { if in == nil { return nil } - out := new(KameletSpec) + out := new(KameletSpecBase) in.DeepCopyInto(out) return out } diff --git a/pkg/apis/camel/v1alpha1/kamelet_types_support.go b/pkg/apis/camel/v1alpha1/kamelet_types_support.go index 55264bea6a..4181675ba3 100644 --- a/pkg/apis/camel/v1alpha1/kamelet_types_support.go +++ b/pkg/apis/camel/v1alpha1/kamelet_types_support.go @@ -184,16 +184,6 @@ func ValidKameletName(name string) bool { return !reservedKameletNames[name] } -func ValidKameletProperties(kamelet *Kamelet) bool { - if kamelet == nil || kamelet.Spec.Definition == nil || kamelet.Spec.Definition.Properties == nil { - return true - } - if _, idPresent := kamelet.Spec.Definition.Properties[KameletIDProperty]; idPresent { - return false - } - return true -} - // NewKamelet creates a new Kamelet. func NewKamelet(namespace string, name string) Kamelet { return Kamelet{ diff --git a/pkg/client/camel/applyconfiguration/camel/v1/kameletspec.go b/pkg/client/camel/applyconfiguration/camel/v1/kameletspec.go index bf1ac56b6c..eebfdce5f1 100644 --- a/pkg/client/camel/applyconfiguration/camel/v1/kameletspec.go +++ b/pkg/client/camel/applyconfiguration/camel/v1/kameletspec.go @@ -26,12 +26,8 @@ import ( // KameletSpecApplyConfiguration represents an declarative configuration of the KameletSpec type for use // with apply. type KameletSpecApplyConfiguration struct { - Definition *JSONSchemaPropsApplyConfiguration `json:"definition,omitempty"` - Sources []SourceSpecApplyConfiguration `json:"sources,omitempty"` - Template *TemplateApplyConfiguration `json:"template,omitempty"` - Types map[camelv1.TypeSlot]EventTypeSpecApplyConfiguration `json:"types,omitempty"` - DataTypes map[camelv1.TypeSlot]DataTypesSpecApplyConfiguration `json:"dataTypes,omitempty"` - Dependencies []string `json:"dependencies,omitempty"` + KameletSpecBaseApplyConfiguration `json:",inline"` + Versions map[string]KameletSpecBaseApplyConfiguration `json:"versions,omitempty"` } // KameletSpecApplyConfiguration constructs an declarative configuration of the KameletSpec type for use with @@ -106,3 +102,17 @@ func (b *KameletSpecApplyConfiguration) WithDependencies(values ...string) *Kame } return b } + +// WithVersions puts the entries into the Versions field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Versions field, +// overwriting an existing map entries in Versions field with the same key. +func (b *KameletSpecApplyConfiguration) WithVersions(entries map[string]KameletSpecBaseApplyConfiguration) *KameletSpecApplyConfiguration { + if b.Versions == nil && len(entries) > 0 { + b.Versions = make(map[string]KameletSpecBaseApplyConfiguration, len(entries)) + } + for k, v := range entries { + b.Versions[k] = v + } + return b +} diff --git a/pkg/client/camel/applyconfiguration/camel/v1/kameletspecbase.go b/pkg/client/camel/applyconfiguration/camel/v1/kameletspecbase.go new file mode 100644 index 0000000000..5f30df08f4 --- /dev/null +++ b/pkg/client/camel/applyconfiguration/camel/v1/kameletspecbase.go @@ -0,0 +1,108 @@ +/* +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. +*/ + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1 + +import ( + camelv1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1" +) + +// KameletSpecBaseApplyConfiguration represents an declarative configuration of the KameletSpecBase type for use +// with apply. +type KameletSpecBaseApplyConfiguration struct { + Definition *JSONSchemaPropsApplyConfiguration `json:"definition,omitempty"` + Sources []SourceSpecApplyConfiguration `json:"sources,omitempty"` + Template *TemplateApplyConfiguration `json:"template,omitempty"` + Types map[camelv1.TypeSlot]EventTypeSpecApplyConfiguration `json:"types,omitempty"` + DataTypes map[camelv1.TypeSlot]DataTypesSpecApplyConfiguration `json:"dataTypes,omitempty"` + Dependencies []string `json:"dependencies,omitempty"` +} + +// KameletSpecBaseApplyConfiguration constructs an declarative configuration of the KameletSpecBase type for use with +// apply. +func KameletSpecBase() *KameletSpecBaseApplyConfiguration { + return &KameletSpecBaseApplyConfiguration{} +} + +// WithDefinition sets the Definition field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Definition field is set to the value of the last call. +func (b *KameletSpecBaseApplyConfiguration) WithDefinition(value *JSONSchemaPropsApplyConfiguration) *KameletSpecBaseApplyConfiguration { + b.Definition = value + return b +} + +// WithSources adds the given value to the Sources field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Sources field. +func (b *KameletSpecBaseApplyConfiguration) WithSources(values ...*SourceSpecApplyConfiguration) *KameletSpecBaseApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithSources") + } + b.Sources = append(b.Sources, *values[i]) + } + return b +} + +// WithTemplate sets the Template field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Template field is set to the value of the last call. +func (b *KameletSpecBaseApplyConfiguration) WithTemplate(value *TemplateApplyConfiguration) *KameletSpecBaseApplyConfiguration { + b.Template = value + return b +} + +// WithTypes puts the entries into the Types field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Types field, +// overwriting an existing map entries in Types field with the same key. +func (b *KameletSpecBaseApplyConfiguration) WithTypes(entries map[camelv1.TypeSlot]EventTypeSpecApplyConfiguration) *KameletSpecBaseApplyConfiguration { + if b.Types == nil && len(entries) > 0 { + b.Types = make(map[camelv1.TypeSlot]EventTypeSpecApplyConfiguration, len(entries)) + } + for k, v := range entries { + b.Types[k] = v + } + return b +} + +// WithDataTypes puts the entries into the DataTypes field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the DataTypes field, +// overwriting an existing map entries in DataTypes field with the same key. +func (b *KameletSpecBaseApplyConfiguration) WithDataTypes(entries map[camelv1.TypeSlot]DataTypesSpecApplyConfiguration) *KameletSpecBaseApplyConfiguration { + if b.DataTypes == nil && len(entries) > 0 { + b.DataTypes = make(map[camelv1.TypeSlot]DataTypesSpecApplyConfiguration, len(entries)) + } + for k, v := range entries { + b.DataTypes[k] = v + } + return b +} + +// WithDependencies adds the given value to the Dependencies field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Dependencies field. +func (b *KameletSpecBaseApplyConfiguration) WithDependencies(values ...string) *KameletSpecBaseApplyConfiguration { + for i := range values { + b.Dependencies = append(b.Dependencies, values[i]) + } + return b +} diff --git a/pkg/client/camel/applyconfiguration/utils.go b/pkg/client/camel/applyconfiguration/utils.go index 626858470c..835f37e452 100644 --- a/pkg/client/camel/applyconfiguration/utils.go +++ b/pkg/client/camel/applyconfiguration/utils.go @@ -168,6 +168,8 @@ func ForKind(kind schema.GroupVersionKind) interface{} { return &camelv1.KameletRepositorySpecApplyConfiguration{} case v1.SchemeGroupVersion.WithKind("KameletSpec"): return &camelv1.KameletSpecApplyConfiguration{} + case v1.SchemeGroupVersion.WithKind("KameletSpecBase"): + return &camelv1.KameletSpecBaseApplyConfiguration{} case v1.SchemeGroupVersion.WithKind("KameletStatus"): return &camelv1.KameletStatusApplyConfiguration{} case v1.SchemeGroupVersion.WithKind("KanikoTask"): diff --git a/pkg/controller/pipe/initialize_test.go b/pkg/controller/pipe/initialize_test.go index f12bf73863..315d899fef 100644 --- a/pkg/controller/pipe/initialize_test.go +++ b/pkg/controller/pipe/initialize_test.go @@ -120,18 +120,20 @@ func TestNewPipeWithKameletsCreating(t *testing.T) { v1.AnnotationIcon: "my-source-icon-base64", } source.Spec = v1.KameletSpec{ - Template: templateOrFail(map[string]interface{}{ - "from": map[string]interface{}{ - "uri": "timer:tick", - "steps": []interface{}{ - map[string]interface{}{ - "to": "kamelet:sink", + KameletSpecBase: v1.KameletSpecBase{ + Template: templateOrFail(map[string]interface{}{ + "from": map[string]interface{}{ + "uri": "timer:tick", + "steps": []interface{}{ + map[string]interface{}{ + "to": "kamelet:sink", + }, }, }, + }), + Dependencies: []string{ + "camel:timer", }, - }), - Dependencies: []string{ - "camel:timer", }, } sink := v1.NewKamelet("ns", "my-sink") @@ -139,20 +141,22 @@ func TestNewPipeWithKameletsCreating(t *testing.T) { v1.AnnotationIcon: "my-sink-icon-base64", } sink.Spec = v1.KameletSpec{ - Template: templateOrFail(map[string]interface{}{ - "from": map[string]interface{}{ - "uri": "kamelet:source", - "steps": []interface{}{ - map[string]interface{}{ - "to": map[string]interface{}{ - "uri": "log:info", + KameletSpecBase: v1.KameletSpecBase{ + Template: templateOrFail(map[string]interface{}{ + "from": map[string]interface{}{ + "uri": "kamelet:source", + "steps": []interface{}{ + map[string]interface{}{ + "to": map[string]interface{}{ + "uri": "log:info", + }, }, }, }, + }), + Dependencies: []string{ + "camel:log", }, - }), - Dependencies: []string{ - "camel:log", }, } pipe := &v1.Pipe{ diff --git a/pkg/resources/config/crd/bases/camel.apache.org_kamelets.yaml b/pkg/resources/config/crd/bases/camel.apache.org_kamelets.yaml index e008ec06d8..777b9e0e11 100644 --- a/pkg/resources/config/crd/bases/camel.apache.org_kamelets.yaml +++ b/pkg/resources/config/crd/bases/camel.apache.org_kamelets.yaml @@ -654,6 +654,599 @@ spec: data specification types for the events consumed/produced by the Kamelet Deprecated: In favor of using DataTypes type: object + versions: + additionalProperties: + description: KameletSpecBase specifies the base configuration of + a Kamelet. + properties: + dataTypes: + additionalProperties: + description: DataTypesSpec represents the specification for + a set of data types. + properties: + default: + description: the default data type for this Kamelet + type: string + headers: + additionalProperties: + description: HeaderSpec represents the specification + for a header used in the Kamelet. + properties: + default: + type: string + description: + type: string + required: + type: boolean + title: + type: string + type: + type: string + type: object + description: one to many header specifications + type: object + types: + additionalProperties: + description: DataTypeSpec represents the specification + for a data type. + properties: + dependencies: + description: the list of Camel or Maven dependencies + required by the data type + items: + type: string + type: array + description: + description: optional description + type: string + format: + description: the data type format name + type: string + headers: + additionalProperties: + description: HeaderSpec represents the specification + for a header used in the Kamelet. + properties: + default: + type: string + description: + type: string + required: + type: boolean + title: + type: string + type: + type: string + type: object + description: one to many header specifications + type: object + mediaType: + description: media type as expected for HTTP media + types (ie, application/json) + type: string + schema: + description: the expected schema for the data type + properties: + $schema: + description: JSONSchemaURL represents a schema + url. + type: string + description: + type: string + example: + description: |- + JSON represents any valid JSON value. + These types are supported: bool, int64, float64, string, []interface{}, map[string]interface{} and nil. + x-kubernetes-preserve-unknown-fields: true + externalDocs: + description: ExternalDocumentation allows referencing + an external resource for extended documentation. + properties: + description: + type: string + url: + type: string + type: object + id: + type: string + properties: + additionalProperties: + properties: + default: + description: default is a default value + for undefined object fields. + x-kubernetes-preserve-unknown-fields: true + deprecated: + type: boolean + description: + type: string + enum: + items: + description: |- + JSON represents any valid JSON value. + These types are supported: bool, int64, float64, string, []interface{}, map[string]interface{} and nil. + x-kubernetes-preserve-unknown-fields: true + type: array + example: + description: |- + JSON represents any valid JSON value. + These types are supported: bool, int64, float64, string, []interface{}, map[string]interface{} and nil. + x-kubernetes-preserve-unknown-fields: true + exclusiveMaximum: + type: boolean + exclusiveMinimum: + type: boolean + format: + description: |- + format is an OpenAPI v3 format string. Unknown formats are ignored. The following formats are validated: + + + - bsonobjectid: a bson object ID, i.e. a 24 characters hex string + - uri: an URI as parsed by Golang net/url.ParseRequestURI + - email: an email address as parsed by Golang net/mail.ParseAddress + - hostname: a valid representation for an Internet host name, as defined by RFC 1034, section 3.1 [RFC1034]. + - ipv4: an IPv4 IP as parsed by Golang net.ParseIP + - ipv6: an IPv6 IP as parsed by Golang net.ParseIP + - cidr: a CIDR as parsed by Golang net.ParseCIDR + - mac: a MAC address as parsed by Golang net.ParseMAC + - uuid: an UUID that allows uppercase defined by the regex (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}$ + - uuid3: an UUID3 that allows uppercase defined by the regex (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?3[0-9a-f]{3}-?[0-9a-f]{4}-?[0-9a-f]{12}$ + - uuid4: an UUID4 that allows uppercase defined by the regex (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?4[0-9a-f]{3}-?[89ab][0-9a-f]{3}-?[0-9a-f]{12}$ + - uuid5: an UUID5 that allows uppercase defined by the regex (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?5[0-9a-f]{3}-?[89ab][0-9a-f]{3}-?[0-9a-f]{12}$ + - isbn: an ISBN10 or ISBN13 number string like "0321751043" or "978-0321751041" + - isbn10: an ISBN10 number string like "0321751043" + - isbn13: an ISBN13 number string like "978-0321751041" + - creditcard: a credit card number defined by the regex ^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\\d{3})\\d{11})$ with any non digit characters mixed in + - ssn: a U.S. social security number following the regex ^\\d{3}[- ]?\\d{2}[- ]?\\d{4}$ + - hexcolor: an hexadecimal color code like "#FFFFFF" following the regex ^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$ + - rgbcolor: an RGB color code like rgb like "rgb(255,255,255)" + - byte: base64 encoded binary data + - password: any kind of string + - date: a date string like "2006-01-02" as defined by full-date in RFC3339 + - duration: a duration string like "22 ns" as parsed by Golang time.ParseDuration or compatible with Scala duration format + - datetime: a date time string like "2014-12-15T19:30:20.000Z" as defined by date-time in RFC3339. + type: string + id: + type: string + maxItems: + format: int64 + type: integer + maxLength: + format: int64 + type: integer + maxProperties: + format: int64 + type: integer + maximum: + description: A Number represents a JSON + number literal. + type: string + minItems: + format: int64 + type: integer + minLength: + format: int64 + type: integer + minProperties: + format: int64 + type: integer + minimum: + description: A Number represents a JSON + number literal. + type: string + multipleOf: + description: A Number represents a JSON + number literal. + type: string + nullable: + type: boolean + pattern: + type: string + title: + type: string + type: + type: string + uniqueItems: + type: boolean + x-descriptors: + description: XDescriptors is a list of + extended properties that trigger a custom + behavior in external systems + items: + type: string + type: array + type: object + type: object + required: + items: + type: string + type: array + title: + type: string + type: + type: string + type: object + scheme: + description: the data type component scheme + type: string + type: object + description: one to many data type specifications + type: object + type: object + description: data specification types for the events consumed/produced + by the Kamelet + type: object + definition: + description: defines the formal configuration of the Kamelet + properties: + $schema: + description: JSONSchemaURL represents a schema url. + type: string + description: + type: string + example: + description: |- + JSON represents any valid JSON value. + These types are supported: bool, int64, float64, string, []interface{}, map[string]interface{} and nil. + x-kubernetes-preserve-unknown-fields: true + externalDocs: + description: ExternalDocumentation allows referencing an + external resource for extended documentation. + properties: + description: + type: string + url: + type: string + type: object + id: + type: string + properties: + additionalProperties: + properties: + default: + description: default is a default value for undefined + object fields. + x-kubernetes-preserve-unknown-fields: true + deprecated: + type: boolean + description: + type: string + enum: + items: + description: |- + JSON represents any valid JSON value. + These types are supported: bool, int64, float64, string, []interface{}, map[string]interface{} and nil. + x-kubernetes-preserve-unknown-fields: true + type: array + example: + description: |- + JSON represents any valid JSON value. + These types are supported: bool, int64, float64, string, []interface{}, map[string]interface{} and nil. + x-kubernetes-preserve-unknown-fields: true + exclusiveMaximum: + type: boolean + exclusiveMinimum: + type: boolean + format: + description: |- + format is an OpenAPI v3 format string. Unknown formats are ignored. The following formats are validated: + + + - bsonobjectid: a bson object ID, i.e. a 24 characters hex string + - uri: an URI as parsed by Golang net/url.ParseRequestURI + - email: an email address as parsed by Golang net/mail.ParseAddress + - hostname: a valid representation for an Internet host name, as defined by RFC 1034, section 3.1 [RFC1034]. + - ipv4: an IPv4 IP as parsed by Golang net.ParseIP + - ipv6: an IPv6 IP as parsed by Golang net.ParseIP + - cidr: a CIDR as parsed by Golang net.ParseCIDR + - mac: a MAC address as parsed by Golang net.ParseMAC + - uuid: an UUID that allows uppercase defined by the regex (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}$ + - uuid3: an UUID3 that allows uppercase defined by the regex (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?3[0-9a-f]{3}-?[0-9a-f]{4}-?[0-9a-f]{12}$ + - uuid4: an UUID4 that allows uppercase defined by the regex (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?4[0-9a-f]{3}-?[89ab][0-9a-f]{3}-?[0-9a-f]{12}$ + - uuid5: an UUID5 that allows uppercase defined by the regex (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?5[0-9a-f]{3}-?[89ab][0-9a-f]{3}-?[0-9a-f]{12}$ + - isbn: an ISBN10 or ISBN13 number string like "0321751043" or "978-0321751041" + - isbn10: an ISBN10 number string like "0321751043" + - isbn13: an ISBN13 number string like "978-0321751041" + - creditcard: a credit card number defined by the regex ^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\\d{3})\\d{11})$ with any non digit characters mixed in + - ssn: a U.S. social security number following the regex ^\\d{3}[- ]?\\d{2}[- ]?\\d{4}$ + - hexcolor: an hexadecimal color code like "#FFFFFF" following the regex ^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$ + - rgbcolor: an RGB color code like rgb like "rgb(255,255,255)" + - byte: base64 encoded binary data + - password: any kind of string + - date: a date string like "2006-01-02" as defined by full-date in RFC3339 + - duration: a duration string like "22 ns" as parsed by Golang time.ParseDuration or compatible with Scala duration format + - datetime: a date time string like "2014-12-15T19:30:20.000Z" as defined by date-time in RFC3339. + type: string + id: + type: string + maxItems: + format: int64 + type: integer + maxLength: + format: int64 + type: integer + maxProperties: + format: int64 + type: integer + maximum: + description: A Number represents a JSON number literal. + type: string + minItems: + format: int64 + type: integer + minLength: + format: int64 + type: integer + minProperties: + format: int64 + type: integer + minimum: + description: A Number represents a JSON number literal. + type: string + multipleOf: + description: A Number represents a JSON number literal. + type: string + nullable: + type: boolean + pattern: + type: string + title: + type: string + type: + type: string + uniqueItems: + type: boolean + x-descriptors: + description: XDescriptors is a list of extended properties + that trigger a custom behavior in external systems + items: + type: string + type: array + type: object + type: object + required: + items: + type: string + type: array + title: + type: string + type: + type: string + type: object + dependencies: + description: Camel dependencies needed by the Kamelet + items: + type: string + type: array + sources: + description: sources in any Camel DSL supported + items: + description: SourceSpec defines the configuration for one + or more routes to be executed in a certain Camel DSL language. + properties: + compression: + description: if the content is compressed (base64 encrypted) + type: boolean + content: + description: the source code (plain text) + type: string + contentKey: + description: the confimap key holding the source content + type: string + contentRef: + description: the confimap reference holding the source + content + type: string + contentType: + description: the content type (tipically text or binary) + type: string + from-kamelet: + description: True if the spec is generated from a Kamelet + type: boolean + interceptors: + description: |- + Interceptors are optional identifiers the org.apache.camel.k.RoutesLoader + uses to pre/post process sources + items: + type: string + type: array + language: + description: specify which is the language (Camel DSL) + used to interpret this source code + type: string + loader: + description: |- + Loader is an optional id of the org.apache.camel.k.RoutesLoader that will + interpret this source at runtime + type: string + name: + description: the name of the specification + type: string + path: + description: the path where the file is stored + type: string + property-names: + description: List of property names defined in the source + (e.g. if type is "template") + items: + type: string + type: array + rawContent: + description: the source code (binary) + format: byte + type: string + type: + description: Type defines the kind of source described + by this object + type: string + type: object + type: array + template: + description: the main source in YAML DSL + type: object + x-kubernetes-preserve-unknown-fields: true + types: + additionalProperties: + description: |- + EventTypeSpec represents a specification for an event type. + Deprecated: In favor of using DataTypeSpec. + properties: + mediaType: + description: media type as expected for HTTP media types + (ie, application/json) + type: string + schema: + description: the expected schema for the event + properties: + $schema: + description: JSONSchemaURL represents a schema url. + type: string + description: + type: string + example: + description: |- + JSON represents any valid JSON value. + These types are supported: bool, int64, float64, string, []interface{}, map[string]interface{} and nil. + x-kubernetes-preserve-unknown-fields: true + externalDocs: + description: ExternalDocumentation allows referencing + an external resource for extended documentation. + properties: + description: + type: string + url: + type: string + type: object + id: + type: string + properties: + additionalProperties: + properties: + default: + description: default is a default value for + undefined object fields. + x-kubernetes-preserve-unknown-fields: true + deprecated: + type: boolean + description: + type: string + enum: + items: + description: |- + JSON represents any valid JSON value. + These types are supported: bool, int64, float64, string, []interface{}, map[string]interface{} and nil. + x-kubernetes-preserve-unknown-fields: true + type: array + example: + description: |- + JSON represents any valid JSON value. + These types are supported: bool, int64, float64, string, []interface{}, map[string]interface{} and nil. + x-kubernetes-preserve-unknown-fields: true + exclusiveMaximum: + type: boolean + exclusiveMinimum: + type: boolean + format: + description: |- + format is an OpenAPI v3 format string. Unknown formats are ignored. The following formats are validated: + + + - bsonobjectid: a bson object ID, i.e. a 24 characters hex string + - uri: an URI as parsed by Golang net/url.ParseRequestURI + - email: an email address as parsed by Golang net/mail.ParseAddress + - hostname: a valid representation for an Internet host name, as defined by RFC 1034, section 3.1 [RFC1034]. + - ipv4: an IPv4 IP as parsed by Golang net.ParseIP + - ipv6: an IPv6 IP as parsed by Golang net.ParseIP + - cidr: a CIDR as parsed by Golang net.ParseCIDR + - mac: a MAC address as parsed by Golang net.ParseMAC + - uuid: an UUID that allows uppercase defined by the regex (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}$ + - uuid3: an UUID3 that allows uppercase defined by the regex (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?3[0-9a-f]{3}-?[0-9a-f]{4}-?[0-9a-f]{12}$ + - uuid4: an UUID4 that allows uppercase defined by the regex (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?4[0-9a-f]{3}-?[89ab][0-9a-f]{3}-?[0-9a-f]{12}$ + - uuid5: an UUID5 that allows uppercase defined by the regex (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?5[0-9a-f]{3}-?[89ab][0-9a-f]{3}-?[0-9a-f]{12}$ + - isbn: an ISBN10 or ISBN13 number string like "0321751043" or "978-0321751041" + - isbn10: an ISBN10 number string like "0321751043" + - isbn13: an ISBN13 number string like "978-0321751041" + - creditcard: a credit card number defined by the regex ^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\\d{3})\\d{11})$ with any non digit characters mixed in + - ssn: a U.S. social security number following the regex ^\\d{3}[- ]?\\d{2}[- ]?\\d{4}$ + - hexcolor: an hexadecimal color code like "#FFFFFF" following the regex ^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$ + - rgbcolor: an RGB color code like rgb like "rgb(255,255,255)" + - byte: base64 encoded binary data + - password: any kind of string + - date: a date string like "2006-01-02" as defined by full-date in RFC3339 + - duration: a duration string like "22 ns" as parsed by Golang time.ParseDuration or compatible with Scala duration format + - datetime: a date time string like "2014-12-15T19:30:20.000Z" as defined by date-time in RFC3339. + type: string + id: + type: string + maxItems: + format: int64 + type: integer + maxLength: + format: int64 + type: integer + maxProperties: + format: int64 + type: integer + maximum: + description: A Number represents a JSON number + literal. + type: string + minItems: + format: int64 + type: integer + minLength: + format: int64 + type: integer + minProperties: + format: int64 + type: integer + minimum: + description: A Number represents a JSON number + literal. + type: string + multipleOf: + description: A Number represents a JSON number + literal. + type: string + nullable: + type: boolean + pattern: + type: string + title: + type: string + type: + type: string + uniqueItems: + type: boolean + x-descriptors: + description: XDescriptors is a list of extended + properties that trigger a custom behavior + in external systems + items: + type: string + type: array + type: object + type: object + required: + items: + type: string + type: array + title: + type: string + type: + type: string + type: object + type: object + description: |- + data specification types for the events consumed/produced by the Kamelet + Deprecated: In favor of using DataTypes + type: object + type: object + description: |- + the optional versions available for this Kamelet. This field may not be taken in account by Camel core and is meant to support + any user defined versioning model on cluster only. If the user wants to use any given version, she must materialize a file with the given version spec + as the `main` Kamelet spec on the runtime. + type: object type: object status: default: diff --git a/pkg/trait/kamelets.go b/pkg/trait/kamelets.go index 912bb63975..8e861c5d33 100644 --- a/pkg/trait/kamelets.go +++ b/pkg/trait/kamelets.go @@ -53,6 +53,8 @@ const ( kameletMountPointAnnotation = "camel.apache.org/kamelet.mount-point" ) +var kameletVersionProperty = fmt.Sprintf("?%s=", v1.KameletVersionProperty) + type kameletsTrait struct { BaseTrait traitv1.KameletsTrait `property:",squash"` @@ -97,7 +99,7 @@ func (t *kameletsTrait) Configure(e *Environment) (bool, *TraitCondition, error) } } - return len(t.getKameletKeys()) > 0, nil, nil + return len(t.getKameletKeys(false)) > 0, nil, nil } func (t *kameletsTrait) Apply(e *Environment) error { @@ -109,6 +111,7 @@ func (t *kameletsTrait) Apply(e *Environment) error { return nil } +// collectKamelets load a Kamelet specification setting the specific version specification. func (t *kameletsTrait) collectKamelets(e *Environment) (map[string]*v1.Kamelet, error) { repo, err := repository.NewForPlatform(e.Ctx, e.Client, e.Platform, e.Integration.Namespace, platform.GetOperatorNamespace()) if err != nil { @@ -119,18 +122,27 @@ func (t *kameletsTrait) collectKamelets(e *Environment) (map[string]*v1.Kamelet, missingKamelets := make([]string, 0) availableKamelets := make([]string, 0) - for _, key := range t.getKameletKeys() { - kamelet, err := repo.Get(e.Ctx, key) + for _, kml := range strings.Split(t.List, ",") { + name := getKameletKey(kml, false) + if !v1.ValidKameletName(name) { + // Skip kamelet sink and source id + continue + } + kamelet, err := repo.Get(e.Ctx, name) if err != nil { return nil, err } - if kamelet == nil { - missingKamelets = append(missingKamelets, key) + missingKamelets = append(missingKamelets, name) + continue } else { - availableKamelets = append(availableKamelets, key) - kamelets[key] = kamelet + availableKamelets = append(availableKamelets, name) + } + clonedKamelet, err := kamelet.CloneWithVersion(getKameletVersion(kml)) + if err != nil { + return nil, err } + kamelets[clonedKamelet.Name] = clonedKamelet } sort.Strings(availableKamelets) @@ -163,17 +175,15 @@ func (t *kameletsTrait) collectKamelets(e *Environment) (map[string]*v1.Kamelet, } func (t *kameletsTrait) addKamelets(e *Environment) error { - if len(t.getKameletKeys()) == 0 { + if len(t.getKameletKeys(false)) == 0 { return nil } - kamelets, err := t.collectKamelets(e) if err != nil { return err } kb := newKameletBundle() - for _, key := range t.getKameletKeys() { - kamelet := kamelets[key] + for _, kamelet := range kamelets { if err := t.addKameletAsSource(e, kamelet); err != nil { return err } @@ -214,19 +224,16 @@ func (t *kameletsTrait) addKameletAsSource(e *Environment, kamelet *v1.Kamelet) sources := make([]v1.SourceSpec, 0) if kamelet.Spec.Template != nil { - template := kamelet.Spec.Template - flowData, err := dsl.TemplateToYamlDSL(*template, kamelet.Name) + flowData, err := dsl.TemplateToYamlDSL(*kamelet.Spec.Template, kamelet.Name) if err != nil { return err } - flowSource := v1.SourceSpec{ DataSpec: v1.DataSpec{ Name: fmt.Sprintf("%s.yaml", kamelet.Name), Content: string(flowData), }, - Language: v1.LanguageYaml, - FromKamelet: true, + Language: v1.LanguageYaml, } flowSource, err = integrationSourceFromKameletSource(e, kamelet, flowSource, fmt.Sprintf("%s-kamelet-%s-template", e.Integration.Name, kamelet.Name)) if err != nil { @@ -259,13 +266,10 @@ func (t *kameletsTrait) addKameletAsSource(e *Environment, kamelet *v1.Kamelet) return nil } -func (t *kameletsTrait) getKameletKeys() []string { +func (t *kameletsTrait) getKameletKeys(withVersion bool) []string { answer := make([]string, 0) for _, item := range strings.Split(t.List, ",") { - i := strings.Trim(item, " \t\"") - if strings.Contains(i, "/") { - i = strings.SplitN(i, "/", 2)[0] - } + i := getKameletKey(item, withVersion) if i != "" && v1.ValidKameletName(i) { util.StringSliceUniqueAdd(&answer, i) } @@ -274,6 +278,30 @@ func (t *kameletsTrait) getKameletKeys() []string { return answer } +func getKameletKey(item string, withVersion bool) string { + i := strings.Trim(item, " \t\"") + if strings.Contains(i, "/") { + i = strings.SplitN(i, "/", 2)[0] + } + if strings.Contains(i, kameletVersionProperty) { + versionedKamelet := strings.SplitN(i, kameletVersionProperty, 2) + if withVersion { + i = fmt.Sprintf("%s-%s", versionedKamelet[0], versionedKamelet[1]) + } else { + i = versionedKamelet[0] + } + } + return i +} + +func getKameletVersion(item string) string { + if strings.Contains(item, fmt.Sprintf("?%s=", v1.KameletVersionProperty)) { + versionedKamelet := strings.SplitN(item, kameletVersionProperty, 2) + return versionedKamelet[1] + } + return "" +} + func integrationSourceFromKameletSource(e *Environment, kamelet *v1.Kamelet, source v1.SourceSpec, name string) (v1.SourceSpec, error) { if source.Type == v1.SourceTypeTemplate { // Kamelets must be named ".extension" @@ -281,6 +309,8 @@ func integrationSourceFromKameletSource(e *Environment, kamelet *v1.Kamelet, sou source.Name = fmt.Sprintf("%s.%s", kamelet.Name, string(language)) } + source.FromKamelet = true + if source.DataSpec.ContentRef != "" { return source, nil } diff --git a/pkg/trait/kamelets_support_test.go b/pkg/trait/kamelets_support_test.go index 5435960756..26188f5306 100644 --- a/pkg/trait/kamelets_support_test.go +++ b/pkg/trait/kamelets_support_test.go @@ -86,29 +86,31 @@ func TestKameletBundleMultiKameletsMultiConfigmap(t *testing.T) { func kamelet(ns, name string) *v1.Kamelet { kamelet := v1.NewKamelet(ns, name) kamelet.Spec = v1.KameletSpec{ - Sources: []v1.SourceSpec{ - { - DataSpec: v1.DataSpec{ - Name: "mykamelet.groovy", - Content: `from("timer1").to("log:info") - from("timer2").to("log:info") - from("timer3").to("log:info") - from("timer4").to("log:info") - from("timer5").to("log:info") - from("timer6").to("log:info") - from("timer7").to("log:info") - from("timer8").to("log:info") - from("timer9").to("log:info") - from("timer10").to("log:info") - from("timer11").to("log:info") - from("timer12").to("log:info") - from("timer13").to("log:info") - from("timer14").to("log:info") - from("timer15").to("log:info") - from("timer16").to("log:info") - from("timer17").to("log:info")`, + KameletSpecBase: v1.KameletSpecBase{ + Sources: []v1.SourceSpec{ + { + DataSpec: v1.DataSpec{ + Name: "mykamelet.groovy", + Content: `from("timer1").to("log:info") + from("timer2").to("log:info") + from("timer3").to("log:info") + from("timer4").to("log:info") + from("timer5").to("log:info") + from("timer6").to("log:info") + from("timer7").to("log:info") + from("timer8").to("log:info") + from("timer9").to("log:info") + from("timer10").to("log:info") + from("timer11").to("log:info") + from("timer12").to("log:info") + from("timer13").to("log:info") + from("timer14").to("log:info") + from("timer15").to("log:info") + from("timer16").to("log:info") + from("timer17").to("log:info")`, + }, + Type: v1.SourceTypeTemplate, }, - Type: v1.SourceTypeTemplate, }, }, } diff --git a/pkg/trait/kamelets_test.go b/pkg/trait/kamelets_test.go index d59e56d797..161ac3714f 100644 --- a/pkg/trait/kamelets_test.go +++ b/pkg/trait/kamelets_test.go @@ -54,6 +54,7 @@ func TestConfigurationWithKamelets(t *testing.T) { uri: kamelet:c1 steps: - to: kamelet:c2 + - to: kamelet:c3?kameletVersion=v1 - to: telegram:bots - to: kamelet://c0?prop=x - to: kamelet://complex-.-.-1a?prop=x&prop2 @@ -66,7 +67,8 @@ func TestConfigurationWithKamelets(t *testing.T) { require.NoError(t, err) assert.True(t, enabled) assert.Nil(t, condition) - assert.Equal(t, []string{"c0", "c1", "c2", "complex-.-.-1a", "complex-.-.-1b", "complex-.-.-1c"}, trait.getKameletKeys()) + assert.Equal(t, []string{"c0", "c1", "c2", "c3", "complex-.-.-1a", "complex-.-.-1b", "complex-.-.-1c"}, trait.getKameletKeys(false)) + assert.Equal(t, []string{"c0", "c1", "c2", "c3-v1", "complex-.-.-1a", "complex-.-.-1b", "complex-.-.-1c"}, trait.getKameletKeys(true)) } func TestKameletLookup(t *testing.T) { @@ -81,14 +83,16 @@ func TestKameletLookup(t *testing.T) { Name: "timer", }, Spec: v1.KameletSpec{ - Template: templateOrFail(map[string]interface{}{ - "from": map[string]interface{}{ - "uri": "timer:tick", + KameletSpecBase: v1.KameletSpecBase{ + Template: templateOrFail(map[string]interface{}{ + "from": map[string]interface{}{ + "uri": "timer:tick", + }, + }), + Dependencies: []string{ + "camel:timer", + "camel:log", }, - }), - Dependencies: []string{ - "camel:timer", - "camel:log", }, }, }) @@ -96,7 +100,7 @@ func TestKameletLookup(t *testing.T) { require.NoError(t, err) assert.True(t, enabled) assert.Nil(t, condition) - assert.Equal(t, []string{"timer"}, trait.getKameletKeys()) + assert.Equal(t, []string{"timer"}, trait.getKameletKeys(false)) err = trait.Apply(environment) require.NoError(t, err) @@ -125,18 +129,20 @@ func TestKameletSecondarySourcesLookup(t *testing.T) { Name: "timer", }, Spec: v1.KameletSpec{ - Template: templateOrFail(map[string]interface{}{ - "from": map[string]interface{}{ - "uri": "timer:tick", - }, - }), - Sources: []v1.SourceSpec{ - { - DataSpec: v1.DataSpec{ - Name: "support.groovy", - Content: "from('xxx:xxx').('to:log:info')", + KameletSpecBase: v1.KameletSpecBase{ + Template: templateOrFail(map[string]interface{}{ + "from": map[string]interface{}{ + "uri": "timer:tick", + }, + }), + Sources: []v1.SourceSpec{ + { + DataSpec: v1.DataSpec{ + Name: "support.groovy", + Content: "from('xxx:xxx').('to:log:info')", + }, + Language: v1.LanguageGroovy, }, - Language: v1.LanguageGroovy, }, }, }, @@ -145,7 +151,7 @@ func TestKameletSecondarySourcesLookup(t *testing.T) { require.NoError(t, err) assert.True(t, enabled) assert.Nil(t, condition) - assert.Equal(t, []string{"timer"}, trait.getKameletKeys()) + assert.Equal(t, []string{"timer"}, trait.getKameletKeys(false)) err = trait.Apply(environment) require.NoError(t, err) @@ -181,13 +187,15 @@ func TestNonYAMLKameletLookup(t *testing.T) { Name: "timer", }, Spec: v1.KameletSpec{ - Sources: []v1.SourceSpec{ - { - DataSpec: v1.DataSpec{ - Name: "mykamelet.groovy", - Content: `from("timer").to("log:info")`, + KameletSpecBase: v1.KameletSpecBase{ + Sources: []v1.SourceSpec{ + { + DataSpec: v1.DataSpec{ + Name: "mykamelet.groovy", + Content: `from("timer").to("log:info")`, + }, + Type: v1.SourceTypeTemplate, }, - Type: v1.SourceTypeTemplate, }, }, }, @@ -196,7 +204,7 @@ func TestNonYAMLKameletLookup(t *testing.T) { require.NoError(t, err) assert.True(t, enabled) assert.Nil(t, condition) - assert.Equal(t, []string{"timer"}, trait.getKameletKeys()) + assert.Equal(t, []string{"timer"}, trait.getKameletKeys(false)) err = trait.Apply(environment) require.NoError(t, err) @@ -214,33 +222,42 @@ func TestNonYAMLKameletLookup(t *testing.T) { func TestMultipleKamelets(t *testing.T) { trait, environment := createKameletsTestEnvironment(` - from: - uri: kamelet:timer + uri: kamelet:timer?kameletVersion=v1 steps: - to: kamelet:logger - - to: kamelet:logger `, &v1.Kamelet{ ObjectMeta: metav1.ObjectMeta{ Namespace: "test", Name: "timer", }, Spec: v1.KameletSpec{ - Template: templateOrFail(map[string]interface{}{ - "from": map[string]interface{}{ - "uri": "timer:tick", - }, - }), - Sources: []v1.SourceSpec{ - { - DataSpec: v1.DataSpec{ - Name: "support.groovy", - Content: "from('xxx:xxx').('to:log:info')", + KameletSpecBase: v1.KameletSpecBase{ + Template: templateOrFail(map[string]interface{}{ + "from": map[string]interface{}{ + "uri": "timer:tick", }, - Language: v1.LanguageGroovy, + }), + Dependencies: []string{ + "camel:timer", + "camel:xxx", }, }, - Dependencies: []string{ - "camel:timer", - "camel:xxx", + Versions: map[string]v1.KameletSpecBase{ + "v1": { + Sources: []v1.SourceSpec{ + { + DataSpec: v1.DataSpec{ + Name: "support.groovy", + Content: "from('xxx:xxx').('to:log:info')", + }, + Language: v1.LanguageGroovy, + }, + }, + Dependencies: []string{ + "camel:timer", + "camel:xxx-2", + }, + }, }, }, }, &v1.Kamelet{ @@ -249,21 +266,43 @@ func TestMultipleKamelets(t *testing.T) { Name: "logger", }, Spec: v1.KameletSpec{ - Template: templateOrFail(map[string]interface{}{ - "from": map[string]interface{}{ - "uri": "tbd:endpoint", - "steps": []interface{}{ - map[string]interface{}{ - "to": map[string]interface{}{ - "uri": "log:info", + KameletSpecBase: v1.KameletSpecBase{ + Template: templateOrFail(map[string]interface{}{ + "from": map[string]interface{}{ + "uri": "tbd:endpoint", + "steps": []interface{}{ + map[string]interface{}{ + "to": map[string]interface{}{ + "uri": "log:info?option=main", + }, + }, + }, + }, + }), + Dependencies: []string{ + "camel:log", + "camel:tbd", + }, + }, + Versions: map[string]v1.KameletSpecBase{ + "v2": { + Template: templateOrFail(map[string]interface{}{ + "from": map[string]interface{}{ + "uri": "tbd:endpoint", + "steps": []interface{}{ + map[string]interface{}{ + "to": map[string]interface{}{ + "uri": "log:info?option=version2", + }, + }, }, }, + }), + Dependencies: []string{ + "camel:log", + "camel:tbd-2", }, }, - }), - Dependencies: []string{ - "camel:log", - "camel:tbd", }, }, }) @@ -271,39 +310,48 @@ func TestMultipleKamelets(t *testing.T) { require.NoError(t, err) assert.True(t, enabled) assert.Nil(t, condition) - assert.Equal(t, []string{"logger", "timer"}, trait.getKameletKeys()) + assert.Equal(t, "logger,timer?kameletVersion=v1", trait.List) + assert.Equal(t, []string{"logger", "timer"}, trait.getKameletKeys(false)) + assert.Equal(t, []string{"logger", "timer-v1"}, trait.getKameletKeys(true)) err = trait.Apply(environment) require.NoError(t, err) - cmFlow := environment.Resources.GetConfigMap(func(c *corev1.ConfigMap) bool { return c.Name == "it-kamelet-timer-template" }) - assert.NotNil(t, cmFlow) - cmRes := environment.Resources.GetConfigMap(func(c *corev1.ConfigMap) bool { return c.Name == "it-kamelet-timer-000" }) - assert.NotNil(t, cmRes) - cmFlow2 := environment.Resources.GetConfigMap(func(c *corev1.ConfigMap) bool { return c.Name == "it-kamelet-logger-template" }) - assert.NotNil(t, cmFlow2) + cmFlowTimerSource := environment.Resources.GetConfigMap(func(c *corev1.ConfigMap) bool { return c.Name == "it-kamelet-timer-000" }) + assert.NotNil(t, cmFlowTimerSource) + assert.Contains(t, cmFlowTimerSource.Data[contentKey], "from('xxx:xxx').('to:log:info')") + cmFlowMissing := environment.Resources.GetConfigMap(func(c *corev1.ConfigMap) bool { return c.Name == "it-kamelet-timer-template" }) + assert.Nil(t, cmFlowMissing) + cmFlowLoggerTemplateMain := environment.Resources.GetConfigMap(func(c *corev1.ConfigMap) bool { return c.Name == "it-kamelet-logger-template" }) + assert.NotNil(t, cmFlowLoggerTemplateMain) + assert.Contains(t, cmFlowLoggerTemplateMain.Data[contentKey], "log:info?option=main") - assert.Len(t, environment.Integration.Status.GeneratedSources, 3) + assert.Len(t, environment.Integration.Status.GeneratedSources, 2) - flowSource2 := environment.Integration.Status.GeneratedSources[0] - assert.Equal(t, "logger.yaml", flowSource2.Name) - assert.Equal(t, "", string(flowSource2.Type)) - assert.Equal(t, "it-kamelet-logger-template", flowSource2.ContentRef) - assert.Equal(t, "content", flowSource2.ContentKey) + expectedFlowSourceTimerV1 := v1.SourceSpec{ + DataSpec: v1.DataSpec{ + Name: "support.groovy", + ContentRef: "it-kamelet-timer-000", + ContentKey: "content", + }, + Language: v1.LanguageGroovy, + FromKamelet: true, + } - flowSource := environment.Integration.Status.GeneratedSources[1] - assert.Equal(t, "timer.yaml", flowSource.Name) - assert.Equal(t, "", string(flowSource.Type)) - assert.Equal(t, "it-kamelet-timer-template", flowSource.ContentRef) - assert.Equal(t, "content", flowSource.ContentKey) + expectedFlowSinkLoggerMain := v1.SourceSpec{ + DataSpec: v1.DataSpec{ + Name: "logger.yaml", + ContentRef: "it-kamelet-logger-template", + ContentKey: "content", + }, + Language: v1.LanguageYaml, + FromKamelet: true, + } - supportSource := environment.Integration.Status.GeneratedSources[2] - assert.Equal(t, "support.groovy", supportSource.Name) - assert.Equal(t, "", string(supportSource.Type)) - assert.Equal(t, "it-kamelet-timer-000", supportSource.ContentRef) - assert.Equal(t, "content", supportSource.ContentKey) + assert.Contains(t, environment.Integration.Status.GeneratedSources, expectedFlowSourceTimerV1, expectedFlowSinkLoggerMain) - assert.Equal(t, []string{"camel:log", "camel:tbd", "camel:timer", "camel:xxx"}, environment.Integration.Status.Dependencies) + assert.Contains(t, environment.Integration.Status.Dependencies, + "camel:log", "camel:tbd", "camel:timer", "camel:xxx", "camel:xxx-2") } func TestKameletConfigLookup(t *testing.T) { @@ -318,14 +366,16 @@ func TestKameletConfigLookup(t *testing.T) { Name: "timer", }, Spec: v1.KameletSpec{ - Template: templateOrFail(map[string]interface{}{ - "from": map[string]interface{}{ - "uri": "timer:tick", + KameletSpecBase: v1.KameletSpecBase{ + Template: templateOrFail(map[string]interface{}{ + "from": map[string]interface{}{ + "uri": "timer:tick", + }, + }), + Dependencies: []string{ + "camel:timer", + "camel:log", }, - }), - Dependencies: []string{ - "camel:timer", - "camel:log", }, }, }, &corev1.Secret{ @@ -358,7 +408,7 @@ func TestKameletConfigLookup(t *testing.T) { require.NoError(t, err) assert.True(t, enabled) assert.Nil(t, condition) - assert.Equal(t, []string{"timer"}, trait.getKameletKeys()) + assert.Equal(t, []string{"timer"}, trait.getKameletKeys(false)) } func TestKameletNamedConfigLookup(t *testing.T) { @@ -373,14 +423,16 @@ func TestKameletNamedConfigLookup(t *testing.T) { Name: "timer", }, Spec: v1.KameletSpec{ - Template: templateOrFail(map[string]interface{}{ - "from": map[string]interface{}{ - "uri": "timer:tick", + KameletSpecBase: v1.KameletSpecBase{ + Template: templateOrFail(map[string]interface{}{ + "from": map[string]interface{}{ + "uri": "timer:tick", + }, + }), + Dependencies: []string{ + "camel:timer", + "camel:log", }, - }), - Dependencies: []string{ - "camel:timer", - "camel:log", }, }, }, &corev1.Secret{ @@ -414,7 +466,7 @@ func TestKameletNamedConfigLookup(t *testing.T) { require.NoError(t, err) assert.True(t, enabled) assert.Nil(t, condition) - assert.Equal(t, []string{"timer"}, trait.getKameletKeys()) + assert.Equal(t, []string{"timer"}, trait.getKameletKeys(false)) } func TestKameletConditionFalse(t *testing.T) { @@ -432,11 +484,13 @@ func TestKameletConditionFalse(t *testing.T) { Name: "timer", }, Spec: v1.KameletSpec{ - Template: templateOrFail(map[string]interface{}{ - "from": map[string]interface{}{ - "uri": "timer:tick", - }, - }), + KameletSpecBase: v1.KameletSpecBase{ + Template: templateOrFail(map[string]interface{}{ + "from": map[string]interface{}{ + "uri": "timer:tick", + }, + }), + }, }, }) @@ -471,11 +525,13 @@ func TestKameletConditionTrue(t *testing.T) { Name: "timer", }, Spec: v1.KameletSpec{ - Template: templateOrFail(map[string]interface{}{ - "from": map[string]interface{}{ - "uri": "timer:tick", - }, - }), + KameletSpecBase: v1.KameletSpecBase{ + Template: templateOrFail(map[string]interface{}{ + "from": map[string]interface{}{ + "uri": "timer:tick", + }, + }), + }, }, }, &v1.Kamelet{ @@ -484,11 +540,13 @@ func TestKameletConditionTrue(t *testing.T) { Name: "none", }, Spec: v1.KameletSpec{ - Template: templateOrFail(map[string]interface{}{ - "from": map[string]interface{}{ - "uri": "timer:tick", - }, - }), + KameletSpecBase: v1.KameletSpecBase{ + Template: templateOrFail(map[string]interface{}{ + "from": map[string]interface{}{ + "uri": "timer:tick", + }, + }), + }, }, }) @@ -568,11 +626,13 @@ func TestKameletSyntheticKitConditionTrue(t *testing.T) { Name: "timer-source", }, Spec: v1.KameletSpec{ - Template: templateOrFail(map[string]interface{}{ - "from": map[string]interface{}{ - "uri": "timer:tick", - }, - }), + KameletSpecBase: v1.KameletSpecBase{ + Template: templateOrFail(map[string]interface{}{ + "from": map[string]interface{}{ + "uri": "timer:tick", + }, + }), + }, }, }) environment.CamelCatalog = nil @@ -611,11 +671,13 @@ func TestKameletSyntheticKitAutoConditionFalse(t *testing.T) { Name: "timer-source", }, Spec: v1.KameletSpec{ - Template: templateOrFail(map[string]interface{}{ - "from": map[string]interface{}{ - "uri": "timer:tick", - }, - }), + KameletSpecBase: v1.KameletSpecBase{ + Template: templateOrFail(map[string]interface{}{ + "from": map[string]interface{}{ + "uri": "timer:tick", + }, + }), + }, }, }) environment.Integration.Spec.Sources = nil diff --git a/pkg/util/bindings/bindings_test.go b/pkg/util/bindings/bindings_test.go index f1e06b7a04..eeff83b805 100644 --- a/pkg/util/bindings/bindings_test.go +++ b/pkg/util/bindings/bindings_test.go @@ -163,6 +163,24 @@ func TestBindings(t *testing.T) { "camel.kamelet.mykamelet.sink.mymessage": "myval", }, }, + { + endpointType: v1.EndpointTypeSink, + endpoint: v1.Endpoint{ + Ref: &corev1.ObjectReference{ + Kind: "Kamelet", + APIVersion: "camel.apache.org/v1any1", + Name: "mykamelet", + }, + Properties: asEndpointProperties(map[string]string{ + "mymessage": "myval", + "kameletVersion": "v1", + }), + }, + uri: "kamelet:mykamelet/sink?kameletVersion=v1", + props: map[string]string{ + "camel.kamelet.mykamelet.sink.mymessage": "myval", + }, + }, { endpoint: v1.Endpoint{ Ref: &corev1.ObjectReference{ diff --git a/pkg/util/bindings/kamelet.go b/pkg/util/bindings/kamelet.go index 300207bad9..d1001c87ed 100644 --- a/pkg/util/bindings/kamelet.go +++ b/pkg/util/bindings/kamelet.go @@ -41,8 +41,6 @@ func (k BindingConverter) ID() string { } // Translate --. -// -//nolint:dupl func (k BindingConverter) Translate(ctx BindingContext, endpointCtx EndpointContext, e v1.Endpoint) (*Binding, error) { if e.Ref == nil { // works only on refs @@ -71,6 +69,12 @@ func (k BindingConverter) Translate(ctx BindingContext, endpointCtx EndpointCont } else { id = endpointCtx.GenerateID() } + version, versionPresent := props[v1.KameletVersionProperty] + if versionPresent { + delete(props, v1.KameletVersionProperty) + } + + kameletTranslated := getKameletName(kameletName, id, version) binding := Binding{} binding.ApplicationProperties = make(map[string]string) @@ -97,7 +101,7 @@ func (k BindingConverter) Translate(ctx BindingContext, endpointCtx EndpointCont steps = append(steps, map[string]interface{}{ "kamelet": map[string]interface{}{ - "name": fmt.Sprintf("%s/%s", kameletName, url.PathEscape(id)), + "name": kameletTranslated, }, }) @@ -126,7 +130,7 @@ func (k BindingConverter) Translate(ctx BindingContext, endpointCtx EndpointCont } } - binding.URI = fmt.Sprintf("kamelet:%s/%s", kameletName, url.PathEscape(id)) + binding.URI = fmt.Sprintf("kamelet:%s", kameletTranslated) case v1.EndpointTypeSink: if in, applicationProperties := k.DataTypeStep(e, id, v1.TypeSlotIn, dataTypeActionKamelet); in != nil { binding.Step = in @@ -135,14 +139,22 @@ func (k BindingConverter) Translate(ctx BindingContext, endpointCtx EndpointCont } } - binding.URI = fmt.Sprintf("kamelet:%s/%s", kameletName, url.PathEscape(id)) + binding.URI = fmt.Sprintf("kamelet:%s", kameletTranslated) default: - binding.URI = fmt.Sprintf("kamelet:%s/%s", kameletName, url.PathEscape(id)) + binding.URI = fmt.Sprintf("kamelet:%s", kameletTranslated) } return &binding, nil } +func getKameletName(name, id, version string) string { + kamelet := fmt.Sprintf("%s/%s", name, url.PathEscape(id)) + if version != "" { + kamelet = fmt.Sprintf("%s?%s=%s", kamelet, v1.KameletVersionProperty, version) + } + return kamelet +} + // DataTypeStep --. func (k BindingConverter) DataTypeStep(e v1.Endpoint, id string, typeSlot v1.TypeSlot, dataTypeActionKamelet string) (map[string]interface{}, map[string]string) { if e.DataTypes == nil { @@ -193,8 +205,6 @@ func (k V1alpha1BindingConverter) ID() string { // Translate -- . // Deprecated. -// -//nolint:dupl func (k V1alpha1BindingConverter) Translate(ctx V1alpha1BindingContext, endpointCtx V1alpha1EndpointContext, e v1alpha1.Endpoint) (*Binding, error) { if e.Ref == nil { // works only on refs diff --git a/pkg/util/source/kamelet.go b/pkg/util/source/kamelet.go index a9a155efac..d44f542a2c 100644 --- a/pkg/util/source/kamelet.go +++ b/pkg/util/source/kamelet.go @@ -18,10 +18,14 @@ limitations under the License. package source import ( + "fmt" "regexp" + + v1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1" ) var kameletNameRegexp = regexp.MustCompile("kamelet:(?://)?([a-z0-9-.]+(/[a-z0-9-.]+)?)(?:$|[^a-z0-9-.].*)") +var kameletVersionRegexp = regexp.MustCompile(v1.KameletVersionProperty + "=([a-z0-9-.]+)") func ExtractKamelets(uris []string) []string { var kamelets []string @@ -37,6 +41,10 @@ func ExtractKamelets(uris []string) []string { func ExtractKamelet(uri string) string { matches := kameletNameRegexp.FindStringSubmatch(uri) if len(matches) > 1 { + version := kameletVersionRegexp.FindString(uri) + if version != "" { + return fmt.Sprintf("%s?%s", matches[1], version) + } return matches[1] } return "" diff --git a/pkg/util/source/kamelet_test.go b/pkg/util/source/kamelet_test.go new file mode 100644 index 0000000000..173183c2d3 --- /dev/null +++ b/pkg/util/source/kamelet_test.go @@ -0,0 +1,34 @@ +/* +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 source + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestExtractKamelets(t *testing.T) { + assert.Equal(t, "", ExtractKamelet("fail")) + assert.Equal(t, "my-test", ExtractKamelet("kamelet:my-test")) + assert.Equal(t, "my-test", ExtractKamelet("kamelet:my-test?")) + assert.Equal(t, "my-test", ExtractKamelet("kamelet:my-test?option=1")) + assert.Equal(t, "my-test", ExtractKamelet("kamelet:my-test?option=1&opt2=2")) + assert.Equal(t, "my-test?kameletVersion=v1", ExtractKamelet("kamelet:my-test?option=1&opt2=2&kameletVersion=v1")) + assert.Equal(t, "my-test?kameletVersion=v1", ExtractKamelet("kamelet:my-test?kameletVersion=v1")) +}