Skip to content

Add replaceOnChanges resource option #7226

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

Merged
merged 9 commits into from
Jul 1, 2021
Merged

Add replaceOnChanges resource option #7226

merged 9 commits into from
Jul 1, 2021

Conversation

lukehoban
Copy link
Contributor

@lukehoban lukehoban commented Jun 7, 2021

Adds a new resource option to force replacement when certain properties report changes, even if the resource provider itself does not require a replacement.

Also supports replaceOnChanges: ["*"] to force any update to be a replacement instead. This includes updates triggered by initialization errors on previous create/update operations even when no inputs have changed (as a more general fix for things like pulumi/pulumi-kubernetes#856).

Fixes #6753.
Addresses pulumi/pulumi-kubernetes#1007 (though via a resource option instead of a Kubernetes-specific annotation).

TODO:

  • Add Go, .NET SDK support
  • Add language-SDK-level test cases for TypeScript, Go and .NET
  • Review versioning (new CLI + old SDK, old SDK + new CLI) considerations

Future work:

  • Add support to codegen (likely along with adding all the remaining resource options not yet supported there)

Without replaceOnChanges:

const bucket = new aws.s3.Bucket("my-bucket", {
   website: {
       indexDocument: "index2.html",
   },
});
$ pulumi preview --non-interactive
Previewing update (dev)
 
View Live: https://app.pulumi.com/lukehoban/replaceonupdate/dev/previews/abebc9fb-2f9d-4ad8-802e-f5da834562d7
 
 
   pulumi:pulumi:Stack replaceonupdate-dev running
~  aws:s3:Bucket my-bucket update [diff: ~website]
   pulumi:pulumi:Stack replaceonupdate-dev 

Resources:
   ~ 1 to update
   1 unchanged

With replaceOnChanges:

const bucket = new aws.s3.Bucket("my-bucket", {
   website: {
       indexDocument: "index2.html",
   },
}, { replaceOnChanges: ["website.indexDocument"] });
$ pulumi preview --non-interactive
Previewing update (dev)
 
View Live: https://app.pulumi.com/lukehoban/replaceonupdate/dev/previews/c886484a-6719-40f8-8007-b328ea4c62d0
 
 
   pulumi:pulumi:Stack replaceonupdate-dev running
++ aws:s3:Bucket my-bucket create replacement [diff: ~website]
+- aws:s3:Bucket my-bucket replace [diff: ~website]
-- aws:s3:Bucket my-bucket delete original [diff: ~website]
   pulumi:pulumi:Stack replaceonupdate-dev 

Outputs:
 ~ bucketName: "my-bucket-d60afc3" => output<string>
 
Resources:
   +-1 to replace
   1 unchanged

With replaceOnChanges and deleteBeforeReplace:

const bucket = new aws.s3.Bucket("my-bucket", {
    website: {
        indexDocument: "index2.html",
    },
}, { replaceOnChanges: ["website.indexDocument"], deleteBeforeReplace: true });
$ pulumi preview --non-interactive
Previewing update (dev)

View Live: https://app.pulumi.com/lukehoban/replaceonupdate/dev/previews/de0427c0-37b1-4e7c-92c9-31243546cce1


    pulumi:pulumi:Stack replaceonupdate-dev running 
 -- aws:s3:Bucket my-bucket delete original 
 +- aws:s3:Bucket my-bucket replace [diff: ~website]
 ++ aws:s3:Bucket my-bucket create replacement [diff: ~website]
    pulumi:pulumi:Stack replaceonupdate-dev  
 
Outputs:
  ~ bucketName: "my-bucket-d60afc3" => output<string>

Resources:
    +-1 to replace
    1 unchanged
Siteproxy
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
DiffF: func(urn resource.URN, id resource.ID,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

It seems like we should have something built-in to produce a plugin.DiffResult from two resource.PropertyMaps (and potentially a set of replace keys). I was surprised I had to implement all this myself. Did I miss where this is defined? cc @pgavlin

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It looks like there is code like this in most of our native providers: https://github.com/pulumi/pulumi-azure-native/blob/ae7f380268c97b951a76848ec45b3bafa22d4338/provider/pkg/provider/diff.go. It's mostly the same sort of thing implemented here - but with some slightly special-cased to pull the "set of replace keys" from the specific provider schema. It feels like that implementation could/should be pulled into the core and generalized so it doesn't have to be reimplemented in every provider (and could also be reused in tests here).

@lblackstone
Copy link
Member

@lukehoban All of the SDK changes + tests should be finished now. It seems like the diff code deduplication could be addressed in a followup PR so we can finish out the immediate work stream.

I'm not as familiar with the version compatibility considerations, so @pgavlin will probably need to verify that.

@lukehoban
Copy link
Contributor Author

@pgavlin This looks to be ready for your review.

It seems like the diff code deduplication could be addressed in a followup PR so we can finish out the immediate work stream.

Opened #7326 on this one.

I'm not as familiar with the version compatibility considerations, so @pgavlin will probably need to verify that.

Since we are not storing this in the state file (at least until #7285), I believe the two scenarios here are just:

  • New CLI, old SDK: There is no way to requests replaceOnChanges, so the new CLI behaviour will never be triggered.
  • Old CLI, new SDK: SDK could request replaceOnChanges, but the CLI won't know about it. It will be ignored by the deployment.

This second case is potentially concerning. I am actually not sure how we handle this generally. Ideally the SDKs could require this support in the CLI and fail telling the user that this feature isn't supported in their CLI if they try to use it? Do we have support for that kind of version negotiation? Curious @pgavlin thoughts on this one.

@lblackstone
Copy link
Member

@lukehoban I was trying to test this out with the k8s provider and noticed that the DiffRequest doesn't include the replaceOnChanges field. Is this expected to work without that change?

@pgavlin
Copy link
Member

pgavlin commented Jun 22, 2021

@lblackstone this might be asking a lot, but would it be possible to split the property-path-related changes into a separate PR? It's a bit difficult to see the forest for the trees at the moment.

@pgavlin
Copy link
Member

pgavlin commented Jun 22, 2021

Old CLI, new SDK: SDK could request replaceOnChanges, but the CLI won't know about it. It will be ignored by the deployment.

This second case is potentially concerning. I am actually not sure how we handle this generally. Ideally the SDKs could require this support in the CLI and fail telling the user that this feature isn't supported in their CLI if they try to use it? Do we have support for that kind of version negotiation? Curious @pgavlin thoughts on this one.

The resource monitor provides the SupportsFeature gRPC API for this. The SDKs can call that method and fail gracefully if replaceOnChanges is not supported.

@lukehoban
Copy link
Contributor Author

would it be possible to split the property-path-related changes into a separate PR? It's a bit difficult to see the forest for the trees at the moment.

Do you mean just the changes in sdk/go/common/resource/properties_path.go?

@lukehoban
Copy link
Contributor Author

I was trying to test this out with the k8s provider and noticed that the DiffRequest doesn't include the replaceOnChanges field. Is this expected to work without that change?

@lblackstone The engine does not pass replaceOnChanges to providers. It manages the requested replaceOnChanges entirely as a modification to the diff returned by the provider. This ensures that providers do not need to be updated to deal with this.

@pgavlin
Copy link
Member

pgavlin commented Jun 22, 2021

And the tests, yes, assuming that those are the only affected files.

@lblackstone
Copy link
Member

Split out the prop path changes into #7354

@lblackstone
Copy link
Member

Rebased and split up commits into logical chunks

Copy link
Member

@pgavlin pgavlin left a comment

Choose a reason for hiding this comment

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

A couple suggestions, but LGTM overall

diff, err = sg.applyReplaceOnChanges(diff, goal.ReplaceOnChanges, hasInitErrors)
if err != nil {
return nil, result.FromError(err)
}
Copy link
Member

Choose a reason for hiding this comment

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

This change to the logic is a bit concerning: we'll now be examining the diff even if there are no changes but there are init errors, where previously we would skip looking at the diff if it had no changes but there were init errors. What if we instead had applyReplaceOnChanges run outside this check?

func (sg *stepGenerator) applyReplaceOnChanges(diff plugin.DiffResult,
replaceOnChanges []string, hasInitErrors bool) (plugin.DiffResult, error) {

var err error
Copy link
Member

Choose a reason for hiding this comment

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

If we take my earlier suggestion re: rearranging the logic in generateStepsFromDiff, then this function should early-out if diff.Changes != plugin.DiffSome && !hasInitErrors

@lblackstone
Copy link
Member

@pgavlin I attempted to address your feedback. Can you double-check that the changes to the diff logic look like you expect?

Verified

This commit was signed with the committer’s verified signature. The key has expired.
lblackstone Levi Blackstone
Adds a new resource option to force replacement when certain properties report changes, even if the resource provider itself does not require a replacement.

Fixes #6753.

Verified

This commit was signed with the committer’s verified signature. The key has expired.
lblackstone Levi Blackstone

Verified

This commit was signed with the committer’s verified signature. The key has expired.
lblackstone Levi Blackstone

Verified

This commit was signed with the committer’s verified signature. The key has expired.
lblackstone Levi Blackstone

Verified

This commit was signed with the committer’s verified signature. The key has expired.
lblackstone Levi Blackstone

Verified

This commit was signed with the committer’s verified signature. The key has expired.
lblackstone Levi Blackstone

Verified

This commit was signed with the committer’s verified signature. The key has expired.
lblackstone Levi Blackstone

Verified

This commit was signed with the committer’s verified signature. The key has expired.
lblackstone Levi Blackstone
Squashed commits:
[bba44ac92] Modify changes logic for init error case
[c07ece4b3] Further feedback from Pat
[50c7c16da] Address naming feedback
[8b61b6ccf] Address diff feedback

Verified

This commit was signed with the committer’s verified signature. The key has expired.
lblackstone Levi Blackstone
# Conflicts:
#	CHANGELOG_PENDING.md
@lblackstone
Copy link
Member

@lukehoban Any final comments before this gets merged?

@lukehoban
Copy link
Contributor Author

LGTM 🚀

I did some manual validation against a few "real world" use cases last night, and this worked great!

@lblackstone lblackstone merged commit eb32039 into master Jul 1, 2021
@pulumi-bot pulumi-bot deleted the lukehoban/6753 branch July 1, 2021 19:32
lukehoban pushed a commit that referenced this pull request Jul 13, 2021
The diff rendering logic tests whether the DetailedDiff is nil to determine whether to use it for rendering or defer to older older supported approaches to computing diffs.

The new logic added in #7226 could lead to replacing a nil DetailedDiff with an empty DetailedDiff, whcih would make the rendering logic believe that a DetailedDiff was present but empty, instead of missing entirely.

This change ensures that we maintain nil-ness of the DetailedDiff when we transform it for handling of replaceOnChanges.

Also adds tests for this and other cases on applyReplaceOnChanges.

Fixes #7486.
lblackstone pushed a commit that referenced this pull request Jul 13, 2021

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
The diff rendering logic tests whether the DetailedDiff is nil to determine whether to use it for rendering or defer to older older supported approaches to computing diffs.

The new logic added in #7226 could lead to replacing a nil DetailedDiff with an empty DetailedDiff, whcih would make the rendering logic believe that a DetailedDiff was present but empty, instead of missing entirely.

This change ensures that we maintain nil-ness of the DetailedDiff when we transform it for handling of replaceOnChanges.

Also adds tests for this and other cases on applyReplaceOnChanges.

Fixes #7486.
abhinav pushed a commit to pulumi/pulumi-dotnet that referenced this pull request Jan 11, 2023
Adds a new resource option to force replacement when certain properties report changes, even if the resource provider itself does not require a replacement.

Fixes pulumi/pulumi#6753.

Co-authored-by: Levi Blackstone <levi@pulumi.com>
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.

Add a resource option to force a replacement
3 participants