Skip to content

Commit e23a91d

Browse files
authored
Merge pull request #93 from viceice/fix/valid-image-name
[JENKINS-67572] Allow docker digest in image names
2 parents 1c1d54f + a2e7cc1 commit e23a91d

File tree

2 files changed

+140
-40
lines changed

2 files changed

+140
-40
lines changed

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

Lines changed: 95 additions & 15 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,12 +76,20 @@ 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) {
7980
int start = slashIdx > 0 ? slashIdx + 1 : 0;
80-
args[1] = userAndRepo.substring(start, tagIdx);
81-
if (tagIdx < userAndRepo.length() - 1) {
82-
args[2] = userAndRepo.substring(tagIdx + 1);
81+
String name = userAndRepo.substring(start, digestIdx);
82+
args[1] = name;
83+
tagIdx = name.lastIndexOf(':');
84+
if (tagIdx > 0) {
85+
args[1] = name.substring(0, tagIdx);
86+
args[2] = name.substring(tagIdx);
8387
}
88+
args[3] = userAndRepo.substring(digestIdx);
89+
} else if (tagIdx > 0) {
90+
int start = slashIdx > 0 ? slashIdx + 1 : 0;
91+
args[1] = userAndRepo.substring(start, tagIdx);
92+
args[2] = userAndRepo.substring(tagIdx);
8493
}
8594
}
8695
return args;
@@ -99,21 +108,27 @@ public static boolean skipped() {
99108
return FormValidation.ok();
100109
}
101110
final String[] args = splitUserAndRepo(userAndRepo);
102-
if (StringUtils.isBlank(args[0]) && StringUtils.isBlank(args[1]) && StringUtils.isBlank(args[2])) {
111+
if (StringUtils.isBlank(args[0]) && StringUtils.isBlank(args[1]) && StringUtils.isBlank(args[2])
112+
&& StringUtils.isBlank(args[3])) {
103113
return FormValidation.error("Bad imageName format: %s", userAndRepo);
104114
}
105115
final FormValidation name = validateName(args[1]);
106116
final FormValidation tag = validateTag(args[2]);
107-
if (name.kind == FormValidation.Kind.OK && tag.kind == FormValidation.Kind.OK) {
117+
final FormValidation digest = validateDigest(args[3]);
118+
if (name.kind == FormValidation.Kind.OK && tag.kind == FormValidation.Kind.OK
119+
&& digest.kind == FormValidation.Kind.OK) {
108120
return FormValidation.ok();
109121
}
110-
if (name.kind == FormValidation.Kind.OK) {
122+
if (name.kind != FormValidation.Kind.OK ) {
123+
return name;
124+
}
125+
if (tag.kind != FormValidation.Kind.OK) {
111126
return tag;
112127
}
113-
if (tag.kind == FormValidation.Kind.OK) {
114-
return name;
128+
if (digest.kind != FormValidation.Kind.OK) {
129+
return digest;
115130
}
116-
return FormValidation.aggregate(Arrays.asList(name, tag));
131+
return FormValidation.aggregate(Arrays.asList(name, tag, digest));
117132
}
118133

119134
/**
@@ -129,14 +144,79 @@ public static void checkUserAndRepo(@NonNull String userAndRepo) throws FormVali
129144
}
130145
}
131146

147+
/**
148+
* A content digest specified by open container spec.
149+
*
150+
* @see <a href="https://docs.docker.com/registry/spec/api/#content-digests">Content Digests</a>
151+
* <a href="https://github.com/opencontainers/image-spec/blob/v1.0.1/descriptor.md#digests">OCI Digests</a>
152+
*/
153+
public static final Pattern VALID_DIGEST = Pattern.compile("^@[a-z0-9]+([+._-][a-z0-9]+)*:[a-zA-Z0-9=_-]+$");
154+
155+
/**
156+
* A SHA-256 content digest specified by open container spec.
157+
*
158+
* @see <a href="https://docs.docker.com/registry/spec/api/#content-digests">Content Digests</a>
159+
* <a href="https://github.com/opencontainers/image-spec/blob/v1.0.1/descriptor.md#digests">OCI Digests</a>
160+
*/
161+
public static final Pattern VALID_DIGEST_SHA256 = Pattern.compile("^@sha256:[a-z0-9]{64}$");
162+
163+
/**
164+
* A SHA-512 content digest specified by open container spec.
165+
*
166+
* @see <a href="https://docs.docker.com/registry/spec/api/#content-digests">Content Digests</a>
167+
* <a href="https://github.com/opencontainers/image-spec/blob/v1.0.1/descriptor.md#digests">OCI Digests</a>
168+
*/
169+
public static final Pattern VALID_DIGEST_SHA512 = Pattern.compile("^@sha512:[a-z0-9]{128}$");
170+
171+
/**
172+
* Validates a digest is following the rules.
173+
*
174+
* If the tag is null or the empty string it is considered valid.
175+
*
176+
* @param digest the digest to validate.
177+
* @return the validation result
178+
* @see #VALID_DIGEST
179+
*/
180+
public static @NonNull FormValidation validateDigest(@CheckForNull String digest) {
181+
if (SKIP) {
182+
return FormValidation.ok();
183+
}
184+
if (StringUtils.isEmpty(digest)) {
185+
return FormValidation.ok();
186+
}
187+
if (digest.startsWith("@sha256")) {
188+
if (digest.length() != 72) {
189+
return FormValidation.error("Digest length != 72");
190+
}
191+
if (!VALID_DIGEST_SHA256.matcher(digest).matches()) {
192+
return FormValidation.error("Digest must follow the pattern '%s' for sha-256 algorithm", VALID_DIGEST_SHA256.pattern());
193+
}
194+
return FormValidation.ok();
195+
}
196+
if (digest.startsWith("@sha512")) {
197+
if (digest.length() != 136) {
198+
return FormValidation.error("Digest length != 136");
199+
}
200+
if (!VALID_DIGEST_SHA512.matcher(digest).matches()) {
201+
return FormValidation.error("Digest must follow the pattern '%s' for sha-512 algorithm", VALID_DIGEST_SHA512.pattern());
202+
}
203+
return FormValidation.ok();
204+
}
205+
if (VALID_DIGEST.matcher(digest).matches()) {
206+
return FormValidation.ok();
207+
} else {
208+
return FormValidation.error("Digest must follow the pattern '%s'", VALID_DIGEST.pattern());
209+
}
210+
}
211+
132212
/**
133213
* A tag name must be valid ASCII and may contain
134214
* lowercase and uppercase letters, digits, underscores, periods and dashes.
135215
* A tag name may not start with a period or a dash and may contain a maximum of 128 characters.
136216
*
137217
* @see <a href="https://docs.docker.com/engine/reference/commandline/tag/">docker tag</a>
138218
*/
139-
public static final Pattern VALID_TAG = Pattern.compile("^[a-zA-Z0-9_]([a-zA-Z0-9_.-]){0,127}");
219+
public static final Pattern VALID_TAG = Pattern.compile("^:[a-zA-Z0-9_]([a-zA-Z0-9_.-]){0,127}");
140220

141221

142222
/**

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

Lines changed: 45 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -15,30 +15,49 @@ 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-
{":tag", FormValidation.Kind.ERROR},
25-
{"name:tag", FormValidation.Kind.OK},
26-
{"name:.tag", FormValidation.Kind.ERROR},
27-
{"name:-tag", FormValidation.Kind.ERROR},
28-
{"name:.tag.", FormValidation.Kind.ERROR},
29-
{"name:tag.", FormValidation.Kind.OK},
30-
{"name:tag-", FormValidation.Kind.OK},
31-
{"_name:tag", FormValidation.Kind.ERROR},
32-
{"na___me:tag", FormValidation.Kind.ERROR},
33-
{"na__me:tag", FormValidation.Kind.OK},
34-
{"name:tag\necho hello", FormValidation.Kind.ERROR},
35-
{"name\necho hello:tag", FormValidation.Kind.ERROR},
36-
{"name:tag$BUILD_NUMBER", FormValidation.Kind.ERROR},
37-
{"name$BUILD_NUMBER:tag", FormValidation.Kind.ERROR},
38-
{null, FormValidation.Kind.ERROR},
39-
{"", FormValidation.Kind.ERROR},
40-
{":", FormValidation.Kind.ERROR},
41-
{" ", 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+
{"jenkinsci/workflow-demo@", FormValidation.Kind.ERROR},
23+
{"workflow-demo:latest", FormValidation.Kind.OK},
24+
{"workflow-demo", FormValidation.Kind.OK},
25+
{"workflow-demo:latest@sha256:56930391cf0e1be83108422bbef43001650cfb75f64b3429928f0c5986fdb750", FormValidation.Kind.OK},
26+
{"workflow-demo:latest@sha256:56930391cf0e1be83108422bbef43001650cfb75f64b", FormValidation.Kind.ERROR},
27+
{"workflow-demo@sha256:56930391cf0e1be83108422bbef43001650cfb75f64b3429928f0c5986fdb750", FormValidation.Kind.OK},
28+
{"workflow-demo@sha256:56930391cf0e1be83108422bbef43001650cfb75f64b3429928f0c5986fdB750", FormValidation.Kind.ERROR},
29+
{"workflow-demo:", FormValidation.Kind.ERROR},
30+
{"workflow-demo:latest@", FormValidation.Kind.ERROR},
31+
{"workflow-demo@", FormValidation.Kind.ERROR},
32+
{"jenkinsci/workflow-demo@sha256:56930391cf0e1be83108422bbef43001650cfb75f64b3429928f0c5986fdb750", FormValidation.Kind.OK},
33+
{"docker:80/jenkinsci/workflow-demo@sha256:56930391cf0e1be83108422bbef43001650cfb75f64b3429928f0c5986fdb750", FormValidation.Kind.OK},
34+
{"docker:80/jenkinsci/workflow-demo:latest@sha256:56930391cf0e1be83108422bbef43001650cfb75f64b3429928f0c5986fdb750", FormValidation.Kind.OK},
35+
{"docker:80/jenkinsci/workflow-demo:latest@sha1:0123456789abcdef", FormValidation.Kind.OK},
36+
{"docker:80/jenkinsci/workflow-demo:latest@sha1:", FormValidation.Kind.ERROR},
37+
{"docker:80/jenkinsci/workflow-demo@", FormValidation.Kind.ERROR},
38+
{"docker:80/jenkinsci/workflow-demo:latest@", FormValidation.Kind.ERROR},
39+
{":tag", FormValidation.Kind.ERROR},
40+
{"name:tag", FormValidation.Kind.OK},
41+
{"name:.tag", FormValidation.Kind.ERROR},
42+
{"name:-tag", FormValidation.Kind.ERROR},
43+
{"name:.tag.", FormValidation.Kind.ERROR},
44+
{"name:tag.", FormValidation.Kind.OK},
45+
{"name:tag-", FormValidation.Kind.OK},
46+
{"_name:tag", FormValidation.Kind.ERROR},
47+
{"na___me:tag", FormValidation.Kind.ERROR},
48+
{"na__me:tag", FormValidation.Kind.OK},
49+
{"name:tag\necho hello", FormValidation.Kind.ERROR},
50+
{"name\necho hello:tag", FormValidation.Kind.ERROR},
51+
{"name:tag$BUILD_NUMBER", FormValidation.Kind.ERROR},
52+
{"name$BUILD_NUMBER:tag", FormValidation.Kind.ERROR},
53+
{null, FormValidation.Kind.ERROR},
54+
{"", FormValidation.Kind.ERROR},
55+
{":", FormValidation.Kind.ERROR},
56+
{" ", FormValidation.Kind.ERROR},
57+
58+
{"a@sha512:56930391cf0e1be83108422bbef43001650cfb75f64b", FormValidation.Kind.ERROR},
59+
{"a@sha512:56930391cf0e1be83108422bbef43001650cfb75f64b3429928f0c5986fdb75056930391cf0e1be83108422bbef43001650cfb75f64b3429928f0c5986fdb750", FormValidation.Kind.OK},
60+
{"a@sha512:B6930391cf0e1be83108422bbef43001650cfb75f64b3429928f0c5986fdb75056930391cf0e1be83108422bbef43001650cfb75f64b3429928f0c5986fdb750", FormValidation.Kind.ERROR}
4261

4362
};
4463
}
@@ -53,6 +72,7 @@ public ImageNameValidatorTest(final String userAndRepo, final FormValidation.Kin
5372

5473
@Test
5574
public void test() {
56-
assertSame(expected, ImageNameValidator.validateUserAndRepo(userAndRepo).kind);
75+
FormValidation res = ImageNameValidator.validateUserAndRepo(userAndRepo);
76+
assertSame(userAndRepo + " : " + res.getMessage(), expected, res.kind);
5777
}
5878
}

0 commit comments

Comments
 (0)