Skip to content

Commit 8dab949

Browse files
authored
Fix image name parsing with tag and digest (#4406)
* Fix image name parsing with name and digest Image names may contain both tag name and digest. For example `nginx:1.21.5@sha256:7826426c9d8d310c62fc68bcd5e8dde70cb39d4fbbd30eda3b1bd03e35fbde29`. Kustomizations with image transforms will not match these image because the image parser assumes either a tag or digest, but not both. For a real life example of kuberenetes deployments that might need to perform these types of transforms is from the [tekton-pipelines](https://github.com/tektoncd/pipeline) project (see the release.yaml). * Return digest property from image name parser image.Split now returns 3 fields: name, tag, and digest. The tag and digest fields no longer include their respective delimiters (`:` and `@`). * Fix merge file indentation * Refactor imagetag updater string builder
1 parent ff40460 commit 8dab949

File tree

4 files changed

+196
-31
lines changed

4 files changed

+196
-31
lines changed

api/filters/imagetag/imagetag_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -750,6 +750,96 @@ spec:
750750
},
751751
},
752752
},
753+
"image with tag and digest new name": {
754+
input: `
755+
apiVersion: example.com/v1
756+
kind: Foo
757+
metadata:
758+
name: instance
759+
spec:
760+
image: nginx:1.2.1@sha256:46d5b90a7f4e9996351ad893a26bcbd27216676ad4d5316088ce351fb2c2c3dd
761+
`,
762+
expectedOutput: `
763+
apiVersion: example.com/v1
764+
kind: Foo
765+
metadata:
766+
name: instance
767+
spec:
768+
image: apache:1.2.1@sha256:46d5b90a7f4e9996351ad893a26bcbd27216676ad4d5316088ce351fb2c2c3dd
769+
`,
770+
filter: Filter{
771+
ImageTag: types.Image{
772+
Name: "nginx",
773+
NewName: "apache",
774+
},
775+
},
776+
fsSlice: []types.FieldSpec{
777+
{
778+
Path: "spec/image",
779+
},
780+
},
781+
},
782+
"image with tag and digest new name new tag": {
783+
input: `
784+
apiVersion: example.com/v1
785+
kind: Foo
786+
metadata:
787+
name: instance
788+
spec:
789+
image: nginx:1.2.1@sha256:46d5b90a7f4e9996351ad893a26bcbd27216676ad4d5316088ce351fb2c2c3dd
790+
`,
791+
expectedOutput: `
792+
apiVersion: example.com/v1
793+
kind: Foo
794+
metadata:
795+
name: instance
796+
spec:
797+
image: apache:1.3.0
798+
`,
799+
filter: Filter{
800+
ImageTag: types.Image{
801+
Name: "nginx",
802+
NewName: "apache",
803+
NewTag: "1.3.0",
804+
},
805+
},
806+
fsSlice: []types.FieldSpec{
807+
{
808+
Path: "spec/image",
809+
},
810+
},
811+
},
812+
"image with tag and digest new name new tag and digest": {
813+
input: `
814+
apiVersion: example.com/v1
815+
kind: Foo
816+
metadata:
817+
name: instance
818+
spec:
819+
image: nginx:1.2.1@sha256:46d5b90a7f4e9996351ad893a26bcbd27216676ad4d5316088ce351fb2c2c3dd
820+
`,
821+
expectedOutput: `
822+
apiVersion: example.com/v1
823+
kind: Foo
824+
metadata:
825+
name: instance
826+
spec:
827+
image: apache:1.3.0@sha256:xyz
828+
`,
829+
filter: Filter{
830+
ImageTag: types.Image{
831+
Name: "nginx",
832+
NewName: "apache",
833+
NewTag: "1.3.0",
834+
Digest: "sha256:xyz",
835+
},
836+
},
837+
fsSlice: []types.FieldSpec{
838+
{
839+
Path: "spec/image",
840+
},
841+
},
842+
},
753843
}
754844

755845
for tn, tc := range testCases {

api/filters/imagetag/updater.go

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,32 @@ func (u imageTagUpdater) SetImageValue(rn *yaml.RNode) error {
3030
return nil
3131
}
3232

33-
name, tag := image.Split(value)
33+
name, tag, digest := image.Split(value)
3434
if u.ImageTag.NewName != "" {
3535
name = u.ImageTag.NewName
3636
}
37-
if u.ImageTag.NewTag != "" {
38-
tag = ":" + u.ImageTag.NewTag
37+
38+
// overriding tag or digest will replace both original tag and digest values
39+
if u.ImageTag.NewTag != "" && u.ImageTag.Digest != "" {
40+
tag = u.ImageTag.NewTag
41+
digest = u.ImageTag.Digest
42+
} else if u.ImageTag.NewTag != "" {
43+
tag = u.ImageTag.NewTag
44+
digest = ""
45+
} else if u.ImageTag.Digest != "" {
46+
tag = ""
47+
digest = u.ImageTag.Digest
48+
}
49+
50+
// build final image name
51+
if tag != "" {
52+
name += ":" + tag
3953
}
40-
if u.ImageTag.Digest != "" {
41-
tag = "@" + u.ImageTag.Digest
54+
if digest != "" {
55+
name += "@" + digest
4256
}
4357

44-
return u.trackableSetter.SetScalar(name + tag)(rn)
58+
return u.trackableSetter.SetScalar(name)(rn)
4559
}
4660

4761
func (u imageTagUpdater) Filter(rn *yaml.RNode) (*yaml.RNode, error) {

api/image/image.go

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,37 +14,53 @@ func IsImageMatched(s, t string) bool {
1414
// Tag values are limited to [a-zA-Z0-9_.{}-].
1515
// Some tools like Bazel rules_k8s allow tag patterns with {} characters.
1616
// More info: https://github.com/bazelbuild/rules_k8s/pull/423
17-
pattern, _ := regexp.Compile("^" + t + "(@sha256)?(:[a-zA-Z0-9_.{}-]*)?$")
17+
pattern, _ := regexp.Compile("^" + t + "(:[a-zA-Z0-9_.{}-]*)?(@sha256:[a-zA-Z0-9_.{}-]*)?$")
1818
return pattern.MatchString(s)
1919
}
2020

2121
// Split separates and returns the name and tag parts
2222
// from the image string using either colon `:` or at `@` separators.
23-
// Note that the returned tag keeps its separator.
24-
func Split(imageName string) (name string, tag string) {
23+
// image reference pattern: [[host[:port]/]component/]component[:tag][@digest]
24+
func Split(imageName string) (name string, tag string, digest string) {
2525
// check if image name contains a domain
2626
// if domain is present, ignore domain and check for `:`
27-
ic := -1
28-
if slashIndex := strings.Index(imageName, "/"); slashIndex < 0 {
29-
ic = strings.LastIndex(imageName, ":")
27+
searchName := imageName
28+
slashIndex := strings.Index(imageName, "/")
29+
if slashIndex > 0 {
30+
searchName = imageName[slashIndex:]
3031
} else {
31-
lastIc := strings.LastIndex(imageName[slashIndex:], ":")
32-
// set ic only if `:` is present
33-
if lastIc > 0 {
34-
ic = slashIndex + lastIc
35-
}
32+
slashIndex = 0
3633
}
37-
ia := strings.LastIndex(imageName, "@")
38-
if ic < 0 && ia < 0 {
39-
return imageName, ""
34+
35+
id := strings.Index(searchName, "@")
36+
ic := strings.Index(searchName, ":")
37+
38+
// no tag or digest
39+
if ic < 0 && id < 0 {
40+
return imageName, "", ""
41+
}
42+
43+
// digest only
44+
if id >= 0 && (id < ic || ic < 0) {
45+
id += slashIndex
46+
name = imageName[:id]
47+
digest = strings.TrimPrefix(imageName[id:], "@")
48+
return name, "", digest
4049
}
4150

42-
i := ic
43-
if ia > 0 {
44-
i = ia
51+
// tag and digest
52+
if id >= 0 && ic >= 0 {
53+
id += slashIndex
54+
ic += slashIndex
55+
name = imageName[:ic]
56+
tag = strings.TrimPrefix(imageName[ic:id], ":")
57+
digest = strings.TrimPrefix(imageName[id:], "@")
58+
return name, tag, digest
4559
}
4660

47-
name = imageName[:i]
48-
tag = imageName[i:]
49-
return
61+
// tag only
62+
ic += slashIndex
63+
name = imageName[:ic]
64+
tag = strings.TrimPrefix(imageName[ic:], ":")
65+
return name, tag, ""
5066
}

api/image/image_test.go

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,23 @@ func TestIsImageMatched(t *testing.T) {
2323
isMatched: true,
2424
},
2525
{
26-
testName: "name is match",
26+
testName: "name is match with tag",
2727
value: "nginx:12345",
2828
name: "nginx",
2929
isMatched: true,
3030
},
31+
{
32+
testName: "name is match with digest",
33+
value: "nginx@sha256:xyz",
34+
name: "nginx",
35+
isMatched: true,
36+
},
37+
{
38+
testName: "name is match with tag and digest",
39+
value: "nginx:12345@sha256:xyz",
40+
name: "nginx",
41+
isMatched: true,
42+
},
3143
{
3244
testName: "name is not a match",
3345
value: "apache:12345",
@@ -49,32 +61,65 @@ func TestSplit(t *testing.T) {
4961
value string
5062
name string
5163
tag string
64+
digest string
5265
}{
5366
{
5467
testName: "no tag",
5568
value: "nginx",
5669
name: "nginx",
5770
tag: "",
71+
digest: "",
5872
},
5973
{
6074
testName: "with tag",
6175
value: "nginx:1.2.3",
6276
name: "nginx",
63-
tag: ":1.2.3",
77+
tag: "1.2.3",
78+
digest: "",
6479
},
6580
{
6681
testName: "with digest",
67-
value: "nginx@12345",
82+
value: "nginx@sha256:12345",
83+
name: "nginx",
84+
tag: "",
85+
digest: "sha256:12345",
86+
},
87+
{
88+
testName: "with tag and digest",
89+
value: "nginx:1.2.3@sha256:12345",
6890
name: "nginx",
69-
tag: "@12345",
91+
tag: "1.2.3",
92+
digest: "sha256:12345",
93+
},
94+
{
95+
testName: "with domain",
96+
value: "docker.io/nginx:1.2.3",
97+
name: "docker.io/nginx",
98+
tag: "1.2.3",
99+
digest: "",
100+
},
101+
{
102+
testName: "with domain and port",
103+
value: "foo.com:443/nginx:1.2.3",
104+
name: "foo.com:443/nginx",
105+
tag: "1.2.3",
106+
digest: "",
107+
},
108+
{
109+
testName: "with domain, port, tag and digest",
110+
value: "foo.com:443/nginx:1.2.3@sha256:12345",
111+
name: "foo.com:443/nginx",
112+
tag: "1.2.3",
113+
digest: "sha256:12345",
70114
},
71115
}
72116

73117
for _, tc := range testCases {
74118
t.Run(tc.testName, func(t *testing.T) {
75-
name, tag := Split(tc.value)
119+
name, tag, digest := Split(tc.value)
76120
assert.Equal(t, tc.name, name)
77121
assert.Equal(t, tc.tag, tag)
122+
assert.Equal(t, tc.digest, digest)
78123
})
79124
}
80125
}

0 commit comments

Comments
 (0)