Skip to content

Commit

Permalink
Implement Patch methods
Browse files Browse the repository at this point in the history
create Patch, PatchOptions and PatchOptionFunc
add patch method to
* Client
* unstructuredClient
* typedClient
implement utility to create merge patches
add tests for both clients and for the utility
  • Loading branch information
adracus committed Mar 15, 2019
1 parent 276610b commit 0e0c86c
Show file tree
Hide file tree
Showing 11 changed files with 415 additions and 0 deletions.
1 change: 1 addition & 0 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions pkg/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,15 @@ func (c *client) Delete(ctx context.Context, obj runtime.Object, opts ...DeleteO
return c.typedClient.Delete(ctx, obj, opts...)
}

// Patch implements client.Client
func (c *client) Patch(ctx context.Context, obj runtime.Object, patch Patch, opts ...PatchOptionFunc) error {
_, ok := obj.(*unstructured.Unstructured)
if ok {
return c.unstructuredClient.Patch(ctx, obj, patch, opts...)
}
return c.typedClient.Patch(ctx, obj, patch, opts...)
}

// Get implements client.Client
func (c *client) Get(ctx context.Context, key ObjectKey, obj runtime.Object) error {
_, ok := obj.(*unstructured.Unstructured)
Expand Down
181 changes: 181 additions & 0 deletions pkg/client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@ package client_test

import (
"context"
"encoding/json"
"fmt"
"sync/atomic"

"k8s.io/apimachinery/pkg/types"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
appsv1 "k8s.io/api/apps/v1"
Expand Down Expand Up @@ -62,6 +65,7 @@ var _ = Describe("Client", func() {
var count uint64 = 0
var replicaCount int32 = 2
var ns = "default"
var mergePatch []byte

BeforeEach(func(done Done) {
atomic.AddUint64(&count, 1)
Expand All @@ -88,6 +92,15 @@ var _ = Describe("Client", func() {
Spec: corev1.NodeSpec{},
}
scheme = kscheme.Scheme
var err error
mergePatch, err = json.Marshal(map[string]interface{}{
"metadata": map[string]interface{}{
"annotations": map[string]interface{}{
"foo": "bar",
},
},
})
Expect(err).NotTo(HaveOccurred())

close(done)
}, serverSideTimeoutSeconds)
Expand Down Expand Up @@ -964,6 +977,174 @@ var _ = Describe("Client", func() {
})
})

Describe("Patch", func() {
Context("with structured objects", func() {
It("should patch an existing object from a go struct", func(done Done) {
cl, err := client.New(cfg, client.Options{})
Expect(err).NotTo(HaveOccurred())
Expect(cl).NotTo(BeNil())

By("initially creating a Deployment")
dep, err := clientset.AppsV1().Deployments(ns).Create(dep)
Expect(err).NotTo(HaveOccurred())

By("patching the Deployment")
err = cl.Patch(context.TODO(), dep, client.NewPatch(types.MergePatchType, mergePatch))
Expect(err).NotTo(HaveOccurred())

By("validating patched Deployment has new annotation")
actual, err := clientset.AppsV1().Deployments(ns).Get(dep.Name, metav1.GetOptions{})
Expect(err).NotTo(HaveOccurred())
Expect(actual).NotTo(BeNil())
Expect(actual.Annotations["foo"]).To(Equal("bar"))

close(done)
})

It("should patch an existing object non-namespace object from a go struct", func(done Done) {
cl, err := client.New(cfg, client.Options{})
Expect(err).NotTo(HaveOccurred())
Expect(cl).NotTo(BeNil())

By("initially creating a Node")
node, err := clientset.CoreV1().Nodes().Create(node)
Expect(err).NotTo(HaveOccurred())

By("patching the Node")
nodeName := node.Name
err = cl.Patch(context.TODO(), node, client.NewPatch(types.MergePatchType, mergePatch))
Expect(err).NotTo(HaveOccurred())

By("validating the Node no longer exists")
actual, err := clientset.CoreV1().Nodes().Get(nodeName, metav1.GetOptions{})
Expect(err).NotTo(HaveOccurred())
Expect(actual).NotTo(BeNil())
Expect(actual.Annotations["foo"]).To(Equal("bar"))

close(done)
})

It("should fail if the object does not exists", func(done Done) {
cl, err := client.New(cfg, client.Options{})
Expect(err).NotTo(HaveOccurred())
Expect(cl).NotTo(BeNil())

By("Patching node before it is ever created")
err = cl.Patch(context.TODO(), node, client.NewPatch(types.MergePatchType, mergePatch))
Expect(err).To(HaveOccurred())

close(done)
})

PIt("should fail if the object doesn't have meta", func() {

})

It("should fail if the object cannot be mapped to a GVK", func(done Done) {
By("creating client with empty Scheme")
emptyScheme := runtime.NewScheme()
cl, err := client.New(cfg, client.Options{Scheme: emptyScheme})
Expect(err).NotTo(HaveOccurred())
Expect(cl).NotTo(BeNil())

By("initially creating a Deployment")
dep, err := clientset.AppsV1().Deployments(ns).Create(dep)
Expect(err).NotTo(HaveOccurred())

By("patching the Deployment fails")
err = cl.Patch(context.TODO(), dep, client.NewPatch(types.MergePatchType, mergePatch))
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("no kind is registered for the type"))

close(done)
})

PIt("should fail if the GVK cannot be mapped to a Resource", func() {

})
})
Context("with unstructured objects", func() {
It("should patch an existing object from a go struct", func(done Done) {
cl, err := client.New(cfg, client.Options{})
Expect(err).NotTo(HaveOccurred())
Expect(cl).NotTo(BeNil())

By("initially creating a Deployment")
dep, err := clientset.AppsV1().Deployments(ns).Create(dep)
Expect(err).NotTo(HaveOccurred())

By("patching the Deployment")
depName := dep.Name
u := &unstructured.Unstructured{}
scheme.Convert(dep, u, nil)
u.SetGroupVersionKind(schema.GroupVersionKind{
Group: "apps",
Kind: "Deployment",
Version: "v1",
})
err = cl.Patch(context.TODO(), u, client.NewPatch(types.MergePatchType, mergePatch))
Expect(err).NotTo(HaveOccurred())

By("validating patched Deployment has new annotation")
actual, err := clientset.AppsV1().Deployments(ns).Get(depName, metav1.GetOptions{})
Expect(err).NotTo(HaveOccurred())
Expect(actual).NotTo(BeNil())
Expect(actual.Annotations["foo"]).To(Equal("bar"))

close(done)
})

It("should patch an existing object non-namespace object from a go struct", func(done Done) {
cl, err := client.New(cfg, client.Options{})
Expect(err).NotTo(HaveOccurred())
Expect(cl).NotTo(BeNil())

By("initially creating a Node")
node, err := clientset.CoreV1().Nodes().Create(node)
Expect(err).NotTo(HaveOccurred())

By("patching the Node")
nodeName := node.Name
u := &unstructured.Unstructured{}
scheme.Convert(node, u, nil)
u.SetGroupVersionKind(schema.GroupVersionKind{
Group: "",
Kind: "Node",
Version: "v1",
})
err = cl.Patch(context.TODO(), u, client.NewPatch(types.MergePatchType, mergePatch))
Expect(err).NotTo(HaveOccurred())

By("validating pathed Node has new annotation")
actual, err := clientset.CoreV1().Nodes().Get(nodeName, metav1.GetOptions{})
Expect(err).NotTo(HaveOccurred())
Expect(actual).NotTo(BeNil())
Expect(actual.Annotations["foo"]).To(Equal("bar"))

close(done)
})

It("should fail if the object does not exist", func(done Done) {
cl, err := client.New(cfg, client.Options{})
Expect(err).NotTo(HaveOccurred())
Expect(cl).NotTo(BeNil())

By("Patching node before it is ever created")
u := &unstructured.Unstructured{}
scheme.Convert(node, u, nil)
u.SetGroupVersionKind(schema.GroupVersionKind{
Group: "",
Kind: "Node",
Version: "v1",
})
err = cl.Patch(context.TODO(), node, client.NewPatch(types.MergePatchType, mergePatch))
Expect(err).To(HaveOccurred())

close(done)
})
})
})

Describe("Get", func() {
Context("with structured objects", func() {
It("should fetch an existing object for a go struct", func(done Done) {
Expand Down
28 changes: 28 additions & 0 deletions pkg/client/fake/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,34 @@ func (c *fakeClient) Update(ctx context.Context, obj runtime.Object, opts ...cli
return c.tracker.Update(gvr, obj, accessor.GetNamespace())
}

func (c *fakeClient) Patch(ctx context.Context, obj runtime.Object, patch client.Patch, opts ...client.PatchOptionFunc) error {
gvr, err := getGVRFromObject(obj, c.scheme)
if err != nil {
return err
}
accessor, err := meta.Accessor(obj)
if err != nil {
return err
}

reaction := testing.ObjectReaction(c.tracker)
handled, o, err := reaction(testing.NewPatchAction(gvr, accessor.GetNamespace(), accessor.GetName(), patch.Type(), patch.Data()))
if err != nil {
return err
}
if !handled {
panic("tracker could not handle patch method")
}

j, err := json.Marshal(o)
if err != nil {
return err
}
decoder := scheme.Codecs.UniversalDecoder()
_, _, err = decoder.Decode(j, nil, obj)
return err
}

func (c *fakeClient) Status() client.StatusWriter {
return &fakeStatusWriter{client: c}
}
Expand Down
26 changes: 26 additions & 0 deletions pkg/client/fake/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ limitations under the License.
package fake

import (
"encoding/json"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"

Expand Down Expand Up @@ -205,6 +207,30 @@ var _ = Describe("Fake client", func() {
Expect(obj).To(Equal(cm))
})
})

It("should be able to Patch", func() {
By("Patching a deployment")
mergePatch, err := json.Marshal(map[string]interface{}{
"metadata": map[string]interface{}{
"annotations": map[string]interface{}{
"foo": "bar",
},
},
})
Expect(err).NotTo(HaveOccurred())
err = cl.Patch(nil, dep, client.NewPatch(types.JSONPatchType, mergePatch))
Expect(err).NotTo(HaveOccurred())

By("Getting the patched deployment")
namespacedName := types.NamespacedName{
Name: "test-deployment",
Namespace: "ns1",
}
obj := &appsv1.Deployment{}
err = cl.Get(nil, namespacedName, obj)
Expect(err).NotTo(HaveOccurred())
Expect(obj.Annotations["foo"]).To(Equal("bar"))
})
}

Context("with default scheme.Scheme", func() {
Expand Down
21 changes: 21 additions & 0 deletions pkg/client/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ func ObjectKeyFromObject(obj runtime.Object) (ObjectKey, error) {
return ObjectKey{Namespace: accessor.GetNamespace(), Name: accessor.GetName()}, nil
}

// Patch is a patch that can be applied to a Kubernetes object.
type Patch interface {
// Type is the PatchType of the patch.
Type() types.PatchType
// Data is the raw data representing the patch.
Data() []byte
}

// TODO(directxman12): is there a sane way to deal with get/delete options?

// Reader knows how to read and list Kubernetes objects.
Expand All @@ -65,6 +73,10 @@ type Writer interface {
// Update updates the given obj in the Kubernetes cluster. obj must be a
// struct pointer so that obj can be updated with the content returned by the Server.
Update(ctx context.Context, obj runtime.Object, opts ...UpdateOptionFunc) error

// Patch patches the given obj in the Kubernetes cluster. obj must be a
// struct pointer so that obj can be updated with the content returned by the Server.
Patch(ctx context.Context, obj runtime.Object, patch Patch, opts ...PatchOptionFunc) error
}

// StatusClient knows how to create a client which can update status subresource
Expand Down Expand Up @@ -428,3 +440,12 @@ func UpdateDryRunAll() UpdateOptionFunc {
opts.DryRun = []string{metav1.DryRunAll}
}
}

// PatchOptions contains options for patch requests.
type PatchOptions struct {
}

// PatchOptionFunc is a function that mutates a PatchOptions struct. It implements
// the functional options pattern. See
// https://github.com/tmrts/go-patterns/blob/master/idiom/functional-options.md.
type PatchOptionFunc func(*PatchOptions)
41 changes: 41 additions & 0 deletions pkg/client/patch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package client

import (
"k8s.io/apimachinery/pkg/types"
)

type patch struct {
patchType types.PatchType
data []byte
}

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

// Data implements Patch.
func (s *patch) Data() []byte {
return s.data
}

// NewPatch constructs a new Patch with the given PatchType and data.
func NewPatch(patchType types.PatchType, data []byte) Patch {
return &patch{patchType, data}
}
Loading

0 comments on commit 0e0c86c

Please sign in to comment.