1
16
17 package create
18
19 import (
20 "context"
21 "fmt"
22 "os"
23 "path"
24 "strings"
25 "unicode/utf8"
26
27 "github.com/spf13/cobra"
28
29 corev1 "k8s.io/api/core/v1"
30 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
31 "k8s.io/apimachinery/pkg/runtime"
32 "k8s.io/apimachinery/pkg/util/validation"
33 "k8s.io/cli-runtime/pkg/genericclioptions"
34 "k8s.io/cli-runtime/pkg/genericiooptions"
35 corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
36 cmdutil "k8s.io/kubectl/pkg/cmd/util"
37 "k8s.io/kubectl/pkg/scheme"
38 "k8s.io/kubectl/pkg/util"
39 "k8s.io/kubectl/pkg/util/hash"
40 "k8s.io/kubectl/pkg/util/i18n"
41 "k8s.io/kubectl/pkg/util/templates"
42 )
43
44 var (
45 configMapLong = templates.LongDesc(i18n.T(`
46 Create a config map based on a file, directory, or specified literal value.
47
48 A single config map may package one or more key/value pairs.
49
50 When creating a config map based on a file, the key will default to the basename of the file, and the value will
51 default to the file content. If the basename is an invalid key, you may specify an alternate key.
52
53 When creating a config map based on a directory, each file whose basename is a valid key in the directory will be
54 packaged into the config map. Any directory entries except regular files are ignored (e.g. subdirectories,
55 symlinks, devices, pipes, etc).`))
56
57 configMapExample = templates.Examples(i18n.T(`
58 # Create a new config map named my-config based on folder bar
59 kubectl create configmap my-config --from-file=path/to/bar
60
61 # Create a new config map named my-config with specified keys instead of file basenames on disk
62 kubectl create configmap my-config --from-file=key1=/path/to/bar/file1.txt --from-file=key2=/path/to/bar/file2.txt
63
64 # Create a new config map named my-config with key1=config1 and key2=config2
65 kubectl create configmap my-config --from-literal=key1=config1 --from-literal=key2=config2
66
67 # Create a new config map named my-config from the key=value pairs in the file
68 kubectl create configmap my-config --from-file=path/to/bar
69
70 # Create a new config map named my-config from an env file
71 kubectl create configmap my-config --from-env-file=path/to/foo.env --from-env-file=path/to/bar.env`))
72 )
73
74
75 type ConfigMapOptions struct {
76
77 PrintFlags *genericclioptions.PrintFlags
78 PrintObj func(obj runtime.Object) error
79
80
81 Name string
82
83 Type string
84
85 FileSources []string
86
87 LiteralSources []string
88
89 EnvFileSources []string
90
91 AppendHash bool
92
93 FieldManager string
94 CreateAnnotation bool
95 Namespace string
96 EnforceNamespace bool
97
98 Client corev1client.CoreV1Interface
99 DryRunStrategy cmdutil.DryRunStrategy
100 ValidationDirective string
101
102 genericiooptions.IOStreams
103 }
104
105
106 func NewConfigMapOptions(ioStreams genericiooptions.IOStreams) *ConfigMapOptions {
107 return &ConfigMapOptions{
108 PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme),
109 IOStreams: ioStreams,
110 }
111 }
112
113
114 func NewCmdCreateConfigMap(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command {
115 o := NewConfigMapOptions(ioStreams)
116
117 cmd := &cobra.Command{
118 Use: "configmap NAME [--from-file=[key=]source] [--from-literal=key1=value1] [--dry-run=server|client|none]",
119 DisableFlagsInUseLine: true,
120 Aliases: []string{"cm"},
121 Short: i18n.T("Create a config map from a local file, directory or literal value"),
122 Long: configMapLong,
123 Example: configMapExample,
124 Run: func(cmd *cobra.Command, args []string) {
125 cmdutil.CheckErr(o.Complete(f, cmd, args))
126 cmdutil.CheckErr(o.Validate())
127 cmdutil.CheckErr(o.Run())
128 },
129 }
130 o.PrintFlags.AddFlags(cmd)
131
132 cmdutil.AddApplyAnnotationFlags(cmd)
133 cmdutil.AddValidateFlags(cmd)
134 cmdutil.AddDryRunFlag(cmd)
135
136 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.")
137 cmd.Flags().StringArrayVar(&o.LiteralSources, "from-literal", o.LiteralSources, "Specify a key and literal value to insert in configmap (i.e. mykey=somevalue)")
138 cmd.Flags().StringSliceVar(&o.EnvFileSources, "from-env-file", o.EnvFileSources, "Specify the path to a file to read lines of key=val pairs to create a configmap.")
139 cmd.Flags().BoolVar(&o.AppendHash, "append-hash", o.AppendHash, "Append a hash of the configmap to its name.")
140
141 cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-create")
142
143 return cmd
144 }
145
146
147 func (o *ConfigMapOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
148 var err error
149 o.Name, err = NameFromCommandArgs(cmd, args)
150 if err != nil {
151 return err
152 }
153
154 restConfig, err := f.ToRESTConfig()
155 if err != nil {
156 return err
157 }
158
159 o.Client, err = corev1client.NewForConfig(restConfig)
160 if err != nil {
161 return err
162 }
163
164 o.CreateAnnotation = cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag)
165
166 o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd)
167 if err != nil {
168 return err
169 }
170
171 o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
172 if err != nil {
173 return err
174 }
175
176 cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy)
177 printer, err := o.PrintFlags.ToPrinter()
178 if err != nil {
179 return err
180 }
181 o.PrintObj = func(obj runtime.Object) error {
182 return printer.PrintObj(obj, o.Out)
183 }
184
185 o.ValidationDirective, err = cmdutil.GetValidationDirective(cmd)
186 if err != nil {
187 return err
188 }
189
190 return nil
191 }
192
193
194 func (o *ConfigMapOptions) Validate() error {
195 if len(o.Name) == 0 {
196 return fmt.Errorf("name must be specified")
197 }
198 if len(o.EnvFileSources) > 0 && (len(o.FileSources) > 0 || len(o.LiteralSources) > 0) {
199 return fmt.Errorf("from-env-file cannot be combined with from-file or from-literal")
200 }
201 return nil
202 }
203
204
205 func (o *ConfigMapOptions) Run() error {
206 configMap, err := o.createConfigMap()
207 if err != nil {
208 return err
209 }
210 if err := util.CreateOrUpdateAnnotation(o.CreateAnnotation, configMap, scheme.DefaultJSONEncoder()); err != nil {
211 return err
212 }
213 if o.DryRunStrategy != cmdutil.DryRunClient {
214 createOptions := metav1.CreateOptions{}
215 if o.FieldManager != "" {
216 createOptions.FieldManager = o.FieldManager
217 }
218 createOptions.FieldValidation = o.ValidationDirective
219 if o.DryRunStrategy == cmdutil.DryRunServer {
220 createOptions.DryRun = []string{metav1.DryRunAll}
221 }
222 configMap, err = o.Client.ConfigMaps(o.Namespace).Create(context.TODO(), configMap, createOptions)
223 if err != nil {
224 return fmt.Errorf("failed to create configmap: %v", err)
225 }
226 }
227
228 return o.PrintObj(configMap)
229 }
230
231
232
233 func (o *ConfigMapOptions) createConfigMap() (*corev1.ConfigMap, error) {
234 namespace := ""
235 if o.EnforceNamespace {
236 namespace = o.Namespace
237 }
238
239 configMap := &corev1.ConfigMap{
240 TypeMeta: metav1.TypeMeta{
241 APIVersion: corev1.SchemeGroupVersion.String(),
242 Kind: "ConfigMap",
243 },
244 ObjectMeta: metav1.ObjectMeta{
245 Name: o.Name,
246 Namespace: namespace,
247 },
248 }
249 configMap.Name = o.Name
250 configMap.Data = map[string]string{}
251 configMap.BinaryData = map[string][]byte{}
252
253 if len(o.FileSources) > 0 {
254 if err := handleConfigMapFromFileSources(configMap, o.FileSources); err != nil {
255 return nil, err
256 }
257 }
258 if len(o.LiteralSources) > 0 {
259 if err := handleConfigMapFromLiteralSources(configMap, o.LiteralSources); err != nil {
260 return nil, err
261 }
262 }
263 if len(o.EnvFileSources) > 0 {
264 if err := handleConfigMapFromEnvFileSources(configMap, o.EnvFileSources); err != nil {
265 return nil, err
266 }
267 }
268 if o.AppendHash {
269 hash, err := hash.ConfigMapHash(configMap)
270 if err != nil {
271 return nil, err
272 }
273 configMap.Name = fmt.Sprintf("%s-%s", configMap.Name, hash)
274 }
275
276 return configMap, nil
277 }
278
279
280
281 func handleConfigMapFromLiteralSources(configMap *corev1.ConfigMap, literalSources []string) error {
282 for _, literalSource := range literalSources {
283 keyName, value, err := util.ParseLiteralSource(literalSource)
284 if err != nil {
285 return err
286 }
287 err = addKeyFromLiteralToConfigMap(configMap, keyName, value)
288 if err != nil {
289 return err
290 }
291 }
292
293 return nil
294 }
295
296
297
298 func handleConfigMapFromFileSources(configMap *corev1.ConfigMap, fileSources []string) error {
299 for _, fileSource := range fileSources {
300 keyName, filePath, err := util.ParseFileSource(fileSource)
301 if err != nil {
302 return err
303 }
304 info, err := os.Stat(filePath)
305 if err != nil {
306 switch err := err.(type) {
307 case *os.PathError:
308 return fmt.Errorf("error reading %s: %v", filePath, err.Err)
309 default:
310 return fmt.Errorf("error reading %s: %v", filePath, err)
311 }
312
313 }
314 if info.IsDir() {
315 if strings.Contains(fileSource, "=") {
316 return fmt.Errorf("cannot give a key name for a directory path")
317 }
318 fileList, err := os.ReadDir(filePath)
319 if err != nil {
320 return fmt.Errorf("error listing files in %s: %v", filePath, err)
321 }
322 for _, item := range fileList {
323 itemPath := path.Join(filePath, item.Name())
324 if item.Type().IsRegular() {
325 keyName = item.Name()
326 err = addKeyFromFileToConfigMap(configMap, keyName, itemPath)
327 if err != nil {
328 return err
329 }
330 }
331 }
332 } else {
333 if err := addKeyFromFileToConfigMap(configMap, keyName, filePath); err != nil {
334 return err
335 }
336
337 }
338 }
339 return nil
340 }
341
342
343
344 func handleConfigMapFromEnvFileSources(configMap *corev1.ConfigMap, envFileSources []string) error {
345 for _, envFileSource := range envFileSources {
346 info, err := os.Stat(envFileSource)
347 if err != nil {
348 switch err := err.(type) {
349 case *os.PathError:
350 return fmt.Errorf("error reading %s: %v", envFileSource, err.Err)
351 default:
352 return fmt.Errorf("error reading %s: %v", envFileSource, err)
353 }
354 }
355 if info.IsDir() {
356 return fmt.Errorf("env config file cannot be a directory")
357 }
358 err = cmdutil.AddFromEnvFile(envFileSource, func(key, value string) error {
359 return addKeyFromLiteralToConfigMap(configMap, key, value)
360 })
361 if err != nil {
362 return err
363 }
364 }
365
366 return nil
367 }
368
369
370
371 func addKeyFromFileToConfigMap(configMap *corev1.ConfigMap, keyName, filePath string) error {
372 data, err := os.ReadFile(filePath)
373 if err != nil {
374 return err
375 }
376 if utf8.Valid(data) {
377 return addKeyFromLiteralToConfigMap(configMap, keyName, string(data))
378 }
379 err = validateNewConfigMap(configMap, keyName)
380 if err != nil {
381 return err
382 }
383 configMap.BinaryData[keyName] = data
384
385 return nil
386 }
387
388
389
390 func addKeyFromLiteralToConfigMap(configMap *corev1.ConfigMap, keyName, data string) error {
391 err := validateNewConfigMap(configMap, keyName)
392 if err != nil {
393 return err
394 }
395 configMap.Data[keyName] = data
396
397 return nil
398 }
399
400
401
402 func validateNewConfigMap(configMap *corev1.ConfigMap, keyName string) error {
403 if errs := validation.IsConfigMapKey(keyName); len(errs) > 0 {
404 return fmt.Errorf("%q is not a valid key name for a ConfigMap: %s", keyName, strings.Join(errs, ","))
405 }
406 if _, exists := configMap.Data[keyName]; exists {
407 return fmt.Errorf("cannot add key %q, another key by that name already exists in Data for ConfigMap %q", keyName, configMap.Name)
408 }
409 if _, exists := configMap.BinaryData[keyName]; exists {
410 return fmt.Errorf("cannot add key %q, another key by that name already exists in BinaryData for ConfigMap %q", keyName, configMap.Name)
411 }
412
413 return nil
414 }
415
View as plain text