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

Change pulumi refresh to report diff relative to desired state instead of relative to only output changes #16146

Open
wants to merge 1 commit into from

Conversation

lukehoban
Copy link
Member

@lukehoban lukehoban commented May 8, 2024

Presently, the behaviour of diffing during refresh steps is incomplete, returning only an "output diff" that presents the changes in outputs. This commit changes refresh steps so that:

  • they compute a diff similar to the one that would be computed if a preview were run immediately after the refresh, which is more typically what users expect and want; and
  • IgnoreChanges resource options are respected when performing the new desired-state diffs, so that property additions or changes reported by a refresh can be ignored.

In particular, IgnoreChanges can now be used to acknowledge that part or all of a resource may change in the provider, but the user is OK with this and doesn't want to be notified about it during a refresh. Importantly, this means that the diff won't be reported, but also that the changes won't be applied to state.

The implementation covers the following:

  • A diff is computed using the inputs from the program and then inverting the result, since in the case of a refresh the diff is being driven by the provider side and not the program. This doesn't change what is stored back into the state, but it does produce a diff that is more aligned with the "true changes to the desired state".
  • IgnoreChanges resource options are now stored in state, so that this information can be used in refresh operations that do not have access to/run the program.
  • In the context of a refresh operation, IgnoreChanges applies to both input and output properties. This differs from the behaviour of a normal update operation, where IgnoreChanges only considers input properties.
  • The special "*" value for IgnoreChanges can be used to ignore all properties. It also ignores the case where the resource cannot be found in the provider, and instead keeps the resource intact in state with its existing input and output properties.

Because the program is not run for refresh operations, IgnoreChanges options must be applied separately before a refresh takes place. This can be accomplished using e.g. a pulumi up that applies the options prior to a refresh. We should investigate perhaps providing a pulumi state set ...-like CLI to make these sorts of changes directly to a state.

For use cases relying on the legacy refresh diff provider, the PULUMI_USE_LEGACY_REFRESH_DIFF environment variable can be set, which will disable desired-state diff computation. We only need to perform checks in RefreshStep.{ResultOp,Apply}, since downstream code will work correctly based on the presence or absence of a DetailedDiff in the step.

Notes

Fixes

@lukehoban lukehoban force-pushed the lukehoban/ignoreChangesRefresh branch from f3b983d to 9cc0f2c Compare May 8, 2024 06:01
@pulumi-bot
Copy link
Contributor

pulumi-bot commented May 8, 2024

Changelog

[uncommitted] (2024-06-12)

Features

  • [engine] Change pulumi refresh to report diffs relative to desired state instead of relative to only output changes. Use PULUMI_ENABLE_LEGACY_REFRESH_DIFF to revert to the old behaviour.
    #16146

@lukehoban lukehoban requested review from Frassle and pgavlin May 8, 2024 18:47
@t0yv0 t0yv0 self-requested a review May 8, 2024 19:40
// * newInputs where oldInputs are expected
// * newOutputs where oldOutputs are expected
// * oldInputs where newInputs are expected
diff, err := diffResource(s.new.URN, s.new.ID, s.new.Inputs, s.new.Outputs, s.old.Inputs, prov, preview,
Copy link
Member

Choose a reason for hiding this comment

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

Assuming this is going to call Diff(), I love that because that automatically engages bridge machinery for planning changes suppressing changes and computing detailed diffs, that is going to be very helpful to run that. The downside is that new inputs returned from Read() are not check-clean and Diff expects these to be checked clean inputs so this may uncover some latent issues in bridged providers when rolled out.

Copy link
Member Author

Choose a reason for hiding this comment

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

new inputs returned from Read() are not check-clean

I think the implied requriement even before these changes is that they are, as they have always been stored directly into the state file, and only "check-clean" inputs are allowed to be stored in state. And that's a reasonable requirement, because these are inputs provided directly by a provider, which is capable of ensuring anything it returns is check-clean. Check is only needed to take an arbitrary bag of inputs from a users and turn it into something the provider knows is valid.

Copy link
Member

Choose a reason for hiding this comment

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

Agreed that's reasonable. I was worried this will promote pulumi/pulumi-aws#2318 to a refresh issue but it's empirically not the case, works the same as before. So that's very good.

@t0yv0
Copy link
Member

t0yv0 commented May 8, 2024

Doing another pass here, really 👍 on this, "Changes to output-only properties are no longer shown as refresh diffs" is very valuable as well. This accomplishes most of the benefit of #16072 without running the program on pulumi refresh which may be an un-releasable change (too invasive because it would slow down pulumi refresh as well as possibly cause some previously working programs to crash as they do not expect being run during pulumi refresh). Summarizing quick background discussion with @EronWright - there inevitably will be cases where resource outputs interact with the program and full pulumi preview is higher fidelity as to actual drift compared this PR form of just calling Diff per-resource, but it seems to me the tradeoffs are worth it.

@lukehoban lukehoban force-pushed the lukehoban/ignoreChangesRefresh branch 2 times, most recently from d266a15 to dc8439b Compare May 9, 2024 03:35
@lukehoban lukehoban marked this pull request as ready for review May 19, 2024 21:13
@lukehoban lukehoban requested a review from a team as a code owner May 19, 2024 21:13
Copy link
Member Author

@lukehoban lukehoban left a comment

Choose a reason for hiding this comment

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

A few notes:

  • Ideally we would offer a way to opt-out of this via env var. The changes are coupled across (a) RefreshStep in the engine (b) diff renderer (c) progress renderer. We would need to incorporate the env var into all three places and maintain old and new behaviour in both. But I think we likely do need to do this.
  • I am nervous about the wide variety of allowed Diff behaviours, and how few of them are tested generally here, especially relative to the true Diff behaviours in providers. Ideally we would have tf-bridge-like, kubernetes-like, and az-native-like diff implementations we could test against here to validate and baseline refresh (and many other features) against.

pkg/backend/display/rows.go Show resolved Hide resolved
pkg/backend/display/diff.go Show resolved Hide resolved
var yes bool

cmd := &cobra.Command{
Use: "set-ignore-changes [resource URN] --path [property-path-1] ...",
Copy link
Member Author

Choose a reason for hiding this comment

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

I honestly wish we had a more general command or set of commands for modifying components of state directly like this, and didn't need to invent a new mechanism here. Open to feedback on the CLI UX here - it's not particularly "nice", but seems as well aligned as possible with surrounding CLI UX and with being able to technically express all use cases.


// ResultOp returns the operation that corresponds to the change to this resource after reading its current state, if
// any.
func (s *RefreshStep) ResultOp() display.StepOp {
if s.new == nil {
return OpDelete
}
if s.new == s.old || s.old.Outputs.Diff(s.new.Outputs) == nil {
if s.new == s.old || s.diff.Changes == plugin.DiffNone {
Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not 100% certain how DiffUnknown should be treated here.

@t0yv0
Copy link
Member

t0yv0 commented Jun 6, 2024

Curious if we expect to land this anytime soon? @VenelinMartinov and I found a root cause of a very specific TF bug that uses old 2019 normalizeNullValues code to replace empty maps with null that currently causes a lot of dirty refreshes in both TF and stock Pulumi, that is alleviated by this change. We're evaluating tailoring a workaround in the bridge, but if we are able to land this change it might be not worth it?

@lukehoban
Copy link
Member Author

Curious if we expect to land this anytime soon?

Yes! @lunaris is picking up taking this over the finish line. 🙌

@lunaris lunaris force-pushed the lukehoban/ignoreChangesRefresh branch from 7a08bb8 to db946da Compare June 7, 2024 19:00
@bernhardloos
Copy link

I always found the current refresh behavior quite useful, especially for keeping an eye on complex resources that get changed by other systems outside of pulumi. So an option to keep the current behavior would be much appreciated.
(But ignore_changes applying to refresh would still be nice).

github-merge-queue bot pushed a commit that referenced this pull request Jun 11, 2024
# Description

There are a number of parts of the deployment process that require
context about and configuration for the operation being executed. For
instance:

* Source evaluation -- evaluating programs in order to emit resource
registrations
* Step generation -- processing resource registrations in order to
generate steps (create this, update that, delete the other, etc.)
* Step execution -- executing steps in order to action a deployment.

Presently, these pieces all take some form of `Options` struct or pass
explicit arguments. This is problematic for a couple of reasons:

* It could be possible for different parts of the codebase to end up
operating in different contexts/with different configurations, whether
due to different values being passed explicitly or due to missed
copying/instantiation.
* Some parts need less context/configuration than others, but still
accept full `Options`, making it hard to discern what information is
actually necessary in any given part of the process.

This commit attempts to clean things up by moving deployment options
directly into the `Deployment` itself. Since step generation and
execution already refer to a `Deployment`, they get a consistent view of
the options for free. For source evaluation, we introduce an
`EvalSourceOptions` struct for configuring just the options necessary
there. At the top level, the engine configures a single set of options
to flow through the deployment steps later on.

As part of this work, a few other things have been changed:

* Preview/dry-run parameters have been incorporated into options. This
lets up lop off another argument and mitigate a bit of "boolean
blindness". We don't appear to flip this flag within a deployment
process (indeed, all options seem to be immutable) and so having it as a
separate flag doesn't seem to buy us anything.
* Several methods representing parts of the deployment process have lost
arguments in favour of state that is already being carried on (or can be
carried on) their receiver. For instance, `deployment.run` no longer
takes actions or preview configuration. While doing so means that a
`deployment` could be run multiple times with different actions/preview
arguments, we don't currently exploit this fact anywhere, so moving this
state to the point of construction both simplifies things considerably
and reduces the possibility for error (e.g. passing different values of
`preview` when instantiating a `deployment` and subsequently calling
`run`).
* Event handlers have been split out of the options object and attached
to `Deployment` separately. This means we can talk about options at a
higher level without having to `nil` out/worry about this field and
mutate it correctly later on.
* Options are no longer mutated during deployment. Presently there
appears to be only one case of this -- when handling `ContinueOnError`
in the presence of `IgnoreChanges` (e.g. when performing a refresh).
This case has been refactored so that the mutation is no longer
necessary.

# Notes

* This change is in preparation for #16146, where we'd like to add an
environment variable to control behaviour and having a single unified
`Options` struct would make it easier to pass this configuration down
with introducing (more) global state into deployments. Indeed, this
change should make it easier to factor global state into `Options` so
that it can be controlled and tested more easily/is less susceptible to
bugs, race conditions, etc.
* I've tweaked/extended some comments while I'm here and have learned
things the hard way (e.g. `Refresh` vs `isRefresh`). Feedback welcome on
this if we'd rather not conflate.
* This change does mean that if in future we wanted e.g. to be able to
run a `Deployment` in multiple different ways with multiple sets of
actions, we'd have to refactor. Pushing state to the point of object
construction reduces the flexibility of the code. However, since we are
not presently using that flexibility (nor is there an obvious [to my
mind] use case in the near future), this seems like a good trade-off to
guard against bugs/make it simpler to move that state around.
* I've left some other review comments in the code around
questions/changes that might be a bad idea; happy to receive feedback on
it all though!
@lunaris lunaris force-pushed the lukehoban/ignoreChangesRefresh branch 4 times, most recently from 5163342 to 6f6a1e3 Compare June 12, 2024 08:54
@lunaris
Copy link
Contributor

lunaris commented Jun 12, 2024

@bernhardloos I've added an environment variable PULUMI_ENABLE_LEGACY_REFRESH_DIFF that should allow you to access the old behaviour if you wish; hopefully that works!

@lunaris lunaris force-pushed the lukehoban/ignoreChangesRefresh branch from 6f6a1e3 to 7e05c70 Compare June 12, 2024 09:04
Copy link
Collaborator

@tgummerer tgummerer left a comment

Choose a reason for hiding this comment

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

Did we run the tests with the legacy flag enabled as well, and made sure that no test changes were necessary/there were no output changes?

pkg/backend/display/diff.go Show resolved Hide resolved
pkg/backend/display/rows.go Show resolved Hide resolved
pkg/cmd/pulumi/state_setignorechanges.go Outdated Show resolved Hide resolved
pkg/cmd/pulumi/state_setignorechanges.go Outdated Show resolved Hide resolved
pkg/resource/deploy/step.go Outdated Show resolved Hide resolved
pkg/resource/deploy/step.go Outdated Show resolved Hide resolved
pkg/resource/deploy/step.go Outdated Show resolved Hide resolved
pkg/resource/deploy/step_test.go Outdated Show resolved Hide resolved
@lunaris lunaris force-pushed the lukehoban/ignoreChangesRefresh branch 2 times, most recently from 4dfa899 to 1ef9018 Compare June 12, 2024 11:56
Presently, the behaviour of diffing during refresh steps is incomplete,
returning only an "output diff" that presents the changes in outputs.
This commit changes refresh steps so that:

* they compute a diff similar to the one that would be computed if a
  `preview` were run immediately after the refresh, which is more
  typically what users expect and want; and
* `IgnoreChanges` resource options are respected when performing the new
  desired-state diffs, so that property additions or changes reported by
  a refresh can be ignored.

In particular, `IgnoreChanges` can now be used to acknowledge that part
or all of a resource may change in the provider, but the user is OK with
this and doesn't want to be notified about it during a refresh.
Importantly, this means that the diff won't be reported, but also that
the changes won't be applied to state.

The implementation covers the following:

* A diff is computed using the inputs from the program and then
  inverting the result, since in the case of a refresh the diff is being
  driven by the provider side and not the program. This doesn't change
  what is stored back into the state, but it does produce a diff that is
  more aligned with the "true changes to the desired state".
* `IgnoreChanges` resource options are now stored in state, so that this
  information can be used in refresh operations that do not have access
  to/run the program.
* In the context of a refresh operation, `IgnoreChanges` applies to
  *both* input and output properties. This differs from the behaviour of
  a normal update operation, where `IgnoreChanges` only considers input
  properties.
* The special `"*"` value for `IgnoreChanges` can be used to ignore all
  properties. It _also_ ignores the case where the resource cannot be
  found in the provider, and instead keeps the resource intact in state
  with its existing input and output properties.

Because the program is not run for refresh operations, `IgnoreChanges`
options must be applied separately before a refresh takes place. This
can be accomplished using e.g. a `pulumi up` that applies the options
prior to a refresh. In future we could introduce a `pulumi state set
...` command or the like to modify `IgnoreChanges` resource options in
state directly, but this commit does not address this.

For use cases relying on the legacy refresh diff provider, the
`PULUMI_USE_LEGACY_REFRESH_DIFF` environment variable can be set, which
will disable desired-state diff computation. We only need to perform
checks in `RefreshStep.{ResultOp,Apply}`, since downstream code will
work correctly based on the presence or absence of a `DetailedDiff` in
the step.

Co-authored-by: Luke Hoban <lukehoban@gmail.com>
@lunaris lunaris force-pushed the lukehoban/ignoreChangesRefresh branch from 1ef9018 to 8f96c80 Compare June 12, 2024 11:58
@lunaris
Copy link
Contributor

lunaris commented Jun 12, 2024

Did we run the tests with the legacy flag enabled as well, and made sure that no test changes were necessary/there were no output changes?

This is a great point. As well as adding some to test the flag going forward, I've since gone back and ran the lifecycle tests from HEAD~1 with the legacy flag enabled and they do indeed pass 🙏 Thanks for suggesting this!

Copy link
Collaborator

@tgummerer tgummerer left a comment

Choose a reason for hiding this comment

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

LGTM

@lunaris lunaris added this pull request to the merge queue Jun 12, 2024
Any commits made after this event will not be merged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
6 participants