-
Notifications
You must be signed in to change notification settings - Fork 14
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
Perform docker builds even during preview so we can get users errors prior to a real update. #135
Changes from 4 commits
7d51927
b37310b
1d9cdf3
ca63d89
0adda8c
238b42a
95666c5
27ab10c
ea791ad
32faa04
6723d3e
1a36f24
37916a8
a479dcd
cffc25c
8555896
ddbfaf6
5f779ea
465e013
6564a41
ed3c25b
e2420a9
35ef5bb
2833fb1
c28858b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
name: broken | ||
runtime: nodejs | ||
description: A minimal TypeScript Pulumi program | ||
template: | ||
description: A minimal TypeScript Pulumi program |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
FR OM nginx | ||
COPY content /usr/share/nginx/html |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
<h1> Hi from Pulumi </h1> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
// 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"; | ||
|
||
export const imageName = docker.buildAndPushImage( | ||
"test-name", "./app", /*repositoryUrl:*/ undefined, /*logResource:*/ undefined!); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"name": "broken", | ||
"devDependencies": { | ||
"@types/node": "^8.0.0" | ||
}, | ||
"dependencies": { | ||
"@pulumi/pulumi": "dev", | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -157,28 +157,55 @@ export function buildAndPushImageAsync( | |
export function buildAndPushImage( | ||
imageName: string, | ||
pathOrBuild: pulumi.Input<string | DockerBuild>, | ||
repositoryUrl: pulumi.Input<string>, | ||
repositoryUrl: pulumi.Input<string> | undefined, | ||
logResource: pulumi.Resource, | ||
connectToRegistry?: () => pulumi.Input<Registry>, | ||
skipPush: boolean = false): pulumi.Output<string> { | ||
|
||
return pulumi.all([pathOrBuild, repositoryUrl]) | ||
.apply(async ([pathOrBuildVal, repositoryUrlVal]) => { | ||
// 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. note: higher level layers will likely need the same. i.e. we call buildAndPushImage from inside Outputs higher up that will likely not run since they won't have a known repoUrl. Will be making further changes in pulumi/awsx once this is in. |
||
return pulumi.output(pathOrBuild).apply(pathOrBuild => { | ||
const op = pulumi.output(repositoryUrl); | ||
|
||
// 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); | ||
// @ts-ignore Allow calling the 'runWithUnknowns' overload. | ||
const res: pulumi.Output<string> = op.apply(u => buildAndPushImageImpl(pathOrBuild, u), /*runWithUnknowns:*/ true); | ||
|
||
const result = await buildAndPushImageWorkerAsync( | ||
imageName, pathOrBuildVal, repositoryUrlVal, logResource, connectToRegistry, skipPush); | ||
return res; | ||
}); | ||
|
||
// 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); | ||
async function buildAndPushImageImpl( | ||
pathOrBuild: string | pulumi.Unwrap<DockerBuild>, | ||
repositoryUrl: string | undefined): Promise<string> { | ||
|
||
return result; | ||
}); | ||
// if we got an unknown repository url, just set to undefined for the remainder of | ||
// processing. The rest of the code can handle that. | ||
if (pulumi.containsUnknowns(repositoryUrl)) { | ||
repositoryUrl = undefined; | ||
} | ||
|
||
if (repositoryUrl) { | ||
checkRepositoryUrl(repositoryUrl); | ||
} | ||
|
||
logEphemeral("Starting docker build...", logResource); | ||
const buildResult = await buildImage(imageName, pathOrBuild, repositoryUrl, logResource, connectToRegistry); | ||
logEphemeral("Completed docker build", logResource); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we always attempt to do the build. |
||
|
||
// If we have no repository url, then we definitely can't push our build result. Same if | ||
// we're in preview. | ||
if (skipPush || !repositoryUrl || pulumi.runtime.isDryRun()) { | ||
return imageName; | ||
} | ||
|
||
logEphemeral("Starting docker push...", logResource); | ||
const result = await pushImage(repositoryUrl, buildResult, logResource); | ||
logEphemeral("Completed docker build", logResource); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. but we only attempt to push if we're in a normal update. |
||
|
||
return result; | ||
} | ||
} | ||
|
||
function logEphemeral(message: string, logResource: pulumi.Resource) { | ||
|
@@ -219,17 +246,12 @@ export function checkRepositoryUrl(repositoryUrl: string) { | |
} | ||
} | ||
|
||
async function buildAndPushImageWorkerAsync( | ||
baseImageName: string, | ||
pathOrBuild: string | pulumi.Unwrap<DockerBuild>, | ||
repositoryUrl: string, | ||
logResource: pulumi.Resource, | ||
connectToRegistry: (() => pulumi.Input<Registry>) | undefined, | ||
skipPush: boolean): Promise<string> { | ||
|
||
checkRepositoryUrl(repositoryUrl); | ||
|
||
const tag = utils.getImageNameAndTag(baseImageName).tag; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we don't need the tag except for pushing. so this moved into |
||
async function buildImage( | ||
baseImageName: string, | ||
pathOrBuild: string | pulumi.Unwrap<DockerBuild>, | ||
repositoryUrl: string | undefined, | ||
logResource: pulumi.Resource, | ||
connectToRegistry: (() => pulumi.Input<Registry>) | undefined): Promise<BuildResult> { | ||
|
||
// login immediately if we're going to have to actually communicate with a remote registry. | ||
// | ||
|
@@ -264,15 +286,25 @@ async function buildAndPushImageWorkerAsync( | |
if (pullFromCache) { | ||
const dockerBuild = <pulumi.UnwrappedObject<DockerBuild>>pathOrBuild; | ||
const cacheFromParam = (typeof dockerBuild.cacheFrom === "boolean" ? {} : dockerBuild.cacheFrom) || {}; | ||
cacheFrom = pullCacheAsync(baseImageName, cacheFromParam, repositoryUrl, logResource); | ||
|
||
// pullFromCache is only true if repositoryUrl is present. | ||
cacheFrom = pullCacheAsync(baseImageName, cacheFromParam, repositoryUrl!, logResource); | ||
CyrusNajmabadi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
// Next, build the image. | ||
const {imageId, stages} = await buildImageAsync(baseImageName, pathOrBuild, logResource, cacheFrom); | ||
if (imageId === undefined) { | ||
const buildResult = await buildImageAsync(baseImageName, pathOrBuild, logResource, cacheFrom); | ||
if (buildResult.imageId === undefined) { | ||
throw new Error("Internal error: docker build did not produce an imageId."); | ||
} | ||
|
||
return buildResult; | ||
} | ||
|
||
async function pushImage(repositoryUrl: string, buildResult: BuildResult, logResource: pulumi.Resource): Promise<string> { | ||
const { imageName: baseImageName, 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, | ||
// this tag doesn't require actually pushing the image, nor does it require communicating with | ||
|
@@ -283,24 +315,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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this moved up to the top-level logic. |
||
// 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; | ||
|
@@ -365,6 +397,7 @@ async function pullCacheAsync( | |
} | ||
|
||
interface BuildResult { | ||
imageName: string; | ||
imageId: string; | ||
stages: string[]; | ||
} | ||
|
@@ -430,7 +463,7 @@ async function buildImageAsync( | |
const colonIndex = imageId.lastIndexOf(":"); | ||
imageId = colonIndex < 0 ? imageId : imageId.substr(colonIndex + 1); | ||
|
||
return {imageId, stages}; | ||
return { imageName, imageId, stages }; | ||
} | ||
|
||
async function dockerBuild( | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note: i will be able to pass in SkipUpdate here once pulumi/pulumi#3894 goes in. This will allow us to validate that it's the preview step that is causing us to fail.