Skip to content
This repository was archived by the owner on May 14, 2025. It is now read-only.

Commit a748017

Browse files
tzolovilayaperumalg
authored andcommitted
Add support for Container image digest reference
- Extend the ContainerImage and ContainerImageParser to allow image digest along with existing image tags. Resolves #4169
1 parent a9eff9f commit a748017

File tree

5 files changed

+97
-24
lines changed

5 files changed

+97
-24
lines changed

spring-cloud-dataflow-configuration-metadata/src/main/java/org/springframework/cloud/dataflow/configuration/metadata/container/ContainerImage.java

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,16 @@
3131
*
3232
* The container image has following structure:
3333
*
34-
* registry-hostname : port / repo-namespace / repo-name : tag
35-
* | REGISTRY-HOST | REPOSITORY | TAG |
34+
* registry-hostname : port / repo-namespace / repo-name : tag|digest
35+
* | REGISTRY-HOST | REPOSITORY | [TAG or DIGEST]|
3636
*
3737
* - The repository namespace is made up of zero or more slash-separated path components (eg. '/ns1/ns2/.../nsN/').
3838
* - The registry hostname (or IP) and the optional port parts together form the REGISTRY HOST. Later is used as a
3939
* unique identifier of the Container Registry hosting this container image. If not explicitly specified, a default
4040
* registry host value is used.
4141
* - The repository namespace together with the repository name form an unique REPOSITORY identifier, unique within
4242
* the REGISTRY HOST.
43-
* - The TAG represents a particular REPOSITORY instance within the REGISTRY HOST.
43+
* - The TAG represents a particular REPOSITORY instance within the REGISTRY HOST. The DIGEST content-addressable identifier.
4444
*
4545
* @author Christian Tzolov
4646
*/
@@ -55,10 +55,13 @@ public class ContainerImage {
5555
// and dashes. A tag name may not start with a period or a dash and may contain a maximum of 128 characters.
5656
// (https://dockr.ly/3chhQZF)
5757
private static final Pattern TAG_PATTERN = Pattern.compile("^[a-zA-Z0-9_][a-zA-Z0-9\\-_.]{0,127}$");
58+
private static final Pattern DIGEST_PATTERN = Pattern.compile("^[A-Za-z][A-Za-z0-9]*(?:[\\-_+.][A-Za-z][A-Za-z0-9]*)*[:][[\\p{XDigit}]]{32,}$");
5859
private static final Pattern HOSTNAME_PATTERN = Pattern.compile("^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])$");
5960
private static final Pattern IP_PATTERN = Pattern.compile("^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$");
6061
private static final Pattern PORT_PATTERN = Pattern.compile("^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$");
6162

63+
enum RepositoryReferenceType {tag, digest, unknown}
64+
6265
/**
6366
* Registry hostname or IP address where the image is stored.
6467
*/
@@ -84,6 +87,10 @@ public class ContainerImage {
8487
*/
8588
private String repositoryTag;
8689

90+
/**
91+
* Repository digest
92+
*/
93+
private String repositoryDigest;
8794

8895
/**
8996
* Helper method that returns the full Registry host address (host:port)
@@ -93,18 +100,18 @@ public String getRegistryHost() {
93100
}
94101

95102
/**
96-
* Helper method that returns the full Repository name (e.g. namespace/registryName) without the tag.
103+
* Helper method that returns the full Repository name (e.g. namespace/registryName) without the tag or digest.
97104
*/
98105
public String getRepository() {
99106
String ns = StringUtils.hasText(this.repositoryNamespace) ? this.repositoryNamespace + "/" : "";
100107
return ns + this.repositoryName;
101108
}
102109

103110
/**
104-
* @return hostname:port/repositoryNamespace/repositoryName:repositoryTag
111+
* @return hostname:port/repositoryNamespace/repositoryName:reference
105112
*/
106113
public String getCanonicalName() {
107-
return getRegistryHost() + "/" + getRepository() + ":" + getRepositoryTag();
114+
return getRegistryHost() + "/" + getRepository() + getReferencePrefix() + getRepositoryReference();
108115
}
109116

110117
// Validated getters and setters
@@ -155,19 +162,52 @@ public void setRepositoryName(String repositoryName) {
155162
this.repositoryName = repositoryName;
156163
}
157164

165+
public String getRepositoryReference() {
166+
return (StringUtils.hasText(this.repositoryTag) ? repositoryTag : repositoryDigest);
167+
}
168+
169+
private String getReferencePrefix() {
170+
if (getRepositoryReferenceType() == RepositoryReferenceType.digest) {
171+
return "@";
172+
}
173+
return ":";
174+
}
175+
176+
public RepositoryReferenceType getRepositoryReferenceType() {
177+
if (StringUtils.hasText(this.repositoryTag)) {
178+
return RepositoryReferenceType.tag;
179+
} if (StringUtils.hasText(this.repositoryDigest)) {
180+
return RepositoryReferenceType.digest;
181+
}
182+
return RepositoryReferenceType.unknown;
183+
}
184+
158185
public String getRepositoryTag() {
159186
return repositoryTag;
160187
}
161188

162189
public void setRepositoryTag(String repositoryTag) {
163-
Assert.isTrue(TAG_PATTERN.matcher(repositoryTag).matches(),
164-
"Invalid repository tag: " + repositoryTag);
190+
Assert.isTrue(TAG_PATTERN.matcher(repositoryTag).matches(), "Invalid repository tag: " + repositoryTag);
191+
Assert.isTrue(!StringUtils.hasText(this.repositoryDigest),
192+
"Can not set repository Tag because of existing Digest " + repositoryDigest);
165193
this.repositoryTag = repositoryTag;
166194
}
167195

196+
public String getRepositoryDigest() {
197+
return repositoryDigest;
198+
}
199+
200+
public void setRepositoryDigest(String repositoryDigest) {
201+
Assert.isTrue(DIGEST_PATTERN.matcher(repositoryDigest).matches(), "Invalid repository digest: " + repositoryDigest);
202+
Assert.isTrue(!StringUtils.hasText(this.repositoryTag),
203+
"Can not set repository digest because of existing tag " + repositoryTag);
204+
this.repositoryDigest = repositoryDigest;
205+
}
206+
168207
@Override
169208
public String toString() {
170209
return "ContainerImage{ host='" + hostname + "', port='" + port + "', namespace='"
171-
+ repositoryNamespace + "', name='" + repositoryName + "', tag='" + repositoryTag + "'}";
210+
+ repositoryNamespace + "', name='" + repositoryName
211+
+ (StringUtils.hasText(repositoryTag) ? "', tag='" + repositoryTag + "'}" : "', digest='" + repositoryDigest + "'}");
172212
}
173213
}

spring-cloud-dataflow-configuration-metadata/src/main/java/org/springframework/cloud/dataflow/configuration/metadata/container/ContainerImageParser.java

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,12 @@ public class ContainerImageParser {
7878
* Characters used to split the image name in parts.
7979
*/
8080
private static final String SLASH_SEPARATOR = "/";
81-
private static final String COLUMN_SEPARATOR = ":";
81+
private static final String PORT_SEPARATOR = ":";
8282
private static final String PERIOD_SEPARATOR = ".";
8383

84+
private static final String TAG_SEPARATOR = ":";
85+
private static final String DIGEST_SEPARATOR = "@";
86+
8487
public ContainerImageParser() {
8588
this(ContainerImageMetadataProperties.DOCKER_HUB_HOST,
8689
ContainerImageMetadataProperties.DEFAULT_TAG,
@@ -94,7 +97,7 @@ public ContainerImageParser(String defaultRegistryHost, String defaultTag, Strin
9497
}
9598

9699
/**
97-
* @param imageName = [registry-host[:port]/] (namespace-path-component/)+ repository-name:tag
100+
* @param imageName = [registry-host[:port]/] (namespace-path-component/)+ repository-name[:tag|@digest]
98101
* @return Returns {@link ContainerImage}
99102
*/
100103
public ContainerImage parse(String imageName) {
@@ -105,7 +108,7 @@ public ContainerImage parse(String imageName) {
105108
String remainder = registryHostAndRemainderSplit[1];
106109

107110
// Registry Host
108-
String[] hostAndPortSplit = registryHost.split(COLUMN_SEPARATOR);
111+
String[] hostAndPortSplit = registryHost.split(PORT_SEPARATOR);
109112
Assert.isTrue(hostAndPortSplit.length > 0 && hostAndPortSplit.length <= 2,
110113
"Invalid registry host address: " + registryHost);
111114
containerImageName.setHostname(hostAndPortSplit[0]);
@@ -118,13 +121,22 @@ public ContainerImage parse(String imageName) {
118121

119122
// Repository name and tag
120123
String repositoryNameAndTag = pathComponents[pathComponents.length - 1];
121-
String[] repositoryNameAndTagSplit = repositoryNameAndTag.split(COLUMN_SEPARATOR);
122-
Assert.isTrue(repositoryNameAndTagSplit.length > 0 && repositoryNameAndTagSplit.length <= 2,
123-
"Invalid repository name: " + repositoryNameAndTag);
124-
containerImageName.setRepositoryName(repositoryNameAndTagSplit[0]);
125124

126-
String repositoryTag = (repositoryNameAndTagSplit.length == 2) ? repositoryNameAndTagSplit[1] : this.defaultTag;
127-
containerImageName.setRepositoryTag(repositoryTag);
125+
if (repositoryNameAndTag.contains(DIGEST_SEPARATOR)) {
126+
String[] repositoryNameAndDigestSplit = repositoryNameAndTag.split(DIGEST_SEPARATOR);
127+
Assert.isTrue(repositoryNameAndDigestSplit.length > 0 && repositoryNameAndDigestSplit.length <= 2,
128+
"Invalid repository name: " + repositoryNameAndTag);
129+
containerImageName.setRepositoryName(repositoryNameAndDigestSplit[0]);
130+
containerImageName.setRepositoryDigest(repositoryNameAndDigestSplit[1]);
131+
} else {
132+
String[] repositoryNameAndTagSplit = repositoryNameAndTag.split(TAG_SEPARATOR);
133+
Assert.isTrue(repositoryNameAndTagSplit.length > 0 && repositoryNameAndTagSplit.length <= 2,
134+
"Invalid repository name: " + repositoryNameAndTag);
135+
containerImageName.setRepositoryName(repositoryNameAndTagSplit[0]);
136+
137+
String repositoryTag = (repositoryNameAndTagSplit.length == 2) ? repositoryNameAndTagSplit[1] : this.defaultTag;
138+
containerImageName.setRepositoryTag(repositoryTag);
139+
}
128140

129141
// Namespace components
130142
if (pathComponents.length >= 2) {
@@ -151,7 +163,7 @@ private String[] splitDockerRegistryHost(String imageName) {
151163
String registryHost;
152164
String remainder;
153165
int i = imageName.indexOf(SLASH_SEPARATOR);
154-
if ((i == -1) || (!(imageName.substring(0, i).contains(PERIOD_SEPARATOR) || imageName.substring(0, i).contains(COLUMN_SEPARATOR))
166+
if ((i == -1) || (!(imageName.substring(0, i).contains(PERIOD_SEPARATOR) || imageName.substring(0, i).contains(PORT_SEPARATOR))
155167
&& !imageName.substring(0, i).equals(LOCALHOST_DOMAIN))) {
156168
// No registry host detected use the default host!
157169
registryHost = this.defaultRegistryHost;

spring-cloud-dataflow-configuration-metadata/src/main/java/org/springframework/cloud/dataflow/configuration/metadata/container/DefaultContainerImageMetadataResolver.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,8 +193,8 @@ private <T> T getImageManifest(RegistryRequest registryRequest, Class<T> respons
193193
.scheme("https")
194194
.host(containerImage.getHostname())
195195
.port(StringUtils.hasText(containerImage.getPort()) ? containerImage.getPort() : null)
196-
.path("v2/{repository}/manifests/{tag}")
197-
.build().expand(containerImage.getRepository(), containerImage.getRepositoryTag());
196+
.path("v2/{repository}/manifests/{reference}")
197+
.build().expand(containerImage.getRepository(), containerImage.getRepositoryReference());
198198

199199
ResponseEntity<T> manifest = registryRequest.getRestTemplate().exchange(manifestUriComponents.toUri(),
200200
HttpMethod.GET, new HttpEntity<>(httpHeaders), responseClassType);

spring-cloud-dataflow-configuration-metadata/src/test/java/org/springframework/cloud/dataflow/configuration/metadata/container/ContainerImageParserTests.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,34 @@ public void testParseWithoutDefaults() {
4040
assertThat(containerImageName.getRepositoryNamespace(), is("scdf/stream"));
4141
assertThat(containerImageName.getRepositoryName(), is("spring-cloud-dataflow-acceptance-image-drivers173"));
4242
assertThat(containerImageName.getRepositoryTag(), is("123"));
43+
assertThat(containerImageName.getRepositoryReference(), is("123"));
44+
assertThat(containerImageName.getRepositoryReferenceType(), is(ContainerImage.RepositoryReferenceType.tag));
4345

4446
assertThat(containerImageName.getRegistryHost(), is("springsource-docker-private-local.jfrog.io:80"));
4547
assertThat(containerImageName.getRepository(), is("scdf/stream/spring-cloud-dataflow-acceptance-image-drivers173"));
4648

4749
assertThat(containerImageName.getCanonicalName(), is("springsource-docker-private-local.jfrog.io:80/scdf/stream/spring-cloud-dataflow-acceptance-image-drivers173:123"));
4850
}
4951

52+
@Test
53+
public void testParseWithoutDigest() {
54+
ContainerImage containerImageName =
55+
containerImageNameParser.parse("springsource-docker-private-local.jfrog.io:80/scdf/stream/spring-cloud-dataflow-acceptance-image-drivers173@sha256:d44e9ac4c4bf53fb0b5424c35c85230a28eb03f24a2ade5bb7f2cc1462846401");
56+
57+
assertThat(containerImageName.getHostname(), is("springsource-docker-private-local.jfrog.io"));
58+
assertThat(containerImageName.getPort(), is("80"));
59+
assertThat(containerImageName.getRepositoryNamespace(), is("scdf/stream"));
60+
assertThat(containerImageName.getRepositoryName(), is("spring-cloud-dataflow-acceptance-image-drivers173"));
61+
assertThat(containerImageName.getRepositoryDigest(), is("sha256:d44e9ac4c4bf53fb0b5424c35c85230a28eb03f24a2ade5bb7f2cc1462846401"));
62+
assertThat(containerImageName.getRepositoryReference(), is("sha256:d44e9ac4c4bf53fb0b5424c35c85230a28eb03f24a2ade5bb7f2cc1462846401"));
63+
assertThat(containerImageName.getRepositoryReferenceType(), is(ContainerImage.RepositoryReferenceType.digest));
64+
65+
assertThat(containerImageName.getRegistryHost(), is("springsource-docker-private-local.jfrog.io:80"));
66+
assertThat(containerImageName.getRepository(), is("scdf/stream/spring-cloud-dataflow-acceptance-image-drivers173"));
67+
68+
assertThat(containerImageName.getCanonicalName(), is("springsource-docker-private-local.jfrog.io:80/scdf/stream/spring-cloud-dataflow-acceptance-image-drivers173@sha256:d44e9ac4c4bf53fb0b5424c35c85230a28eb03f24a2ade5bb7f2cc1462846401"));
69+
}
70+
5071
@Test
5172
public void testParseWithDefaults() {
5273
ContainerImage containerImageName = containerImageNameParser.parse("simple-repo-name");

spring-cloud-dataflow-configuration-metadata/src/test/java/org/springframework/cloud/dataflow/configuration/metadata/container/DefaultContainerImageMetadataResolverTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -209,14 +209,14 @@ public void getImageLabelsWithInvalidLabels() {
209209
}
210210

211211
private void mockManifestRestTemplateCall(Map<String, Object> mapToReturn, String registryHost,
212-
String registryPort, String repository, String tag) {
212+
String registryPort, String repository, String tagOrDigest) {
213213

214214
UriComponents manifestUriComponents = UriComponentsBuilder.newInstance()
215215
.scheme("https")
216216
.host(registryHost)
217217
.port(StringUtils.hasText(registryPort) ? registryPort : null)
218-
.path("v2/{repository}/manifests/{tag}")
219-
.build().expand(repository, tag);
218+
.path("v2/{repository}/manifests/{reference}")
219+
.build().expand(repository, tagOrDigest);
220220

221221

222222
when(mockRestTemplate.exchange(

0 commit comments

Comments
 (0)