Allow for predicate based checking of image names

Update `DockerComposeConnectionDetailsFactory` to accept a `Predicate`
based check to determine if the source should be accepted.

The existing name based checks have also been improved to allow names
outside of official docker images. The `ImageReference` and `ImageName`
classes have been mainly copied from
`org.springframework.boot.buildpack.platform.docker.type`.

Closes gh-35154
This commit is contained in:
Phillip Webb 2023-04-24 16:05:03 -07:00
parent 19221f00f3
commit 0f032c290a
12 changed files with 802 additions and 85 deletions

View File

@ -0,0 +1,127 @@
/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.docker.compose.core;
import org.springframework.util.Assert;
/**
* A Docker image name of the form {@literal "docker.io/library/ubuntu"}.
*
* @author Phillip Webb
* @author Scott Frederick
*/
class ImageName {
private static final String DEFAULT_DOMAIN = "docker.io";
private static final String OFFICIAL_REPOSITORY_NAME = "library";
private static final String LEGACY_DOMAIN = "index.docker.io";
private final String domain;
private final String name;
private final String string;
ImageName(String domain, String path) {
Assert.hasText(path, "Path must not be empty");
this.domain = getDomainOrDefault(domain);
this.name = getNameWithDefaultPath(this.domain, path);
this.string = this.domain + "/" + this.name;
}
/**
* Return the domain for this image name.
* @return the domain
*/
String getDomain() {
return this.domain;
}
/**
* Return the name of this image.
* @return the image name
*/
String getName() {
return this.name;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
ImageName other = (ImageName) obj;
boolean result = true;
result = result && this.domain.equals(other.domain);
result = result && this.name.equals(other.name);
return result;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + this.domain.hashCode();
result = prime * result + this.name.hashCode();
return result;
}
@Override
public String toString() {
return this.string;
}
private String getDomainOrDefault(String domain) {
if (domain == null || LEGACY_DOMAIN.equals(domain)) {
return DEFAULT_DOMAIN;
}
return domain;
}
private String getNameWithDefaultPath(String domain, String name) {
if (DEFAULT_DOMAIN.equals(domain) && !name.contains("/")) {
return OFFICIAL_REPOSITORY_NAME + "/" + name;
}
return name;
}
static String parseDomain(String value) {
int firstSlash = value.indexOf('/');
String candidate = (firstSlash != -1) ? value.substring(0, firstSlash) : null;
if (candidate != null && Regex.DOMAIN.matcher(candidate).matches()) {
return candidate;
}
return null;
}
static ImageName of(String value) {
Assert.hasText(value, "Value must not be empty");
String domain = parseDomain(value);
String path = (domain != null) ? value.substring(domain.length() + 1) : value;
Assert.isTrue(Regex.PATH.matcher(path).matches(),
() -> "Unable to parse name \"" + value + "\". "
+ "Image name must be in the form '[domainHost:port/][path/]name', "
+ "with 'path' and 'name' containing only [a-z0-9][.][_][-]");
return new ImageName(domain, path);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -16,31 +16,68 @@
package org.springframework.boot.docker.compose.core;
import java.util.regex.Matcher;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/**
* A docker image reference of form
* {@code [<registry>/][<project>/]<image>[:<tag>|@<digest>]}.
* A reference to a Docker image of the form {@code "imagename[:tag|@digest]"}.
*
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
* @since 3.1.0
* @see <a href="https://docs.docker.com/compose/compose-file/#image">docker
* documentation</a>
* @author Scott Frederick
* @since 2.3.0
*/
public final class ImageReference {
private final String reference;
private final ImageName name;
private final String imageName;
private final String tag;
ImageReference(String reference) {
this.reference = reference;
int lastSlashIndex = reference.lastIndexOf('/');
String imageTagDigest = (lastSlashIndex != -1) ? reference.substring(lastSlashIndex + 1) : reference;
int digestIndex = imageTagDigest.indexOf('@');
String imageTag = (digestIndex != -1) ? imageTagDigest.substring(0, digestIndex) : imageTagDigest;
int colon = imageTag.indexOf(':');
this.imageName = (colon != -1) ? imageTag.substring(0, colon) : imageTag;
private final String digest;
private final String string;
private ImageReference(ImageName name, String tag, String digest) {
Assert.notNull(name, "Name must not be null");
this.name = name;
this.tag = tag;
this.digest = digest;
this.string = buildString(name.toString(), tag, digest);
}
/**
* Return the domain for this image name.
* @return the domain
* @see ImageName#getDomain()
*/
public String getDomain() {
return this.name.getDomain();
}
/**
* Return the name of this image.
* @return the image name
* @see ImageName#getName()
*/
public String getName() {
return this.name.getName();
}
/**
* Return the tag from the reference or {@code null}.
* @return the referenced tag
*/
public String getTag() {
return this.tag;
}
/**
* Return the digest from the reference or {@code null}.
* @return the referenced digest
*/
public String getDigest() {
return this.digest;
}
@Override
@ -52,35 +89,84 @@ public final class ImageReference {
return false;
}
ImageReference other = (ImageReference) obj;
return this.reference.equals(other.reference);
boolean result = true;
result = result && this.name.equals(other.name);
result = result && ObjectUtils.nullSafeEquals(this.tag, other.tag);
result = result && ObjectUtils.nullSafeEquals(this.digest, other.digest);
return result;
}
@Override
public int hashCode() {
return this.reference.hashCode();
final int prime = 31;
int result = 1;
result = prime * result + this.name.hashCode();
result = prime * result + ObjectUtils.nullSafeHashCode(this.tag);
result = prime * result + ObjectUtils.nullSafeHashCode(this.digest);
return result;
}
@Override
public String toString() {
return this.reference;
return this.string;
}
private String buildString(String name, String tag, String digest) {
StringBuilder string = new StringBuilder(name);
if (tag != null) {
string.append(":").append(tag);
}
if (digest != null) {
string.append("@").append(digest);
}
return string.toString();
}
/**
* Return the referenced image, excluding the registry or project. For example, a
* reference of {@code my_private.registry:5000/redis:5} would return {@code redis}.
* @return the referenced image
*/
public String getImageName() {
return this.imageName;
}
/**
* Create an image reference from the given String value.
* @param value the string used to create the reference
* Create a new {@link ImageReference} from the given value. The following value forms
* can be used:
* <ul>
* <li>{@code name} (maps to {@code docker.io/library/name})</li>
* <li>{@code domain/name}</li>
* <li>{@code domain:port/name}</li>
* <li>{@code domain:port/name:tag}</li>
* <li>{@code domain:port/name@digest}</li>
* </ul>
* @param value the value to parse
* @return an {@link ImageReference} instance
*/
public static ImageReference of(String value) {
return (value != null) ? new ImageReference(value) : null;
Assert.hasText(value, "Value must not be null");
String domain = ImageName.parseDomain(value);
String path = (domain != null) ? value.substring(domain.length() + 1) : value;
String digest = null;
int digestSplit = path.indexOf("@");
if (digestSplit != -1) {
String remainder = path.substring(digestSplit + 1);
Matcher matcher = Regex.DIGEST.matcher(remainder);
if (matcher.find()) {
digest = remainder.substring(0, matcher.end());
remainder = remainder.substring(matcher.end());
path = path.substring(0, digestSplit) + remainder;
}
}
String tag = null;
int tagSplit = path.lastIndexOf(":");
if (tagSplit != -1) {
String remainder = path.substring(tagSplit + 1);
Matcher matcher = Regex.TAG.matcher(remainder);
if (matcher.find()) {
tag = remainder.substring(0, matcher.end());
remainder = remainder.substring(matcher.end());
path = path.substring(0, tagSplit) + remainder;
}
}
Assert.isTrue(Regex.PATH.matcher(path).matches(),
() -> "Unable to parse image reference \"" + value + "\". "
+ "Image reference must be in the form '[domainHost:port/][path/]name[:tag][@digest]', "
+ "with 'path' and 'name' containing only [a-z0-9][.][_][-]");
ImageName name = new ImageName(domain, path);
return new ImageReference(name, tag, digest);
}
}

View File

@ -0,0 +1,121 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.docker.compose.core;
import java.util.regex.Pattern;
/**
* Regular Expressions for image names and references based on those found in the Docker
* codebase.
*
* @author Scott Frederick
* @author Phillip Webb
* @see <a href=
* "https://github.com/docker/distribution/blob/master/reference/reference.go">Docker
* grammar reference</a>
* @see <a href=
* "https://github.com/docker/distribution/blob/master/reference/regexp.go">Docker grammar
* implementation</a>
* @see <a href=
* "https://stackoverflow.com/questions/37861791/how-are-docker-image-names-parsed">How
* are Docker image names parsed?</a>
*/
final class Regex implements CharSequence {
static final Pattern DOMAIN;
static {
Regex component = Regex.oneOf("[a-zA-Z0-9]", "[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]");
Regex dotComponent = Regex.group("[.]", component);
Regex colonPort = Regex.of("[:][0-9]+");
Regex dottedDomain = Regex.group(component, dotComponent.oneOrMoreTimes());
Regex dottedDomainAndPort = Regex.group(component, dotComponent.oneOrMoreTimes(), colonPort);
Regex nameAndPort = Regex.group(component, colonPort);
DOMAIN = Regex.oneOf(dottedDomain, nameAndPort, dottedDomainAndPort, "localhost").compile();
}
private static final Regex PATH_COMPONENT;
static {
Regex segment = Regex.of("[a-z0-9]+");
Regex separator = Regex.group("[._]|__|[-]*");
Regex separatedSegment = Regex.group(separator, segment).oneOrMoreTimes();
PATH_COMPONENT = Regex.of(segment, Regex.group(separatedSegment).zeroOrOnce());
}
static final Pattern PATH;
static {
Regex component = PATH_COMPONENT;
Regex slashComponent = Regex.group("[/]", component);
Regex slashComponents = Regex.group(slashComponent.oneOrMoreTimes());
PATH = Regex.of(component, slashComponents.zeroOrOnce()).compile();
}
static final Pattern TAG = Regex.of("^[\\w][\\w.-]{0,127}").compile();
static final Pattern DIGEST = Regex.of("^[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[A-Fa-f0-9]]{32,}")
.compile();
private final String value;
private Regex(CharSequence value) {
this.value = value.toString();
}
private Regex oneOrMoreTimes() {
return new Regex(this.value + "+");
}
private Regex zeroOrOnce() {
return new Regex(this.value + "?");
}
Pattern compile() {
return Pattern.compile("^" + this.value + "$");
}
@Override
public int length() {
return this.value.length();
}
@Override
public char charAt(int index) {
return this.value.charAt(index);
}
@Override
public CharSequence subSequence(int start, int end) {
return this.value.subSequence(start, end);
}
@Override
public String toString() {
return this.value;
}
private static Regex of(CharSequence... expressions) {
return new Regex(String.join("", expressions));
}
private static Regex oneOf(CharSequence... expressions) {
return new Regex("(?:" + String.join("|", expressions) + ")");
}
private static Regex group(CharSequence... expressions) {
return new Regex("(?:" + String.join("", expressions) + ")");
}
}

View File

@ -0,0 +1,52 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.docker.compose.service.connection;
import java.util.function.Predicate;
import org.springframework.boot.docker.compose.core.ImageReference;
import org.springframework.boot.docker.compose.core.RunningService;
/**
* {@link Predicate} that matches against connection names.
*
* @author Phillip Webb
*/
class ConnectionNamePredicate implements Predicate<DockerComposeConnectionSource> {
private String required;
ConnectionNamePredicate(String required) {
this.required = asCanonicalName(required);
}
@Override
public boolean test(DockerComposeConnectionSource source) {
String actual = getActual(source.getRunningService());
return this.required.equals(actual);
}
private String getActual(RunningService service) {
String label = service.labels().get("org.springframework.boot.service-connection");
return (label != null) ? asCanonicalName(label) : service.image().getName();
}
private String asCanonicalName(String name) {
return ImageReference.of(name).getName();
}
}

View File

@ -17,6 +17,7 @@
package org.springframework.boot.docker.compose.service.connection;
import java.util.Arrays;
import java.util.function.Predicate;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory;
@ -40,7 +41,7 @@ import org.springframework.util.ObjectUtils;
public abstract class DockerComposeConnectionDetailsFactory<D extends ConnectionDetails>
implements ConnectionDetailsFactory<DockerComposeConnectionSource, D> {
private final String connectionName;
private final Predicate<DockerComposeConnectionSource> predicate;
private final String[] requiredClassNames;
@ -50,7 +51,17 @@ public abstract class DockerComposeConnectionDetailsFactory<D extends Connection
* @param requiredClassNames the names of classes that must be present
*/
protected DockerComposeConnectionDetailsFactory(String connectionName, String... requiredClassNames) {
this.connectionName = connectionName;
this(new ConnectionNamePredicate(connectionName), requiredClassNames);
}
/**
* Create a new {@link DockerComposeConnectionDetailsFactory} instance.
* @param predicate a predicate used to check when a service is accepted
* @param requiredClassNames the names of classes that must be present
*/
protected DockerComposeConnectionDetailsFactory(Predicate<DockerComposeConnectionSource> predicate,
String... requiredClassNames) {
this.predicate = predicate;
this.requiredClassNames = requiredClassNames;
}
@ -60,12 +71,7 @@ public abstract class DockerComposeConnectionDetailsFactory<D extends Connection
}
private boolean accept(DockerComposeConnectionSource source) {
return hasRequiredClasses() && this.connectionName.equals(getConnectionName(source.getRunningService()));
}
private String getConnectionName(RunningService service) {
String connectionName = service.labels().get("org.springframework.boot.service-connection");
return (connectionName != null) ? connectionName : service.image().getImageName();
return hasRequiredClasses() && this.predicate.test(source);
}
private boolean hasRequiredClasses() {

View File

@ -35,7 +35,8 @@ class ZipkinDockerComposeConnectionDetailsFactory
private static final int ZIPKIN_PORT = 9411;
ZipkinDockerComposeConnectionDetailsFactory() {
super("zipkin", "org.springframework.boot.actuate.autoconfigure.tracing.zipkin.ZipkinAutoConfiguration");
super("openzipkin/zipkin",
"org.springframework.boot.actuate.autoconfigure.tracing.zipkin.ZipkinAutoConfiguration");
}
@Override

View File

@ -133,7 +133,7 @@ class DefaultDockerComposeTests {
assertThat(runningServices).hasSize(1);
RunningService runningService = runningServices.get(0);
assertThat(runningService.name()).isEqualTo("name");
assertThat(runningService.image()).hasToString("redis");
assertThat(runningService.image()).hasToString("docker.io/library/redis");
assertThat(runningService.host()).isEqualTo(HOST);
assertThat(runningService.ports().getAll()).isEmpty();
assertThat(runningService.env()).containsExactly(entry("a", "b"));

View File

@ -77,13 +77,13 @@ class DefaultRunningServiceTests {
@Test
void imageReturnsImageFromPsResponse() {
assertThat(this.runningService.image()).hasToString("redis");
assertThat(this.runningService.image()).hasToString("docker.io/library/redis");
}
@Test // gh-34992
void imageWhenUsingEarlierDockerVersionReturnsImageFromInspectResult() {
DefaultRunningService runningService = createRunningService(false);
assertThat(runningService.image()).hasToString("redis");
assertThat(runningService.image()).hasToString("docker.io/library/redis");
}

View File

@ -0,0 +1,163 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.docker.compose.core;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link ImageName}.
*
* @author Phillip Webb
* @author Scott Frederick
*/
class ImageNameTests {
@Test
void ofWhenNameOnlyCreatesImageName() {
ImageName imageName = ImageName.of("ubuntu");
assertThat(imageName).hasToString("docker.io/library/ubuntu");
assertThat(imageName.getDomain()).isEqualTo("docker.io");
assertThat(imageName.getName()).isEqualTo("library/ubuntu");
}
@Test
void ofWhenSlashedNameCreatesImageName() {
ImageName imageName = ImageName.of("canonical/ubuntu");
assertThat(imageName).hasToString("docker.io/canonical/ubuntu");
assertThat(imageName.getDomain()).isEqualTo("docker.io");
assertThat(imageName.getName()).isEqualTo("canonical/ubuntu");
}
@Test
void ofWhenLocalhostNameCreatesImageName() {
ImageName imageName = ImageName.of("localhost/canonical/ubuntu");
assertThat(imageName).hasToString("localhost/canonical/ubuntu");
assertThat(imageName.getDomain()).isEqualTo("localhost");
assertThat(imageName.getName()).isEqualTo("canonical/ubuntu");
}
@Test
void ofWhenDomainAndNameCreatesImageName() {
ImageName imageName = ImageName.of("repo.spring.io/canonical/ubuntu");
assertThat(imageName).hasToString("repo.spring.io/canonical/ubuntu");
assertThat(imageName.getDomain()).isEqualTo("repo.spring.io");
assertThat(imageName.getName()).isEqualTo("canonical/ubuntu");
}
@Test
void ofWhenDomainNameAndPortCreatesImageName() {
ImageName imageName = ImageName.of("repo.spring.io:8080/canonical/ubuntu");
assertThat(imageName).hasToString("repo.spring.io:8080/canonical/ubuntu");
assertThat(imageName.getDomain()).isEqualTo("repo.spring.io:8080");
assertThat(imageName.getName()).isEqualTo("canonical/ubuntu");
}
@Test
void ofWhenSimpleNameAndPortCreatesImageName() {
ImageName imageName = ImageName.of("repo:8080/ubuntu");
assertThat(imageName).hasToString("repo:8080/ubuntu");
assertThat(imageName.getDomain()).isEqualTo("repo:8080");
assertThat(imageName.getName()).isEqualTo("ubuntu");
}
@Test
void ofWhenSimplePathAndPortCreatesImageName() {
ImageName imageName = ImageName.of("repo:8080/canonical/ubuntu");
assertThat(imageName).hasToString("repo:8080/canonical/ubuntu");
assertThat(imageName.getDomain()).isEqualTo("repo:8080");
assertThat(imageName.getName()).isEqualTo("canonical/ubuntu");
}
@Test
void ofWhenNameWithLongPathCreatesImageName() {
ImageName imageName = ImageName.of("path1/path2/path3/ubuntu");
assertThat(imageName).hasToString("docker.io/path1/path2/path3/ubuntu");
assertThat(imageName.getDomain()).isEqualTo("docker.io");
assertThat(imageName.getName()).isEqualTo("path1/path2/path3/ubuntu");
}
@Test
void ofWhenLocalhostDomainCreatesImageName() {
ImageName imageName = ImageName.of("localhost/ubuntu");
assertThat(imageName.getDomain()).isEqualTo("localhost");
assertThat(imageName.getName()).isEqualTo("ubuntu");
}
@Test
void ofWhenLocalhostDomainAndPathCreatesImageName() {
ImageName imageName = ImageName.of("localhost/library/ubuntu");
assertThat(imageName.getDomain()).isEqualTo("localhost");
assertThat(imageName.getName()).isEqualTo("library/ubuntu");
}
@Test
void ofWhenLegacyDomainUsesNewDomain() {
ImageName imageName = ImageName.of("index.docker.io/ubuntu");
assertThat(imageName).hasToString("docker.io/library/ubuntu");
assertThat(imageName.getDomain()).isEqualTo("docker.io");
assertThat(imageName.getName()).isEqualTo("library/ubuntu");
}
@Test
void ofWhenNameIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> ImageName.of(null))
.withMessage("Value must not be empty");
}
@Test
void ofWhenNameIsEmptyThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> ImageName.of("")).withMessage("Value must not be empty");
}
@Test
void ofWhenContainsUppercaseThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> ImageName.of("Test"))
.withMessageContaining("Unable to parse name")
.withMessageContaining("Test");
}
@Test
void ofWhenNameIncludesTagThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> ImageName.of("ubuntu:latest"))
.withMessageContaining("Unable to parse name")
.withMessageContaining(":latest");
}
@Test
void ofWhenNameIncludeDigestThrowsException() {
assertThatIllegalArgumentException().isThrownBy(
() -> ImageName.of("ubuntu@sha256:47bfdb88c3ae13e488167607973b7688f69d9e8c142c2045af343ec199649c09"))
.withMessageContaining("Unable to parse name")
.withMessageContaining("@sha256:47b");
}
@Test
void hashCodeAndEquals() {
ImageName n1 = ImageName.of("ubuntu");
ImageName n2 = ImageName.of("library/ubuntu");
ImageName n3 = ImageName.of("docker.io/ubuntu");
ImageName n4 = ImageName.of("docker.io/library/ubuntu");
ImageName n5 = ImageName.of("index.docker.io/library/ubuntu");
ImageName n6 = ImageName.of("alpine");
assertThat(n1).hasSameHashCodeAs(n2).hasSameHashCodeAs(n3).hasSameHashCodeAs(n4).hasSameHashCodeAs(n5);
assertThat(n1).isEqualTo(n1).isEqualTo(n2).isEqualTo(n3).isEqualTo(n4).isNotEqualTo(n6);
}
}

View File

@ -19,85 +19,150 @@ package org.springframework.boot.docker.compose.core;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link ImageReference}.
*
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
* @author Scott Frederick
*/
class ImageReferenceTests {
@Test
void getImageNameWhenImageOnly() {
ImageReference imageReference = ImageReference.of("redis");
assertThat(imageReference.getImageName()).isEqualTo("redis");
void ofSimpleName() {
ImageReference reference = ImageReference.of("ubuntu");
assertThat(reference.getDomain()).isEqualTo("docker.io");
assertThat(reference.getName()).isEqualTo("library/ubuntu");
assertThat(reference.getTag()).isNull();
assertThat(reference.getDigest()).isNull();
assertThat(reference).hasToString("docker.io/library/ubuntu");
}
@Test
void getImageNameWhenImageAndTag() {
ImageReference imageReference = ImageReference.of("redis:5");
assertThat(imageReference.getImageName()).isEqualTo("redis");
void ofLibrarySlashName() {
ImageReference reference = ImageReference.of("library/ubuntu");
assertThat(reference.getDomain()).isEqualTo("docker.io");
assertThat(reference.getName()).isEqualTo("library/ubuntu");
assertThat(reference.getTag()).isNull();
assertThat(reference.getDigest()).isNull();
assertThat(reference).hasToString("docker.io/library/ubuntu");
}
@Test
void getImageNameWhenImageAndDigest() {
ImageReference imageReference = ImageReference
.of("redis@sha256:0ed5d5928d4737458944eb604cc8509e245c3e19d02ad83935398bc4b991aac7");
assertThat(imageReference.getImageName()).isEqualTo("redis");
void ofSlashName() {
ImageReference reference = ImageReference.of("adoptopenjdk/openjdk11");
assertThat(reference.getDomain()).isEqualTo("docker.io");
assertThat(reference.getName()).isEqualTo("adoptopenjdk/openjdk11");
assertThat(reference.getTag()).isNull();
assertThat(reference.getDigest()).isNull();
assertThat(reference).hasToString("docker.io/adoptopenjdk/openjdk11");
}
@Test
void getImageNameWhenProjectAndImage() {
ImageReference imageReference = ImageReference.of("library/redis");
assertThat(imageReference.getImageName()).isEqualTo("redis");
void ofCustomDomain() {
ImageReference reference = ImageReference.of("repo.example.com/java/jdk");
assertThat(reference.getDomain()).isEqualTo("repo.example.com");
assertThat(reference.getName()).isEqualTo("java/jdk");
assertThat(reference.getTag()).isNull();
assertThat(reference.getDigest()).isNull();
assertThat(reference).hasToString("repo.example.com/java/jdk");
}
@Test
void getImageNameWhenRegistryLibraryAndImage() {
ImageReference imageReference = ImageReference.of("docker.io/library/redis");
assertThat(imageReference.getImageName()).isEqualTo("redis");
void ofCustomDomainAndPort() {
ImageReference reference = ImageReference.of("repo.example.com:8080/java/jdk");
assertThat(reference.getDomain()).isEqualTo("repo.example.com:8080");
assertThat(reference.getName()).isEqualTo("java/jdk");
assertThat(reference.getTag()).isNull();
assertThat(reference.getDigest()).isNull();
assertThat(reference).hasToString("repo.example.com:8080/java/jdk");
}
@Test
void getImageNameWhenRegistryLibraryImageAndTag() {
ImageReference imageReference = ImageReference.of("docker.io/library/redis:5");
assertThat(imageReference.getImageName()).isEqualTo("redis");
void ofLegacyDomain() {
ImageReference reference = ImageReference.of("index.docker.io/ubuntu");
assertThat(reference.getDomain()).isEqualTo("docker.io");
assertThat(reference.getName()).isEqualTo("library/ubuntu");
assertThat(reference.getTag()).isNull();
assertThat(reference.getDigest()).isNull();
assertThat(reference).hasToString("docker.io/library/ubuntu");
}
@Test
void getImageNameWhenRegistryLibraryImageAndDigest() {
ImageReference imageReference = ImageReference
.of("docker.io/library/redis@sha256:0ed5d5928d4737458944eb604cc8509e245c3e19d02ad83935398bc4b991aac7");
assertThat(imageReference.getImageName()).isEqualTo("redis");
void ofNameAndTag() {
ImageReference reference = ImageReference.of("ubuntu:bionic");
assertThat(reference.getDomain()).isEqualTo("docker.io");
assertThat(reference.getName()).isEqualTo("library/ubuntu");
assertThat(reference.getTag()).isEqualTo("bionic");
assertThat(reference.getDigest()).isNull();
assertThat(reference).hasToString("docker.io/library/ubuntu:bionic");
}
@Test
void getImageNameWhenRegistryWithPort() {
ImageReference imageReference = ImageReference.of("my_private.registry:5000/redis");
assertThat(imageReference.getImageName()).isEqualTo("redis");
void ofDomainPortAndTag() {
ImageReference reference = ImageReference.of("repo.example.com:8080/library/ubuntu:v1");
assertThat(reference.getDomain()).isEqualTo("repo.example.com:8080");
assertThat(reference.getName()).isEqualTo("library/ubuntu");
assertThat(reference.getTag()).isEqualTo("v1");
assertThat(reference.getDigest()).isNull();
assertThat(reference).hasToString("repo.example.com:8080/library/ubuntu:v1");
}
@Test
void getImageNameWhenRegistryWithPortAndTag() {
ImageReference imageReference = ImageReference.of("my_private.registry:5000/redis:5");
assertThat(imageReference.getImageName()).isEqualTo("redis");
void ofNameAndDigest() {
ImageReference reference = ImageReference
.of("ubuntu@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d");
assertThat(reference.getDomain()).isEqualTo("docker.io");
assertThat(reference.getName()).isEqualTo("library/ubuntu");
assertThat(reference.getTag()).isNull();
assertThat(reference.getDigest())
.isEqualTo("sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d");
assertThat(reference).hasToString(
"docker.io/library/ubuntu@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d");
}
@Test
void toStringReturnsReferenceString() {
ImageReference imageReference = ImageReference.of("docker.io/library/redis");
assertThat(imageReference).hasToString("docker.io/library/redis");
void ofNameAndTagAndDigest() {
ImageReference reference = ImageReference
.of("ubuntu:bionic@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d");
assertThat(reference.getDomain()).isEqualTo("docker.io");
assertThat(reference.getName()).isEqualTo("library/ubuntu");
assertThat(reference.getTag()).isEqualTo("bionic");
assertThat(reference.getDigest())
.isEqualTo("sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d");
assertThat(reference).hasToString(
"docker.io/library/ubuntu:bionic@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d");
}
@Test
void ofCustomDomainAndPortWithTag() {
ImageReference reference = ImageReference
.of("example.com:8080/canonical/ubuntu:bionic@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d");
assertThat(reference.getDomain()).isEqualTo("example.com:8080");
assertThat(reference.getName()).isEqualTo("canonical/ubuntu");
assertThat(reference.getTag()).isEqualTo("bionic");
assertThat(reference.getDigest())
.isEqualTo("sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d");
assertThat(reference).hasToString(
"example.com:8080/canonical/ubuntu:bionic@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d");
}
@Test
void ofWhenHasIllegalCharacter() {
assertThatIllegalArgumentException()
.isThrownBy(() -> ImageReference
.of("registry.example.com/example/example-app:1.6.0-dev.2.uncommitted+wip.foo.c75795d"))
.withMessageContaining("Unable to parse image reference");
}
@Test
void equalsAndHashCode() {
ImageReference imageReference1 = ImageReference.of("docker.io/library/redis");
ImageReference imageReference2 = ImageReference.of("docker.io/library/redis");
ImageReference imageReference3 = ImageReference.of("docker.io/library/other");
assertThat(imageReference1.hashCode()).isEqualTo(imageReference2.hashCode());
assertThat(imageReference1).isEqualTo(imageReference1).isEqualTo(imageReference2).isNotEqualTo(imageReference3);
ImageReference r1 = ImageReference.of("ubuntu:bionic");
ImageReference r2 = ImageReference.of("docker.io/library/ubuntu:bionic");
ImageReference r3 = ImageReference.of("docker.io/library/ubuntu:latest");
assertThat(r1).hasSameHashCodeAs(r2);
assertThat(r1).isEqualTo(r1).isEqualTo(r2).isNotEqualTo(r3);
}
}

View File

@ -0,0 +1,96 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.docker.compose.service.connection;
import java.util.Map;
import java.util.function.Predicate;
import org.junit.jupiter.api.Test;
import org.springframework.boot.docker.compose.core.ImageReference;
import org.springframework.boot.docker.compose.core.RunningService;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link ConnectionNamePredicate}.
*
* @author Phillip Webb
*/
class ConnectionNamePredicateTests {
@Test
void offical() {
assertThat(predicateOf("elasticsearch")).accepts(sourceOf("elasticsearch"));
assertThat(predicateOf("elasticsearch")).accepts(sourceOf("library/elasticsearch"));
assertThat(predicateOf("elasticsearch")).accepts(sourceOf("docker.io/library/elasticsearch"));
assertThat(predicateOf("elasticsearch")).accepts(sourceOf("docker.io/elasticsearch"));
assertThat(predicateOf("elasticsearch")).accepts(sourceOf("docker.io/elasticsearch:latest"));
assertThat(predicateOf("elasticsearch")).rejects(sourceOf("redis"));
assertThat(predicateOf("elasticsearch")).rejects(sourceOf("library/redis"));
assertThat(predicateOf("elasticsearch")).rejects(sourceOf("docker.io/library/redis"));
assertThat(predicateOf("elasticsearch")).rejects(sourceOf("docker.io/redis"));
assertThat(predicateOf("elasticsearch")).rejects(sourceOf("docker.io/redis"));
assertThat(predicateOf("zipkin")).rejects(sourceOf("openzipkin/zipkin"));
}
@Test
void organization() {
assertThat(predicateOf("openzipkin/zipkin")).accepts(sourceOf("openzipkin/zipkin"));
assertThat(predicateOf("openzipkin/zipkin")).accepts(sourceOf("openzipkin/zipkin:latest"));
assertThat(predicateOf("openzipkin/zipkin")).rejects(sourceOf("openzipkin/zapkin"));
assertThat(predicateOf("openzipkin/zipkin")).rejects(sourceOf("zipkin"));
}
@Test
void customDomain() {
assertThat(predicateOf("redis")).accepts(sourceOf("internalhost:8080/library/redis"));
assertThat(predicateOf("redis")).accepts(sourceOf("myhost.com/library/redis"));
assertThat(predicateOf("redis")).accepts(sourceOf("myhost.com:8080/library/redis"));
assertThat(predicateOf("redis")).rejects(sourceOf("internalhost:8080/redis"));
}
@Test
void labeled() {
assertThat(predicateOf("redis")).accepts(sourceOf("internalhost:8080/myredis", "redis"));
assertThat(predicateOf("redis")).accepts(sourceOf("internalhost:8080/myredis", "library/redis"));
assertThat(predicateOf("openzipkin/zipkin"))
.accepts(sourceOf("internalhost:8080/libs/libs/mzipkin", "openzipkin/zipkin"));
}
private Predicate<DockerComposeConnectionSource> predicateOf(String required) {
return new ConnectionNamePredicate(required);
}
private DockerComposeConnectionSource sourceOf(String connectioName) {
return sourceOf(connectioName, null);
}
private DockerComposeConnectionSource sourceOf(String connectioName, String label) {
DockerComposeConnectionSource source = mock(DockerComposeConnectionSource.class);
RunningService runningService = mock(RunningService.class);
given(source.getRunningService()).willReturn(runningService);
given(runningService.image()).willReturn(ImageReference.of(connectioName));
if (label != null) {
given(runningService.labels()).willReturn(Map.of("org.springframework.boot.service-connection", label));
}
return source;
}
}

View File

@ -56,7 +56,7 @@ The following service connections are currently supported:
| Containers named "redis"
| `ZipkinConnectionDetails`
| Containers named "zipkin".
| Containers named "openzipkin/zipkin".
|===