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

✨ Add client.StrategicMergeFrom #1406

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
80 changes: 79 additions & 1 deletion pkg/client/client_test.go
Expand Up @@ -3240,7 +3240,7 @@ var _ = Describe("DelegatingClient", func() {
})

var _ = Describe("Patch", func() {
Describe("CreateMergePatch", func() {
Describe("MergeFrom", func() {
var cm *corev1.ConfigMap

BeforeEach(func() {
Expand Down Expand Up @@ -3303,6 +3303,84 @@ var _ = Describe("Patch", func() {
Expect(data).To(Equal([]byte(fmt.Sprintf(`{"metadata":{"annotations":{"%s":"%s"},"resourceVersion":"%s"}}`, annotationKey, annotationValue, cm.ResourceVersion))))
})
})

Describe("StrategicMergeFrom", func() {
var dep *appsv1.Deployment

BeforeEach(func() {
dep = &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Namespace: metav1.NamespaceDefault,
Name: "dep",
ResourceVersion: "10",
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{Containers: []corev1.Container{{
Name: "main",
Image: "foo:v1",
}, {
Name: "sidecar",
Image: "bar:v1",
}}},
},
},
}
})

It("creates a strategic merge patch with the modifications applied during the mutation", func() {
By("creating a strategic merge patch")
patch := client.StrategicMergeFrom(dep.DeepCopy())

By("returning a patch with type StrategicMergePatchType")
Expect(patch.Type()).To(Equal(types.StrategicMergePatchType))

By("updating the main container's image")
for i, c := range dep.Spec.Template.Spec.Containers {
if c.Name == "main" {
c.Image = "foo:v2"
}
dep.Spec.Template.Spec.Containers[i] = c
}

By("computing the patch data")
data, err := patch.Data(dep)

By("returning no error")
Expect(err).NotTo(HaveOccurred())

By("returning a patch with data only containing the image change")
Expect(data).To(Equal([]byte(`{"spec":{"template":{"spec":{"$setElementOrder/containers":[{"name":"main"},` +
`{"name":"sidecar"}],"containers":[{"image":"foo:v2","name":"main"}]}}}}`)))
})

It("creates a strategic merge patch with the modifications applied during the mutation, using optimistic locking", func() {
By("creating a strategic merge patch")
patch := client.StrategicMergeFrom(dep.DeepCopy(), client.MergeFromWithOptimisticLock{})

By("returning a patch with type StrategicMergePatchType")
Expect(patch.Type()).To(Equal(types.StrategicMergePatchType))

By("updating the main container's image")
for i, c := range dep.Spec.Template.Spec.Containers {
if c.Name == "main" {
c.Image = "foo:v2"
}
dep.Spec.Template.Spec.Containers[i] = c
}

By("computing the patch data")
data, err := patch.Data(dep)

By("returning no error")
Expect(err).NotTo(HaveOccurred())

By("returning a patch with data containing the image change and the resourceVersion change")
Expect(data).To(Equal([]byte(fmt.Sprintf(`{"metadata":{"resourceVersion":"%s"},`+
`"spec":{"template":{"spec":{"$setElementOrder/containers":[{"name":"main"},{"name":"sidecar"}],"containers":[{"image":"foo:v2","name":"main"}]}}}}`,
dep.ResourceVersion))))
})
})
})

var _ = Describe("IgnoreNotFound", func() {
Expand Down
47 changes: 41 additions & 6 deletions pkg/client/patch.go
Expand Up @@ -23,6 +23,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/json"
"k8s.io/apimachinery/pkg/util/strategicpatch"
)

var (
Expand Down Expand Up @@ -85,13 +86,15 @@ type MergeFromOptions struct {
}

type mergeFromPatch struct {
from Object
opts MergeFromOptions
patchType types.PatchType
createPatch func(originalJSON, modifiedJSON []byte, dataStruct interface{}) ([]byte, error)
from Object
opts MergeFromOptions
}

// Type implements patch.
func (s *mergeFromPatch) Type() types.PatchType {
return types.MergePatchType
return s.patchType
}

// Data implements Patch.
Expand Down Expand Up @@ -122,26 +125,58 @@ func (s *mergeFromPatch) Data(obj Object) ([]byte, error) {
return nil, err
}

data, err := jsonpatch.CreateMergePatch(originalJSON, modifiedJSON)
data, err := s.createPatch(originalJSON, modifiedJSON, obj)
if err != nil {
return nil, err
}

return data, nil
}

func createMergePatch(originalJSON, modifiedJSON []byte, _ interface{}) ([]byte, error) {
return jsonpatch.CreateMergePatch(originalJSON, modifiedJSON)
}

func createStrategicMergePatch(originalJSON, modifiedJSON []byte, dataStruct interface{}) ([]byte, error) {
return strategicpatch.CreateTwoWayMergePatch(originalJSON, modifiedJSON, dataStruct)
}

// MergeFrom creates a Patch that patches using the merge-patch strategy with the given object as base.
// The difference between MergeFrom and StrategicMergeFrom lays in the handling of modified list fields.
// When using MergeFrom, existing lists will be completely replaced by new lists.
// When using StrategicMergeFrom, the list field's `patchStrategy` is respected if specified in the API type,
// e.g. the existing list is not replaced completely but rather merged with the new one using the list's `patchMergeKey`.
// See https://kubernetes.io/docs/tasks/manage-kubernetes-objects/update-api-object-kubectl-patch/ for more details on
// the difference between merge-patch and strategic-merge-patch.
func MergeFrom(obj Object) Patch {
return &mergeFromPatch{from: obj}
return &mergeFromPatch{patchType: types.MergePatchType, createPatch: createMergePatch, from: obj}
}

// MergeFromWithOptions creates a Patch that patches using the merge-patch strategy with the given object as base.
// See MergeFrom for more details.
func MergeFromWithOptions(obj Object, opts ...MergeFromOption) Patch {
options := &MergeFromOptions{}
for _, opt := range opts {
opt.ApplyToMergeFrom(options)
}
return &mergeFromPatch{from: obj, opts: *options}
return &mergeFromPatch{patchType: types.MergePatchType, createPatch: createMergePatch, from: obj, opts: *options}
}

// StrategicMergeFrom creates a Patch that patches using the strategic-merge-patch strategy with the given object as base.
// The difference between MergeFrom and StrategicMergeFrom lays in the handling of modified list fields.
// When using MergeFrom, existing lists will be completely replaced by new lists.
// When using StrategicMergeFrom, the list field's `patchStrategy` is respected if specified in the API type,
// e.g. the existing list is not replaced completely but rather merged with the new one using the list's `patchMergeKey`.
// See https://kubernetes.io/docs/tasks/manage-kubernetes-objects/update-api-object-kubectl-patch/ for more details on
// the difference between merge-patch and strategic-merge-patch.
// Please note, that CRDs don't support strategic-merge-patch, see
// https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/#advanced-features-and-flexibility
func StrategicMergeFrom(obj Object, opts ...MergeFromOption) Patch {
options := &MergeFromOptions{}
for _, opt := range opts {
opt.ApplyToMergeFrom(options)
}
return &mergeFromPatch{patchType: types.StrategicMergePatchType, createPatch: createStrategicMergePatch, from: obj, opts: *options}
}

// mergePatch uses a raw merge strategy to patch the object.
Expand Down