| /* |
| Licensed to the Apache Software Foundation (ASF) under one or more |
| contributor license agreements. See the NOTICE file distributed with |
| this work for additional information regarding copyright ownership. |
| The ASF licenses this file to You under the Apache License, Version 2.0 |
| (the "License"); you may not use this file except in compliance with |
| the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| */ |
| |
| package trait |
| |
| import ( |
| "context" |
| "encoding/json" |
| "errors" |
| "fmt" |
| "reflect" |
| "regexp" |
| "sort" |
| "strings" |
| |
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
| |
| ctrl "sigs.k8s.io/controller-runtime/pkg/client" |
| |
| v1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1" |
| "github.com/apache/camel-k/v2/pkg/apis/camel/v1alpha1" |
| "github.com/apache/camel-k/v2/pkg/client" |
| "github.com/apache/camel-k/v2/pkg/metadata" |
| "github.com/apache/camel-k/v2/pkg/util" |
| "github.com/apache/camel-k/v2/pkg/util/camel" |
| "github.com/apache/camel-k/v2/pkg/util/kubernetes" |
| "github.com/apache/camel-k/v2/pkg/util/property" |
| "github.com/apache/camel-k/v2/pkg/util/sets" |
| "github.com/apache/camel-k/v2/pkg/util/uri" |
| ) |
| |
| func ptrFrom[T any](value T) *T { |
| return &value |
| } |
| |
| type Options map[string]map[string]interface{} |
| |
| func (u Options) Get(id string) (map[string]interface{}, bool) { |
| if t, ok := u[id]; ok { |
| return t, true |
| } |
| |
| if addons, ok := u["addons"]; ok { |
| if addon, ok := addons[id]; ok { |
| if t, ok := addon.(map[string]interface{}); ok { |
| return t, true |
| } |
| } |
| } |
| |
| return nil, false |
| } |
| |
| var exactVersionRegexp = regexp.MustCompile(`^(\d+)\.(\d+)\.(\d+)([\w-.]*)$`) |
| |
| // getIntegrationKit retrieves the kit set on the integration. |
| func getIntegrationKit(ctx context.Context, c client.Client, integration *v1.Integration) (*v1.IntegrationKit, error) { |
| if integration.Status.IntegrationKit == nil { |
| return nil, nil |
| } |
| kit := v1.NewIntegrationKit(integration.Status.IntegrationKit.Namespace, integration.Status.IntegrationKit.Name) |
| err := c.Get(ctx, ctrl.ObjectKeyFromObject(kit), kit) |
| return kit, err |
| } |
| |
| func collectConfigurationValues(configurationType string, configurable ...v1.Configurable) []string { |
| result := sets.NewSet() |
| |
| for _, c := range configurable { |
| c := c |
| |
| if c == nil || reflect.ValueOf(c).IsNil() { |
| continue |
| } |
| |
| entries := c.Configurations() |
| if entries == nil { |
| continue |
| } |
| |
| for _, entry := range entries { |
| if entry.Type == configurationType { |
| result.Add(entry.Value) |
| } |
| } |
| } |
| |
| s := result.List() |
| sort.Strings(s) |
| return s |
| } |
| |
| func collectConfigurations(configurationType string, configurable ...v1.Configurable) []map[string]string { |
| var result []map[string]string |
| |
| for _, c := range configurable { |
| c := c |
| |
| if c == nil || reflect.ValueOf(c).IsNil() { |
| continue |
| } |
| |
| entries := c.Configurations() |
| if entries == nil { |
| continue |
| } |
| |
| for _, entry := range entries { |
| if entry.Type == configurationType { |
| item := make(map[string]string) |
| item["value"] = entry.Value |
| result = append(result, item) |
| } |
| } |
| } |
| |
| return result |
| } |
| |
| func collectConfigurationPairs(configurationType string, configurable ...v1.Configurable) []variable { |
| result := make([]variable, 0) |
| |
| for _, c := range configurable { |
| c := c |
| |
| if c == nil || reflect.ValueOf(c).IsNil() { |
| continue |
| } |
| |
| entries := c.Configurations() |
| if entries == nil { |
| continue |
| } |
| |
| for _, entry := range entries { |
| if entry.Type == configurationType { |
| k, v := property.SplitPropertyFileEntry(entry.Value) |
| if k == "" { |
| continue |
| } |
| |
| ok := false |
| for i, variable := range result { |
| if variable.Name == k { |
| result[i].Value = v |
| ok = true |
| break |
| } |
| } |
| if !ok { |
| result = append(result, variable{Name: k, Value: v}) |
| } |
| } |
| } |
| } |
| |
| return result |
| } |
| |
| var keyValuePairRegexp = regexp.MustCompile(`^(\w+)=(.+)$`) |
| |
| func keyValuePairArrayAsStringMap(pairs []string) (map[string]string, error) { |
| m := make(map[string]string) |
| |
| for _, pair := range pairs { |
| if match := keyValuePairRegexp.FindStringSubmatch(pair); match != nil { |
| m[match[1]] = match[2] |
| } else { |
| return nil, fmt.Errorf("unable to parse key/value pair: %s", pair) |
| } |
| } |
| |
| return m, nil |
| } |
| |
| // filterTransferableAnnotations returns a map containing annotations that are meaningful for being transferred to child resources. |
| func filterTransferableAnnotations(annotations map[string]string) map[string]string { |
| res := make(map[string]string) |
| for k, v := range annotations { |
| if strings.HasPrefix(k, "kubectl.kubernetes.io") { |
| // filter out kubectl annotations |
| continue |
| } |
| res[k] = v |
| } |
| return res |
| } |
| |
| // ExtractSourceDependencies extracts dependencies from source. |
| func ExtractSourceDependencies(source v1.SourceSpec, catalog *camel.RuntimeCatalog) (*sets.Set, error) { |
| dependencies := sets.NewSet() |
| |
| // Add auto-detected dependencies |
| meta, err := metadata.Extract(catalog, source) |
| if err != nil { |
| return nil, err |
| } |
| dependencies.Merge(meta.Dependencies) |
| |
| // Add loader dependencies |
| lang := source.InferLanguage() |
| for loader, v := range catalog.Loaders { |
| // add loader specific dependencies |
| if source.Loader != "" && source.Loader == loader { |
| dependencies.Add(v.GetDependencyID()) |
| |
| for _, d := range v.Dependencies { |
| dependencies.Add(d.GetDependencyID()) |
| } |
| } else if source.Loader == "" { |
| // add language specific dependencies |
| if util.StringSliceExists(v.Languages, string(lang)) { |
| dependencies.Add(v.GetDependencyID()) |
| |
| for _, d := range v.Dependencies { |
| dependencies.Add(d.GetDependencyID()) |
| } |
| } |
| } |
| } |
| |
| return dependencies, nil |
| } |
| |
| // AssertTraitsType asserts that traits is either v1.Traits or v1.IntegrationKitTraits. |
| // This function is provided because Go doesn't have Either nor union types. |
| func AssertTraitsType(traits interface{}) error { |
| _, ok1 := traits.(v1.Traits) |
| _, ok2 := traits.(v1.IntegrationKitTraits) |
| if !ok1 && !ok2 { |
| return errors.New("traits must be either v1.Traits or v1.IntegrationKitTraits") |
| } |
| |
| return nil |
| } |
| |
| // ToTraitMap accepts either v1.Traits or v1.IntegrationKitTraits and converts it to a map of traits. |
| func ToTraitMap(traits interface{}) (Options, error) { |
| if err := AssertTraitsType(traits); err != nil { |
| return nil, err |
| } |
| |
| data, err := json.Marshal(traits) |
| if err != nil { |
| return nil, err |
| } |
| traitMap := make(Options) |
| if err = json.Unmarshal(data, &traitMap); err != nil { |
| return nil, err |
| } |
| |
| return traitMap, nil |
| } |
| |
| // ToPropertyMap accepts a trait and converts it to a map of trait properties. |
| func ToPropertyMap(trait interface{}) (map[string]interface{}, error) { |
| data, err := json.Marshal(trait) |
| if err != nil { |
| return nil, err |
| } |
| propMap := make(map[string]interface{}) |
| if err = json.Unmarshal(data, &propMap); err != nil { |
| return nil, err |
| } |
| |
| return propMap, nil |
| } |
| |
| // MigrateLegacyConfiguration moves up the legacy configuration in a trait to the new top-level properties. |
| // Values of the new properties always take precedence over the ones from the legacy configuration |
| // with the same property names. |
| func MigrateLegacyConfiguration(trait map[string]interface{}) error { |
| if trait["configuration"] == nil { |
| return nil |
| } |
| |
| if config, ok := trait["configuration"].(map[string]interface{}); ok { |
| // For traits that had the same property name "configuration", |
| // the property needs to be renamed to "config" to avoid naming conflicts |
| // (e.g. Knative trait). |
| if config["configuration"] != nil { |
| config["config"] = config["configuration"] |
| delete(config, "configuration") |
| } |
| |
| for k, v := range config { |
| if trait[k] == nil { |
| trait[k] = v |
| } |
| } |
| delete(trait, "configuration") |
| } else { |
| return fmt.Errorf(`unexpected type for "configuration" field: %v`, reflect.TypeOf(trait["configuration"])) |
| } |
| |
| return nil |
| } |
| |
| // ToTrait unmarshals a map configuration to a target trait. |
| func ToTrait(trait map[string]interface{}, target interface{}) error { |
| data, err := json.Marshal(trait) |
| if err != nil { |
| return err |
| } |
| err = json.Unmarshal(data, &target) |
| if err != nil { |
| return err |
| } |
| |
| return nil |
| } |
| |
| func getBuilderTask(tasks []v1.Task) *v1.BuilderTask { |
| for i, task := range tasks { |
| if task.Builder != nil { |
| return tasks[i].Builder |
| } |
| } |
| return nil |
| } |
| |
| func getPackageTask(tasks []v1.Task) *v1.BuilderTask { |
| for i, task := range tasks { |
| if task.Package != nil { |
| return tasks[i].Package |
| } |
| } |
| return nil |
| } |
| |
| // Equals return if traits are the same. |
| func Equals(i1 Options, i2 Options) bool { |
| return reflect.DeepEqual(i1, i2) |
| } |
| |
| // IntegrationsHaveSameTraits return if traits are the same. |
| func IntegrationsHaveSameTraits(i1 *v1.Integration, i2 *v1.Integration) (bool, error) { |
| c1, err := NewSpecTraitsOptionsForIntegration(i1) |
| if err != nil { |
| return false, err |
| } |
| c2, err := NewSpecTraitsOptionsForIntegration(i2) |
| if err != nil { |
| return false, err |
| } |
| |
| return Equals(c1, c2), nil |
| } |
| |
| // PipesHaveSameTraits return if traits are the same. |
| func PipesHaveSameTraits(i1 *v1.Pipe, i2 *v1.Pipe) (bool, error) { |
| c1, err := NewTraitsOptionsForPipe(i1) |
| if err != nil { |
| return false, err |
| } |
| c2, err := NewTraitsOptionsForPipe(i2) |
| if err != nil { |
| return false, err |
| } |
| |
| return Equals(c1, c2), nil |
| } |
| |
| // KameletBindingsHaveSameTraits return if traits are the same. |
| // Deprecated. |
| func KameletBindingsHaveSameTraits(i1 *v1alpha1.KameletBinding, i2 *v1alpha1.KameletBinding) (bool, error) { |
| c1, err := NewTraitsOptionsForKameletBinding(i1) |
| if err != nil { |
| return false, err |
| } |
| c2, err := NewTraitsOptionsForKameletBinding(i2) |
| if err != nil { |
| return false, err |
| } |
| |
| return Equals(c1, c2), nil |
| } |
| |
| // IntegrationAndPipeSameTraits return if traits are the same. |
| // The comparison is done for the subset of traits defines on the binding as during the trait processing, |
| // some traits may be added to the Integration i.e. knative configuration in case of sink binding. |
| func IntegrationAndPipeSameTraits(i1 *v1.Integration, i2 *v1.Pipe) (bool, error) { |
| itOpts, err := NewSpecTraitsOptionsForIntegration(i1) |
| if err != nil { |
| return false, err |
| } |
| klbOpts, err := NewTraitsOptionsForPipe(i2) |
| if err != nil { |
| return false, err |
| } |
| |
| toCompare := make(Options) |
| for k := range klbOpts { |
| if v, ok := itOpts[k]; ok { |
| toCompare[k] = v |
| } |
| } |
| |
| return Equals(klbOpts, toCompare), nil |
| } |
| |
| // IntegrationAndKameletBindingSameTraits return if traits are the same. |
| // The comparison is done for the subset of traits defines on the binding as during the trait processing, |
| // some traits may be added to the Integration i.e. knative configuration in case of sink binding. |
| // Deprecated. |
| func IntegrationAndKameletBindingSameTraits(i1 *v1.Integration, i2 *v1alpha1.KameletBinding) (bool, error) { |
| itOpts, err := NewSpecTraitsOptionsForIntegration(i1) |
| if err != nil { |
| return false, err |
| } |
| klbOpts, err := NewTraitsOptionsForKameletBinding(i2) |
| if err != nil { |
| return false, err |
| } |
| |
| toCompare := make(Options) |
| for k := range klbOpts { |
| if v, ok := itOpts[k]; ok { |
| toCompare[k] = v |
| } |
| } |
| |
| return Equals(klbOpts, toCompare), nil |
| } |
| |
| func newTraitsOptions(opts Options, objectMeta *metav1.ObjectMeta) (Options, error) { |
| m2, err := FromAnnotations(objectMeta) |
| if err != nil { |
| return nil, err |
| } |
| |
| for k, v := range m2 { |
| opts[k] = v |
| } |
| |
| return opts, nil |
| } |
| |
| func NewSpecTraitsOptionsForIntegrationAndPlatform(i *v1.Integration, pl *v1.IntegrationPlatform) (Options, error) { |
| var options Options |
| var err error |
| if pl != nil { |
| options, err = ToTraitMap(pl.Status.Traits) |
| if err != nil { |
| return nil, err |
| } |
| } else { |
| options = Options{} |
| } |
| |
| m1, err := ToTraitMap(i.Spec.Traits) |
| if err != nil { |
| return nil, err |
| } |
| |
| for k, v := range m1 { |
| options[k] = v |
| } |
| |
| return newTraitsOptions(options, &i.ObjectMeta) |
| } |
| |
| func NewSpecTraitsOptionsForIntegration(i *v1.Integration) (Options, error) { |
| m1, err := ToTraitMap(i.Spec.Traits) |
| if err != nil { |
| return nil, err |
| } |
| |
| return newTraitsOptions(m1, &i.ObjectMeta) |
| } |
| |
| func newTraitsOptionsForIntegrationKit(i *v1.IntegrationKit, traits v1.IntegrationKitTraits) (Options, error) { |
| m1, err := ToTraitMap(traits) |
| if err != nil { |
| return nil, err |
| } |
| |
| return newTraitsOptions(m1, &i.ObjectMeta) |
| } |
| |
| func NewSpecTraitsOptionsForIntegrationKit(i *v1.IntegrationKit) (Options, error) { |
| return newTraitsOptionsForIntegrationKit(i, i.Spec.Traits) |
| } |
| |
| func NewTraitsOptionsForPipe(pipe *v1.Pipe) (Options, error) { |
| options := Options{} |
| |
| if pipe.Spec.Integration != nil { |
| m1, err := ToTraitMap(pipe.Spec.Integration.Traits) |
| if err != nil { |
| return nil, err |
| } |
| |
| for k, v := range m1 { |
| options[k] = v |
| } |
| } |
| |
| return newTraitsOptions(options, &pipe.ObjectMeta) |
| } |
| |
| // Deprecated. |
| func NewTraitsOptionsForKameletBinding(kb *v1alpha1.KameletBinding) (Options, error) { |
| options := Options{} |
| |
| if kb.Spec.Integration != nil { |
| m1, err := ToTraitMap(kb.Spec.Integration.Traits) |
| if err != nil { |
| return nil, err |
| } |
| |
| for k, v := range m1 { |
| options[k] = v |
| } |
| } |
| |
| return newTraitsOptions(options, &kb.ObjectMeta) |
| } |
| |
| func FromAnnotations(meta *metav1.ObjectMeta) (Options, error) { |
| options := make(Options) |
| for k, v := range meta.Annotations { |
| if strings.HasPrefix(k, v1.TraitAnnotationPrefix) { |
| configKey := strings.TrimPrefix(k, v1.TraitAnnotationPrefix) |
| if strings.Contains(configKey, ".") { |
| parts := strings.SplitN(configKey, ".", 2) |
| id := parts[0] |
| prop := parts[1] |
| if _, ok := options[id]; !ok { |
| options[id] = make(map[string]interface{}) |
| } |
| options[id][prop] = stringOrSlice(v) |
| } else { |
| return options, fmt.Errorf("wrong format for trait annotation %q: missing trait ID", k) |
| } |
| } |
| } |
| |
| return options, nil |
| } |
| |
| // stringOrSlice returns either a string or a slice with trimmed values when the input is |
| // represented as an array style (ie, [a,b,c]). |
| func stringOrSlice(val string) interface{} { |
| if val == "[]" { |
| // empty array |
| return []string{} |
| } |
| if strings.HasPrefix(val, "[") && strings.HasSuffix(val, "]") { |
| slice := strings.Split(val[1:len(val)-1], ",") |
| for i := range slice { |
| slice[i] = strings.Trim(slice[i], " ") |
| } |
| return slice |
| } else { |
| return val |
| } |
| } |
| |
| // verify if the integration in the Environment contains an endpoint. |
| func containsEndpoint(name string, e *Environment, c client.Client) (bool, error) { |
| sources, err := kubernetes.ResolveIntegrationSources(e.Ctx, c, e.Integration, e.Resources) |
| if err != nil { |
| return false, err |
| } |
| |
| meta, err := metadata.ExtractAll(e.CamelCatalog, sources) |
| if err != nil { |
| return false, err |
| } |
| |
| hasKnativeEndpoint := false |
| endpoints := make([]string, 0) |
| endpoints = append(endpoints, meta.FromURIs...) |
| endpoints = append(endpoints, meta.ToURIs...) |
| for _, endpoint := range endpoints { |
| if uri.GetComponent(endpoint) == name { |
| hasKnativeEndpoint = true |
| break |
| } |
| } |
| return hasKnativeEndpoint, nil |
| } |
| |
| // HasMatchingTraits verifies if two traits options match. |
| func HasMatchingTraits(traitMap Options, kitTraitMap Options) (bool, error) { |
| catalog := NewCatalog(nil) |
| |
| for _, t := range catalog.AllTraits() { |
| if t == nil || !t.InfluencesKit() { |
| // We don't store the trait configuration if the trait cannot influence the kit behavior |
| continue |
| } |
| id := string(t.ID()) |
| it, _ := traitMap.Get(id) |
| kt, _ := kitTraitMap.Get(id) |
| if ct, ok := t.(ComparableTrait); ok { |
| // if it's match trait use its matches method to determine the match |
| if match, err := matchesComparableTrait(ct, it, kt); !match || err != nil { |
| return false, err |
| } |
| } else { |
| if !matchesTrait(it, kt) { |
| return false, nil |
| } |
| } |
| } |
| |
| return true, nil |
| } |
| |
| func matchesComparableTrait(ct ComparableTrait, it map[string]interface{}, kt map[string]interface{}) (bool, error) { |
| t1 := reflect.New(reflect.TypeOf(ct).Elem()).Interface() |
| if err := ToTrait(it, &t1); err != nil { |
| return false, err |
| } |
| t2 := reflect.New(reflect.TypeOf(ct).Elem()).Interface() |
| if err := ToTrait(kt, &t2); err != nil { |
| return false, err |
| } |
| ct2, ok := t2.(ComparableTrait) |
| if !ok { |
| return false, fmt.Errorf("type assertion failed: %v", t2) |
| } |
| tt1, ok := t1.(Trait) |
| if !ok { |
| return false, fmt.Errorf("type assertion failed: %v", t1) |
| } |
| |
| return ct2.Matches(tt1), nil |
| } |
| |
| func matchesTrait(it map[string]interface{}, kt map[string]interface{}) bool { |
| // perform exact match on the two trait maps |
| return reflect.DeepEqual(it, kt) |
| } |