Merge pull request #147 from guineveresaenger/labels-and-annotations

Edit add label/annotation
This commit is contained in:
Jeff Regan
2018-08-22 10:33:00 -07:00
committed by GitHub
5 changed files with 508 additions and 0 deletions

172
pkg/commands/addmetadata.go Normal file
View File

@@ -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)
}

View File

@@ -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())
}
}

View File

@@ -96,6 +96,12 @@ func newCmdAdd(fsys fs.FileSystem) *cobra.Command {
# Adds one or more base directories to the kustomization # Adds one or more base directories to the kustomization
kustomize edit add base <filepath> kustomize edit add base <filepath>
kustomize edit add base <filepath1>,<filepath2>,<filepath3> kustomize edit add base <filepath1>,<filepath2>,<filepath3>
# 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), Args: cobra.MinimumNArgs(1),
} }
@@ -104,6 +110,8 @@ func newCmdAdd(fsys fs.FileSystem) *cobra.Command {
newCmdAddPatch(fsys), newCmdAddPatch(fsys),
newCmdAddConfigMap(fsys), newCmdAddConfigMap(fsys),
newCmdAddBase(fsys), newCmdAddBase(fsys),
newCmdAddLabel(fsys),
newCmdAddAnnotation(fsys),
) )
return c return c
} }

35
pkg/validate/validate.go Normal file
View File

@@ -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
}

View File

@@ -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)
}
}
}