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

Preview of update plans #8448

Merged
merged 160 commits into from
Jan 31, 2022
Merged

Preview of update plans #8448

merged 160 commits into from
Jan 31, 2022

Conversation

Frassle
Copy link
Member

@Frassle Frassle commented Nov 18, 2021

Description

This PR adds the concept of "update plans" to pulumi. Plans can be generated via a preview command with the extra argument "--save-plan=<file_path>.json". This will run the preview and save the set of expected operations to the given JSON file. The user can then run update with "--plan=<file_path>.json" to run a constrained update. This will only allow operations that are either recorded in the plan file or are deemed a safe subset of those operations.

Some examples of safe subsets:

  • If a plan file says nothing about resource R but the update doesn't want to change anything about resource R this is not a constraint error.
  • If a plan says resource R should be updated to value X, but at update time it's already at value X this is not a constraint error.

Using this feature requires the environment variable PULUMI_EXPERIMENTAL to be true.

Fixes #2318

Checklist

  • I have added tests that prove my fix is effective or that my feature works
  • Yes, there are changes in this PR that warrants bumping the Pulumi Service API version

Todo

  • Auto refresh in update if --constraint-file is given
  • Work out what to do about auto preview in update if --constraint-file is given

for _, e := range os.Environ() {
// golang has so many questionable design decisions, lets just hope no one puts '=' in there env var names.
pair := strings.SplitN(e, "=", 2)
environmentVariables[pair[0]] = pair[1]
Copy link
Member

Choose a reason for hiding this comment

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

There was a comment elsewhere these were encrypted (at the struct property), but there's no encryption I can see. So passwords from env vars would go into EnvironmentVariables prop. Should we change the comments here?

Copy link
Member Author

Choose a reason for hiding this comment

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

config values are encrypted/decrypted as used. env vars are encrypted when we write them out. We could change this so we pass a crypter in to NewPlan and just encrypt the envvars straight away.

Copy link
Member

Choose a reason for hiding this comment

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

Just curious whether or not we need the envvars? More information is probably good, but I might leave them out of the preview if we're not yet sure how we'll use them.

Copy link
Member Author

Choose a reason for hiding this comment

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

They were in the prototype branch and I just kept them, I'm not sure if we would ever do anything with them. There's so many other ways users could cause their programs to be non-deterministic.

// the resource's checked input properties we expect to change.
InputDiff PlanDiff
// the resource's output properties we expect to change (only set for RegisterResourceOutputs)
OutputDiff *PlanDiff
Copy link
Member

Choose a reason for hiding this comment

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

*PlanDiff intentional vs PlanDiff? So for non RegisterResourceOutputs it's nil?

Outputs resource.PropertyMap
}

func (rp *ResourcePlan) diffURNs(a, b []resource.URN) (message string, changed bool) {
Copy link
Member

Choose a reason for hiding this comment

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

Are these sorted/deduped, that is are these [] slices actually representing sets? Should we be defensive and sort/dedup here?

Copy link
Member Author

@Frassle Frassle Jan 24, 2022

Choose a reason for hiding this comment

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

Yes they are sets that's why we make the two string maps and then diff that (see inside diffStringSets)

return rp.diffStrings(stringsA, stringsB)
}

func (rp *ResourcePlan) diffStrings(a, b []string) (message string, changed bool) {
Copy link
Member

Choose a reason for hiding this comment

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

Ah ok set logic here. "diffStringSets"? perhaps a more pointed name.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

return nil
}

func (rp *ResourcePlan) checkGoal(
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 is written with early termination semantic (return first error), would it be sometimes helpful to collect and present all errors or it does not matter?

Copy link
Member Author

Choose a reason for hiding this comment

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

Probably, need to think about how best to display large error sets to the user. I think this will be ok for preview release.

@t0yv0 t0yv0 self-requested a review January 21, 2022 16:09
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.

Finished reading through - LGTM, added a few comments/ questions. My familiarity with some of the code is a bit light, esp around stateful struct updates + flow through the system. I believe @pgavlin was intending to do a review pass here also.

pkg/cmd/pulumi/preview.go Outdated Show resolved Hide resolved
pkg/cmd/pulumi/preview.go Outdated Show resolved Hide resolved
pkg/cmd/pulumi/util.go Outdated Show resolved Hide resolved
Comment on lines +287 to +288
// We haven't done a delete for this resource check if it was in the snapshot,
// if it's already gone this wasn't done because it wasn't needed
Copy link
Member

Choose a reason for hiding this comment

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

How does this scenario arise?

Copy link
Member Author

Choose a reason for hiding this comment

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

If you planned to delete a resource, then someone else deletes it, then you run your plan. End result is the same, the resource is deleted so it didn't feel like it should be an error case.

for _, e := range os.Environ() {
// golang has so many questionable design decisions, lets just hope no one puts '=' in there env var names.
pair := strings.SplitN(e, "=", 2)
environmentVariables[pair[0]] = pair[1]
Copy link
Member

Choose a reason for hiding this comment

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

Just curious whether or not we need the envvars? More information is probably good, but I might leave them out of the preview if we're not yet sure how we'll use them.

Comment on lines +63 to +68
// PlanDiff holds the results of diffing two object property maps.
type PlanDiff struct {
Adds resource.PropertyMap // the resource's properties we expect to add.
Deletes []resource.PropertyKey // the resource's properties we expect to delete.
Updates resource.PropertyMap // the resource's properties we expect to update.
}
Copy link
Member

Choose a reason for hiding this comment

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

I'm a little surprised by the way we're storing diffs.

I think that using the inputs like this imposes a very strong requirement that all changes are visible via the inputs--i.e. there are no input vs. state differences. This is different from the model used by providers, where diffs can be observed either from input <-> input or from input <-> state. An alternative approach would be to store the detailed diff as returned by the provider (polyfilling a detailed diff where necessary), then checking that the detailed diff from the update is constrained to the detailed diff from the preview, with old values sourced from the old inputs or old state as directed by the detailed diff.

I think that what is implemented might be okay assuming that in the vast majority of cases all input <-> state differences are essentially input <-> input differences b/c the differing property has the same value in the state and the inputs (though that assumption requires that the provider's Read method can accurately reflect values from the state back to the inputs).

I was also a little surprised that we're storing a flat list of top-level diffs rather than the tree diff (ala resource.ValueDiff). Is there a benefit to the flat list? Storing the tree diff might make error presentation a little more precise.

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 think that using the inputs like this imposes a very strong requirement that all changes are visible via the inputs

What would an input state difference be? I'm not sure I follow how we could have a state difference and not also have an input difference?

Is there a benefit to the flat list?

Depends on what semantics we're going for. Like if you have a dict property and you run a plan to go from{a: 1, b: 2} to {a: 1, c: 3} is that recording "update to {a: 1, c: 3}" or "delete key b, add key c with 3"? Because the former only allows a plan to execute if it ends up with the value {a: 1, c: 3} but the later would allow someone to come and change the dict to something like {b: 2, d: 4} and the plan would allow the value {c: 3, d: 4} but not allow the value {a: 1, c: 3}.

Might be that second semantic is useful but it seemed more complex to code and explain and I don't have a concrete scenario that would use it so I err'ed on simplicity.

Copy link
Member

Choose a reason for hiding this comment

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

What would an input state difference be? I'm not sure I follow how we could have a state difference and not also have an input difference?

Consider a resource R with (inputs, state) => ({foo: bar}, {foo: bar}). If we refresh R and the provider returns ({foo: bar}, {foo: baz}), the provider is still allowed to indicate that R needs to be updated on a subsequent run even if the program presents the inputs {foo: bar} b/c foo has different properties in the inputs and the state. This is arguably a bug, and I'm not sure it happens in practice, but it is a "feature" of the model that we might need to consider.

More common is the case where an input has no corresponding entry in the state, so diffs are only detectable from input to input (e.g. k8s secrets).

the later would allow someone to come and change the dict to something like {b: 2, d: 4} and the plan would allow the value {c: 3, d: 4} but not allow the value {a: 1, c: 3}.

I think the latter would still allow {a: 1, c: 3} b/c the same on c is constrained to the add in the diff. I think the argument here is the same as for the analogous case at top level in that we want to allow the base state to differ as long as the diffs are compatible.

Copy link
Member

Choose a reason for hiding this comment

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

This is good feedback and it sounds like we need to give it a thought. Would it be okay to go ahead with this PR to kick off examples testing and do this work as a follow-up?

Copy link
Member

Choose a reason for hiding this comment

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

Yep, definitely. We can probably even ship the preview as-is and revisit this while we gather more data.

@Frassle Frassle merged commit 40eee58 into master Jan 31, 2022
@pulumi-bot pulumi-bot deleted the fraser/planPrototype branch January 31, 2022 10:31
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.

Enable constraining an update or refresh to a preview result
6 participants