Skip to content

Commit

Permalink
Merge pull request #93 from viceice/fix/valid-image-name
Browse files Browse the repository at this point in the history
[JENKINS-67572] Allow docker digest in image names
  • Loading branch information
rsandell authored Jan 27, 2022
2 parents 1c1d54f + a2e7cc1 commit e23a91d
Show file tree
Hide file tree
Showing 2 changed files with 140 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 <code>null</code>
*/
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
Expand All @@ -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;
Expand All @@ -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));
}

/**
Expand All @@ -129,14 +144,79 @@ public static void checkUserAndRepo(@NonNull String userAndRepo) throws FormVali
}
}

/**
* A content digest specified by open container spec.
*
* @see <a href="https://docs.docker.com/registry/spec/api/#content-digests">Content Digests</a>
* <a href="https://github.com/opencontainers/image-spec/blob/v1.0.1/descriptor.md#digests">OCI Digests</a>
*/
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 <a href="https://docs.docker.com/registry/spec/api/#content-digests">Content Digests</a>
* <a href="https://github.com/opencontainers/image-spec/blob/v1.0.1/descriptor.md#digests">OCI Digests</a>
*/
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 <a href="https://docs.docker.com/registry/spec/api/#content-digests">Content Digests</a>
* <a href="https://github.com/opencontainers/image-spec/blob/v1.0.1/descriptor.md#digests">OCI Digests</a>
*/
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.
* A tag name may not start with a period or a dash and may contain a maximum of 128 characters.
*
* @see <a href="https://docs.docker.com/engine/reference/commandline/tag/">docker tag</a>
*/
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}");


/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}

};
}
Expand All @@ -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);
}
}

0 comments on commit e23a91d

Please sign in to comment.