Skip to content

Commit a51085a

Browse files
committed
[grid] Simplifying docker image name parsing
Borrows most of the logic used to parse image names from the container registry repo. This should cover most (all?) cases for public and private registries. Fixes SeleniumHQ/docker-selenium#1275
1 parent 93573e3 commit a51085a

3 files changed

Lines changed: 105 additions & 74 deletions

File tree

java/server/src/org/openqa/selenium/docker/internal/Reference.java

Lines changed: 63 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -24,66 +24,86 @@
2424
import org.openqa.selenium.docker.DockerException;
2525
import org.openqa.selenium.internal.Require;
2626

27-
import java.util.Map;
2827
import java.util.Objects;
29-
import java.util.function.Function;
30-
import java.util.regex.Matcher;
31-
import java.util.regex.Pattern;
3228

3329
@Beta
3430
public class Reference {
35-
3631
private static final String DEFAULT_DOMAIN = "docker.io";
32+
private static final String LEGACY_DEFAULT_DOMAIN = "index.docker.io";
3733
private static final String DEFAULT_REPO = "library";
3834
private static final String DEFAULT_TAG = "latest";
3935

40-
// Capturing groups used in patterns below
41-
private static final String DOMAIN = "([\\w\\d-_.]+?(:(\\d+))?)";
42-
private static final String REPO = "([\\w\\d-_.]+?)";
43-
private static final String NAME = "([\\w\\d-_.]+?)";
44-
private static final String TAG = "([\\w\\d-_.]+?)";
45-
private static final String DIGEST = "(sha256:[A-Fa-f0-9]{64})";
46-
47-
// name -> {domain: "docker.io", repository: "repository", name: name, tag: "latest", digest: null}
48-
// name:tag -> {domain: "docker.io", repository: "repository", name: name, tag: tag, digest: null}
49-
// name@digest -> {domain: "docker.io", repository: "repository", name: name, tag: null, digest: digest}
50-
// repository/name -> {domain: "docker.io", repository: repository, name: name, tag: "latest", digest: null}
51-
// repository/name:tag -> {domain: "docker.io", repository: repository, name: name, tag: tag, digest: null}
52-
// repository/name@digest -> {domain: "docker.io", repository: repository, name: name, tag: null, digest: digest}
53-
// domain/repository/name:tag -> {domain: "domain", repository: repository, name: name, tag: tag, digest: null}
54-
// domain:port/repository/name@digest -> {domain: "domain:port", repository: repository, name: name, tag: tag, digest: null}
55-
private static final Map<Pattern, Function<Matcher, Reference>> PATTERNS = ImmutableMap.<Pattern, Function<Matcher, Reference>>builder()
56-
.put(Pattern.compile(TAG), m -> new Reference(DEFAULT_DOMAIN, DEFAULT_REPO, m.group(1), DEFAULT_TAG, null))
57-
.put(Pattern.compile(String.format("%s:%s", NAME, TAG)), m -> new Reference(DEFAULT_DOMAIN, DEFAULT_REPO, m.group(1), m.group(2), null))
58-
.put(Pattern.compile(String.format("%s/%s", REPO, NAME)), m -> new Reference(DEFAULT_DOMAIN, m.group(1), m.group(2), DEFAULT_TAG, null))
59-
.put(Pattern.compile(String.format("%s@%s", NAME, DIGEST)), m -> new Reference(DEFAULT_DOMAIN, DEFAULT_REPO, m.group(1), null, m.group(2)))
60-
.put(Pattern.compile(String.format("%s/%s:%s", REPO, NAME, TAG)), m -> new Reference(DEFAULT_DOMAIN, m.group(1), m.group(2), m.group(3), null))
61-
.put(Pattern.compile(String.format("%s/%s@%s", REPO, NAME, DIGEST)), m -> new Reference(DEFAULT_DOMAIN, m.group(1), m.group(2), null, m.group(3)))
62-
.put(Pattern.compile(String.format("%s/%s/%s", DOMAIN, REPO, NAME)), m -> new Reference(m.group(1), m.group(4), m.group(5), DEFAULT_TAG, null))
63-
.put(Pattern.compile(String.format("%s/%s/%s:%s", DOMAIN, REPO, NAME, TAG)), m -> new Reference(m.group(1), m.group(4), m.group(5), m.group(6), null))
64-
.build();
65-
6636
private final String domain;
67-
private final String repository;
6837
private final String name;
6938
private final String tag;
7039
private final String digest;
7140

7241
@VisibleForTesting
73-
Reference(String domain, String repository, String name, String tag, String digest) {
42+
Reference(String domain, String name, String tag, String digest) {
7443
this.domain = Require.nonNull("Domain", domain);
75-
this.repository = Require.nonNull("Repository", repository);
7644
this.name = Require.nonNull("Name", name);
7745
this.tag = tag;
7846
this.digest = digest;
7947
}
8048

81-
public String getDomain() {
82-
return domain;
49+
// Logic taken from https://github.com/distribution/distribution/blob/main/reference/normalize.go
50+
public static Reference parse(String input) {
51+
Require.nonNull("Reference to parse", input);
52+
53+
ImmutableMap<String, String> splitDockerDomain = splitDockerDomain(input);
54+
String domain = splitDockerDomain.get("domain");
55+
String remainder = splitDockerDomain.get("remainder");
56+
57+
String name;
58+
String digest = null;
59+
String tag = DEFAULT_TAG;
60+
61+
int digestSep = remainder.indexOf("@");
62+
int tagSep = remainder.indexOf(":");
63+
if (digestSep > -1 && tagSep > -1) {
64+
digest = remainder.substring(digestSep + 1);
65+
name = remainder.substring(0, digestSep);
66+
tag = null;
67+
} else if (tagSep > -1) {
68+
tag = remainder.substring(tagSep + 1);
69+
name = remainder.substring(0, tagSep);
70+
} else {
71+
name = remainder;
72+
}
73+
74+
if (!name.toLowerCase().equals(name)) {
75+
throw new DockerException(String.format(
76+
"Invalid reference format: repository name (%s) must be lowercase", name));
77+
}
78+
79+
return new Reference(domain, name, tag, digest);
8380
}
8481

85-
public String getRepository() {
86-
return repository;
82+
private static ImmutableMap<String, String> splitDockerDomain(String name) {
83+
String domain;
84+
String remainder;
85+
int domSep = name.indexOf("/");
86+
String possibleDomain = domSep == -1 ? "" : name.substring(0, domSep);
87+
if (domSep == -1 || (!possibleDomain.contains(".") && !possibleDomain.contains(":")
88+
&& !"localhost".equalsIgnoreCase(possibleDomain)
89+
&& possibleDomain.toLowerCase().equals(possibleDomain))) {
90+
remainder = name;
91+
domain = DEFAULT_DOMAIN;
92+
} else {
93+
domain = possibleDomain;
94+
remainder = name.substring(domSep + 1);
95+
}
96+
if (LEGACY_DEFAULT_DOMAIN.equals(domain)) {
97+
domain = DEFAULT_DOMAIN;
98+
}
99+
if (DEFAULT_DOMAIN.equals(domain) && !remainder.contains("/")) {
100+
remainder = String.format("%s/%s", DEFAULT_REPO, remainder);
101+
}
102+
return ImmutableMap.of("domain", domain, "remainder", remainder);
103+
}
104+
105+
public String getDomain() {
106+
return domain;
87107
}
88108

89109
public String getName() {
@@ -105,12 +125,12 @@ public String getFamiliarName() {
105125
familiar.append(domain).append("/");
106126
}
107127

108-
if (!DEFAULT_REPO.equals(repository)) {
109-
familiar.append(repository).append("/");
128+
if (name.contains(DEFAULT_REPO) && DEFAULT_DOMAIN.equals(domain)) {
129+
familiar.append(name.replace(DEFAULT_REPO + "/", ""));
130+
} else {
131+
familiar.append(name);
110132
}
111133

112-
familiar.append(name);
113-
114134
if (digest != null) {
115135
familiar.append("@").append(digest);
116136
} else if (tag != null) {
@@ -122,24 +142,10 @@ public String getFamiliarName() {
122142
return familiar.toString();
123143
}
124144

125-
public static Reference parse(String input) {
126-
Require.nonNull("Reference to parse", input);
127-
128-
for (Map.Entry<Pattern, Function<Matcher, Reference>> entry : PATTERNS.entrySet()) {
129-
Matcher matcher = entry.getKey().matcher(input);
130-
if (matcher.matches()) {
131-
return entry.getValue().apply(matcher);
132-
}
133-
}
134-
135-
throw new DockerException("Unable to parse: " + input);
136-
}
137-
138145
@Override
139146
public String toString() {
140147
return "Reference{" +
141148
"domain='" + domain + '\'' +
142-
", repository='" + repository + '\'' +
143149
", name='" + name + '\'' +
144150
", tag='" + tag + '\'' +
145151
", digest='" + digest + '\'' +
@@ -154,14 +160,13 @@ public boolean equals(Object o) {
154160

155161
Reference that = (Reference) o;
156162
return this.domain.equals(that.domain) &&
157-
this.repository.equals(that.repository) &&
158163
this.name.equals(that.name) &&
159164
Objects.equals(tag, that.tag) &&
160165
Objects.equals(digest, that.digest);
161166
}
162167

163168
@Override
164169
public int hashCode() {
165-
return Objects.hash(domain, repository, name, tag, digest);
170+
return Objects.hash(domain, name, tag, digest);
166171
}
167172
}

java/server/src/org/openqa/selenium/docker/v1_40/PullImage.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public void apply(Reference ref) {
4747

4848
LOG.info("Pulling " + ref);
4949

50-
String image = String.format("%s/%s/%s", ref.getDomain(), ref.getRepository(), ref.getName());
50+
String image = String.format("%s/%s", ref.getDomain(), ref.getName());
5151
HttpRequest req = new HttpRequest(POST, "/v1.40/images/create")
5252
.addHeader("Content-Type", JSON_UTF_8)
5353
.addHeader("Content-Length", "0")

java/server/test/org/openqa/selenium/docker/internal/ReferenceTest.java

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -31,26 +31,45 @@ public class ReferenceTest {
3131

3232
private final String input;
3333
private final Reference expected;
34+
private final String familiarName;
35+
36+
public ReferenceTest(String input, Reference expected, String familiarName) {
37+
this.input = input;
38+
this.expected = expected;
39+
this.familiarName = familiarName;
40+
}
3441

3542
@Parameterized.Parameters
3643
public static Collection<Object[]> data() {
44+
String sha256 = "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff";
3745
return Arrays.asList(new Object[][]{
3846
// input -> expected result
39-
{"imageName", new Reference("docker.io", "library", "imageName", "latest", null)},
40-
{"img:tg", new Reference("docker.io", "library", "img", "tg", null)},
41-
{"img@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", new Reference("docker.io", "library", "img", null, "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")},
42-
{"repo/img", new Reference("docker.io", "repo", "img", "latest",null)},
43-
{"repo/img:tag", new Reference("docker.io", "repo", "img", "tag",null)},
44-
{"repo/img@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", new Reference("docker.io", "repo", "img", null,"sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")},
45-
{"images.sample.io/repo/img", new Reference("images.sample.io", "repo", "img", "latest",null)},
46-
{"images.sample.io/repo/img:tag", new Reference("images.sample.io", "repo", "img", "tag",null)},
47-
// domain:port/repository/name@digest ->
48-
});
49-
}
50-
51-
public ReferenceTest(String input, Reference expected) {
52-
this.input = input;
53-
this.expected = expected;
47+
{"image", new Reference("docker.io", "library/image", "latest", null), "image:latest"},
48+
{"img:tg", new Reference("docker.io", "library/img", "tg", null), "img:tg"},
49+
{String.format("img@%s", sha256), new Reference("docker.io", "library/img", null, sha256),
50+
String.format("img@%s", sha256)},
51+
{"repo/img", new Reference("docker.io", "repo/img", "latest", null), "repo/img:latest"},
52+
{"repo/img:tag", new Reference("docker.io", "repo/img", "tag", null), "repo/img:tag"},
53+
{String.format("repo/img@%s", sha256), new Reference("docker.io", "repo/img", null, sha256),
54+
String.format("repo/img@%s", sha256)},
55+
{"images.sample.io/repo/img", new Reference("images.sample.io", "repo/img", "latest", null),
56+
"images.sample.io/repo/img:latest"},
57+
{"images.sample.io/repo/img:tag", new Reference("images.sample.io", "repo/img", "tag", null),
58+
"images.sample.io/repo/img:tag"},
59+
{"gcr.io/gouda/brie/cheddar/img:tag",
60+
new Reference("gcr.io", "gouda/brie/cheddar/img", "tag", null),
61+
"gcr.io/gouda/brie/cheddar/img:tag"},
62+
{String.format("gcr.io/gouda/brie/cheddar/img@%s", sha256),
63+
new Reference("gcr.io", "gouda/brie/cheddar/img", null, sha256),
64+
String.format("gcr.io/gouda/brie/cheddar/img@%s", sha256)},
65+
{"localhost:5000/gouda/brie/cheddar/img:tag",
66+
new Reference("localhost:5000", "gouda/brie/cheddar/img", "tag", null),
67+
"localhost:5000/gouda/brie/cheddar/img:tag"},
68+
{String.format("localhost:5000/gouda/brie/cheddar/img@%s", sha256),
69+
new Reference("localhost:5000", "gouda/brie/cheddar/img", null, sha256),
70+
String.format("localhost:5000/gouda/brie/cheddar/img@%s", sha256)},
71+
}
72+
);
5473
}
5574

5675
@Test
@@ -59,4 +78,11 @@ public void shouldEvaluateValidInputsAsReferences() {
5978
assertThat(seen).describedAs("%s -> %s", input, expected).isEqualTo(expected);
6079
}
6180

81+
@Test
82+
public void shouldEvaluateReferencesFamiliarName() {
83+
Reference seen = Reference.parse(input);
84+
assertThat(seen.getFamiliarName()).describedAs("%s -> %s", input, familiarName)
85+
.isEqualTo(familiarName);
86+
}
87+
6288
}

0 commit comments

Comments
 (0)