-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Preview of update plans #8448
Conversation
pkg/resource/deploy/plan.go
Outdated
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] |
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 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?
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.
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.
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.
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.
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.
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.
pkg/resource/deploy/plan.go
Outdated
// 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 |
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.
*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) { |
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.
Are these sorted/deduped, that is are these []
slices actually representing sets? Should we be defensive and sort/dedup here?
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.
Yes they are sets that's why we make the two string maps and then diff that (see inside diffStringSets)
pkg/resource/deploy/plan.go
Outdated
return rp.diffStrings(stringsA, stringsB) | ||
} | ||
|
||
func (rp *ResourcePlan) diffStrings(a, b []string) (message string, changed 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.
Ah ok set logic here. "diffStringSets"? perhaps a more pointed name.
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.
Done
return nil | ||
} | ||
|
||
func (rp *ResourcePlan) checkGoal( |
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 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?
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.
Probably, need to think about how best to display large error sets to the user. I think this will be ok for preview release.
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.
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.
// 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 |
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.
How does this scenario arise?
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.
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.
pkg/resource/deploy/plan.go
Outdated
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] |
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.
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.
// 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. | ||
} |
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'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.
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 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.
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.
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.
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 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?
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.
Yep, definitely. We can probably even ship the preview as-is and revisit this while we gather more data.
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:
Using this feature requires the environment variable PULUMI_EXPERIMENTAL to be true.
Fixes #2318
Checklist
Todo