1/*
2Copyright 2018 The Kubernetes Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package add
18
19import (
20	"fmt"
21	"strings"
22
23	"github.com/spf13/cobra"
24	"sigs.k8s.io/kustomize/pkg/commands/kustfile"
25	"sigs.k8s.io/kustomize/pkg/constants"
26	"sigs.k8s.io/kustomize/pkg/fs"
27	"sigs.k8s.io/kustomize/pkg/types"
28)
29
30// kindOfAdd is the kind of metadata being added: label or annotation
31type kindOfAdd int
32
33const (
34	annotation kindOfAdd = iota
35	label
36)
37
38func (k kindOfAdd) String() string {
39	kinds := [...]string{
40		"annotation",
41		"label",
42	}
43	if k < 0 || k > 1 {
44		return "Unknown metadatakind"
45	}
46	return kinds[k]
47}
48
49type addMetadataOptions struct {
50	metadata     map[string]string
51	mapValidator func(map[string]string) error
52	kind         kindOfAdd
53}
54
55// newCmdAddAnnotation adds one or more commonAnnotations to the kustomization file.
56func newCmdAddAnnotation(fSys fs.FileSystem, v func(map[string]string) error) *cobra.Command {
57	var o addMetadataOptions
58	o.kind = annotation
59	o.mapValidator = v
60	cmd := &cobra.Command{
61		Use:   "annotation",
62		Short: "Adds one or more commonAnnotations to " + constants.KustomizationFileNames[0],
63		Example: `
64		add annotation {annotationKey1:annotationValue1},{annotationKey2:annotationValue2}`,
65		RunE: func(cmd *cobra.Command, args []string) error {
66			return o.runE(args, fSys, o.addAnnotations)
67		},
68	}
69	return cmd
70}
71
72// newCmdAddLabel adds one or more commonLabels to the kustomization file.
73func newCmdAddLabel(fSys fs.FileSystem, v func(map[string]string) error) *cobra.Command {
74	var o addMetadataOptions
75	o.kind = label
76	o.mapValidator = v
77	cmd := &cobra.Command{
78		Use:   "label",
79		Short: "Adds one or more commonLabels to " + constants.KustomizationFileNames[0],
80		Example: `
81		add label {labelKey1:labelValue1},{labelKey2:labelValue2}`,
82		RunE: func(cmd *cobra.Command, args []string) error {
83			return o.runE(args, fSys, o.addLabels)
84		},
85	}
86	return cmd
87}
88
89func (o *addMetadataOptions) runE(
90	args []string, fSys fs.FileSystem, adder func(*types.Kustomization) error) error {
91	err := o.validateAndParse(args)
92	if err != nil {
93		return err
94	}
95	kf, err := kustfile.NewKustomizationFile(fSys)
96	if err != nil {
97		return err
98	}
99	m, err := kf.Read()
100	if err != nil {
101		return err
102	}
103	err = adder(m)
104	if err != nil {
105		return err
106	}
107	return kf.Write(m)
108}
109
110// validateAndParse validates `add` commands and parses them into o.metadata
111func (o *addMetadataOptions) validateAndParse(args []string) error {
112	if len(args) < 1 {
113		return fmt.Errorf("must specify %s", o.kind)
114	}
115	if len(args) > 1 {
116		return fmt.Errorf("%ss must be comma-separated, with no spaces", o.kind)
117	}
118	m, err := o.convertToMap(args[0])
119	if err != nil {
120		return err
121	}
122	if err = o.mapValidator(m); err != nil {
123		return err
124	}
125	o.metadata = m
126	return nil
127}
128
129func (o *addMetadataOptions) convertToMap(arg string) (map[string]string, error) {
130	result := make(map[string]string)
131	inputs := strings.Split(arg, ",")
132	for _, input := range inputs {
133		c := strings.Index(input, ":")
134		if c == 0 {
135			// key is not passed
136			return nil, o.makeError(input, "need k:v pair where v may be quoted")
137		} else if c < 0 {
138			// only key passed
139			result[input] = ""
140		} else {
141			// both key and value passed
142			key := input[:c]
143			value := trimQuotes(input[c+1:])
144			result[key] = value
145		}
146	}
147	return result, nil
148}
149
150func (o *addMetadataOptions) addAnnotations(m *types.Kustomization) error {
151	if m.CommonAnnotations == nil {
152		m.CommonAnnotations = make(map[string]string)
153	}
154	return o.writeToMap(m.CommonAnnotations, annotation)
155}
156
157func (o *addMetadataOptions) addLabels(m *types.Kustomization) error {
158	if m.CommonLabels == nil {
159		m.CommonLabels = make(map[string]string)
160	}
161	return o.writeToMap(m.CommonLabels, label)
162}
163
164func (o *addMetadataOptions) writeToMap(m map[string]string, kind kindOfAdd) error {
165	for k, v := range o.metadata {
166		if _, ok := m[k]; ok {
167			return fmt.Errorf("%s %s already in kustomization file", kind, k)
168		}
169		m[k] = v
170	}
171	return nil
172}
173
174func (o *addMetadataOptions) makeError(input string, message string) error {
175	return fmt.Errorf("invalid %s: '%s' (%s)", o.kind, input, message)
176}
177
178func trimQuotes(s string) string {
179	if len(s) >= 2 {
180		if s[0] == '"' && s[len(s)-1] == '"' {
181			return s[1 : len(s)-1]
182		}
183	}
184	return s
185}
186