diff --git a/src/main/java/org/jenkinsci/plugins/docker/commons/credentials/ImageNameValidator.java b/src/main/java/org/jenkinsci/plugins/docker/commons/credentials/ImageNameValidator.java
index 993c253..634066f 100644
--- a/src/main/java/org/jenkinsci/plugins/docker/commons/credentials/ImageNameValidator.java
+++ b/src/main/java/org/jenkinsci/plugins/docker/commons/credentials/ImageNameValidator.java
@@ -50,21 +50,22 @@ public static boolean skipped() {
}
/**
- * Splits a repository id namespace/name into it's three components (repo/namespace[/*],name,tag)
+ * Splits a repository id namespace/name into it's four components (repo/namespace[/*],name,tag, digest)
*
* @param userAndRepo the repository ID namespace/name (ie. "jenkinsci/workflow-demo:latest").
* The namespace can have more than one path element.
- * @return an array where position 0 is the namespace, 1 is the name and 2 is the tag.
+ * @return an array where position 0 is the namespace, 1 is the name and 2 is the tag and 3 is the digest.
* Any position could be null
*/
public static @NonNull String[] splitUserAndRepo(@NonNull String userAndRepo) {
- String[] args = new String[3];
+ String[] args = new String[4];
if (StringUtils.isEmpty(userAndRepo)) {
return args;
}
int slashIdx = userAndRepo.lastIndexOf('/');
int tagIdx = userAndRepo.lastIndexOf(':');
- if (tagIdx == -1 && slashIdx == -1) {
+ int digestIdx = userAndRepo.lastIndexOf('@');
+ if (tagIdx == -1 && slashIdx == -1 && digestIdx == -1) {
args[1] = userAndRepo;
} else if (tagIdx < slashIdx) {
//something:port/something or something/something
@@ -75,12 +76,20 @@ public static boolean skipped() {
args[0] = userAndRepo.substring(0, slashIdx);
args[1] = userAndRepo.substring(slashIdx + 1);
}
- if (tagIdx > 0) {
+ if (digestIdx > 0) {
int start = slashIdx > 0 ? slashIdx + 1 : 0;
- args[1] = userAndRepo.substring(start, tagIdx);
- if (tagIdx < userAndRepo.length() - 1) {
- args[2] = userAndRepo.substring(tagIdx + 1);
+ String name = userAndRepo.substring(start, digestIdx);
+ args[1] = name;
+ tagIdx = name.lastIndexOf(':');
+ if (tagIdx > 0) {
+ args[1] = name.substring(0, tagIdx);
+ args[2] = name.substring(tagIdx);
}
+ args[3] = userAndRepo.substring(digestIdx);
+ } else if (tagIdx > 0) {
+ int start = slashIdx > 0 ? slashIdx + 1 : 0;
+ args[1] = userAndRepo.substring(start, tagIdx);
+ args[2] = userAndRepo.substring(tagIdx);
}
}
return args;
@@ -99,21 +108,27 @@ public static boolean skipped() {
return FormValidation.ok();
}
final String[] args = splitUserAndRepo(userAndRepo);
- if (StringUtils.isBlank(args[0]) && StringUtils.isBlank(args[1]) && StringUtils.isBlank(args[2])) {
+ if (StringUtils.isBlank(args[0]) && StringUtils.isBlank(args[1]) && StringUtils.isBlank(args[2])
+ && StringUtils.isBlank(args[3])) {
return FormValidation.error("Bad imageName format: %s", userAndRepo);
}
final FormValidation name = validateName(args[1]);
final FormValidation tag = validateTag(args[2]);
- if (name.kind == FormValidation.Kind.OK && tag.kind == FormValidation.Kind.OK) {
+ final FormValidation digest = validateDigest(args[3]);
+ if (name.kind == FormValidation.Kind.OK && tag.kind == FormValidation.Kind.OK
+ && digest.kind == FormValidation.Kind.OK) {
return FormValidation.ok();
}
- if (name.kind == FormValidation.Kind.OK) {
+ if (name.kind != FormValidation.Kind.OK ) {
+ return name;
+ }
+ if (tag.kind != FormValidation.Kind.OK) {
return tag;
}
- if (tag.kind == FormValidation.Kind.OK) {
- return name;
+ if (digest.kind != FormValidation.Kind.OK) {
+ return digest;
}
- return FormValidation.aggregate(Arrays.asList(name, tag));
+ return FormValidation.aggregate(Arrays.asList(name, tag, digest));
}
/**
@@ -129,6 +144,71 @@ public static void checkUserAndRepo(@NonNull String userAndRepo) throws FormVali
}
}
+ /**
+ * A content digest specified by open container spec.
+ *
+ * @see Content Digests
+ * OCI Digests
+ */
+ public static final Pattern VALID_DIGEST = Pattern.compile("^@[a-z0-9]+([+._-][a-z0-9]+)*:[a-zA-Z0-9=_-]+$");
+
+ /**
+ * A SHA-256 content digest specified by open container spec.
+ *
+ * @see Content Digests
+ * OCI Digests
+ */
+ public static final Pattern VALID_DIGEST_SHA256 = Pattern.compile("^@sha256:[a-z0-9]{64}$");
+
+ /**
+ * A SHA-512 content digest specified by open container spec.
+ *
+ * @see Content Digests
+ * OCI Digests
+ */
+ public static final Pattern VALID_DIGEST_SHA512 = Pattern.compile("^@sha512:[a-z0-9]{128}$");
+
+ /**
+ * Validates a digest is following the rules.
+ *
+ * If the tag is null or the empty string it is considered valid.
+ *
+ * @param digest the digest to validate.
+ * @return the validation result
+ * @see #VALID_DIGEST
+ */
+ public static @NonNull FormValidation validateDigest(@CheckForNull String digest) {
+ if (SKIP) {
+ return FormValidation.ok();
+ }
+ if (StringUtils.isEmpty(digest)) {
+ return FormValidation.ok();
+ }
+ if (digest.startsWith("@sha256")) {
+ if (digest.length() != 72) {
+ return FormValidation.error("Digest length != 72");
+ }
+ if (!VALID_DIGEST_SHA256.matcher(digest).matches()) {
+ return FormValidation.error("Digest must follow the pattern '%s' for sha-256 algorithm", VALID_DIGEST_SHA256.pattern());
+ }
+ return FormValidation.ok();
+ }
+ if (digest.startsWith("@sha512")) {
+ if (digest.length() != 136) {
+ return FormValidation.error("Digest length != 136");
+ }
+ if (!VALID_DIGEST_SHA512.matcher(digest).matches()) {
+ return FormValidation.error("Digest must follow the pattern '%s' for sha-512 algorithm", VALID_DIGEST_SHA512.pattern());
+ }
+ return FormValidation.ok();
+ }
+ if (VALID_DIGEST.matcher(digest).matches()) {
+ return FormValidation.ok();
+ } else {
+ return FormValidation.error("Digest must follow the pattern '%s'", VALID_DIGEST.pattern());
+ }
+ }
+
/**
* A tag name must be valid ASCII and may contain
* lowercase and uppercase letters, digits, underscores, periods and dashes.
@@ -136,7 +216,7 @@ public static void checkUserAndRepo(@NonNull String userAndRepo) throws FormVali
*
* @see docker tag
*/
- public static final Pattern VALID_TAG = Pattern.compile("^[a-zA-Z0-9_]([a-zA-Z0-9_.-]){0,127}");
+ public static final Pattern VALID_TAG = Pattern.compile("^:[a-zA-Z0-9_]([a-zA-Z0-9_.-]){0,127}");
/**
diff --git a/src/test/java/org/jenkinsci/plugins/docker/commons/credentials/ImageNameValidatorTest.java b/src/test/java/org/jenkinsci/plugins/docker/commons/credentials/ImageNameValidatorTest.java
index 1ae53ae..36f8304 100644
--- a/src/test/java/org/jenkinsci/plugins/docker/commons/credentials/ImageNameValidatorTest.java
+++ b/src/test/java/org/jenkinsci/plugins/docker/commons/credentials/ImageNameValidatorTest.java
@@ -15,30 +15,49 @@ public class ImageNameValidatorTest {
@Parameterized.Parameters(name = "{index}:{0}") public static Object[][] data(){
return new Object[][] {
- {"jenkinsci/workflow-demo", FormValidation.Kind.OK},
- {"docker:80/jenkinsci/workflow-demo", FormValidation.Kind.OK},
- {"jenkinsci/workflow-demo:latest", FormValidation.Kind.OK},
- {"docker:80/jenkinsci/workflow-demo:latest", FormValidation.Kind.OK},
- {"workflow-demo:latest", FormValidation.Kind.OK},
- {"workflow-demo", FormValidation.Kind.OK},
- {":tag", FormValidation.Kind.ERROR},
- {"name:tag", FormValidation.Kind.OK},
- {"name:.tag", FormValidation.Kind.ERROR},
- {"name:-tag", FormValidation.Kind.ERROR},
- {"name:.tag.", FormValidation.Kind.ERROR},
- {"name:tag.", FormValidation.Kind.OK},
- {"name:tag-", FormValidation.Kind.OK},
- {"_name:tag", FormValidation.Kind.ERROR},
- {"na___me:tag", FormValidation.Kind.ERROR},
- {"na__me:tag", FormValidation.Kind.OK},
- {"name:tag\necho hello", FormValidation.Kind.ERROR},
- {"name\necho hello:tag", FormValidation.Kind.ERROR},
- {"name:tag$BUILD_NUMBER", FormValidation.Kind.ERROR},
- {"name$BUILD_NUMBER:tag", FormValidation.Kind.ERROR},
- {null, FormValidation.Kind.ERROR},
- {"", FormValidation.Kind.ERROR},
- {":", FormValidation.Kind.ERROR},
- {" ", FormValidation.Kind.ERROR},
+ {"jenkinsci/workflow-demo", FormValidation.Kind.OK},
+ {"docker:80/jenkinsci/workflow-demo", FormValidation.Kind.OK},
+ {"jenkinsci/workflow-demo:latest", FormValidation.Kind.OK},
+ {"docker:80/jenkinsci/workflow-demo:latest", FormValidation.Kind.OK},
+ {"jenkinsci/workflow-demo@", FormValidation.Kind.ERROR},
+ {"workflow-demo:latest", FormValidation.Kind.OK},
+ {"workflow-demo", FormValidation.Kind.OK},
+ {"workflow-demo:latest@sha256:56930391cf0e1be83108422bbef43001650cfb75f64b3429928f0c5986fdb750", FormValidation.Kind.OK},
+ {"workflow-demo:latest@sha256:56930391cf0e1be83108422bbef43001650cfb75f64b", FormValidation.Kind.ERROR},
+ {"workflow-demo@sha256:56930391cf0e1be83108422bbef43001650cfb75f64b3429928f0c5986fdb750", FormValidation.Kind.OK},
+ {"workflow-demo@sha256:56930391cf0e1be83108422bbef43001650cfb75f64b3429928f0c5986fdB750", FormValidation.Kind.ERROR},
+ {"workflow-demo:", FormValidation.Kind.ERROR},
+ {"workflow-demo:latest@", FormValidation.Kind.ERROR},
+ {"workflow-demo@", FormValidation.Kind.ERROR},
+ {"jenkinsci/workflow-demo@sha256:56930391cf0e1be83108422bbef43001650cfb75f64b3429928f0c5986fdb750", FormValidation.Kind.OK},
+ {"docker:80/jenkinsci/workflow-demo@sha256:56930391cf0e1be83108422bbef43001650cfb75f64b3429928f0c5986fdb750", FormValidation.Kind.OK},
+ {"docker:80/jenkinsci/workflow-demo:latest@sha256:56930391cf0e1be83108422bbef43001650cfb75f64b3429928f0c5986fdb750", FormValidation.Kind.OK},
+ {"docker:80/jenkinsci/workflow-demo:latest@sha1:0123456789abcdef", FormValidation.Kind.OK},
+ {"docker:80/jenkinsci/workflow-demo:latest@sha1:", FormValidation.Kind.ERROR},
+ {"docker:80/jenkinsci/workflow-demo@", FormValidation.Kind.ERROR},
+ {"docker:80/jenkinsci/workflow-demo:latest@", FormValidation.Kind.ERROR},
+ {":tag", FormValidation.Kind.ERROR},
+ {"name:tag", FormValidation.Kind.OK},
+ {"name:.tag", FormValidation.Kind.ERROR},
+ {"name:-tag", FormValidation.Kind.ERROR},
+ {"name:.tag.", FormValidation.Kind.ERROR},
+ {"name:tag.", FormValidation.Kind.OK},
+ {"name:tag-", FormValidation.Kind.OK},
+ {"_name:tag", FormValidation.Kind.ERROR},
+ {"na___me:tag", FormValidation.Kind.ERROR},
+ {"na__me:tag", FormValidation.Kind.OK},
+ {"name:tag\necho hello", FormValidation.Kind.ERROR},
+ {"name\necho hello:tag", FormValidation.Kind.ERROR},
+ {"name:tag$BUILD_NUMBER", FormValidation.Kind.ERROR},
+ {"name$BUILD_NUMBER:tag", FormValidation.Kind.ERROR},
+ {null, FormValidation.Kind.ERROR},
+ {"", FormValidation.Kind.ERROR},
+ {":", FormValidation.Kind.ERROR},
+ {" ", FormValidation.Kind.ERROR},
+
+ {"a@sha512:56930391cf0e1be83108422bbef43001650cfb75f64b", FormValidation.Kind.ERROR},
+ {"a@sha512:56930391cf0e1be83108422bbef43001650cfb75f64b3429928f0c5986fdb75056930391cf0e1be83108422bbef43001650cfb75f64b3429928f0c5986fdb750", FormValidation.Kind.OK},
+ {"a@sha512:B6930391cf0e1be83108422bbef43001650cfb75f64b3429928f0c5986fdb75056930391cf0e1be83108422bbef43001650cfb75f64b3429928f0c5986fdb750", FormValidation.Kind.ERROR}
};
}
@@ -53,6 +72,7 @@ public ImageNameValidatorTest(final String userAndRepo, final FormValidation.Kin
@Test
public void test() {
- assertSame(expected, ImageNameValidator.validateUserAndRepo(userAndRepo).kind);
+ FormValidation res = ImageNameValidator.validateUserAndRepo(userAndRepo);
+ assertSame(userAndRepo + " : " + res.getMessage(), expected, res.kind);
}
}
\ No newline at end of file