Skip to content
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

Add image compatibility checks #3021

Merged
merged 36 commits into from
Sep 29, 2020
Merged

Add image compatibility checks #3021

merged 36 commits into from
Sep 29, 2020

Conversation

rnorth
Copy link
Member

@rnorth rnorth commented Jul 26, 2020

Background to this change: the majority of modules make assumptions about the container image being used - for example, port numbers, expected log lines, etc. When asking users to provide their own images with modules, it is potentially confusing if the provided image diverges from the original 'vendor-provided' image that the module was built to support.

This change is intended to ensure that, if the user provides their own image that is not the same as the vendor-provided one, they are given adequate warning and forced to signal that this is intentional.

For example:

  • new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:any")) will just work, because confluentinc/cp-kafka matches the image name that KafkaContainer was designed to work with
  • but new KafkaContainer(DockerImageName.parse("some-other-kafka")) will not work immediately, because some-other-kafka may be an entirely divergent image from confluentinc/cp-kafka. In this case, the user would be prompted to add .asCompatibleSubstituteFor("confluentinc/cp-kafka") which tells Testcontainers that this is a conscious decision

This PR:

  • Adds to DockerImageName:

    • asCompatibleSubstituteFor(DockerImageName) and asCompatibleSubstituteFor(String) methods which may be used to claim compatibility with a vendor-provided image
    • isCompatibleWith(DockerImageName) and assertCompatibleWith(DockerImageName) methods which can be used by Testcontainers to check that the provided image is compatible with the expected vendor-provided image
  • Refactors all modules to use this new mechanism

  • Amends the TestcontainersConfiguration class so that any file-based overrides automatically claim compatibility

So that compatibility assurances can be made in code rather than just being assumed.
@rnorth rnorth marked this pull request as ready for review August 14, 2020 14:51
@@ -21,6 +21,7 @@ public boolean supports(ConnectionFactoryOptions options) {

@Override
public R2DBCDatabaseContainer createContainer(ConnectionFactoryOptions options) {
// TODO work out how best to do this if these constants become private
Copy link
Member Author

Choose a reason for hiding this comment

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

A TODO for a point in the near future. This has a lot to do with mandatory bring-your-own-image in R2DBC and JDBC URLs as discussed in Slack (@bsideup)

Copy link
Member

Choose a reason for hiding this comment

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

I think I missed the Slack discussion, but just being pragmatic and make the constants packacke-private?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, that's probably going to be the answer. This isn't something to worry about too much for now, anyway.

kiview
kiview previously requested changes Aug 14, 2020
Copy link
Member

@kiview kiview left a comment

Choose a reason for hiding this comment

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

PR looks good overall, just had some questions and suggestion.

One copy-paste error with KafkaContainer it seems ;)

/**
* @deprecated use {@link MongoDBContainer(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.

Do we really un-deprecate the String constructors?

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 missed this - will look for others.

* @param other the other image that we are trying to check compatibility with
* @throws IllegalStateException if {@link DockerImageName#isCompatibleWith(DockerImageName)} returns false
*/
public void assertCompatibleWith(DockerImageName other) {
Copy link
Member

Choose a reason for hiding this comment

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

If we would return DockerImageName, we could use this method in super constructor arguments.

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 think I'd push back against this - it feels a bit strange to have a value be passed through a method that does assertion. I think I like the assertion being a distinct line in each constructor (after the call to super), as it feels more visible.

Copy link
Member

Choose a reason for hiding this comment

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

The JDK likes to object this argument of feeling strange 🙂
https://docs.oracle.com/javase/8/docs/api/java/util/Objects.html#requireNonNull-T-

But this is not a hill I need to die on. I like it in super constructor though, because this means it gets evaluated before the super constructor is called.

/**
* @deprecated use {@link MongoDBContainer(DockerImageName)} instead
*/
@Deprecated
public MongoDBContainer(@NonNull final String dockerImageName) {
this(DockerImageName.parse(dockerImageName));
}

public MongoDBContainer(final DockerImageName dockerImageName) {
super(dockerImageName);
Copy link
Member

Choose a reason for hiding this comment

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

If assertCompatibleWith would return DockerImageName, we coould use it as argument for the super constructor.

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 above.

/**
* @deprecated use {@link #CassandraContainer(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.

By which logic are the deprecations of constructors removed now? Seems kind of inconsistent between classes.

Copy link
Member Author

Choose a reason for hiding this comment

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

We were missing the deprecated annotation on CassandraContainer's no-arg constructor 🤦

The logic should be:

  • No-arg constructors: always deprecated
  • String, image name constructors: not deprecated
  • String, version constructors: always deprecated

}

/**
* @deprecated use {@link KafkaContainer(DockerImageName)} instead
*/
@Deprecated
public KafkaContainer(String confluentPlatformVersion) {
this(DockerImageName.parse(TestcontainersConfiguration.getInstance().getKafkaImage() + ":" + confluentPlatformVersion));
this(TestcontainersConfiguration.getInstance().getPulsarDockerImageName().withTag(confluentPlatformVersion));
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
this(TestcontainersConfiguration.getInstance().getPulsarDockerImageName().withTag(confluentPlatformVersion));
this(TestcontainersConfiguration.getInstance().getKafkaDockerImageName().withTag(confluentPlatformVersion));

Is this constructor missing a test therefore?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good spot - silly c&p error 😬

Yes, this is missing test coverage. Will add.


String getSeparator();

@Data
Copy link
Member

Choose a reason for hiding this comment

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

@Value instead of @Data? Or @EqualsAndHashcode? Or we don't use lombok in the first plance, since we already define toString() and the constructor.

Copy link
Member Author

Choose a reason for hiding this comment

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

@EqualsAndHashCode would do the trick - good suggestion.

public void testLatestTreatedAsWildcard() {
final DockerImageName subject = DockerImageName.parse("foo:4.5.6");

assertTrue("foo:4.5.6 ~= foo:latest", subject.isCompatibleWith(DockerImageName.parse("foo:1.2.3").withTag("latest")));
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
assertTrue("foo:4.5.6 ~= foo:latest", subject.isCompatibleWith(DockerImageName.parse("foo:1.2.3").withTag("latest")));
assertTrue("foo:4.5.6 ~= foo:latest", subject.isCompatibleWith(DockerImageName.parse("foo")));

Since latest is default?

Copy link
Member Author

@rnorth rnorth Aug 15, 2020

Choose a reason for hiding this comment

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

Ah, the intent here is to make sure that setting latest tag doesn't mess things up. I'll add a clarifying comment:

foo:1.2.3 != foo:4.5.6
foo:1.2.3 ~= foo
foo:1.2.3 ~= foo:latest

The test is effectively making sure that no tag and `latest` tag are equivalent

}

@Test
public void testImageWithAutomaticCompatibility() {
Copy link
Member

Choose a reason for hiding this comment

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

after my suggestion, this would be the same test as testLatestTreatedAsWildcard

Copy link
Member Author

Choose a reason for hiding this comment

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

Removed

}

@Test
public void testCheckMethodRejectsIncompatible() {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
public void testCheckMethodRejectsIncompatible() {
public void testAssertMethodRejectsIncompatible() {

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

@rnorth rnorth requested a review from kiview August 15, 2020 19:10
@rnorth rnorth mentioned this pull request Aug 16, 2020
@@ -63,14 +62,16 @@ public DockerImageName(String fullImageName) {

if (remoteName.contains("@sha256:")) {
repo = remoteName.split("@sha256:")[0];
versioning = new Sha256Versioning(remoteName.split("@sha256:")[1]);
versioning = new Versioning.Sha256Versioning(remoteName.split("@sha256:")[1]);
Copy link
Member

Choose a reason for hiding this comment

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

tiny nit: if we import Versioning.Sha256Versioning and other Versioning.* classes, the changelog should be smaller :)

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed

Copy link
Member

Choose a reason for hiding this comment

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

@rnorth is it? 😅

Copy link
Member Author

Choose a reason for hiding this comment

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

Oops, must have reverted it during moving the interface. Done again.

private DockerImageName(String rawName,
String registry,
String repo,
@Nullable Versioning versioning,
Copy link
Member

Choose a reason for hiding this comment

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

marked as @Nullable while the field isn't

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed

@@ -125,14 +141,14 @@ public String getUnversionedPart() {
* @return the versioned part of this name (tag or sha256)
*/
public String getVersionPart() {
return versioning.toString();
return versioning == null ? "latest" : versioning.toString();
Copy link
Member

Choose a reason for hiding this comment

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

can we make versioning @NonNull and use Versioning.TagVersioning.LATEST if null is passed to @Nullable methods?

Copy link
Member Author

Choose a reason for hiding this comment

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

So, this is one that requires some discussion, and potentially a change or just clear docs!

I'd expect most usage of this feature to be like foo.asCompatibleSubstituteFor("bar") meaning bar with any tag is accepted.

I wanted to leave the possibility open to specify an exact tag match, i.e. foo.asCompatibleSubstituteFor("bar:1.2.3").

So that this works:

  • an absent tag is recorded as null
  • the compatitility check code treats this null as a wildcard
  • conversion to a string treats this null as an implicit latest

It doesn't have to be this way, though. I reckon we could:

  • ignore tags altogether for compatibility checks
  • OR be more explicit about wildcards, e.g. foo.asCompatibleSubstituteFor("bar:*") in the API, and/or have a Versioning.Wildcard type internally instead of null.

WDYT?

Copy link
Member

Choose a reason for hiding this comment

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

have a Versioning.Wildcard type internally instead of null

I like this one! My main concern was the Nullable field that we can avoid and I think Versioning.Wildcard solves the problem very well 👍

Copy link
Member Author

Choose a reason for hiding this comment

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

👍 cool, I'll go with that then.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done (called it AnyVersion)

}

/**
* @return canonical name for the image
*/
public String asCanonicalNameString() {
return getUnversionedPart() + versioning.getSeparator() + versioning.toString();
return getUnversionedPart() + (versioning == null ? ":" : versioning.getSeparator()) + getVersionPart();
Copy link
Member

Choose a reason for hiding this comment

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

ditto re null vs Versioning.TagVersioning.LATEST

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

* @return an immutable copy of this {@link DockerImageName} with the compatibility declaration attached.
*/
public DockerImageName asCompatibleSubstituteFor(DockerImageName otherImageName) {
return new DockerImageName(rawName, registry, repo, versioning, otherImageName);
Copy link
Member

Choose a reason for hiding this comment

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

Consider adding @With(AccessLevel.PRIVATE) to otherImageName, so that we can do:

Suggested change
return new DockerImageName(rawName, registry, repo, versioning, otherImageName);
return withOtherImageName(otherImageName);

Copy link
Member Author

Choose a reason for hiding this comment

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

Nice, this works well (same for withTag).

N.B. I've upgraded the Lombok dependency so that we can use modern @With rather than deprecated @Wither

@rnorth rnorth dismissed kiview’s stale review September 12, 2020 18:13

All comments responded to (either changed or I think I've explained!)

Copy link
Member

@bsideup bsideup left a comment

Choose a reason for hiding this comment

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

🎉 🚀

@rnorth
Copy link
Member Author

rnorth commented Sep 28, 2020

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 2 pipeline(s).

*/
private static final String ELASTICSEARCH_DEFAULT_IMAGE = "docker.elastic.co/elasticsearch/elasticsearch";
private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("docker.elastic.co/elasticsearch/elasticsearch");
Copy link
Contributor

@dadoonet dadoonet Oct 16, 2020

Choose a reason for hiding this comment

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

I'm wondering if we could make this static constant public so people can simply do something like:

new ElasticsearchContainer(ElasticsearchContainer.DEFAULT_IMAGE_NAME.withTag("7.9.2"));

rnorth added a commit that referenced this pull request Oct 29, 2020
Builds upon #3021:

* adds a pluggable image substitution mechanism using ServiceLoader, enabling users to perform custom substitution/auditing of images being used by their tests

* provides a default implementation that behaves similarly to legacy `TestcontainersConfiguration` approach (`testcontainers.properties`), but also...

* For many orgs, sticking a prefix on the front of image names might be enough to use a private registry. I've added a default behaviour whereby, if a particular environment variable is present, image names are automatically substituted. e.g. `TESTCONTAINERS_IMAGE_NAME_PREFIX=my.registry.com/` would transform `redis` to `my.registry.com/redis` etc.

Notes:

* behaviour is similar but not quite identical to `TestcontainersConfiguration`: use of a configured custom image for, e.g. Kafka/Pulsar that does not have a tag specified causes the substitution to take effect for all usages. It seems very unlikely that people would be using a mix of the config file image overrides in some places _and_ specific images specified in code in others.

* Duplication of default image names in modules vs `TestcontainersConfiguration` class is intentional: specifying image overrides in `testcontainers.properties` should be removed in the future.

* ~Add log deprecation warnings when `testcontainers.properties` image overrides are used.~ Defer to a future release?
rnorth added a commit that referenced this pull request Oct 29, 2020
Builds upon #3021:

* adds a pluggable image substitution mechanism using ServiceLoader, enabling users to perform custom substitution/auditing of images being used by their tests

* provides a default implementation that behaves similarly to legacy `TestcontainersConfiguration` approach (`testcontainers.properties`), but also...

* For many orgs, sticking a prefix on the front of image names might be enough to use a private registry. I've added a default behaviour whereby, if a particular environment variable is present, image names are automatically substituted. e.g. `TESTCONTAINERS_IMAGE_NAME_PREFIX=my.registry.com/` would transform `redis` to `my.registry.com/redis` etc.

Notes:

* behaviour is similar but not quite identical to `TestcontainersConfiguration`: use of a configured custom image for, e.g. Kafka/Pulsar that does not have a tag specified causes the substitution to take effect for all usages. It seems very unlikely that people would be using a mix of the config file image overrides in some places _and_ specific images specified in code in others.

* Duplication of default image names in modules vs `TestcontainersConfiguration` class is intentional: specifying image overrides in `testcontainers.properties` should be removed in the future.

* ~Add log deprecation warnings when `testcontainers.properties` image overrides are used.~ Defer to a future release?
rnorth added a commit that referenced this pull request Oct 29, 2020
Builds upon #3021 and #3411:

* adds a pluggable image substitution mechanism using ServiceLoader, enabling users to perform custom substitution/auditing of images being used by their tests

* provides a default implementation that behaves similarly to legacy `TestcontainersConfiguration` approach (`testcontainers.properties`)

Notes:

* behaviour is similar but not quite identical to `TestcontainersConfiguration`: use of a configured custom image for, e.g. Kafka/Pulsar that does not have a tag specified causes the substitution to take effect for all usages. It seems very unlikely that people would be using a mix of the config file image overrides in some places _and_ specific images specified in code in others.

* Duplication of default image names in modules vs `TestcontainersConfiguration` class is intentional: specifying image overrides in `testcontainers.properties` should be removed in the future.

* ~Add log deprecation warnings when `testcontainers.properties` image overrides are used.~ Defer to a future release?
rnorth added a commit that referenced this pull request Oct 29, 2020
Builds upon #3021 and #3411:

* adds a pluggable image substitution mechanism using ServiceLoader, enabling users to perform custom substitution/auditing of images being used by their tests

* provides a default implementation that behaves similarly to legacy `TestcontainersConfiguration` approach (`testcontainers.properties`)

Notes:

* behaviour is similar but not quite identical to `TestcontainersConfiguration`: use of a configured custom image for, e.g. Kafka/Pulsar that does not have a tag specified causes the substitution to take effect for all usages. It seems very unlikely that people would be using a mix of the config file image overrides in some places _and_ specific images specified in code in others.

* Duplication of default image names in modules vs `TestcontainersConfiguration` class is intentional: specifying image overrides in `testcontainers.properties` should be removed in the future.

* ~Add log deprecation warnings when `testcontainers.properties` image overrides are used.~ Defer to a future release?
rnorth added a commit that referenced this pull request Nov 5, 2020
* Refactor Testcontainers configuration to allow config by env var

* Add Image substitution mechanism

Builds upon #3021 and #3411:

* adds a pluggable image substitution mechanism using ServiceLoader, enabling users to perform custom substitution/auditing of images being used by their tests

* provides a default implementation that behaves similarly to legacy `TestcontainersConfiguration` approach (`testcontainers.properties`)

Notes:

* behaviour is similar but not quite identical to `TestcontainersConfiguration`: use of a configured custom image for, e.g. Kafka/Pulsar that does not have a tag specified causes the substitution to take effect for all usages. It seems very unlikely that people would be using a mix of the config file image overrides in some places _and_ specific images specified in code in others.

* Duplication of default image names in modules vs `TestcontainersConfiguration` class is intentional: specifying image overrides in `testcontainers.properties` should be removed in the future.

* ~Add log deprecation warnings when `testcontainers.properties` image overrides are used.~ Defer to a future release?

* Remove extraneous change

* Un-ignore docs example test by implementing a 'reversing' image name substitutor

* Use configuration, not service loader, to select an ImageNameSubstitutor

* Add check for order of config setting precedence

* Extract classpath scanner and support finding of multiple resources

* Introduce deterministic merging of classpath properties files

* Update docs

* Update docs

* Remove service loader reference

* Chain substitution through default and configured implementations

* Small tweaks following review

* Fix test compile error

* Add UnstableAPI annotation

* Move TestSpecificImageNameSubstitutor back to original package and remove duplicate use of default substitutor
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants