Improve error handling when builder image isn't a builder
Fixes gh-22179
This commit is contained in:
parent
0e1ded6893
commit
9317135690
|
|
@ -26,6 +26,7 @@ import org.springframework.util.StringUtils;
|
||||||
* The {@link Owner} that should perform the build.
|
* The {@link Owner} that should perform the build.
|
||||||
*
|
*
|
||||||
* @author Phillip Webb
|
* @author Phillip Webb
|
||||||
|
* @author Andy Wilkinson
|
||||||
*/
|
*/
|
||||||
class BuildOwner implements Owner {
|
class BuildOwner implements Owner {
|
||||||
|
|
||||||
|
|
@ -49,13 +50,14 @@ class BuildOwner implements Owner {
|
||||||
|
|
||||||
private long getValue(Map<String, String> env, String name) {
|
private long getValue(Map<String, String> env, String name) {
|
||||||
String value = env.get(name);
|
String value = env.get(name);
|
||||||
Assert.state(StringUtils.hasText(value), () -> "Missing '" + name + "' value from the builder environment");
|
Assert.state(StringUtils.hasText(value),
|
||||||
|
() -> "Missing '" + name + "' value from the builder environment '" + env + "'");
|
||||||
try {
|
try {
|
||||||
return Long.parseLong(value);
|
return Long.parseLong(value);
|
||||||
}
|
}
|
||||||
catch (NumberFormatException ex) {
|
catch (NumberFormatException ex) {
|
||||||
throw new IllegalStateException("Malformed '" + name + "' value '" + value + "' in the builder environment",
|
throw new IllegalStateException(
|
||||||
ex);
|
"Malformed '" + name + "' value '" + value + "' in the builder environment '" + env + "'", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ package org.springframework.boot.buildpack.platform.build;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.invoke.MethodHandles;
|
import java.lang.invoke.MethodHandles;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
|
@ -30,11 +29,13 @@ import org.springframework.boot.buildpack.platform.docker.type.ImageConfig;
|
||||||
import org.springframework.boot.buildpack.platform.json.MappedObject;
|
import org.springframework.boot.buildpack.platform.json.MappedObject;
|
||||||
import org.springframework.boot.buildpack.platform.json.SharedObjectMapper;
|
import org.springframework.boot.buildpack.platform.json.SharedObjectMapper;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builder metadata information.
|
* Builder metadata information.
|
||||||
*
|
*
|
||||||
* @author Phillip Webb
|
* @author Phillip Webb
|
||||||
|
* @author Andy Wilkinson
|
||||||
*/
|
*/
|
||||||
class BuilderMetadata extends MappedObject {
|
class BuilderMetadata extends MappedObject {
|
||||||
|
|
||||||
|
|
@ -121,9 +122,9 @@ class BuilderMetadata extends MappedObject {
|
||||||
*/
|
*/
|
||||||
static BuilderMetadata fromImageConfig(ImageConfig imageConfig) throws IOException {
|
static BuilderMetadata fromImageConfig(ImageConfig imageConfig) throws IOException {
|
||||||
Assert.notNull(imageConfig, "ImageConfig must not be null");
|
Assert.notNull(imageConfig, "ImageConfig must not be null");
|
||||||
Map<String, String> labels = imageConfig.getLabels();
|
String json = imageConfig.getLabels().get(LABEL_NAME);
|
||||||
String json = (labels != null) ? labels.get(LABEL_NAME) : null;
|
Assert.notNull(json, () -> "No '" + LABEL_NAME + "' label found in image config labels '"
|
||||||
Assert.notNull(json, () -> "No '" + LABEL_NAME + "' label found in image config");
|
+ StringUtils.collectionToCommaDelimitedString(imageConfig.getLabels().keySet()) + "'");
|
||||||
return fromJson(json);
|
return fromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,6 @@
|
||||||
|
|
||||||
package org.springframework.boot.buildpack.platform.build;
|
package org.springframework.boot.buildpack.platform.build;
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import org.springframework.boot.buildpack.platform.docker.type.Image;
|
import org.springframework.boot.buildpack.platform.docker.type.Image;
|
||||||
import org.springframework.boot.buildpack.platform.docker.type.ImageConfig;
|
import org.springframework.boot.buildpack.platform.docker.type.ImageConfig;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
|
@ -27,6 +25,7 @@ import org.springframework.util.StringUtils;
|
||||||
* A Stack ID.
|
* A Stack ID.
|
||||||
*
|
*
|
||||||
* @author Phillip Webb
|
* @author Phillip Webb
|
||||||
|
* @author Andy Wilkinson
|
||||||
*/
|
*/
|
||||||
class StackId {
|
class StackId {
|
||||||
|
|
||||||
|
|
@ -75,8 +74,7 @@ class StackId {
|
||||||
* @return the extracted stack ID
|
* @return the extracted stack ID
|
||||||
*/
|
*/
|
||||||
private static StackId fromImageConfig(ImageConfig imageConfig) {
|
private static StackId fromImageConfig(ImageConfig imageConfig) {
|
||||||
Map<String, String> labels = imageConfig.getLabels();
|
String value = imageConfig.getLabels().get(LABEL_NAME);
|
||||||
String value = (labels != null) ? labels.get(LABEL_NAME) : null;
|
|
||||||
Assert.state(StringUtils.hasText(value), () -> "Missing '" + LABEL_NAME + "' stack label");
|
Assert.state(StringUtils.hasText(value), () -> "Missing '" + LABEL_NAME + "' stack label");
|
||||||
return new StackId(value);
|
return new StackId(value);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import org.springframework.boot.buildpack.platform.json.MappedObject;
|
||||||
* Image configuration information.
|
* Image configuration information.
|
||||||
*
|
*
|
||||||
* @author Phillip Webb
|
* @author Phillip Webb
|
||||||
|
* @author Andy Wilkinson
|
||||||
* @since 2.3.0
|
* @since 2.3.0
|
||||||
*/
|
*/
|
||||||
public class ImageConfig extends MappedObject {
|
public class ImageConfig extends MappedObject {
|
||||||
|
|
@ -39,16 +40,27 @@ public class ImageConfig extends MappedObject {
|
||||||
|
|
||||||
private final Map<String, String> configEnv;
|
private final Map<String, String> configEnv;
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
ImageConfig(JsonNode node) {
|
ImageConfig(JsonNode node) {
|
||||||
super(node, MethodHandles.lookup());
|
super(node, MethodHandles.lookup());
|
||||||
this.labels = valueAt("/Labels", Map.class);
|
this.labels = extractLabels();
|
||||||
this.configEnv = parseConfigEnv();
|
this.configEnv = parseConfigEnv();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private Map<String, String> extractLabels() {
|
||||||
|
Map<String, String> labels = valueAt("/Labels", Map.class);
|
||||||
|
if (labels == null) {
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
|
||||||
private Map<String, String> parseConfigEnv() {
|
private Map<String, String> parseConfigEnv() {
|
||||||
Map<String, String> env = new LinkedHashMap<>();
|
|
||||||
String[] entries = valueAt("/Env", String[].class);
|
String[] entries = valueAt("/Env", String[].class);
|
||||||
|
if (entries == null) {
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
Map<String, String> env = new LinkedHashMap<>();
|
||||||
for (String entry : entries) {
|
for (String entry : entries) {
|
||||||
int i = entry.indexOf('=');
|
int i = entry.indexOf('=');
|
||||||
String name = (i != -1) ? entry.substring(0, i) : entry;
|
String name = (i != -1) ? entry.substring(0, i) : entry;
|
||||||
|
|
@ -63,16 +75,18 @@ public class ImageConfig extends MappedObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the image labels.
|
* Return the image labels. If the image has no labels, an empty {@code Map} is
|
||||||
* @return the image labels
|
* returned.
|
||||||
|
* @return the image labels, never {@code null}
|
||||||
*/
|
*/
|
||||||
public Map<String, String> getLabels() {
|
public Map<String, String> getLabels() {
|
||||||
return this.labels;
|
return this.labels;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the image environment variables.
|
* Return the image environment variables. If the image has no environment variables,
|
||||||
* @return the env
|
* an empty {@code Map} is returned.
|
||||||
|
* @return the env, never {@code null}
|
||||||
*/
|
*/
|
||||||
public Map<String, String> getEnv() {
|
public Map<String, String> getEnv() {
|
||||||
return this.configEnv;
|
return this.configEnv;
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||||
* Tests for {@link BuildOwner}.
|
* Tests for {@link BuildOwner}.
|
||||||
*
|
*
|
||||||
* @author Phillip Webb
|
* @author Phillip Webb
|
||||||
|
* @author Andy Wilkinson
|
||||||
*/
|
*/
|
||||||
class BuildOwnerTests {
|
class BuildOwnerTests {
|
||||||
|
|
||||||
|
|
@ -54,7 +55,7 @@ class BuildOwnerTests {
|
||||||
Map<String, String> env = new LinkedHashMap<>();
|
Map<String, String> env = new LinkedHashMap<>();
|
||||||
env.put("CNB_GROUP_ID", "456");
|
env.put("CNB_GROUP_ID", "456");
|
||||||
assertThatIllegalStateException().isThrownBy(() -> BuildOwner.fromEnv(env))
|
assertThatIllegalStateException().isThrownBy(() -> BuildOwner.fromEnv(env))
|
||||||
.withMessage("Missing 'CNB_USER_ID' value from the builder environment");
|
.withMessage("Missing 'CNB_USER_ID' value from the builder environment '" + env + "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -62,7 +63,7 @@ class BuildOwnerTests {
|
||||||
Map<String, String> env = new LinkedHashMap<>();
|
Map<String, String> env = new LinkedHashMap<>();
|
||||||
env.put("CNB_USER_ID", "123");
|
env.put("CNB_USER_ID", "123");
|
||||||
assertThatIllegalStateException().isThrownBy(() -> BuildOwner.fromEnv(env))
|
assertThatIllegalStateException().isThrownBy(() -> BuildOwner.fromEnv(env))
|
||||||
.withMessage("Missing 'CNB_GROUP_ID' value from the builder environment");
|
.withMessage("Missing 'CNB_GROUP_ID' value from the builder environment '" + env + "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -71,7 +72,7 @@ class BuildOwnerTests {
|
||||||
env.put("CNB_USER_ID", "nope");
|
env.put("CNB_USER_ID", "nope");
|
||||||
env.put("CNB_GROUP_ID", "456");
|
env.put("CNB_GROUP_ID", "456");
|
||||||
assertThatIllegalStateException().isThrownBy(() -> BuildOwner.fromEnv(env))
|
assertThatIllegalStateException().isThrownBy(() -> BuildOwner.fromEnv(env))
|
||||||
.withMessage("Malformed 'CNB_USER_ID' value 'nope' in the builder environment");
|
.withMessage("Malformed 'CNB_USER_ID' value 'nope' in the builder environment '" + env + "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -80,7 +81,7 @@ class BuildOwnerTests {
|
||||||
env.put("CNB_USER_ID", "123");
|
env.put("CNB_USER_ID", "123");
|
||||||
env.put("CNB_GROUP_ID", "nope");
|
env.put("CNB_GROUP_ID", "nope");
|
||||||
assertThatIllegalStateException().isThrownBy(() -> BuildOwner.fromEnv(env))
|
assertThatIllegalStateException().isThrownBy(() -> BuildOwner.fromEnv(env))
|
||||||
.withMessage("Malformed 'CNB_GROUP_ID' value 'nope' in the builder environment");
|
.withMessage("Malformed 'CNB_GROUP_ID' value 'nope' in the builder environment '" + env + "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
package org.springframework.boot.buildpack.platform.build;
|
package org.springframework.boot.buildpack.platform.build;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
|
@ -34,6 +35,7 @@ import static org.mockito.Mockito.mock;
|
||||||
*
|
*
|
||||||
* @author Phillip Webb
|
* @author Phillip Webb
|
||||||
* @author Scott Frederick
|
* @author Scott Frederick
|
||||||
|
* @author Andy Wilkinson
|
||||||
*/
|
*/
|
||||||
class BuilderMetadataTests extends AbstractJsonTests {
|
class BuilderMetadataTests extends AbstractJsonTests {
|
||||||
|
|
||||||
|
|
@ -69,8 +71,9 @@ class BuilderMetadataTests extends AbstractJsonTests {
|
||||||
Image image = mock(Image.class);
|
Image image = mock(Image.class);
|
||||||
ImageConfig imageConfig = mock(ImageConfig.class);
|
ImageConfig imageConfig = mock(ImageConfig.class);
|
||||||
given(image.getConfig()).willReturn(imageConfig);
|
given(image.getConfig()).willReturn(imageConfig);
|
||||||
|
given(imageConfig.getLabels()).willReturn(Collections.singletonMap("alpha", "a"));
|
||||||
assertThatIllegalArgumentException().isThrownBy(() -> BuilderMetadata.fromImage(image))
|
assertThatIllegalArgumentException().isThrownBy(() -> BuilderMetadata.fromImage(image))
|
||||||
.withMessage("No 'io.buildpacks.builder.metadata' label found in image config");
|
.withMessage("No 'io.buildpacks.builder.metadata' label found in image config labels 'alpha'");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import static org.assertj.core.api.Assertions.entry;
|
||||||
* Tests for {@link ImageConfig}.
|
* Tests for {@link ImageConfig}.
|
||||||
*
|
*
|
||||||
* @author Phillip Webb
|
* @author Phillip Webb
|
||||||
|
* @author Andy Wilkinson
|
||||||
*/
|
*/
|
||||||
class ImageConfigTests extends AbstractJsonTests {
|
class ImageConfigTests extends AbstractJsonTests {
|
||||||
|
|
||||||
|
|
@ -42,6 +43,20 @@ class ImageConfigTests extends AbstractJsonTests {
|
||||||
entry("CNB_STACK_ID", "org.cloudfoundry.stacks.cflinuxfs3"));
|
entry("CNB_STACK_ID", "org.cloudfoundry.stacks.cflinuxfs3"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void whenConfigHasNoEnvThenImageConfigEnvIsEmpty() throws Exception {
|
||||||
|
ImageConfig imageConfig = getMinimalImageConfig();
|
||||||
|
Map<String, String> env = imageConfig.getEnv();
|
||||||
|
assertThat(env).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void whenConfigHasNoLabelsThenImageConfigLabelsIsEmpty() throws Exception {
|
||||||
|
ImageConfig imageConfig = getMinimalImageConfig();
|
||||||
|
Map<String, String> env = imageConfig.getLabels();
|
||||||
|
assertThat(env).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getLabelsReturnsLabels() throws Exception {
|
void getLabelsReturnsLabels() throws Exception {
|
||||||
ImageConfig imageConfig = getImageConfig();
|
ImageConfig imageConfig = getImageConfig();
|
||||||
|
|
@ -63,4 +78,8 @@ class ImageConfigTests extends AbstractJsonTests {
|
||||||
return new ImageConfig(getObjectMapper().readTree(getContent("image-config.json")));
|
return new ImageConfig(getObjectMapper().readTree(getContent("image-config.json")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ImageConfig getMinimalImageConfig() throws IOException {
|
||||||
|
return new ImageConfig(getObjectMapper().readTree(getContent("minimal-image-config.json")));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"Hostname": "",
|
||||||
|
"Domainname": "",
|
||||||
|
"User": "",
|
||||||
|
"AttachStdin": false,
|
||||||
|
"AttachStdout": false,
|
||||||
|
"AttachStderr": false,
|
||||||
|
"Tty": false,
|
||||||
|
"OpenStdin": false,
|
||||||
|
"StdinOnce": false,
|
||||||
|
"Env": null,
|
||||||
|
"Cmd": null,
|
||||||
|
"Image": "",
|
||||||
|
"Volumes": null,
|
||||||
|
"WorkingDir": "",
|
||||||
|
"Entrypoint": null,
|
||||||
|
"OnBuild": null,
|
||||||
|
"Labels": null
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue