1/*
2Copyright 2019 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 set
18
19import (
20	"errors"
21	"regexp"
22	"sort"
23	"strings"
24
25	"github.com/spf13/cobra"
26	"sigs.k8s.io/kustomize/pkg/commands/kustfile"
27	"sigs.k8s.io/kustomize/pkg/fs"
28	"sigs.k8s.io/kustomize/pkg/image"
29)
30
31type setImageOptions struct {
32	imageMap map[string]image.Image
33}
34
35var pattern = regexp.MustCompile("^(.*):([a-zA-Z0-9._-]*)$")
36
37// errors
38
39var (
40	errImageNoArgs      = errors.New("no image specified")
41	errImageInvalidArgs = errors.New(`invalid format of image, use one of the following options:
42- <image>=<newimage>:<newtag>
43- <image>=<newimage>@<newtag>
44- <image>=<newimage>
45- <image>:<newtag>
46- <image>@<digest>`)
47)
48
49const separator = "="
50
51// newCmdSetImage sets the new names, tags or digests for images in the kustomization.
52func newCmdSetImage(fsys fs.FileSystem) *cobra.Command {
53	var o setImageOptions
54
55	cmd := &cobra.Command{
56		Use:   "image",
57		Short: `Sets images and their new names, new tags or digests in the kustomization file`,
58		Example: `
59The command
60  set image postgres=eu.gcr.io/my-project/postgres:latest my-app=my-registry/my-app@sha256:24a0c4b4a4c0eb97a1aabb8e29f18e917d05abfe1b7a7c07857230879ce7d3d3
61will add
62
63image:
64- name: postgres
65  newName: eu.gcr.io/my-project/postgres
66  newTag: latest
67- digest: sha256:24a0c4b4a4c0eb97a1aabb8e29f18e917d05abfe1b7a7c07857230879ce7d3d3
68  name: my-app
69  newName: my-registry/my-app
70
71to the kustomization file if it doesn't exist,
72and overwrite the previous ones if the image name exists.
73
74The command
75  set image node:8.15.0 mysql=mariadb alpine@sha256:24a0c4b4a4c0eb97a1aabb8e29f18e917d05abfe1b7a7c07857230879ce7d3d3
76will add
77
78image:
79- name: node
80  newTag: 8.15.0
81- name: mysql
82  newName: mariadb
83- digest: sha256:24a0c4b4a4c0eb97a1aabb8e29f18e917d05abfe1b7a7c07857230879ce7d3d3
84  name: alpine
85
86to the kustomization file if it doesn't exist,
87and overwrite the previous ones if the image name exists.
88`,
89		RunE: func(cmd *cobra.Command, args []string) error {
90			err := o.Validate(args)
91			if err != nil {
92				return err
93			}
94			return o.RunSetImage(fsys)
95		},
96	}
97	return cmd
98}
99
100type overwrite struct {
101	name   string
102	digest string
103	tag    string
104}
105
106// Validate validates setImage command.
107func (o *setImageOptions) Validate(args []string) error {
108	if len(args) == 0 {
109		return errImageNoArgs
110	}
111
112	o.imageMap = make(map[string]image.Image)
113
114	for _, arg := range args {
115
116		img, err := parse(arg)
117		if err != nil {
118			return err
119		}
120		o.imageMap[img.Name] = img
121	}
122	return nil
123}
124
125// RunSetImage runs setImage command.
126func (o *setImageOptions) RunSetImage(fSys fs.FileSystem) error {
127	mf, err := kustfile.NewKustomizationFile(fSys)
128	if err != nil {
129		return err
130	}
131	m, err := mf.Read()
132	if err != nil {
133		return err
134	}
135
136	// append only new images from ksutomize file
137	for _, im := range m.Images {
138		if _, ok := o.imageMap[im.Name]; ok {
139			continue
140		}
141
142		o.imageMap[im.Name] = im
143	}
144
145	var images []image.Image
146	for _, v := range o.imageMap {
147		images = append(images, v)
148	}
149
150	sort.Slice(images, func(i, j int) bool {
151		return images[i].Name < images[j].Name
152	})
153
154	m.Images = images
155	return mf.Write(m)
156}
157
158func parse(arg string) (image.Image, error) {
159
160	// matches if there is an image name to overwrite
161	// <image>=<new-image><:|@><new-tag>
162	if s := strings.Split(arg, separator); len(s) == 2 {
163		p, err := parseOverwrite(s[1])
164		return image.Image{
165			Name:    s[0],
166			NewName: p.name,
167			NewTag:  p.tag,
168			Digest:  p.digest,
169		}, err
170	}
171
172	// matches only for <tag|digest> overwrites
173	// <image><:|@><new-tag>
174	p, err := parseOverwrite(arg)
175	return image.Image{
176		Name:   p.name,
177		NewTag: p.tag,
178		Digest: p.digest,
179	}, err
180}
181
182// parseOverwrite parses the overwrite parameters
183// from the given arg into a struct
184func parseOverwrite(arg string) (overwrite, error) {
185	// match <image>@<digest>
186	if d := strings.Split(arg, "@"); len(d) > 1 {
187		return overwrite{
188			name:   d[0],
189			digest: d[1],
190		}, nil
191	}
192
193	// match <image>:<tag>
194	if t := pattern.FindStringSubmatch(arg); len(t) == 3 {
195		return overwrite{
196			name: t[1],
197			tag:  t[2],
198		}, nil
199	}
200	return overwrite{}, errImageInvalidArgs
201}
202