-
Notifications
You must be signed in to change notification settings - Fork 16
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
Enable preview for create and update #1891
Conversation
Does the PR have any schema changes?Looking good! No breaking changes found. |
c2f761c
to
bb662bd
Compare
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #1891 +/- ##
==========================================
+ Coverage 49.36% 50.37% +1.00%
==========================================
Files 47 48 +1
Lines 6814 6998 +184
==========================================
+ Hits 3364 3525 +161
- Misses 3206 3224 +18
- Partials 244 249 +5 ☔ View full report in Codecov by Sentry. |
// - `aws-native:securityhub:PolicyAssociation` has a property `associationIdentifier` | ||
// - TODO[pulumi/aws-native#1892]: in some cases this property is the `primaryIdentifier`. Could we use that as another heuristic? | ||
// a readonly property that is also a primary identifier? It doesn't catch all cases, but would catch more | ||
func isStableOutput(propName string, resourceTypeName tokens.TypeName) bool { |
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.
Nice!
readOnly []string, | ||
) resource.PropertyMap { | ||
result := resource.PropertyMap{} | ||
// Then this is an Extension resource which has all outputs in an "outputs" property |
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 CFN custom resources end up in the same if statement here?
They're also custom resources.
Could we make this more explicit?
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.
Yeah maybe I should just split it into separate functions.
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 moved these to be methods on the CustomResource
type since that seems to be the only good way of telling them apart.
if err != nil { | ||
return nil, err | ||
} | ||
return &pulumirpc.UpdateResponse{Properties: previewState}, 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.
Id is omitted because it's 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.
Id isn't part of the UpdateResponse
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.
Ah. Well I meant to comment on CreateResponse. I've checked the bridge code it's a little complicated, in CreateResponse it's returning plannedState.ID()
and encoding unknowns as "". This makes me suspect sometimes it can predict the ID and send it out during preview. But perhaps omitting it works just as well.
case inputValue.IsObject() && strings.HasPrefix(prop.Ref, "#/types/"): | ||
typeName := strings.TrimPrefix(prop.Ref, "#/types/") | ||
if t, ok := types[typeName]; ok { | ||
v := previewResourceOutputs(inputValue.ObjectValue(), types, t.Properties, readOnly) |
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 call here is no longer addressing a resource, but a nested object type, but the naming is confusing and it's hard to be sure this always does the right thing.
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 can rename it to something else, but why do you think it's hard to be sure this always does the right thing?
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.
That's a good point, I had to look at it with fresh eyes this morning, I think it's good! It does the right thing. readOnly properties are specified using paths into conforming objects, and jumping into #ref
properties does the right thing. You have it tested too. I noticed some wrinkle inconsistencies that I added some more comments about but I'm comfortable with this now, thank you!
Let's discuss briefly tomorrow, are recursive cases strictly necessary - perhaps worth shipping a smaller much simpler change first to test the waters (covering ARN properties) and then extend as needed with some additional testing? Or perhaps we'll decide to ship as-is. Thanks. |
This made me curious to poke at codecov. Besides the map case which is certainly interesting, there is this bit that's potentially interesting to cover:
|
I think there's a consistency issue with capitalization of our metadata (pre-this PR). In CF schema, we have Dns property of "#/definitions/DnsMap" type like this:
But in the metadata we have:
These are not captialized consistently. "dns/Http" and "dns/Realtime". |
Ah "Note: as part of this I had to add readOnly properties to metadata.json" - that's a separate piece. Let's maybe get that in as a separate quick PR and figure out the capitalization there? Thanks a ton! |
This change is part of the following stack: Change managed by git-spice. |
3288f39
to
fc89719
Compare
a38f447
to
1f68e18
Compare
Currently if you update a resource, all outputs are shown as computed during preview. This means that downstream resources might show a replacement in the plan if they are using one of those output values. There are some outputs that we know will not change unless the resource is replaced (I'm referring to these as "stable" outputs). For stable outputs we can copy the value from the state in order to show an accurate preview. The problem is that the CCAPI schema has no way of determining programmatically which outputs are stable and which are not. Because of this, this PR introduces a heuristic to determine if an output is a stable output. - Is the value a `readOnlyProperty` in the schema? This indicates that it is a computed output property - Is the property the resource `id`, `arn` or `name`? If the id, arn, or name is a computed property then we can assume that it is a stable property. I don't know of any cases where these change outside of a resource replacement. This is not 100% coverage of stable properties, but it should get us most of the impactful properties. To get close to 100% coverage we will probably need a schema overlay where we can contribute to mapping these stable properties for each resource. closes #1141
8b6a67d
to
a4ee708
Compare
} | ||
} else { | ||
key := resource.PropertyKey(readOnlyProp) | ||
if output, ok := outputsFromPriorState[key]; ok { |
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 branch will not consider outpusFromInputs? Why?
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.
outputsFromInputs
will have all possible outputs properties because of previewResourceOutputs
, so if it's a stable property and it exists in outputsFromPriorState
then copy it over.
// will have `name`, `id`, and `arn` properties, and sometimes they will have `resourceName`, `resourceId`, | ||
// and `resourceArn` properties. | ||
func populateStableOutputs( | ||
outputsFromInputs resource.PropertyMap, |
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 code would read much easier to me if the argument was called newInputs
and the result of the function was a separate map:
outputsFromInputs := newInputs.Copy()
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.
Like relying on modifying the first parameter is highly unexpected.
key := resource.PropertyKey(current) | ||
if outputFromInput, ok := outputsFromInputs[key]; ok { | ||
if outputFromState, ok := outputsFromPriorState[key]; ok { | ||
outputsFromInputs[key] = resource.NewObjectProperty(populateStableOutputs( |
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.
Will this only fire up when both inputs and prior state is present? Curious if this will exclude the case when an input is present but output not.. Ah ok that may be expected because this is only working with 'stable' info propagation.
How about when a state entry is present but the input entry is not? Why will it skip 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.
I think I've changed my mind about a previous comment you made about this being part of previewResourceOutputs
. This function is meant to only be called with the output of previewResourceOutputs
so outputsFromInputs
contains all the possible output properties already.
If an entry is not present in the input, but is in the prior state then it will be marked as computed (as part of the previewResourceOutputs
function.
readOnly []string, | ||
) resource.PropertyValue { | ||
switch { | ||
case inputValue.IsSecret(): |
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.
👍
- combine entrypoint into a single function for both create and update
) resource.PropertyMap { | ||
outputsFromInputs := previewResourceOutputs(newInputs, types, outputTypes, readOnlyProperties) | ||
if outputsFromPriorState != nil { | ||
return populateStableOutputs(outputsFromInputs, *outputsFromPriorState, readOnlyProperties, resourceTypeName) |
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.
👍
// will have `name`, `id`, and `arn` properties, and sometimes they will have `resourceName`, `resourceId`, | ||
// and `resourceArn` properties. | ||
func populateStableOutputs( | ||
// outputsFromInputs has to be the output of previewResourceOutputs |
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.
👍
return resource.NewObjectProperty(v) | ||
} | ||
// AdditionalProperties (map types) | ||
case inputValue.IsObject() && prop.AdditionalProperties != 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.
Nice.
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.
🚢
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 it!
test := pulumitest.NewPulumiTest(t, filepath.Join(cwd, "stable-outputs-preview"), options...) | ||
test.SetConfig(t, "lambdaDescription", "Lambda 1") | ||
defer func() { | ||
test.Destroy(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.
Doesn't pulumitest auto destroy by default?
|
||
|
||
const bucket = new ccapi.s3.Bucket('bucket'); | ||
const object = new aws.s3.BucketObjectv2( |
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.
Meta: Kind of frustrating that many rather basic use cases require layering aws classic on top of aws native in order to make it usable
:(
// eslint-disable-next-line @typescript-eslint/no-var-requires | ||
const archiver = require('archiver'); | ||
|
||
export function zipDirectory(directory: string, outputFile: string): Promise<string> { |
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.
Couldn't we do that by using an AssetArchive
?
// - `aws-native:securityhub:PolicyAssociation` has a property `associationIdentifier` | ||
// - TODO[pulumi/aws-native#1892]: in some cases this property is the `primaryIdentifier`. Could we use that as another heuristic? | ||
// a readonly property that is also a primary identifier? It doesn't catch all cases, but would catch more | ||
func isStableOutput(propName string, resourceTypeName tokens.TypeName) bool { |
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.
Nice!
|
||
// cfn custom resources have outputs returned in a "data" property | ||
// since it can be any arbitrary data, we mark the entire thing as computed | ||
func (c *cfnCustomResource) PreviewCustomResourceOutputs() resource.PropertyMap { |
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.
Small nit: this should be covered by a unit ideally
@@ -21,4 +21,6 @@ type CustomResource interface { | |||
Update(ctx context.Context, urn resource.URN, id string, inputs, oldInputs, state resource.PropertyMap, timeout time.Duration) (resource.PropertyMap, error) | |||
// Delete removes the resource from the cloud provider. | |||
Delete(ctx context.Context, urn resource.URN, id string, inputs, state resource.PropertyMap, timeout time.Duration) error | |||
// PreviewCustomResourceOutputs returns the outputs of the resource based on the inputs and the output properties in the resource schema. | |||
PreviewCustomResourceOutputs() resource.PropertyMap |
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.
Nice!
Currently if you update a resource, all outputs are shown as computed
during preview. This means that downstream resources might show a
replacement in the plan if they are using one of those output values.
There are some outputs that we know will not change unless the resource
is replaced (I'm referring to these as "stable" outputs). For stable
outputs we can copy the value from the state in order to show an
accurate preview.
The problem is that the CCAPI schema has no way of determining
programmatically which outputs are stable and which are not. Because of
this, this PR introduces a heuristic to determine if an output is a
stable output.
readOnlyProperty
in the schema? This indicates thatit is a computed output property
id
,arn
orname
? If the id, arn, orname is a computed property then we can assume that it is a stable
property. I don't know of any cases where these change outside of a
resource replacement.
This is not 100% coverage of stable properties, but it should get us
most of the impactful properties. To get close to 100% coverage we will
probably need a schema overlay where we can contribute to mapping these
stable properties for each resource.
Note: as part of this I had to add
readOnly
properties tometadata.json
closes #1141, re #1892