From 7b5e43d343ebf2fdb29c7f6a6af072c895b26d2f Mon Sep 17 00:00:00 2001 From: Adrian Berger <43774417+adberger@users.noreply.github.com> Date: Wed, 11 Aug 2021 01:00:40 +0200 Subject: [PATCH] Feature: Add edit set annotation (#4073) * Add edit set annotation feature * Apply suggested code improvements * Apply suggested changes * Fix regex, add more tests * Add constant for common error message * Fix too many characters per line error * Use string concatenation instead, add FailNow call --- kustomize/commands/edit/set/all.go | 1 + kustomize/commands/edit/set/setannotation.go | 99 ++++++++ .../commands/edit/set/setannotation_test.go | 229 ++++++++++++++++++ 3 files changed, 329 insertions(+) create mode 100644 kustomize/commands/edit/set/setannotation.go create mode 100644 kustomize/commands/edit/set/setannotation_test.go diff --git a/kustomize/commands/edit/set/all.go b/kustomize/commands/edit/set/all.go index 6fc373bc6..e04f64c1d 100644 --- a/kustomize/commands/edit/set/all.go +++ b/kustomize/commands/edit/set/all.go @@ -32,6 +32,7 @@ func NewCmdSet(fSys filesys.FileSystem, ldr ifc.KvLoader, v ifc.Validator) *cobr newCmdSetImage(fSys), newCmdSetReplicas(fSys), newCmdSetLabel(fSys, ldr.Validator().MakeLabelValidator()), + newCmdSetAnnotation(fSys, ldr.Validator().MakeAnnotationValidator()), ) return c } diff --git a/kustomize/commands/edit/set/setannotation.go b/kustomize/commands/edit/set/setannotation.go new file mode 100644 index 000000000..bd23edf52 --- /dev/null +++ b/kustomize/commands/edit/set/setannotation.go @@ -0,0 +1,99 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package set + +import ( + "errors" + "fmt" + "regexp" + + "github.com/spf13/cobra" + "sigs.k8s.io/kustomize/api/konfig" + "sigs.k8s.io/kustomize/api/types" + "sigs.k8s.io/kustomize/kustomize/v4/commands/internal/kustfile" + "sigs.k8s.io/kustomize/kustomize/v4/commands/internal/util" + "sigs.k8s.io/kustomize/kyaml/filesys" +) + +type setAnnotationOptions struct { + metadata map[string]string + mapValidator func(map[string]string) error +} + +// IsValidKey checks key against regex. First part for prefix segment (DNS1123Label) of an annotation followed by a slash, second part for name segment of an annotation +// see https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ +var IsValidKey = regexp.MustCompile(`^([a-zA-Z](([-a-zA-Z0-9.]{0,251})[a-zA-Z0-9])?\/)?[a-zA-Z0-9]([-a-zA-Z0-9_.]{0,61}[a-zA-Z0-9])?$`).MatchString + +// newCmdSetAnnotation sets one or more commonAnnotations to the kustomization file. +func newCmdSetAnnotation(fSys filesys.FileSystem, v func(map[string]string) error) *cobra.Command { + var o setAnnotationOptions + o.mapValidator = v + cmd := &cobra.Command{ + Use: "annotation", + Short: "Sets one or more commonAnnotations in " + + konfig.DefaultKustomizationFileName(), + Example: ` + set annotation {annotationKey1:annotationValue1} {annotationKey2:annotationValue2}`, + RunE: func(cmd *cobra.Command, args []string) error { + return o.runE(args, fSys, o.setAnnotations) + }, + } + return cmd +} + +func (o *setAnnotationOptions) runE( + args []string, fSys filesys.FileSystem, setter func(*types.Kustomization) error) error { + err := o.validateAndParse(args) + if err != nil { + return err + } + kf, err := kustfile.NewKustomizationFile(fSys) + if err != nil { + return err + } + m, err := kf.Read() + if err != nil { + return err + } + err = setter(m) + if err != nil { + return err + } + return kf.Write(m) +} + +// validateAndParse validates `set` commands and parses them into o.metadata +func (o *setAnnotationOptions) validateAndParse(args []string) error { + if len(args) < 1 { + return fmt.Errorf("must specify annotation") + } + m, err := util.ConvertSliceToMap(args, "annotation") + if err != nil { + return err + } + if err = o.mapValidator(m); err != nil { + return err + } + for key := range m { + if !IsValidKey(key) { + return errors.New("invalid annotation key: see the syntax and character set rules at https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/") + } + } + o.metadata = m + return nil +} + +func (o *setAnnotationOptions) setAnnotations(m *types.Kustomization) error { + if m.CommonAnnotations == nil { + m.CommonAnnotations = make(map[string]string) + } + return o.writeToMap(m.CommonAnnotations) +} + +func (o *setAnnotationOptions) writeToMap(m map[string]string) error { + for k, v := range o.metadata { + m[k] = v + } + return nil +} diff --git a/kustomize/commands/edit/set/setannotation_test.go b/kustomize/commands/edit/set/setannotation_test.go new file mode 100644 index 000000000..a109301fb --- /dev/null +++ b/kustomize/commands/edit/set/setannotation_test.go @@ -0,0 +1,229 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package set + +import ( + "testing" + + valtest_test "sigs.k8s.io/kustomize/api/testutils/valtest" + "sigs.k8s.io/kustomize/api/types" + "sigs.k8s.io/kustomize/kustomize/v4/commands/internal/kustfile" + testutils_test "sigs.k8s.io/kustomize/kustomize/v4/commands/internal/testutils" + "sigs.k8s.io/kustomize/kyaml/filesys" +) + +const invalidAnnotationKey string = "invalid annotation key: see the syntax and character set rules at " + + "https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/" + +func makeAnnotationKustomization(t *testing.T) *types.Kustomization { + fSys := filesys.MakeFsInMemory() + testutils_test.WriteTestKustomization(fSys) + kf, err := kustfile.NewKustomizationFile(fSys) + if err != nil { + t.Errorf("unexpected new error %v", err) + } + m, err := kf.Read() + if err != nil { + t.Errorf("unexpected read error %v", err) + } + return m +} + +func TestRunSetAnnotation(t *testing.T) { + var o setAnnotationOptions + o.metadata = map[string]string{"owls": "cute", "otters": "adorable"} + + m := makeAnnotationKustomization(t) + err := o.setAnnotations(m) + if err != nil { + t.Errorf("unexpected error: could not write to kustomization file") + } + // adding the same test input should work + err = o.setAnnotations(m) + if err != nil { + t.Errorf("unexpected error: could not write to kustomization file") + } + // adding new annotations should work + o.metadata = map[string]string{"new": "annotation", "owls": "not cute"} + err = o.setAnnotations(m) + if err != nil { + t.Errorf("unexpected error: could not write to kustomization file") + } +} + +func TestSetAnnotationNoArgs(t *testing.T) { + fSys := filesys.MakeFsInMemory() + v := valtest_test.MakeHappyMapValidator(t) + cmd := newCmdSetAnnotation(fSys, v.Validator) + err := cmd.Execute() + v.VerifyNoCall() + if err == nil { + t.Errorf("expected an error") + t.FailNow() + } + if err.Error() != "must specify annotation" { + t.Errorf("incorrect error: %v", err.Error()) + } +} + +func TestSetAnnotationInvalidFormat(t *testing.T) { + fSys := filesys.MakeFsInMemory() + v := valtest_test.MakeSadMapValidator(t) + cmd := newCmdSetAnnotation(fSys, v.Validator) + args := []string{"exclamation!:point"} + err := cmd.RunE(cmd, args) + v.VerifyCall() + if err == nil { + t.Errorf("expected an error") + t.FailNow() + } + if err.Error() != valtest_test.SAD { + t.Errorf("incorrect error: %v", err.Error()) + } +} + +func TestSetAnnotationPrefixColonName(t *testing.T) { + var o setAnnotationOptions + o.metadata = map[string]string{"internal.config.kubernetes.io/options": "true"} + + m := makeAnnotationKustomization(t) + err := o.setAnnotations(m) + if err != nil { + t.Errorf("unexpected error: %v", err.Error()) + } +} + +func TestSetAnnotation253Prefix63Name(t *testing.T) { + var o setAnnotationOptions + o.metadata = map[string]string{"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstu" + + "vwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz" + + "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcde" + + "fghijklmnopqrstuvwxyzabcdefghijklmnopqrs/abcdefghijklmnopqrstuvwxyzabcdefghijklmnop" + + "qrstuvwxyzabcdefghijk": "true"} + + m := makeAnnotationKustomization(t) + err := o.setAnnotations(m) + if err != nil { + t.Errorf("unexpected error: %v", err.Error()) + } +} + +func TestSetAnnotation254Prefix62Name(t *testing.T) { + fSys := filesys.MakeFsInMemory() + v := valtest_test.MakeHappyMapValidator(t) + cmd := newCmdSetAnnotation(fSys, v.Validator) + args := []string{"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghi" + + "jklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmn" + + "opqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrs" + + "tuvwxyzabcdefghijklmnopqrst/abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabc" + + "defghij:true"} + err := cmd.RunE(cmd, args) + v.VerifyCall() + if err == nil { + t.Errorf("expected an error") + t.FailNow() + } + if err.Error() != invalidAnnotationKey { + t.Errorf("incorrect error: %v", err.Error()) + } +} + +func TestSetAnnotation252Prefix64Name(t *testing.T) { + fSys := filesys.MakeFsInMemory() + v := valtest_test.MakeHappyMapValidator(t) + cmd := newCmdSetAnnotation(fSys, v.Validator) + args := []string{"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghi" + + "jklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmn" + + "opqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrs" + + "tuvwxyzabcdefghijklmnopqr/abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcde" + + "fghijkl:true"} + err := cmd.RunE(cmd, args) + v.VerifyCall() + if err == nil { + t.Errorf("expected an error") + t.FailNow() + } + if err.Error() != invalidAnnotationKey { + t.Errorf("incorrect error: %v", err.Error()) + } +} + +func TestSetAnnotationNoKey(t *testing.T) { + fSys := filesys.MakeFsInMemory() + v := valtest_test.MakeHappyMapValidator(t) + cmd := newCmdSetAnnotation(fSys, v.Validator) + args := []string{":nokey"} + err := cmd.RunE(cmd, args) + v.VerifyNoCall() + if err == nil { + t.Errorf("expected an error") + t.FailNow() + } + if err.Error() != "invalid annotation: ':nokey' (need k:v pair where v may be quoted)" { + t.Errorf("incorrect error: %v", err.Error()) + } +} + +func TestSetAnnotationTooManyColons(t *testing.T) { + fSys := filesys.MakeFsInMemory() + testutils_test.WriteTestKustomization(fSys) + v := valtest_test.MakeHappyMapValidator(t) + cmd := newCmdSetAnnotation(fSys, v.Validator) + args := []string{"key:v1:v2"} + err := cmd.RunE(cmd, args) + v.VerifyCall() + if err != nil { + t.Errorf("unexpected error: %v", err.Error()) + } +} + +func TestSetAnnotationNoValue(t *testing.T) { + fSys := filesys.MakeFsInMemory() + testutils_test.WriteTestKustomization(fSys) + v := valtest_test.MakeHappyMapValidator(t) + cmd := newCmdSetAnnotation(fSys, v.Validator) + args := []string{"no,value:"} + err := cmd.RunE(cmd, args) + v.VerifyCall() + if err == nil { + t.Errorf("expected an error") + t.FailNow() + } + if err.Error() != invalidAnnotationKey { + t.Errorf("incorrect error: %v", err.Error()) + } +} + +func TestSetAnnotationMultipleArgs(t *testing.T) { + fSys := filesys.MakeFsInMemory() + testutils_test.WriteTestKustomization(fSys) + v := valtest_test.MakeHappyMapValidator(t) + cmd := newCmdSetAnnotation(fSys, v.Validator) + args := []string{"this:input", "has:spaces"} + err := cmd.RunE(cmd, args) + v.VerifyCall() + if err != nil { + t.Errorf("unexpected error: %v", err.Error()) + } +} + +func TestSetAnnotationExisting(t *testing.T) { + fSys := filesys.MakeFsInMemory() + testutils_test.WriteTestKustomization(fSys) + v := valtest_test.MakeHappyMapValidator(t) + cmd := newCmdSetAnnotation(fSys, v.Validator) + args := []string{"key:foo"} + err := cmd.RunE(cmd, args) + v.VerifyCall() + if err != nil { + t.Errorf("unexpected error: %v", err.Error()) + } + v = valtest_test.MakeHappyMapValidator(t) + cmd = newCmdSetAnnotation(fSys, v.Validator) + err = cmd.RunE(cmd, args) + v.VerifyCall() + if err != nil { + t.Errorf("unexpected error: %v", err.Error()) + } +}