4
4
import com .google .common .net .HostAndPort ;
5
5
import lombok .AccessLevel ;
6
6
import lombok .AllArgsConstructor ;
7
- import lombok .Data ;
8
7
import lombok .EqualsAndHashCode ;
8
+ import lombok .With ;
9
9
import org .jetbrains .annotations .NotNull ;
10
+ import org .jetbrains .annotations .Nullable ;
11
+ import org .testcontainers .utility .Versioning .Sha256Versioning ;
12
+ import org .testcontainers .utility .Versioning .TagVersioning ;
10
13
11
14
import java .util .regex .Pattern ;
12
15
13
- @ EqualsAndHashCode (exclude = "rawName" )
16
+ @ EqualsAndHashCode (exclude = { "rawName" , "compatibleSubstituteFor" } )
14
17
@ AllArgsConstructor (access = AccessLevel .PRIVATE )
15
18
public final class DockerImageName {
16
19
17
20
/* Regex patterns used for validation */
18
21
private static final String ALPHA_NUMERIC = "[a-z0-9]+" ;
19
- private static final String SEPARATOR = "([\\ .]{1} |_{1,2}|-+)" ;
22
+ private static final String SEPARATOR = "([.] |_{1,2}|-+)" ;
20
23
private static final String REPO_NAME_PART = ALPHA_NUMERIC + "(" + SEPARATOR + ALPHA_NUMERIC + ")*" ;
21
24
private static final Pattern REPO_NAME = Pattern .compile (REPO_NAME_PART + "(/" + REPO_NAME_PART + ")*" );
22
25
23
26
private final String rawName ;
24
27
private final String registry ;
25
28
private final String repo ;
26
- @ NotNull private final Versioning versioning ;
29
+ @ NotNull @ With (AccessLevel .PRIVATE )
30
+ private final Versioning versioning ;
31
+ @ Nullable @ With (AccessLevel .PRIVATE )
32
+ private final DockerImageName compatibleSubstituteFor ;
27
33
28
34
/**
29
35
* Parses a docker image name from a provided string.
@@ -52,8 +58,8 @@ public DockerImageName(String fullImageName) {
52
58
String remoteName ;
53
59
if (slashIndex == -1 ||
54
60
(!fullImageName .substring (0 , slashIndex ).contains ("." ) &&
55
- !fullImageName .substring (0 , slashIndex ).contains (":" ) &&
56
- !fullImageName .substring (0 , slashIndex ).equals ("localhost" ))) {
61
+ !fullImageName .substring (0 , slashIndex ).contains (":" ) &&
62
+ !fullImageName .substring (0 , slashIndex ).equals ("localhost" ))) {
57
63
registry = "" ;
58
64
remoteName = fullImageName ;
59
65
} else {
@@ -69,8 +75,10 @@ public DockerImageName(String fullImageName) {
69
75
versioning = new TagVersioning (remoteName .split (":" )[1 ]);
70
76
} else {
71
77
repo = remoteName ;
72
- versioning = new TagVersioning ( "latest" ) ;
78
+ versioning = Versioning . ANY ;
73
79
}
80
+
81
+ compatibleSubstituteFor = null ;
74
82
}
75
83
76
84
/**
@@ -92,8 +100,8 @@ public DockerImageName(String nameWithoutTag, @NotNull String version) {
92
100
String remoteName ;
93
101
if (slashIndex == -1 ||
94
102
(!nameWithoutTag .substring (0 , slashIndex ).contains ("." ) &&
95
- !nameWithoutTag .substring (0 , slashIndex ).contains (":" ) &&
96
- !nameWithoutTag .substring (0 , slashIndex ).equals ("localhost" ))) {
103
+ !nameWithoutTag .substring (0 , slashIndex ).contains (":" ) &&
104
+ !nameWithoutTag .substring (0 , slashIndex ).equals ("localhost" ))) {
97
105
registry = "" ;
98
106
remoteName = nameWithoutTag ;
99
107
} else {
@@ -108,6 +116,8 @@ public DockerImageName(String nameWithoutTag, @NotNull String version) {
108
116
repo = remoteName ;
109
117
versioning = new TagVersioning (version );
110
118
}
119
+
120
+ compatibleSubstituteFor = null ;
111
121
}
112
122
113
123
/**
@@ -132,7 +142,7 @@ public String getVersionPart() {
132
142
* @return canonical name for the image
133
143
*/
134
144
public String asCanonicalNameString () {
135
- return getUnversionedPart () + versioning .getSeparator () + versioning . toString ();
145
+ return getUnversionedPart () + versioning .getSeparator () + getVersionPart ();
136
146
}
137
147
138
148
@ Override
@@ -146,7 +156,8 @@ public String toString() {
146
156
* @throws IllegalArgumentException if not valid
147
157
*/
148
158
public void assertValid () {
149
- HostAndPort .fromString (registry );
159
+ //noinspection UnstableApiUsage
160
+ HostAndPort .fromString (registry ); // return value ignored - this throws if registry is not a valid host:port string
150
161
if (!REPO_NAME .matcher (repo ).matches ()) {
151
162
throw new IllegalArgumentException (repo + " is not a valid Docker image name (in " + rawName + ")" );
152
163
}
@@ -159,63 +170,98 @@ public String getRegistry() {
159
170
return registry ;
160
171
}
161
172
173
+ /**
174
+ * @param newTag version tag for the copy to use
175
+ * @return an immutable copy of this {@link DockerImageName} with the new version tag
176
+ */
162
177
public DockerImageName withTag (final String newTag ) {
163
- return new DockerImageName ( rawName , registry , repo , new TagVersioning (newTag ));
178
+ return withVersioning ( new TagVersioning (newTag ));
164
179
}
165
180
166
- private interface Versioning {
167
- boolean isValid ();
168
-
169
- String getSeparator ();
181
+ /**
182
+ * Declare that this {@link DockerImageName} is a compatible substitute for another image - i.e. that this image
183
+ * behaves as the other does, and is compatible with Testcontainers' assumptions about the other image.
184
+ *
185
+ * @param otherImageName the image name of the other image
186
+ * @return an immutable copy of this {@link DockerImageName} with the compatibility declaration attached.
187
+ */
188
+ public DockerImageName asCompatibleSubstituteFor (String otherImageName ) {
189
+ return withCompatibleSubstituteFor (DockerImageName .parse (otherImageName ));
170
190
}
171
191
172
- @ Data
173
- private static class TagVersioning implements Versioning {
174
- public static final String TAG_REGEX = "[\\ w][\\ w\\ .\\ -]{0,127}" ;
175
- private final String tag ;
176
-
177
- TagVersioning (String tag ) {
178
- this .tag = tag ;
179
- }
192
+ /**
193
+ * Declare that this {@link DockerImageName} is a compatible substitute for another image - i.e. that this image
194
+ * behaves as the other does, and is compatible with Testcontainers' assumptions about the other image.
195
+ *
196
+ * @param otherImageName the image name of the other image
197
+ * @return an immutable copy of this {@link DockerImageName} with the compatibility declaration attached.
198
+ */
199
+ public DockerImageName asCompatibleSubstituteFor (DockerImageName otherImageName ) {
200
+ return withCompatibleSubstituteFor (otherImageName );
201
+ }
180
202
181
- @ Override
182
- public boolean isValid () {
183
- return tag .matches (TAG_REGEX );
203
+ /**
204
+ * Test whether this {@link DockerImageName} has declared compatibility with another image (set using
205
+ * {@link DockerImageName#asCompatibleSubstituteFor(String)} or
206
+ * {@link DockerImageName#asCompatibleSubstituteFor(DockerImageName)}.
207
+ * <p>
208
+ * If a version tag part is present in the <code>other</code> image name, the tags must exactly match, unless it
209
+ * is 'latest'. If a version part is not present in the <code>other</code> image name, the tag contents are ignored.
210
+ *
211
+ * @param other the other image that we are trying to test compatibility with
212
+ * @return whether this image has declared compatibility.
213
+ */
214
+ public boolean isCompatibleWith (DockerImageName other ) {
215
+ // is this image already the same or equivalent?
216
+ if (other .equals (this )) {
217
+ return true ;
184
218
}
185
219
186
- @ Override
187
- public String getSeparator () {
188
- return ":" ;
220
+ if (this .compatibleSubstituteFor == null ) {
221
+ return false ;
189
222
}
190
223
191
- @ Override
192
- public String toString () {
193
- return tag ;
194
- }
224
+ return this .compatibleSubstituteFor .isCompatibleWith (other );
195
225
}
196
226
197
- @ Data
198
- private static class Sha256Versioning implements Versioning {
199
- public static final String HASH_REGEX = "[0-9a-fA-F]{32,}" ;
200
- private final String hash ;
201
-
202
- Sha256Versioning (String hash ) {
203
- this .hash = hash ;
204
- }
205
-
206
- @ Override
207
- public boolean isValid () {
208
- return hash .matches (HASH_REGEX );
227
+ /**
228
+ * Behaves as {@link DockerImageName#isCompatibleWith(DockerImageName)} but throws an exception
229
+ * rather than returning false if a mismatch is detected.
230
+ *
231
+ * @param anyOthers the other image(s) that we are trying to check compatibility with. If more
232
+ * than one is provided, this method will check compatibility with at least one
233
+ * of them.
234
+ * @throws IllegalStateException if {@link DockerImageName#isCompatibleWith(DockerImageName)}
235
+ * returns false
236
+ */
237
+ public void assertCompatibleWith (DockerImageName ... anyOthers ) {
238
+ if (anyOthers .length == 0 ) {
239
+ throw new IllegalArgumentException ("anyOthers parameter must be non-empty" );
209
240
}
210
241
211
- @ Override
212
- public String getSeparator () {
213
- return "@" ;
242
+ for (DockerImageName anyOther : anyOthers ) {
243
+ if (this .isCompatibleWith (anyOther )) {
244
+ return ;
245
+ }
214
246
}
215
247
216
- @ Override
217
- public String toString () {
218
- return "sha256:" + hash ;
219
- }
248
+ final DockerImageName exampleOther = anyOthers [0 ];
249
+
250
+ throw new IllegalStateException (
251
+ String .format (
252
+ "Failed to verify that image '%s' is a compatible substitute for '%s'. This generally means that "
253
+ +
254
+ "you are trying to use an image that Testcontainers has not been designed to use. If this is "
255
+ +
256
+ "deliberate, and if you are confident that the image is compatible, you should declare "
257
+ +
258
+ "compatibility in code using the `asCompatibleSubstituteFor` method. For example:\n "
259
+ +
260
+ " DockerImageName myImage = DockerImageName.parse(\" %s\" ).asCompatibleSubstituteFor(\" %s\" );\n "
261
+ +
262
+ "and then use `myImage` instead." ,
263
+ this .rawName , exampleOther .rawName , this .rawName , exampleOther .rawName
264
+ )
265
+ );
220
266
}
221
267
}
0 commit comments