Skip to content

Commit 95e4256

Browse files
committed
fix: support digests
1 parent d141c55 commit 95e4256

File tree

2 files changed

+96
-37
lines changed

2 files changed

+96
-37
lines changed

src/main/java/org/jenkinsci/plugins/docker/commons/credentials/ImageNameValidator.java

Lines changed: 66 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,21 +50,22 @@ public static boolean skipped() {
5050
}
5151

5252
/**
53-
* Splits a repository id namespace/name into it's three components (repo/namespace[/*],name,tag)
53+
* Splits a repository id namespace/name into it's four components (repo/namespace[/*],name,tag, digest)
5454
*
5555
* @param userAndRepo the repository ID namespace/name (ie. "jenkinsci/workflow-demo:latest").
5656
* The namespace can have more than one path element.
57-
* @return an array where position 0 is the namespace, 1 is the name and 2 is the tag.
57+
* @return an array where position 0 is the namespace, 1 is the name and 2 is the tag and 3 is the digest.
5858
* Any position could be <code>null</code>
5959
*/
6060
public static @NonNull String[] splitUserAndRepo(@NonNull String userAndRepo) {
61-
String[] args = new String[3];
61+
String[] args = new String[4];
6262
if (StringUtils.isEmpty(userAndRepo)) {
6363
return args;
6464
}
6565
int slashIdx = userAndRepo.lastIndexOf('/');
6666
int tagIdx = userAndRepo.lastIndexOf(':');
67-
if (tagIdx == -1 && slashIdx == -1) {
67+
int digestIdx = userAndRepo.lastIndexOf('@');
68+
if (tagIdx == -1 && slashIdx == -1 && digestIdx == -1) {
6869
args[1] = userAndRepo;
6970
} else if (tagIdx < slashIdx) {
7071
//something:port/something or something/something
@@ -75,7 +76,18 @@ public static boolean skipped() {
7576
args[0] = userAndRepo.substring(0, slashIdx);
7677
args[1] = userAndRepo.substring(slashIdx + 1);
7778
}
78-
if (tagIdx > 0) {
79+
if (digestIdx > 0) {
80+
int start = slashIdx > 0 ? slashIdx + 1 : 0;
81+
args[1] = userAndRepo.substring(start, digestIdx);
82+
tagIdx = args[1].lastIndexOf(':');
83+
if (tagIdx > 0 && tagIdx < args[1].length() - 1) {
84+
args[2] = args[1].substring(tagIdx + 1);
85+
args[1] = args[1].substring(0, tagIdx);
86+
}
87+
if (digestIdx < userAndRepo.length() - 1) {
88+
args[3] = userAndRepo.substring(digestIdx + 1);
89+
}
90+
} else if (tagIdx > 0) {
7991
int start = slashIdx > 0 ? slashIdx + 1 : 0;
8092
args[1] = userAndRepo.substring(start, tagIdx);
8193
if (tagIdx < userAndRepo.length() - 1) {
@@ -99,21 +111,27 @@ public static boolean skipped() {
99111
return FormValidation.ok();
100112
}
101113
final String[] args = splitUserAndRepo(userAndRepo);
102-
if (StringUtils.isBlank(args[0]) && StringUtils.isBlank(args[1]) && StringUtils.isBlank(args[2])) {
114+
if (StringUtils.isBlank(args[0]) && StringUtils.isBlank(args[1]) && StringUtils.isBlank(args[2])
115+
&& StringUtils.isBlank(args[3])) {
103116
return FormValidation.error("Bad imageName format: %s", userAndRepo);
104117
}
105118
final FormValidation name = validateName(args[1]);
106119
final FormValidation tag = validateTag(args[2]);
107-
if (name.kind == FormValidation.Kind.OK && tag.kind == FormValidation.Kind.OK) {
120+
final FormValidation digest = validateDigest(args[3]);
121+
if (name.kind == FormValidation.Kind.OK && tag.kind == FormValidation.Kind.OK
122+
&& digest.kind == FormValidation.Kind.OK) {
108123
return FormValidation.ok();
109124
}
110-
if (name.kind == FormValidation.Kind.OK) {
125+
if (name.kind != FormValidation.Kind.OK ) {
126+
return name;
127+
}
128+
if (tag.kind != FormValidation.Kind.OK) {
111129
return tag;
112130
}
113-
if (tag.kind == FormValidation.Kind.OK) {
114-
return name;
131+
if (digest.kind != FormValidation.Kind.OK) {
132+
return digest;
115133
}
116-
return FormValidation.aggregate(Arrays.asList(name, tag));
134+
return FormValidation.aggregate(Arrays.asList(name, tag, digest));
117135
}
118136

119137
/**
@@ -129,6 +147,43 @@ public static void checkUserAndRepo(@NonNull String userAndRepo) throws FormVali
129147
}
130148
}
131149

150+
/**
151+
* A digest starts with 'sha256:' and must be valid ASCII and may contain
152+
* lowercase and digits.
153+
* A digest contains 71 additional characters.
154+
*
155+
* @see <a href=
156+
* "https://docs.docker.com/engine/reference/commandline/images/#list-the-full-length-image-ids">docker
157+
* digests</a>
158+
*/
159+
public static final Pattern VALID_DIGEST = Pattern.compile("^sha256:([a-z0-9]){64}$");
160+
161+
/**
162+
* Validates a digest is following the rules.
163+
*
164+
* If the tag is null or the empty string it is considered valid.
165+
*
166+
* @param digest the digest to validate.
167+
* @return the validation result
168+
* @see #VALID_DIGEST
169+
*/
170+
public static @NonNull FormValidation validateDigest(@CheckForNull String digest) {
171+
if (SKIP) {
172+
return FormValidation.ok();
173+
}
174+
if (StringUtils.isEmpty(digest)) {
175+
return FormValidation.ok();
176+
}
177+
if (digest.length() != 71) {
178+
return FormValidation.error("Digest length != 71");
179+
}
180+
if (VALID_DIGEST.matcher(digest).matches()) {
181+
return FormValidation.ok();
182+
} else {
183+
return FormValidation.error("Digest must follow the pattern '%s'", VALID_DIGEST.pattern());
184+
}
185+
}
186+
132187
/**
133188
* A tag name must be valid ASCII and may contain
134189
* lowercase and uppercase letters, digits, underscores, periods and dashes.

src/test/java/org/jenkinsci/plugins/docker/commons/credentials/ImageNameValidatorTest.java

Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,32 +15,36 @@ public class ImageNameValidatorTest {
1515

1616
@Parameterized.Parameters(name = "{index}:{0}") public static Object[][] data(){
1717
return new Object[][] {
18-
{"jenkinsci/workflow-demo", FormValidation.Kind.OK},
19-
{"docker:80/jenkinsci/workflow-demo", FormValidation.Kind.OK},
20-
{"jenkinsci/workflow-demo:latest", FormValidation.Kind.OK},
21-
{"docker:80/jenkinsci/workflow-demo:latest", FormValidation.Kind.OK},
22-
{"workflow-demo:latest", FormValidation.Kind.OK},
23-
{"workflow-demo", FormValidation.Kind.OK},
24-
{"workflow-demo:latest@sha256:56930391cf0e1be83108422bbef43001650cfb75f64b3429928f0c5986fdb750", FormValidation.Kind.OK},
25-
{"workflow-demo@sha256:56930391cf0e1be83108422bbef43001650cfb75f64b3429928f0c5986fdb750", FormValidation.Kind.OK},
26-
{":tag", FormValidation.Kind.ERROR},
27-
{"name:tag", FormValidation.Kind.OK},
28-
{"name:.tag", FormValidation.Kind.ERROR},
29-
{"name:-tag", FormValidation.Kind.ERROR},
30-
{"name:.tag.", FormValidation.Kind.ERROR},
31-
{"name:tag.", FormValidation.Kind.OK},
32-
{"name:tag-", FormValidation.Kind.OK},
33-
{"_name:tag", FormValidation.Kind.ERROR},
34-
{"na___me:tag", FormValidation.Kind.ERROR},
35-
{"na__me:tag", FormValidation.Kind.OK},
36-
{"name:tag\necho hello", FormValidation.Kind.ERROR},
37-
{"name\necho hello:tag", FormValidation.Kind.ERROR},
38-
{"name:tag$BUILD_NUMBER", FormValidation.Kind.ERROR},
39-
{"name$BUILD_NUMBER:tag", FormValidation.Kind.ERROR},
40-
{null, FormValidation.Kind.ERROR},
41-
{"", FormValidation.Kind.ERROR},
42-
{":", FormValidation.Kind.ERROR},
43-
{" ", FormValidation.Kind.ERROR},
18+
{"jenkinsci/workflow-demo", FormValidation.Kind.OK},
19+
{"docker:80/jenkinsci/workflow-demo", FormValidation.Kind.OK},
20+
{"jenkinsci/workflow-demo:latest", FormValidation.Kind.OK},
21+
{"docker:80/jenkinsci/workflow-demo:latest", FormValidation.Kind.OK},
22+
{"workflow-demo:latest", FormValidation.Kind.OK},
23+
{"workflow-demo", FormValidation.Kind.OK},
24+
{"workflow-demo:latest@sha256:56930391cf0e1be83108422bbef43001650cfb75f64b3429928f0c5986fdb750", FormValidation.Kind.OK},
25+
{"workflow-demo:latest@sha256:56930391cf0e1be83108422bbef43001650cfb75f64b", FormValidation.Kind.ERROR},
26+
{"workflow-demo@sha256:56930391cf0e1be83108422bbef43001650cfb75f64b3429928f0c5986fdb750", FormValidation.Kind.OK},
27+
{"jenkinsci/workflow-demo@sha256:56930391cf0e1be83108422bbef43001650cfb75f64b3429928f0c5986fdb750", FormValidation.Kind.OK},
28+
{"docker:80/jenkinsci/workflow-demo@sha256:56930391cf0e1be83108422bbef43001650cfb75f64b3429928f0c5986fdb750", FormValidation.Kind.OK},
29+
{"docker:80/jenkinsci/workflow-demo:latest@sha256:56930391cf0e1be83108422bbef43001650cfb75f64b3429928f0c5986fdb750", FormValidation.Kind.OK},
30+
{":tag", FormValidation.Kind.ERROR},
31+
{"name:tag", FormValidation.Kind.OK},
32+
{"name:.tag", FormValidation.Kind.ERROR},
33+
{"name:-tag", FormValidation.Kind.ERROR},
34+
{"name:.tag.", FormValidation.Kind.ERROR},
35+
{"name:tag.", FormValidation.Kind.OK},
36+
{"name:tag-", FormValidation.Kind.OK},
37+
{"_name:tag", FormValidation.Kind.ERROR},
38+
{"na___me:tag", FormValidation.Kind.ERROR},
39+
{"na__me:tag", FormValidation.Kind.OK},
40+
{"name:tag\necho hello", FormValidation.Kind.ERROR},
41+
{"name\necho hello:tag", FormValidation.Kind.ERROR},
42+
{"name:tag$BUILD_NUMBER", FormValidation.Kind.ERROR},
43+
{"name$BUILD_NUMBER:tag", FormValidation.Kind.ERROR},
44+
{null, FormValidation.Kind.ERROR},
45+
{"", FormValidation.Kind.ERROR},
46+
{":", FormValidation.Kind.ERROR},
47+
{" ", FormValidation.Kind.ERROR},
4448

4549
};
4650
}

0 commit comments

Comments
 (0)