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

Make UnstructuredContent return contents without mutating the source #62063

Merged

Conversation

ash2k
Copy link
Member

@ash2k ash2k commented Apr 3, 2018

What this PR does / why we need it:
This PR solves the issues described in #56316

Before this change:

  • A call to UnstructuredContent() potentially modified Object
  • The values returned by UnstructuredContent() could be manipulated to modify the value in Object. Going through the history it looks like this behavior was added before the addition of SetUnstructuredContent(). IMO it makes more sense now to use SetUnstructuredContent() or make changes to the exposed Object property
  • UnstructuredList did not implement the behavior described in the godoc. The godoc stated that the value returned should be mutable, but if u.Object == nil the map returned had no effect on Object

With this PR I'm proposing UnstructuredContent() returns the data without providing the contract of a mutable map. It also ensures all implementations of the Unstructured interface abide by the doc

Which issue(s) this PR fixes:
Fixes #56316

Special notes for your reviewer:
This PR continues work started in #57713.

Release note:

NONE

/kind bug
/sig api-machinery
/cc sttts deads2k

@k8s-ci-robot k8s-ci-robot added cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. release-note-none Denotes a PR that doesn't merit a release note. labels Apr 3, 2018
@k8s-ci-robot k8s-ci-robot added kind/bug Categorizes issue or PR as related to a bug. size/M Denotes a PR that changes 30-99 lines, ignoring generated files. sig/api-machinery Categorizes an issue or PR as relevant to SIG API Machinery. labels Apr 3, 2018
@ash2k
Copy link
Member Author

ash2k commented Apr 3, 2018

/retest

out := u.Object
if out == nil {
out = make(map[string]interface{})
out := make(map[string]interface{}, len(u.Object)+1)
Copy link
Contributor

Choose a reason for hiding this comment

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

why +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.

The contents may not have items.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok, so the case in line 62 should not happen?

Copy link
Member Author

Choose a reason for hiding this comment

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

u.Object may or may not contain items. We can remove if in the loop because the key will be overwritten anyway. It made sense to have it before (in the previous PR) because a deep copy was made here and skipping items reduced the unnecessary work. WDYT?

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've removed the if.

@sttts
Copy link
Contributor

sttts commented Apr 3, 2018

Could we make Unstructured/UnstructuredList.Object private?

@ash2k
Copy link
Member Author

ash2k commented Apr 3, 2018

Could we make Unstructured/UnstructuredList.Object private?

It is quite convenient to be able to construct one of these as a literal, especially as part of a bigger literal. For example https://github.com/atlassian/smith/blob/ba1ff1e867c74ee29f1172257b5b555134ca2e77/pkg/controller/spec_processor_test.go#L332-L338

However I can also see value in hiding it and making everyone access the contents via the methods. Maybe we can have a constructor NewUnstructured(map[string]interface{}) to replace literals?
BTW, the constructor and the setter can do type checks if, say, an environment variable is set to catch misuse. Ok, that would, I think, add enough value to do it. WDYT?

@sttts
Copy link
Contributor

sttts commented Apr 4, 2018

A constructor sounds good.

@ash2k
Copy link
Member Author

ash2k commented Apr 4, 2018

@sttts can we merge this one first and then I'll submit another one with the constructor change? It is going to be a big PR and I think it makes sense to keep the changes separate. Also this is a bugfix and a refactoring - should be separate.

@ash2k ash2k force-pushed the no-mutation-unstructuredcontent branch from 5d14bf4 to d5fdac3 Compare April 4, 2018 10:55
@@ -2133,14 +2133,14 @@ URL: http://localhost
"name": "MyName",
"namespace": "MyNamespace",
"creationTimestamp": "2017-04-01T00:00:00Z",
"resourceVersion": 123,
"resourceVersion": int64(123),
Copy link
Contributor

Choose a reason for hiding this comment

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

this is unrelated. Can you move it to another PR? (I cannot approve this).

Copy link
Member Author

Choose a reason for hiding this comment

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

Most likely it was causing a failure because 123 literal has an int type by default but deep copy only likes float64 and int64. But this fix is no longer needed because we are not making a deep copy.

var u Unstructured
content := u.UnstructuredContent()
expContent := make(map[string]interface{})
assert.EqualValues(t, expContent, content)
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you check that the actual object stays the same? Make a deep copy before and then compare afterwards.

@@ -0,0 +1,30 @@
/*
Copyright 2017 The Kubernetes Authors.
Copy link
Contributor

Choose a reason for hiding this comment

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

2018

@sttts
Copy link
Contributor

sttts commented Apr 4, 2018

can we merge this one first and then I'll submit another one with the constructor change?

yes, makes sense.

switch x := x.(type) {
case map[string]interface{}:
if x == nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

does this really exist?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, without this change (and below for completeness) the newly added check in the test (deep copy and compare) was failing. https://play.golang.org/p/KASoa2DeTnp

Copy link
Member Author

Choose a reason for hiding this comment

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

This will change the output - this code used to produce non-nil maps and slices for typed nils but now will produce typed nils. JSON encoding will change - typed nils as null in JSON (https://play.golang.org/p/Uz2ajLEQx9A).

I think this change is a good one - it makes the deep copy more precise.

Copy link
Contributor

Choose a reason for hiding this comment

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

We should never see typed nils somewhere deep inside IMO. On the toplevel maybe.

Compare https://play.golang.org/p/R45uKKqDsm6

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, we should not see them, but they exist. I'm fine with removing this fix from this PR if that's what you think is best. However I think that way deep copy is more correct. It just mirrors the input precisely. If there are no typed nils, we don't introduce them here. If they create a problem somewhere then the place that "spawns" them should be fixed. Or, even better, a place that does not handle them well could be made more robust.

Also, I'm not sure what exactly you wanted me to compare? typed and untyped nils are both marshaled into null, yes :)

Copy link
Contributor

Choose a reason for hiding this comment

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

Have we ever seen this "cannot deep copy" panic? You say that they show up. Do you know where?

Copy link
Member Author

Choose a reason for hiding this comment

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

Panic? Current code does not cause any panics AFAIK. Are you talking about failing test without these changes? It does not panic, it just fails because objects are not equal. They are not equal because the deep copied Object field ("expected" in the test) is an empty map and the original is `nil.

func TestNilUnstructuredContent(t *testing.T) {
	var u Unstructured
	uCopy := u.DeepCopy()
	content := u.UnstructuredContent()
	expContent := make(map[string]interface{})
	assert.EqualValues(t, expContent, content)
	assert.Equal(t, uCopy, &u)
}
--- FAIL: TestNilUnstructuredContent (0.00s)
	Error Trace:	unstructured_test.go:31
	Error:      	Not equal: 
	            	expected: &unstructured.Unstructured{Object:map[string]interface {}{}}
	            	actual: &unstructured.Unstructured{Object:map[string]interface {}(nil)}
	            	
	            	Diff:
	            	--- Expected
	            	+++ Actual
	            	@@ -1,4 +1,3 @@
	            	 (*unstructured.Unstructured)({
	            	- Object: (map[string]interface {}) {
	            	- }
	            	+ Object: (map[string]interface {}) <nil>
	            	 })

@ash2k
Copy link
Member Author

ash2k commented Apr 5, 2018

/retest

clone := make([]interface{}, len(x))
for i, v := range x {
clone[i] = DeepCopyJSONValue(v)
}
return clone
case string, int64, bool, float64, nil, encodingjson.Number:
case string, int64, bool, float64, encodingjson.Number:
Copy link
Contributor

Choose a reason for hiding this comment

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

why not keep the nil and remove the if clause above?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good question. My IntelliJ is complaining that nil is not a valid type but Go compiler is happy 🤷‍♂️ It actually works fine https://play.golang.org/p/PBIyhFgWn8H
I'll revert this change.

Copy link
Contributor

Choose a reason for hiding this comment

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

Don't feel strong about it. Was just curious.

@ash2k
Copy link
Member Author

ash2k commented Apr 5, 2018

/retest

@sttts
Copy link
Contributor

sttts commented Apr 5, 2018

the verify job is broken: #62162

/lgtm
/approve

@k8s-ci-robot k8s-ci-robot added the lgtm "Looks good to me", indicates that a PR is ready to be merged. label Apr 5, 2018
@k8s-ci-robot
Copy link
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: ash2k, sttts

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@k8s-ci-robot k8s-ci-robot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label Apr 5, 2018
@fejta-bot
Copy link

/retest
This bot automatically retries jobs that failed/flaked on approved PRs (send feedback to fejta).

Review the full test history for this PR.

Silence the bot with an /lgtm cancel comment for consistent failures.

2 similar comments
@fejta-bot
Copy link

/retest
This bot automatically retries jobs that failed/flaked on approved PRs (send feedback to fejta).

Review the full test history for this PR.

Silence the bot with an /lgtm cancel comment for consistent failures.

@fejta-bot
Copy link

/retest
This bot automatically retries jobs that failed/flaked on approved PRs (send feedback to fejta).

Review the full test history for this PR.

Silence the bot with an /lgtm cancel comment for consistent failures.

@k8s-github-robot
Copy link

/test all [submit-queue is verifying that this PR is safe to merge]

@k8s-github-robot
Copy link

Automatic merge from submit-queue. If you want to cherry-pick this change to another branch, please follow the instructions here.

@k8s-github-robot k8s-github-robot merged commit 2dee210 into kubernetes:master Apr 6, 2018
@ash2k ash2k deleted the no-mutation-unstructuredcontent branch April 6, 2018 01:29
@wenjiaswe
Copy link
Contributor

cc @mbohlool

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
approved Indicates a PR has been approved by an approver from all required OWNERS files. cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. kind/bug Categorizes issue or PR as related to a bug. lgtm "Looks good to me", indicates that a PR is ready to be merged. release-note-none Denotes a PR that doesn't merit a release note. sig/api-machinery Categorizes an issue or PR as relevant to SIG API Machinery. size/M Denotes a PR that changes 30-99 lines, ignoring generated files.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

runtime.Unstructured.UnstructuredContent() is easy to misuse
6 participants