Skip to content
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

Propagate base image's environment variable config to built image #716

Merged
merged 16 commits into from
Jul 26, 2018
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import com.google.common.util.concurrent.ListeningExecutorService;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;

Expand All @@ -39,6 +40,7 @@ class BuildImageStep
private static final String DESCRIPTION = "Building container configuration";

private final BuildConfiguration buildConfiguration;
private final PullBaseImageStep pullBaseImageStep;
private final PullAndCacheBaseImageLayersStep pullAndCacheBaseImageLayersStep;
private final ImmutableList<BuildAndCacheApplicationLayerStep> buildAndCacheApplicationLayerSteps;

Expand All @@ -48,15 +50,18 @@ class BuildImageStep
BuildImageStep(
ListeningExecutorService listeningExecutorService,
BuildConfiguration buildConfiguration,
PullBaseImageStep pullBaseImageStep,
PullAndCacheBaseImageLayersStep pullAndCacheBaseImageLayersStep,
ImmutableList<BuildAndCacheApplicationLayerStep> buildAndCacheApplicationLayerSteps) {
this.listeningExecutorService = listeningExecutorService;
this.buildConfiguration = buildConfiguration;
this.pullBaseImageStep = pullBaseImageStep;
this.pullAndCacheBaseImageLayersStep = pullAndCacheBaseImageLayersStep;
this.buildAndCacheApplicationLayerSteps = buildAndCacheApplicationLayerSteps;

listenableFuture =
Futures.whenAllSucceed(pullAndCacheBaseImageLayersStep.getFuture())
Futures.whenAllSucceed(
pullBaseImageStep.getFuture(), pullAndCacheBaseImageLayersStep.getFuture())
.call(this, listeningExecutorService);
}

Expand Down Expand Up @@ -96,8 +101,16 @@ private Image<CachedLayer> afterCachedLayersSteps()
buildAndCacheApplicationLayerSteps) {
imageBuilder.addLayer(NonBlockingSteps.get(buildAndCacheApplicationLayerStep));
}

// Use environment from base image
imageBuilder.setEnvironment(
NonBlockingSteps.get(pullBaseImageStep).getBaseImage().getEnvironment());
if (buildConfiguration.getEnvironment() != null) {
for (Map.Entry<String, String> entry : buildConfiguration.getEnvironment().entrySet()) {
imageBuilder.setEnvironmentVariable(entry.getKey(), entry.getValue());
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this could be simplified by instead having just a imageBuilder.addEnvironment():

+      // Initialize to environment from base image
+      imageBuilder.addEnvironment(
+          NonBlockingSteps.get(pullBaseImageStep).getBaseImage().getEnvironment());
+      // override with configured environment
+      if (buildConfiguration.getEnvironment() != null) {
+         imageBuilder.addEnvironment(buildConfiguration.getEnvironment());
+      }

}
imageBuilder.setCreated(buildConfiguration.getCreationTime());
imageBuilder.setEnvironment(buildConfiguration.getEnvironment());
imageBuilder.setEntrypoint(buildConfiguration.getEntrypoint());
imageBuilder.setJavaArguments(buildConfiguration.getJavaArguments());
imageBuilder.setExposedPorts(buildConfiguration.getExposedPorts());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import com.google.cloud.tools.jib.registry.RegistryClient;
import com.google.cloud.tools.jib.registry.RegistryException;
import com.google.cloud.tools.jib.registry.RegistryUnauthorizedException;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
Expand All @@ -62,7 +63,8 @@ static class BaseImageWithAuthorization {
private final Image<Layer> baseImage;
private final @Nullable Authorization baseImageAuthorization;

private BaseImageWithAuthorization(
@VisibleForTesting
BaseImageWithAuthorization(
Image<Layer> baseImage, @Nullable Authorization baseImageAuthorization) {
this.baseImage = baseImage;
this.baseImageAuthorization = baseImageAuthorization;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ public StepsRunner runBuildImageStep() {
new BuildImageStep(
listeningExecutorService,
buildConfiguration,
Preconditions.checkNotNull(pullBaseImageStep),
Preconditions.checkNotNull(pullAndCacheBaseImageLayersStep),
Preconditions.checkNotNull(buildAndCacheApplicationLayerSteps));
return this;
Expand Down
28 changes: 8 additions & 20 deletions jib-core/src/main/java/com/google/cloud/tools/jib/image/Image.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import com.google.cloud.tools.jib.configuration.Port;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.time.Instant;
import java.util.List;
import java.util.Map;
Expand All @@ -30,7 +31,7 @@ public class Image<T extends Layer> {
public static class Builder<T extends Layer> {

private final ImageLayers.Builder<T> imageLayersBuilder = ImageLayers.builder();
private ImmutableList.Builder<String> environmentBuilder = ImmutableList.builder();
private ImmutableMap.Builder<String, String> environmentBuilder = ImmutableMap.builder();

@Nullable private Instant created;
@Nullable private ImmutableList<String> entrypoint;
Expand All @@ -56,11 +57,9 @@ public Builder<T> setCreated(Instant created) {
*/
public Builder<T> setEnvironment(@Nullable Map<String, String> environment) {
if (environment == null) {
this.environmentBuilder = ImmutableList.builder();
this.environmentBuilder = ImmutableMap.builder();
} else {
for (Map.Entry<String, String> environmentVariable : environment.entrySet()) {
setEnvironmentVariable(environmentVariable.getKey(), environmentVariable.getValue());
}
this.environmentBuilder.putAll(ImmutableMap.copyOf(environment));
Copy link
Member

@chanseokoh chanseokoh Jul 26, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking this is no different from and just could be environmentBuilder.putAll(environment), am I right? You are just putting references to objects (Strings in this case) into the builder, not like you are doing a deep copy of String instances. (And Strings are immutable, so there is no point of deep copying, actually.) Am I right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds right to me.

}
return this;
}
Expand All @@ -73,18 +72,7 @@ public Builder<T> setEnvironment(@Nullable Map<String, String> environment) {
* @return this
*/
public Builder<T> setEnvironmentVariable(String name, String value) {
environmentBuilder.add(name + "=" + value);
return this;
}

/**
* Adds an environment variable definition in the format {@code NAME=VALUE}.
*
* @param environmentVariableDefinition the definition to add
* @return this
*/
public Builder<T> addEnvironmentVariableDefinition(String environmentVariableDefinition) {
environmentBuilder.add(environmentVariableDefinition);
environmentBuilder.put(name, value);
return this;
}

Expand Down Expand Up @@ -167,7 +155,7 @@ public static <T extends Layer> Builder<T> builder() {
private final ImageLayers<T> layers;

/** Environment variable definitions for running the image, in the format {@code NAME=VALUE}. */
@Nullable private final ImmutableList<String> environment;
@Nullable private final ImmutableMap<String, String> environment;

/** Initial command to run when running the image. */
@Nullable private final ImmutableList<String> entrypoint;
Expand All @@ -181,7 +169,7 @@ public static <T extends Layer> Builder<T> builder() {
private Image(
@Nullable Instant created,
ImageLayers<T> layers,
@Nullable ImmutableList<String> environment,
@Nullable ImmutableMap<String, String> environment,
@Nullable ImmutableList<String> entrypoint,
@Nullable ImmutableList<String> javaArguments,
@Nullable ImmutableList<Port> exposedPorts) {
Expand All @@ -199,7 +187,7 @@ public Instant getCreated() {
}

@Nullable
public ImmutableList<String> getEnvironment() {
public ImmutableMap<String, String> getEnvironment() {
return environment;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import com.google.cloud.tools.jib.image.Image;
import com.google.cloud.tools.jib.json.JsonTemplateMapper;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSortedMap;
import java.lang.reflect.InvocationTargetException;
import java.util.Collections;
Expand Down Expand Up @@ -67,6 +68,24 @@ public class ImageToJsonTranslator {
return result.build();
}

/**
* Converts the map of environment variables to a list with items in the format "NAME=VALUE".
*
* @return the list
*/
@VisibleForTesting
@Nullable
static ImmutableList<String> environmentMapToList(@Nullable Map<String, String> environment) {
if (environment == null) {
return null;
}
ImmutableList.Builder<String> builder = ImmutableList.builder();
for (Map.Entry<String, String> environmentVariable : environment.entrySet()) {
builder.add(environmentVariable.getKey() + "=" + environmentVariable.getValue());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see this PR does not change the current behavior, but I'm just wondering if any kind of escaping should also be considered. Do we have to convert this to a list? Maybe changing some API, we could pass the map and don't worry about escaping at all, although I admit it'd be very rare to have = in a variable name.

Copy link
Contributor Author

@TadCordle TadCordle Jul 25, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I definitely think this can be more bullet proof. Actually I think this breaks if there's an = in the value, not just the name.

Edit: Not this, the opposite conversion in JsonToImageTranslator

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I think we might want to guard this at the environment-setting level - so maybe we might need to have a separate Environment class to be this abstraction (like mentioned in #15)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can handle this when we handle #15 then?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can just throw an IllegalArgumentException for now?

}
return builder.build();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you like streams, you could simplify this a bit:

    Preconditions.checkArgument(
        Iterables.any(environment.keySet(), s -> s.contains("=")),
        "Environment variable name cannot contain '='");
    return environment
        .entrySet()
        .stream()
        .map(entry -> entry.getKey() + "=" + entry.getValue())
        .collect(ImmutableList.toImmutableList());

}

private final Image<CachedLayer> image;

/**
Expand Down Expand Up @@ -96,7 +115,7 @@ public Blob getContainerConfigurationBlob() {
template.setCreated(image.getCreated() == null ? null : image.getCreated().toString());

// Adds the environment variables.
template.setContainerEnvironment(image.getEnvironment());
template.setContainerEnvironment(environmentMapToList(image.getEnvironment()));

// Sets the entrypoint.
template.setContainerEntrypoint(image.getEntrypoint());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ public class JsonToImageTranslator {
*/
private static final Pattern portPattern = Pattern.compile("(\\d+)(?:/(tcp|udp))?");

/**
* Pattern used for parsing environment variables in the format {@code NAME=VALUE}. {@code NAME}
* should start with a letter and contain only letters (uppercase and lower) and numbers. {@code
* VALUE} can contain anything.
*
* <p>Example matches: NAME=VALUE, A12345=$$$$$
*/
@VisibleForTesting static final Pattern environmentPattern = Pattern.compile("([^=].*?)=(.*)");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems weird. ([^=].*?) matches any string that doesn't start with =. I think it's conceptually correct to say ([^=]+).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So as a concrete example,

    Pattern pattern = Pattern.compile("([^=].*?)");
    Matcher matcher = pattern.matcher("ab==c==d");
    matcher.matches();
    System.out.println(matcher.group(1));

It prints out ab==c==d.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I guess within the context of the rest of the expression, a name containing an = would only be detected if it started with an =. But yours makes more sense and is easier to read.


/**
* Translates {@link V21ManifestTemplate} to {@link Image}.
*
Expand Down Expand Up @@ -139,7 +148,14 @@ public static Image<Layer> toImage(

if (containerConfigurationTemplate.getContainerEnvironment() != null) {
for (String environmentVariable : containerConfigurationTemplate.getContainerEnvironment()) {
imageBuilder.addEnvironmentVariableDefinition(environmentVariable);
Matcher matcher = environmentPattern.matcher(environmentVariable);
if (!matcher.matches()) {
throw new BadContainerConfigurationFormatException(
"Invalid environment variable definition: " + environmentVariable);
}
String name = matcher.group(1);
String value = matcher.group(2);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When patterns are far away from their use, it can help to use a named group (e.g., matcher.group("name") and matcher.group("value")).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 We should probably do this for some of the other regex patterns too.

imageBuilder.setEnvironmentVariable(name, value);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import com.google.cloud.tools.jib.configuration.BuildConfiguration;
import com.google.cloud.tools.jib.image.DescriptorDigest;
import com.google.cloud.tools.jib.image.Image;
import com.google.cloud.tools.jib.image.Layer;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.util.concurrent.Futures;
Expand All @@ -46,6 +47,7 @@ public class BuildImageStepTest {

@Mock private BuildConfiguration mockBuildConfiguration;
@Mock private BuildLogger mockBuildLogger;
@Mock private PullBaseImageStep mockPullBaseImageStep;
@Mock private PullAndCacheBaseImageLayersStep mockPullAndCacheBaseImageLayersStep;
@Mock private PullAndCacheBaseImageLayerStep mockPullAndCacheBaseImageLayerStep;
@Mock private BuildAndCacheApplicationLayerStep mockBuildAndCacheApplicationLayerStep;
Expand All @@ -70,15 +72,21 @@ public void setUp() throws DigestException {
Mockito.when(mockBuildConfiguration.getExposedPorts()).thenReturn(ImmutableList.of());
Mockito.when(mockBuildConfiguration.getEntrypoint()).thenReturn(ImmutableList.of());

Image<Layer> baseImage =
Image.builder().setEnvironment(ImmutableMap.of("NAME", "VALUE")).build();
Mockito.when(mockPullAndCacheBaseImageLayerStep.getFuture())
.thenReturn(Futures.immediateFuture(testCachedLayer));
Mockito.when(mockPullAndCacheBaseImageLayersStep.getFuture())
.thenReturn(
Futures.immediateFuture(
ImmutableList.of(
mockPullAndCacheBaseImageLayerStep,
mockPullAndCacheBaseImageLayerStep,
mockPullAndCacheBaseImageLayerStep)));
Mockito.when(mockPullAndCacheBaseImageLayerStep.getFuture())
.thenReturn(Futures.immediateFuture(testCachedLayer));
Mockito.when(mockPullBaseImageStep.getFuture())
.thenReturn(
Futures.immediateFuture(
new PullBaseImageStep.BaseImageWithAuthorization(baseImage, null)));
Mockito.when(mockBuildAndCacheApplicationLayerStep.getFuture())
.thenReturn(Futures.immediateFuture(testCachedLayer));
}
Expand All @@ -89,6 +97,7 @@ public void test_validateAsyncDependencies() throws ExecutionException, Interrup
new BuildImageStep(
MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()),
mockBuildConfiguration,
mockPullBaseImageStep,
mockPullAndCacheBaseImageLayersStep,
ImmutableList.of(
mockBuildAndCacheApplicationLayerStep,
Expand All @@ -98,4 +107,23 @@ public void test_validateAsyncDependencies() throws ExecutionException, Interrup
Assert.assertEquals(
testDescriptorDigest, image.getLayers().asList().get(0).getBlobDescriptor().getDigest());
}

@Test
public void test_propagateBaseImageConfiguration()
throws ExecutionException, InterruptedException {
Mockito.when(mockBuildConfiguration.getEnvironment())
.thenReturn(ImmutableMap.of("BASE", "IMAGE"));
BuildImageStep buildImageStep =
new BuildImageStep(
MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()),
mockBuildConfiguration,
mockPullBaseImageStep,
mockPullAndCacheBaseImageLayersStep,
ImmutableList.of(
mockBuildAndCacheApplicationLayerStep,
mockBuildAndCacheApplicationLayerStep,
mockBuildAndCacheApplicationLayerStep));
Image<CachedLayer> image = buildImageStep.getFuture().get().getFuture().get();
Assert.assertEquals(ImmutableMap.of("NAME", "VALUE", "BASE", "IMAGE"), image.getEnvironment());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import com.google.cloud.tools.jib.configuration.Port;
import com.google.cloud.tools.jib.configuration.Port.Protocol;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.time.Instant;
import java.util.Arrays;
import org.junit.Assert;
Expand All @@ -45,9 +46,6 @@ public void setUp() throws LayerPropertyNotFoundException {

@Test
public void test_smokeTest() throws LayerPropertyNotFoundException {
ImmutableList<String> expectedEnvironment =
ImmutableList.of("crepecake=is great", "VARIABLE=VALUE");

Image<Layer> image =
Image.builder()
.setCreated(Instant.ofEpochSecond(10000))
Expand All @@ -63,7 +61,8 @@ public void test_smokeTest() throws LayerPropertyNotFoundException {
Assert.assertEquals(
mockDescriptorDigest, image.getLayers().get(0).getBlobDescriptor().getDigest());
Assert.assertEquals(Instant.ofEpochSecond(10000), image.getCreated());
Assert.assertEquals(expectedEnvironment, image.getEnvironment());
Assert.assertEquals(
ImmutableMap.of("crepecake", "is great", "VARIABLE", "VALUE"), image.getEnvironment());
Assert.assertEquals(Arrays.asList("some", "command"), image.getEntrypoint());
Assert.assertEquals(Arrays.asList("arg1", "arg2"), image.getJavaArguments());
Assert.assertEquals(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,13 @@ public void testPortListToMap() {
Assert.assertEquals(expected, ImageToJsonTranslator.portListToMap(input));
}

@Test
public void testEnvironmentMapToList() {
ImmutableMap<String, String> input = ImmutableMap.of("NAME1", "VALUE1", "NAME2", "VALUE2");
ImmutableList<String> expected = ImmutableList.of("NAME1=VALUE1", "NAME2=VALUE2");
Assert.assertEquals(expected, ImageToJsonTranslator.environmentMapToList(input));
}

/** Tests translation of image to {@link BuildableManifestTemplate}. */
private <T extends BuildableManifestTemplate> void testGetManifest(
Class<T> manifestTemplateClass, String translatedJsonFilename)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import org.junit.Assert;
import org.junit.Test;

Expand Down Expand Up @@ -110,6 +111,31 @@ public void testPortMapToList() throws BadContainerConfigurationFormatException
}
}

@Test
public void testJsonToImageTranslatorRegex() {
assertGoodEnvironmentPattern("NAME=VALUE", "NAME", "VALUE");
assertGoodEnvironmentPattern("A1203921=www=ww", "A1203921", "www=ww");
assertGoodEnvironmentPattern("&*%(&#$(*@(%&@$*$(=", "&*%(&#$(*@(%&@$*$(", "");
assertGoodEnvironmentPattern("m_a_8943=100", "m_a_8943", "100");
assertGoodEnvironmentPattern("A_B_C_D=*****", "A_B_C_D", "*****");

assertBadEnvironmentPattern("=================");
assertBadEnvironmentPattern("A_B_C");
}

private void assertGoodEnvironmentPattern(
String input, String expectedName, String expectedValue) {
Matcher matcher = JsonToImageTranslator.environmentPattern.matcher(input);
Assert.assertTrue(matcher.matches());
Assert.assertEquals(expectedName, matcher.group(1));
Assert.assertEquals(expectedValue, matcher.group(2));
}

private void assertBadEnvironmentPattern(String input) {
Matcher matcher = JsonToImageTranslator.environmentPattern.matcher(input);
Assert.assertFalse(matcher.matches());
}

private <T extends BuildableManifestTemplate> void testToImage_buildable(
String jsonFilename, Class<T> manifestTemplateClass)
throws IOException, LayerPropertyNotFoundException, LayerCountMismatchException,
Expand Down Expand Up @@ -144,7 +170,7 @@ private <T extends BuildableManifestTemplate> void testToImage_buildable(
layers.get(0).getDiffId());
Assert.assertEquals(Instant.ofEpochSecond(20), image.getCreated());
Assert.assertEquals(Arrays.asList("some", "entrypoint", "command"), image.getEntrypoint());
Assert.assertEquals(Arrays.asList("VAR1=VAL1", "VAR2=VAL2"), image.getEnvironment());
Assert.assertEquals(ImmutableMap.of("VAR1", "VAL1", "VAR2", "VAL2"), image.getEnvironment());
Assert.assertEquals(
ImmutableList.of(
new Port(1000, Protocol.TCP),
Expand Down
Loading