diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2e259152..0e801e17 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,8 @@
## HEAD (Unreleased)
+* `docker.buildAndPushImage` will now build images during a `preview`, not just during an `update`.
+ This allows docker errors to be found earlier and more safely in the development cycle.
+
## 1.2.0 (2020-01-29)
* Upgrade to pulumi-terraform-bridge v1.6.4
diff --git a/examples/broken_dockerfile/Pulumi.yaml b/examples/broken_dockerfile/Pulumi.yaml
new file mode 100644
index 00000000..ac850127
--- /dev/null
+++ b/examples/broken_dockerfile/Pulumi.yaml
@@ -0,0 +1,5 @@
+name: broken
+runtime: nodejs
+description: A minimal TypeScript Pulumi program
+template:
+ description: A minimal TypeScript Pulumi program
diff --git a/examples/broken_dockerfile/app/Dockerfile b/examples/broken_dockerfile/app/Dockerfile
new file mode 100644
index 00000000..3c4e99ee
--- /dev/null
+++ b/examples/broken_dockerfile/app/Dockerfile
@@ -0,0 +1,2 @@
+FR OM nginx
+COPY content /usr/share/nginx/html
\ No newline at end of file
diff --git a/examples/broken_dockerfile/app/content/index.html b/examples/broken_dockerfile/app/content/index.html
new file mode 100644
index 00000000..22cf0726
--- /dev/null
+++ b/examples/broken_dockerfile/app/content/index.html
@@ -0,0 +1 @@
+
Hi from Pulumi
\ No newline at end of file
diff --git a/examples/broken_dockerfile/index.ts b/examples/broken_dockerfile/index.ts
new file mode 100644
index 00000000..aeb17240
--- /dev/null
+++ b/examples/broken_dockerfile/index.ts
@@ -0,0 +1,19 @@
+// Copyright 2016-2018, Pulumi Corporation.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import * as docker from "@pulumi/docker";
+
+// This should fail during preview as ./app points at a broken docker file.
+export const imageName = docker.buildAndPushImage(
+ "test-name", "./app", /*repositoryUrl:*/ undefined, /*logResource:*/ undefined!);
diff --git a/examples/broken_dockerfile/package.json b/examples/broken_dockerfile/package.json
new file mode 100644
index 00000000..62e6c1a5
--- /dev/null
+++ b/examples/broken_dockerfile/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "broken",
+ "devDependencies": {
+ "@types/node": "^8.0.0"
+ },
+ "dependencies": {
+ "@pulumi/pulumi": "dev",
+ }
+}
diff --git a/examples/examples_test.go b/examples/examples_test.go
index f03b79f4..a1fe8a92 100644
--- a/examples/examples_test.go
+++ b/examples/examples_test.go
@@ -66,6 +66,23 @@ func TestAws(t *testing.T) {
}
}
+func TestBrokenDockerfile(t *testing.T) {
+ cwd, err := os.Getwd()
+ if !assert.NoError(t, err) {
+ t.FailNow()
+ }
+
+ opts := base.With(integration.ProgramTestOptions{
+ Dependencies: []string{
+ "@pulumi/docker",
+ },
+ Dir: path.Join(cwd, "broken_dockerfile"),
+ ExpectFailure: true,
+ SkipRefresh: true,
+ })
+ integration.ProgramTest(t, &opts)
+}
+
func TestNginx(t *testing.T) {
cwd, err := os.Getwd()
if !assert.NoError(t, err) {
@@ -139,4 +156,4 @@ func TestDockerfileWithMultipleTargets(t *testing.T) {
Dir: path.Join(cwd, "dockerfile-with-targets"),
})
integration.ProgramTest(t, &opts)
-}
\ No newline at end of file
+}
diff --git a/sdk/nodejs/docker.ts b/sdk/nodejs/docker.ts
index a055a895..377e2d2a 100644
--- a/sdk/nodejs/docker.ts
+++ b/sdk/nodejs/docker.ts
@@ -138,9 +138,10 @@ export function buildAndPushImageAsync(
repositoryUrl: pulumi.Input,
logResource: pulumi.Resource,
connectToRegistry?: () => pulumi.Input,
- skipPush: boolean = false): Promise {
+ skipPush: boolean = false,
+ runDuringPreview: boolean = true): Promise {
- const output = buildAndPushImage(baseImageName, pathOrBuild, repositoryUrl, logResource, connectToRegistry, skipPush);
+ const output = buildAndPushImage(baseImageName, pathOrBuild, repositoryUrl, logResource, connectToRegistry, skipPush, runDuringPreview);
// Ugly, but necessary to bridge between the proper Output-returning function and this
// Promise-returning one.
@@ -157,28 +158,32 @@ export function buildAndPushImageAsync(
export function buildAndPushImage(
imageName: string,
pathOrBuild: pulumi.Input,
- repositoryUrl: pulumi.Input,
+ repositoryUrl: pulumi.Input | undefined,
logResource: pulumi.Resource,
connectToRegistry?: () => pulumi.Input,
- skipPush: boolean = false): pulumi.Output {
-
- return pulumi.all([pathOrBuild, repositoryUrl])
- .apply(async ([pathOrBuildVal, repositoryUrlVal]) => {
+ skipPush: boolean = false,
+ runDuringPreview: boolean = true): pulumi.Output {
- // Give an initial message indicating what we're about to do. That way, if anything
- // takes a while, the user has an idea about what's going on.
- logEphemeral("Starting docker build and push...", logResource);
+ // We do something rather interesting here. We do not want to proceed if we don't actually have
+ // a value yet for `pathOrBuild`. So we do a normal `ouput(...).apply(...)`. However, we *do*
+ // want proceed if we don't have a value yet for `repositoryUrl`. In that case, we'll just
+ // build without actually pushing. To support that, we run `.apply` on the repoUrl, but we pass
+ // in `runWithUnknowns:true` to actually continue on in that case.
+ return pulumi.output(pathOrBuild).apply(pathOrBuild => {
+ const op = pulumi.output(repositoryUrl);
- const result = await buildAndPushImageWorkerAsync(
- imageName, pathOrBuildVal, repositoryUrlVal, logResource, connectToRegistry, skipPush);
+ // @ts-ignore Allow calling the 'runWithUnknowns' overload.
+ const res: pulumi.Output = op.apply(u => helper(pathOrBuild, u), /*runWithUnknowns:*/ runDuringPreview);
- // If we got here, then building/pushing didn't throw any errors. Update the status bar
- // indicating that things worked properly. That way, the info bar isn't stuck showing the very
- // last thing printed by some subcommand we launched.
- logEphemeral("Successfully pushed to docker", logResource);
+ return res;
+ });
- return result;
- });
+ function helper(pathOrBuild: string | pulumi.Unwrap, repositoryUrl: string | undefined) {
+ // if we got an unknown repository url, just set to undefined for the remainder of
+ // processing. The rest of the code can handle that.
+ repositoryUrl = pulumi.containsUnknowns(repositoryUrl) ? undefined : repositoryUrl;
+ return buildAndPushImageWorkerAsync(imageName, pathOrBuild, repositoryUrl, logResource, connectToRegistry, skipPush, runDuringPreview);
+ }
}
function logEphemeral(message: string, logResource: pulumi.Resource) {
@@ -222,14 +227,50 @@ export function checkRepositoryUrl(repositoryUrl: string) {
async function buildAndPushImageWorkerAsync(
baseImageName: string,
pathOrBuild: string | pulumi.Unwrap,
- repositoryUrl: string,
+ repositoryUrl: string | undefined,
logResource: pulumi.Resource,
connectToRegistry: (() => pulumi.Input) | undefined,
- skipPush: boolean): Promise {
+ skipPush: boolean,
+ runDuringPreview: boolean): Promise {
- checkRepositoryUrl(repositoryUrl);
+ const isPreview = pulumi.runtime.isDryRun()
+ if (!runDuringPreview && isPreview) {
+ logEphemeral("Skipping docker build during preview", logResource);
+ return baseImageName;
+ }
- const tag = utils.getImageNameAndTag(baseImageName).tag;
+ if (repositoryUrl) {
+ checkRepositoryUrl(repositoryUrl);
+ }
+
+ // First, login and pulling from docker if we can.
+ const cacheFrom = await loginAndPullFromCacheAsync(baseImageName, pathOrBuild, repositoryUrl, logResource, connectToRegistry);
+
+ // Then actually kick off the build.
+ logEphemeral("Starting docker build...", logResource);
+ const buildResult = await buildImageAsync(baseImageName, pathOrBuild, logResource, cacheFrom);
+
+ // If we have no repository url, then we definitely can't push our build result. Same if
+ // we're in preview.
+ if (skipPush || !repositoryUrl || isPreview) {
+ logEphemeral("Completed docker build (without pushing)", logResource);
+ return baseImageName;
+ }
+
+ // Finally, if this a real update, push the built images to docker.
+ logEphemeral("Starting docker push...", logResource);
+ const result = await pushImageAsync(baseImageName, repositoryUrl, buildResult, logResource);
+ logEphemeral("Completed docker build and push", logResource);
+
+ return result;
+}
+
+async function loginAndPullFromCacheAsync(
+ baseImageName: string,
+ pathOrBuild: string | pulumi.Unwrap,
+ repositoryUrl: string | undefined,
+ logResource: pulumi.Resource,
+ connectToRegistry: (() => pulumi.Input) | undefined) {
// login immediately if we're going to have to actually communicate with a remote registry.
//
@@ -260,15 +301,21 @@ async function buildAndPushImageWorkerAsync(
}
// If the container specified a cacheFrom parameter, first set up the cached stages.
- let cacheFrom: string[] = [];
if (pullFromCache) {
const dockerBuild = >pathOrBuild;
const cacheFromParam = (typeof dockerBuild.cacheFrom === "boolean" ? {} : dockerBuild.cacheFrom) || {};
- cacheFrom = await pullCacheAsync(baseImageName, cacheFromParam, repositoryUrl, logResource);
+
+ // pullFromCache is only true if repositoryUrl is present.
+ return await pullCacheAsync(baseImageName, cacheFromParam, repositoryUrl!, logResource);
}
- // Next, build the image.
- const {imageId, stages} = await buildImageAsync(baseImageName, pathOrBuild, logResource, cacheFrom);
+ return [];
+}
+
+async function pushImageAsync(baseImageName: string, repositoryUrl: string, buildResult: BuildResult, logResource: pulumi.Resource): Promise {
+ const { imageId, stages } = buildResult;
+
+ const tag = utils.getImageNameAndTag(baseImageName).tag;
// Generate a name that uniquely will identify this built image. This is similar in purpose to
// the name@digest form that can be normally be retrieved from a docker repository. However,
@@ -280,24 +327,24 @@ async function buildAndPushImageWorkerAsync(
// Use those to push the image. Then just return the unique target name. as the final result
// for our caller to use. Only push the image during an update, do not push during a preview.
- if (!pulumi.runtime.isDryRun() && !skipPush) {
- // Push the final image first, then push the stage images to use for caching.
-
- // First, push with both the optionally-requested-tag *and* imageId (which is guaranteed to
- // be defined). By using the imageId we give the image a fully unique location that we can
- // successfully pull regardless of whatever else has happened at this repositoryUrl.
-
- // Next, push only with the optionally-requested-tag. Users of this API still want to get a
- // nice and simple url that they can reach this image at, without having the explicit imageId
- // hash added to it. Note: this location is not guaranteed to be idempotent. For example,
- // pushes on other machines might overwrite that location.
- await tagAndPushImageAsync(baseImageName, repositoryUrl, tag, imageId, logResource);
- await tagAndPushImageAsync(baseImageName, repositoryUrl, tag, /*imageId:*/ undefined, logResource);
-
- for (const stage of stages) {
- await tagAndPushImageAsync(
- localStageImageName(baseImageName, stage), repositoryUrl, stage, /*imageId:*/ undefined, logResource);
- }
+
+
+ // Push the final image first, then push the stage images to use for caching.
+
+ // First, push with both the optionally-requested-tag *and* imageId (which is guaranteed to
+ // be defined). By using the imageId we give the image a fully unique location that we can
+ // successfully pull regardless of whatever else has happened at this repositoryUrl.
+
+ // Next, push only with the optionally-requested-tag. Users of this API still want to get a
+ // nice and simple url that they can reach this image at, without having the explicit imageId
+ // hash added to it. Note: this location is not guaranteed to be idempotent. For example,
+ // pushes on other machines might overwrite that location.
+ await tagAndPushImageAsync(baseImageName, repositoryUrl, tag, imageId, logResource);
+ await tagAndPushImageAsync(baseImageName, repositoryUrl, tag, /*imageId:*/ undefined, logResource);
+
+ for (const stage of stages) {
+ await tagAndPushImageAsync(
+ localStageImageName(baseImageName, stage), repositoryUrl, stage, /*imageId:*/ undefined, logResource);
}
return uniqueTaggedImageName;
@@ -427,7 +474,7 @@ async function buildImageAsync(
const colonIndex = imageId.lastIndexOf(":");
imageId = colonIndex < 0 ? imageId : imageId.substr(colonIndex + 1);
- return {imageId, stages};
+ return { imageId, stages };
}
async function dockerBuild(