This is an automated email from the ASF dual-hosted git repository. pcongiusti pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel-k.git
commit d2de352d01dd5491d3bdef37ce99d3a05cb737b7 Author: Pasquale Congiusti <[email protected]> AuthorDate: Fri Nov 26 12:36:55 2021 +0100 chore(cmd/run): resource type package reorg --- pkg/cmd/run.go | 7 +- pkg/cmd/run_help.go | 240 +++------------------ pkg/cmd/run_help_test.go | 134 ------------ pkg/util/resource/config.go | 234 ++++++++++++++++++++ .../resource/config_test.go} | 150 +++---------- 5 files changed, 301 insertions(+), 464 deletions(-) diff --git a/pkg/cmd/run.go b/pkg/cmd/run.go index 767dada..da6eb5e 100644 --- a/pkg/cmd/run.go +++ b/pkg/cmd/run.go @@ -50,6 +50,7 @@ import ( "github.com/apache/camel-k/pkg/util/kubernetes" k8slog "github.com/apache/camel-k/pkg/util/kubernetes/log" "github.com/apache/camel-k/pkg/util/property" + "github.com/apache/camel-k/pkg/util/resource" "github.com/apache/camel-k/pkg/util/sync" "github.com/apache/camel-k/pkg/util/watch" ) @@ -570,8 +571,8 @@ func (o *runCmdOptions) createOrUpdateIntegration(cmd *cobra.Command, c client.C generatedConfigmaps := make([]*corev1.ConfigMap, 0) - for _, resource := range o.Resources { - if config, parseErr := ParseResourceOption(resource); parseErr == nil { + for _, res := range o.Resources { + if config, parseErr := resource.ParseResource(res); parseErr == nil { if genCm, applyResourceOptionErr := ApplyResourceOption(o.Context, config, integration, c, namespace, o.Compression); applyResourceOptionErr != nil { return nil, applyResourceOptionErr } else if genCm != nil { @@ -583,7 +584,7 @@ func (o *runCmdOptions) createOrUpdateIntegration(cmd *cobra.Command, c client.C } for _, item := range o.Configs { - if config, parseErr := ParseConfigOption(item); parseErr == nil { + if config, parseErr := resource.ParseConfig(item); parseErr == nil { if genCm, applyConfigOptionErr := ApplyConfigOption(o.Context, config, integration, c, namespace, o.Compression); applyConfigOptionErr != nil { return nil, applyConfigOptionErr } else if genCm != nil { diff --git a/pkg/cmd/run_help.go b/pkg/cmd/run_help.go index 0a27687..186eecb 100644 --- a/pkg/cmd/run_help.go +++ b/pkg/cmd/run_help.go @@ -22,176 +22,51 @@ import ( "crypto/sha1" //nolint "fmt" "path" - "path/filepath" - "regexp" "strings" v1 "github.com/apache/camel-k/pkg/apis/camel/v1" "github.com/apache/camel-k/pkg/client" - "github.com/apache/camel-k/pkg/util/camel" "github.com/apache/camel-k/pkg/util/kubernetes" + "github.com/apache/camel-k/pkg/util/resource" "github.com/magiconair/properties" corev1 "k8s.io/api/core/v1" - k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) var invalidPaths = []string{"/etc/camel", "/deployments/dependencies"} -// RunConfigOption represents a config option. -type RunConfigOption struct { - configType configOptionType - resourceName string - resourceKey string - destinationPath string -} - -// DestinationPath is the location where the resource will be stored on destination. -func (runConfigOption *RunConfigOption) DestinationPath() string { - return runConfigOption.destinationPath -} - -// Type is the type, converted as string. -func (runConfigOption *RunConfigOption) Type() string { - return string(runConfigOption.configType) -} - -// Name is the name of the resource. -func (runConfigOption *RunConfigOption) Name() string { - return runConfigOption.resourceName -} - -// Key is the key specified for the resource. -func (runConfigOption *RunConfigOption) Key() string { - return runConfigOption.resourceKey -} - -// Validate checks if the DestinationPath is correctly configured. -func (runConfigOption *RunConfigOption) Validate() error { - if runConfigOption.destinationPath == "" { - return nil - } - - // Check for invalid path - for _, invalidPath := range invalidPaths { - if runConfigOption.destinationPath == invalidPath || strings.HasPrefix(runConfigOption.destinationPath, invalidPath+"/") { - return fmt.Errorf("you cannot mount a file under %s path", invalidPath) - } - } - return nil -} - -type configOptionType string - -const ( - // ConfigOptionTypeConfigmap --. - ConfigOptionTypeConfigmap configOptionType = "configmap" - // ConfigOptionTypeSecret --. - ConfigOptionTypeSecret configOptionType = "secret" - // ConfigOptionTypeFile --. - ConfigOptionTypeFile configOptionType = "file" -) - -var ( - validConfigSecretRegexp = regexp.MustCompile(`^(configmap|secret)\:([\w\.\-\_\:\/@]+)$`) - validFileRegexp = regexp.MustCompile(`^file\:([\w\.\-\_\:\/@" ]+)$`) - validResourceRegexp = regexp.MustCompile(`^([\w\.\-\_\:]+)(\/([\w\.\-\_\:]+))?(\@([\w\.\-\_\:\/]+))?$`) -) - -func newRunConfigOption(configType configOptionType, value string) *RunConfigOption { - rn, mk, mp := parseResourceValue(configType, value) - return &RunConfigOption{ - configType: configType, - resourceName: rn, - resourceKey: mk, - destinationPath: mp, - } -} - -func parseResourceValue(configType configOptionType, value string) (resource string, maybeKey string, maybeDestinationPath string) { - if configType == ConfigOptionTypeFile { - resource, maybeDestinationPath = parseFileValue(value) - return resource, "", maybeDestinationPath - } - - return parseCMOrSecretValue(value) -} - -func parseFileValue(value string) (localPath string, maybeDestinationPath string) { - split := strings.SplitN(value, "@", 2) - if len(split) == 2 { - return split[0], split[1] - } - - return value, "" -} - -func parseCMOrSecretValue(value string) (resource string, maybeKey string, maybeDestinationPath string) { - if !validResourceRegexp.MatchString(value) { - return value, "", "" +//nolint +func hashFrom(contents ...[]byte) string { + // SHA1 because we need to limit the length to less than 64 chars + hash := sha1.New() + for _, c := range contents { + hash.Write(c) } - // Must have 3 values - groups := validResourceRegexp.FindStringSubmatch(value) - return groups[1], groups[3], groups[5] + return fmt.Sprintf("%x", hash.Sum(nil)) } -// ParseResourceOption will parse and return a runConfigOption. -func ParseResourceOption(item string) (*RunConfigOption, error) { - // Deprecated: ensure backward compatibility with `--resource filename` format until version 1.5.x - // then replace with parseOption() func directly - option, err := parseOption(item) - if err != nil { - if strings.HasPrefix(err.Error(), "could not match config, secret or file configuration") { - fmt.Printf("Warn: --resource %s has been deprecated. You should use --resource file:%s instead.\n", item, item) - return parseOption("file:" + item) - } - return nil, err +// ApplyConfigOption will set the proper --config option behavior to the IntegrationSpec. +func ApplyConfigOption(ctx context.Context, config *resource.Config, integration *v1.Integration, c client.Client, + namespace string, enableCompression bool) (*corev1.ConfigMap, error) { + // A config option cannot specify destination path + if config.DestinationPath() != "" { + return nil, fmt.Errorf("cannot specify a destination path for this option type") } - - return option, nil -} - -// ParseConfigOption will parse and return a runConfigOption. -func ParseConfigOption(item string) (*RunConfigOption, error) { - return parseOption(item) + return applyOption(ctx, config, integration, c, namespace, enableCompression, v1.ResourceTypeConfig) } -func parseOption(item string) (*RunConfigOption, error) { - var cot configOptionType - var value string - switch { - case validConfigSecretRegexp.MatchString(item): - // parse as secret/configmap - groups := validConfigSecretRegexp.FindStringSubmatch(item) - switch groups[1] { - case "configmap": - cot = ConfigOptionTypeConfigmap - case "secret": - cot = ConfigOptionTypeSecret - } - value = groups[2] - case validFileRegexp.MatchString(item): - // parse as file - groups := validFileRegexp.FindStringSubmatch(item) - cot = ConfigOptionTypeFile - value = groups[1] - default: - return nil, fmt.Errorf("could not match config, secret or file configuration as %s", item) - } - - configurationOption := newRunConfigOption(cot, value) - if err := configurationOption.Validate(); err != nil { - return nil, err - } - return configurationOption, nil +// ApplyResourceOption will set the proper --resource option behavior to the IntegrationSpec. +func ApplyResourceOption(ctx context.Context, config *resource.Config, integration *v1.Integration, c client.Client, + namespace string, enableCompression bool) (*corev1.ConfigMap, error) { + return applyOption(ctx, config, integration, c, namespace, enableCompression, v1.ResourceTypeData) } -func applyOption(ctx context.Context, config *RunConfigOption, integration *v1.Integration, +func applyOption(ctx context.Context, config *resource.Config, integration *v1.Integration, c client.Client, namespace string, enableCompression bool, resourceType v1.ResourceType) (*corev1.ConfigMap, error) { var maybeGenCm *corev1.ConfigMap - switch config.configType { - case ConfigOptionTypeConfigmap: + switch config.Type() { + case resource.StorageTypeConfigmap: cm := kubernetes.LookupConfigmap(ctx, c, namespace, config.Name()) if cm == nil { fmt.Printf("Warn: %s Configmap not found in %s namespace, make sure to provide it before the Integration can run\n", @@ -199,13 +74,13 @@ func applyOption(ctx context.Context, config *RunConfigOption, integration *v1.I } else if resourceType != v1.ResourceTypeData && cm.BinaryData != nil { return maybeGenCm, fmt.Errorf("you cannot provide a binary config, use a text file instead") } - case ConfigOptionTypeSecret: + case resource.StorageTypeSecret: secret := kubernetes.LookupSecret(ctx, c, namespace, config.Name()) if secret == nil { fmt.Printf("Warn: %s Secret not found in %s namespace, make sure to provide it before the Integration can run\n", config.Name(), namespace) } - case ConfigOptionTypeFile: + case resource.StorageTypeFile: // Don't allow a binary non compressed resource rawData, contentType, err := loadRawContent(ctx, config.Name()) if err != nil { @@ -218,79 +93,20 @@ func applyOption(ctx context.Context, config *RunConfigOption, integration *v1.I if err != nil { return maybeGenCm, err } - maybeGenCm, err = convertFileToConfigmap(ctx, c, resourceSpec, config, integration.Namespace, resourceType) + maybeGenCm, err = resource.ConvertFileToConfigmap(ctx, c, resourceSpec, config, integration.Namespace, resourceType) if err != nil { return maybeGenCm, err } default: // Should never reach this - return maybeGenCm, fmt.Errorf("invalid option type %s", config.configType) + return maybeGenCm, fmt.Errorf("invalid option type %s", config.Type()) } - integration.Spec.AddConfigurationAsResource(config.Type(), config.Name(), string(resourceType), config.DestinationPath(), config.Key()) + integration.Spec.AddConfigurationAsResource(string(config.Type()), config.Name(), string(resourceType), config.DestinationPath(), config.Key()) return maybeGenCm, nil } -func convertFileToConfigmap(ctx context.Context, c client.Client, resourceSpec v1.ResourceSpec, config *RunConfigOption, - namespace string, resourceType v1.ResourceType) (*corev1.ConfigMap, error) { - if config.DestinationPath() == "" { - config.resourceKey = filepath.Base(config.Name()) - // As we are changing the resource to a configmap type - // we need to declare the mount path not to use the - // default behavior of a configmap (which include a subdirectory with the configmap name) - if resourceType == v1.ResourceTypeData { - config.destinationPath = camel.ResourcesDefaultMountPath - } else { - config.destinationPath = camel.ConfigResourcesMountPath - } - } else { - config.resourceKey = filepath.Base(config.DestinationPath()) - config.destinationPath = filepath.Dir(config.DestinationPath()) - } - genCmName := fmt.Sprintf("cm-%s", hashFrom([]byte(resourceSpec.Content), resourceSpec.RawContent)) - cm := kubernetes.NewConfigmap(namespace, genCmName, filepath.Base(config.Name()), config.Key(), resourceSpec.Content, resourceSpec.RawContent) - err := c.Create(ctx, cm) - if err != nil { - if k8serrors.IsAlreadyExists(err) { - // We'll reuse it, as is - } else { - return cm, err - } - } - config.configType = ConfigOptionTypeConfigmap - config.resourceName = cm.Name - - return cm, nil -} - -//nolint -func hashFrom(contents ...[]byte) string { - // SHA1 because we need to limit the length to less than 64 chars - hash := sha1.New() - for _, c := range contents { - hash.Write(c) - } - - return fmt.Sprintf("%x", hash.Sum(nil)) -} - -// ApplyConfigOption will set the proper --config option behavior to the IntegrationSpec. -func ApplyConfigOption(ctx context.Context, config *RunConfigOption, integration *v1.Integration, c client.Client, - namespace string, enableCompression bool) (*corev1.ConfigMap, error) { - // A config option cannot specify destination path - if config.DestinationPath() != "" { - return nil, fmt.Errorf("cannot specify a destination path for this option type") - } - return applyOption(ctx, config, integration, c, namespace, enableCompression, v1.ResourceTypeConfig) -} - -// ApplyResourceOption will set the proper --resource option behavior to the IntegrationSpec. -func ApplyResourceOption(ctx context.Context, config *RunConfigOption, integration *v1.Integration, c client.Client, - namespace string, enableCompression bool) (*corev1.ConfigMap, error) { - return applyOption(ctx, config, integration, c, namespace, enableCompression, v1.ResourceTypeData) -} - func binaryOrTextResource(fileName string, data []byte, contentType string, base64Compression bool, resourceType v1.ResourceType, destinationPath string) (v1.ResourceSpec, error) { resourceSpec := v1.ResourceSpec{ DataSpec: v1.DataSpec{ @@ -325,7 +141,7 @@ func filterFileLocation(maybeFileLocations []string) []string { filteredOptions := make([]string, 0) for _, option := range maybeFileLocations { if strings.HasPrefix(option, "file:") { - localPath, _ := parseFileValue(strings.Replace(option, "file:", "", 1)) + localPath, _ := resource.ParseFileValue(strings.Replace(option, "file:", "", 1)) filteredOptions = append(filteredOptions, localPath) } } diff --git a/pkg/cmd/run_help_test.go b/pkg/cmd/run_help_test.go index 0763c06..0fa7c9a 100644 --- a/pkg/cmd/run_help_test.go +++ b/pkg/cmd/run_help_test.go @@ -25,125 +25,6 @@ import ( "github.com/stretchr/testify/assert" ) -func TestParseConfigOption(t *testing.T) { - validConfigMap := "configmap:my-config_map" - validSecret := "secret:my-secret" - validFile := "file:/tmp/my-file.txt" - notValid := "someprotocol:wrong" - validLocation := "file:my-file.txt@/tmp/another-name.xml" - - configmap, err := ParseConfigOption(validConfigMap) - assert.Nil(t, err) - assert.Equal(t, ConfigOptionTypeConfigmap, configmap.configType) - assert.Equal(t, "my-config_map", configmap.Name()) - secret, err := ParseConfigOption(validSecret) - assert.Nil(t, err) - assert.Equal(t, ConfigOptionTypeSecret, secret.configType) - assert.Equal(t, "my-secret", secret.Name()) - file, err := ParseConfigOption(validFile) - assert.Nil(t, err) - assert.Equal(t, ConfigOptionTypeFile, file.configType) - assert.Equal(t, "/tmp/my-file.txt", file.Name()) - _, err = ParseConfigOption(notValid) - assert.NotNil(t, err) - location, err := ParseConfigOption(validLocation) - assert.Nil(t, err) - assert.Equal(t, ConfigOptionTypeFile, location.configType) - assert.Equal(t, "my-file.txt", location.Name()) - assert.Equal(t, "/tmp/another-name.xml", location.DestinationPath()) -} - -func TestParseConfigOptionAllParams(t *testing.T) { - cm1 := "configmap:my-config_map/key@/tmp/my" - cm2 := "configmap:my-config_map/key" - cm3 := "configmap:my-config_map@/tmp/my" - cm4 := "configmap:my-config_map" - sec1 := "secret:sec/key@/tmp/sec" - sec2 := "secret:sec/key" - sec3 := "secret:sec@/tmp/sec" - sec4 := "secret:sec" - file1 := "file:/path/to/my-file.txt@/tmp/file.txt" - file2 := "file:/path/to/my-file.txt" - file3 := "file:/path to/my-file.txt" - - parsedCm1, err := ParseConfigOption(cm1) - assert.Nil(t, err) - assert.Equal(t, "configmap", parsedCm1.Type()) - assert.Equal(t, "my-config_map", parsedCm1.Name()) - assert.Equal(t, "key", parsedCm1.Key()) - assert.Equal(t, "/tmp/my", parsedCm1.DestinationPath()) - - parsedCm2, err := ParseConfigOption(cm2) - assert.Nil(t, err) - assert.Equal(t, "configmap", parsedCm2.Type()) - assert.Equal(t, "my-config_map", parsedCm2.Name()) - assert.Equal(t, "key", parsedCm2.Key()) - assert.Equal(t, "", parsedCm2.DestinationPath()) - - parsedCm3, err := ParseConfigOption(cm3) - assert.Nil(t, err) - assert.Equal(t, "configmap", parsedCm3.Type()) - assert.Equal(t, "my-config_map", parsedCm3.Name()) - assert.Equal(t, "", parsedCm3.Key()) - assert.Equal(t, "/tmp/my", parsedCm3.DestinationPath()) - - parsedCm4, err := ParseConfigOption(cm4) - assert.Nil(t, err) - assert.Equal(t, "configmap", parsedCm4.Type()) - assert.Equal(t, "my-config_map", parsedCm4.Name()) - assert.Equal(t, "", parsedCm4.Key()) - assert.Equal(t, "", parsedCm4.DestinationPath()) - - parsedSec1, err := ParseConfigOption(sec1) - assert.Nil(t, err) - assert.Equal(t, "secret", parsedSec1.Type()) - assert.Equal(t, "sec", parsedSec1.Name()) - assert.Equal(t, "key", parsedSec1.Key()) - assert.Equal(t, "/tmp/sec", parsedSec1.DestinationPath()) - - parsedSec2, err := ParseConfigOption(sec2) - assert.Nil(t, err) - assert.Equal(t, "secret", parsedSec2.Type()) - assert.Equal(t, "sec", parsedSec2.Name()) - assert.Equal(t, "key", parsedSec2.Key()) - assert.Equal(t, "", parsedSec2.DestinationPath()) - - parsedSec3, err := ParseConfigOption(sec3) - assert.Nil(t, err) - assert.Equal(t, "secret", parsedSec3.Type()) - assert.Equal(t, "sec", parsedSec3.Name()) - assert.Equal(t, "", parsedSec3.Key()) - assert.Equal(t, "/tmp/sec", parsedSec3.DestinationPath()) - - parsedSec4, err := ParseConfigOption(sec4) - assert.Nil(t, err) - assert.Equal(t, "secret", parsedSec4.Type()) - assert.Equal(t, "sec", parsedSec4.Name()) - assert.Equal(t, "", parsedSec4.Key()) - assert.Equal(t, "", parsedSec4.DestinationPath()) - - parsedFile1, err := ParseConfigOption(file1) - assert.Nil(t, err) - assert.Equal(t, "file", parsedFile1.Type()) - assert.Equal(t, "/path/to/my-file.txt", parsedFile1.Name()) - assert.Equal(t, "", parsedFile1.Key()) - assert.Equal(t, "/tmp/file.txt", parsedFile1.DestinationPath()) - - parsedFile2, err := ParseConfigOption(file2) - assert.Nil(t, err) - assert.Equal(t, "file", parsedFile2.Type()) - assert.Equal(t, "/path/to/my-file.txt", parsedFile2.Name()) - assert.Equal(t, "", parsedFile2.Key()) - assert.Equal(t, "", parsedFile2.DestinationPath()) - - parsedFile3, err := ParseConfigOption(file3) - assert.Nil(t, err) - assert.Equal(t, "file", parsedFile3.Type()) - assert.Equal(t, "/path to/my-file.txt", parsedFile3.Name()) - assert.Equal(t, "", parsedFile3.Key()) - assert.Equal(t, "", parsedFile3.DestinationPath()) -} - func TestFilterFileLocation(t *testing.T) { optionFileLocations := []string{ "file:/path/to/valid/file", @@ -161,21 +42,6 @@ func TestFilterFileLocation(t *testing.T) { assert.Equal(t, "/validfile", filteredOptions[2]) } -func TestValidateFileLocation(t *testing.T) { - validLocation := "file:my-file.txt@/tmp/another-name.xml" - etcCamelLocation := "configmap:my-cm@/etc/camel/configmaps" - deploymentsDepsLocation := "secret:my-sec@/deployments/dependencies" - - _, err := ParseConfigOption(validLocation) - assert.Nil(t, err) - _, err = ParseConfigOption(etcCamelLocation) - assert.NotNil(t, err) - assert.Equal(t, "you cannot mount a file under /etc/camel path", err.Error()) - _, err = ParseConfigOption(deploymentsDepsLocation) - assert.NotNil(t, err) - assert.Equal(t, "you cannot mount a file under /deployments/dependencies path", err.Error()) -} - func TestExtractProperties_SingleKeyValue(t *testing.T) { correctValues := []string{"key=val", "key = val", "key= val", " key = val"} for _, val := range correctValues { diff --git a/pkg/util/resource/config.go b/pkg/util/resource/config.go new file mode 100644 index 0000000..f5ff97d --- /dev/null +++ b/pkg/util/resource/config.go @@ -0,0 +1,234 @@ +/* +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 resource + +import ( + "context" + "crypto/sha1" //nolint + "fmt" + "path/filepath" + "regexp" + "strings" + + v1 "github.com/apache/camel-k/pkg/apis/camel/v1" + "github.com/apache/camel-k/pkg/client" + "github.com/apache/camel-k/pkg/util/camel" + "github.com/apache/camel-k/pkg/util/kubernetes" + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" +) + +var invalidPaths = []string{"/etc/camel", "/deployments/dependencies"} + +// Config represents a config option. +type Config struct { + configType StorageType + resourceName string + resourceKey string + destinationPath string +} + +// DestinationPath is the location where the resource will be stored on destination. +func (config *Config) DestinationPath() string { + return config.destinationPath +} + +// Type is the type, converted as string. +func (config *Config) Type() StorageType { + return config.configType +} + +// Name is the name of the resource. +func (config *Config) Name() string { + return config.resourceName +} + +// Key is the key specified for the resource. +func (config *Config) Key() string { + return config.resourceKey +} + +// Validate checks if the DestinationPath is correctly configured. +func (config *Config) Validate() error { + if config.destinationPath == "" { + return nil + } + + // Check for invalid path + for _, invalidPath := range invalidPaths { + if config.destinationPath == invalidPath || strings.HasPrefix(config.destinationPath, invalidPath+"/") { + return fmt.Errorf("you cannot mount a file under %s path", invalidPath) + } + } + return nil +} + +// StorageType represent a resource/config type such as configmap, secret or local file. +type StorageType string + +const ( + // StorageTypeConfigmap --. + StorageTypeConfigmap StorageType = "configmap" + // StorageTypeSecret --. + StorageTypeSecret StorageType = "secret" + // StorageTypeFile --. + StorageTypeFile StorageType = "file" +) + +var ( + validConfigSecretRegexp = regexp.MustCompile(`^(configmap|secret)\:([\w\.\-\_\:\/@]+)$`) + validFileRegexp = regexp.MustCompile(`^file\:([\w\.\-\_\:\/@" ]+)$`) + validResourceRegexp = regexp.MustCompile(`^([\w\.\-\_\:]+)(\/([\w\.\-\_\:]+))?(\@([\w\.\-\_\:\/]+))?$`) +) + +func newConfig(configType StorageType, value string) *Config { + rn, mk, mp := parseResourceValue(configType, value) + return &Config{ + configType: configType, + resourceName: rn, + resourceKey: mk, + destinationPath: mp, + } +} + +func parseResourceValue(configType StorageType, value string) (resource string, maybeKey string, maybeDestinationPath string) { + if configType == StorageTypeFile { + resource, maybeDestinationPath = ParseFileValue(value) + return resource, "", maybeDestinationPath + } + + return parseCMOrSecretValue(value) +} + +// ParseFileValue will parse a file resource/config option to return the local path and the +// destination path expected. +func ParseFileValue(value string) (localPath string, maybeDestinationPath string) { + split := strings.SplitN(value, "@", 2) + if len(split) == 2 { + return split[0], split[1] + } + + return value, "" +} + +func parseCMOrSecretValue(value string) (resource string, maybeKey string, maybeDestinationPath string) { + if !validResourceRegexp.MatchString(value) { + return value, "", "" + } + // Must have 3 values + groups := validResourceRegexp.FindStringSubmatch(value) + + return groups[1], groups[3], groups[5] +} + +// ParseResource will parse and return a Config. +func ParseResource(item string) (*Config, error) { + // Deprecated: ensure backward compatibility with `--resource filename` format until version 1.5.x + // then replace with parseOption() func directly + option, err := parse(item) + if err != nil { + if strings.HasPrefix(err.Error(), "could not match config, secret or file configuration") { + fmt.Printf("Warn: --resource %s has been deprecated. You should use --resource file:%s instead.\n", item, item) + return parse("file:" + item) + } + return nil, err + } + + return option, nil +} + +// ParseConfig will parse and return a Config. +func ParseConfig(item string) (*Config, error) { + return parse(item) +} + +func parse(item string) (*Config, error) { + var cot StorageType + var value string + switch { + case validConfigSecretRegexp.MatchString(item): + // parse as secret/configmap + groups := validConfigSecretRegexp.FindStringSubmatch(item) + switch groups[1] { + case "configmap": + cot = StorageTypeConfigmap + case "secret": + cot = StorageTypeSecret + } + value = groups[2] + case validFileRegexp.MatchString(item): + // parse as file + groups := validFileRegexp.FindStringSubmatch(item) + cot = StorageTypeFile + value = groups[1] + default: + return nil, fmt.Errorf("could not match config, secret or file configuration as %s", item) + } + + configurationOption := newConfig(cot, value) + if err := configurationOption.Validate(); err != nil { + return nil, err + } + return configurationOption, nil +} + +// ConvertFileToConfigmap convert a local file resource type in a configmap type +// taking care to create the Configmap on the cluster. The method will change the value of config parameter +// to reflect the conversion applied transparently. +func ConvertFileToConfigmap(ctx context.Context, c client.Client, resourceSpec v1.ResourceSpec, config *Config, + namespace string, resourceType v1.ResourceType) (*corev1.ConfigMap, error) { + if config.DestinationPath() == "" { + config.resourceKey = filepath.Base(config.Name()) + // As we are changing the resource to a configmap type + // we need to declare the mount path not to use the + // default behavior of a configmap (which include a subdirectory with the configmap name) + if resourceType == v1.ResourceTypeData { + config.destinationPath = camel.ResourcesDefaultMountPath + } else { + config.destinationPath = camel.ConfigResourcesMountPath + } + } else { + config.resourceKey = filepath.Base(config.DestinationPath()) + config.destinationPath = filepath.Dir(config.DestinationPath()) + } + genCmName := fmt.Sprintf("cm-%s", hashFrom([]byte(resourceSpec.Content), resourceSpec.RawContent)) + cm := kubernetes.NewConfigmap(namespace, genCmName, filepath.Base(config.Name()), config.Key(), resourceSpec.Content, resourceSpec.RawContent) + err := c.Create(ctx, cm) + if err != nil { + if k8serrors.IsAlreadyExists(err) { + // We'll reuse it, as is + } else { + return cm, err + } + } + config.configType = StorageTypeConfigmap + config.resourceName = cm.Name + + return cm, nil +} + +//nolint +func hashFrom(contents ...[]byte) string { + // SHA1 because we need to limit the length to less than 64 chars + hash := sha1.New() + for _, c := range contents { + hash.Write(c) + } + + return fmt.Sprintf("%x", hash.Sum(nil)) +} diff --git a/pkg/cmd/run_help_test.go b/pkg/util/resource/config_test.go similarity index 53% copy from pkg/cmd/run_help_test.go copy to pkg/util/resource/config_test.go index 0763c06..44173f9 100644 --- a/pkg/cmd/run_help_test.go +++ b/pkg/util/resource/config_test.go @@ -15,11 +15,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -package cmd +package resource import ( - "io/ioutil" - "os" "testing" "github.com/stretchr/testify/assert" @@ -32,23 +30,23 @@ func TestParseConfigOption(t *testing.T) { notValid := "someprotocol:wrong" validLocation := "file:my-file.txt@/tmp/another-name.xml" - configmap, err := ParseConfigOption(validConfigMap) + configmap, err := ParseConfig(validConfigMap) assert.Nil(t, err) - assert.Equal(t, ConfigOptionTypeConfigmap, configmap.configType) + assert.Equal(t, StorageTypeConfigmap, configmap.configType) assert.Equal(t, "my-config_map", configmap.Name()) - secret, err := ParseConfigOption(validSecret) + secret, err := ParseConfig(validSecret) assert.Nil(t, err) - assert.Equal(t, ConfigOptionTypeSecret, secret.configType) + assert.Equal(t, StorageTypeSecret, secret.configType) assert.Equal(t, "my-secret", secret.Name()) - file, err := ParseConfigOption(validFile) + file, err := ParseConfig(validFile) assert.Nil(t, err) - assert.Equal(t, ConfigOptionTypeFile, file.configType) + assert.Equal(t, StorageTypeFile, file.configType) assert.Equal(t, "/tmp/my-file.txt", file.Name()) - _, err = ParseConfigOption(notValid) + _, err = ParseConfig(notValid) assert.NotNil(t, err) - location, err := ParseConfigOption(validLocation) + location, err := ParseConfig(validLocation) assert.Nil(t, err) - assert.Equal(t, ConfigOptionTypeFile, location.configType) + assert.Equal(t, StorageTypeFile, location.configType) assert.Equal(t, "my-file.txt", location.Name()) assert.Equal(t, "/tmp/another-name.xml", location.DestinationPath()) } @@ -66,173 +64,95 @@ func TestParseConfigOptionAllParams(t *testing.T) { file2 := "file:/path/to/my-file.txt" file3 := "file:/path to/my-file.txt" - parsedCm1, err := ParseConfigOption(cm1) + parsedCm1, err := ParseConfig(cm1) assert.Nil(t, err) - assert.Equal(t, "configmap", parsedCm1.Type()) + assert.Equal(t, StorageTypeConfigmap, parsedCm1.Type()) assert.Equal(t, "my-config_map", parsedCm1.Name()) assert.Equal(t, "key", parsedCm1.Key()) assert.Equal(t, "/tmp/my", parsedCm1.DestinationPath()) - parsedCm2, err := ParseConfigOption(cm2) + parsedCm2, err := ParseConfig(cm2) assert.Nil(t, err) - assert.Equal(t, "configmap", parsedCm2.Type()) + assert.Equal(t, StorageTypeConfigmap, parsedCm2.Type()) assert.Equal(t, "my-config_map", parsedCm2.Name()) assert.Equal(t, "key", parsedCm2.Key()) assert.Equal(t, "", parsedCm2.DestinationPath()) - parsedCm3, err := ParseConfigOption(cm3) + parsedCm3, err := ParseConfig(cm3) assert.Nil(t, err) - assert.Equal(t, "configmap", parsedCm3.Type()) + assert.Equal(t, StorageTypeConfigmap, parsedCm3.Type()) assert.Equal(t, "my-config_map", parsedCm3.Name()) assert.Equal(t, "", parsedCm3.Key()) assert.Equal(t, "/tmp/my", parsedCm3.DestinationPath()) - parsedCm4, err := ParseConfigOption(cm4) + parsedCm4, err := ParseConfig(cm4) assert.Nil(t, err) - assert.Equal(t, "configmap", parsedCm4.Type()) + assert.Equal(t, StorageTypeConfigmap, parsedCm4.Type()) assert.Equal(t, "my-config_map", parsedCm4.Name()) assert.Equal(t, "", parsedCm4.Key()) assert.Equal(t, "", parsedCm4.DestinationPath()) - parsedSec1, err := ParseConfigOption(sec1) + parsedSec1, err := ParseConfig(sec1) assert.Nil(t, err) - assert.Equal(t, "secret", parsedSec1.Type()) + assert.Equal(t, StorageTypeSecret, parsedSec1.Type()) assert.Equal(t, "sec", parsedSec1.Name()) assert.Equal(t, "key", parsedSec1.Key()) assert.Equal(t, "/tmp/sec", parsedSec1.DestinationPath()) - parsedSec2, err := ParseConfigOption(sec2) + parsedSec2, err := ParseConfig(sec2) assert.Nil(t, err) - assert.Equal(t, "secret", parsedSec2.Type()) + assert.Equal(t, StorageTypeSecret, parsedSec2.Type()) assert.Equal(t, "sec", parsedSec2.Name()) assert.Equal(t, "key", parsedSec2.Key()) assert.Equal(t, "", parsedSec2.DestinationPath()) - parsedSec3, err := ParseConfigOption(sec3) + parsedSec3, err := ParseConfig(sec3) assert.Nil(t, err) - assert.Equal(t, "secret", parsedSec3.Type()) + assert.Equal(t, StorageTypeSecret, parsedSec3.Type()) assert.Equal(t, "sec", parsedSec3.Name()) assert.Equal(t, "", parsedSec3.Key()) assert.Equal(t, "/tmp/sec", parsedSec3.DestinationPath()) - parsedSec4, err := ParseConfigOption(sec4) + parsedSec4, err := ParseConfig(sec4) assert.Nil(t, err) - assert.Equal(t, "secret", parsedSec4.Type()) + assert.Equal(t, StorageTypeSecret, parsedSec4.Type()) assert.Equal(t, "sec", parsedSec4.Name()) assert.Equal(t, "", parsedSec4.Key()) assert.Equal(t, "", parsedSec4.DestinationPath()) - parsedFile1, err := ParseConfigOption(file1) + parsedFile1, err := ParseConfig(file1) assert.Nil(t, err) - assert.Equal(t, "file", parsedFile1.Type()) + assert.Equal(t, StorageTypeFile, parsedFile1.Type()) assert.Equal(t, "/path/to/my-file.txt", parsedFile1.Name()) assert.Equal(t, "", parsedFile1.Key()) assert.Equal(t, "/tmp/file.txt", parsedFile1.DestinationPath()) - parsedFile2, err := ParseConfigOption(file2) + parsedFile2, err := ParseConfig(file2) assert.Nil(t, err) - assert.Equal(t, "file", parsedFile2.Type()) + assert.Equal(t, StorageTypeFile, parsedFile2.Type()) assert.Equal(t, "/path/to/my-file.txt", parsedFile2.Name()) assert.Equal(t, "", parsedFile2.Key()) assert.Equal(t, "", parsedFile2.DestinationPath()) - parsedFile3, err := ParseConfigOption(file3) + parsedFile3, err := ParseConfig(file3) assert.Nil(t, err) - assert.Equal(t, "file", parsedFile3.Type()) + assert.Equal(t, StorageTypeFile, parsedFile3.Type()) assert.Equal(t, "/path to/my-file.txt", parsedFile3.Name()) assert.Equal(t, "", parsedFile3.Key()) assert.Equal(t, "", parsedFile3.DestinationPath()) } -func TestFilterFileLocation(t *testing.T) { - optionFileLocations := []string{ - "file:/path/to/valid/file", - "file:app.properties", - "configmap:my-configmap", - "secret:my-secret", - "file:/validfile@/tmp/destination", - } - - filteredOptions := filterFileLocation(optionFileLocations) - - assert.Equal(t, 3, len(filteredOptions)) - assert.Equal(t, "/path/to/valid/file", filteredOptions[0]) - assert.Equal(t, "app.properties", filteredOptions[1]) - assert.Equal(t, "/validfile", filteredOptions[2]) -} - func TestValidateFileLocation(t *testing.T) { validLocation := "file:my-file.txt@/tmp/another-name.xml" etcCamelLocation := "configmap:my-cm@/etc/camel/configmaps" deploymentsDepsLocation := "secret:my-sec@/deployments/dependencies" - _, err := ParseConfigOption(validLocation) + _, err := ParseConfig(validLocation) assert.Nil(t, err) - _, err = ParseConfigOption(etcCamelLocation) + _, err = ParseConfig(etcCamelLocation) assert.NotNil(t, err) assert.Equal(t, "you cannot mount a file under /etc/camel path", err.Error()) - _, err = ParseConfigOption(deploymentsDepsLocation) + _, err = ParseConfig(deploymentsDepsLocation) assert.NotNil(t, err) assert.Equal(t, "you cannot mount a file under /deployments/dependencies path", err.Error()) } - -func TestExtractProperties_SingleKeyValue(t *testing.T) { - correctValues := []string{"key=val", "key = val", "key= val", " key = val"} - for _, val := range correctValues { - prop, err := extractProperties(val) - assert.Nil(t, err) - value, ok := prop.Get("key") - assert.True(t, ok) - assert.Equal(t, "val", value) - } -} - -func TestExtractProperties_FromFile(t *testing.T) { - var tmpFile1 *os.File - var err error - if tmpFile1, err = ioutil.TempFile("", "camel-k-*.properties"); err != nil { - t.Error(err) - } - - assert.Nil(t, tmpFile1.Close()) - assert.Nil(t, ioutil.WriteFile(tmpFile1.Name(), []byte(` - key=value - #key2=value2 - my.key=value - `), 0o400)) - - props, err := extractProperties("file:" + tmpFile1.Name()) - assert.Nil(t, err) - assert.Equal(t, 2, props.Len()) - for _, prop := range props.Keys() { - value, ok := props.Get(prop) - assert.True(t, ok) - assert.Equal(t, "value", value) - } -} - -func TestExtractPropertiesFromFileAndSingleValue(t *testing.T) { - var tmpFile1 *os.File - var err error - if tmpFile1, err = ioutil.TempFile("", "camel-k-*.properties"); err != nil { - t.Error(err) - } - - assert.Nil(t, tmpFile1.Close()) - assert.Nil(t, ioutil.WriteFile(tmpFile1.Name(), []byte(` - key=value - #key2=value2 - my.key=value - `), 0o400)) - - properties := []string{"key=override", "file:" + tmpFile1.Name(), "my.key = override"} - props, err := mergePropertiesWithPrecedence(properties) - assert.Nil(t, err) - assert.Equal(t, 2, props.Len()) - val, ok := props.Get("key") - assert.True(t, ok) - assert.Equal(t, "override", val) - val, ok = props.Get("my.key") - assert.True(t, ok) - assert.Equal(t, "override", val) -}
