| /* |
| 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 cmd |
| |
| import ( |
| "encoding/json" |
| "errors" |
| "fmt" |
| "reflect" |
| "regexp" |
| "strings" |
| |
| v1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1" |
| "github.com/apache/camel-k/v2/pkg/trait" |
| "github.com/apache/camel-k/v2/pkg/util" |
| "github.com/mitchellh/mapstructure" |
| ) |
| |
| type optionMap map[string]map[string]interface{} |
| |
| // The list of known addons is used for handling backward compatibility. |
| var knownAddons = []string{"keda", "master", "strimzi", "3scale", "tracing"} |
| |
| var traitConfigRegexp = regexp.MustCompile(`^([a-z0-9-]+)((?:\.[a-z0-9-]+)(?:\[[0-9]+\]|\..+)*)=(.*)$`) |
| |
| func validateTraits(catalog *trait.Catalog, traits []string) error { |
| for _, t := range traits { |
| tr := catalog.GetTrait(t) |
| if tr == nil { |
| return fmt.Errorf("trait %s does not exist in catalog", t) |
| } |
| } |
| |
| return nil |
| } |
| |
| func configureTraits(options []string, traits interface{}, catalog trait.Finder) error { |
| config, err := optionsToMap(options) |
| if err != nil { |
| return err |
| } |
| |
| // Known addons need to be put aside here, as otherwise the deprecated addon fields on |
| // Traits might be accidentally populated. The deprecated addon fields are preserved |
| // for backward compatibility and should be populated only when the operator reads |
| // existing CRs from the API server. |
| addons := make(optionMap) |
| for _, id := range knownAddons { |
| if config[id] != nil { |
| addons[id] = config[id] |
| delete(config, id) |
| } |
| } |
| |
| md := mapstructure.Metadata{} |
| decoder, err := mapstructure.NewDecoder( |
| &mapstructure.DecoderConfig{ |
| Metadata: &md, |
| WeaklyTypedInput: true, |
| TagName: "property", |
| Result: &traits, |
| }, |
| ) |
| if err != nil { |
| return err |
| } |
| |
| if err = decoder.Decode(config); err != nil { |
| return err |
| } |
| |
| // in case there are unknown addons |
| for _, prop := range md.Unused { |
| id := strings.Split(prop, ".")[0] |
| addons[id] = config[id] |
| } |
| |
| if len(addons) == 0 { |
| return nil |
| } |
| return configureAddons(addons, traits, catalog) |
| } |
| |
| func optionsToMap(options []string) (optionMap, error) { |
| optionMap := make(optionMap) |
| |
| for _, option := range options { |
| parts := traitConfigRegexp.FindStringSubmatch(option) |
| if len(parts) < 4 { |
| return nil, errors.New("unrecognized config format (expected \"<trait>.<prop>=<value>\"): " + option) |
| } |
| id := parts[1] |
| fullProp := parts[2][1:] |
| value := parts[3] |
| if _, ok := optionMap[id]; !ok { |
| optionMap[id] = make(map[string]interface{}) |
| } |
| |
| propParts := util.ConfigTreePropertySplit(fullProp) |
| var current = optionMap[id] |
| if len(propParts) > 1 { |
| c, err := util.NavigateConfigTree(current, propParts[0:len(propParts)-1]) |
| if err != nil { |
| return nil, err |
| } |
| if cc, ok := c.(map[string]interface{}); ok { |
| current = cc |
| } else { |
| return nil, errors.New("trait configuration cannot end with a slice") |
| } |
| } |
| |
| prop := propParts[len(propParts)-1] |
| switch v := current[prop].(type) { |
| case []string: |
| current[prop] = append(v, value) |
| case string: |
| // Aggregate multiple occurrences of the same option into a string array, to emulate POSIX conventions. |
| // This enables executing: |
| // $ kamel run -t <trait>.<property>=<value_1> ... -t <trait>.<property>=<value_N> |
| // Or: |
| // $ kamel run --trait <trait>.<property>=<value_1>,...,<trait>.<property>=<value_N> |
| current[prop] = []string{v, value} |
| case nil: |
| current[prop] = value |
| } |
| } |
| |
| return optionMap, nil |
| } |
| |
| func configureAddons(config optionMap, traits interface{}, catalog trait.Finder) error { |
| // Addon traits require raw message mapping |
| addons := make(map[string]v1.AddonTrait) |
| for id, props := range config { |
| t := catalog.GetTrait(id) |
| if t != nil { |
| // let's take a clone to prevent default values set at runtime from being serialized |
| zero := reflect.New(reflect.TypeOf(t)).Interface() |
| if err := configureAddon(props, zero); err != nil { |
| return err |
| } |
| data, err := json.Marshal(zero) |
| if err != nil { |
| return err |
| } |
| addon := v1.AddonTrait{} |
| if err = json.Unmarshal(data, &addon); err != nil { |
| return err |
| } |
| addons[id] = addon |
| } |
| } |
| if len(addons) > 0 { |
| if ts, ok := traits.(*v1.Traits); ok { |
| ts.Addons = addons |
| } |
| if ikts, ok := traits.(*v1.IntegrationKitTraits); ok { |
| ikts.Addons = addons |
| } |
| } |
| |
| return nil |
| } |
| |
| func configureAddon(props map[string]interface{}, trait interface{}) error { |
| md := mapstructure.Metadata{} |
| decoder, err := mapstructure.NewDecoder( |
| &mapstructure.DecoderConfig{ |
| Metadata: &md, |
| WeaklyTypedInput: true, |
| TagName: "property", |
| Result: &trait, |
| }, |
| ) |
| if err != nil { |
| return err |
| } |
| |
| return decoder.Decode(props) |
| } |