-
Notifications
You must be signed in to change notification settings - Fork 325
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
[Net-5510][Net-5455]: CRD controller should only patch the finalizer in the metadata of a CRD, rather than the whole object #3362
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
```release-note:bug-fix | ||
crd-controllers: When the CRD controller reconciles a config entry CRD, only patch finalizers in the metadata of the CRD, rather than updating the entire entry, which causes changes to the CRD spec (such as adding in unspecified zero values) when using a Kubernetes client such as kubectl with `replace` mode. | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
78 changes: 78 additions & 0 deletions
78
control-plane/config-entries/controllers/finalizer_patch.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
package controllers | ||
|
||
import ( | ||
"encoding/json" | ||
|
||
jsonpatch "github.com/evanphx/json-patch" | ||
"k8s.io/apimachinery/pkg/types" | ||
"sigs.k8s.io/controller-runtime/pkg/client" | ||
) | ||
|
||
type FinalizerPatcher struct{} | ||
|
||
type FinalizerPatch struct { | ||
NewFinalizers []string | ||
} | ||
|
||
// Type implements client.Patch. Since this patch is used for a custom CRD, Kubernetes does not allow the more advanced | ||
// StrategicMergePatch. Therefore, this patcher will replace the entire list of finalizers with the new list, rather | ||
// than adding/removing individual entries. | ||
// | ||
// This can result in a small race condition where we could overwrite recently modified finalizers (either modified by a | ||
// user or another controller process). Before the addition of this finalizer patcher implementation, this race | ||
// condition still existed, but applied to the entirety of the CRD because we used to update the entire CRD rather than | ||
// just the finalizer, so this reduces the surface area of the race condition. Generally we should not expect users or | ||
// other controllers to be touching the finalizers of consul-k8s managed CRDs. | ||
func (fp *FinalizerPatch) Type() types.PatchType { | ||
return types.MergePatchType | ||
} | ||
|
||
var _ client.Patch = (*FinalizerPatch)(nil) | ||
|
||
func (f *FinalizerPatcher) AddFinalizersPatch(oldObj client.Object, addFinalizers ...string) *FinalizerPatch { | ||
output := make([]string, 0, len(addFinalizers)) | ||
existing := make(map[string]bool) | ||
for _, f := range oldObj.GetFinalizers() { | ||
existing[f] = true | ||
output = append(output, f) | ||
} | ||
for _, f := range addFinalizers { | ||
if !existing[f] { | ||
output = append(output, f) | ||
} | ||
} | ||
return &FinalizerPatch{ | ||
NewFinalizers: output, | ||
} | ||
} | ||
|
||
func (f *FinalizerPatcher) RemoveFinalizersPatch(oldObj client.Object, removeFinalizers ...string) *FinalizerPatch { | ||
output := make([]string, 0) | ||
remove := make(map[string]bool) | ||
for _, f := range removeFinalizers { | ||
remove[f] = true | ||
} | ||
for _, f := range oldObj.GetFinalizers() { | ||
if !remove[f] { | ||
output = append(output, f) | ||
} | ||
} | ||
return &FinalizerPatch{ | ||
NewFinalizers: output, | ||
} | ||
} | ||
|
||
// Data implements client.Patch. | ||
func (fp *FinalizerPatch) Data(obj client.Object) ([]byte, error) { | ||
newData, err := json.Marshal(map[string]any{ | ||
"metadata": map[string]any{ | ||
"finalizers": fp.NewFinalizers, | ||
}, | ||
}) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
p, err := jsonpatch.CreateMergePatch([]byte(`{}`), newData) | ||
return p, err | ||
} |
97 changes: 97 additions & 0 deletions
97
control-plane/config-entries/controllers/finalizer_patch_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
package controllers | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/require" | ||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"sigs.k8s.io/controller-runtime/pkg/client" | ||
|
||
"github.com/hashicorp/consul-k8s/control-plane/api/v1alpha1" | ||
) | ||
|
||
func TestFinalizersPatcher(t *testing.T) { | ||
cases := []struct { | ||
name string | ||
oldObject client.Object | ||
addFinalizers []string | ||
removeFinalizers []string | ||
expectedFinalizerPatch *FinalizerPatch | ||
op string | ||
}{ | ||
{ | ||
name: "adds finalizers at the end and keeps the original list in order", | ||
oldObject: &v1alpha1.ServiceResolver{ | ||
ObjectMeta: v1.ObjectMeta{ | ||
Finalizers: []string{ | ||
"a", | ||
"b", | ||
"c", | ||
}, | ||
}, | ||
}, | ||
addFinalizers: []string{"d", "e"}, | ||
expectedFinalizerPatch: &FinalizerPatch{ | ||
NewFinalizers: []string{"a", "b", "c", "d", "e"}, | ||
}, | ||
}, | ||
{ | ||
name: "adds finalizers when original list is empty", | ||
oldObject: &v1alpha1.ServiceResolver{ | ||
ObjectMeta: v1.ObjectMeta{ | ||
Finalizers: []string{}, | ||
}, | ||
}, | ||
addFinalizers: []string{"d", "e"}, | ||
expectedFinalizerPatch: &FinalizerPatch{ | ||
NewFinalizers: []string{"d", "e"}, | ||
}, | ||
}, | ||
{ | ||
name: "removes finalizers keeping the original list in order", | ||
oldObject: &v1alpha1.ServiceResolver{ | ||
ObjectMeta: v1.ObjectMeta{ | ||
Finalizers: []string{ | ||
"a", | ||
"b", | ||
"c", | ||
"d", | ||
}, | ||
}, | ||
}, | ||
removeFinalizers: []string{"b"}, | ||
expectedFinalizerPatch: &FinalizerPatch{ | ||
NewFinalizers: []string{"a", "c", "d"}, | ||
}, | ||
}, | ||
{ | ||
name: "removes all finalizers if specified", | ||
oldObject: &v1alpha1.ServiceResolver{ | ||
ObjectMeta: v1.ObjectMeta{ | ||
Finalizers: []string{ | ||
"a", | ||
"b", | ||
}, | ||
}, | ||
}, | ||
removeFinalizers: []string{"a", "b"}, | ||
expectedFinalizerPatch: &FinalizerPatch{ | ||
NewFinalizers: []string{}, | ||
}, | ||
}, | ||
} | ||
for _, c := range cases { | ||
t.Run(c.name, func(t *testing.T) { | ||
f := FinalizerPatcher{} | ||
var patch *FinalizerPatch | ||
|
||
if len(c.addFinalizers) > 0 { | ||
patch = f.AddFinalizersPatch(c.oldObject, c.addFinalizers...) | ||
} else if len(c.removeFinalizers) > 0 { | ||
patch = f.RemoveFinalizersPatch(c.oldObject, c.removeFinalizers...) | ||
} | ||
|
||
require.Equal(t, c.expectedFinalizerPatch, patch) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
@hashi-derek does this interface look ok? I tried keeping it as AddFinalizers and RemoveFinalizers, implemented by the FinalizerPatcher in finalizer_patch.go but if I do the patching within those functions, I'd need to pass in the k8s client to to the FinalizerPatcher. Keeping Patch as a function on the controller allows the FinalizerPatcher to not need another k8s client that's already being embedded in the controller implementations.
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.
Yeah, this is fine. It's simpler than having the client passed in on each function call.