Skip to content

Commit 0f032c2

Browse files
committed
Allow for predicate based checking of image names
Update `DockerComposeConnectionDetailsFactory` to accept a `Predicate` based check to determine if the source should be accepted. The existing name based checks have also been improved to allow names outside of official docker images. The `ImageReference` and `ImageName` classes have been mainly copied from `org.springframework.boot.buildpack.platform.docker.type`. Closes gh-35154
1 parent 19221f0 commit 0f032c2

File tree

12 files changed

+800
-83
lines changed

12 files changed

+800
-83
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
* Copyright 2012-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.docker.compose.core;
18+
19+
import org.springframework.util.Assert;
20+
21+
/**
22+
* A Docker image name of the form {@literal "docker.io/library/ubuntu"}.
23+
*
24+
* @author Phillip Webb
25+
* @author Scott Frederick
26+
*/
27+
class ImageName {
28+
29+
private static final String DEFAULT_DOMAIN = "docker.io";
30+
31+
private static final String OFFICIAL_REPOSITORY_NAME = "library";
32+
33+
private static final String LEGACY_DOMAIN = "index.docker.io";
34+
35+
private final String domain;
36+
37+
private final String name;
38+
39+
private final String string;
40+
41+
ImageName(String domain, String path) {
42+
Assert.hasText(path, "Path must not be empty");
43+
this.domain = getDomainOrDefault(domain);
44+
this.name = getNameWithDefaultPath(this.domain, path);
45+
this.string = this.domain + "/" + this.name;
46+
}
47+
48+
/**
49+
* Return the domain for this image name.
50+
* @return the domain
51+
*/
52+
String getDomain() {
53+
return this.domain;
54+
}
55+
56+
/**
57+
* Return the name of this image.
58+
* @return the image name
59+
*/
60+
String getName() {
61+
return this.name;
62+
}
63+
64+
@Override
65+
public boolean equals(Object obj) {
66+
if (this == obj) {
67+
return true;
68+
}
69+
if (obj == null || getClass() != obj.getClass()) {
70+
return false;
71+
}
72+
ImageName other = (ImageName) obj;
73+
boolean result = true;
74+
result = result && this.domain.equals(other.domain);
75+
result = result && this.name.equals(other.name);
76+
return result;
77+
}
78+
79+
@Override
80+
public int hashCode() {
81+
final int prime = 31;
82+
int result = 1;
83+
result = prime * result + this.domain.hashCode();
84+
result = prime * result + this.name.hashCode();
85+
return result;
86+
}
87+
88+
@Override
89+
public String toString() {
90+
return this.string;
91+
}
92+
93+
private String getDomainOrDefault(String domain) {
94+
if (domain == null || LEGACY_DOMAIN.equals(domain)) {
95+
return DEFAULT_DOMAIN;
96+
}
97+
return domain;
98+
}
99+
100+
private String getNameWithDefaultPath(String domain, String name) {
101+
if (DEFAULT_DOMAIN.equals(domain) && !name.contains("/")) {
102+
return OFFICIAL_REPOSITORY_NAME + "/" + name;
103+
}
104+
return name;
105+
}
106+
107+
static String parseDomain(String value) {
108+
int firstSlash = value.indexOf('/');
109+
String candidate = (firstSlash != -1) ? value.substring(0, firstSlash) : null;
110+
if (candidate != null && Regex.DOMAIN.matcher(candidate).matches()) {
111+
return candidate;
112+
}
113+
return null;
114+
}
115+
116+
static ImageName of(String value) {
117+
Assert.hasText(value, "Value must not be empty");
118+
String domain = parseDomain(value);
119+
String path = (domain != null) ? value.substring(domain.length() + 1) : value;
120+
Assert.isTrue(Regex.PATH.matcher(path).matches(),
121+
() -> "Unable to parse name \"" + value + "\". "
122+
+ "Image name must be in the form '[domainHost:port/][path/]name', "
123+
+ "with 'path' and 'name' containing only [a-z0-9][.][_][-]");
124+
return new ImageName(domain, path);
125+
}
126+
127+
}
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,31 +16,68 @@
1616

1717
package org.springframework.boot.docker.compose.core;
1818

19+
import java.util.regex.Matcher;
20+
21+
import org.springframework.util.Assert;
22+
import org.springframework.util.ObjectUtils;
23+
1924
/**
20-
* A docker image reference of form
21-
* {@code [<registry>/][<project>/]<image>[:<tag>|@<digest>]}.
25+
* A reference to a Docker image of the form {@code "imagename[:tag|@digest]"}.
2226
*
23-
* @author Moritz Halbritter
24-
* @author Andy Wilkinson
2527
* @author Phillip Webb
26-
* @since 3.1.0
27-
* @see <a href="https://docs.docker.com/compose/compose-file/#image">docker
28-
* documentation</a>
28+
* @author Scott Frederick
29+
* @since 2.3.0
2930
*/
3031
public final class ImageReference {
3132

32-
private final String reference;
33+
private final ImageName name;
34+
35+
private final String tag;
36+
37+
private final String digest;
38+
39+
private final String string;
40+
41+
private ImageReference(ImageName name, String tag, String digest) {
42+
Assert.notNull(name, "Name must not be null");
43+
this.name = name;
44+
this.tag = tag;
45+
this.digest = digest;
46+
this.string = buildString(name.toString(), tag, digest);
47+
}
48+
49+
/**
50+
* Return the domain for this image name.
51+
* @return the domain
52+
* @see ImageName#getDomain()
53+
*/
54+
public String getDomain() {
55+
return this.name.getDomain();
56+
}
57+
58+
/**
59+
* Return the name of this image.
60+
* @return the image name
61+
* @see ImageName#getName()
62+
*/
63+
public String getName() {
64+
return this.name.getName();
65+
}
3366

34-
private final String imageName;
67+
/**
68+
* Return the tag from the reference or {@code null}.
69+
* @return the referenced tag
70+
*/
71+
public String getTag() {
72+
return this.tag;
73+
}
3574

36-
ImageReference(String reference) {
37-
this.reference = reference;
38-
int lastSlashIndex = reference.lastIndexOf('/');
39-
String imageTagDigest = (lastSlashIndex != -1) ? reference.substring(lastSlashIndex + 1) : reference;
40-
int digestIndex = imageTagDigest.indexOf('@');
41-
String imageTag = (digestIndex != -1) ? imageTagDigest.substring(0, digestIndex) : imageTagDigest;
42-
int colon = imageTag.indexOf(':');
43-
this.imageName = (colon != -1) ? imageTag.substring(0, colon) : imageTag;
75+
/**
76+
* Return the digest from the reference or {@code null}.
77+
* @return the referenced digest
78+
*/
79+
public String getDigest() {
80+
return this.digest;
4481
}
4582

4683
@Override
@@ -52,35 +89,84 @@ public boolean equals(Object obj) {
5289
return false;
5390
}
5491
ImageReference other = (ImageReference) obj;
55-
return this.reference.equals(other.reference);
92+
boolean result = true;
93+
result = result && this.name.equals(other.name);
94+
result = result && ObjectUtils.nullSafeEquals(this.tag, other.tag);
95+
result = result && ObjectUtils.nullSafeEquals(this.digest, other.digest);
96+
return result;
5697
}
5798

5899
@Override
59100
public int hashCode() {
60-
return this.reference.hashCode();
101+
final int prime = 31;
102+
int result = 1;
103+
result = prime * result + this.name.hashCode();
104+
result = prime * result + ObjectUtils.nullSafeHashCode(this.tag);
105+
result = prime * result + ObjectUtils.nullSafeHashCode(this.digest);
106+
return result;
61107
}
62108

63109
@Override
64110
public String toString() {
65-
return this.reference;
111+
return this.string;
66112
}
67113

68-
/**
69-
* Return the referenced image, excluding the registry or project. For example, a
70-
* reference of {@code my_private.registry:5000/redis:5} would return {@code redis}.
71-
* @return the referenced image
72-
*/
73-
public String getImageName() {
74-
return this.imageName;
114+
private String buildString(String name, String tag, String digest) {
115+
StringBuilder string = new StringBuilder(name);
116+
if (tag != null) {
117+
string.append(":").append(tag);
118+
}
119+
if (digest != null) {
120+
string.append("@").append(digest);
121+
}
122+
return string.toString();
75123
}
76124

77125
/**
78-
* Create an image reference from the given String value.
79-
* @param value the string used to create the reference
126+
* Create a new {@link ImageReference} from the given value. The following value forms
127+
* can be used:
128+
* <ul>
129+
* <li>{@code name} (maps to {@code docker.io/library/name})</li>
130+
* <li>{@code domain/name}</li>
131+
* <li>{@code domain:port/name}</li>
132+
* <li>{@code domain:port/name:tag}</li>
133+
* <li>{@code domain:port/name@digest}</li>
134+
* </ul>
135+
* @param value the value to parse
80136
* @return an {@link ImageReference} instance
81137
*/
82138
public static ImageReference of(String value) {
83-
return (value != null) ? new ImageReference(value) : null;
139+
Assert.hasText(value, "Value must not be null");
140+
String domain = ImageName.parseDomain(value);
141+
String path = (domain != null) ? value.substring(domain.length() + 1) : value;
142+
String digest = null;
143+
int digestSplit = path.indexOf("@");
144+
if (digestSplit != -1) {
145+
String remainder = path.substring(digestSplit + 1);
146+
Matcher matcher = Regex.DIGEST.matcher(remainder);
147+
if (matcher.find()) {
148+
digest = remainder.substring(0, matcher.end());
149+
remainder = remainder.substring(matcher.end());
150+
path = path.substring(0, digestSplit) + remainder;
151+
}
152+
}
153+
String tag = null;
154+
int tagSplit = path.lastIndexOf(":");
155+
if (tagSplit != -1) {
156+
String remainder = path.substring(tagSplit + 1);
157+
Matcher matcher = Regex.TAG.matcher(remainder);
158+
if (matcher.find()) {
159+
tag = remainder.substring(0, matcher.end());
160+
remainder = remainder.substring(matcher.end());
161+
path = path.substring(0, tagSplit) + remainder;
162+
}
163+
}
164+
Assert.isTrue(Regex.PATH.matcher(path).matches(),
165+
() -> "Unable to parse image reference \"" + value + "\". "
166+
+ "Image reference must be in the form '[domainHost:port/][path/]name[:tag][@digest]', "
167+
+ "with 'path' and 'name' containing only [a-z0-9][.][_][-]");
168+
ImageName name = new ImageName(domain, path);
169+
return new ImageReference(name, tag, digest);
84170
}
85171

86172
}

0 commit comments

Comments
 (0)