Skip to content

Commit

Permalink
Merge pull request #338 from pusher/options
Browse files Browse the repository at this point in the history
⚠️ Add Server-Side DryRun support to Client
  • Loading branch information
k8s-ci-robot authored Mar 14, 2019
2 parents ff6ae79 + f7bb591 commit 276610b
Show file tree
Hide file tree
Showing 8 changed files with 279 additions and 17 deletions.
2 changes: 1 addition & 1 deletion hack/check-everything.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ set -e
hack_dir=$(dirname ${BASH_SOURCE})
source ${hack_dir}/common.sh

k8s_version=1.10.1
k8s_version=1.13.1
goarch=amd64
goos="unknown"

Expand Down
12 changes: 6 additions & 6 deletions pkg/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,21 +104,21 @@ type client struct {
}

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

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

// Delete implements client.Client
Expand Down
78 changes: 78 additions & 0 deletions pkg/client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,25 @@ var _ = Describe("Client", func() {
// TODO(seans3): implement these
// Example: ListOptions
})

Context("with the DryRun option", func() {
It("should not create a new object", func(done Done) {
cl, err := client.New(cfg, client.Options{})
Expect(err).NotTo(HaveOccurred())
Expect(cl).NotTo(BeNil())

By("creating the object (with DryRun)")
err = cl.Create(context.TODO(), dep, client.CreateDryRunAll())
Expect(err).NotTo(HaveOccurred())

actual, err := clientset.AppsV1().Deployments(ns).Get(dep.Name, metav1.GetOptions{})
Expect(err).To(HaveOccurred())
Expect(errors.IsNotFound(err)).To(BeTrue())
Expect(actual).To(Equal(&appsv1.Deployment{}))

close(done)
})
})
})

Context("with unstructured objects", func() {
Expand Down Expand Up @@ -367,6 +386,33 @@ var _ = Describe("Client", func() {

})

Context("with the DryRun option", func() {
It("should not create a new object from a go struct", func(done Done) {
cl, err := client.New(cfg, client.Options{})
Expect(err).NotTo(HaveOccurred())
Expect(cl).NotTo(BeNil())

By("encoding the deployment as unstructured")
u := &unstructured.Unstructured{}
scheme.Convert(dep, u, nil)
u.SetGroupVersionKind(schema.GroupVersionKind{
Group: "apps",
Kind: "Deployment",
Version: "v1",
})

By("creating the object")
err = cl.Create(context.TODO(), u, client.CreateDryRunAll())
Expect(err).NotTo(HaveOccurred())

actual, err := clientset.AppsV1().Deployments(ns).Get(dep.Name, metav1.GetOptions{})
Expect(err).To(HaveOccurred())
Expect(errors.IsNotFound(err)).To(BeTrue())
Expect(actual).To(Equal(&appsv1.Deployment{}))

close(done)
})
})
})

Describe("Update", func() {
Expand Down Expand Up @@ -1720,6 +1766,22 @@ var _ = Describe("Client", func() {
})
})

Describe("CreateOptions", func() {
It("should allow setting DryRun to 'all'", func() {
co := &client.CreateOptions{}
client.CreateDryRunAll()(co)
all := []string{metav1.DryRunAll}
Expect(co.AsCreateOptions().DryRun).To(Equal(all))
})

It("should produce empty metav1.CreateOptions if nil", func() {
var co *client.CreateOptions
Expect(co.AsCreateOptions()).To(Equal(&metav1.CreateOptions{}))
co = &client.CreateOptions{}
Expect(co.AsCreateOptions()).To(Equal(&metav1.CreateOptions{}))
})
})

Describe("DeleteOptions", func() {
It("should allow setting GracePeriodSeconds", func() {
do := &client.DeleteOptions{}
Expand Down Expand Up @@ -1844,6 +1906,22 @@ var _ = Describe("Client", func() {
Expect(lo.Namespace).To(Equal("test"))
})
})

Describe("UpdateOptions", func() {
It("should allow setting DryRun to 'all'", func() {
uo := &client.UpdateOptions{}
client.UpdateDryRunAll()(uo)
all := []string{metav1.DryRunAll}
Expect(uo.AsUpdateOptions().DryRun).To(Equal(all))
})

It("should produce empty metav1.UpdateOptions if nil", func() {
var co *client.UpdateOptions
Expect(co.AsUpdateOptions()).To(Equal(&metav1.UpdateOptions{}))
co = &client.UpdateOptions{}
Expect(co.AsUpdateOptions()).To(Equal(&metav1.UpdateOptions{}))
})
})
})

var _ = Describe("DelegatingReader", func() {
Expand Down
23 changes: 21 additions & 2 deletions pkg/client/fake/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"strings"

"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/kubernetes/scheme"
Expand Down Expand Up @@ -138,7 +139,16 @@ func (c *fakeClient) List(ctx context.Context, obj runtime.Object, opts ...clien
return nil
}

func (c *fakeClient) Create(ctx context.Context, obj runtime.Object) error {
func (c *fakeClient) Create(ctx context.Context, obj runtime.Object, opts ...client.CreateOptionFunc) error {
createOptions := &client.CreateOptions{}
createOptions.ApplyOptions(opts)

for _, dryRunOpt := range createOptions.DryRun {
if dryRunOpt == metav1.DryRunAll {
return nil
}
}

gvr, err := getGVRFromObject(obj, c.scheme)
if err != nil {
return err
Expand All @@ -163,7 +173,16 @@ func (c *fakeClient) Delete(ctx context.Context, obj runtime.Object, opts ...cli
return c.tracker.Delete(gvr, accessor.GetNamespace(), accessor.GetName())
}

func (c *fakeClient) Update(ctx context.Context, obj runtime.Object) error {
func (c *fakeClient) Update(ctx context.Context, obj runtime.Object, opts ...client.UpdateOptionFunc) error {
updateOptions := &client.UpdateOptions{}
updateOptions.ApplyOptions(opts)

for _, dryRunOpt := range updateOptions.DryRun {
if dryRunOpt == metav1.DryRunAll {
return nil
}
}

gvr, err := getGVRFromObject(obj, c.scheme)
if err != nil {
return err
Expand Down
51 changes: 51 additions & 0 deletions pkg/client/fake/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (

appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
Expand Down Expand Up @@ -154,6 +155,56 @@ var _ = Describe("Fake client", func() {
Expect(list.Items).To(HaveLen(1))
Expect(list.Items).To(ConsistOf(*dep2))
})

Context("with the DryRun option", func() {
It("should not create a new object", func() {
By("Creating a new configmap with DryRun")
newcm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "new-test-cm",
Namespace: "ns2",
},
}
err := cl.Create(nil, newcm, client.CreateDryRunAll())
Expect(err).To(BeNil())

By("Getting the new configmap")
namespacedName := types.NamespacedName{
Name: "new-test-cm",
Namespace: "ns2",
}
obj := &corev1.ConfigMap{}
err = cl.Get(nil, namespacedName, obj)
Expect(err).To(HaveOccurred())
Expect(errors.IsNotFound(err)).To(BeTrue())
Expect(obj).NotTo(Equal(newcm))
})

It("should not Update the object", func() {
By("Updating a new configmap with DryRun")
newcm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-cm",
Namespace: "ns2",
},
Data: map[string]string{
"test-key": "new-value",
},
}
err := cl.Update(nil, newcm, client.UpdateDryRunAll())
Expect(err).To(BeNil())

By("Getting the new configmap")
namespacedName := types.NamespacedName{
Name: "test-cm",
Namespace: "ns2",
}
obj := &corev1.ConfigMap{}
err = cl.Get(nil, namespacedName, obj)
Expect(err).To(BeNil())
Expect(obj).To(Equal(cm))
})
})
}

Context("with default scheme.Scheme", func() {
Expand Down
106 changes: 104 additions & 2 deletions pkg/client/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,14 @@ type Reader interface {
// Writer knows how to create, delete, and update Kubernetes objects.
type Writer interface {
// Create saves the object obj in the Kubernetes cluster.
Create(ctx context.Context, obj runtime.Object) error
Create(ctx context.Context, obj runtime.Object, opts ...CreateOptionFunc) error

// Delete deletes the given obj from Kubernetes cluster.
Delete(ctx context.Context, obj runtime.Object, opts ...DeleteOptionFunc) error

// 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) error
Update(ctx context.Context, obj runtime.Object, opts ...UpdateOptionFunc) error
}

// StatusClient knows how to create a client which can update status subresource
Expand Down Expand Up @@ -106,6 +106,57 @@ type FieldIndexer interface {
IndexField(obj runtime.Object, field string, extractValue IndexerFunc) error
}

// CreateOptions contains options for create requests. It's generally a subset
// of metav1.CreateOptions.
type CreateOptions struct {
// When present, indicates that modifications should not be
// persisted. An invalid or unrecognized dryRun directive will
// result in an error response and no further processing of the
// request. Valid values are:
// - All: all dry run stages will be processed
DryRun []string

// Raw represents raw CreateOptions, as passed to the API server.
Raw *metav1.CreateOptions
}

// AsCreateOptions returns these options as a metav1.CreateOptions.
// This may mutate the Raw field.
func (o *CreateOptions) AsCreateOptions() *metav1.CreateOptions {

if o == nil {
return &metav1.CreateOptions{}
}
if o.Raw == nil {
o.Raw = &metav1.CreateOptions{}
}

o.Raw.DryRun = o.DryRun
return o.Raw
}

// ApplyOptions executes the given CreateOptionFuncs and returns the mutated
// CreateOptions.
func (o *CreateOptions) ApplyOptions(optFuncs []CreateOptionFunc) *CreateOptions {
for _, optFunc := range optFuncs {
optFunc(o)
}
return o
}

// CreateOptionFunc is a function that mutates a CreateOptions struct. It implements
// the functional options pattern. See
// https://github.com/tmrts/go-patterns/blob/master/idiom/functional-options.md.
type CreateOptionFunc func(*CreateOptions)

// CreateDryRunAll is a functional option that sets the DryRun
// field of a CreateOptions struct to metav1.DryRunAll.
func CreateDryRunAll() CreateOptionFunc {
return func(opts *CreateOptions) {
opts.DryRun = []string{metav1.DryRunAll}
}
}

// DeleteOptions contains options for delete requests. It's generally a subset
// of metav1.DeleteOptions.
type DeleteOptions struct {
Expand Down Expand Up @@ -326,3 +377,54 @@ func UseListOptions(newOpts *ListOptions) ListOptionFunc {
*opts = *newOpts
}
}

// UpdateOptions contains options for create requests. It's generally a subset
// of metav1.UpdateOptions.
type UpdateOptions struct {
// When present, indicates that modifications should not be
// persisted. An invalid or unrecognized dryRun directive will
// result in an error response and no further processing of the
// request. Valid values are:
// - All: all dry run stages will be processed
DryRun []string

// Raw represents raw UpdateOptions, as passed to the API server.
Raw *metav1.UpdateOptions
}

// AsUpdateOptions returns these options as a metav1.UpdateOptions.
// This may mutate the Raw field.
func (o *UpdateOptions) AsUpdateOptions() *metav1.UpdateOptions {

if o == nil {
return &metav1.UpdateOptions{}
}
if o.Raw == nil {
o.Raw = &metav1.UpdateOptions{}
}

o.Raw.DryRun = o.DryRun
return o.Raw
}

// ApplyOptions executes the given UpdateOptionFuncs and returns the mutated
// UpdateOptions.
func (o *UpdateOptions) ApplyOptions(optFuncs []UpdateOptionFunc) *UpdateOptions {
for _, optFunc := range optFuncs {
optFunc(o)
}
return o
}

// UpdateOptionFunc is a function that mutates a UpdateOptions struct. It implements
// the functional options pattern. See
// https://github.com/tmrts/go-patterns/blob/master/idiom/functional-options.md.
type UpdateOptionFunc func(*UpdateOptions)

// UpdateDryRunAll is a functional option that sets the DryRun
// field of a UpdateOptions struct to metav1.DryRunAll.
func UpdateDryRunAll() UpdateOptionFunc {
return func(opts *UpdateOptions) {
opts.DryRun = []string{metav1.DryRunAll}
}
}
Loading

0 comments on commit 276610b

Please sign in to comment.