Skip to content

Deprecate ambiguous constructors #2839

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
Jul 19, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
68ee3bc
WIP
rnorth Jun 25, 2020
46c7e1b
WIP
rnorth Jun 25, 2020
054b637
WIP
rnorth Jun 25, 2020
196f1a9
WIP
rnorth Jun 29, 2020
f3fe807
Tidy up test image references
rnorth Jul 2, 2020
c523b38
Merge remote-tracking branch 'origin/master' into deprecate-ambiguous…
rnorth Jul 2, 2020
296f9f7
Update docs re contrainer creation and specifying versions
rnorth Jul 2, 2020
aa94289
Update Javadocs with deprecation warnings.
rnorth Jul 2, 2020
77aa418
Add `asCanonicalNameString()` method on `DockerImageName`
rnorth Jul 3, 2020
592f08a
Use static factory method instead of constructor
rnorth Jul 3, 2020
1edc307
Use constant for docker registry image name
rnorth Jul 4, 2020
113f3b3
Simplify image usage in ReusabilityUnitTests
rnorth Jul 4, 2020
fc7f625
Use `DockerImageName` instead of `String` for image name
rnorth Jul 4, 2020
cd7e13b
Inline Mongo image name parsing
rnorth Jul 4, 2020
a35b3c4
Fix constructor delegation
rnorth Jul 4, 2020
868f962
Undo deprecation of SocatContainer no-args constructor
rnorth Jul 4, 2020
faf1217
Update references to DockerImageName.parse in example code
rnorth Jul 4, 2020
ee45003
Merge remote-tracking branch 'origin/master' into deprecate-ambiguous…
rnorth Jul 4, 2020
8960ceb
Merge remote-tracking branch 'origin/master' into deprecate-ambiguous…
rnorth Jul 6, 2020
f4e314d
Merge remote-tracking branch 'origin/master' into deprecate-ambiguous…
rnorth Jul 16, 2020
cc4190d
Update core/src/test/java/org/testcontainers/images/ImagePullPolicyTe…
rnorth Jul 19, 2020
0eee5d8
Merge branch 'master' into deprecate-ambiguous-constructors
bsideup Jul 19, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public class DockerClientFactory {
TESTCONTAINERS_SESSION_ID_LABEL, SESSION_ID
);

private static final String TINY_IMAGE = TestcontainersConfiguration.getInstance().getTinyImage();
private static final String TINY_IMAGE = TestcontainersConfiguration.getInstance().getTinyDockerImageName().asCanonicalNameString();
private static DockerClientFactory instance;

// Cached client configuration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -586,7 +586,7 @@ class ContainerisedDockerCompose extends GenericContainer<ContainerisedDockerCom

public ContainerisedDockerCompose(List<File> composeFiles, String identifier) {

super(TestcontainersConfiguration.getInstance().getDockerComposeContainerImage());
super(TestcontainersConfiguration.getInstance().getDockerComposeDockerImageName());
addEnv(ENV_PROJECT_NAME, identifier);

// Map the docker compose file into the container
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
* not available - which could manifest as flaky or unstable tests.</p>
*/
public class FixedHostPortGenericContainer<SELF extends FixedHostPortGenericContainer<SELF>> extends GenericContainer<SELF> {

/**
* @deprecated it is highly recommended that {@link FixedHostPortGenericContainer} not be used, as it risks port conflicts.
*/
@Deprecated
public FixedHostPortGenericContainer(@NotNull String dockerImageName) {
super(dockerImageName);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
import com.github.dockerjava.api.model.ContainerNetwork;
import com.github.dockerjava.api.model.ExposedPort;
import com.github.dockerjava.api.model.HostConfig;
import com.github.dockerjava.api.model.Info;
import com.github.dockerjava.api.model.Link;
import com.github.dockerjava.api.model.PortBinding;
import com.github.dockerjava.api.model.Volume;
Expand Down Expand Up @@ -55,6 +54,7 @@
import org.testcontainers.lifecycle.TestDescription;
import org.testcontainers.lifecycle.TestLifecycleAware;
import org.testcontainers.utility.Base58;
import org.testcontainers.utility.DockerImageName;
import org.testcontainers.utility.DockerLoggerFactory;
import org.testcontainers.utility.DockerMachineClient;
import org.testcontainers.utility.MountableFile;
Expand Down Expand Up @@ -227,10 +227,27 @@ public class GenericContainer<SELF extends GenericContainer<SELF>>
@Setter(AccessLevel.NONE)
private boolean shouldBeReused = false;


public GenericContainer(@NonNull final DockerImageName dockerImageName) {
this.image = new RemoteDockerImage(dockerImageName);
}

public GenericContainer(@NonNull final RemoteDockerImage image) {
this.image = image;
}

/**
* @deprecated use {@link GenericContainer(DockerImageName)} instead
*/
@Deprecated
public GenericContainer() {
this(TestcontainersConfiguration.getInstance().getTinyImage());
}

/**
* @deprecated use {@link GenericContainer(DockerImageName)} instead
*/
@Deprecated
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about GenericContainer(Future)? Since it can be replaced with new GenericContainer(new RemoteDockerImage(future))

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooh, interesting suggestion - I'll explore that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've not done this yet.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've had a quick try, but I'm not sure about this.

This example from existing code:

new GenericContainer(new ImageFromDockerfile() ...

would become

new GenericContainer(new RemoteDockerImage(new ImageFromDockerfile() ...

which I think is quite ugly - it's really not a 'remote docker image', so naming-wise it's a bit of a stretch.

I wonder if, really, the way to make this work would be to rename RemoteDockerImage to FutureDockerImageName, which more accurately reflects what it's used for. So the example would become:

new GenericContainer(new FutureDockerImageName(new ImageFromDockerfile() ...

We'd of course keep RemoteDockerImage around, deprecated, as no more than an alias to FutureDockerImageName.

Two problems I see with this that I don't think we can magic away. I think I could live with these:

  • the three levels of constructors involved are a bit clunky, but it's not a big deal
  • it's such as shame that type erasure prevents us from overloading the GenericContainer(Future<String>) constructor, because otherwise FutureDockerImageName could just be Future<DockerImageName> 😂 😭

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I agree that "Remote" is misleading here.

But the example you provided made me think whether we should even wrap ImageFromDockerfile with RemoteDockerImage, since it is definitely local, does not need an auth resolve, etc etc...
what if we extract a common interface from RemoteDockerImage and make ImageFromDockerfile implement it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the interface being DockerImage?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed on Slack, we'll defer this to a separate PR.

It feels like more extensive and complicated changes are needed to refactor the Remote/Future images code, and I feel it would be sensible to not add that complexity on top of this, already massive, PR.

public GenericContainer(@NonNull final String dockerImageName) {
this.setDockerImageName(dockerImageName);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
public enum PortForwardingContainer {
INSTANCE;

private GenericContainer container;
private GenericContainer<?> container;

private final Set<Entry<Integer, Integer>> exposedPorts = Collections.newSetFromMap(new ConcurrentHashMap<>());

Expand All @@ -29,7 +29,7 @@ public enum PortForwardingContainer {
@SneakyThrows
private Connection createSSHSession() {
String password = UUID.randomUUID().toString();
container = new GenericContainer<>(TestcontainersConfiguration.getInstance().getSSHdImage())
container = new GenericContainer<>(TestcontainersConfiguration.getInstance().getSSHdDockerImageName())
.withExposedPorts(22)
.withEnv("PASSWORD", password)
.withCommand(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.testcontainers.containers;

import org.testcontainers.utility.Base58;
import org.testcontainers.utility.DockerImageName;
import org.testcontainers.utility.TestcontainersConfiguration;

import java.util.HashMap;
Expand All @@ -16,7 +17,11 @@ public class SocatContainer extends GenericContainer<SocatContainer> {
private final Map<Integer, String> targets = new HashMap<>();

public SocatContainer() {
super(TestcontainersConfiguration.getInstance().getSocatContainerImage());
this(TestcontainersConfiguration.getInstance().getSocatDockerImageName());
}

public SocatContainer(final DockerImageName dockerImageName) {
super(dockerImageName);
withCreateContainerCmdModifier(it -> it.withEntrypoint("/bin/sh"));
withCreateContainerCmdModifier(it -> it.withName("testcontainers-socat-" + Base58.randomString(8)));
}
Expand All @@ -39,4 +44,4 @@ protected void configure() {
.collect(Collectors.joining(" & "))
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public VncRecordingContainer(@NonNull GenericContainer<?> targetContainer) {
* Create a sidekick container and attach it to another container. The VNC output of that container will be recorded.
*/
public VncRecordingContainer(@NonNull Network network, @NonNull String targetNetworkAlias) throws IllegalStateException {
super(TestcontainersConfiguration.getInstance().getVncRecordedContainerImage());
super(TestcontainersConfiguration.getInstance().getVncDockerImageName());

this.targetNetworkAlias = targetNetworkAlias;
withNetwork(network);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public AuthConfig effectiveAuthConfig(String imageName) {
}

// try and obtain more accurate auth config using our resolution
final DockerImageName parsed = new DockerImageName(imageName);
final DockerImageName parsed = DockerImageName.parse(imageName);
final AuthConfig effectiveAuthConfig = RegistryAuthLocator.instance()
.lookupAuthConfig(parsed, fallbackAuthConfig);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public abstract class AbstractImagePullPolicy implements ImagePullPolicy {

@Override
public boolean shouldPull(DockerImageName imageName) {
Logger logger = DockerLoggerFactory.getLogger(imageName.toString());
Logger logger = DockerLoggerFactory.getLogger(imageName.asCanonicalNameString());

// Does our cache already know the image?
ImageData cachedImageData = LOCAL_IMAGES_CACHE.get(imageName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public Optional<ImageData> refreshCache(DockerImageName imageName) {

InspectImageResponse response = null;
try {
response = dockerClient.inspectImageCmd(imageName.toString()).exec();
response = dockerClient.inspectImageCmd(imageName.asCanonicalNameString()).exec();
} catch (NotFoundException e) {
log.trace("Image {} not found", imageName, e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,18 @@ public class RemoteDockerImage extends LazyFuture<String> {
@ToString.Exclude
private DockerClient dockerClient = DockerClientFactory.lazyClient();

public RemoteDockerImage(DockerImageName dockerImageName) {
this.imageNameFuture = CompletableFuture.completedFuture(dockerImageName);
}

@Deprecated
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the contract was always that RemoteDockerImage accepts a full image name, perhaps we can keep this and (String,String) constructors?

Otherwise, it becomes a bit too long:

new GenericContainer(new RemoveDockerImage(new DockerImageName("missed-me:1"))

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will defer as per #2839 (comment)

public RemoteDockerImage(String dockerImageName) {
this.imageNameFuture = CompletableFuture.completedFuture(new DockerImageName(dockerImageName));
this.imageNameFuture = CompletableFuture.completedFuture(DockerImageName.parse(dockerImageName));
}

@Deprecated
public RemoteDockerImage(@NonNull String repository, @NonNull String tag) {
this.imageNameFuture = CompletableFuture.completedFuture(new DockerImageName(repository, tag));
this.imageNameFuture = CompletableFuture.completedFuture(DockerImageName.parse(repository).withTag(tag));
}

public RemoteDockerImage(@NonNull Future<String> imageFuture) {
Expand All @@ -57,7 +63,7 @@ protected final String resolve() {
Logger logger = DockerLoggerFactory.getLogger(imageName.toString());
try {
if (!imagePullPolicy.shouldPull(imageName)) {
return imageName.toString();
return imageName.asCanonicalNameString();
}

// The image is not available locally - pull it
Expand All @@ -76,7 +82,7 @@ protected final String resolve() {

LocalImagesCache.INSTANCE.refreshCache(imageName);

return imageName.toString();
return imageName.asCanonicalNameString();
} catch (InterruptedException | InternalServerErrorException e) {
// these classes of exception often relate to timeout/connection errors so should be retried
lastFailure = e;
Expand Down Expand Up @@ -104,7 +110,7 @@ private String imageNameToString() {
}

try {
return getImageName().toString();
return getImageName().asCanonicalNameString();
} catch (InterruptedException | ExecutionException e) {
return e.getMessage();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
public interface FromStatementTrait<SELF extends FromStatementTrait<SELF> & DockerfileBuilderTrait<SELF>> {

default SELF from(String dockerImageName) {
new DockerImageName(dockerImageName).assertValid();
DockerImageName.parse(dockerImageName).assertValid();

return ((SELF) this).withStatement(new SingleArgumentStatement("FROM", dockerImageName));
}
Expand Down
104 changes: 72 additions & 32 deletions core/src/main/java/org/testcontainers/utility/DockerImageName.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@


import com.google.common.net.HostAndPort;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.jetbrains.annotations.NotNull;

import java.util.regex.Pattern;

@EqualsAndHashCode(exclude = "rawName")
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public final class DockerImageName {

/* Regex patterns used for validation */
Expand All @@ -19,22 +23,42 @@ public final class DockerImageName {
private final String rawName;
private final String registry;
private final String repo;
private final Versioning versioning;
@NotNull private final Versioning versioning;

public DockerImageName(String name) {
this.rawName = name;
final int slashIndex = name.indexOf('/');
/**
* Parses a docker image name from a provided string.
*
* @param fullImageName in standard Docker format, e.g. <code>name:tag</code>,
* <code>some.registry/path/name:tag</code>,
* <code>some.registry/path/name@sha256:abcdef...</code>, etc.
*/
public static DockerImageName parse(String fullImageName) {
return new DockerImageName(fullImageName);
}

/**
* Parses a docker image name from a provided string.
*
* @param fullImageName in standard Docker format, e.g. <code>name:tag</code>,
* <code>some.registry/path/name:tag</code>,
* <code>some.registry/path/name@sha256:abcdef...</code>, etc.
* @deprecated use {@link DockerImageName#parse(String)} instead
*/
@Deprecated
public DockerImageName(String fullImageName) {
this.rawName = fullImageName;
final int slashIndex = fullImageName.indexOf('/');

String remoteName;
if (slashIndex == -1 ||
(!name.substring(0, slashIndex).contains(".") &&
!name.substring(0, slashIndex).contains(":") &&
!name.substring(0, slashIndex).equals("localhost"))) {
(!fullImageName.substring(0, slashIndex).contains(".") &&
!fullImageName.substring(0, slashIndex).contains(":") &&
!fullImageName.substring(0, slashIndex).equals("localhost"))) {
registry = "";
remoteName = name;
remoteName = fullImageName;
} else {
registry = name.substring(0, slashIndex);
remoteName = name.substring(slashIndex + 1);
registry = fullImageName.substring(0, slashIndex);
remoteName = fullImageName.substring(slashIndex + 1);
}

if (remoteName.contains("@sha256:")) {
Expand All @@ -49,28 +73,40 @@ public DockerImageName(String name) {
}
}

public DockerImageName(String name, String tag) {
this.rawName = name;
final int slashIndex = name.indexOf('/');
/**
* Parses a docker image name from a provided string, and uses a separate provided version.
*
* @param nameWithoutTag in standard Docker format, e.g. <code>name</code>,
* <code>some.registry/path/name</code>,
* <code>some.registry/path/name</code>, etc.
* @param version a docker image version identifier, either as a tag or sha256 checksum, e.g.
* <code>tag</code>,
* <code>sha256:abcdef...</code>.
* @deprecated use {@link DockerImageName#parse(String)}.{@link DockerImageName#withTag(String)} instead
*/
@Deprecated
public DockerImageName(String nameWithoutTag, @NotNull String version) {
this.rawName = nameWithoutTag;
final int slashIndex = nameWithoutTag.indexOf('/');

String remoteName;
if (slashIndex == -1 ||
(!name.substring(0, slashIndex).contains(".") &&
!name.substring(0, slashIndex).contains(":") &&
!name.substring(0, slashIndex).equals("localhost"))) {
(!nameWithoutTag.substring(0, slashIndex).contains(".") &&
!nameWithoutTag.substring(0, slashIndex).contains(":") &&
!nameWithoutTag.substring(0, slashIndex).equals("localhost"))) {
registry = "";
remoteName = name;
remoteName = nameWithoutTag;
} else {
registry = name.substring(0, slashIndex);
remoteName = name.substring(slashIndex + 1);
registry = nameWithoutTag.substring(0, slashIndex);
remoteName = nameWithoutTag.substring(slashIndex + 1);
}

if (tag.startsWith("sha256:")) {
if (version.startsWith("sha256:")) {
repo = remoteName;
versioning = new Sha256Versioning(tag.replace("sha256:", ""));
versioning = new Sha256Versioning(version.replace("sha256:", ""));
} else {
repo = remoteName;
versioning = new TagVersioning(tag);
versioning = new TagVersioning(version);
}
}

Expand All @@ -92,13 +128,16 @@ public String getVersionPart() {
return versioning.toString();
}

/**
* @return canonical name for the image
*/
public String asCanonicalNameString() {
return getUnversionedPart() + versioning.getSeparator() + versioning.toString();
}

@Override
public String toString() {
if (versioning == null) {
return getUnversionedPart();
} else {
return getUnversionedPart() + versioning.getSeparator() + versioning.toString();
}
return asCanonicalNameString();
}

/**
Expand All @@ -111,10 +150,6 @@ public void assertValid() {
if (!REPO_NAME.matcher(repo).matches()) {
throw new IllegalArgumentException(repo + " is not a valid Docker image name (in " + rawName + ")");
}
if (versioning == null) {
throw new IllegalArgumentException("No image tag was specified in docker image name " +
"(" + rawName + "). Please provide a tag; this may be 'latest' or a specific version");
}
if (!versioning.isValid()) {
throw new IllegalArgumentException(versioning + " is not a valid image versioning identifier (in " + rawName + ")");
}
Expand All @@ -124,8 +159,13 @@ public String getRegistry() {
return registry;
}

public DockerImageName withTag(final String newTag) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love it!

Made me thinking whether we should even have a fully featured builder as well:

DockerImageName.builder()
    .registry("repo.example.com")
    .image("services/foo")
    .tag("123")
    .build();

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like the halfway house of Dockerfilename.of("repo.example.com/services/foo").withTag("123") is probably enough - I'd like to try that first.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FTR the builder idea is just an idea, don't need to be in this PR and can easily be a follow up :)

return new DockerImageName(rawName, registry, repo, new TagVersioning(newTag));
}

private interface Versioning {
boolean isValid();

String getSeparator();
}

Expand Down Expand Up @@ -155,7 +195,7 @@ public String toString() {
}

@Data
private class Sha256Versioning implements Versioning {
private static class Sha256Versioning implements Versioning {
public static final String HASH_REGEX = "[0-9a-fA-F]{32,}";
private final String hash;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package org.testcontainers.utility;

import lombok.NonNull;

import org.apache.commons.lang.SystemUtils;
import org.slf4j.Logger;

Expand Down
Loading