-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
Conversation
So that compatibility assurances can be made in code rather than just being assumed.
71fe1b7
to
484f3cd
Compare
@@ -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 |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this 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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this(TestcontainersConfiguration.getInstance().getPulsarDockerImageName().withTag(confluentPlatformVersion)); | |
this(TestcontainersConfiguration.getInstance().getKafkaDockerImageName().withTag(confluentPlatformVersion)); |
Is this constructor missing a test therefore?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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"))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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?
There was a problem hiding this comment.
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() { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed
core/src/test/java/org/testcontainers/utility/DockerImageNameCompatibilityTest.java
Outdated
Show resolved
Hide resolved
} | ||
|
||
@Test | ||
public void testCheckMethodRejectsIncompatible() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
public void testCheckMethodRejectsIncompatible() { | |
public void testAssertMethodRejectsIncompatible() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
Fix gap in testing and docs
…ompatibilityTest.java Co-authored-by: Kevin Wittek <kiview@users.noreply.github.com>
Adapt trim() usage to new code structure
…tespace in property files
@@ -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]); |
There was a problem hiding this comment.
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 :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rnorth is it? 😅
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 implicitlatest
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 aVersioning.Wildcard
type internally instead ofnull
.
WDYT?
There was a problem hiding this comment.
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 ofnull
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 👍
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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:
return new DockerImageName(rawName, registry, repo, versioning, otherImageName); | |
return withOtherImageName(otherImageName); |
There was a problem hiding this comment.
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
All comments responded to (either changed or I think I've explained!)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉 🚀
/azp run |
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"); |
There was a problem hiding this comment.
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"));
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?
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?
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?
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?
* 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
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, becauseconfluentinc/cp-kafka
matches the image name thatKafkaContainer
was designed to work withnew KafkaContainer(DockerImageName.parse("some-other-kafka"))
will not work immediately, becausesome-other-kafka
may be an entirely divergent image fromconfluentinc/cp-kafka
. In this case, the user would be prompted to add.asCompatibleSubstituteFor("confluentinc/cp-kafka")
which tells Testcontainers that this is a conscious decisionThis PR:
Adds to
DockerImageName
:asCompatibleSubstituteFor(DockerImageName)
andasCompatibleSubstituteFor(String)
methods which may be used to claim compatibility with a vendor-provided imageisCompatibleWith(DockerImageName)
andassertCompatibleWith(DockerImageName)
methods which can be used by Testcontainers to check that the provided image is compatible with the expected vendor-provided imageRefactors all modules to use this new mechanism
Amends the
TestcontainersConfiguration
class so that any file-based overrides automatically claim compatibility