Skip to content

Commit 5128571

Browse files
authored
Add DescriptorDigest and ImageReference (#15)
1 parent f6c86da commit 5128571

5 files changed

Lines changed: 675 additions & 0 deletions

File tree

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* Copyright 2018 Google LLC. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
17+
package com.google.cloud.tools.skaffold.image;
18+
19+
import java.security.DigestException;
20+
21+
/**
22+
* Represents a SHA-256 content descriptor digest as defined by the Registry HTTP API v2 reference.
23+
*
24+
* @see <a
25+
* href="https://docs.docker.com/registry/spec/api/#content-digests">https://docs.docker.com/registry/spec/api/#content-digests</a>
26+
* @see <a href="https://github.com/opencontainers/image-spec/blob/master/descriptor.md#digests">OCI
27+
* Content Descriptor Digest</a>
28+
*/
29+
public class DescriptorDigest {
30+
31+
/** Pattern matches a SHA-256 hash - 32 bytes in lowercase hexadecimal. */
32+
private static final String HASH_REGEX = "[a-f0-9]{64}";
33+
34+
/** The algorithm prefix for the digest string. */
35+
private static final String DIGEST_PREFIX = "sha256:";
36+
37+
/** Pattern matches a SHA-256 digest - a SHA-256 hash prefixed with "sha256:". */
38+
static final String DIGEST_REGEX = DIGEST_PREFIX + HASH_REGEX;
39+
40+
private final String hash;
41+
42+
/**
43+
* Creates a new instance from a valid hash string.
44+
*
45+
* @param hash the hash to generate the {@link DescriptorDigest} from
46+
* @return a new {@link DescriptorDigest} created from the hash
47+
* @throws DigestException if the hash is invalid
48+
*/
49+
public static DescriptorDigest fromHash(String hash) throws DigestException {
50+
if (!hash.matches(HASH_REGEX)) {
51+
throw new DigestException("Invalid hash: " + hash);
52+
}
53+
54+
return new DescriptorDigest(hash);
55+
}
56+
57+
/**
58+
* Creates a new instance from a valid digest string.
59+
*
60+
* @param digest the digest to generate the {@link DescriptorDigest} from
61+
* @return a new {@link DescriptorDigest} created from the digest
62+
* @throws DigestException if the digest is invalid
63+
*/
64+
public static DescriptorDigest fromDigest(String digest) throws DigestException {
65+
if (!digest.matches(DIGEST_REGEX)) {
66+
throw new DigestException("Invalid digest: " + digest);
67+
}
68+
69+
// Extracts the hash portion of the digest.
70+
String hash = digest.substring(DIGEST_PREFIX.length());
71+
return new DescriptorDigest(hash);
72+
}
73+
74+
private DescriptorDigest(String hash) {
75+
this.hash = hash;
76+
}
77+
78+
public String getHash() {
79+
return hash;
80+
}
81+
82+
@Override
83+
public String toString() {
84+
return "sha256:" + hash;
85+
}
86+
87+
/** Pass-through hash code of the digest string. */
88+
@Override
89+
public int hashCode() {
90+
return hash.hashCode();
91+
}
92+
93+
/** Two digest objects are equal if their digest strings are equal. */
94+
@Override
95+
public boolean equals(Object obj) {
96+
if (obj instanceof DescriptorDigest) {
97+
return hash.equals(((DescriptorDigest) obj).hash);
98+
}
99+
100+
return false;
101+
}
102+
}
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
/*
2+
* Copyright 2018 Google LLC. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
17+
package com.google.cloud.tools.skaffold.image;
18+
19+
import com.google.common.base.Strings;
20+
import java.util.regex.Matcher;
21+
import java.util.regex.Pattern;
22+
import javax.annotation.Nullable;
23+
24+
/**
25+
* Represents an image reference.
26+
*
27+
* @see <a
28+
* href="https://github.com/docker/distribution/blob/master/reference/reference.go">https://github.com/docker/distribution/blob/master/reference/reference.go</a>
29+
* @see <a
30+
* href="https://docs.docker.com/engine/reference/commandline/tag/#extended-description">https://docs.docker.com/engine/reference/commandline/tag/#extended-description</a>
31+
*/
32+
public class ImageReference {
33+
34+
private static final String DOCKER_HUB_REGISTRY = "registry.hub.docker.com";
35+
private static final String DEFAULT_TAG = "latest";
36+
private static final String LIBRARY_REPOSITORY_PREFIX = "library/";
37+
38+
/**
39+
* Matches all sequences of alphanumeric characters possibly separated by any number of dashes in
40+
* the middle.
41+
*/
42+
private static final String REGISTRY_COMPONENT_REGEX =
43+
"(?:[a-zA-Z\\d]|(?:[a-zA-Z\\d][a-zA-Z\\d-]*[a-zA-Z\\d]))";
44+
45+
/**
46+
* Matches sequences of {@code REGISTRY_COMPONENT_REGEX} separated by a dot, with an optional
47+
* {@code :port} at the end.
48+
*/
49+
private static final String REGISTRY_REGEX =
50+
String.format("%s(?:\\.%s)*(?::\\d+)?", REGISTRY_COMPONENT_REGEX, REGISTRY_COMPONENT_REGEX);
51+
52+
/**
53+
* Matches all sequences of alphanumeric characters separated by a separator.
54+
*
55+
* <p>A separator is either an underscore, a dot, two underscores, or any number of dashes.
56+
*/
57+
private static final String REPOSITORY_COMPONENT_REGEX = "[a-z\\d]+(?:(?:[_.]|__|-+)[a-z\\d]+)*";
58+
59+
/** Matches all repetitions of {@code REPOSITORY_COMPONENT_REGEX} separated by a backslash. */
60+
private static final String REPOSITORY_REGEX =
61+
String.format("(?:%s/)*%s", REPOSITORY_COMPONENT_REGEX, REPOSITORY_COMPONENT_REGEX);
62+
63+
/** Matches a tag of max length 128. */
64+
private static final String TAG_REGEX = "[\\w][\\w.-]{0,127}";
65+
66+
/**
67+
* Matches a full image reference, which is the registry, repository, and tag/digest separated by
68+
* backslashes. The repository is required, but the registry and tag/digest are optional.
69+
*/
70+
private static final String REFERENCE_REGEX =
71+
String.format(
72+
"^(?:(%s)/)?(%s)(?:(?::(%s))|(?:@(%s)))?$",
73+
REGISTRY_REGEX, REPOSITORY_REGEX, TAG_REGEX, DescriptorDigest.DIGEST_REGEX);
74+
75+
private static final Pattern REFERENCE_PATTERN = Pattern.compile(REFERENCE_REGEX);
76+
77+
/**
78+
* @param reference the string to parse
79+
* @return an {@link ImageReference} parsed from the string
80+
* @throws InvalidImageReferenceException if {@code reference} is formatted incorrectly
81+
*/
82+
public static ImageReference parse(String reference) throws InvalidImageReferenceException {
83+
Matcher matcher = REFERENCE_PATTERN.matcher(reference);
84+
85+
if (!matcher.find() || matcher.groupCount() < 4) {
86+
throw new InvalidImageReferenceException(reference);
87+
}
88+
89+
String registry = matcher.group(1);
90+
String repository = matcher.group(2);
91+
String tag = matcher.group(3);
92+
String digest = matcher.group(4);
93+
94+
// If no registry was matched, use Docker Hub by default.
95+
if (Strings.isNullOrEmpty(registry)) {
96+
registry = DOCKER_HUB_REGISTRY;
97+
}
98+
99+
if (Strings.isNullOrEmpty(repository)) {
100+
throw new InvalidImageReferenceException(reference);
101+
}
102+
/*
103+
* If a registry was matched but it does not contain any dots or colons, it should actually be
104+
* part of the repository unless it is "localhost".
105+
*
106+
* See https://github.com/docker/distribution/blob/245ca4659e09e9745f3cc1217bf56e946509220c/reference/normalize.go#L62
107+
*/
108+
if (!registry.contains(".") && !registry.contains(":") && !"localhost".equals(registry)) {
109+
repository = registry + "/" + repository;
110+
registry = DOCKER_HUB_REGISTRY;
111+
}
112+
/*
113+
* For Docker Hub, if the repository is only one component, then it should be prefixed with
114+
* 'library/'.
115+
*
116+
* See https://docs.docker.com/engine/reference/commandline/pull/#pull-an-image-from-docker-hub
117+
*/
118+
if (DOCKER_HUB_REGISTRY.equals(registry) && repository.indexOf('/') < 0) {
119+
repository = LIBRARY_REPOSITORY_PREFIX + repository;
120+
}
121+
122+
if (!Strings.isNullOrEmpty(tag)) {
123+
if (!Strings.isNullOrEmpty(digest)) {
124+
// Cannot have matched both tag and digest.
125+
throw new InvalidImageReferenceException(reference);
126+
}
127+
} else if (!Strings.isNullOrEmpty(digest)) {
128+
tag = digest;
129+
} else {
130+
tag = DEFAULT_TAG;
131+
}
132+
133+
return new ImageReference(registry, repository, tag);
134+
}
135+
136+
/**
137+
* @param registry the image registry
138+
* @param repository the image repository
139+
* @param tag the image tag
140+
* @return an {@link ImageReference} built from the given registry, repository, and tag
141+
*/
142+
public static ImageReference of(
143+
@Nullable String registry, String repository, @Nullable String tag) {
144+
if (Strings.isNullOrEmpty(registry)) {
145+
registry = DOCKER_HUB_REGISTRY;
146+
}
147+
if (Strings.isNullOrEmpty(tag)) {
148+
tag = DEFAULT_TAG;
149+
}
150+
return new ImageReference(registry, repository, tag);
151+
}
152+
153+
/**
154+
* @param registry the registry to check
155+
* @return {@code true} if is a valid registry; {@code false} otherwise
156+
*/
157+
public static boolean isValidRegistry(String registry) {
158+
return registry.matches(REGISTRY_REGEX);
159+
}
160+
161+
/**
162+
* @param repository the repository to check
163+
* @return {@code true} if is a valid repository; {@code false} otherwise
164+
*/
165+
public static boolean isValidRepository(String repository) {
166+
return repository.matches(REPOSITORY_REGEX);
167+
}
168+
169+
/**
170+
* @param tag the tag to check
171+
* @return {@code true} if is a valid tag; {@code false} otherwise
172+
*/
173+
public static boolean isValidTag(String tag) {
174+
return tag.matches(TAG_REGEX);
175+
}
176+
177+
private final String registry;
178+
private final String repository;
179+
private final String tag;
180+
181+
/** Use {@link #parse} to construct. */
182+
private ImageReference(String registry, String repository, String tag) {
183+
this.registry = registry;
184+
this.repository = repository;
185+
this.tag = tag;
186+
}
187+
188+
public String getRegistry() {
189+
return registry;
190+
}
191+
192+
public String getRepository() {
193+
return repository;
194+
}
195+
196+
public String getTag() {
197+
return tag;
198+
}
199+
200+
public boolean usesDefaultTag() {
201+
return DEFAULT_TAG.equals(tag);
202+
}
203+
204+
/** @return the image reference in Docker-readable format (inverse of {@link #parse}) */
205+
@Override
206+
public String toString() {
207+
StringBuilder referenceString = new StringBuilder();
208+
209+
if (!DOCKER_HUB_REGISTRY.equals(registry)) {
210+
// Use registry and repository if not Docker Hub.
211+
referenceString.append(registry).append('/').append(repository);
212+
213+
} else if (repository.startsWith(LIBRARY_REPOSITORY_PREFIX)) {
214+
// If Docker Hub and repository has 'library/' prefix, remove the 'library/' prefix.
215+
referenceString.append(repository.substring(LIBRARY_REPOSITORY_PREFIX.length()));
216+
217+
} else {
218+
// Use just repository if Docker Hub.
219+
referenceString.append(repository);
220+
}
221+
222+
// Use tag if not the default tag.
223+
if (!DEFAULT_TAG.equals(tag)) {
224+
referenceString.append(':').append(tag);
225+
}
226+
227+
return referenceString.toString();
228+
}
229+
230+
/** @return the image reference in Docker-readable format, without hiding the tag. */
231+
public String toStringWithTag() {
232+
return this + (usesDefaultTag() ? ":" + DEFAULT_TAG : "");
233+
}
234+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright 2018 Google LLC. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
17+
package com.google.cloud.tools.skaffold.image;
18+
19+
/** Thrown when attempting to parse an invalid image reference. */
20+
public class InvalidImageReferenceException extends Exception {
21+
22+
public InvalidImageReferenceException(String reference) {
23+
super("Invalid image reference: " + reference);
24+
}
25+
}

0 commit comments

Comments
 (0)