diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/create/BUILD b/staging/src/k8s.io/kubectl/pkg/cmd/create/BUILD index e586029bdfc..70ff839361e 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/create/BUILD +++ b/staging/src/k8s.io/kubectl/pkg/cmd/create/BUILD @@ -63,6 +63,7 @@ go_library( "//staging/src/k8s.io/kubectl/pkg/rawhttp:go_default_library", "//staging/src/k8s.io/kubectl/pkg/scheme:go_default_library", "//staging/src/k8s.io/kubectl/pkg/util:go_default_library", + "//staging/src/k8s.io/kubectl/pkg/util/hash:go_default_library", "//staging/src/k8s.io/kubectl/pkg/util/i18n:go_default_library", "//staging/src/k8s.io/kubectl/pkg/util/templates:go_default_library", "//vendor/github.com/spf13/cobra:go_default_library", diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/create/create_configmap.go b/staging/src/k8s.io/kubectl/pkg/cmd/create/create_configmap.go index b93c87db445..22728f5fb68 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/create/create_configmap.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/create/create_configmap.go @@ -17,12 +17,27 @@ limitations under the License. package create import ( + "context" + "fmt" + "io/ioutil" + "os" + "path" + "strings" + "unicode/utf8" + "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation" "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/resource" + corev1client "k8s.io/client-go/kubernetes/typed/core/v1" cmdutil "k8s.io/kubectl/pkg/cmd/util" - "k8s.io/kubectl/pkg/generate" - generateversioned "k8s.io/kubectl/pkg/generate/versioned" + "k8s.io/kubectl/pkg/scheme" + "k8s.io/kubectl/pkg/util" + "k8s.io/kubectl/pkg/util/hash" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) @@ -57,16 +72,50 @@ var ( kubectl create configmap my-config --from-env-file=path/to/bar.env`)) ) -// ConfigMapOpts holds properties for create configmap sub-command -type ConfigMapOpts struct { - CreateSubcommandOptions *CreateSubcommandOptions +// ConfigMapOptions holds properties for create configmap sub-command +type ConfigMapOptions struct { + // PrintFlags holds options necessary for obtaining a printer + PrintFlags *genericclioptions.PrintFlags + PrintObj func(obj runtime.Object) error + + // Name of configMap (required) + Name string + // Type of configMap (optional) + Type string + // FileSources to derive the configMap from (optional) + FileSources []string + // LiteralSources to derive the configMap from (optional) + LiteralSources []string + // EnvFileSource to derive the configMap from (optional) + EnvFileSource string + // AppendHash; if true, derive a hash from the ConfigMap and append it to the name + AppendHash bool + + FieldManager string + CreateAnnotation bool + Namespace string + EnforceNamespace bool + + Client corev1client.CoreV1Interface + DryRunStrategy cmdutil.DryRunStrategy + DryRunVerifier *resource.DryRunVerifier + + genericclioptions.IOStreams } -// NewCmdCreateConfigMap initializes and returns ConfigMapOpts -func NewCmdCreateConfigMap(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command { - options := &ConfigMapOpts{ - CreateSubcommandOptions: NewCreateSubcommandOptions(ioStreams), +// NewConfigMapOptions creates a new *ConfigMapOptions with default value +func NewConfigMapOptions(ioStreams genericclioptions.IOStreams) *ConfigMapOptions { + return &ConfigMapOptions{ + FileSources: []string{}, + LiteralSources: []string{}, + PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme), + IOStreams: ioStreams, } +} + +// NewCmdCreateConfigMap creates the `create configmap` Cobra command +func NewCmdCreateConfigMap(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command { + o := NewConfigMapOptions(ioStreams) cmd := &cobra.Command{ Use: "configmap NAME [--from-file=[key=]source] [--from-literal=key1=value1] [--dry-run=server|client|none]", @@ -76,49 +125,296 @@ func NewCmdCreateConfigMap(f cmdutil.Factory, ioStreams genericclioptions.IOStre Long: configMapLong, Example: configMapExample, Run: func(cmd *cobra.Command, args []string) { - cmdutil.CheckErr(options.Complete(f, cmd, args)) - cmdutil.CheckErr(options.Run()) + cmdutil.CheckErr(o.Complete(f, cmd, args)) + cmdutil.CheckErr(o.Validate()) + cmdutil.CheckErr(o.Run()) }, } - - options.CreateSubcommandOptions.PrintFlags.AddFlags(cmd) + o.PrintFlags.AddFlags(cmd) cmdutil.AddApplyAnnotationFlags(cmd) cmdutil.AddValidateFlags(cmd) - cmdutil.AddGeneratorFlags(cmd, generateversioned.ConfigMapV1GeneratorName) - cmd.Flags().StringSlice("from-file", []string{}, "Key file can be specified using its file path, in which case file basename will be used as configmap key, or optionally with a key and file path, in which case the given key will be used. Specifying a directory will iterate each named file in the directory whose basename is a valid configmap key.") - cmd.Flags().StringArray("from-literal", []string{}, "Specify a key and literal value to insert in configmap (i.e. mykey=somevalue)") - cmd.Flags().String("from-env-file", "", "Specify the path to a file to read lines of key=val pairs to create a configmap (i.e. a Docker .env file).") - cmd.Flags().Bool("append-hash", false, "Append a hash of the configmap to its name.") - cmdutil.AddFieldManagerFlagVar(cmd, &options.CreateSubcommandOptions.FieldManager, "kubectl-create") + cmdutil.AddDryRunFlag(cmd) + + cmd.Flags().StringSliceVar(&o.FileSources, "from-file", o.FileSources, "Key file can be specified using its file path, in which case file basename will be used as configmap key, or optionally with a key and file path, in which case the given key will be used. Specifying a directory will iterate each named file in the directory whose basename is a valid configmap key.") + cmd.Flags().StringArrayVar(&o.LiteralSources, "from-literal", o.LiteralSources, "Specify a key and literal value to insert in configmap (i.e. mykey=somevalue)") + cmd.Flags().StringVar(&o.EnvFileSource, "from-env-file", o.EnvFileSource, "Specify the path to a file to read lines of key=val pairs to create a configmap (i.e. a Docker .env file).") + cmd.Flags().BoolVar(&o.AppendHash, "append-hash", o.AppendHash, "Append a hash of the configmap to its name.") + + cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-create") + return cmd } -// Complete completes all the required options -func (o *ConfigMapOpts) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { - name, err := NameFromCommandArgs(cmd, args) +// Complete loads data from the command line environment +func (o *ConfigMapOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { + var err error + o.Name, err = NameFromCommandArgs(cmd, args) if err != nil { return err } - var generator generate.StructuredGenerator - switch generatorName := cmdutil.GetFlagString(cmd, "generator"); generatorName { - case generateversioned.ConfigMapV1GeneratorName: - generator = &generateversioned.ConfigMapGeneratorV1{ - Name: name, - FileSources: cmdutil.GetFlagStringSlice(cmd, "from-file"), - LiteralSources: cmdutil.GetFlagStringArray(cmd, "from-literal"), - EnvFileSource: cmdutil.GetFlagString(cmd, "from-env-file"), - AppendHash: cmdutil.GetFlagBool(cmd, "append-hash"), - } - default: - return errUnsupportedGenerator(cmd, generatorName) + restConfig, err := f.ToRESTConfig() + if err != nil { + return err } - return o.CreateSubcommandOptions.Complete(f, cmd, args, generator) + o.Client, err = corev1client.NewForConfig(restConfig) + if err != nil { + return err + } + + o.CreateAnnotation = cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag) + + o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) + if err != nil { + return err + } + + dynamicClient, err := f.DynamicClient() + if err != nil { + return err + } + + discoveryClient, err := f.ToDiscoveryClient() + if err != nil { + return err + } + + o.DryRunVerifier = resource.NewDryRunVerifier(dynamicClient, discoveryClient) + + o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() + if err != nil { + return err + } + + cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy) + printer, err := o.PrintFlags.ToPrinter() + if err != nil { + return err + } + o.PrintObj = func(obj runtime.Object) error { + return printer.PrintObj(obj, o.Out) + } + + return nil } -// Run performs the execution of 'create' sub command options -func (o *ConfigMapOpts) Run() error { - return o.CreateSubcommandOptions.Run() +// Validate checks if ConfigMapOptions has sufficient value to run +func (o *ConfigMapOptions) Validate() error { + if len(o.Name) == 0 { + return fmt.Errorf("name must be specified") + } + if len(o.EnvFileSource) > 0 && (len(o.FileSources) > 0 || len(o.LiteralSources) > 0) { + return fmt.Errorf("from-env-file cannot be combined with from-file or from-literal") + } + return nil +} + +// Run calls createConfigMap and filled in value for configMap object +func (o *ConfigMapOptions) Run() error { + configMap, err := o.createConfigMap() + if err != nil { + return nil + } + if err := util.CreateOrUpdateAnnotation(o.CreateAnnotation, configMap, scheme.DefaultJSONEncoder()); err != nil { + return err + } + if o.DryRunStrategy != cmdutil.DryRunClient { + createOptions := metav1.CreateOptions{} + if o.FieldManager != "" { + createOptions.FieldManager = o.FieldManager + } + if o.DryRunStrategy == cmdutil.DryRunServer { + if err := o.DryRunVerifier.HasSupport(configMap.GroupVersionKind()); err != nil { + return err + } + createOptions.DryRun = []string{metav1.DryRunAll} + } + configMap, err = o.Client.ConfigMaps(o.Namespace).Create(context.TODO(), configMap, createOptions) + if err != nil { + return fmt.Errorf("failed to create configmap: %v", err) + } + } + + return o.PrintObj(configMap) +} + +// createConfigMap fills in key value pair from the information given in +// ConfigMapOptions into *corev1.ConfigMap +func (o *ConfigMapOptions) createConfigMap() (*corev1.ConfigMap, error) { + namespace := "" + if o.EnforceNamespace { + namespace = o.Namespace + } + + configMap := &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: o.Name, + Namespace: namespace, + }, + } + configMap.Name = o.Name + configMap.Data = map[string]string{} + configMap.BinaryData = map[string][]byte{} + + if len(o.FileSources) > 0 { + if err := handleConfigMapFromFileSources(configMap, o.FileSources); err != nil { + return nil, err + } + } + if len(o.LiteralSources) > 0 { + if err := handleConfigMapFromLiteralSources(configMap, o.LiteralSources); err != nil { + return nil, err + } + } + if len(o.EnvFileSource) > 0 { + if err := handleConfigMapFromEnvFileSource(configMap, o.EnvFileSource); err != nil { + return nil, err + } + } + if o.AppendHash { + hash, err := hash.ConfigMapHash(configMap) + if err != nil { + return nil, err + } + configMap.Name = fmt.Sprintf("%s-%s", configMap.Name, hash) + } + + return configMap, nil +} + +// handleConfigMapFromLiteralSources adds the specified literal source +// information into the provided configMap. +func handleConfigMapFromLiteralSources(configMap *corev1.ConfigMap, literalSources []string) error { + for _, literalSource := range literalSources { + keyName, value, err := util.ParseLiteralSource(literalSource) + if err != nil { + return err + } + err = addKeyFromLiteralToConfigMap(configMap, keyName, value) + if err != nil { + return err + } + } + + return nil +} + +// handleConfigMapFromFileSources adds the specified file source information +// into the provided configMap +func handleConfigMapFromFileSources(configMap *corev1.ConfigMap, fileSources []string) error { + for _, fileSource := range fileSources { + keyName, filePath, err := util.ParseFileSource(fileSource) + if err != nil { + return err + } + info, err := os.Stat(filePath) + if err != nil { + switch err := err.(type) { + case *os.PathError: + return fmt.Errorf("error reading %s: %v", filePath, err.Err) + default: + return fmt.Errorf("error reading %s: %v", filePath, err) + } + + } + if info.IsDir() { + if strings.Contains(fileSource, "=") { + return fmt.Errorf("cannot give a key name for a directory path") + } + fileList, err := ioutil.ReadDir(filePath) + if err != nil { + return fmt.Errorf("error listing files in %s: %v", filePath, err) + } + for _, item := range fileList { + itemPath := path.Join(filePath, item.Name()) + if item.Mode().IsRegular() { + keyName = item.Name() + err = addKeyFromFileToConfigMap(configMap, keyName, itemPath) + if err != nil { + return err + } + } + } + } else { + if err := addKeyFromFileToConfigMap(configMap, keyName, filePath); err != nil { + return err + } + + } + } + return nil +} + +// handleConfigMapFromEnvFileSource adds the specified env file source information +// into the provided configMap +func handleConfigMapFromEnvFileSource(configMap *corev1.ConfigMap, envFileSource string) error { + info, err := os.Stat(envFileSource) + if err != nil { + switch err := err.(type) { + case *os.PathError: + return fmt.Errorf("error reading %s: %v", envFileSource, err.Err) + default: + return fmt.Errorf("error reading %s: %v", envFileSource, err) + } + } + if info.IsDir() { + return fmt.Errorf("env config file cannot be a directory") + } + + return cmdutil.AddFromEnvFile(envFileSource, func(key, value string) error { + return addKeyFromLiteralToConfigMap(configMap, key, value) + }) +} + +// addKeyFromFileToConfigMap adds a key with the given name to a ConfigMap, populating +// the value with the content of the given file path, or returns an error. +func addKeyFromFileToConfigMap(configMap *corev1.ConfigMap, keyName, filePath string) error { + data, err := ioutil.ReadFile(filePath) + if err != nil { + return err + } + if utf8.Valid(data) { + return addKeyFromLiteralToConfigMap(configMap, keyName, string(data)) + } + err = validateNewConfigMap(configMap, keyName) + if err != nil { + return err + } + configMap.BinaryData[keyName] = data + + return nil +} + +// addKeyFromLiteralToConfigMap adds the given key and data to the given config map, +// returning an error if the key is not valid or if the key already exists. +func addKeyFromLiteralToConfigMap(configMap *corev1.ConfigMap, keyName, data string) error { + err := validateNewConfigMap(configMap, keyName) + if err != nil { + return err + } + configMap.Data[keyName] = data + + return nil +} + +// validateNewConfigMap checks whether the keyname is valid +// Note, the rules for ConfigMap keys are the exact same as the ones for SecretKeys. +func validateNewConfigMap(configMap *corev1.ConfigMap, keyName string) error { + if errs := validation.IsConfigMapKey(keyName); len(errs) > 0 { + return fmt.Errorf("%q is not a valid key name for a ConfigMap: %s", keyName, strings.Join(errs, ",")) + } + if _, exists := configMap.Data[keyName]; exists { + return fmt.Errorf("cannot add key %q, another key by that name already exists in Data for ConfigMap %q", keyName, configMap.Name) + } + if _, exists := configMap.BinaryData[keyName]; exists { + return fmt.Errorf("cannot add key %q, another key by that name already exists in BinaryData for ConfigMap %q", keyName, configMap.Name) + } + + return nil } diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/create/create_configmap_test.go b/staging/src/k8s.io/kubectl/pkg/cmd/create/create_configmap_test.go index 01b3dbe192b..e3c6c92c0a4 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/create/create_configmap_test.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/create/create_configmap_test.go @@ -17,45 +17,449 @@ limitations under the License. package create import ( - "net/http" + "io/ioutil" + "os" "testing" - "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/cli-runtime/pkg/genericclioptions" - "k8s.io/client-go/rest/fake" - cmdtesting "k8s.io/kubectl/pkg/cmd/testing" - "k8s.io/kubectl/pkg/scheme" + corev1 "k8s.io/api/core/v1" + apiequality "k8s.io/apimachinery/pkg/api/equality" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestCreateConfigMap(t *testing.T) { - configMap := &v1.ConfigMap{} - configMap.Name = "my-configmap" - tf := cmdtesting.NewTestFactory().WithNamespace("test") - defer tf.Cleanup() + tests := map[string]struct { + configMapName string + configMapType string + appendHash bool + fromLiteral []string + fromFile []string + fromEnvFile string + setup func(t *testing.T, configMapOptions *ConfigMapOptions) func() - codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) - ns := scheme.Codecs.WithoutConversion() - - tf.Client = &fake.RESTClient{ - GroupVersion: schema.GroupVersion{Group: "", Version: "v1"}, - NegotiatedSerializer: ns, - Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { - switch p, m := req.URL.Path, req.Method; { - case p == "/namespaces/test/configmaps" && m == "POST": - return &http.Response{StatusCode: http.StatusCreated, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, configMap)}, nil - default: - t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) - return nil, nil - } - }), + expected *corev1.ConfigMap + expectErr bool + }{ + "create_foo_configmap": { + configMapName: "foo", + expected: &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Data: map[string]string{}, + BinaryData: map[string][]byte{}, + }, + expectErr: false, + }, + "create_foo_hash_configmap": { + configMapName: "foo", + appendHash: true, + expected: &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-867km9574f", + }, + Data: map[string]string{}, + BinaryData: map[string][]byte{}, + }, + expectErr: false, + }, + "create_foo_type_configmap": { + configMapName: "foo", + configMapType: "my-type", + expected: &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Data: map[string]string{}, + BinaryData: map[string][]byte{}, + }, + expectErr: false, + }, + "create_foo_type_hash_configmap": { + configMapName: "foo", + configMapType: "my-type", + appendHash: true, + expected: &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-867km9574f", + }, + Data: map[string]string{}, + BinaryData: map[string][]byte{}, + }, + expectErr: false, + }, + "create_foo_two_literal_configmap": { + configMapName: "foo", + fromLiteral: []string{"key1=value1", "key2=value2"}, + expected: &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Data: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + BinaryData: map[string][]byte{}, + }, + expectErr: false, + }, + "create_foo_two_literal_hash_configmap": { + configMapName: "foo", + fromLiteral: []string{"key1=value1", "key2=value2"}, + appendHash: true, + expected: &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-gcb75dd9gb", + }, + Data: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + BinaryData: map[string][]byte{}, + }, + expectErr: false, + }, + "create_foo_key1_=value1_configmap": { + configMapName: "foo", + fromLiteral: []string{"key1==value1"}, + expected: &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Data: map[string]string{ + "key1": "=value1", + }, + BinaryData: map[string][]byte{}, + }, + expectErr: false, + }, + "create_foo_key1_=value1_hash_configmap": { + configMapName: "foo", + fromLiteral: []string{"key1==value1"}, + appendHash: true, + expected: &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-bdgk9ttt7m", + }, + Data: map[string]string{ + "key1": "=value1", + }, + BinaryData: map[string][]byte{}, + }, + expectErr: false, + }, + "create_foo_from_file_foo1_foo2_configmap": { + configMapName: "foo", + setup: setupBinaryFile([]byte{0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64}, "foo1", "foo2"), + fromFile: []string{"foo1", "foo2"}, + expected: &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Data: map[string]string{ + "foo1": "hello world", + "foo2": "hello world", + }, + BinaryData: map[string][]byte{}, + }, + expectErr: false, + }, + "create_foo_from_file_foo1_foo2_and_configmap": { + configMapName: "foo", + setup: setupBinaryFile([]byte{0xff, 0xfd}, "foo1", "foo2"), + fromFile: []string{"foo1", "foo2"}, + expected: &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Data: map[string]string{}, + BinaryData: map[string][]byte{ + "foo1": {0xff, 0xfd}, + "foo2": {0xff, 0xfd}, + }, + }, + expectErr: false, + }, + "create_valid_env_from_env_file_configmap": { + configMapName: "valid_env", + setup: setupEnvFile("key1=value1", "#", "", "key2=value2"), + fromEnvFile: "file.env", + expected: &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "valid_env", + }, + Data: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + BinaryData: map[string][]byte{}, + }, + expectErr: false, + }, + "create_valid_env_from_env_file_hash_configmap": { + configMapName: "valid_env", + setup: setupEnvFile("key1=value1", "#", "", "key2=value2"), + fromEnvFile: "file.env", + appendHash: true, + expected: &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "valid_env-2cgh8552ch", + }, + Data: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + BinaryData: map[string][]byte{}, + }, + expectErr: false, + }, + "create_get_env_from_env_file_configmap": { + configMapName: "get_env", + setup: func() func(t *testing.T, configMapOptions *ConfigMapOptions) func() { + os.Setenv("g_key1", "1") + os.Setenv("g_key2", "2") + return setupEnvFile("g_key1", "g_key2=") + }(), + fromEnvFile: "file.env", + expected: &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "get_env", + }, + Data: map[string]string{ + "g_key1": "1", + "g_key2": "", + }, + BinaryData: map[string][]byte{}, + }, + expectErr: false, + }, + "create_get_env_from_env_file_hash_configmap": { + configMapName: "get_env", + setup: func() func(t *testing.T, configMapOptions *ConfigMapOptions) func() { + os.Setenv("g_key1", "1") + os.Setenv("g_key2", "2") + return setupEnvFile("g_key1", "g_key2=") + }(), + fromEnvFile: "file.env", + appendHash: true, + expected: &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "get_env-54k882kkd2", + }, + Data: map[string]string{ + "g_key1": "1", + "g_key2": "", + }, + BinaryData: map[string][]byte{}, + }, + expectErr: false, + }, + "create_value_with_space_from_env_file_configmap": { + configMapName: "value_with_space", + setup: setupEnvFile("key1= value1"), + fromEnvFile: "file.env", + expected: &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "value_with_space", + }, + Data: map[string]string{ + "key1": " value1", + }, + BinaryData: map[string][]byte{}, + }, + expectErr: false, + }, + "create_value_with_space_from_env_file_hash_configmap": { + configMapName: "valid_with_space", + setup: setupEnvFile("key1= value1"), + fromEnvFile: "file.env", + appendHash: true, + expected: &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "valid_with_space-b4448m7gdm", + }, + Data: map[string]string{ + "key1": " value1", + }, + BinaryData: map[string][]byte{}, + }, + expectErr: false, + }, + "create_invalid_configmap_filepath_contains_=": { + configMapName: "foo", + fromFile: []string{"key1=/file=2"}, + expectErr: true, + }, + "create_invalid_configmap_filepath_key_contains_=": { + configMapName: "foo", + fromFile: []string{"=key=/file1"}, + expectErr: true, + }, + "create_invalid_configmap_literal_key_contains_=": { + configMapName: "foo", + fromFile: []string{"=key=value1"}, + expectErr: true, + }, + "create_invalid_configmap_duplicate_key1": { + configMapName: "foo", + fromLiteral: []string{"key1=value1", "key1=value2"}, + expectErr: true, + }, + "create_invalid_configmap_no_file": { + configMapName: "foo", + fromFile: []string{"key1=/file1"}, + expectErr: true, + }, + "create_invalid_configmap_invalid_literal": { + configMapName: "foo", + fromLiteral: []string{"key1value1"}, + expectErr: true, + }, + "create_invalid_configmap_invalid_filepath": { + configMapName: "foo", + fromFile: []string{"key1==file1"}, + expectErr: true, + }, + "create_invalid_configmap_too_many_args": { + configMapName: "too_many_args", + fromFile: []string{"key1=/file1"}, + fromEnvFile: "foo", + expectErr: true, + }, + "create_invalid_configmap_too_many_args_1": { + configMapName: "too_many_args_1", + fromLiteral: []string{"key1=value1"}, + fromEnvFile: "foo", + expectErr: true, + }, } - ioStreams, _, buf, _ := genericclioptions.NewTestIOStreams() - cmd := NewCmdCreateConfigMap(tf, ioStreams) - cmd.Flags().Set("output", "name") - cmd.Run(cmd, []string{configMap.Name}) - expectedOutput := "configmap/" + configMap.Name + "\n" - if buf.String() != expectedOutput { - t.Errorf("expected output: %s, but got: %s", expectedOutput, buf.String()) + + // run all the tests + for name, test := range tests { + t.Run(name, func(t *testing.T) { + configMapOptions := ConfigMapOptions{ + Name: test.configMapName, + Type: test.configMapType, + AppendHash: test.appendHash, + FileSources: test.fromFile, + LiteralSources: test.fromLiteral, + EnvFileSource: test.fromEnvFile, + } + + if test.setup != nil { + if teardown := test.setup(t, &configMapOptions); teardown != nil { + defer teardown() + } + } + + configMap, err := configMapOptions.createConfigMap() + if !test.expectErr && err != nil { + t.Errorf("test %s, unexpected error: %v", name, err) + } + if test.expectErr && err == nil { + t.Errorf("test %s was expecting an error but no error occurred", name) + } + if !apiequality.Semantic.DeepEqual(configMap, test.expected) { + t.Errorf("test %s expected:\n%#v\ngot:\n%#v", name, test.expected, configMap) + } + }) + } +} + +func setupEnvFile(lines ...string) func(*testing.T, *ConfigMapOptions) func() { + return func(t *testing.T, configMapOptions *ConfigMapOptions) func() { + f, err := ioutil.TempFile("", "cme") + if err != nil { + t.Errorf("unexpected error: %v", err) + } + for _, l := range lines { + f.WriteString(l) + f.WriteString("\r\n") + } + f.Close() + configMapOptions.EnvFileSource = f.Name() + return func() { + os.Remove(f.Name()) + } + } +} + +func setupBinaryFile(data []byte, files ...string) func(*testing.T, *ConfigMapOptions) func() { + return func(t *testing.T, configMapOptions *ConfigMapOptions) func() { + tmp, _ := ioutil.TempDir("", "") + for i, file := range files { + f := tmp + "/" + file + ioutil.WriteFile(f, data, 0644) + configMapOptions.FileSources[i] = f + } + return func() { + for _, file := range files { + f := tmp + "/" + file + os.Remove(f) + } + } } } diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/util/BUILD b/staging/src/k8s.io/kubectl/pkg/cmd/util/BUILD index 626f6129448..9780a062eed 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/util/BUILD +++ b/staging/src/k8s.io/kubectl/pkg/cmd/util/BUILD @@ -3,6 +3,7 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") go_library( name = "go_default_library", srcs = [ + "env_file.go", "factory.go", "factory_client_access.go", "helpers.go", @@ -21,6 +22,7 @@ go_library( "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/errors:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/validation:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/yaml:go_default_library", "//staging/src/k8s.io/cli-runtime/pkg/genericclioptions:go_default_library", "//staging/src/k8s.io/cli-runtime/pkg/resource:go_default_library", @@ -46,7 +48,10 @@ go_library( go_test( name = "go_default_test", - srcs = ["helpers_test.go"], + srcs = [ + "env_file_test.go", + "helpers_test.go", + ], embed = [":go_default_library"], deps = [ "//staging/src/k8s.io/api/core/v1:go_default_library", diff --git a/staging/src/k8s.io/kubectl/pkg/generate/versioned/env_file.go b/staging/src/k8s.io/kubectl/pkg/cmd/util/env_file.go similarity index 86% rename from staging/src/k8s.io/kubectl/pkg/generate/versioned/env_file.go rename to staging/src/k8s.io/kubectl/pkg/cmd/util/env_file.go index 1277257f24b..ed1255839ed 100644 --- a/staging/src/k8s.io/kubectl/pkg/generate/versioned/env_file.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/util/env_file.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package versioned +package util import ( "bufio" @@ -30,9 +30,9 @@ import ( var utf8bom = []byte{0xEF, 0xBB, 0xBF} -// proccessEnvFileLine returns a blank key if the line is empty or a comment. +// processEnvFileLine returns a blank key if the line is empty or a comment. // The value will be retrieved from the environment if necessary. -func proccessEnvFileLine(line []byte, filePath string, +func processEnvFileLine(line []byte, filePath string, currentLine int) (key, value string, err error) { if !utf8.Valid(line) { @@ -69,9 +69,9 @@ func proccessEnvFileLine(line []byte, filePath string, return } -// addFromEnvFile processes an env file allows a generic addTo to handle the +// AddFromEnvFile processes an env file allows a generic addTo to handle the // collection of key value pairs or returns an error. -func addFromEnvFile(filePath string, addTo func(key, value string) error) error { +func AddFromEnvFile(filePath string, addTo func(key, value string) error) error { f, err := os.Open(filePath) if err != nil { return err @@ -84,7 +84,7 @@ func addFromEnvFile(filePath string, addTo func(key, value string) error) error // Process the current line, retrieving a key/value pair if // possible. scannedBytes := scanner.Bytes() - key, value, err := proccessEnvFileLine(scannedBytes, filePath, currentLine) + key, value, err := processEnvFileLine(scannedBytes, filePath, currentLine) if err != nil { return err } diff --git a/staging/src/k8s.io/kubectl/pkg/generate/versioned/env_file_test.go b/staging/src/k8s.io/kubectl/pkg/cmd/util/env_file_test.go similarity index 94% rename from staging/src/k8s.io/kubectl/pkg/generate/versioned/env_file_test.go rename to staging/src/k8s.io/kubectl/pkg/cmd/util/env_file_test.go index 5f98b46e305..89b9f73a2dc 100644 --- a/staging/src/k8s.io/kubectl/pkg/generate/versioned/env_file_test.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/util/env_file_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package versioned +package util import ( "os" @@ -50,7 +50,7 @@ func Test_processEnvFileLine(t *testing.T) { } for _, tt := range testCases { t.Run(tt.name, func(t *testing.T) { - key, value, err := proccessEnvFileLine(tt.line, `filename`, tt.currentLine) + key, value, err := processEnvFileLine(tt.line, `filename`, tt.currentLine) t.Logf("Testing that %s.", tt.name) if tt.expectedKey != key { t.Errorf("\texpected key %q, received %q", tt.expectedKey, key) @@ -92,7 +92,7 @@ func Test_processEnvFileLine_readEnvironment(t *testing.T) { os.Setenv(realKey, `my_value`) - key, value, err := proccessEnvFileLine([]byte(realKey), `filename`, 3) + key, value, err := processEnvFileLine([]byte(realKey), `filename`, 3) if err != nil { t.Fatal(err) } diff --git a/staging/src/k8s.io/kubectl/pkg/generate/versioned/BUILD b/staging/src/k8s.io/kubectl/pkg/generate/versioned/BUILD index 5bc62e35566..e88dc261233 100644 --- a/staging/src/k8s.io/kubectl/pkg/generate/versioned/BUILD +++ b/staging/src/k8s.io/kubectl/pkg/generate/versioned/BUILD @@ -7,7 +7,6 @@ go_library( "clusterrolebinding.go", "configmap.go", "deployment.go", - "env_file.go", "generator.go", "namespace.go", "pdb.go", @@ -59,7 +58,6 @@ go_test( "clusterrolebinding_test.go", "configmap_test.go", "deployment_test.go", - "env_file_test.go", "namespace_test.go", "pdb_test.go", "priorityclass_test.go", diff --git a/staging/src/k8s.io/kubectl/pkg/generate/versioned/configmap.go b/staging/src/k8s.io/kubectl/pkg/generate/versioned/configmap.go index 52a5ebe3e1d..76b4676cdbd 100644 --- a/staging/src/k8s.io/kubectl/pkg/generate/versioned/configmap.go +++ b/staging/src/k8s.io/kubectl/pkg/generate/versioned/configmap.go @@ -24,9 +24,10 @@ import ( "strings" "unicode/utf8" - "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/validation" + cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/generate" "k8s.io/kubectl/pkg/util" "k8s.io/kubectl/pkg/util/hash" @@ -246,7 +247,7 @@ func handleConfigMapFromEnvFileSource(configMap *v1.ConfigMap, envFileSource str return fmt.Errorf("env config file cannot be a directory") } - return addFromEnvFile(envFileSource, func(key, value string) error { + return cmdutil.AddFromEnvFile(envFileSource, func(key, value string) error { return addKeyFromLiteralToConfigMap(configMap, key, value) }) } diff --git a/staging/src/k8s.io/kubectl/pkg/generate/versioned/secret.go b/staging/src/k8s.io/kubectl/pkg/generate/versioned/secret.go index 9acaecf337e..9a331b7467b 100644 --- a/staging/src/k8s.io/kubectl/pkg/generate/versioned/secret.go +++ b/staging/src/k8s.io/kubectl/pkg/generate/versioned/secret.go @@ -23,9 +23,10 @@ import ( "path" "strings" - "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/validation" + cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/generate" "k8s.io/kubectl/pkg/util" "k8s.io/kubectl/pkg/util/hash" @@ -246,7 +247,7 @@ func handleFromEnvFileSource(secret *v1.Secret, envFileSource string) error { return fmt.Errorf("env secret file cannot be a directory") } - return addFromEnvFile(envFileSource, func(key, value string) error { + return cmdutil.AddFromEnvFile(envFileSource, func(key, value string) error { return addKeyFromLiteralToSecret(secret, key, []byte(value)) }) }