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

Enable preview for create and update #1891

Merged
merged 9 commits into from
Dec 12, 2024
Merged

Conversation

corymhall
Copy link
Contributor

@corymhall corymhall commented Dec 6, 2024

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.

Note: as part of this I had to add readOnly properties to metadata.json

closes #1141, re #1892

Copy link
Contributor

github-actions bot commented Dec 6, 2024

Does the PR have any schema changes?

Looking good! No breaking changes found.
No new resources/functions.

@corymhall corymhall force-pushed the corymhall/stable-properties branch from c2f761c to bb662bd Compare December 6, 2024 18:00
Copy link

codecov bot commented Dec 6, 2024

Codecov Report

Attention: Patch coverage is 87.63441% with 23 lines in your changes missing coverage. Please review.

Project coverage is 50.37%. Comparing base (7ca5a58) to head (784e3ad).
Report is 4 commits behind head on master.

Files with missing lines Patch % Lines
provider/pkg/provider/provider.go 71.42% 8 Missing and 4 partials ⚠️
provider/pkg/resources/cfn_custom_resource.go 0.00% 4 Missing ⚠️
provider/pkg/resources/extension_resource.go 0.00% 4 Missing ⚠️
provider/pkg/outputs/previewOutputs.go 97.61% 2 Missing and 1 partial ⚠️
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.
📢 Have feedback on the report? Share it here.

// - `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 {
Copy link
Contributor

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
Copy link
Contributor

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?

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 maybe I should just split it into separate functions.

Copy link
Contributor Author

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
Copy link
Member

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?

Copy link
Contributor Author

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

Copy link
Member

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)
Copy link
Member

@t0yv0 t0yv0 Dec 9, 2024

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.

Copy link
Contributor Author

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?

Copy link
Member

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!

@t0yv0
Copy link
Member

t0yv0 commented Dec 9, 2024

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.

@t0yv0
Copy link
Member

t0yv0 commented Dec 10, 2024

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:

+ 	// we can't handle arrays because we don't know which item in the array
+ 	// the value corresponds to
+ 	if isArrayProperty(propName) {
+ 		return false
+ 	}
+ 

@t0yv0
Copy link
Member

t0yv0 commented Dec 10, 2024

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:

    "DnsMap" : {
      "description" : "A map of DNS names for the AppSync API.",
      "type" : "object",
      "additionalProperties" : false,
      "properties" : {
        "Realtime" : {
          "type" : "string"
        },
        "Http" : {
          "type" : "string"
        }
      }
    },

But in the metadata we have:

#/types/aws-native:appsync:ApiDnsMap

            "readOnly": [
                "apiArn",
                "apiId",
                "dns",
                "dns/Http",
                "dns/Realtime"
            ],


        "aws-native:appsync:ApiDnsMap": {
            "type": "object",
            "properties": {
                "http": {
                    "type": "string",
                    "description": "The domain name of the Api's HTTP endpoint."
                },
                "realtime": {
                    "type": "string",
                    "description": "The domain name of the Api's real-time endpoint."
                }
            }
        },

These are not captialized consistently. "dns/Http" and "dns/Realtime".

@t0yv0
Copy link
Member

t0yv0 commented Dec 10, 2024

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!

@corymhall corymhall changed the base branch from master to corymhall/read-only-metadata December 10, 2024 18:00
@corymhall
Copy link
Contributor Author

corymhall commented Dec 10, 2024

This change is part of the following stack:

Change managed by git-spice.

@corymhall corymhall force-pushed the corymhall/stable-properties branch from 3288f39 to fc89719 Compare December 10, 2024 18:00
@corymhall corymhall force-pushed the corymhall/stable-properties branch from a38f447 to 1f68e18 Compare December 11, 2024 15:18
Base automatically changed from corymhall/read-only-metadata to master December 11, 2024 15:33
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
@corymhall corymhall force-pushed the corymhall/stable-properties branch from 8b6a67d to a4ee708 Compare December 11, 2024 15:38
}
} else {
key := resource.PropertyKey(readOnlyProp)
if output, ok := outputsFromPriorState[key]; ok {
Copy link
Member

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?

Copy link
Contributor Author

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,
Copy link
Member

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()

Copy link
Member

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(
Copy link
Member

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?

Copy link
Contributor Author

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():
Copy link
Member

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)
Copy link
Member

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
Copy link
Member

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:
Copy link
Member

Choose a reason for hiding this comment

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

Nice.

Copy link
Member

@t0yv0 t0yv0 left a comment

Choose a reason for hiding this comment

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

🚢

@corymhall corymhall merged commit 6008c70 into master Dec 12, 2024
18 checks passed
@corymhall corymhall deleted the corymhall/stable-properties branch December 12, 2024 20:35
Copy link
Contributor

@flostadler flostadler left a 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)
Copy link
Contributor

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(
Copy link
Contributor

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> {
Copy link
Contributor

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 {
Copy link
Contributor

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 {
Copy link
Contributor

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
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

KMS keyPolicy update on preview wrongly indicates that KMS key arn will change
3 participants