Skip to content

Conversation

@VenelinMartinov
Copy link
Contributor

@VenelinMartinov VenelinMartinov commented Oct 17, 2024

This PR changes the logic in the detailed diff v2 calculation to short-circuit on encountering nils and unknowns.

Previously, when we encountered a null or unknown in either news or olds we would still recurse down and compare the non-nil values versus a nil value in order to get any replacements which might be happening. We would then simplify the diff later to trim these extra diffs but would propagate the replacement to the right level.

We would now instead short-circuit on encountering a null or unknown and walk the non-null value to find any properties which can trigger a replacement. This makes it much easier to handle sets in #2451 as recursing into sets is not necessary, as they are only compared by hashes.

This change is not meant to change any behaviour and is tested in #2405

Stacked on #2515 and #2516

@codecov
Copy link

codecov bot commented Oct 17, 2024

Codecov Report

Attention: Patch coverage is 90.72165% with 9 lines in your changes missing coverage. Please review.

Project coverage is 62.73%. Comparing base (a1be35d) to head (749dfbb).
Report is 6 commits behind head on master.

Files with missing lines Patch % Lines
pkg/tfbridge/detailed_diff.go 90.72% 6 Missing and 3 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #2496      +/-   ##
==========================================
+ Coverage   62.71%   62.73%   +0.02%     
==========================================
  Files         381      382       +1     
  Lines       51515    51545      +30     
==========================================
+ Hits        32307    32338      +31     
+ Misses      17395    17394       -1     
  Partials     1813     1813              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@VenelinMartinov VenelinMartinov marked this pull request as ready for review October 17, 2024 17:37
@iwahbe
Copy link
Member

iwahbe commented Oct 18, 2024

Reviewing the tests added in #2405, they look sufficient for nested forcenew in top level and singly nested cases. Can you point me to where multiple nesting levels are tested? I want to see a test that validates our behavior on a schema like this:

List{
  ForceNew: true,
  Elem: Object{
    Fields: {
       "f0": String,
    }
  },
}

I couldn't find tests for more then one level of nesting.

…refactor_make_detailed_diff_to_short_circuit
@VenelinMartinov VenelinMartinov changed the base branch from master to vvm/collection_force_new_detailed_diff_tests October 28, 2024 15:43
@VenelinMartinov VenelinMartinov changed the base branch from vvm/collection_force_new_detailed_diff_tests to vvm/detailed_diff_v2_unknown_collections October 28, 2024 15:50
@VenelinMartinov
Copy link
Contributor Author

Reviewing the tests added in #2405, they look sufficient for nested forcenew in top level and singly nested cases. Can you point me to where multiple nesting levels are tested? I want to see a test that validates our behavior on a schema like this:

Here's the tests for ForceNew:

func TestDetailedDiffTFForceNewPlain(t *testing.T) {

func TestDetailedDiffTFForceNewAttributeCollection(t *testing.T) {

func TestDetailedDiffTFForceNewBlockCollection(t *testing.T) {

func TestDetailedDiffTFForceNewElemBlockCollection(t *testing.T) {

func TestDetailedDiffTFForceNewObject(t *testing.T) {

I believe I've covered all shapes and nestings of TF schemas there.

I've also added additional tests which focus on how forceNew interacts with Unknowns in #2515

VenelinMartinov added a commit that referenced this pull request Oct 28, 2024
This adds tests to record the existing behaviour around ForceNew
interacting with collection and collection element changes with
unknowns.

Addresses
#2496 (comment)
Base automatically changed from vvm/detailed_diff_v2_unknown_collections to master October 28, 2024 19:50
VenelinMartinov added a commit that referenced this pull request Oct 28, 2024
…ed diff v2 (#2516)

This re-records the tests in
#2515 under
detailed diff v2 to show the changes.

The new behaviour is more correct: When the parent of a known property
with ForceNew is changed to unknown we now mark the resource for
replacement. Both are guesses to the actual effect of the changes but
this one seems more likely.

stacked on https://github.com/pulumi/pulumi-terraform-bridge/pull/2515\

related to
#2496 (comment)
@t0yv0
Copy link
Member

t0yv0 commented Oct 29, 2024

Here's a case where it just returns an empty set of diffs (no changes) because "the shape mismatched". Why?

func TestAddingSecretArray(t *testing.T) {
	old := resource.PropertyMap{"x": resource.NewNullProperty()}
	new := resource.PropertyMap{"x": resource.MakeSecret(resource.NewArrayProperty([]resource.PropertyValue{
		resource.NewStringProperty("SECRET"),
	}))}
	tfs := shimv2.NewSchemaMap(map[string]*schema.Schema{
		"x": &schema.Schema{
			Type: schema.TypeList,
			Elem: &schema.Schema{
				Type: schema.TypeString,
			},
			Optional: true,
		},
	})
	runDetailedDiffTest(t, old, new, tfs, nil, map[string]*pulumirpc.PropertyDiff{
		"x": &pulumirpc.PropertyDiff{Kind: pulumirpc.PropertyDiff_ADD},
	})
}

@VenelinMartinov
Copy link
Contributor Author

VenelinMartinov commented Oct 29, 2024

@t0yv0 the test you posted seems to work fine. It returns an {x: ADD} which is the expected value.

I guess we might get it off in cases like these:

func TestMistypedArray(t *testing.T) {
	old := resource.PropertyMap{"x": resource.NewStringProperty("a")}
	new := resource.PropertyMap{"x": resource.NewArrayProperty([]resource.PropertyValue{
		resource.NewStringProperty("VAL"),
	})}
	tfs := shimv2.NewSchemaMap(map[string]*schema.Schema{
		"x": &schema.Schema{
			Type: schema.TypeList,
			Elem: &schema.Schema{
				Type: schema.TypeString,
			},
			Optional: true,
		},
	})
	runDetailedDiffTest(t, old, new, tfs, nil, map[string]*pulumirpc.PropertyDiff{
		"x[0]": &pulumirpc.PropertyDiff{Kind: pulumirpc.PropertyDiff_ADD},
	})
}

Here we present an ADD diff when it is really an UPDATE. I think this is enough of a corner case and the diff is still comprehensible that it's probably fine.

Additionally the behaviour was there before, I am fairly sure this PR does not change that.

@t0yv0
Copy link
Member

t0yv0 commented Oct 29, 2024

Hmm, interesting, do I have a problem with local setup? The test fails for me.

=== RUN   TestAddingSecretArray
    detailed_diff_test.go:311: 
        	Error Trace:	/Users/anton/code/pulumi-terraform-bridge/pkg/tfbridge/detailed_diff_test.go:294
        	            				/Users/anton/code/pulumi-terraform-bridge/pkg/tfbridge/detailed_diff_test.go:311
        	Error:      	Not equal: 
        	            	expected: map[string]*pulumirpc.PropertyDiff{"x":(*pulumirpc.PropertyDiff)(0x140009b94a0)}
        	            	actual  : map[string]*pulumirpc.PropertyDiff{}
        	            	
        	            	Diff:
        	            	--- Expected
        	            	+++ Actual
        	            	@@ -1,17 +1,2 @@
        	            	-(map[string]*pulumirpc.PropertyDiff) (len=1) {
        	            	- (string) (len=1) "x": (*pulumirpc.PropertyDiff)({
        	            	-  state: (impl.MessageState) {
        	            	-   NoUnkeyedLiterals: (pragma.NoUnkeyedLiterals) {
        	            	-   },
        	            	-   DoNotCompare: (pragma.DoNotCompare) {
        	            	-   },
        	            	-   DoNotCopy: (pragma.DoNotCopy) {
        	            	-   },
        	            	-   atomicMessageInfo: (*impl.MessageInfo)(<nil>)
        	            	-  },
        	            	-  sizeCache: (int32) 0,
        	            	-  unknownFields: ([]uint8) <nil>,
        	            	-  Kind: (pulumirpc.PropertyDiff_Kind) 0,
        	            	-  InputDiff: (bool) false
        	            	- })
        	            	+(map[string]*pulumirpc.PropertyDiff) {
        	            	 }
        	Test:       	TestAddingSecretArray

@t0yv0
Copy link
Member

t0yv0 commented Oct 29, 2024

Additionally the behaviour was there before, I am fairly sure this PR does not change that.

So we have a bug, yes?

If I replace code with this:

func isTypeShapeMismatched(val resource.PropertyValue, propType shim.ValueType) bool {
	return false
}

All the tests still pass.

@VenelinMartinov
Copy link
Contributor Author

I'll add some tests to make the behaviour clearer

@t0yv0
Copy link
Member

t0yv0 commented Oct 29, 2024

I've pulled your latest changes, and now TestAddingSecretArray panics.

@VenelinMartinov
Copy link
Contributor Author

AFAIK the bridge doesn't accept secrets and the engine shouldn't send secrets, is that correct?

@t0yv0
Copy link
Member

t0yv0 commented Oct 29, 2024

Yes, but, we will need to change that to support secrets properly, and at a minimum we need the work backlogged so we know there are additional bugs in diff code.

I think I'd like to see the code factored out and made a little simpler until we have confidence that we're not adding panics into the product or replacing diffs with nil diffs.

Your TestMistypedArray is another bug example. To reduce confidence further, I get a different result on my machine than claimed. IT produces an "x": ADD diff. It really an UPDATE as you point out.

What are the next steps here, are we fixing the code or logging known bugs?

@VenelinMartinov
Copy link
Contributor Author

VenelinMartinov commented Oct 29, 2024

Handling secrets is out of scope for the epic - what panics is an assert saying we don't support secrets in detailed diff.

I'll add an item to add additional tests for mistyped state so that we can decide if this is worth including in the epic. This is out of scope for this PR.

EDIT: I've changed the logic for typeMismatch to trigger an update instead of assume the property is Null and also added tests for this.

@t0yv0
Copy link
Member

t0yv0 commented Oct 29, 2024

I've added #2525 that'd give some basic confidence that these asserts do not result in panics.

@t0yv0 t0yv0 self-requested a review October 29, 2024 14:37
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.

This is better. LGTM now, but don't try to handle Outputs incorrectly, rather panic, I think we've looked and they do not show up in the bridge protocol I think.

If there's any know remaining bugs in this code, please file, otherwise lgtm.

@VenelinMartinov
Copy link
Contributor Author

VenelinMartinov commented Oct 29, 2024

Discussed offline, will add Output handling in a separate PR - Outputs are not currently used and we can afford to completely strip them for the detailed diff algorithm as it doesn't need the dependency information or secrets.

EDIT: Opened #2526

@VenelinMartinov VenelinMartinov merged commit bbb5f5c into master Oct 29, 2024
17 checks passed
@VenelinMartinov VenelinMartinov deleted the vvm/refactor_make_detailed_diff_to_short_circuit branch October 29, 2024 17:36
VenelinMartinov added a commit that referenced this pull request Oct 29, 2024
This is a pure refactor and just moves the propertyPath-related code to
a new file. This is a separate PR to make reviewing easier.

Stacked on #2515,
#2516 and
#2496
VenelinMartinov added a commit that referenced this pull request Oct 30, 2024
This change adds improved TF set handling to the detailed diff v2. The
main challenge here is that pulumi does not have native sets, so set
types are represented as lists.

### Diffing sets using the hash ###

To correctly find the diff of two sets we calculate the hash of each
element ourselves and do the diffs based on that. What makes this
somewhat non-trivial is that due to MaxItemsOne flattening we can't just
hash the pulumi `PropertyValue`s given to us by the engine. Instead we
use `makeSingleTerraformInput` to transform the values using the schema.
We then use the hashes of the elements in the set to calculate the
diffs. This allows us to correctly account for shuffling and duplicates,
matching the terraform behaviour of sets.

When returning the element indices back to the engine, we need to return
them in terms of oldState and newInputs because the engine does not have
access to the plannedState (see
#2281). To do
that we keep the newInputs and match plannedState elements to their
newInputs index by the set hash. Note that this is not possible if the
set element contains any computed properties - these can change during
the planning process, so we do not attempt to match and print a warning
about possibly inaccurate diff results instead.


### Unknowns in sets ###
Note that the terraform planning process does not allow a set to contain
any unknowns, because that breaks the hashing. Because of that plan
should always return an unknown for a set which contains any unknowns.
This accounts for cases where resolving the unknown can result in
duplicate elements.

Unknown elements in sets - the whole set becomes unknown in the plan, so
the algorithm no longer works. Currently we return an update for the
whole set to the engine and it does the diff on its side.

### Testing ###
This PR also includes tests for the set behaviour, both unit tests for
the detailed diff algorithm and integration tests using pulumi programs
for:
- Single element additions, updates and removals
- Shuffling, also with additions, updates and removals
- Multi-element additions, updates and removals
- Unknowns

### Issues ###

Builds on #2405

Stacked on #2515,
#2516,
#2496 and
#2497

fixes #2200
fixes #2300
fixes #1904
fixes #186
@mjeffryes mjeffryes added this to the 0.112 milestone Oct 30, 2024
@pulumi-bot
Copy link
Contributor

This PR has been shipped in release v3.94.0.

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.

5 participants