pkg/trait/util.go (479 lines of code) (raw):
/*
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
}
func ptrDerefOr[T any](value *T, def T) T {
if value != nil {
return *value
}
return def
}
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
}
// 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 := NewTraitsOptionsForIntegration(i1)
if err != nil {
return false, err
}
c2, err := NewTraitsOptionsForIntegration(i2)
if err != nil {
return false, err
}
return Equals(c1, c2), nil
}
// IntegrationKitsHaveSameTraits return if traits are the same.
func IntegrationKitsHaveSameTraits(i1 *v1.IntegrationKit, i2 *v1.IntegrationKit) (bool, error) {
c1, err := NewTraitsOptionsForIntegrationKit(i1)
if err != nil {
return false, err
}
c2, err := NewTraitsOptionsForIntegrationKit(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 := NewTraitsOptionsForIntegration(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 := NewTraitsOptionsForIntegration(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
}
// IntegrationAndKitHaveSameTraits return if traits are the same.
func IntegrationAndKitHaveSameTraits(i1 *v1.Integration, i2 *v1.IntegrationKit) (bool, error) {
itOpts, err := NewTraitsOptionsForIntegration(i1)
if err != nil {
return false, err
}
ikOpts, err := NewTraitsOptionsForIntegrationKit(i2)
if err != nil {
return false, err
}
return Equals(ikOpts, itOpts), nil
}
func NewTraitsOptionsForIntegration(i *v1.Integration) (Options, error) {
m1, err := ToTraitMap(i.Spec.Traits)
if err != nil {
return nil, err
}
m2, err := FromAnnotations(&i.ObjectMeta)
if err != nil {
return nil, err
}
for k, v := range m2 {
m1[k] = v
}
return m1, nil
}
func NewTraitsOptionsForIntegrationKit(i *v1.IntegrationKit) (Options, error) {
m1, err := ToTraitMap(i.Spec.Traits)
if err != nil {
return nil, err
}
m2, err := FromAnnotations(&i.ObjectMeta)
if err != nil {
return nil, err
}
for k, v := range m2 {
m1[k] = v
}
return m1, nil
}
func NewTraitsOptionsForIntegrationPlatform(i *v1.IntegrationPlatform) (Options, error) {
m1, err := ToTraitMap(i.Spec.Traits)
if err != nil {
return nil, err
}
m2, err := FromAnnotations(&i.ObjectMeta)
if err != nil {
return nil, err
}
for k, v := range m2 {
m1[k] = v
}
return m1, nil
}
func NewTraitsOptionsForPipe(i *v1.Pipe) (Options, error) {
if i.Spec.Integration != nil {
m1, err := ToTraitMap(i.Spec.Integration.Traits)
if err != nil {
return nil, err
}
m2, err := FromAnnotations(&i.ObjectMeta)
if err != nil {
return nil, err
}
for k, v := range m2 {
m1[k] = v
}
return m1, nil
}
m1, err := FromAnnotations(&i.ObjectMeta)
if err != nil {
return nil, err
}
return m1, nil
}
// Deprecated.
func NewTraitsOptionsForKameletBinding(i *v1alpha1.KameletBinding) (Options, error) {
if i.Spec.Integration != nil {
m1, err := ToTraitMap(i.Spec.Integration.Traits)
if err != nil {
return nil, err
}
m2, err := FromAnnotations(&i.ObjectMeta)
if err != nil {
return nil, err
}
for k, v := range m2 {
m1[k] = v
}
return m1, nil
}
m1, err := FromAnnotations(&i.ObjectMeta)
if err != nil {
return nil, err
}
return m1, nil
}
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{})
}
propParts := util.ConfigTreePropertySplit(prop)
var current = options[id]
if len(propParts) > 1 {
c, err := util.NavigateConfigTree(current, propParts[0:len(propParts)-1])
if err != nil {
return options, err
}
if cc, ok := c.(map[string]interface{}); ok {
current = cc
} else {
return options, errors.New(`invalid array specification: to set an array value use the ["v1", "v2"] format`)
}
}
current[prop] = v
} else {
return options, fmt.Errorf("wrong format for trait annotation %q: missing trait ID", k)
}
}
}
return options, nil
}
// 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
}