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 Server-Side DryRun support to Client #338

Merged
merged 6 commits into from
Mar 14, 2019
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -1722,6 +1768,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 @@ -1846,6 +1908,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