Fix StatefulSet set-based selector bug#59365
Fix StatefulSet set-based selector bug#59365k8s-github-robot merged 4 commits intokubernetes:masterfrom
Conversation
|
Please add a test :) |
|
/assign @cheftako |
| func getPodLabelsFromData(data runtime.RawExtension) map[string]string { | ||
| var dat map[string]interface{} | ||
| labelMap := make(map[string]string) | ||
| if err := json.Unmarshal(data.Raw, &dat); err != nil { |
There was a problem hiding this comment.
How do we know that the raw extension will always be json and not say protobuf?
There was a problem hiding this comment.
@cheftako Please have a look at my recent commit :)
| var dat map[string]interface{} | ||
| labelMap := make(map[string]string) | ||
| if err := json.Unmarshal(data.Raw, &dat); err != nil { | ||
| return labelMap |
There was a problem hiding this comment.
If the raw data does not parse we should not be silently swallowing that error. Additionally we clearly are missing tests for cases where either the key or value are invalid.
There was a problem hiding this comment.
Will a log warning work? I didn't want it to break like previously if the problem is only with parsing of labels, as they aren't being used anywhere anyway.
| return labelMap | ||
| } | ||
| for key, value := range labels { | ||
| labelMap[key] = value.(string) |
There was a problem hiding this comment.
We should make sure to validate both the key and value to make sure they are valid label key and label values.
There was a problem hiding this comment.
Are we required to validate key and values again? Because they must have already been validated when creating the StatefulSet (or pods).
|
@kubernetes/sig-apps-pr-reviews |
| if err != nil { | ||
| return nil, err | ||
| } | ||
| labelMap := getPodLabelsFromData(data) |
a89d796 to
16a1e75
Compare
There was a problem hiding this comment.
podLabels should never be nil
- The statefulset selector is required to be non-empty
- The podtemplate labels are required to match the selector.
There was a problem hiding this comment.
I had this doubt, did the check anyway to be on safer side. Removed the check now. Please see the recent commit.
16a1e75 to
4f84a1c
Compare
|
lgtm also needs a unit test that uses a set-based selector |
b8ec12a to
eed99b8
Compare
|
@Kargakis I've modified newStatefulSet() to include a set based selector, this should ensure controller revisions don't fail when stateful sets are having set-based selectors. |
There was a problem hiding this comment.
TestSortControllerRevisions was failing because of the incorrect version number (2 was being used for ss1Rev3).
|
/ok-to-test |
eed99b8 to
52ed164
Compare
|
/retest |
|
Why is |
|
The failure looks relevant to the change made in this PR. |
| if err != nil { | ||
| return nil, err | ||
| } | ||
| labelMap := podLabels |
There was a problem hiding this comment.
Need to make a copy here; otherwise, podLabels gets mutated when cr.Labels is mutated in L91.
There was a problem hiding this comment.
And this means template labels (of the given StatefulSet) gets mutated.
|
|
||
| // NewControllerRevision returns a ControllerRevision with a ControllerRef pointing to parent and indicating that | ||
| // parent is of parentKind. The ControllerRevision has labels matching selector, contains Data equal to data, and | ||
| // parent is of parentKind. The ControllerRevision has labels matching pod, contains Data equal to data, and |
There was a problem hiding this comment.
CR's labels is a copy of template labels; "matching" is between selector and labels.
| func NewControllerRevision(parent metav1.Object, | ||
| parentKind schema.GroupVersionKind, | ||
| selector labels.Selector, | ||
| podLabels map[string]string, |
| cr, err := history.NewControllerRevision(set, | ||
| controllerKind, | ||
| selector, | ||
| podLabels, |
| t.Fatal(err) | ||
| } | ||
| ss1Rev1, err := NewControllerRevision(ss1, parentKind, sel1, rawTemplate(&ss1.Spec.Template), 1, nil) | ||
| sel1 := labels.Set(ss1.Spec.Template.Labels).AsSelector() |
There was a problem hiding this comment.
I think sel1 should remain what it was (pulling the actual selector from the StatefulSet). This selector ends up being used to list ControllerRevisions. If the selector has MatchExpressions, it's important that sel1 includes those as well.
There was a problem hiding this comment.
The selector selects ControllerRevisions on the basis of labels, earlier because the labels of controller revisions and selectors of StatefulSet were same, we could use StatefulSet selector. But now the labels of CRs are the template labels, so I changed the CR's selector to that.
Every StatefulSet selector should have a matching pod label (which is also CR's label), so I think the previous version would work as well, but it's not consistent with labels logic.
There was a problem hiding this comment.
It's always safe to use the StatefulSet's selector, because we ensure through validation that it selects the pod template labels. Since we now apply the pod template labels to revisions too, we can also expect that the StatefulSet's selector will match those revisions.
On the other hand, generating a selector purely from the template labels will not always give the right result. Here is a contrived example of that:
...
metadata:
name: ss1
spec:
selector:
matchLabels:
component: mything
matchExpressions:
- {key: role, operator: DoesNotExist}
template:
metadata:
labels:
component: mything
...
---
...
metadata:
name: ss2
spec:
selector:
matchLabels:
component: mything
role: test
template:
metadata:
labels:
component: mything
role: testIf you generate a selector directly from the pod template labels, ss1 would end up also selecting ss2's revisions, whereas ss1's actual selector means that it will avoid selecting ss2's pods. We want the behavior for selecting pods and revisions to be consistent with each other, so we respect the user's intention.
I called this example contrived because it's probably a bad idea for the user to do this. However, the API allows it, so we need to make sure we do the right thing.
| if err != nil { | ||
| return nil, err | ||
| } | ||
| labelMap := podLabels |
There was a problem hiding this comment.
This variable isn't needed anymore since there's no conversion happening. We can just directly use the input parameter as Labels: podLabels.
Nevermind, see comment above by @janetkuo.
There was a problem hiding this comment.
podLabels is not being used anywhere later, I agree with what you struck out. Am I missing something?
There was a problem hiding this comment.
@janetkuo pointed out in the other thread that we mutate this map below on this line:
cr.Labels[ControllerRevisionHashLabel] = strconv.FormatInt(int64(hash), 10)Since map types are references, this means we're mutating the map that the caller passed in to us. So it turns out we do need to make our own labelMap var after all, to hold a deep copy of podLabels. Unless you can find a utility helper for this, you'll need to just allocate a new map and then range loop over the input map to copy each entry.
There was a problem hiding this comment.
Okay, I was thinking that even if it gets mutated it's not affecting anything. But yeah, we should not mutate anything that caller passes.
There was a problem hiding this comment.
it's not affecting anything
It actually mutates template labels (set.Spec.Template.Labels) the caller passes.
There was a problem hiding this comment.
Got it. Thanks.
| t.Fatal(err) | ||
| } | ||
| ss1Rev1, err := NewControllerRevision(ss1, parentKind, sel1, rawTemplate(&ss1.Spec.Template), 1, nil) | ||
| sel1 := labels.Set(ss1.Spec.Template.Labels).AsSelector() |
| Spec: apps.StatefulSetSpec{ | ||
| Selector: &metav1.LabelSelector{ | ||
| MatchLabels: labels, | ||
| MatchExpressions: []metav1.LabelSelectorRequirement{ |
There was a problem hiding this comment.
I agree that this change to the test would have prevented the exact manifestation of the bug we hit (converting a new-style, set-based selector into a string and then trying to parse it as an old-style, map-only selector). However, there are other possible manifestations of the general problem (forgetting about set-based selectors) that this test won't prevent in the future. For example, if we regress to pulling the labels from MatchLabels without converting via string format/parse, this test won't catch that.
I think we'll catch more potential regressions if we include test cases with no MatchLabels at all, so we're sure it will break if any link in the chain ignores the set-based MatchExpressions. We could do that by converting all entries in labels into MatchExpressions (instead of just the first pair), and leaving MatchLabels empty.
Also, we should leave a comment here saying that we are intentionally using a selector with empty MatchLabels to make sure MatchExpressions works as expected, to reduce the chance that someone will weaken our test in the future.
| cr, err := history.NewControllerRevision(set, | ||
| controllerKind, | ||
| selector, | ||
| podLabels, |
There was a problem hiding this comment.
You can directly put set.Spec.Template.Labels here.
84bc5b7 to
c6dfd51
Compare
c6dfd51 to
a269491
Compare
|
/approve |
|
/lgtm |
|
[APPROVALNOTIFIER] This PR is APPROVED This pull-request has been approved by: ayushpateria, enisoc, janetkuo, kow3ns The full list of commands accepted by this bot can be found here. The pull request process is described here DetailsNeeds approval from an approver in each of these files:
Approvers can indicate their approval by writing |
|
The /test pull-kubernetes-e2e-gce |
|
fix PR for verify merged |
|
/test all [submit-queue is verifying that this PR is safe to merge] |
|
Automatic merge from submit-queue. If you want to cherry-pick this change to another branch, please follow the instructions here. |
| cr, err := history.NewControllerRevision(set, | ||
| controllerKind, | ||
| selector, | ||
| set.Spec.Template.Labels, |
There was a problem hiding this comment.
What happens if the selector doesn't match these labels?
There was a problem hiding this comment.
Validation prevents a StatefulSet with a selector that doesn't match its own template labels from being created. apps/v1 StatefulSet's selector cannot be changed after creation.
What this PR does / why we need it:
ControllerRevisions were using selectors as the labels, in case of set-based selectors, the helper function to convert selectors to labels would break. This PR uses pod labels for ControllerRevision labels instead of selectors.
Which issue(s) this PR fixes (optional, in
fixes #<issue number>(, fixes #<issue_number>, ...)format, will close the issue(s) when PR gets merged):Fixes #59266
Special notes for your reviewer:
I'm trying to learn Kubernetes codebase and would be happy to make changes if anything is off.
Release note: