Skip to content

Commit a6138bb

Browse files
committed
Add server-side apply patch support
This adds a patch type for server-side apply (Kubernetes 1.14+), which allows you to write code like ```go client.Patch(ctx, &corev1.Pod{/* fields I care about */}, client.Apply, client.IgnoreConflicts()) ``` and have it just work.
1 parent 1478c39 commit a6138bb

File tree

7 files changed

+81
-27
lines changed

7 files changed

+81
-27
lines changed

pkg/client/client_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ var _ = Describe("Client", func() {
276276
Expect(cl).NotTo(BeNil())
277277

278278
By("creating the object (with DryRun)")
279-
err = cl.Create(context.TODO(), dep, client.CreateDryRunAll())
279+
err = cl.Create(context.TODO(), dep, client.CreateDryRunAll)
280280
Expect(err).NotTo(HaveOccurred())
281281

282282
actual, err := clientset.AppsV1().Deployments(ns).Get(dep.Name, metav1.GetOptions{})
@@ -415,7 +415,7 @@ var _ = Describe("Client", func() {
415415
})
416416

417417
By("creating the object")
418-
err = cl.Create(context.TODO(), u, client.CreateDryRunAll())
418+
err = cl.Create(context.TODO(), u, client.CreateDryRunAll)
419419
Expect(err).NotTo(HaveOccurred())
420420

421421
actual, err := clientset.AppsV1().Deployments(ns).Get(dep.Name, metav1.GetOptions{})
@@ -1074,7 +1074,7 @@ var _ = Describe("Client", func() {
10741074
Expect(err).NotTo(HaveOccurred())
10751075

10761076
By("patching the Deployment with dry-run")
1077-
err = cl.Patch(context.TODO(), dep, client.ConstantPatch(types.MergePatchType, mergePatch), client.UpdatePatchWith(client.UpdateDryRunAll()))
1077+
err = cl.Patch(context.TODO(), dep, client.ConstantPatch(types.MergePatchType, mergePatch), client.PatchDryRunAll)
10781078
Expect(err).NotTo(HaveOccurred())
10791079

10801080
By("validating patched Deployment doesn't have the new annotation")
@@ -1183,7 +1183,7 @@ var _ = Describe("Client", func() {
11831183
Kind: "Deployment",
11841184
Version: "v1",
11851185
})
1186-
err = cl.Patch(context.TODO(), u, client.ConstantPatch(types.MergePatchType, mergePatch), client.UpdatePatchWith(client.UpdateDryRunAll()))
1186+
err = cl.Patch(context.TODO(), u, client.ConstantPatch(types.MergePatchType, mergePatch), client.PatchDryRunAll)
11871187
Expect(err).NotTo(HaveOccurred())
11881188

11891189
By("validating patched Deployment does not have the new annotation")
@@ -2000,7 +2000,7 @@ var _ = Describe("Client", func() {
20002000
Describe("CreateOptions", func() {
20012001
It("should allow setting DryRun to 'all'", func() {
20022002
co := &client.CreateOptions{}
2003-
client.CreateDryRunAll()(co)
2003+
client.CreateDryRunAll(co)
20042004
all := []string{metav1.DryRunAll}
20052005
Expect(co.AsCreateOptions().DryRun).To(Equal(all))
20062006
})
@@ -2141,7 +2141,7 @@ var _ = Describe("Client", func() {
21412141
Describe("UpdateOptions", func() {
21422142
It("should allow setting DryRun to 'all'", func() {
21432143
uo := &client.UpdateOptions{}
2144-
client.UpdateDryRunAll()(uo)
2144+
client.UpdateDryRunAll(uo)
21452145
all := []string{metav1.DryRunAll}
21462146
Expect(uo.AsUpdateOptions().DryRun).To(Equal(all))
21472147
})

pkg/client/doc.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ limitations under the License.
3636
// For instance, to use a label selector on list, you can call
3737
// err := someReader.List(context.Background(), &podList, client.MatchingLabels(someLabelMap))
3838
//
39+
// Options that take no arguments don't need to be called as a function:
40+
// err := someWriter.Update(context.Background(), &pod, client.UpdateDryRunAll)
41+
//
3942
// Indexing
4043
//
4144
// Indexes may be added to caches using a FieldIndexer. This allows you to easily

pkg/client/interfaces.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ type Patch interface {
4141
// Type is the PatchType of the patch.
4242
Type() types.PatchType
4343
// Data is the raw data representing the patch.
44+
// It receives the object passed to client.Patch, in order to
45+
// allow constructing diffs, etc.
4446
Data(obj runtime.Object) ([]byte, error)
4547
}
4648

pkg/client/options.go

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"k8s.io/apimachinery/pkg/fields"
2222
"k8s.io/apimachinery/pkg/labels"
2323
)
24+
2425
// CreateOptions contains options for create requests. It's generally a subset
2526
// of metav1.CreateOptions.
2627
type CreateOptions struct {
@@ -66,10 +67,8 @@ type CreateOptionFunc func(*CreateOptions)
6667

6768
// CreateDryRunAll is a functional option that sets the DryRun
6869
// field of a CreateOptions struct to metav1.DryRunAll.
69-
func CreateDryRunAll() CreateOptionFunc {
70-
return func(opts *CreateOptions) {
71-
opts.DryRun = []string{metav1.DryRunAll}
72-
}
70+
var CreateDryRunAll CreateOptionFunc = func(opts *CreateOptions) {
71+
opts.DryRun = []string{metav1.DryRunAll}
7372
}
7473

7574
// DeleteOptions contains options for delete requests. It's generally a subset
@@ -338,15 +337,26 @@ type UpdateOptionFunc func(*UpdateOptions)
338337

339338
// UpdateDryRunAll is a functional option that sets the DryRun
340339
// field of a UpdateOptions struct to metav1.DryRunAll.
341-
func UpdateDryRunAll() UpdateOptionFunc {
342-
return func(opts *UpdateOptions) {
343-
opts.DryRun = []string{metav1.DryRunAll}
344-
}
340+
var UpdateDryRunAll UpdateOptionFunc = func(opts *UpdateOptions) {
341+
opts.DryRun = []string{metav1.DryRunAll}
345342
}
346343

347344
// PatchOptions contains options for patch requests.
348345
type PatchOptions struct {
349-
UpdateOptions
346+
// When present, indicates that modifications should not be
347+
// persisted. An invalid or unrecognized dryRun directive will
348+
// result in an error response and no further processing of the
349+
// request. Valid values are:
350+
// - All: all dry run stages will be processed
351+
DryRun []string
352+
353+
// Force ignores conflicts during server-side apply,
354+
// re-marking ownership of any fields that aren't already owned.
355+
// It's probably what controllers want to do.
356+
Force *bool
357+
358+
// Raw represets raw patch options, passed directly to the server.
359+
Raw *metav1.PatchOptions
350360
}
351361

352362
// ApplyOptions executes the given PatchOptionFuncs, mutating these PatchOptions.
@@ -358,19 +368,35 @@ func (o *PatchOptions) ApplyOptions(optFuncs []PatchOptionFunc) *PatchOptions {
358368
return o
359369
}
360370

371+
func (o *PatchOptions) AsPatchOptions() *metav1.PatchOptions {
372+
if o == nil {
373+
return &metav1.PatchOptions{}
374+
}
375+
if o.Raw == nil {
376+
o.Raw = &metav1.PatchOptions{}
377+
}
378+
379+
o.Raw.DryRun = o.DryRun
380+
o.Raw.Force = o.Force
381+
return o.Raw
382+
}
383+
361384
// PatchOptionFunc is a function that mutates a PatchOptions struct. It implements
362385
// the functional options pattern. See
363386
// https://github.com/tmrts/go-patterns/blob/master/idiom/functional-options.md.
364387
type PatchOptionFunc func(*PatchOptions)
365388

366-
// Sadly, we need a separate function to "adapt" PatchOptions to the constituent
367-
// update options, since there's no way to write a function that works for both.
389+
// ForceOwnership sets the Force option, indicating that
390+
// in case of conflicts with server-side apply, the client should
391+
// acquire ownership of the conflicting field. Most controllers
392+
// should use this.
393+
var ForceOwnership PatchOptionFunc = func(opts *PatchOptions) {
394+
definitelyTrue := true
395+
opts.Force = &definitelyTrue
396+
}
368397

369-
// UpdatePatchWith adapts the given UpdateOptionFuncs to be a PatchOptionFunc.
370-
func UpdatePatchWith(optFuncs ...UpdateOptionFunc) PatchOptionFunc {
371-
return func(opts *PatchOptions) {
372-
for _, optFunc := range optFuncs {
373-
optFunc(&opts.UpdateOptions)
374-
}
375-
}
398+
// PatchDryRunAll is a functional option that sets the DryRun
399+
// field of a CreateOptions struct to metav1.DryRunAll.
400+
var PatchDryRunAll PatchOptionFunc = func(opts *PatchOptions) {
401+
opts.DryRun = []string{metav1.DryRunAll}
376402
}

pkg/client/patch.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ import (
2323
"k8s.io/apimachinery/pkg/util/strategicpatch"
2424
)
2525

26+
var (
27+
// Apply uses server-side apply to patch the given object.
28+
Apply = applyPatch{}
29+
)
30+
2631
type patch struct {
2732
patchType types.PatchType
2833
data []byte
@@ -47,7 +52,7 @@ type mergeFromPatch struct {
4752
from runtime.Object
4853
}
4954

50-
// Type implements patch.
55+
// Type implements Patch.
5156
func (s *mergeFromPatch) Type() types.PatchType {
5257
return types.StrategicMergePatchType
5358
}
@@ -72,3 +77,21 @@ func (s *mergeFromPatch) Data(obj runtime.Object) ([]byte, error) {
7277
func MergeFrom(obj runtime.Object) Patch {
7378
return &mergeFromPatch{obj}
7479
}
80+
81+
// applyPatch uses server-side apply to patch the object.
82+
type applyPatch struct{}
83+
84+
// Type implements Patch.
85+
func (p applyPatch) Type() types.PatchType {
86+
// TODO(directxman12): when we update to 1.14, just use types.ApplyPatch
87+
return "application/apply-patch+yaml"
88+
}
89+
90+
// Data implements Patch.
91+
func (p applyPatch) Data(obj runtime.Object) ([]byte, error) {
92+
// NB(directxman12): we might techically want to be using an actual encoder
93+
// here (in case some more performant encoder is introduced) but this is
94+
// correct and sufficient for our uses (it's what the JSON serializer in
95+
// client-go does, more-or-less).
96+
return json.Marshal(obj)
97+
}

pkg/client/typed_client.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ func (c *typedClient) Patch(ctx context.Context, obj runtime.Object, patch Patch
103103
NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()).
104104
Resource(o.resource()).
105105
Name(o.GetName()).
106-
VersionedParams(patchOpts.ApplyOptions(opts).AsUpdateOptions(), c.paramCodec).
106+
VersionedParams(patchOpts.ApplyOptions(opts).AsPatchOptions(), c.paramCodec).
107107
Body(data).
108108
Context(ctx).
109109
Do().

pkg/client/unstructured_client.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ func (uc *unstructuredClient) Patch(_ context.Context, obj runtime.Object, patch
108108
}
109109

110110
patchOpts := &PatchOptions{}
111-
i, err := r.Patch(u.GetName(), patch.Type(), data, *patchOpts.ApplyOptions(opts).AsUpdateOptions())
111+
i, err := r.Patch(u.GetName(), patch.Type(), data, *patchOpts.ApplyOptions(opts).AsPatchOptions())
112112
if err != nil {
113113
return err
114114
}

0 commit comments

Comments
 (0)