diff --git a/pkg/commands/addmetadata.go b/pkg/commands/addmetadata.go new file mode 100644 index 000000000..56e57ba32 --- /dev/null +++ b/pkg/commands/addmetadata.go @@ -0,0 +1,172 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed 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 commands + +import ( + "fmt" + "strings" + + "github.com/kubernetes-sigs/kustomize/pkg/constants" + "github.com/kubernetes-sigs/kustomize/pkg/fs" + "github.com/kubernetes-sigs/kustomize/pkg/validate" + "github.com/spf13/cobra" +) + +// KindOfAdd is the kind of metadata being added: label or annotation +type KindOfAdd int + +const ( + annotation KindOfAdd = iota + label +) + +func (k KindOfAdd) String() string { + kinds := [...]string{ + "annotation", + "label", + } + if k < 0 || k > 1 { + return "Unknown metadatakind" + } + return kinds[k] +} + +type addMetadataOptions struct { + metadata map[string]string +} + +// newCmdAddAnnotation adds one or more commonAnnotations to the kustomization file. +func newCmdAddAnnotation(fsys fs.FileSystem) *cobra.Command { + var o addMetadataOptions + + cmd := &cobra.Command{ + Use: "annotation", + Short: "Adds one or more commonAnnotations to the kustomization.yaml in current directory", + Example: ` + add annotation {annotationKey1:annotationValue1},{annotationKey2:annotationValue2}`, + RunE: func(cmd *cobra.Command, args []string) error { + err := o.ValidateAndParse(args, annotation) + if err != nil { + return err + } + return o.RunAddAnnotation(fsys, annotation) + }, + } + return cmd +} + +// newCmdAddLabel adds one or more commonLabels to the kustomization file. +func newCmdAddLabel(fsys fs.FileSystem) *cobra.Command { + var o addMetadataOptions + + cmd := &cobra.Command{ + Use: "label", + Short: "Adds one or more commonLabels to the kustomization.yaml in current directory", + Example: ` + add label {labelKey1:labelValue1},{labelKey2:labelValue2}`, + RunE: func(cmd *cobra.Command, args []string) error { + err := o.ValidateAndParse(args, label) + if err != nil { + return err + } + return o.RunAddLabel(fsys, label) + }, + } + return cmd +} + +// ValidateAndParse validates addLabel and addAnnotation commands and parses them into o.metadata +func (o *addMetadataOptions) ValidateAndParse(args []string, k KindOfAdd) error { + o.metadata = make(map[string]string) + if len(args) < 1 { + return fmt.Errorf("must specify %s", k) + } + if len(args) > 1 { + return fmt.Errorf("%ss must be comma-separated, with no spaces. See help text for example", k) + } + inputs := strings.Split(args[0], ",") + for _, input := range inputs { + switch k { + case label: + valid, err := validate.IsValidLabel(input) + if !valid { + return err + } + case annotation: + valid, err := validate.IsValidAnnotation(input) + if !valid { + return err + } + default: + return fmt.Errorf("unknown metadata kind %s", k) + } + //parse annotation keys and values into metadata + kv := strings.Split(input, ":") + o.metadata[kv[0]] = kv[1] + } + return nil +} + +// RunAddAnnotation runs addAnnotation command, doing the real work. +func (o *addMetadataOptions) RunAddAnnotation(fsys fs.FileSystem, k KindOfAdd) error { + mf, err := newKustomizationFile(constants.KustomizationFileName, fsys) + if err != nil { + return err + } + m, err := mf.read() + if err != nil { + return err + } + + if m.CommonAnnotations == nil { + m.CommonAnnotations = make(map[string]string) + } + + for key, value := range o.metadata { + if k == annotation { + if _, ok := m.CommonAnnotations[key]; ok { + return fmt.Errorf("%s %s already in kustomization file", k, key) + } + m.CommonAnnotations[key] = value + } + } + return mf.write(m) +} + +// RunAddLabel runs addLabel command, doing the real work. +func (o *addMetadataOptions) RunAddLabel(fsys fs.FileSystem, k KindOfAdd) error { + mf, err := newKustomizationFile(constants.KustomizationFileName, fsys) + if err != nil { + return err + } + m, err := mf.read() + if err != nil { + return err + } + + if m.CommonLabels == nil { + m.CommonLabels = make(map[string]string) + } + + for key, value := range o.metadata { + if _, ok := m.CommonLabels[key]; ok { + return fmt.Errorf("%s %s already in kustomization file", k, key) + } + m.CommonLabels[key] = value + } + return mf.write(m) +} diff --git a/pkg/commands/addmetadata_test.go b/pkg/commands/addmetadata_test.go new file mode 100644 index 000000000..e796ff04b --- /dev/null +++ b/pkg/commands/addmetadata_test.go @@ -0,0 +1,190 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed 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 commands + +import ( + "reflect" + "testing" + + "github.com/kubernetes-sigs/kustomize/pkg/constants" + "github.com/kubernetes-sigs/kustomize/pkg/fs" +) + +func TestParseValidateInput(t *testing.T) { + var testcases = []struct { + input string + valid bool + name string + expectedData map[string]string + kind KindOfAdd + }{ + { + input: "otters:cute", + valid: true, + name: "Adds single input", + expectedData: map[string]string{ + "otters": "cute", + }, + kind: label, + }, + { + input: "owls:great,unicorns:magical", + valid: true, + name: "Adds two items", + expectedData: map[string]string{ + "owls": "great", + "unicorns": "magical", + }, + kind: label, + }, + { + input: "123:45", + valid: true, + name: "Numeric input is allowed", + expectedData: map[string]string{ + "123": "45", + }, + kind: annotation, + }, + { + input: " ", + valid: false, + name: "Empty space input", + expectedData: nil, + kind: annotation, + }, + } + var o addMetadataOptions + for _, tc := range testcases { + args := []string{tc.input} + err := o.ValidateAndParse(args, tc.kind) + if err != nil && tc.valid { + t.Errorf("for test case %s, unexpected cmd error: %v", tc.name, err) + } + if err == nil && !tc.valid { + t.Errorf("unexpected error: expected invalid format error for test case %v", tc.name) + } + //o.metadata should be the same as expectedData + if tc.valid { + if !reflect.DeepEqual(o.metadata, tc.expectedData) { + t.Errorf("unexpected error: for test case %s, unexpected data was added", tc.name) + } + } else { + if len(o.metadata) != 0 { + t.Errorf("unexpected error: for test case %s, expected no data to be added", tc.name) + } + } + } +} + +func TestRunAddAnnotation(t *testing.T) { + fakeFS := fs.MakeFakeFS() + fakeFS.WriteFile(constants.KustomizationFileName, []byte(kustomizationContent)) + var o addMetadataOptions + o.metadata = map[string]string{"owls": "cute", "otters": "adorable"} + + err := o.RunAddAnnotation(fakeFS, annotation) + if err != nil { + t.Errorf("unexpected error: could not write to kustomization file") + } + // adding the same test input should not work + err = o.RunAddAnnotation(fakeFS, annotation) + if err == nil { + t.Errorf("expected already in kustomization file error") + } + // adding new annotations should work + o.metadata = map[string]string{"new": "annotation"} + err = o.RunAddAnnotation(fakeFS, annotation) + if err != nil { + t.Errorf("unexpected error: could not write to kustomization file") + } +} + +func TestAddAnnotationNoArgs(t *testing.T) { + fakeFS := fs.MakeFakeFS() + cmd := newCmdAddAnnotation(fakeFS) + err := cmd.Execute() + if err == nil { + t.Errorf("expected an error but error is %v", err) + } + if err != nil && err.Error() != "must specify annotation" { + t.Errorf("incorrect error: %v", err.Error()) + } +} +func TestAddAnnotationMultipleArgs(t *testing.T) { + fakeFS := fs.MakeFakeFS() + fakeFS.WriteFile(constants.KustomizationFileName, []byte(kustomizationContent)) + cmd := newCmdAddAnnotation(fakeFS) + args := []string{"this:annotation", "has:spaces"} + err := cmd.RunE(cmd, args) + if err == nil { + t.Errorf("expected an error but error is %v", err) + } + if err != nil && err.Error() != "annotations must be comma-separated, with no spaces. See help text for example" { + t.Errorf("incorrect error: %v", err.Error()) + } +} + +func TestRunAddLabel(t *testing.T) { + fakeFS := fs.MakeFakeFS() + fakeFS.WriteFile(constants.KustomizationFileName, []byte(kustomizationContent)) + var o addMetadataOptions + o.metadata = map[string]string{"owls": "cute", "otters": "adorable"} + + err := o.RunAddLabel(fakeFS, label) + if err != nil { + t.Errorf("unexpected error: could not write to kustomization file") + } + // adding the same test input should not work + err = o.RunAddLabel(fakeFS, label) + if err == nil { + t.Errorf("expected already in kustomization file error") + } + // adding new labels should work + o.metadata = map[string]string{"new": "label"} + err = o.RunAddLabel(fakeFS, label) + if err != nil { + t.Errorf("unexpected error: could not write to kustomization file") + } +} + +func TestAddLabelNoArgs(t *testing.T) { + fakeFS := fs.MakeFakeFS() + + cmd := newCmdAddLabel(fakeFS) + err := cmd.Execute() + if err == nil { + t.Errorf("expected an error but error is: %v", err) + } + if err != nil && err.Error() != "must specify label" { + t.Errorf("incorrect error: %v", err.Error()) + } +} + +func TestAddLabelMultipleArgs(t *testing.T) { + fakeFS := fs.MakeFakeFS() + fakeFS.WriteFile(constants.KustomizationFileName, []byte(kustomizationContent)) + cmd := newCmdAddLabel(fakeFS) + args := []string{"this:input", "has:spaces"} + err := cmd.RunE(cmd, args) + if err == nil { + t.Errorf("expected an error but error is: %v", err) + } + if err != nil && err.Error() != "labels must be comma-separated, with no spaces. See help text for example" { + t.Errorf("incorrect error: %v", err.Error()) + } +} diff --git a/pkg/commands/commands.go b/pkg/commands/commands.go index 20648465a..20e987a66 100644 --- a/pkg/commands/commands.go +++ b/pkg/commands/commands.go @@ -96,6 +96,12 @@ func newCmdAdd(fsys fs.FileSystem) *cobra.Command { # Adds one or more base directories to the kustomization kustomize edit add base kustomize edit add base ,, + + # Adds one or more commonLabels to the kustomization + kustomize edit add label {labelKey1:labelValue1},{labelKey2:labelValue2} + + # Adds one or more commonAnnotations to the kustomization + kustomize edit add annotation {annotationKey1:annotationValue1},{annotationKey2:annotationValue2} `, Args: cobra.MinimumNArgs(1), } @@ -104,6 +110,8 @@ func newCmdAdd(fsys fs.FileSystem) *cobra.Command { newCmdAddPatch(fsys), newCmdAddConfigMap(fsys), newCmdAddBase(fsys), + newCmdAddLabel(fsys), + newCmdAddAnnotation(fsys), ) return c } diff --git a/pkg/validate/validate.go b/pkg/validate/validate.go new file mode 100644 index 000000000..94ae5de11 --- /dev/null +++ b/pkg/validate/validate.go @@ -0,0 +1,35 @@ +package validate + +import ( + "fmt" + "regexp" +) + +// TODO: these are rudimentary placeholder validation functions and need +// additional work to truly match expected syntax rules. + +// IsValidLabel checks whether a label key/value pair has correct syntax and +// character set +func IsValidLabel(keyval string) (bool, error) { + ok, err := regexp.MatchString(`\A([a-zA-Z0-9_.-]+):([a-zA-Z0-9_.-]+)\z`, keyval) + if err != nil { + return false, err + } + if !ok { + return false, fmt.Errorf("invalid label format: %s", keyval) + } + return true, nil +} + +// IsValidAnnotation checks whether an annotation key/value pair has correct +// syntax and character set +func IsValidAnnotation(keyval string) (bool, error) { + ok, err := regexp.MatchString(`\A([a-zA-Z0-9_.-]+):([a-zA-Z0-9_.-]+)\z`, keyval) + if err != nil { + return false, err + } + if !ok { + return false, fmt.Errorf("invalid annotation format: %s", keyval) + } + return true, nil +} diff --git a/pkg/validate/validate_test.go b/pkg/validate/validate_test.go new file mode 100644 index 000000000..ff84f82b2 --- /dev/null +++ b/pkg/validate/validate_test.go @@ -0,0 +1,103 @@ +package validate + +import "testing" + +func TestIsValidLabel(t *testing.T) { + testcases := []struct { + input, name string + valid bool + }{ + { + input: "otters:cute", + valid: true, + name: "Valid input format", + }, + { + input: "dogs,cats", + valid: false, + name: "Does not contain colon", + }, + { + input: ":noKey", + valid: false, + name: "Missing key", + }, + { + input: "noValue:", + valid: false, + name: "Missing value", + }, + { + input: "exclamation!:point", + valid: false, + name: "Non-alphanumeric input", + }, + { + input: "123:45", + valid: true, + name: "Numeric input is allowed", + }, + } + for _, tc := range testcases { + ok, err := IsValidLabel(tc.input) + if tc.valid && err != nil { + t.Errorf("unexpected error: for test case %s, expected no error but got: %s", tc.name, err.Error()) + } + if ok && !tc.valid { + t.Errorf("for test case %s, expected invalid label format error", tc.name) + } + if !ok && tc.valid { + t.Errorf("unexpected error: for test case %s, expected test to pass", tc.name) + } + } +} + +func TestIsValidAnnotation(t *testing.T) { + testcases := []struct { + input, name string + valid bool + }{ + { + input: "owls:adorable", + valid: true, + name: "Valid input format", + }, + { + input: "cake,cookies", + valid: false, + name: "Does not contain colon", + }, + { + input: ":noKey", + valid: false, + name: "Missing key", + }, + { + input: "noValue:", + valid: false, + name: "Missing value", + }, + { + input: "exclamation!:point", + valid: false, + name: "Input has a bang!", + }, + { + input: "987:65", + valid: true, + name: "Numeric input is valid", + }, + } + for _, tc := range testcases { + ok, err := IsValidAnnotation(tc.input) + if tc.valid && err != nil { + t.Errorf("unexpected error: for test case %s, expected no error but got: %s", tc.name, err.Error()) + } + if ok && !tc.valid { + t.Errorf("for test case %s, expected invalid annotation format error", tc.name) + } + if !ok && tc.valid { + t.Errorf("unexpected error: for test case %s, expected test to pass", tc.name) + } + } +}