-
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
docker:Image - Add flag to enable build on preview #855
Conversation
Does the PR have any schema changes?Does the PR have any schema changes?Looking good! No breaking changes found. Maintainer note: consult the runbook for dealing with any breaking changes. |
SkipUpdate: true, //only run Preview | ||
SkipExportImport: true, | ||
Verbose: true, //we need this to verify the build output logs | ||
AllowEmptyPreviewChanges: true, |
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.
Wouldn't we expect to build an image in the preview?
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.
So I think this flag is poorly named. All it does is it tells the programtest to not pass the --expect-no-changes
flag to pulumi update
, as the test fails spuriously otherwise. The real check is a few lines below , in whether the preview output contains messages that point to a built image. This test only runs preview, not update.
if !buildObject["context"].ContainsUnknowns() { | ||
if buildObject["context"].IsNull() { | ||
// set default | ||
build.Context = "." | ||
} else { | ||
build.Context = buildObject["context"].StringValue() | ||
} | ||
} | ||
|
||
// Dockerfile | ||
if buildObject["dockerfile"].IsNull() { | ||
// set default | ||
build.Dockerfile = path.Join(build.Context, defaultDockerfile) | ||
} else { | ||
build.Dockerfile = buildObject["dockerfile"].StringValue() | ||
if !buildObject["dockerfile"].ContainsUnknowns() { | ||
if buildObject["dockerfile"].IsNull() { | ||
// set default | ||
build.Dockerfile = path.Join(build.Context, defaultDockerfile) | ||
} else { | ||
build.Dockerfile = buildObject["dockerfile"].StringValue() | ||
} | ||
} | ||
|
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.
So if I understand correctly, we'll only run a preview as long as all of our inputs have resolved. That seems reasonable. I think that matches how the bridge provider previously did these previews?
provider/image.go
Outdated
@@ -629,7 +635,7 @@ func marshalCachedImages(b resource.PropertyValue) ([]string, error) { | |||
} | |||
c := b.ObjectValue()["cacheFrom"] | |||
|
|||
if c.IsNull() { | |||
if c.IsNull() || c.IsComputed() { |
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.
Unfortunately, resource.PropertyValue.IsComputed()
is a footgun.1 Basically, resource.PropertyValue
has two different ways to represent a value as "unknown": resource.Computed{ ... }
and resource.Output{ Known: false, ...}
. There can also be secret computed values (which this will not catch).
if c.IsNull() || c.IsComputed() { | |
if c.IsNull() || c.ContainsUnknowns() { |
I can't think of a place where it would be correct to use c.IsComputed()
directly in this provider.
Footnotes
if !r.IsNull() { | ||
if !r.ObjectValue()["server"].IsNull() { | ||
|
||
if !r.ObjectValue()["server"].IsNull() && !r.ObjectValue()["server"].ContainsUnknowns() { |
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.
If r
isn't an object value or null, this function will panic. I'd be very nervous here vis a vis if r
is unknown or secret.
I would recommend changing the type signature of this function to operate on a resource.PropertyMap
. I'm commenting on something not in this PR, so feel free to leave that for later.
if !v.ContainsUnknowns() { | ||
vStr := v.StringValue() | ||
args[key] = &vStr | ||
} |
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.
Nit: keeping things left indented
if !v.ContainsUnknowns() { | |
vStr := v.StringValue() | |
args[key] = &vStr | |
} | |
if v.ContainsUnknowns() { | |
continue | |
} | |
vStr := v.StringValue() | |
args[key] = &vStr | |
provider/image_test.go
Outdated
resource.NewComputedProperty( | ||
resource.Computed{Element: resource.NewStringProperty("looking-for-my-image")}, | ||
), |
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.
You can do this, but you don't need to.
resource.NewComputedProperty( | |
resource.Computed{Element: resource.NewStringProperty("looking-for-my-image")}, | |
), | |
resource.MakeComputed(resource.NewStringProperty("looking-for-my-image")), |
This applies in many test cases.
knownDockerfile = true | ||
} else { | ||
// We do not want to set these fields if their values are Unknown. | ||
if !inputs["build"].ObjectValue()["dockerfile"].ContainsUnknowns() { |
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.
It looks like if inputs["build"]
is computed, then this will panic. Am I missing a check somewhere?
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.
I did think about this and was having a difficult time coming up with a practical example of a situation where a program would pass in the entire build object as Unknown.
It's not impossible, and definitely worth a safety check, but my thinking was, in most situations unknowns would be passed to the concrete fields, not the overall build object (and I haven't seen any bugs filed that showed this kind of pattern).
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.
It's weird but definitely possible! I've done things like this:
build: thing.arn.apply((arn) => {
return {
dockerfile: "Dockerfile",
args: {
THE_ARN: arn
}
}
})
Sure you could have done that apply deeper in the object... but anything is possible :)
I added a small test to sanity check everything still works when build is computed and it caught a few panics... whew!
provider/provider.go
Outdated
@@ -325,6 +334,18 @@ func (p *dockerNativeProvider) Create(ctx context.Context, req *rpc.CreateReques | |||
return nil, errors.Wrapf(err, "malformed resource inputs") | |||
} | |||
|
|||
// if we're in preview mode and buildOnPreview is set to false, we return the inputs | |||
if req.GetPreview() && !inputs["buildOnPreview"].BoolValue() { |
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.
What if inputs["buildOnPreview"]
is unknown?
- This code will panic.
- What should the provider do?
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.
Again, thinking about this from a user perspective, I was having a difficult time coming up with a situation where anyone would make buildOnPreview unknown, but you're right that the world is a large and diverse place so we should add a check. My feeling is to warn and continue.
provider/provider.go
Outdated
if req.GetPreview() && !inputs["buildOnPreview"].BoolValue() { | ||
return &rpc.CreateResponse{ | ||
Properties: req.GetProperties(), | ||
}, nil | ||
} | ||
// buildOnPreview needs all inputs to be resolved. Return error if trying to build on preview and there are Unknowns. | ||
if req.GetPreview() && inputs["buildOnPreview"].BoolValue() && inputs.ContainsUnknowns() { | ||
return nil, errors.New("cannot build on preview with unresolved inputs. " + | ||
"Set buildOnPreview to False, or ensure all inputs are resolved at preview.") | ||
} |
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.
Code structure: Can we fold the req.GetPreview()
call one level higher:
if req.GetPreview() && !inputs["buildOnPreview"].BoolValue() { | |
return &rpc.CreateResponse{ | |
Properties: req.GetProperties(), | |
}, nil | |
} | |
// buildOnPreview needs all inputs to be resolved. Return error if trying to build on preview and there are Unknowns. | |
if req.GetPreview() && inputs["buildOnPreview"].BoolValue() && inputs.ContainsUnknowns() { | |
return nil, errors.New("cannot build on preview with unresolved inputs. " + | |
"Set buildOnPreview to False, or ensure all inputs are resolved at preview.") | |
} | |
if req.GetPreview() { | |
if !inputs["buildOnPreview"].BoolValue() { | |
return &rpc.CreateResponse{ | |
Properties: req.GetProperties(), | |
}, nil | |
} | |
// buildOnPreview needs all inputs to be resolved. Return error if trying to build on preview and there are Unknowns. | |
if inputs.ContainsUnknowns() { | |
return nil, errors.New("cannot build on preview with unresolved inputs. " + | |
"Set buildOnPreview to False, or ensure all inputs are resolved at preview.") | |
} | |
} |
provider/provider.go
Outdated
} | ||
// buildOnPreview needs all inputs to be resolved. Return error if trying to build on preview and there are Unknowns. | ||
if req.GetPreview() && inputs["buildOnPreview"].BoolValue() && inputs.ContainsUnknowns() { | ||
return nil, errors.New("cannot build on preview with unresolved inputs. " + |
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.
It feels like this should be a warning instead of an error. If we have agreed that this should error in the past, please ignore this comment.
provider/provider.go
Outdated
// buildOnPreview needs all inputs to be resolved. Warn and continue without building the image | ||
// TODO: there is room for some future granularity here - we should be able to build a local image without | ||
// (TODO cont) knowing inputs for a registry, for example. | ||
if inputs["buildOnPreview"].BoolValue() && inputs.ContainsUnknowns() { |
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.
We already know here that inputs["buildOnPreview"].BoolValue()
is true
, since if it wasn't we would have exited the function on L352.
if inputs["buildOnPreview"].BoolValue() && inputs.ContainsUnknowns() { | |
if inputs.ContainsUnknowns() { |
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.
Barring the comments I have already made, this looks good vis a vis resource.PropertyValue
handling. I'll leave it to @blampe to check that the PR does what it is supposed to.
provider/image.go
Outdated
@@ -639,38 +645,38 @@ func marshalCachedImages(b resource.PropertyValue) ([]string, error) { | |||
if !ok { | |||
return cacheImages, fmt.Errorf("cacheFrom requires an `images` field") | |||
} | |||
if images.IsNull() { | |||
if images.IsNull() || images.IsComputed() { |
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.
if images.IsNull() || images.IsComputed() { | |
if images.IsNull() || images.ContainsUnknowns() { |
provider/provider.go
Outdated
|
||
func marshalBuildOnPreview(inputs resource.PropertyMap) bool { | ||
//set default if not set | ||
if inputs["buildOnPreview"].IsNull() || inputs["buildOnPreview"].IsComputed() { |
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.
if inputs["buildOnPreview"].IsNull() || inputs["buildOnPreview"].IsComputed() { | |
if inputs["buildOnPreview"].IsNull() || inputs["buildOnPreview"].ContainsUnknowns() { |
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.
Looking good, only some minor nits and one high-level question that's not blocking.
"buildOnPreview": { | ||
"type": "boolean", | ||
"description": "A flag to build an image on preview", | ||
"default": false | ||
}, |
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.
I realize it's late in the game to be asking this, but I'm curious why this is a resource property and not some configuration on the provider.
With 3.x, it would build all images on preview. So, strictly speaking, for anyone that wants that behavior back it seems like it would be OK to set at the provider level.
I'm not saying that because I think we need to do it, only to make sure we've considered it. (If anything, doing it this way might make the buildx.Image resource slightly easier because we could defer or punt on this behavior altogether.)
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.
What if someone wants to build some images on preview, but not others? I could easily imagine only wanting to check this conditionally. I opted for flexibility here, rather than "all or nothing", but I admit I did not consider inheriting this for other inflections of Image.
provider/image.go
Outdated
if !images.ContainsUnknowns() { | ||
return cacheImages, fmt.Errorf("the `images` field must be a list of strings") | ||
} else { | ||
return cacheImages, nil | ||
} | ||
|
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.
if !images.ContainsUnknowns() { | |
return cacheImages, fmt.Errorf("the `images` field must be a list of strings") | |
} else { | |
return cacheImages, nil | |
} | |
if !images.ContainsUnknowns() { | |
return cacheImages, fmt.Errorf("the `images` field must be a list of strings") | |
} | |
return cacheImages, nil | |
provider/provider.go
Outdated
} else { | ||
// avoid panic if inputs["build"] is not an Object - we only want to set these fields if their values are Known. | ||
if inputs["build"].IsObject() { | ||
if !inputs["build"].ObjectValue()["dockerfile"].ContainsUnknowns() { | ||
inputs["build"].ObjectValue()["dockerfile"] = resource.NewStringProperty(build.Dockerfile) | ||
knownDockerfile = true | ||
} | ||
if !inputs["build"].ObjectValue()["context"].ContainsUnknowns() { | ||
inputs["build"].ObjectValue()["context"] = resource.NewStringProperty(build.Context) | ||
} | ||
} | ||
} |
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.
} else { | |
// avoid panic if inputs["build"] is not an Object - we only want to set these fields if their values are Known. | |
if inputs["build"].IsObject() { | |
if !inputs["build"].ObjectValue()["dockerfile"].ContainsUnknowns() { | |
inputs["build"].ObjectValue()["dockerfile"] = resource.NewStringProperty(build.Dockerfile) | |
knownDockerfile = true | |
} | |
if !inputs["build"].ObjectValue()["context"].ContainsUnknowns() { | |
inputs["build"].ObjectValue()["context"] = resource.NewStringProperty(build.Context) | |
} | |
} | |
} | |
} else if inputs["build"].IsObject() { | |
// avoid panic if inputs["build"] is not an Object - we only want to set these fields if their values are Known. | |
if !inputs["build"].ObjectValue()["dockerfile"].ContainsUnknowns() { | |
inputs["build"].ObjectValue()["dockerfile"] = resource.NewStringProperty(build.Dockerfile) | |
knownDockerfile = true | |
} | |
if !inputs["build"].ObjectValue()["context"].ContainsUnknowns() { | |
inputs["build"].ObjectValue()["context"] = resource.NewStringProperty(build.Context) | |
} | |
} |
provider/provider.go
Outdated
|
||
if _, statErr := os.Stat(build.Dockerfile); statErr != nil { | ||
if filepath.IsAbs(build.Dockerfile) { | ||
return nil, fmt.Errorf("could not open dockerfile at absolute path %s: %v", build.Dockerfile, statErr) |
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.
Nit: instead of "could not open..." a more helpful error might say something like "expected a relative path, got %q instead".
76b2863
to
545c35b
Compare
If build object contains unknowns, skip Dockerfile hashing and platform logic in Check(). This is a blunt approach, as it doesn't allow us to granularly apply verificaton logic the first time Chieck() is called.
…ckerfile, Context; and registry: username, password, server
It turns out the random provider does not in fact return computed values, so this test never tested what it said to test. Using Command.stdout fixes this.
…and Create when run in preview mode. Disambiguate test name.
…ne is trying to build on preview with unresolved inputs.
…ainsUnknowns() for cacheFrom and cacheFrom.Images
545c35b
to
96d5cf1
Compare
provider/provider.go
Outdated
if inputs["buildOnPreview"].ContainsUnknowns() { | ||
msg = "buildOnPreview is unresolved; cannot build on preview. Continuing without preview image build. " + | ||
"To avoid this warning, set buildOnPreview explicitly, and ensure all inputs are resolved at preview." | ||
returnWithoutBuild = true |
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.
could we return early here? then we don't need to do the double verification on L#755.
We could return msg
instead of an error, and put the logging logic and error handling in the main function.
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.
Great idea, fixed!
@@ -458,3 +458,93 @@ func TestCheck(t *testing.T) { | |||
}) | |||
} | |||
} | |||
|
|||
func TestCanPreview(t *testing.T) { |
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.
love this!
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.
It's a little more work up front to crank out some unit tests but worth it for the peace of mind!
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.
Looks great! Let's cut a prerelease and give this a spin internally! I think there's already a 4.6.0-alpha.0 if it matters.
"dependencies": { | ||
"@pulumi/aws": "^6.10.0", | ||
"@pulumi/pulumi": "^3.0.0", | ||
"@pulumi/aws": "latest", | ||
"@pulumi/random": "latest" | ||
"@pulumi/random": "^4.14.0" |
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.
Unrelated to your PR but shouldn't these include @pulumi/docker
? I was a little confused when I tried running one locally and it didn't work.
}) | ||
actual, err := marshalCachedImages(buildInput) | ||
assert.NoError(t, err) | ||
assert.Equal(t, expected, actual) | ||
}) | ||
} | ||
|
||
// TODO: do we want to allow Builder to be Unknown? there's very little use case here |
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.
I think we can skip this one, very few people are setting this in the wild so I'm not too concerned about it working in exotic ways.
provider/image_test.go
Outdated
@@ -366,6 +477,7 @@ func TestMarshalBuilder(t *testing.T) { | |||
}) | |||
} | |||
|
|||
// TODO: do we want to allow SkipPush to be Unknown? |
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.
This one is more plausible and fortunately you've already handled it!
@@ -134,58 +133,86 @@ func (p *dockerNativeProvider) log(ctx context.Context, sev diag.Severity, urn r | |||
// the provider inputs are using for detecting and rendering diffs. | |||
func (p *dockerNativeProvider) Check(ctx context.Context, req *rpc.CheckRequest) (*rpc.CheckResponse, error) { | |||
urn := resource.URN(req.GetUrn()) | |||
label := fmt.Sprintf("%s.Create(%s)", p.name, urn) | |||
label := fmt.Sprintf("%s.Check(%s)", p.name, urn) |
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.
🙌
knownDockerfile = true | ||
} else { | ||
// We do not want to set these fields if their values are Unknown. | ||
if !inputs["build"].ObjectValue()["dockerfile"].ContainsUnknowns() { |
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.
It's weird but definitely possible! I've done things like this:
build: thing.arn.apply((arn) => {
return {
dockerfile: "Dockerfile",
args: {
THE_ARN: arn
}
}
})
Sure you could have done that apply deeper in the object... but anything is possible :)
I added a small test to sanity check everything still works when build is computed and it caught a few panics... whew!
provider/provider.go
Outdated
if inputs["buildOnPreview"].ContainsUnknowns() { | ||
msg = "buildOnPreview is unresolved; cannot build on preview. Continuing without preview image build. " + | ||
"To avoid this warning, set buildOnPreview explicitly, and ensure all inputs are resolved at preview." | ||
returnWithoutBuild = true |
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.
Great idea, fixed!
@@ -458,3 +458,93 @@ func TestCheck(t *testing.T) { | |||
}) | |||
} | |||
} | |||
|
|||
func TestCanPreview(t *testing.T) { |
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.
It's a little more work up front to crank out some unit tests but worth it for the peace of mind!
This pull request's intent is to allow a user to build their image during
pulumi preview
by using the newbuildOnPreview
resource field.This field is added to the Image schema as a boolean and defaults to false.
The implementation specifics are as follows, with some things for the reviewer to weigh in on.
supportPreview
is enabled and all implemented RPD methods should handle Unknowns. This should also help address Message sooner about building for the host architecture #847 and Unable to use dynamic build property #620.ContainsUnknowns()
checks are added to the marshaler and some of theCheck()
logic. I wasn't sure ifContainsUnknowns()
orIsComputed()
should be used here; the former contains a check for the latter.Check()
, instead we apply other defaults and carry on. We will calculate the build hash on theupdate
call once Unknowns are computed.buildOnPreview
to false.buildOnPreview
flag is set to true.buildOnPreview
flag is set to trueFixes #540.