diff --git a/apis/duck/patch.go b/apis/duck/patch.go new file mode 100644 index 000000000..ed06d7da7 --- /dev/null +++ b/apis/duck/patch.go @@ -0,0 +1,44 @@ +/* +Copyright 2018 The Knative 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 duck + +import ( + "encoding/json" + + "github.com/mattbaird/jsonpatch" +) + +func CreatePatch(before, after interface{}) (JSONPatch, error) { + // Marshal the before and after. + rawBefore, err := json.Marshal(before) + if err != nil { + return nil, err + } + + rawAfter, err := json.Marshal(after) + if err != nil { + return nil, err + } + + return jsonpatch.CreatePatch(rawBefore, rawAfter) +} + +type JSONPatch []jsonpatch.JsonPatchOperation + +func (p JSONPatch) MarshalJSON() ([]byte, error) { + return json.Marshal([]jsonpatch.JsonPatchOperation(p)) +} diff --git a/apis/duck/patch_test.go b/apis/duck/patch_test.go new file mode 100644 index 000000000..4d878be64 --- /dev/null +++ b/apis/duck/patch_test.go @@ -0,0 +1,191 @@ +/* +Copyright 2018 The Knative 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 duck + +import ( + "encoding/json" + "errors" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestCreatePatch(t *testing.T) { + tests := []struct { + name string + before interface{} + after interface{} + wantErr bool + want JSONPatch + }{{ + name: "patch single field", + before: &Patch{ + Spec: PatchSpec{ + Patchable: &Patchable{ + Field1: 12, + }, + }, + }, + after: &Patch{ + Spec: PatchSpec{ + Patchable: &Patchable{ + Field1: 13, + }, + }, + }, + want: JSONPatch{{ + Operation: "replace", + Path: "/status/patchable/field1", + Value: 13.0, + }}, + }, { + name: "patch two fields", + before: &Patch{ + Spec: PatchSpec{ + Patchable: &Patchable{ + Field1: 12, + Field2: true, + }, + }, + }, + after: &Patch{ + Spec: PatchSpec{ + Patchable: &Patchable{ + Field1: 42, + Field2: false, + }, + }, + }, + want: JSONPatch{{ + Operation: "replace", + Path: "/status/patchable/field1", + Value: 42.0, + }, { + Operation: "remove", + Path: "/status/patchable/field2", + }}, + }, { + name: "patch array", + before: &Patch{ + Spec: PatchSpec{ + Patchable: &Patchable{ + Array: []string{"foo", "baz"}, + }, + }, + }, + after: &Patch{ + Spec: PatchSpec{ + Patchable: &Patchable{ + Array: []string{"foo", "bar", "baz"}, + }, + }, + }, + want: JSONPatch{{ + Operation: "add", + Path: "/status/patchable/array/1", + Value: "bar", + }}, + }, { + name: "before doesn't marshal", + before: &DoesntMarshal{}, + after: &Patch{}, + wantErr: true, + }, { + name: "after doesn't marshal", + before: &Patch{}, + after: &DoesntMarshal{}, + wantErr: true, + }} + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := CreatePatch(test.before, test.after) + if err != nil { + if !test.wantErr { + t.Errorf("CreatePatch() = %v", err) + } + return + } else if test.wantErr { + t.Errorf("CreatePatch() = %v, wanted error", got) + return + } + + if diff := cmp.Diff(test.want, got); diff != "" { + t.Errorf("CreatePatch (-want, +got) = %v", diff) + } + }) + } +} + +func TestPatchToJSON(t *testing.T) { + input := JSONPatch{{ + Operation: "replace", + Path: "/status/patchable/field1", + Value: 42.0, + }, { + Operation: "remove", + Path: "/status/patchable/field2", + }} + + b, err := input.MarshalJSON() + if err != nil { + t.Errorf("MarshalJSON() = %v", err) + } + + want := `[{"op":"replace","path":"/status/patchable/field1","value":42},{"op":"remove","path":"/status/patchable/field2"}]` + + got := string(b) + if got != want { + t.Errorf("MarshalJSON() = %v, wanted %v", got, want) + } +} + +type DoesntMarshal struct{} + +var _ json.Marshaler = (*DoesntMarshal)(nil) + +func (_ *DoesntMarshal) MarshalJSON() ([]byte, error) { + return nil, errors.New("what did you expect?") +} + +// Define a "Patchable" duck type. +type Patchable struct { + Field1 int `json:"field1,omitempty"` + Field2 bool `json:"field2,omitempty"` + Array []string `json:"array,omitempty"` +} +type Patch struct { + Spec PatchSpec `json:"status"` +} +type PatchSpec struct { + Patchable *Patchable `json:"patchable,omitempty"` +} + +var _ Implementable = (*Patchable)(nil) +var _ Populatable = (*Patch)(nil) + +func (_ *Patchable) GetFullType() Populatable { + return &Patch{} +} + +func (f *Patch) Populate() { + f.Spec.Patchable = &Patchable{ + // Populate ALL fields + Field1: 42, + Field2: true, + } +} diff --git a/webhook/webhook.go b/webhook/webhook.go index b94fa9573..a65289e33 100644 --- a/webhook/webhook.go +++ b/webhook/webhook.go @@ -224,7 +224,7 @@ func SetDefaults(ctx context.Context) ResourceDefaulter { before, after := crd.DeepCopyObject(), crd after.SetDefaults() - patch, err := createPatch(before, after) + patch, err := duck.CreatePatch(before, after) if err != nil { return err } @@ -233,21 +233,6 @@ func SetDefaults(ctx context.Context) ResourceDefaulter { } } -func createPatch(before, after interface{}) ([]jsonpatch.JsonPatchOperation, error) { - // Marshal the before and after. - rawBefore, err := json.Marshal(before) - if err != nil { - return nil, err - } - - rawAfter, err := json.Marshal(after) - if err != nil { - return nil, err - } - - return jsonpatch.CreatePatch(rawBefore, rawAfter) -} - func configureCerts(ctx context.Context, client kubernetes.Interface, options *ControllerOptions) (*tls.Config, []byte, error) { apiServerCACert, err := getAPIServerExtensionCACert(client) if err != nil { @@ -594,7 +579,7 @@ func updateGeneration(ctx context.Context, patches *[]jsonpatch.JsonPatchOperati after := before.DeepCopyObject().(*duckv1alpha1.Generational) after.Spec.Generation = after.Spec.Generation + 1 - genBump, err := createPatch(before, after) + genBump, err := duck.CreatePatch(before, after) if err != nil { return err }