Skip to content

Commit 5118ae5

Browse files
authored
Merge pull request #1042 from kubernetes-sigs/master
Prepare for v0.6.1
2 parents 1c83ff6 + ea795f6 commit 5118ae5

File tree

28 files changed

+498
-146
lines changed

28 files changed

+498
-146
lines changed

OWNERS_ALIASES

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ aliases:
77
- directxman12
88
- droot
99
- mengqiy
10+
- pwittrock
1011

1112
# non-admin folks who have write-access and can approve any PRs in the repo
1213
controller-runtime-maintainers:
@@ -36,5 +37,4 @@ aliases:
3637

3738
# folks who may have context on ancient history,
3839
# but are no longer directly involved
39-
controller-runtime-emeritus-maintainers:
40-
- pwittrock
40+
# controller-runtime-emeritus-maintainers:

examples/builtins/controller.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package main
1818

1919
import (
2020
"context"
21+
"fmt"
2122

2223
"github.com/go-logr/logr"
2324

@@ -50,8 +51,7 @@ func (r *reconcileReplicaSet) Reconcile(request reconcile.Request) (reconcile.Re
5051
}
5152

5253
if err != nil {
53-
log.Error(err, "Could not fetch ReplicaSet")
54-
return reconcile.Result{}, err
54+
return reconcile.Result{}, fmt.Errorf("could not fetch ReplicaSet: %+v", err)
5555
}
5656

5757
// Print the ReplicaSet
@@ -69,8 +69,7 @@ func (r *reconcileReplicaSet) Reconcile(request reconcile.Request) (reconcile.Re
6969
rs.Labels["hello"] = "world"
7070
err = r.client.Update(context.TODO(), rs)
7171
if err != nil {
72-
log.Error(err, "Could not write ReplicaSet")
73-
return reconcile.Result{}, err
72+
return reconcile.Result{}, fmt.Errorf("could not write ReplicaSet: %+v", err)
7473
}
7574

7675
return reconcile.Result{}, nil

go.mod

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,34 @@ module sigs.k8s.io/controller-runtime
33
go 1.13
44

55
require (
6+
github.com/beorn7/perks v1.0.1 // indirect
67
github.com/evanphx/json-patch v4.5.0+incompatible
8+
github.com/fsnotify/fsnotify v1.4.9
79
github.com/go-logr/logr v0.1.0
810
github.com/go-logr/zapr v0.1.0
911
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef // indirect
1012
github.com/googleapis/gnostic v0.3.1 // indirect
11-
github.com/imdario/mergo v0.3.6 // indirect
12-
github.com/onsi/ginkgo v1.11.0
13-
github.com/onsi/gomega v1.8.1
13+
github.com/hashicorp/golang-lru v0.5.4 // indirect
14+
github.com/imdario/mergo v0.3.9 // indirect
15+
github.com/json-iterator/go v1.1.10 // indirect
16+
github.com/onsi/ginkgo v1.12.1
17+
github.com/onsi/gomega v1.10.1
1418
github.com/prometheus/client_golang v1.0.0
1519
github.com/prometheus/client_model v0.2.0
20+
github.com/prometheus/procfs v0.0.11 // indirect
1621
github.com/spf13/pflag v1.0.5
1722
go.uber.org/atomic v1.4.0 // indirect
1823
go.uber.org/zap v1.10.0
24+
golang.org/x/text v0.3.3 // indirect
1925
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4
2026
gomodules.xyz/jsonpatch/v2 v2.0.1
2127
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
22-
gopkg.in/fsnotify.v1 v1.4.7
23-
k8s.io/api v0.18.2
24-
k8s.io/apiextensions-apiserver v0.18.2
25-
k8s.io/apimachinery v0.18.2
26-
k8s.io/client-go v0.18.2
27-
k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89
28+
k8s.io/api v0.18.4
29+
k8s.io/apiextensions-apiserver v0.18.4
30+
k8s.io/apimachinery v0.18.4
31+
k8s.io/client-go v0.18.4
32+
k8s.io/utils v0.0.0-20200603063816-c1c6865ac451
2833
sigs.k8s.io/yaml v1.2.0
2934
)
35+
36+
replace github.com/evanphx/json-patch => github.com/evanphx/json-patch v0.0.0-20190815234213-e83c0a1c26c8

go.sum

Lines changed: 71 additions & 20 deletions
Large diffs are not rendered by default.

pkg/client/apiutil/apimachinery.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func GVKForObject(obj runtime.Object, scheme *runtime.Scheme) (schema.GroupVersi
5353
return schema.GroupVersionKind{}, err
5454
}
5555
if isUnversioned {
56-
return schema.GroupVersionKind{}, fmt.Errorf("cannot create a new informer for the unversioned type %T", obj)
56+
return schema.GroupVersionKind{}, fmt.Errorf("cannot create group-version-kind for unversioned type %T", obj)
5757
}
5858

5959
if len(gvks) < 1 {

pkg/client/client_test.go

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1210,6 +1210,42 @@ var _ = Describe("Client", func() {
12101210
close(done)
12111211
})
12121212

1213+
It("should patch an existing object from a go struct, using optimistic locking", func(done Done) {
1214+
cl, err := client.New(cfg, client.Options{})
1215+
Expect(err).NotTo(HaveOccurred())
1216+
Expect(cl).NotTo(BeNil())
1217+
1218+
By("initially creating a Deployment")
1219+
dep, err := clientset.AppsV1().Deployments(ns).Create(ctx, dep, metav1.CreateOptions{})
1220+
Expect(err).NotTo(HaveOccurred())
1221+
1222+
By("creating a patch from with optimistic lock")
1223+
patch := client.MergeFromWithOptions(dep.DeepCopy(), client.MergeFromWithOptimisticLock{})
1224+
1225+
By("adding a new annotation")
1226+
dep.Annotations = map[string]string{
1227+
"foo": "bar",
1228+
}
1229+
1230+
By("patching the Deployment")
1231+
err = cl.Patch(context.TODO(), dep, patch)
1232+
Expect(err).NotTo(HaveOccurred())
1233+
1234+
By("validating patched Deployment has new annotation")
1235+
actual, err := clientset.AppsV1().Deployments(ns).Get(ctx, dep.Name, metav1.GetOptions{})
1236+
Expect(err).NotTo(HaveOccurred())
1237+
Expect(actual).NotTo(BeNil())
1238+
Expect(actual.Annotations["foo"]).To(Equal("bar"))
1239+
1240+
By("validating that a patch should fail with conflict, when it has an outdated resource version")
1241+
dep.Annotations["should"] = "conflict"
1242+
err = cl.Patch(context.TODO(), dep, patch)
1243+
Expect(err).To(HaveOccurred())
1244+
Expect(apierrors.IsConflict(err)).To(BeTrue())
1245+
1246+
close(done)
1247+
})
1248+
12131249
It("should patch and preserve type information", func(done Done) {
12141250
cl, err := client.New(cfg, client.Options{})
12151251
Expect(err).NotTo(HaveOccurred())
@@ -2656,8 +2692,9 @@ var _ = Describe("Patch", func() {
26562692
BeforeEach(func() {
26572693
cm = &corev1.ConfigMap{
26582694
ObjectMeta: metav1.ObjectMeta{
2659-
Namespace: metav1.NamespaceDefault,
2660-
Name: "cm",
2695+
Namespace: metav1.NamespaceDefault,
2696+
Name: "cm",
2697+
ResourceVersion: "10",
26612698
},
26622699
}
26632700
})
@@ -2686,6 +2723,31 @@ var _ = Describe("Patch", func() {
26862723
By("returning a patch with data only containing the annotation change")
26872724
Expect(data).To(Equal([]byte(fmt.Sprintf(`{"metadata":{"annotations":{"%s":"%s"}}}`, annotationKey, annotationValue))))
26882725
})
2726+
2727+
It("creates a merge patch with the modifications applied during the mutation, using optimistic locking", func() {
2728+
const (
2729+
annotationKey = "test"
2730+
annotationValue = "foo"
2731+
)
2732+
2733+
By("creating a merge patch")
2734+
patch := client.MergeFromWithOptions(cm.DeepCopy(), client.MergeFromWithOptimisticLock{})
2735+
2736+
By("returning a patch with type MergePatch")
2737+
Expect(patch.Type()).To(Equal(types.MergePatchType))
2738+
2739+
By("retrieving modifying the config map")
2740+
metav1.SetMetaDataAnnotation(&cm.ObjectMeta, annotationKey, annotationValue)
2741+
2742+
By("computing the patch data")
2743+
data, err := patch.Data(cm)
2744+
2745+
By("returning no error")
2746+
Expect(err).NotTo(HaveOccurred())
2747+
2748+
By("returning a patch with data containing the annotation change and the resourceVersion change")
2749+
Expect(data).To(Equal([]byte(fmt.Sprintf(`{"metadata":{"annotations":{"%s":"%s"},"resourceVersion":"%s"}}`, annotationKey, annotationValue, cm.ResourceVersion))))
2750+
})
26892751
})
26902752
})
26912753

pkg/client/example_test.go

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,15 @@ import (
2020
"context"
2121
"fmt"
2222
"os"
23+
"time"
2324

2425
corev1 "k8s.io/api/core/v1"
2526
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2627
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2728
"k8s.io/apimachinery/pkg/runtime"
2829
"k8s.io/apimachinery/pkg/runtime/schema"
30+
"k8s.io/apimachinery/pkg/types"
31+
2932
"sigs.k8s.io/controller-runtime/pkg/client"
3033
"sigs.k8s.io/controller-runtime/pkg/client/config"
3134
)
@@ -97,8 +100,10 @@ func ExampleClient_create() {
97100
// Using a unstructured object.
98101
u := &unstructured.Unstructured{}
99102
u.Object = map[string]interface{}{
100-
"name": "name",
101-
"namespace": "namespace",
103+
"metadata": map[string]interface{}{
104+
"name": "name",
105+
"namespace": "namespace",
106+
},
102107
"spec": map[string]interface{}{
103108
"replicas": 2,
104109
"selector": map[string]interface{}{
@@ -173,6 +178,35 @@ func ExampleClient_update() {
173178
_ = c.Update(context.Background(), u)
174179
}
175180

181+
// This example shows how to use the client with typed and unstructured objects to patch objects.
182+
func ExampleClient_patch() {
183+
patch := []byte(`{"metadata":{"annotations":{"version": "v2"}}}`)
184+
_ = c.Patch(context.Background(), &corev1.Pod{
185+
ObjectMeta: metav1.ObjectMeta{
186+
Namespace: "namespace",
187+
Name: "name",
188+
},
189+
}, client.RawPatch(types.StrategicMergePatchType, patch))
190+
}
191+
192+
// This example shows how to use the client with typed and unstructured objects to patch objects' status.
193+
func ExampleClient_patchStatus() {
194+
u := &unstructured.Unstructured{}
195+
u.Object = map[string]interface{}{
196+
"metadata": map[string]interface{}{
197+
"name": "foo",
198+
"namespace": "namespace",
199+
},
200+
}
201+
u.SetGroupVersionKind(schema.GroupVersionKind{
202+
Group: "batch",
203+
Version: "v1beta1",
204+
Kind: "CronJob",
205+
})
206+
patch := []byte(fmt.Sprintf(`{"status":{"lastScheduleTime":"%s"}}`, time.Now().Format(time.RFC3339)))
207+
_ = c.Status().Patch(context.Background(), u, client.RawPatch(types.MergePatchType, patch))
208+
}
209+
176210
// This example shows how to use the client with typed and unstructured objects to delete objects.
177211
func ExampleClient_delete() {
178212
// Using a typed object.

pkg/client/fake/client.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,11 @@ func (t versionedTracker) Create(gvr schema.GroupVersionResource, obj runtime.Ob
9696
return apierrors.NewBadRequest("resourceVersion can not be set for Create requests")
9797
}
9898
accessor.SetResourceVersion("1")
99-
return t.ObjectTracker.Create(gvr, obj, ns)
99+
if err := t.ObjectTracker.Create(gvr, obj, ns); err != nil {
100+
accessor.SetResourceVersion("")
101+
return err
102+
}
103+
return nil
100104
}
101105

102106
func (t versionedTracker) Update(gvr schema.GroupVersionResource, obj runtime.Object, ns string) error {
@@ -267,7 +271,7 @@ func (c *fakeClient) Delete(ctx context.Context, obj runtime.Object, opts ...cli
267271
}
268272

269273
func (c *fakeClient) DeleteAllOf(ctx context.Context, obj runtime.Object, opts ...client.DeleteAllOfOption) error {
270-
gvk, err := apiutil.GVKForObject(obj, scheme.Scheme)
274+
gvk, err := apiutil.GVKForObject(obj, c.scheme)
271275
if err != nil {
272276
return err
273277
}

pkg/client/fake/client_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,14 @@ var _ = Describe("Fake client", func() {
186186
Expect(apierrors.IsBadRequest(err)).To(BeTrue())
187187
})
188188

189+
It("should not change the submitted object if Create failed", func() {
190+
By("Trying to create an existing configmap")
191+
submitted := cm.DeepCopy()
192+
err := cl.Create(context.Background(), submitted)
193+
Expect(apierrors.IsAlreadyExists(err)).To(BeTrue())
194+
Expect(submitted).To(Equal(cm))
195+
})
196+
189197
It("should error on Create with empty Name", func() {
190198
By("Creating a new configmap")
191199
newcm := &corev1.ConfigMap{

pkg/client/patch.go

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ limitations under the License.
1717
package client
1818

1919
import (
20+
"fmt"
21+
2022
jsonpatch "github.com/evanphx/json-patch"
23+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
24+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2125
"k8s.io/apimachinery/pkg/runtime"
2226
"k8s.io/apimachinery/pkg/types"
2327
"k8s.io/apimachinery/pkg/util/json"
@@ -59,8 +63,39 @@ func ConstantPatch(patchType types.PatchType, data []byte) Patch {
5963
return RawPatch(patchType, data)
6064
}
6165

66+
// MergeFromWithOptimisticLock can be used if clients want to make sure a patch
67+
// is being applied to the latest resource version of an object.
68+
//
69+
// The behavior is similar to what an Update would do, without the need to send the
70+
// whole object. Usually this method is useful if you might have multiple clients
71+
// acting on the same object and the same API version, but with different versions of the Go structs.
72+
//
73+
// For example, an "older" copy of a Widget that has fields A and B, and a "newer" copy with A, B, and C.
74+
// Sending an update using the older struct definition results in C being dropped, whereas using a patch does not.
75+
type MergeFromWithOptimisticLock struct{}
76+
77+
// ApplyToMergeFrom applies this configuration to the given patch options.
78+
func (m MergeFromWithOptimisticLock) ApplyToMergeFrom(in *MergeFromOptions) {
79+
in.OptimisticLock = true
80+
}
81+
82+
// MergeFromOption is some configuration that modifies options for a merge-from patch data.
83+
type MergeFromOption interface {
84+
// ApplyToMergeFrom applies this configuration to the given patch options.
85+
ApplyToMergeFrom(*MergeFromOptions)
86+
}
87+
88+
// MergeFromOptions contains options to generate a merge-from patch data.
89+
type MergeFromOptions struct {
90+
// OptimisticLock, when true, includes `metadata.resourceVersion` into the final
91+
// patch data. If the `resourceVersion` field doesn't match what's stored,
92+
// the operation results in a conflict and clients will need to try again.
93+
OptimisticLock bool
94+
}
95+
6296
type mergeFromPatch struct {
6397
from runtime.Object
98+
opts MergeFromOptions
6499
}
65100

66101
// Type implements patch.
@@ -80,12 +115,47 @@ func (s *mergeFromPatch) Data(obj runtime.Object) ([]byte, error) {
80115
return nil, err
81116
}
82117

83-
return jsonpatch.CreateMergePatch(originalJSON, modifiedJSON)
118+
data, err := jsonpatch.CreateMergePatch(originalJSON, modifiedJSON)
119+
if err != nil {
120+
return nil, err
121+
}
122+
123+
if s.opts.OptimisticLock {
124+
dataMap := map[string]interface{}{}
125+
if err := json.Unmarshal(data, &dataMap); err != nil {
126+
return nil, err
127+
}
128+
fromMeta, ok := s.from.(metav1.Object)
129+
if !ok {
130+
return nil, fmt.Errorf("cannot use OptimisticLock, from object %q is not a valid metav1.Object", s.from)
131+
}
132+
resourceVersion := fromMeta.GetResourceVersion()
133+
if len(resourceVersion) == 0 {
134+
return nil, fmt.Errorf("cannot use OptimisticLock, from object %q does not have any resource version we can use", s.from)
135+
}
136+
u := &unstructured.Unstructured{Object: dataMap}
137+
u.SetResourceVersion(resourceVersion)
138+
data, err = json.Marshal(u)
139+
if err != nil {
140+
return nil, err
141+
}
142+
}
143+
144+
return data, nil
84145
}
85146

86147
// MergeFrom creates a Patch that patches using the merge-patch strategy with the given object as base.
87148
func MergeFrom(obj runtime.Object) Patch {
88-
return &mergeFromPatch{obj}
149+
return &mergeFromPatch{from: obj}
150+
}
151+
152+
// MergeFromWithOptions creates a Patch that patches using the merge-patch strategy with the given object as base.
153+
func MergeFromWithOptions(obj runtime.Object, opts ...MergeFromOption) Patch {
154+
options := &MergeFromOptions{}
155+
for _, opt := range opts {
156+
opt.ApplyToMergeFrom(options)
157+
}
158+
return &mergeFromPatch{from: obj, opts: *options}
89159
}
90160

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

0 commit comments

Comments
 (0)