From b22e43a4a756b4e355c0321306ef71b1e47e9897 Mon Sep 17 00:00:00 2001 From: Nestor Date: Fri, 18 Jan 2019 10:56:16 +0100 Subject: [PATCH] add set image command --- pkg/commands/edit/set/all.go | 1 + pkg/commands/edit/set/setimage.go | 186 +++++++++++++++ pkg/commands/edit/set/setimage_test.go | 225 ++++++++++++++++++ pkg/commands/edit/set/setimagetag.go | 4 +- pkg/commands/kustfile/kustomizationfile.go | 1 + .../kustfile/kustomizationfile_test.go | 1 + pkg/types/kustomization.go | 2 + 7 files changed, 419 insertions(+), 1 deletion(-) create mode 100644 pkg/commands/edit/set/setimage.go create mode 100644 pkg/commands/edit/set/setimage_test.go diff --git a/pkg/commands/edit/set/all.go b/pkg/commands/edit/set/all.go index 67321bbe8..ed78b06ff 100644 --- a/pkg/commands/edit/set/all.go +++ b/pkg/commands/edit/set/all.go @@ -43,6 +43,7 @@ func NewCmdSet(fsys fs.FileSystem, v ifc.Validator) *cobra.Command { newCmdSetNameSuffix(fsys), newCmdSetNamespace(fsys, v), newCmdSetImageTag(fsys), + newCmdSetImage(fsys), ) return c } diff --git a/pkg/commands/edit/set/setimage.go b/pkg/commands/edit/set/setimage.go new file mode 100644 index 000000000..cba76575d --- /dev/null +++ b/pkg/commands/edit/set/setimage.go @@ -0,0 +1,186 @@ +/* +Copyright 2019 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 set + +import ( + "errors" + "sort" + "strings" + + "github.com/spf13/cobra" + "sigs.k8s.io/kustomize/pkg/commands/kustfile" + "sigs.k8s.io/kustomize/pkg/fs" + "sigs.k8s.io/kustomize/pkg/image" +) + +type setImageOptions struct { + imageMap map[string]image.Image +} + +// errors + +var ( + errImageNoArgs = errors.New("no image specified") + errImageInvalidArgs = errors.New(`invalid format of image, use one of the following options: +- =: +- =@ +- : +- @`) +) + +const separator = "=" + +// newCmdSetImage sets the new names, tags or digests for images in the kustomization. +func newCmdSetImage(fsys fs.FileSystem) *cobra.Command { + var o setImageOptions + + cmd := &cobra.Command{ + Use: "image", + Short: `Sets images and their new names, new tags or digests in the kustomization file`, + Example: ` +The command + set image postgres=my-registry/postgres:latest nginx:1.8.0 my-app=my-registry/my-app alpine@sha256:24a0c4b4a4c0eb97a1aabb8e29f18e917d05abfe1b7a7c07857230879ce7d3d3 +will add + +image: +- name: postgres + newName: my-registry/postgres + newTag: latest +- name: nginx + newTag: 1.8.0 +- name: my-app + newName: my-registry/my-app +- digest: sha256:24a0c4b4a4c0eb97a1aabb8e29f18e917d05abfe1b7a7c07857230879ce7d3d3 + name: alpine + +to the kustomization file if it doesn't exist, +and overwrite the previous ones if the image name exists. + +`, + RunE: func(cmd *cobra.Command, args []string) error { + err := o.Validate(args) + if err != nil { + return err + } + return o.RunSetImage(fsys) + }, + } + return cmd +} + +type overwrite struct { + name string + digest string + tag string +} + +// Validate validates setImage command. +func (o *setImageOptions) Validate(args []string) error { + if len(args) == 0 { + return errImageNoArgs + } + + o.imageMap = make(map[string]image.Image) + + for _, arg := range args { + + img, err := parse(arg) + if err != nil { + return err + } + o.imageMap[img.Name] = img + } + return nil +} + +// RunSetImage runs setImage command. +func (o *setImageOptions) RunSetImage(fSys fs.FileSystem) error { + mf, err := kustfile.NewKustomizationFile(fSys) + if err != nil { + return err + } + m, err := mf.Read() + if err != nil { + return err + } + + // append only new images from ksutomize file + for _, im := range m.Images { + if _, ok := o.imageMap[im.Name]; ok { + continue + } + + o.imageMap[im.Name] = im + } + + var images []image.Image + for _, v := range o.imageMap { + images = append(images, v) + } + + sort.Slice(images, func(i, j int) bool { + return images[i].Name < images[j].Name + }) + + m.Images = images + return mf.Write(m) +} + +func parse(arg string) (image.Image, error) { + + // matches if there is an image name to overwrite + // =<:|@> + if s := strings.Split(arg, separator); len(s) == 2 { + p, err := parseOverwrite(s[1]) + return image.Image{ + Name: s[0], + NewName: p.name, + NewTag: p.tag, + Digest: p.digest, + }, err + } + + // matches only for overwrites + // <:|@> + p, err := parseOverwrite(arg) + return image.Image{ + Name: p.name, + NewTag: p.tag, + Digest: p.digest, + }, err +} + +// parseOverwrite parses the overwrite parameters +// from the given arg into a struct +func parseOverwrite(arg string) (overwrite, error) { + // match @ + if d := strings.Split(arg, "@"); len(d) > 1 { + return overwrite{ + name: d[0], + digest: d[1], + }, nil + } + + // match : + if t := pattern.FindStringSubmatch(arg); len(t) == 3 { + return overwrite{ + name: t[1], + tag: t[2], + }, nil + } + return overwrite{}, errImageInvalidArgs +} diff --git a/pkg/commands/edit/set/setimage_test.go b/pkg/commands/edit/set/setimage_test.go new file mode 100644 index 000000000..e851d4f56 --- /dev/null +++ b/pkg/commands/edit/set/setimage_test.go @@ -0,0 +1,225 @@ +/* +Copyright 2019 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 set + +import ( + "fmt" + "strings" + "testing" + + "sigs.k8s.io/kustomize/pkg/fs" +) + +func TestSetImage(t *testing.T) { + type given struct { + args []string + infileImages []string + } + type expected struct { + fileOutput []string + err error + } + testCases := []struct { + description string + given given + expected expected + }{ + { + given: given{ + args: []string{"image1=my-image1:my-tag"}, + }, + expected: expected{ + fileOutput: []string{ + "images:", + "- name: image1", + " newName: my-image1", + " newTag: my-tag", + }}, + }, + { + given: given{ + args: []string{"image1=my-image1@sha256:24a0c4b4a4c0eb97a1aabb8e29f18e917d05abfe1b7a7c07857230879ce7d3d3"}, + }, + expected: expected{ + fileOutput: []string{ + "images:", + "- digest: sha256:24a0c4b4a4c0eb97a1aabb8e29f18e917d05abfe1b7a7c07857230879ce7d3d3", + " name: image1", + " newName: my-image1", + }}, + }, + { + given: given{ + args: []string{"image1:my-tag"}, + }, + expected: expected{ + fileOutput: []string{ + "images:", + "- name: image1", + " newTag: my-tag", + }}, + }, + { + given: given{ + args: []string{"image1@sha256:24a0c4b4a4c0eb97a1aabb8e29f18e917d05abfe1b7a7c07857230879ce7d3d3"}, + }, + expected: expected{ + fileOutput: []string{ + "images:", + "- digest: sha256:24a0c4b4a4c0eb97a1aabb8e29f18e917d05abfe1b7a7c07857230879ce7d3d3", + " name: image1", + }}, + }, + { + given: given{ + args: []string{"ngnix=localhost:5000/my-project/ngnix:dev-01"}, + }, + expected: expected{ + fileOutput: []string{ + "images:", + "- name: ngnix", + " newName: localhost:5000/my-project/ngnix", + " newTag: dev-01", + }}, + }, + { + description: "override file", + given: given{ + args: []string{"image1=foo.bar.foo:8800/foo/image1:foo-bar"}, + infileImages: []string{ + "images:", + "- name: image1", + " newName: my-image1", + " newTag: my-tag", + "- name: image2", + " newName: my-image2", + " newTag: my-tag2", + }, + }, + expected: expected{ + fileOutput: []string{ + "images:", + "- name: image1", + " newName: foo.bar.foo:8800/foo/image1", + " newTag: foo-bar", + "- name: image2", + " newName: my-image2", + " newTag: my-tag2", + }}, + }, + { + description: "override new tag and new name with just a new tag", + given: given{ + args: []string{"image1:v1"}, + infileImages: []string{ + "images:", + "- name: image1", + " newTag: my-tag", + }, + }, + expected: expected{ + fileOutput: []string{ + "images:", + "- name: image1", + " newTag: v1", + }}, + }, + { + description: "multiple args with multiple overrides", + given: given{ + args: []string{ + "image1=foo.bar.foo:8800/foo/image1:foo-bar", + "image2=my-image2@sha256:24a0c4b4a4c0eb97a1aabb8e29f18e917d05abfe1b7a7c07857230879ce7d3d3", + "image3:my-tag", + }, + infileImages: []string{ + "images:", + "- name: image1", + " newName: my-image1", + " newTag: my-tag1", + "- name: image2", + " newName: my-image2", + " newTag: my-tag2", + "- name: image3", + " newTag: my-tag", + }, + }, + expected: expected{ + fileOutput: []string{ + "images:", + "- name: image1", + " newName: foo.bar.foo:8800/foo/image1", + " newTag: foo-bar", + "- digest: sha256:24a0c4b4a4c0eb97a1aabb8e29f18e917d05abfe1b7a7c07857230879ce7d3d3", + " name: image2", + " newName: my-image2", + "- name: image3", + " newTag: my-tag", + }}, + }, + { + description: "error: no args", + expected: expected{ + err: errImageNoArgs, + }, + }, + { + description: "error: invalid args", + given: given{ + args: []string{"bad", "args"}, + }, + expected: expected{ + err: errImageInvalidArgs, + }, + }, + } + + for _, tc := range testCases { + t.Run(fmt.Sprintf("%s%v", tc.description, tc.given.args), func(t *testing.T) { + // arrange + fakeFS := fs.MakeFakeFS() + cmd := newCmdSetImage(fakeFS) + + if len(tc.given.infileImages) > 0 { + // write file with infileImages + fakeFS.WriteTestKustomizationWith([]byte(strings.Join(tc.given.infileImages, "\n"))) + } else { + // writes default kustomization file + fakeFS.WriteTestKustomization() + } + + // act + err := cmd.RunE(cmd, tc.given.args) + + // assert + if err != tc.expected.err { + t.Errorf("Unexpedted error from set image command. Actual: %v\nExpected: %v", err, tc.expected.err) + t.FailNow() + } + + content, err := fakeFS.ReadTestKustomization() + if err != nil { + t.Errorf("unexpected read error: %v", err) + t.FailNow() + } + expectedStr := strings.Join(tc.expected.fileOutput, "\n") + if !strings.Contains(string(content), expectedStr) { + t.Errorf("unexpected image in kustomization file. \nActual:\n%s\nExpected:\n%s", content, expectedStr) + } + }) + } +} diff --git a/pkg/commands/edit/set/setimagetag.go b/pkg/commands/edit/set/setimagetag.go index 43eb4b139..41baedbd7 100644 --- a/pkg/commands/edit/set/setimagetag.go +++ b/pkg/commands/edit/set/setimagetag.go @@ -18,6 +18,7 @@ package set import ( "errors" + "log" "regexp" "sort" "strings" @@ -40,7 +41,7 @@ func newCmdSetImageTag(fsys fs.FileSystem) *cobra.Command { cmd := &cobra.Command{ Use: "imagetag", - Short: "Sets images and their new tags or digests in the kustomization file", + Short: "[*** DEPRECATED, use: kustomize edit set image ***] Sets images and their new tags or digests in the kustomization file", Example: ` The command set imagetag nginx:1.8.0 my-app:latest alpine@sha256:24a0c4b4a4c0eb97a1aabb8e29f18e917d05abfe1b7a7c07857230879ce7d3d3 @@ -58,6 +59,7 @@ to the kustomization file if it doesn't exist, and overwrite the previous ones if the image tag exists. `, RunE: func(cmd *cobra.Command, args []string) error { + log.Print("This is a deprecated command, it still works but will be removed in a further release. Use better kustomize edit set image") err := o.Validate(args) if err != nil { return err diff --git a/pkg/commands/kustfile/kustomizationfile.go b/pkg/commands/kustfile/kustomizationfile.go index abd9cfe0b..177ec604f 100644 --- a/pkg/commands/kustfile/kustomizationfile.go +++ b/pkg/commands/kustfile/kustomizationfile.go @@ -66,6 +66,7 @@ func determineFieldOrder() []string { "SecretGenerator", "GeneratorOptions", "Vars", + "Images", "ImageTags", "Configurations", } diff --git a/pkg/commands/kustfile/kustomizationfile_test.go b/pkg/commands/kustfile/kustomizationfile_test.go index 3738baf62..b1b1cc42a 100644 --- a/pkg/commands/kustfile/kustomizationfile_test.go +++ b/pkg/commands/kustfile/kustomizationfile_test.go @@ -44,6 +44,7 @@ func TestFieldOrder(t *testing.T) { "SecretGenerator", "GeneratorOptions", "Vars", + "Images", "ImageTags", "Configurations", } diff --git a/pkg/types/kustomization.go b/pkg/types/kustomization.go index 73cb2a3e3..2b4f58963 100644 --- a/pkg/types/kustomization.go +++ b/pkg/types/kustomization.go @@ -157,6 +157,8 @@ func (k *Kustomization) DealWithDeprecatedFields() { if len(k.ImageTags) > 0 { // Transform `ImageTag` to `Image` // for backwards compatibility + // images are appended first to keep + // higher precedence k.Images = image.Append( k.Images, k.ImageTags...) }