Unverified Commit e412ad53 authored by Kubernetes Submit Queue's avatar Kubernetes Submit Queue Committed by GitHub

Merge pull request #51321 from mengqiy/kubectl_apply_openapi

Automatic merge from submit-queue (batch tested with PRs 51321, 55969, 55039, 56183, 55976). If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>. Kubectl apply and strategic merge patch using openapi - [x] support openapi in strategic merge patch - [x] test openapi in strategic merge patch - [x] kubectl apply use openapi to calculate diff be default. It will fall back to use baked-in types when openapi is not available. - [x] test openapi in kubectl apply Fixes: kubernetes/kubectl#55 ```release-note kubectl apply use openapi to calculate diff be default. It will fall back to use baked-in types when openapi is not available. ``` /assign @apelisse
parents 4e2f5e22 be20a675
......@@ -86,6 +86,7 @@ go_library(
"//pkg/kubectl/cmd/templates:go_default_library",
"//pkg/kubectl/cmd/util:go_default_library",
"//pkg/kubectl/cmd/util/editor:go_default_library",
"//pkg/kubectl/cmd/util/openapi:go_default_library",
"//pkg/kubectl/explain:go_default_library",
"//pkg/kubectl/metricsutil:go_default_library",
"//pkg/kubectl/plugins:go_default_library",
......@@ -147,6 +148,7 @@ go_library(
"//vendor/k8s.io/client-go/tools/portforward:go_default_library",
"//vendor/k8s.io/client-go/tools/remotecommand:go_default_library",
"//vendor/k8s.io/client-go/transport/spdy:go_default_library",
"//vendor/k8s.io/kube-openapi/pkg/util/proto:go_default_library",
"//vendor/k8s.io/utils/exec:go_default_library",
],
)
......@@ -214,6 +216,7 @@ go_test(
"//pkg/kubectl:go_default_library",
"//pkg/kubectl/cmd/testing:go_default_library",
"//pkg/kubectl/cmd/util:go_default_library",
"//pkg/kubectl/cmd/util/openapi:go_default_library",
"//pkg/kubectl/plugins:go_default_library",
"//pkg/kubectl/resource:go_default_library",
"//pkg/kubectl/scheme:go_default_library",
......@@ -241,6 +244,7 @@ go_test(
"//vendor/k8s.io/apimachinery/pkg/util/intstr:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/strategicpatch:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/strategicpatch/testing:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",
"//vendor/k8s.io/client-go/dynamic:go_default_library",
"//vendor/k8s.io/client-go/rest:go_default_library",
......
......@@ -37,11 +37,13 @@ import (
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/strategicpatch"
"k8s.io/apimachinery/pkg/util/wait"
oapi "k8s.io/kube-openapi/pkg/util/proto"
api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
"k8s.io/kubernetes/pkg/kubectl"
"k8s.io/kubernetes/pkg/kubectl/cmd/templates"
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
"k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi"
"k8s.io/kubernetes/pkg/kubectl/resource"
"k8s.io/kubernetes/pkg/kubectl/scheme"
"k8s.io/kubernetes/pkg/kubectl/util/i18n"
......@@ -127,6 +129,7 @@ func NewCmdApply(baseName string, f cmdutil.Factory, out, errOut io.Writer) *cob
cmd.Flags().StringVarP(&options.Selector, "selector", "l", "", "Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2)")
cmd.Flags().Bool("all", false, "Select all resources in the namespace of the specified resource types.")
cmd.Flags().StringArray("prune-whitelist", []string{}, "Overwrite the default whitelist with <group/version/kind> for --prune")
cmd.Flags().Bool("openapi-patch", true, "If true, use openapi to calculate diff when the openapi presents and the resource can be found in the openapi spec. Otherwise, fall back to use baked-in types.")
cmdutil.AddDryRunFlag(cmd)
cmdutil.AddPrinterFlags(cmd)
cmdutil.AddRecordFlag(cmd)
......@@ -193,6 +196,14 @@ func RunApply(f cmdutil.Factory, cmd *cobra.Command, out, errOut io.Writer, opti
return err
}
var openapiSchema openapi.Resources
if cmdutil.GetFlagBool(cmd, "openapi-patch") {
openapiSchema, err = f.OpenAPISchema()
if err != nil {
openapiSchema = nil
}
}
cmdNamespace, enforceNamespace, err := f.DefaultNamespace()
if err != nil {
return err
......@@ -312,9 +323,10 @@ func RunApply(f cmdutil.Factory, cmd *cobra.Command, out, errOut io.Writer, opti
cascade: options.Cascade,
timeout: options.Timeout,
gracePeriod: options.GracePeriod,
openapiSchema: openapiSchema,
}
patchBytes, patchedObject, err := patcher.patch(info.Object, modified, info.Source, info.Namespace, info.Name)
patchBytes, patchedObject, err := patcher.patch(info.Object, modified, info.Source, info.Namespace, info.Name, errOut)
if err != nil {
return cmdutil.AddSourceToErr(fmt.Sprintf("applying patch:\n%s\nto:\n%v\nfor:", patchBytes, info), info.Source, err)
}
......@@ -570,9 +582,11 @@ type patcher struct {
cascade bool
timeout time.Duration
gracePeriod int
openapiSchema openapi.Resources
}
func (p *patcher) patchSimple(obj runtime.Object, modified []byte, source, namespace, name string) ([]byte, runtime.Object, error) {
func (p *patcher) patchSimple(obj runtime.Object, modified []byte, source, namespace, name string, errOut io.Writer) ([]byte, runtime.Object, error) {
// Serialize the current configuration of the object from the server.
current, err := runtime.Encode(p.encoder, obj)
if err != nil {
......@@ -585,33 +599,55 @@ func (p *patcher) patchSimple(obj runtime.Object, modified []byte, source, names
return nil, nil, cmdutil.AddSourceToErr(fmt.Sprintf("retrieving original configuration from:\n%v\nfor:", obj), source, err)
}
// Create the versioned struct from the type defined in the restmapping
// (which is the API version we'll be submitting the patch to)
versionedObject, err := scheme.Scheme.New(p.mapping.GroupVersionKind)
var patchType types.PatchType
var patch []byte
var lookupPatchMeta strategicpatch.LookupPatchMeta
var schema oapi.Schema
createPatchErrFormat := "creating patch with:\noriginal:\n%s\nmodified:\n%s\ncurrent:\n%s\nfor:"
switch {
case runtime.IsNotRegisteredError(err):
// fall back to generic JSON merge patch
patchType = types.MergePatchType
preconditions := []mergepatch.PreconditionFunc{mergepatch.RequireKeyUnchanged("apiVersion"),
mergepatch.RequireKeyUnchanged("kind"), mergepatch.RequireMetadataKeyUnchanged("name")}
patch, err = jsonmergepatch.CreateThreeWayJSONMergePatch(original, modified, current, preconditions...)
if err != nil {
if mergepatch.IsPreconditionFailed(err) {
return nil, nil, fmt.Errorf("%s", "At least one of apiVersion, kind and name was changed")
// Try to use openapi first if the openapi spec is available and can successfully calculate the patch.
// Otherwise, fall back to baked-in types.
if p.openapiSchema != nil {
if schema = p.openapiSchema.LookupResource(p.mapping.GroupVersionKind); schema != nil {
lookupPatchMeta = strategicpatch.PatchMetaFromOpenAPI{Schema: schema}
if openapiPatch, err := strategicpatch.CreateThreeWayMergePatch(original, modified, current, lookupPatchMeta, p.overwrite); err != nil {
fmt.Fprintf(errOut, "warning: error calculating patch from openapi spec: %v\n", err)
} else {
patchType = types.StrategicMergePatchType
patch = openapiPatch
}
return nil, nil, cmdutil.AddSourceToErr(fmt.Sprintf(createPatchErrFormat, original, modified, current), source, err)
}
case err != nil:
return nil, nil, cmdutil.AddSourceToErr(fmt.Sprintf("getting instance of versioned object for %v:", p.mapping.GroupVersionKind), source, err)
case err == nil:
// Compute a three way strategic merge patch to send to server.
patchType = types.StrategicMergePatchType
patch, err = strategicpatch.CreateThreeWayMergePatch(original, modified, current, versionedObject, p.overwrite)
if err != nil {
return nil, nil, cmdutil.AddSourceToErr(fmt.Sprintf(createPatchErrFormat, original, modified, current), source, err)
}
if patch == nil {
// Create the versioned struct from the type defined in the restmapping
// (which is the API version we'll be submitting the patch to)
versionedObject, err := scheme.Scheme.New(p.mapping.GroupVersionKind)
switch {
case runtime.IsNotRegisteredError(err):
// fall back to generic JSON merge patch
patchType = types.MergePatchType
preconditions := []mergepatch.PreconditionFunc{mergepatch.RequireKeyUnchanged("apiVersion"),
mergepatch.RequireKeyUnchanged("kind"), mergepatch.RequireMetadataKeyUnchanged("name")}
patch, err = jsonmergepatch.CreateThreeWayJSONMergePatch(original, modified, current, preconditions...)
if err != nil {
if mergepatch.IsPreconditionFailed(err) {
return nil, nil, fmt.Errorf("%s", "At least one of apiVersion, kind and name was changed")
}
return nil, nil, cmdutil.AddSourceToErr(fmt.Sprintf(createPatchErrFormat, original, modified, current), source, err)
}
case err != nil:
return nil, nil, cmdutil.AddSourceToErr(fmt.Sprintf("getting instance of versioned object for %v:", p.mapping.GroupVersionKind), source, err)
case err == nil:
// Compute a three way strategic merge patch to send to server.
patchType = types.StrategicMergePatchType
lookupPatchMeta, err = strategicpatch.NewPatchMetaFromStruct(versionedObject)
if err != nil {
return nil, nil, cmdutil.AddSourceToErr(fmt.Sprintf(createPatchErrFormat, original, modified, current), source, err)
}
patch, err = strategicpatch.CreateThreeWayMergePatch(original, modified, current, lookupPatchMeta, p.overwrite)
if err != nil {
return nil, nil, cmdutil.AddSourceToErr(fmt.Sprintf(createPatchErrFormat, original, modified, current), source, err)
}
}
}
......@@ -623,9 +659,9 @@ func (p *patcher) patchSimple(obj runtime.Object, modified []byte, source, names
return patch, patchedObj, err
}
func (p *patcher) patch(current runtime.Object, modified []byte, source, namespace, name string) ([]byte, runtime.Object, error) {
func (p *patcher) patch(current runtime.Object, modified []byte, source, namespace, name string, errOut io.Writer) ([]byte, runtime.Object, error) {
var getErr error
patchBytes, patchObject, err := p.patchSimple(current, modified, source, namespace, name)
patchBytes, patchObject, err := p.patchSimple(current, modified, source, namespace, name, errOut)
for i := 1; i <= maxPatchRetry && errors.IsConflict(err); i++ {
if i > triesBeforeBackOff {
p.backOff.Sleep(backOffPeriod)
......@@ -634,7 +670,7 @@ func (p *patcher) patch(current runtime.Object, modified []byte, source, namespa
if getErr != nil {
return nil, nil, getErr
}
patchBytes, patchObject, err = p.patchSimple(current, modified, source, namespace, name)
patchBytes, patchObject, err = p.patchSimple(current, modified, source, namespace, name, errOut)
}
if err != nil && p.force {
patchBytes, patchObject, err = p.deleteAndCreate(modified, namespace, name)
......
......@@ -19,10 +19,12 @@ package cmd
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strings"
"testing"
......@@ -33,15 +35,33 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
sptest "k8s.io/apimachinery/pkg/util/strategicpatch/testing"
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/rest/fake"
"k8s.io/kubernetes/pkg/api/testapi"
api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/apis/extensions"
cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing"
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
"k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi"
"k8s.io/kubernetes/pkg/printers"
)
var (
fakeSchema = sptest.Fake{Path: filepath.Join("..", "..", "..", "api", "openapi-spec", "swagger.json")}
testingOpenAPISchemaFns = []func() (openapi.Resources, error){nil, AlwaysErrorOpenAPISchemaFn, openAPISchemaFn}
AlwaysErrorOpenAPISchemaFn = func() (openapi.Resources, error) {
return nil, errors.New("cannot get openapi spec")
}
openAPISchemaFn = func() (openapi.Resources, error) {
s, err := fakeSchema.OpenAPISchema()
if err != nil {
return nil, err
}
return openapi.NewOpenAPIData(s)
}
)
func TestApplyExtraArgsFail(t *testing.T) {
buf := bytes.NewBuffer([]byte{})
errBuf := bytes.NewBuffer([]byte{})
......@@ -410,38 +430,44 @@ func TestApplyObject(t *testing.T) {
nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC)
pathRC := "/namespaces/test/replicationcontrollers/" + nameRC
f, tf, _, _ := cmdtesting.NewAPIFactory()
tf.Printer = &testPrinter{}
tf.UnstructuredClient = &fake.RESTClient{
NegotiatedSerializer: unstructuredSerializer,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
switch p, m := req.URL.Path, req.Method; {
case p == pathRC && m == "GET":
bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodyRC}, nil
case p == pathRC && m == "PATCH":
validatePatchApplication(t, req)
bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodyRC}, nil
default:
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
return nil, nil
}
}),
}
tf.Namespace = "test"
buf := bytes.NewBuffer([]byte{})
errBuf := bytes.NewBuffer([]byte{})
for _, fn := range testingOpenAPISchemaFns {
f, tf, _, _ := cmdtesting.NewAPIFactory()
tf.Printer = &testPrinter{}
tf.UnstructuredClient = &fake.RESTClient{
NegotiatedSerializer: unstructuredSerializer,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
switch p, m := req.URL.Path, req.Method; {
case p == pathRC && m == "GET":
bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodyRC}, nil
case p == pathRC && m == "PATCH":
validatePatchApplication(t, req)
bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodyRC}, nil
default:
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
return nil, nil
}
}),
}
tf.OpenAPISchemaFunc = fn
tf.Namespace = "test"
buf := bytes.NewBuffer([]byte{})
errBuf := bytes.NewBuffer([]byte{})
cmd := NewCmdApply("kubectl", f, buf, errBuf)
cmd.Flags().Set("filename", filenameRC)
cmd.Flags().Set("output", "name")
cmd.Run(cmd, []string{})
cmd := NewCmdApply("kubectl", f, buf, errBuf)
cmd.Flags().Set("filename", filenameRC)
cmd.Flags().Set("output", "name")
cmd.Run(cmd, []string{})
// uses the name from the file, not the response
expectRC := "replicationcontroller/" + nameRC + "\n"
if buf.String() != expectRC {
t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expectRC)
// uses the name from the file, not the response
expectRC := "replicationcontroller/" + nameRC + "\n"
if buf.String() != expectRC {
t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expectRC)
}
if errBuf.String() != "" {
t.Fatalf("unexpected error output: %s", errBuf.String())
}
}
}
......@@ -466,39 +492,45 @@ func TestApplyObjectOutput(t *testing.T) {
t.Fatal(err)
}
f, tf, _, _ := cmdtesting.NewAPIFactory()
tf.Printer = &printers.YAMLPrinter{}
tf.UnstructuredClient = &fake.RESTClient{
NegotiatedSerializer: unstructuredSerializer,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
switch p, m := req.URL.Path, req.Method; {
case p == pathRC && m == "GET":
bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodyRC}, nil
case p == pathRC && m == "PATCH":
validatePatchApplication(t, req)
bodyRC := ioutil.NopCloser(bytes.NewReader(postPatchData))
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodyRC}, nil
default:
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
return nil, nil
}
}),
}
tf.Namespace = "test"
buf := bytes.NewBuffer([]byte{})
errBuf := bytes.NewBuffer([]byte{})
for _, fn := range testingOpenAPISchemaFns {
f, tf, _, _ := cmdtesting.NewAPIFactory()
tf.Printer = &printers.YAMLPrinter{}
tf.UnstructuredClient = &fake.RESTClient{
NegotiatedSerializer: unstructuredSerializer,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
switch p, m := req.URL.Path, req.Method; {
case p == pathRC && m == "GET":
bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodyRC}, nil
case p == pathRC && m == "PATCH":
validatePatchApplication(t, req)
bodyRC := ioutil.NopCloser(bytes.NewReader(postPatchData))
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodyRC}, nil
default:
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
return nil, nil
}
}),
}
tf.OpenAPISchemaFunc = fn
tf.Namespace = "test"
buf := bytes.NewBuffer([]byte{})
errBuf := bytes.NewBuffer([]byte{})
cmd := NewCmdApply("kubectl", f, buf, errBuf)
cmd.Flags().Set("filename", filenameRC)
cmd.Flags().Set("output", "yaml")
cmd.Run(cmd, []string{})
cmd := NewCmdApply("kubectl", f, buf, errBuf)
cmd.Flags().Set("filename", filenameRC)
cmd.Flags().Set("output", "yaml")
cmd.Run(cmd, []string{})
if !strings.Contains(buf.String(), "name: test-rc") {
t.Fatalf("unexpected output: %s\nexpected to contain: %s", buf.String(), "name: test-rc")
}
if !strings.Contains(buf.String(), "post-patch: value") {
t.Fatalf("unexpected output: %s\nexpected to contain: %s", buf.String(), "post-patch: value")
if !strings.Contains(buf.String(), "name: test-rc") {
t.Fatalf("unexpected output: %s\nexpected to contain: %s", buf.String(), "name: test-rc")
}
if !strings.Contains(buf.String(), "post-patch: value") {
t.Fatalf("unexpected output: %s\nexpected to contain: %s", buf.String(), "post-patch: value")
}
if errBuf.String() != "" {
t.Fatalf("unexpected error output: %s", errBuf.String())
}
}
}
......@@ -507,54 +539,60 @@ func TestApplyRetry(t *testing.T) {
nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC)
pathRC := "/namespaces/test/replicationcontrollers/" + nameRC
firstPatch := true
retry := false
getCount := 0
f, tf, _, _ := cmdtesting.NewAPIFactory()
tf.Printer = &testPrinter{}
tf.UnstructuredClient = &fake.RESTClient{
NegotiatedSerializer: unstructuredSerializer,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
switch p, m := req.URL.Path, req.Method; {
case p == pathRC && m == "GET":
getCount++
bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodyRC}, nil
case p == pathRC && m == "PATCH":
if firstPatch {
firstPatch = false
statusErr := kubeerr.NewConflict(schema.GroupResource{Group: "", Resource: "rc"}, "test-rc", fmt.Errorf("the object has been modified. Please apply at first."))
bodyBytes, _ := json.Marshal(statusErr)
bodyErr := ioutil.NopCloser(bytes.NewReader(bodyBytes))
return &http.Response{StatusCode: http.StatusConflict, Header: defaultHeader(), Body: bodyErr}, nil
for _, fn := range testingOpenAPISchemaFns {
firstPatch := true
retry := false
getCount := 0
f, tf, _, _ := cmdtesting.NewAPIFactory()
tf.Printer = &testPrinter{}
tf.UnstructuredClient = &fake.RESTClient{
NegotiatedSerializer: unstructuredSerializer,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
switch p, m := req.URL.Path, req.Method; {
case p == pathRC && m == "GET":
getCount++
bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodyRC}, nil
case p == pathRC && m == "PATCH":
if firstPatch {
firstPatch = false
statusErr := kubeerr.NewConflict(schema.GroupResource{Group: "", Resource: "rc"}, "test-rc", fmt.Errorf("the object has been modified. Please apply at first."))
bodyBytes, _ := json.Marshal(statusErr)
bodyErr := ioutil.NopCloser(bytes.NewReader(bodyBytes))
return &http.Response{StatusCode: http.StatusConflict, Header: defaultHeader(), Body: bodyErr}, nil
}
retry = true
validatePatchApplication(t, req)
bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodyRC}, nil
default:
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
return nil, nil
}
retry = true
validatePatchApplication(t, req)
bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodyRC}, nil
default:
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
return nil, nil
}
}),
}
tf.Namespace = "test"
buf := bytes.NewBuffer([]byte{})
errBuf := bytes.NewBuffer([]byte{})
}),
}
tf.OpenAPISchemaFunc = fn
tf.Namespace = "test"
buf := bytes.NewBuffer([]byte{})
errBuf := bytes.NewBuffer([]byte{})
cmd := NewCmdApply("kubectl", f, buf, errBuf)
cmd.Flags().Set("filename", filenameRC)
cmd.Flags().Set("output", "name")
cmd.Run(cmd, []string{})
cmd := NewCmdApply("kubectl", f, buf, errBuf)
cmd.Flags().Set("filename", filenameRC)
cmd.Flags().Set("output", "name")
cmd.Run(cmd, []string{})
if !retry || getCount != 2 {
t.Fatalf("apply didn't retry when get conflict error")
}
if !retry || getCount != 2 {
t.Fatalf("apply didn't retry when get conflict error")
}
// uses the name from the file, not the response
expectRC := "replicationcontroller/" + nameRC + "\n"
if buf.String() != expectRC {
t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expectRC)
// uses the name from the file, not the response
expectRC := "replicationcontroller/" + nameRC + "\n"
if buf.String() != expectRC {
t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expectRC)
}
if errBuf.String() != "" {
t.Fatalf("unexpected error output: %s", errBuf.String())
}
}
}
......@@ -682,55 +720,61 @@ func testApplyMultipleObjects(t *testing.T, asList bool) {
nameSVC, currentSVC := readAndAnnotateService(t, filenameSVC)
pathSVC := "/namespaces/test/services/" + nameSVC
f, tf, _, _ := cmdtesting.NewAPIFactory()
tf.Printer = &testPrinter{}
tf.UnstructuredClient = &fake.RESTClient{
NegotiatedSerializer: unstructuredSerializer,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
switch p, m := req.URL.Path, req.Method; {
case p == pathRC && m == "GET":
bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodyRC}, nil
case p == pathRC && m == "PATCH":
validatePatchApplication(t, req)
bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodyRC}, nil
case p == pathSVC && m == "GET":
bodySVC := ioutil.NopCloser(bytes.NewReader(currentSVC))
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodySVC}, nil
case p == pathSVC && m == "PATCH":
validatePatchApplication(t, req)
bodySVC := ioutil.NopCloser(bytes.NewReader(currentSVC))
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodySVC}, nil
default:
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
return nil, nil
}
}),
}
tf.Namespace = "test"
buf := bytes.NewBuffer([]byte{})
errBuf := bytes.NewBuffer([]byte{})
cmd := NewCmdApply("kubectl", f, buf, errBuf)
if asList {
cmd.Flags().Set("filename", filenameRCSVC)
} else {
cmd.Flags().Set("filename", filenameRC)
cmd.Flags().Set("filename", filenameSVC)
}
cmd.Flags().Set("output", "name")
cmd.Run(cmd, []string{})
// Names should come from the REST response, NOT the files
expectRC := "replicationcontroller/" + nameRC + "\n"
expectSVC := "service/" + nameSVC + "\n"
// Test both possible orders since output is non-deterministic.
expectOne := expectRC + expectSVC
expectTwo := expectSVC + expectRC
if buf.String() != expectOne && buf.String() != expectTwo {
t.Fatalf("unexpected output: %s\nexpected: %s OR %s", buf.String(), expectOne, expectTwo)
for _, fn := range testingOpenAPISchemaFns {
f, tf, _, _ := cmdtesting.NewAPIFactory()
tf.Printer = &testPrinter{}
tf.UnstructuredClient = &fake.RESTClient{
NegotiatedSerializer: unstructuredSerializer,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
switch p, m := req.URL.Path, req.Method; {
case p == pathRC && m == "GET":
bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodyRC}, nil
case p == pathRC && m == "PATCH":
validatePatchApplication(t, req)
bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodyRC}, nil
case p == pathSVC && m == "GET":
bodySVC := ioutil.NopCloser(bytes.NewReader(currentSVC))
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodySVC}, nil
case p == pathSVC && m == "PATCH":
validatePatchApplication(t, req)
bodySVC := ioutil.NopCloser(bytes.NewReader(currentSVC))
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodySVC}, nil
default:
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
return nil, nil
}
}),
}
tf.OpenAPISchemaFunc = fn
tf.Namespace = "test"
buf := bytes.NewBuffer([]byte{})
errBuf := bytes.NewBuffer([]byte{})
cmd := NewCmdApply("kubectl", f, buf, errBuf)
if asList {
cmd.Flags().Set("filename", filenameRCSVC)
} else {
cmd.Flags().Set("filename", filenameRC)
cmd.Flags().Set("filename", filenameSVC)
}
cmd.Flags().Set("output", "name")
cmd.Run(cmd, []string{})
// Names should come from the REST response, NOT the files
expectRC := "replicationcontroller/" + nameRC + "\n"
expectSVC := "service/" + nameSVC + "\n"
// Test both possible orders since output is non-deterministic.
expectOne := expectRC + expectSVC
expectTwo := expectSVC + expectRC
if buf.String() != expectOne && buf.String() != expectTwo {
t.Fatalf("unexpected output: %s\nexpected: %s OR %s", buf.String(), expectOne, expectTwo)
}
if errBuf.String() != "" {
t.Fatalf("unexpected error output: %s", errBuf.String())
}
}
}
......@@ -760,62 +804,67 @@ func TestApplyNULLPreservation(t *testing.T) {
verifiedPatch := false
deploymentBytes := readDeploymentFromFile(t, filenameDeployObjServerside)
f, tf, _, _ := cmdtesting.NewTestFactory()
tf.UnstructuredClient = &fake.RESTClient{
NegotiatedSerializer: unstructuredSerializer,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
switch p, m := req.URL.Path, req.Method; {
case p == deploymentPath && m == "GET":
body := ioutil.NopCloser(bytes.NewReader(deploymentBytes))
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: body}, nil
case p == deploymentPath && m == "PATCH":
patch, err := ioutil.ReadAll(req.Body)
if err != nil {
t.Fatal(err)
}
for _, fn := range testingOpenAPISchemaFns {
f, tf, _, _ := cmdtesting.NewAPIFactory()
tf.UnstructuredClient = &fake.RESTClient{
NegotiatedSerializer: unstructuredSerializer,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
switch p, m := req.URL.Path, req.Method; {
case p == deploymentPath && m == "GET":
body := ioutil.NopCloser(bytes.NewReader(deploymentBytes))
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: body}, nil
case p == deploymentPath && m == "PATCH":
patch, err := ioutil.ReadAll(req.Body)
if err != nil {
t.Fatal(err)
}
patchMap := map[string]interface{}{}
if err := json.Unmarshal(patch, &patchMap); err != nil {
t.Fatal(err)
}
annotationMap := walkMapPath(t, patchMap, []string{"metadata", "annotations"})
if _, ok := annotationMap[api.LastAppliedConfigAnnotation]; !ok {
t.Fatalf("patch does not contain annotation:\n%s\n", patch)
}
strategy := walkMapPath(t, patchMap, []string{"spec", "strategy"})
if value, ok := strategy["rollingUpdate"]; !ok || value != nil {
t.Fatalf("patch did not retain null value in key: rollingUpdate:\n%s\n", patch)
}
verifiedPatch = true
patchMap := map[string]interface{}{}
if err := json.Unmarshal(patch, &patchMap); err != nil {
t.Fatal(err)
}
annotationMap := walkMapPath(t, patchMap, []string{"metadata", "annotations"})
if _, ok := annotationMap[api.LastAppliedConfigAnnotation]; !ok {
t.Fatalf("patch does not contain annotation:\n%s\n", patch)
}
strategy := walkMapPath(t, patchMap, []string{"spec", "strategy"})
if value, ok := strategy["rollingUpdate"]; !ok || value != nil {
t.Fatalf("patch did not retain null value in key: rollingUpdate:\n%s\n", patch)
}
verifiedPatch = true
// The real API server would had returned the patched object but Kubectl
// is ignoring the actual return object.
// TODO: Make this match actual server behavior by returning the patched object.
body := ioutil.NopCloser(bytes.NewReader(deploymentBytes))
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: body}, nil
default:
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
return nil, nil
}
}),
}
tf.Namespace = "test"
tf.ClientConfig = defaultClientConfig()
buf := bytes.NewBuffer([]byte{})
errBuf := bytes.NewBuffer([]byte{})
// The real API server would had returned the patched object but Kubectl
// is ignoring the actual return object.
// TODO: Make this match actual server behavior by returning the patched object.
body := ioutil.NopCloser(bytes.NewReader(deploymentBytes))
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: body}, nil
default:
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
return nil, nil
}
}),
}
tf.OpenAPISchemaFunc = fn
tf.Namespace = "test"
buf := bytes.NewBuffer([]byte{})
errBuf := bytes.NewBuffer([]byte{})
cmd := NewCmdApply("kubectl", f, buf, errBuf)
cmd.Flags().Set("filename", filenameDeployObjClientside)
cmd.Flags().Set("output", "name")
cmd := NewCmdApply("kubectl", f, buf, errBuf)
cmd.Flags().Set("filename", filenameDeployObjClientside)
cmd.Flags().Set("output", "name")
cmd.Run(cmd, []string{})
cmd.Run(cmd, []string{})
expected := "deployment/" + deploymentName + "\n"
if buf.String() != expected {
t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expected)
}
if !verifiedPatch {
t.Fatal("No server-side patch call detected")
expected := "deployment/" + deploymentName + "\n"
if buf.String() != expected {
t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expected)
}
if errBuf.String() != "" {
t.Fatalf("unexpected error output: %s", errBuf.String())
}
if !verifiedPatch {
t.Fatal("No server-side patch call detected")
}
}
}
......@@ -827,53 +876,58 @@ func TestUnstructuredApply(t *testing.T) {
verifiedPatch := false
f, tf, _, _ := cmdtesting.NewAPIFactory()
tf.Printer = &testPrinter{}
tf.UnstructuredClient = &fake.RESTClient{
NegotiatedSerializer: unstructuredSerializer,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
switch p, m := req.URL.Path, req.Method; {
case p == path && m == "GET":
body := ioutil.NopCloser(bytes.NewReader(curr))
return &http.Response{
StatusCode: 200,
Header: defaultHeader(),
Body: body}, nil
case p == path && m == "PATCH":
contentType := req.Header.Get("Content-Type")
if contentType != "application/merge-patch+json" {
t.Fatalf("Unexpected Content-Type: %s", contentType)
for _, fn := range testingOpenAPISchemaFns {
f, tf, _, _ := cmdtesting.NewAPIFactory()
tf.Printer = &testPrinter{}
tf.UnstructuredClient = &fake.RESTClient{
NegotiatedSerializer: unstructuredSerializer,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
switch p, m := req.URL.Path, req.Method; {
case p == path && m == "GET":
body := ioutil.NopCloser(bytes.NewReader(curr))
return &http.Response{
StatusCode: 200,
Header: defaultHeader(),
Body: body}, nil
case p == path && m == "PATCH":
contentType := req.Header.Get("Content-Type")
if contentType != "application/merge-patch+json" {
t.Fatalf("Unexpected Content-Type: %s", contentType)
}
validatePatchApplication(t, req)
verifiedPatch = true
body := ioutil.NopCloser(bytes.NewReader(curr))
return &http.Response{
StatusCode: 200,
Header: defaultHeader(),
Body: body}, nil
default:
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
return nil, nil
}
validatePatchApplication(t, req)
verifiedPatch = true
body := ioutil.NopCloser(bytes.NewReader(curr))
return &http.Response{
StatusCode: 200,
Header: defaultHeader(),
Body: body}, nil
default:
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
return nil, nil
}
}),
}
tf.Namespace = "test"
buf := bytes.NewBuffer([]byte{})
errBuf := bytes.NewBuffer([]byte{})
}),
}
tf.OpenAPISchemaFunc = fn
tf.Namespace = "test"
buf := bytes.NewBuffer([]byte{})
errBuf := bytes.NewBuffer([]byte{})
cmd := NewCmdApply("kubectl", f, buf, errBuf)
cmd.Flags().Set("filename", filenameWidgetClientside)
cmd.Flags().Set("output", "name")
cmd.Run(cmd, []string{})
cmd := NewCmdApply("kubectl", f, buf, errBuf)
cmd.Flags().Set("filename", filenameWidgetClientside)
cmd.Flags().Set("output", "name")
cmd.Run(cmd, []string{})
expected := "widget/" + name + "\n"
if buf.String() != expected {
t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expected)
}
if !verifiedPatch {
t.Fatal("No server-side patch call detected")
expected := "widget/" + name + "\n"
if buf.String() != expected {
t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expected)
}
if errBuf.String() != "" {
t.Fatalf("unexpected error output: %s", errBuf.String())
}
if !verifiedPatch {
t.Fatal("No server-side patch call detected")
}
}
}
......@@ -890,76 +944,81 @@ func TestUnstructuredIdempotentApply(t *testing.T) {
verifiedPatch := false
f, tf, _, _ := cmdtesting.NewAPIFactory()
tf.Printer = &testPrinter{}
tf.UnstructuredClient = &fake.RESTClient{
NegotiatedSerializer: unstructuredSerializer,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
switch p, m := req.URL.Path, req.Method; {
case p == path && m == "GET":
body := ioutil.NopCloser(bytes.NewReader(serversideData))
return &http.Response{
StatusCode: 200,
Header: defaultHeader(),
Body: body}, nil
case p == path && m == "PATCH":
// In idempotent updates, kubectl sends a logically empty
// request body with the PATCH request.
// Should look like this:
// Request Body: {"metadata":{"annotations":{}}}
patch, err := ioutil.ReadAll(req.Body)
if err != nil {
t.Fatal(err)
}
contentType := req.Header.Get("Content-Type")
if contentType != "application/merge-patch+json" {
t.Fatalf("Unexpected Content-Type: %s", contentType)
}
for _, fn := range testingOpenAPISchemaFns {
f, tf, _, _ := cmdtesting.NewAPIFactory()
tf.Printer = &testPrinter{}
tf.UnstructuredClient = &fake.RESTClient{
NegotiatedSerializer: unstructuredSerializer,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
switch p, m := req.URL.Path, req.Method; {
case p == path && m == "GET":
body := ioutil.NopCloser(bytes.NewReader(serversideData))
return &http.Response{
StatusCode: 200,
Header: defaultHeader(),
Body: body}, nil
case p == path && m == "PATCH":
// In idempotent updates, kubectl sends a logically empty
// request body with the PATCH request.
// Should look like this:
// Request Body: {"metadata":{"annotations":{}}}
patch, err := ioutil.ReadAll(req.Body)
if err != nil {
t.Fatal(err)
}
patchMap := map[string]interface{}{}
if err := json.Unmarshal(patch, &patchMap); err != nil {
t.Fatal(err)
}
if len(patchMap) != 1 {
t.Fatalf("Unexpected Patch. Has more than 1 entry. path: %s", patch)
}
contentType := req.Header.Get("Content-Type")
if contentType != "application/merge-patch+json" {
t.Fatalf("Unexpected Content-Type: %s", contentType)
}
annotationsMap := walkMapPath(t, patchMap, []string{"metadata", "annotations"})
if len(annotationsMap) != 0 {
t.Fatalf("Unexpected Patch. Found unexpected annotation: %s", patch)
}
patchMap := map[string]interface{}{}
if err := json.Unmarshal(patch, &patchMap); err != nil {
t.Fatal(err)
}
if len(patchMap) != 1 {
t.Fatalf("Unexpected Patch. Has more than 1 entry. path: %s", patch)
}
verifiedPatch = true
annotationsMap := walkMapPath(t, patchMap, []string{"metadata", "annotations"})
if len(annotationsMap) != 0 {
t.Fatalf("Unexpected Patch. Found unexpected annotation: %s", patch)
}
body := ioutil.NopCloser(bytes.NewReader(serversideData))
return &http.Response{
StatusCode: 200,
Header: defaultHeader(),
Body: body}, nil
default:
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
return nil, nil
}
}),
}
verifiedPatch = true
tf.Namespace = "test"
buf := bytes.NewBuffer([]byte{})
errBuf := bytes.NewBuffer([]byte{})
body := ioutil.NopCloser(bytes.NewReader(serversideData))
return &http.Response{
StatusCode: 200,
Header: defaultHeader(),
Body: body}, nil
default:
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
return nil, nil
}
}),
}
tf.OpenAPISchemaFunc = fn
tf.Namespace = "test"
buf := bytes.NewBuffer([]byte{})
errBuf := bytes.NewBuffer([]byte{})
cmd := NewCmdApply("kubectl", f, buf, errBuf)
cmd.Flags().Set("filename", filenameWidgetClientside)
cmd.Flags().Set("output", "name")
cmd.Run(cmd, []string{})
cmd := NewCmdApply("kubectl", f, buf, errBuf)
cmd.Flags().Set("filename", filenameWidgetClientside)
cmd.Flags().Set("output", "name")
cmd.Run(cmd, []string{})
expected := "widget/widget\n"
if buf.String() != expected {
t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expected)
}
if !verifiedPatch {
t.Fatal("No server-side patch call detected")
expected := "widget/widget\n"
if buf.String() != expected {
t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expected)
}
if errBuf.String() != "" {
t.Fatalf("unexpected error output: %s", errBuf.String())
}
if !verifiedPatch {
t.Fatal("No server-side patch call detected")
}
}
}
......@@ -1093,8 +1152,6 @@ func TestForceApply(t *testing.T) {
nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC)
pathRC := "/namespaces/test/replicationcontrollers/" + nameRC
pathRCList := "/namespaces/test/replicationcontrollers"
deleted := false
counts := map[string]int{}
expected := map[string]int{
"getOk": 9,
"getNotFound": 1,
......@@ -1105,84 +1162,92 @@ func TestForceApply(t *testing.T) {
"post": 1,
}
f, tf, _, _ := cmdtesting.NewAPIFactory()
tf.Printer = &testPrinter{}
tf.UnstructuredClient = &fake.RESTClient{
NegotiatedSerializer: unstructuredSerializer,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
switch p, m := req.URL.Path, req.Method; {
case strings.HasSuffix(p, pathRC) && m == "GET":
if deleted {
counts["getNotFound"]++
return &http.Response{StatusCode: 404, Header: defaultHeader(), Body: ioutil.NopCloser(bytes.NewReader([]byte{}))}, nil
}
counts["getOk"]++
bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodyRC}, nil
case strings.HasSuffix(p, pathRCList) && m == "GET":
counts["getList"]++
rcObj := readUnstructuredFromFile(t, filenameRC)
list := &unstructured.UnstructuredList{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "ReplicationControllerList",
},
Items: []unstructured.Unstructured{*rcObj},
}
listBytes, err := runtime.Encode(testapi.Default.Codec(), list)
if err != nil {
t.Fatal(err)
}
bodyRCList := ioutil.NopCloser(bytes.NewReader(listBytes))
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodyRCList}, nil
case strings.HasSuffix(p, pathRC) && m == "PATCH":
counts["patch"]++
if counts["patch"] <= 6 {
statusErr := kubeerr.NewConflict(schema.GroupResource{Group: "", Resource: "rc"}, "test-rc", fmt.Errorf("the object has been modified. Please apply at first."))
bodyBytes, _ := json.Marshal(statusErr)
bodyErr := ioutil.NopCloser(bytes.NewReader(bodyBytes))
return &http.Response{StatusCode: http.StatusConflict, Header: defaultHeader(), Body: bodyErr}, nil
for _, fn := range testingOpenAPISchemaFns {
deleted := false
counts := map[string]int{}
f, tf, _, _ := cmdtesting.NewAPIFactory()
tf.Printer = &testPrinter{}
tf.UnstructuredClient = &fake.RESTClient{
NegotiatedSerializer: unstructuredSerializer,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
switch p, m := req.URL.Path, req.Method; {
case strings.HasSuffix(p, pathRC) && m == "GET":
if deleted {
counts["getNotFound"]++
return &http.Response{StatusCode: 404, Header: defaultHeader(), Body: ioutil.NopCloser(bytes.NewReader([]byte{}))}, nil
}
counts["getOk"]++
bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodyRC}, nil
case strings.HasSuffix(p, pathRCList) && m == "GET":
counts["getList"]++
rcObj := readUnstructuredFromFile(t, filenameRC)
list := &unstructured.UnstructuredList{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "ReplicationControllerList",
},
Items: []unstructured.Unstructured{*rcObj},
}
listBytes, err := runtime.Encode(testapi.Default.Codec(), list)
if err != nil {
t.Fatal(err)
}
bodyRCList := ioutil.NopCloser(bytes.NewReader(listBytes))
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodyRCList}, nil
case strings.HasSuffix(p, pathRC) && m == "PATCH":
counts["patch"]++
if counts["patch"] <= 6 {
statusErr := kubeerr.NewConflict(schema.GroupResource{Group: "", Resource: "rc"}, "test-rc", fmt.Errorf("the object has been modified. Please apply at first."))
bodyBytes, _ := json.Marshal(statusErr)
bodyErr := ioutil.NopCloser(bytes.NewReader(bodyBytes))
return &http.Response{StatusCode: http.StatusConflict, Header: defaultHeader(), Body: bodyErr}, nil
}
t.Fatalf("unexpected request: %#v after %v tries\n%#v", req.URL, counts["patch"], req)
return nil, nil
case strings.HasSuffix(p, pathRC) && m == "DELETE":
counts["delete"]++
deleted = true
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: ioutil.NopCloser(bytes.NewReader([]byte{}))}, nil
case strings.HasSuffix(p, pathRC) && m == "PUT":
counts["put"]++
bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodyRC}, nil
case strings.HasSuffix(p, pathRCList) && m == "POST":
counts["post"]++
deleted = false
bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodyRC}, nil
default:
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
return nil, nil
}
t.Fatalf("unexpected request: %#v after %v tries\n%#v", req.URL, counts["patch"], req)
return nil, nil
case strings.HasSuffix(p, pathRC) && m == "DELETE":
counts["delete"]++
deleted = true
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: ioutil.NopCloser(bytes.NewReader([]byte{}))}, nil
case strings.HasSuffix(p, pathRC) && m == "PUT":
counts["put"]++
bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodyRC}, nil
case strings.HasSuffix(p, pathRCList) && m == "POST":
counts["post"]++
deleted = false
bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodyRC}, nil
default:
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
return nil, nil
}
}),
}
tf.Client = tf.UnstructuredClient
tf.ClientConfig = defaultClientConfig()
tf.Namespace = "test"
buf := bytes.NewBuffer([]byte{})
errBuf := bytes.NewBuffer([]byte{})
}),
}
tf.OpenAPISchemaFunc = fn
tf.Client = tf.UnstructuredClient
tf.ClientConfig = &restclient.Config{}
tf.Namespace = "test"
buf := bytes.NewBuffer([]byte{})
errBuf := bytes.NewBuffer([]byte{})
cmd := NewCmdApply("kubectl", f, buf, errBuf)
cmd.Flags().Set("filename", filenameRC)
cmd.Flags().Set("output", "name")
cmd.Flags().Set("force", "true")
cmd.Run(cmd, []string{})
cmd := NewCmdApply("kubectl", f, buf, errBuf)
cmd.Flags().Set("filename", filenameRC)
cmd.Flags().Set("output", "name")
cmd.Flags().Set("force", "true")
cmd.Run(cmd, []string{})
for method, exp := range expected {
if exp != counts[method] {
t.Errorf("Unexpected amount of %q API calls, wanted %v got %v", method, exp, counts[method])
for method, exp := range expected {
if exp != counts[method] {
t.Errorf("Unexpected amount of %q API calls, wanted %v got %v", method, exp, counts[method])
}
}
}
if expected := "replicationcontroller/" + nameRC + "\n"; buf.String() != expected {
t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expected)
if expected := "replicationcontroller/" + nameRC + "\n"; buf.String() != expected {
t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expected)
}
if errBuf.String() != "" {
t.Fatalf("unexpected error output: %s", errBuf.String())
}
}
}
......@@ -1643,6 +1643,10 @@
"Rev": "39a7bf85c140f972372c2a0d1ee40adbf0c8bfe1"
},
{
"ImportPath": "k8s.io/kube-openapi/pkg/util/proto",
"Rev": "39a7bf85c140f972372c2a0d1ee40adbf0c8bfe1"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/api/equality",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
......
......@@ -83,10 +83,38 @@
"Rev": "1643683e1b54a9e88ad26d98f81400c8c9d9f4f9"
},
{
"ImportPath": "github.com/golang/protobuf/ptypes",
"Rev": "1643683e1b54a9e88ad26d98f81400c8c9d9f4f9"
},
{
"ImportPath": "github.com/golang/protobuf/ptypes/any",
"Rev": "1643683e1b54a9e88ad26d98f81400c8c9d9f4f9"
},
{
"ImportPath": "github.com/golang/protobuf/ptypes/duration",
"Rev": "1643683e1b54a9e88ad26d98f81400c8c9d9f4f9"
},
{
"ImportPath": "github.com/golang/protobuf/ptypes/timestamp",
"Rev": "1643683e1b54a9e88ad26d98f81400c8c9d9f4f9"
},
{
"ImportPath": "github.com/google/gofuzz",
"Rev": "44d81051d367757e1c7c6a5a86423ece9afcf63c"
},
{
"ImportPath": "github.com/googleapis/gnostic/OpenAPIv2",
"Rev": "0c5108395e2debce0d731cf0287ddf7242066aba"
},
{
"ImportPath": "github.com/googleapis/gnostic/compiler",
"Rev": "0c5108395e2debce0d731cf0287ddf7242066aba"
},
{
"ImportPath": "github.com/googleapis/gnostic/extensions",
"Rev": "0c5108395e2debce0d731cf0287ddf7242066aba"
},
{
"ImportPath": "github.com/hashicorp/golang-lru",
"Rev": "a0d98a5f288019575c6d1f4bb1573fef2d1fcdc4"
},
......@@ -217,6 +245,10 @@
{
"ImportPath": "k8s.io/kube-openapi/pkg/common",
"Rev": "39a7bf85c140f972372c2a0d1ee40adbf0c8bfe1"
},
{
"ImportPath": "k8s.io/kube-openapi/pkg/util/proto",
"Rev": "39a7bf85c140f972372c2a0d1ee40adbf0c8bfe1"
}
]
}
......@@ -9,26 +9,38 @@ load(
go_test(
name = "go_default_test",
srcs = ["patch_test.go"],
data = [
"testdata/swagger-merge-item.json",
"testdata/swagger-precision-item.json",
],
importpath = "k8s.io/apimachinery/pkg/util/strategicpatch",
library = ":go_default_library",
deps = [
"//vendor/github.com/davecgh/go-spew/spew:go_default_library",
"//vendor/github.com/ghodss/yaml:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/json:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/mergepatch:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/strategicpatch/testing:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = ["patch.go"],
srcs = [
"errors.go",
"meta.go",
"patch.go",
"types.go",
],
importpath = "k8s.io/apimachinery/pkg/util/strategicpatch",
deps = [
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/json:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/mergepatch:go_default_library",
"//vendor/k8s.io/apimachinery/third_party/forked/golang/json:go_default_library",
"//vendor/k8s.io/kube-openapi/pkg/util/proto:go_default_library",
],
)
......@@ -41,6 +53,9 @@ filegroup(
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
srcs = [
":package-srcs",
"//staging/src/k8s.io/apimachinery/pkg/util/strategicpatch/testing:all-srcs",
],
tags = ["automanaged"],
)
/*
Copyright 2017 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 strategicpatch
import (
"fmt"
)
type LookupPatchMetaError struct {
Path string
Err error
}
func (e LookupPatchMetaError) Error() string {
return fmt.Sprintf("LookupPatchMetaError(%s): %v", e.Path, e.Err)
}
type FieldNotFoundError struct {
Path string
Field string
}
func (e FieldNotFoundError) Error() string {
return fmt.Sprintf("unable to find api field %q in %s", e.Field, e.Path)
}
type InvalidTypeError struct {
Path string
Expected string
Actual string
}
func (e InvalidTypeError) Error() string {
return fmt.Sprintf("invalid type for %s: got %q, expected %q", e.Path, e.Actual, e.Expected)
}
/*
Copyright 2017 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 strategicpatch
import (
"errors"
"fmt"
"reflect"
"k8s.io/apimachinery/pkg/util/mergepatch"
forkedjson "k8s.io/apimachinery/third_party/forked/golang/json"
openapi "k8s.io/kube-openapi/pkg/util/proto"
)
type PatchMeta struct {
patchStrategies []string
patchMergeKey string
}
func (pm PatchMeta) GetPatchStrategies() []string {
if pm.patchStrategies == nil {
return []string{}
}
return pm.patchStrategies
}
func (pm PatchMeta) SetPatchStrategies(ps []string) {
pm.patchStrategies = ps
}
func (pm PatchMeta) GetPatchMergeKey() string {
return pm.patchMergeKey
}
func (pm PatchMeta) SetPatchMergeKey(pmk string) {
pm.patchMergeKey = pmk
}
type LookupPatchMeta interface {
// LookupPatchMetadataForStruct gets subschema and the patch metadata (e.g. patch strategy and merge key) for map.
LookupPatchMetadataForStruct(key string) (LookupPatchMeta, PatchMeta, error)
// LookupPatchMetadataForSlice get subschema and the patch metadata for slice.
LookupPatchMetadataForSlice(key string) (LookupPatchMeta, PatchMeta, error)
// Get the type name of the field
Name() string
}
type PatchMetaFromStruct struct {
T reflect.Type
}
func NewPatchMetaFromStruct(dataStruct interface{}) (PatchMetaFromStruct, error) {
t, err := getTagStructType(dataStruct)
return PatchMetaFromStruct{T: t}, err
}
var _ LookupPatchMeta = PatchMetaFromStruct{}
func (s PatchMetaFromStruct) LookupPatchMetadataForStruct(key string) (LookupPatchMeta, PatchMeta, error) {
fieldType, fieldPatchStrategies, fieldPatchMergeKey, err := forkedjson.LookupPatchMetadataForStruct(s.T, key)
if err != nil {
return nil, PatchMeta{}, err
}
return PatchMetaFromStruct{T: fieldType},
PatchMeta{
patchStrategies: fieldPatchStrategies,
patchMergeKey: fieldPatchMergeKey,
}, nil
}
func (s PatchMetaFromStruct) LookupPatchMetadataForSlice(key string) (LookupPatchMeta, PatchMeta, error) {
subschema, patchMeta, err := s.LookupPatchMetadataForStruct(key)
if err != nil {
return nil, PatchMeta{}, err
}
elemPatchMetaFromStruct := subschema.(PatchMetaFromStruct)
t := elemPatchMetaFromStruct.T
var elemType reflect.Type
switch t.Kind() {
// If t is an array or a slice, get the element type.
// If element is still an array or a slice, return an error.
// Otherwise, return element type.
case reflect.Array, reflect.Slice:
elemType = t.Elem()
if elemType.Kind() == reflect.Array || elemType.Kind() == reflect.Slice {
return nil, PatchMeta{}, errors.New("unexpected slice of slice")
}
// If t is an pointer, get the underlying element.
// If the underlying element is neither an array nor a slice, the pointer is pointing to a slice,
// e.g. https://github.com/kubernetes/kubernetes/blob/bc22e206c79282487ea0bf5696d5ccec7e839a76/staging/src/k8s.io/apimachinery/pkg/util/strategicpatch/patch_test.go#L2782-L2822
// If the underlying element is either an array or a slice, return its element type.
case reflect.Ptr:
t = t.Elem()
if t.Kind() == reflect.Array || t.Kind() == reflect.Slice {
t = t.Elem()
}
elemType = t
default:
return nil, PatchMeta{}, fmt.Errorf("expected slice or array type, but got: %s", s.T.Kind().String())
}
return PatchMetaFromStruct{T: elemType}, patchMeta, nil
}
func (s PatchMetaFromStruct) Name() string {
return s.T.Kind().String()
}
func getTagStructType(dataStruct interface{}) (reflect.Type, error) {
if dataStruct == nil {
return nil, mergepatch.ErrBadArgKind(struct{}{}, nil)
}
t := reflect.TypeOf(dataStruct)
// Get the underlying type for pointers
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
if t.Kind() != reflect.Struct {
return nil, mergepatch.ErrBadArgKind(struct{}{}, dataStruct)
}
return t, nil
}
func GetTagStructTypeOrDie(dataStruct interface{}) reflect.Type {
t, err := getTagStructType(dataStruct)
if err != nil {
panic(err)
}
return t
}
type PatchMetaFromOpenAPI struct {
Schema openapi.Schema
}
func NewPatchMetaFromOpenAPI(s openapi.Schema) PatchMetaFromOpenAPI {
return PatchMetaFromOpenAPI{Schema: s}
}
var _ LookupPatchMeta = PatchMetaFromOpenAPI{}
func (s PatchMetaFromOpenAPI) LookupPatchMetadataForStruct(key string) (LookupPatchMeta, PatchMeta, error) {
if s.Schema == nil {
return nil, PatchMeta{}, nil
}
kindItem := NewKindItem(key, s.Schema.GetPath())
s.Schema.Accept(kindItem)
err := kindItem.Error()
if err != nil {
return nil, PatchMeta{}, err
}
return PatchMetaFromOpenAPI{Schema: kindItem.subschema},
kindItem.patchmeta, nil
}
func (s PatchMetaFromOpenAPI) LookupPatchMetadataForSlice(key string) (LookupPatchMeta, PatchMeta, error) {
if s.Schema == nil {
return nil, PatchMeta{}, nil
}
sliceItem := NewSliceItem(key, s.Schema.GetPath())
s.Schema.Accept(sliceItem)
err := sliceItem.Error()
if err != nil {
return nil, PatchMeta{}, err
}
return PatchMetaFromOpenAPI{Schema: sliceItem.subschema},
sliceItem.patchmeta, nil
}
func (s PatchMetaFromOpenAPI) Name() string {
schema := s.Schema
return schema.GetName()
}
......@@ -25,7 +25,6 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/util/json"
"k8s.io/apimachinery/pkg/util/mergepatch"
forkedjson "k8s.io/apimachinery/third_party/forked/golang/json"
)
// An alternate implementation of JSON Merge Patch
......@@ -93,6 +92,16 @@ type MergeOptions struct {
// return a patch that yields the modified document when applied to the original document, or an error
// if either of the two documents is invalid.
func CreateTwoWayMergePatch(original, modified []byte, dataStruct interface{}, fns ...mergepatch.PreconditionFunc) ([]byte, error) {
schema, err := NewPatchMetaFromStruct(dataStruct)
if err != nil {
return nil, err
}
return CreateTwoWayMergePatchUsingLookupPatchMeta(original, modified, schema, fns...)
}
func CreateTwoWayMergePatchUsingLookupPatchMeta(
original, modified []byte, schema LookupPatchMeta, fns ...mergepatch.PreconditionFunc) ([]byte, error) {
originalMap := map[string]interface{}{}
if len(original) > 0 {
if err := json.Unmarshal(original, &originalMap); err != nil {
......@@ -107,7 +116,7 @@ func CreateTwoWayMergePatch(original, modified []byte, dataStruct interface{}, f
}
}
patchMap, err := CreateTwoWayMergeMapPatch(originalMap, modifiedMap, dataStruct, fns...)
patchMap, err := CreateTwoWayMergeMapPatchUsingLookupPatchMeta(originalMap, modifiedMap, schema, fns...)
if err != nil {
return nil, err
}
......@@ -119,15 +128,19 @@ func CreateTwoWayMergePatch(original, modified []byte, dataStruct interface{}, f
// encoded JSONMap.
// The serialized version of the map can then be passed to StrategicMergeMapPatch.
func CreateTwoWayMergeMapPatch(original, modified JSONMap, dataStruct interface{}, fns ...mergepatch.PreconditionFunc) (JSONMap, error) {
t, err := getTagStructType(dataStruct)
schema, err := NewPatchMetaFromStruct(dataStruct)
if err != nil {
return nil, err
}
return CreateTwoWayMergeMapPatchUsingLookupPatchMeta(original, modified, schema, fns...)
}
func CreateTwoWayMergeMapPatchUsingLookupPatchMeta(original, modified JSONMap, schema LookupPatchMeta, fns ...mergepatch.PreconditionFunc) (JSONMap, error) {
diffOptions := DiffOptions{
SetElementOrder: true,
}
patchMap, err := diffMaps(original, modified, t, diffOptions)
patchMap, err := diffMaps(original, modified, schema, diffOptions)
if err != nil {
return nil, err
}
......@@ -152,12 +165,9 @@ func CreateTwoWayMergeMapPatch(original, modified JSONMap, dataStruct interface{
// - IFF list of primitives && merge strategy - use parallel deletion list
// - IFF list of maps or primitives with replace strategy (default) - set patch value to the value in modified
// - Build $retainKeys directive for fields with retainKeys patch strategy
func diffMaps(original, modified map[string]interface{}, t reflect.Type, diffOptions DiffOptions) (map[string]interface{}, error) {
func diffMaps(original, modified map[string]interface{}, schema LookupPatchMeta, diffOptions DiffOptions) (map[string]interface{}, error) {
patch := map[string]interface{}{}
// Get the underlying type for pointers
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
// This will be used to build the $retainKeys directive sent in the patch
retainKeysList := make([]interface{}, 0, len(modified))
......@@ -199,10 +209,10 @@ func diffMaps(original, modified map[string]interface{}, t reflect.Type, diffOpt
switch originalValueTyped := originalValue.(type) {
case map[string]interface{}:
modifiedValueTyped := modifiedValue.(map[string]interface{})
err = handleMapDiff(key, originalValueTyped, modifiedValueTyped, patch, t, diffOptions)
err = handleMapDiff(key, originalValueTyped, modifiedValueTyped, patch, schema, diffOptions)
case []interface{}:
modifiedValueTyped := modifiedValue.([]interface{})
err = handleSliceDiff(key, originalValueTyped, modifiedValueTyped, patch, t, diffOptions)
err = handleSliceDiff(key, originalValueTyped, modifiedValueTyped, patch, schema, diffOptions)
default:
replacePatchFieldIfNotEqual(key, originalValue, modifiedValue, patch, diffOptions)
}
......@@ -249,8 +259,9 @@ func handleDirectiveMarker(key string, originalValue, modifiedValue interface{},
// patch is the patch map that contains key and the updated value, and it is the parent of originalValue, modifiedValue
// diffOptions contains multiple options to control how we do the diff.
func handleMapDiff(key string, originalValue, modifiedValue, patch map[string]interface{},
t reflect.Type, diffOptions DiffOptions) error {
fieldType, fieldPatchStrategies, _, err := forkedjson.LookupPatchMetadata(t, key)
schema LookupPatchMeta, diffOptions DiffOptions) error {
subschema, patchMeta, err := schema.LookupPatchMetadataForStruct(key)
if err != nil {
// We couldn't look up metadata for the field
// If the values are identical, this doesn't matter, no patch is needed
......@@ -260,7 +271,7 @@ func handleMapDiff(key string, originalValue, modifiedValue, patch map[string]in
// Otherwise, return the error
return err
}
retainKeys, patchStrategy, err := extractRetainKeysPatchStrategy(fieldPatchStrategies)
retainKeys, patchStrategy, err := extractRetainKeysPatchStrategy(patchMeta.GetPatchStrategies())
if err != nil {
return err
}
......@@ -272,7 +283,7 @@ func handleMapDiff(key string, originalValue, modifiedValue, patch map[string]in
patch[key] = modifiedValue
}
default:
patchValue, err := diffMaps(originalValue, modifiedValue, fieldType, diffOptions)
patchValue, err := diffMaps(originalValue, modifiedValue, subschema, diffOptions)
if err != nil {
return err
}
......@@ -291,8 +302,8 @@ func handleMapDiff(key string, originalValue, modifiedValue, patch map[string]in
// patch is the patch map that contains key and the updated value, and it is the parent of originalValue, modifiedValue
// diffOptions contains multiple options to control how we do the diff.
func handleSliceDiff(key string, originalValue, modifiedValue []interface{}, patch map[string]interface{},
t reflect.Type, diffOptions DiffOptions) error {
fieldType, fieldPatchStrategies, fieldPatchMergeKey, err := forkedjson.LookupPatchMetadata(t, key)
schema LookupPatchMeta, diffOptions DiffOptions) error {
subschema, patchMeta, err := schema.LookupPatchMetadataForSlice(key)
if err != nil {
// We couldn't look up metadata for the field
// If the values are identical, this doesn't matter, no patch is needed
......@@ -302,7 +313,7 @@ func handleSliceDiff(key string, originalValue, modifiedValue []interface{}, pat
// Otherwise, return the error
return err
}
retainKeys, patchStrategy, err := extractRetainKeysPatchStrategy(fieldPatchStrategies)
retainKeys, patchStrategy, err := extractRetainKeysPatchStrategy(patchMeta.GetPatchStrategies())
if err != nil {
return err
}
......@@ -310,7 +321,7 @@ func handleSliceDiff(key string, originalValue, modifiedValue []interface{}, pat
// Merge the 2 slices using mergePatchKey
case mergeDirective:
diffOptions.BuildRetainKeysDirective = retainKeys
addList, deletionList, setOrderList, err := diffLists(originalValue, modifiedValue, fieldType.Elem(), fieldPatchMergeKey, diffOptions)
addList, deletionList, setOrderList, err := diffLists(originalValue, modifiedValue, subschema, patchMeta.GetPatchMergeKey(), diffOptions)
if err != nil {
return err
}
......@@ -537,7 +548,7 @@ func normalizeSliceOrder(toSort, order []interface{}, mergeKey string, kind refl
// another list to set the order of the list
// Only list of primitives with merge strategy will generate a parallel deletion list.
// These two lists should yield modified when applied to original, for lists with merge semantics.
func diffLists(original, modified []interface{}, t reflect.Type, mergeKey string, diffOptions DiffOptions) ([]interface{}, []interface{}, []interface{}, error) {
func diffLists(original, modified []interface{}, schema LookupPatchMeta, mergeKey string, diffOptions DiffOptions) ([]interface{}, []interface{}, []interface{}, error) {
if len(original) == 0 {
// Both slices are empty - do nothing
if len(modified) == 0 || diffOptions.IgnoreChangesAndAdditions {
......@@ -557,7 +568,7 @@ func diffLists(original, modified []interface{}, t reflect.Type, mergeKey string
kind := elementType.Kind()
switch kind {
case reflect.Map:
patchList, deleteList, err = diffListsOfMaps(original, modified, t, mergeKey, diffOptions)
patchList, deleteList, err = diffListsOfMaps(original, modified, schema, mergeKey, diffOptions)
if err != nil {
return nil, nil, nil, err
}
......@@ -703,15 +714,15 @@ func compareListValuesAtIndex(list1Inbounds, list2Inbounds bool, list1Value, lis
// diffListsOfMaps takes a pair of lists and
// returns a (recursive) strategic merge patch list contains additions and changes and
// a deletion list contains deletions
func diffListsOfMaps(original, modified []interface{}, t reflect.Type, mergeKey string, diffOptions DiffOptions) ([]interface{}, []interface{}, error) {
func diffListsOfMaps(original, modified []interface{}, schema LookupPatchMeta, mergeKey string, diffOptions DiffOptions) ([]interface{}, []interface{}, error) {
patch := make([]interface{}, 0, len(modified))
deletionList := make([]interface{}, 0, len(original))
originalSorted, err := sortMergeListsByNameArray(original, t, mergeKey, false)
originalSorted, err := sortMergeListsByNameArray(original, schema, mergeKey, false)
if err != nil {
return nil, nil, err
}
modifiedSorted, err := sortMergeListsByNameArray(modified, t, mergeKey, false)
modifiedSorted, err := sortMergeListsByNameArray(modified, schema, mergeKey, false)
if err != nil {
return nil, nil, err
}
......@@ -746,7 +757,7 @@ func diffListsOfMaps(original, modified []interface{}, t reflect.Type, mergeKey
switch {
case bothInBounds && ItemMatchesOriginalAndModifiedSlice(originalElementMergeKeyValueString, modifiedElementMergeKeyValueString):
// Merge key values are equal, so recurse
patchValue, err := diffMaps(originalElement, modifiedElement, t, diffOptions)
patchValue, err := diffMaps(originalElement, modifiedElement, schema, diffOptions)
if err != nil {
return nil, nil, err
}
......@@ -799,6 +810,15 @@ func getMapAndMergeKeyValueByIndex(index int, mergeKey string, listOfMaps []inte
// must be json encoded content. A patch can be created from an original and a modified document
// by calling CreateStrategicMergePatch.
func StrategicMergePatch(original, patch []byte, dataStruct interface{}) ([]byte, error) {
schema, err := NewPatchMetaFromStruct(dataStruct)
if err != nil {
return nil, err
}
return StrategicMergePatchUsingLookupPatchMeta(original, patch, schema)
}
func StrategicMergePatchUsingLookupPatchMeta(original, patch []byte, schema LookupPatchMeta) ([]byte, error) {
originalMap, err := handleUnmarshal(original)
if err != nil {
return nil, err
......@@ -808,7 +828,7 @@ func StrategicMergePatch(original, patch []byte, dataStruct interface{}) ([]byte
return nil, err
}
result, err := StrategicMergeMapPatch(originalMap, patchMap, dataStruct)
result, err := StrategicMergeMapPatchUsingLookupPatchMeta(originalMap, patchMap, schema)
if err != nil {
return nil, err
}
......@@ -834,7 +854,7 @@ func handleUnmarshal(j []byte) (map[string]interface{}, error) {
// calling CreateTwoWayMergeMapPatch.
// Warning: the original and patch JSONMap objects are mutated by this function and should not be reused.
func StrategicMergeMapPatch(original, patch JSONMap, dataStruct interface{}) (JSONMap, error) {
t, err := getTagStructType(dataStruct)
schema, err := NewPatchMetaFromStruct(dataStruct)
if err != nil {
return nil, err
}
......@@ -849,29 +869,15 @@ func StrategicMergeMapPatch(original, patch JSONMap, dataStruct interface{}) (JS
return nil, mergepatch.ErrUnsupportedStrategicMergePatchFormat
}
return StrategicMergeMapPatchUsingLookupPatchMeta(original, patch, schema)
}
func StrategicMergeMapPatchUsingLookupPatchMeta(original, patch JSONMap, schema LookupPatchMeta) (JSONMap, error) {
mergeOptions := MergeOptions{
MergeParallelList: true,
IgnoreUnmatchedNulls: true,
}
return mergeMap(original, patch, t, mergeOptions)
}
func getTagStructType(dataStruct interface{}) (reflect.Type, error) {
if dataStruct == nil {
return nil, mergepatch.ErrBadArgKind(struct{}{}, nil)
}
t := reflect.TypeOf(dataStruct)
// Get the underlying type for pointers
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
if t.Kind() != reflect.Struct {
return nil, mergepatch.ErrBadArgKind(struct{}{}, dataStruct)
}
return t, nil
return mergeMap(original, patch, schema, mergeOptions)
}
// handleDirectiveInMergeMap handles the patch directive when merging 2 maps.
......@@ -1079,7 +1085,7 @@ func applyRetainKeysDirective(original, patch map[string]interface{}, options Me
// The precedence is $setElementOrder > order in patch list > order in live list.
// This function will delete the item after merging it to prevent process it again in the future.
// Ref: https://git.k8s.io/community/contributors/design-proposals/cli/preserve-order-in-strategic-merge-patch.md
func mergePatchIntoOriginal(original, patch map[string]interface{}, t reflect.Type, mergeOptions MergeOptions) error {
func mergePatchIntoOriginal(original, patch map[string]interface{}, schema LookupPatchMeta, mergeOptions MergeOptions) error {
for key, patchV := range patch {
// Do nothing if there is no ordering directive
if !strings.HasPrefix(key, setElementOrderDirectivePrefix) {
......@@ -1106,9 +1112,9 @@ func mergePatchIntoOriginal(original, patch map[string]interface{}, t reflect.Ty
var (
ok bool
originalFieldValue, patchFieldValue, merged []interface{}
patchStrategy, mergeKey string
patchStrategies []string
fieldType reflect.Type
patchStrategy string
patchMeta PatchMeta
subschema LookupPatchMeta
)
typedSetElementOrderList, ok := setElementOrderInPatch.([]interface{})
if !ok {
......@@ -1134,16 +1140,16 @@ func mergePatchIntoOriginal(original, patch map[string]interface{}, t reflect.Ty
return mergepatch.ErrBadArgType(patchFieldValue, patchList)
}
}
fieldType, patchStrategies, mergeKey, err = forkedjson.LookupPatchMetadata(t, originalKey)
subschema, patchMeta, err = schema.LookupPatchMetadataForSlice(originalKey)
if err != nil {
return err
}
_, patchStrategy, err = extractRetainKeysPatchStrategy(patchStrategies)
_, patchStrategy, err = extractRetainKeysPatchStrategy(patchMeta.GetPatchStrategies())
if err != nil {
return err
}
// Check for consistency between the element order list and the field it applies to
err = validatePatchWithSetOrderList(patchFieldValue, typedSetElementOrderList, mergeKey)
err = validatePatchWithSetOrderList(patchFieldValue, typedSetElementOrderList, patchMeta.GetPatchMergeKey())
if err != nil {
return err
}
......@@ -1156,8 +1162,8 @@ func mergePatchIntoOriginal(original, patch map[string]interface{}, t reflect.Ty
// list was added
merged = patchFieldValue
case foundOriginal && foundPatch:
merged, err = mergeSliceHandler(originalList, patchList, fieldType,
patchStrategy, mergeKey, false, mergeOptions)
merged, err = mergeSliceHandler(originalList, patchList, subschema,
patchStrategy, patchMeta.GetPatchMergeKey(), false, mergeOptions)
if err != nil {
return err
}
......@@ -1167,13 +1173,13 @@ func mergePatchIntoOriginal(original, patch map[string]interface{}, t reflect.Ty
// Split all items into patch items and server-only items and then enforce the order.
var patchItems, serverOnlyItems []interface{}
if len(mergeKey) == 0 {
if len(patchMeta.GetPatchMergeKey()) == 0 {
// Primitives doesn't need merge key to do partitioning.
patchItems, serverOnlyItems = partitionPrimitivesByPresentInList(merged, typedSetElementOrderList)
} else {
// Maps need merge key to do partitioning.
patchItems, serverOnlyItems, err = partitionMapsByPresentInList(merged, typedSetElementOrderList, mergeKey)
patchItems, serverOnlyItems, err = partitionMapsByPresentInList(merged, typedSetElementOrderList, patchMeta.GetPatchMergeKey())
if err != nil {
return err
}
......@@ -1187,7 +1193,7 @@ func mergePatchIntoOriginal(original, patch map[string]interface{}, t reflect.Ty
// normalize merged list
// typedSetElementOrderList contains all the relative order in typedPatchList,
// so don't need to use typedPatchList
both, err := normalizeElementOrder(patchItems, serverOnlyItems, typedSetElementOrderList, originalFieldValue, mergeKey, kind)
both, err := normalizeElementOrder(patchItems, serverOnlyItems, typedSetElementOrderList, originalFieldValue, patchMeta.GetPatchMergeKey(), kind)
if err != nil {
return err
}
......@@ -1249,7 +1255,7 @@ func partitionMapsByPresentInList(original, partitionBy []interface{}, mergeKey
// If patch contains any null field (e.g. field_1: null) that is not
// present in original, then to propagate it to the end result use
// mergeOptions.IgnoreUnmatchedNulls == false.
func mergeMap(original, patch map[string]interface{}, t reflect.Type, mergeOptions MergeOptions) (map[string]interface{}, error) {
func mergeMap(original, patch map[string]interface{}, schema LookupPatchMeta, mergeOptions MergeOptions) (map[string]interface{}, error) {
if v, ok := patch[directiveMarker]; ok {
return handleDirectiveInMergeMap(v, patch)
}
......@@ -1269,7 +1275,7 @@ func mergeMap(original, patch map[string]interface{}, t reflect.Type, mergeOptio
// When not merging the directive, it will make sure $setElementOrder list exist only in original.
// When merging the directive, it will process $setElementOrder and its patch list together.
// This function will delete the merged elements from patch so they will not be reprocessed
err = mergePatchIntoOriginal(original, patch, t, mergeOptions)
err = mergePatchIntoOriginal(original, patch, schema, mergeOptions)
if err != nil {
return nil, err
}
......@@ -1307,11 +1313,6 @@ func mergeMap(original, patch map[string]interface{}, t reflect.Type, mergeOptio
continue
}
// If the data type is a pointer, resolve the element.
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
originalType := reflect.TypeOf(original[k])
patchType := reflect.TypeOf(patchV)
if originalType != patchType {
......@@ -1319,22 +1320,27 @@ func mergeMap(original, patch map[string]interface{}, t reflect.Type, mergeOptio
continue
}
// If they're both maps or lists, recurse into the value.
// First find the fieldPatchStrategy and fieldPatchMergeKey.
fieldType, fieldPatchStrategies, fieldPatchMergeKey, err := forkedjson.LookupPatchMetadata(t, k)
if err != nil {
return nil, err
}
_, patchStrategy, err := extractRetainKeysPatchStrategy(fieldPatchStrategies)
if err != nil {
return nil, err
}
switch originalType.Kind() {
case reflect.Map:
original[k], err = mergeMapHandler(original[k], patchV, fieldType, patchStrategy, mergeOptions)
subschema, patchMeta, err := schema.LookupPatchMetadataForStruct(k)
if err != nil {
return nil, err
}
_, patchStrategy, err := extractRetainKeysPatchStrategy(patchMeta.GetPatchStrategies())
if err != nil {
return nil, err
}
original[k], err = mergeMapHandler(original[k], patchV, subschema, patchStrategy, mergeOptions)
case reflect.Slice:
original[k], err = mergeSliceHandler(original[k], patchV, fieldType, patchStrategy, fieldPatchMergeKey, isDeleteList, mergeOptions)
subschema, patchMeta, err := schema.LookupPatchMetadataForSlice(k)
if err != nil {
return nil, err
}
_, patchStrategy, err := extractRetainKeysPatchStrategy(patchMeta.GetPatchStrategies())
if err != nil {
return nil, err
}
original[k], err = mergeSliceHandler(original[k], patchV, subschema, patchStrategy, patchMeta.GetPatchMergeKey(), isDeleteList, mergeOptions)
default:
original[k] = patchV
}
......@@ -1347,7 +1353,7 @@ func mergeMap(original, patch map[string]interface{}, t reflect.Type, mergeOptio
// mergeMapHandler handles how to merge `patchV` whose key is `key` with `original` respecting
// fieldPatchStrategy and mergeOptions.
func mergeMapHandler(original, patch interface{}, fieldType reflect.Type,
func mergeMapHandler(original, patch interface{}, schema LookupPatchMeta,
fieldPatchStrategy string, mergeOptions MergeOptions) (map[string]interface{}, error) {
typedOriginal, typedPatch, err := mapTypeAssertion(original, patch)
if err != nil {
......@@ -1355,7 +1361,7 @@ func mergeMapHandler(original, patch interface{}, fieldType reflect.Type,
}
if fieldPatchStrategy != replaceDirective {
return mergeMap(typedOriginal, typedPatch, fieldType, mergeOptions)
return mergeMap(typedOriginal, typedPatch, schema, mergeOptions)
} else {
return typedPatch, nil
}
......@@ -1363,7 +1369,7 @@ func mergeMapHandler(original, patch interface{}, fieldType reflect.Type,
// mergeSliceHandler handles how to merge `patchV` whose key is `key` with `original` respecting
// fieldPatchStrategy, fieldPatchMergeKey, isDeleteList and mergeOptions.
func mergeSliceHandler(original, patch interface{}, fieldType reflect.Type,
func mergeSliceHandler(original, patch interface{}, schema LookupPatchMeta,
fieldPatchStrategy, fieldPatchMergeKey string, isDeleteList bool, mergeOptions MergeOptions) ([]interface{}, error) {
typedOriginal, typedPatch, err := sliceTypeAssertion(original, patch)
if err != nil {
......@@ -1371,8 +1377,7 @@ func mergeSliceHandler(original, patch interface{}, fieldType reflect.Type,
}
if fieldPatchStrategy == mergeDirective {
elemType := fieldType.Elem()
return mergeSlice(typedOriginal, typedPatch, elemType, fieldPatchMergeKey, mergeOptions, isDeleteList)
return mergeSlice(typedOriginal, typedPatch, schema, fieldPatchMergeKey, mergeOptions, isDeleteList)
} else {
return typedPatch, nil
}
......@@ -1381,7 +1386,7 @@ func mergeSliceHandler(original, patch interface{}, fieldType reflect.Type,
// Merge two slices together. Note: This may modify both the original slice and
// the patch because getting a deep copy of a slice in golang is highly
// non-trivial.
func mergeSlice(original, patch []interface{}, elemType reflect.Type, mergeKey string, mergeOptions MergeOptions, isDeleteList bool) ([]interface{}, error) {
func mergeSlice(original, patch []interface{}, schema LookupPatchMeta, mergeKey string, mergeOptions MergeOptions, isDeleteList bool) ([]interface{}, error) {
if len(original) == 0 && len(patch) == 0 {
return original, nil
}
......@@ -1406,7 +1411,7 @@ func mergeSlice(original, patch []interface{}, elemType reflect.Type, mergeKey s
} else {
if mergeKey == "" {
return nil, fmt.Errorf("cannot merge lists without merge key for type %s", elemType.Kind().String())
return nil, fmt.Errorf("cannot merge lists without merge key for %s", schema.Name())
}
original, patch, err = mergeSliceWithSpecialElements(original, patch, mergeKey)
......@@ -1414,7 +1419,7 @@ func mergeSlice(original, patch []interface{}, elemType reflect.Type, mergeKey s
return nil, err
}
merged, err = mergeSliceWithoutSpecialElements(original, patch, mergeKey, elemType, mergeOptions)
merged, err = mergeSliceWithoutSpecialElements(original, patch, mergeKey, schema, mergeOptions)
if err != nil {
return nil, err
}
......@@ -1492,7 +1497,7 @@ func deleteMatchingEntries(original []interface{}, mergeKey string, mergeValue i
// mergeSliceWithoutSpecialElements merges slices with non-special elements.
// original and patch must be slices of maps, they should be checked before calling this function.
func mergeSliceWithoutSpecialElements(original, patch []interface{}, mergeKey string, elemType reflect.Type, mergeOptions MergeOptions) ([]interface{}, error) {
func mergeSliceWithoutSpecialElements(original, patch []interface{}, mergeKey string, schema LookupPatchMeta, mergeOptions MergeOptions) ([]interface{}, error) {
for _, v := range patch {
typedV := v.(map[string]interface{})
mergeValue, ok := typedV[mergeKey]
......@@ -1511,7 +1516,7 @@ func mergeSliceWithoutSpecialElements(original, patch []interface{}, mergeKey st
var mergedMaps interface{}
var err error
// Merge into original.
mergedMaps, err = mergeMap(originalMap, typedV, elemType, mergeOptions)
mergedMaps, err = mergeMap(originalMap, typedV, schema, mergeOptions)
if err != nil {
return nil, err
}
......@@ -1560,14 +1565,14 @@ func findMapInSliceBasedOnKeyValue(m []interface{}, key string, value interface{
// by key. This is needed by tests because in JSON, list order is significant,
// but in Strategic Merge Patch, merge lists do not have significant order.
// Sorting the lists allows for order-insensitive comparison of patched maps.
func sortMergeListsByName(mapJSON []byte, dataStruct interface{}) ([]byte, error) {
func sortMergeListsByName(mapJSON []byte, schema LookupPatchMeta) ([]byte, error) {
var m map[string]interface{}
err := json.Unmarshal(mapJSON, &m)
if err != nil {
return nil, mergepatch.ErrBadJSONDoc
}
newM, err := sortMergeListsByNameMap(m, reflect.TypeOf(dataStruct))
newM, err := sortMergeListsByNameMap(m, schema)
if err != nil {
return nil, err
}
......@@ -1576,7 +1581,7 @@ func sortMergeListsByName(mapJSON []byte, dataStruct interface{}) ([]byte, error
}
// Function sortMergeListsByNameMap recursively sorts the merge lists by its mergeKey in a map.
func sortMergeListsByNameMap(s map[string]interface{}, t reflect.Type) (map[string]interface{}, error) {
func sortMergeListsByNameMap(s map[string]interface{}, schema LookupPatchMeta) (map[string]interface{}, error) {
newS := map[string]interface{}{}
for k, v := range s {
if k == retainKeysDirective {
......@@ -1597,26 +1602,29 @@ func sortMergeListsByNameMap(s map[string]interface{}, t reflect.Type) (map[stri
return nil, mergepatch.ErrBadPatchFormatForSetElementOrderList
}
} else if k != directiveMarker {
fieldType, fieldPatchStrategies, fieldPatchMergeKey, err := forkedjson.LookupPatchMetadata(t, k)
if err != nil {
return nil, err
}
_, patchStrategy, err := extractRetainKeysPatchStrategy(fieldPatchStrategies)
if err != nil {
return nil, err
}
// If v is a map or a merge slice, recurse.
if typedV, ok := v.(map[string]interface{}); ok {
var err error
v, err = sortMergeListsByNameMap(typedV, fieldType)
// recurse for map and slice.
switch typedV := v.(type) {
case map[string]interface{}:
subschema, _, err := schema.LookupPatchMetadataForStruct(k)
if err != nil {
return nil, err
}
v, err = sortMergeListsByNameMap(typedV, subschema)
if err != nil {
return nil, err
}
case []interface{}:
subschema, patchMeta, err := schema.LookupPatchMetadataForSlice(k)
if err != nil {
return nil, err
}
_, patchStrategy, err := extractRetainKeysPatchStrategy(patchMeta.GetPatchStrategies())
if err != nil {
return nil, err
}
} else if typedV, ok := v.([]interface{}); ok {
if patchStrategy == mergeDirective {
var err error
v, err = sortMergeListsByNameArray(typedV, fieldType.Elem(), fieldPatchMergeKey, true)
v, err = sortMergeListsByNameArray(typedV, subschema, patchMeta.GetPatchMergeKey(), true)
if err != nil {
return nil, err
}
......@@ -1631,7 +1639,7 @@ func sortMergeListsByNameMap(s map[string]interface{}, t reflect.Type) (map[stri
}
// Function sortMergeListsByNameMap recursively sorts the merge lists by its mergeKey in an array.
func sortMergeListsByNameArray(s []interface{}, elemType reflect.Type, mergeKey string, recurse bool) ([]interface{}, error) {
func sortMergeListsByNameArray(s []interface{}, schema LookupPatchMeta, mergeKey string, recurse bool) ([]interface{}, error) {
if len(s) == 0 {
return s, nil
}
......@@ -1654,7 +1662,7 @@ func sortMergeListsByNameArray(s []interface{}, elemType reflect.Type, mergeKey
for _, elem := range s {
if recurse {
typedElem := elem.(map[string]interface{})
newElem, err := sortMergeListsByNameMap(typedElem, elemType)
newElem, err := sortMergeListsByNameMap(typedElem, schema)
if err != nil {
return nil, err
}
......@@ -1800,18 +1808,13 @@ func sliceElementType(slices ...[]interface{}) (reflect.Type, error) {
// objects overlap with different values in any key. All keys are required to be
// strings. Since patches of the same Type have congruent keys, this is valid
// for multiple patch types. This method supports strategic merge patch semantics.
func MergingMapsHaveConflicts(left, right map[string]interface{}, dataStruct interface{}) (bool, error) {
t, err := getTagStructType(dataStruct)
if err != nil {
return true, err
}
return mergingMapFieldsHaveConflicts(left, right, t, "", "")
func MergingMapsHaveConflicts(left, right map[string]interface{}, schema LookupPatchMeta) (bool, error) {
return mergingMapFieldsHaveConflicts(left, right, schema, "", "")
}
func mergingMapFieldsHaveConflicts(
left, right interface{},
fieldType reflect.Type,
schema LookupPatchMeta,
fieldPatchStrategy, fieldPatchMergeKey string,
) (bool, error) {
switch leftType := left.(type) {
......@@ -1842,15 +1845,14 @@ func mergingMapFieldsHaveConflicts(
return false, nil
}
// Check the individual keys.
return mapsHaveConflicts(leftType, rightType, fieldType)
return mapsHaveConflicts(leftType, rightType, schema)
case []interface{}:
rightType, ok := right.([]interface{})
if !ok {
return true, nil
}
return slicesHaveConflicts(leftType, rightType, fieldType, fieldPatchStrategy, fieldPatchMergeKey)
return slicesHaveConflicts(leftType, rightType, schema, fieldPatchStrategy, fieldPatchMergeKey)
case string, float64, bool, int, int64, nil:
return !reflect.DeepEqual(left, right), nil
default:
......@@ -1858,21 +1860,37 @@ func mergingMapFieldsHaveConflicts(
}
}
func mapsHaveConflicts(typedLeft, typedRight map[string]interface{}, structType reflect.Type) (bool, error) {
func mapsHaveConflicts(typedLeft, typedRight map[string]interface{}, schema LookupPatchMeta) (bool, error) {
for key, leftValue := range typedLeft {
if key != directiveMarker && key != retainKeysDirective {
if rightValue, ok := typedRight[key]; ok {
fieldType, fieldPatchStrategies, fieldPatchMergeKey, err := forkedjson.LookupPatchMetadata(structType, key)
if err != nil {
return true, err
}
_, patchStrategy, err := extractRetainKeysPatchStrategy(fieldPatchStrategies)
if err != nil {
return true, err
var subschema LookupPatchMeta
var patchMeta PatchMeta
var patchStrategy string
var err error
switch leftValue.(type) {
case []interface{}:
subschema, patchMeta, err = schema.LookupPatchMetadataForSlice(key)
if err != nil {
return true, err
}
_, patchStrategy, err = extractRetainKeysPatchStrategy(patchMeta.patchStrategies)
if err != nil {
return true, err
}
case map[string]interface{}:
subschema, patchMeta, err = schema.LookupPatchMetadataForStruct(key)
if err != nil {
return true, err
}
_, patchStrategy, err = extractRetainKeysPatchStrategy(patchMeta.patchStrategies)
if err != nil {
return true, err
}
}
if hasConflicts, err := mergingMapFieldsHaveConflicts(leftValue, rightValue,
fieldType, patchStrategy, fieldPatchMergeKey); hasConflicts {
subschema, patchStrategy, patchMeta.GetPatchMergeKey()); hasConflicts {
return true, err
}
}
......@@ -1884,7 +1902,7 @@ func mapsHaveConflicts(typedLeft, typedRight map[string]interface{}, structType
func slicesHaveConflicts(
typedLeft, typedRight []interface{},
fieldType reflect.Type,
schema LookupPatchMeta,
fieldPatchStrategy, fieldPatchMergeKey string,
) (bool, error) {
elementType, err := sliceElementType(typedLeft, typedRight)
......@@ -1892,7 +1910,6 @@ func slicesHaveConflicts(
return true, err
}
valueType := fieldType.Elem()
if fieldPatchStrategy == mergeDirective {
// Merging lists of scalars have no conflicts by definition
// So we only need to check further if the elements are maps
......@@ -1911,7 +1928,7 @@ func slicesHaveConflicts(
return true, err
}
return mapsOfMapsHaveConflicts(leftMap, rightMap, valueType)
return mapsOfMapsHaveConflicts(leftMap, rightMap, schema)
}
// Either we don't have type information, or these are non-merging lists
......@@ -1929,7 +1946,7 @@ func slicesHaveConflicts(
// Compare the slices element by element in order
// This test will fail if the slices are not sorted
for i := range typedLeft {
if hasConflicts, err := mergingMapFieldsHaveConflicts(typedLeft[i], typedRight[i], valueType, "", ""); hasConflicts {
if hasConflicts, err := mergingMapFieldsHaveConflicts(typedLeft[i], typedRight[i], schema, "", ""); hasConflicts {
return true, err
}
}
......@@ -1956,10 +1973,10 @@ func sliceOfMapsToMapOfMaps(slice []interface{}, mergeKey string) (map[string]in
return result, nil
}
func mapsOfMapsHaveConflicts(typedLeft, typedRight map[string]interface{}, structType reflect.Type) (bool, error) {
func mapsOfMapsHaveConflicts(typedLeft, typedRight map[string]interface{}, schema LookupPatchMeta) (bool, error) {
for key, leftValue := range typedLeft {
if rightValue, ok := typedRight[key]; ok {
if hasConflicts, err := mergingMapFieldsHaveConflicts(leftValue, rightValue, structType, "", ""); hasConflicts {
if hasConflicts, err := mergingMapFieldsHaveConflicts(leftValue, rightValue, schema, "", ""); hasConflicts {
return true, err
}
}
......@@ -1979,7 +1996,7 @@ func mapsOfMapsHaveConflicts(typedLeft, typedRight map[string]interface{}, struc
// in a way that is different from how it is changed in current (e.g., deleting it, changing its
// value). We also propagate values fields that do not exist in original but are explicitly
// defined in modified.
func CreateThreeWayMergePatch(original, modified, current []byte, dataStruct interface{}, overwrite bool, fns ...mergepatch.PreconditionFunc) ([]byte, error) {
func CreateThreeWayMergePatch(original, modified, current []byte, schema LookupPatchMeta, overwrite bool, fns ...mergepatch.PreconditionFunc) ([]byte, error) {
originalMap := map[string]interface{}{}
if len(original) > 0 {
if err := json.Unmarshal(original, &originalMap); err != nil {
......@@ -2001,11 +2018,6 @@ func CreateThreeWayMergePatch(original, modified, current []byte, dataStruct int
}
}
t, err := getTagStructType(dataStruct)
if err != nil {
return nil, err
}
// The patch is the difference from current to modified without deletions, plus deletions
// from original to modified. To find it, we compute deletions, which are the deletions from
// original to modified, and delta, which is the difference from current to modified without
......@@ -2014,7 +2026,7 @@ func CreateThreeWayMergePatch(original, modified, current []byte, dataStruct int
IgnoreDeletions: true,
SetElementOrder: true,
}
deltaMap, err := diffMaps(currentMap, modifiedMap, t, deltaMapDiffOptions)
deltaMap, err := diffMaps(currentMap, modifiedMap, schema, deltaMapDiffOptions)
if err != nil {
return nil, err
}
......@@ -2022,13 +2034,13 @@ func CreateThreeWayMergePatch(original, modified, current []byte, dataStruct int
SetElementOrder: true,
IgnoreChangesAndAdditions: true,
}
deletionsMap, err := diffMaps(originalMap, modifiedMap, t, deletionsMapDiffOptions)
deletionsMap, err := diffMaps(originalMap, modifiedMap, schema, deletionsMapDiffOptions)
if err != nil {
return nil, err
}
mergeOptions := MergeOptions{}
patchMap, err := mergeMap(deletionsMap, deltaMap, t, mergeOptions)
patchMap, err := mergeMap(deletionsMap, deltaMap, schema, mergeOptions)
if err != nil {
return nil, err
}
......@@ -2044,12 +2056,12 @@ func CreateThreeWayMergePatch(original, modified, current []byte, dataStruct int
// then return a conflict error.
if !overwrite {
changeMapDiffOptions := DiffOptions{}
changedMap, err := diffMaps(originalMap, currentMap, t, changeMapDiffOptions)
changedMap, err := diffMaps(originalMap, currentMap, schema, changeMapDiffOptions)
if err != nil {
return nil, err
}
hasConflicts, err := MergingMapsHaveConflicts(patchMap, changedMap, dataStruct)
hasConflicts, err := MergingMapsHaveConflicts(patchMap, changedMap, schema)
if err != nil {
return nil, err
}
......
......@@ -17,17 +17,25 @@ limitations under the License.
package strategicpatch
import (
"encoding/json"
"fmt"
"path/filepath"
"reflect"
"strings"
"testing"
"github.com/davecgh/go-spew/spew"
"github.com/ghodss/yaml"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/json"
"k8s.io/apimachinery/pkg/util/mergepatch"
"k8s.io/apimachinery/pkg/util/sets"
sptest "k8s.io/apimachinery/pkg/util/strategicpatch/testing"
)
var (
fakeMergeItemSchema = sptest.Fake{Path: filepath.Join("testdata", "swagger-merge-item.json")}
fakePrecisionItemSchema = sptest.Fake{Path: filepath.Join("testdata", "swagger-precision-item.json")}
)
type SortMergeListTestCases struct {
......@@ -86,31 +94,34 @@ type StrategicMergePatchRawTestCaseData struct {
}
type MergeItem struct {
Name string
Value string
Other string
MergingList []MergeItem `patchStrategy:"merge" patchMergeKey:"name"`
NonMergingList []MergeItem
MergingIntList []int `patchStrategy:"merge"`
NonMergingIntList []int
MergeItemPtr *MergeItem `patchStrategy:"merge" patchMergeKey:"name"`
SimpleMap map[string]string
ReplacingItem runtime.RawExtension `patchStrategy:"replace"`
RetainKeysMap RetainKeysMergeItem `patchStrategy:"retainKeys"`
RetainKeysMergingList []MergeItem `patchStrategy:"merge,retainKeys" patchMergeKey:"name"`
Name string `json:"name,omitempty"`
Value string `json:"value,omitempty"`
Other string `json:"other,omitempty"`
MergingList []MergeItem `json:"mergingList,omitempty" patchStrategy:"merge" patchMergeKey:"name"`
NonMergingList []MergeItem `json:"nonMergingList,omitempty"`
MergingIntList []int `json:"mergingIntList,omitempty" patchStrategy:"merge"`
NonMergingIntList []int `json:"nonMergingIntList,omitempty"`
MergeItemPtr *MergeItem `json:"mergeItemPtr,omitempty" patchStrategy:"merge" patchMergeKey:"name"`
SimpleMap map[string]string `json:"simpleMap,omitempty"`
ReplacingItem runtime.RawExtension `json:"replacingItem,omitempty" patchStrategy:"replace"`
RetainKeysMap RetainKeysMergeItem `json:"retainKeysMap,omitempty" patchStrategy:"retainKeys"`
RetainKeysMergingList []MergeItem `json:"retainKeysMergingList,omitempty" patchStrategy:"merge,retainKeys" patchMergeKey:"name"`
}
type RetainKeysMergeItem struct {
Name string
Value string
Other string
SimpleMap map[string]string
MergingIntList []int `patchStrategy:"merge"`
MergingList []MergeItem `patchStrategy:"merge" patchMergeKey:"name"`
NonMergingList []MergeItem
Name string `json:"name,omitempty"`
Value string `json:"value,omitempty"`
Other string `json:"other,omitempty"`
SimpleMap map[string]string `json:"simpleMap,omitempty"`
MergingIntList []int `json:"mergingIntList,omitempty" patchStrategy:"merge"`
MergingList []MergeItem `json:"mergingList,omitempty" patchStrategy:"merge" patchMergeKey:"name"`
NonMergingList []MergeItem `json:"nonMergingList,omitempty"`
}
var mergeItem MergeItem
var (
mergeItem MergeItem
mergeItemStructSchema = PatchMetaFromStruct{T: GetTagStructTypeOrDie(mergeItem)}
)
// These are test cases for SortMergeList, used to assert that it (recursively)
// sorts both merging and non merging lists correctly.
......@@ -151,7 +162,6 @@ testCases:
- name: 3
- name: 2
- description: sort lists of maps and nested lists of maps
fieldTypes:
original:
mergingList:
- name: 2
......@@ -271,6 +281,14 @@ testCases:
`)
func TestSortMergeLists(t *testing.T) {
mergeItemOpenapiSchema := PatchMetaFromOpenAPI{
Schema: sptest.GetSchemaOrDie(fakeMergeItemSchema, "mergeItem"),
}
schemas := []LookupPatchMeta{
mergeItemStructSchema,
mergeItemOpenapiSchema,
}
tc := SortMergeListTestCases{}
err := yaml.Unmarshal(sortMergeListTestCaseData, &tc)
if err != nil {
......@@ -278,12 +296,15 @@ func TestSortMergeLists(t *testing.T) {
return
}
for _, c := range tc.TestCases {
got := sortJsonOrFail(t, testObjectToJSONOrFail(t, c.Original), c.Description)
expected := testObjectToJSONOrFail(t, c.Sorted)
if !reflect.DeepEqual(got, expected) {
t.Errorf("error in test case: %s\ncannot sort object:\n%s\nexpected:\n%s\ngot:\n%s\n",
c.Description, mergepatch.ToYAMLOrError(c.Original), mergepatch.ToYAMLOrError(c.Sorted), jsonToYAMLOrError(got))
for _, schema := range schemas {
for _, c := range tc.TestCases {
temp := testObjectToJSONOrFail(t, c.Original)
got := sortJsonOrFail(t, temp, c.Description, schema)
expected := testObjectToJSONOrFail(t, c.Sorted)
if !reflect.DeepEqual(got, expected) {
t.Errorf("using %s error in test case: %s\ncannot sort object:\n%s\nexpected:\n%s\ngot:\n%s\n",
getSchemaType(schema), c.Description, mergepatch.ToYAMLOrError(c.Original), mergepatch.ToYAMLOrError(c.Sorted), jsonToYAMLOrError(got))
}
}
}
}
......@@ -636,6 +657,14 @@ mergingIntList:
}
func TestCustomStrategicMergePatch(t *testing.T) {
mergeItemOpenapiSchema := PatchMetaFromOpenAPI{
Schema: sptest.GetSchemaOrDie(fakeMergeItemSchema, "mergeItem"),
}
schemas := []LookupPatchMeta{
mergeItemStructSchema,
mergeItemOpenapiSchema,
}
tc := StrategicMergePatchTestCases{}
err := yaml.Unmarshal(customStrategicMergePatchTestCaseData, &tc)
if err != nil {
......@@ -643,14 +672,16 @@ func TestCustomStrategicMergePatch(t *testing.T) {
return
}
for _, c := range tc.TestCases {
original, expectedTwoWayPatch, _, expectedResult := twoWayTestCaseToJSONOrFail(t, c)
testPatchApplication(t, original, expectedTwoWayPatch, expectedResult, c.Description, "")
}
for _, schema := range schemas {
for _, c := range tc.TestCases {
original, expectedTwoWayPatch, _, expectedResult := twoWayTestCaseToJSONOrFail(t, c, schema)
testPatchApplication(t, original, expectedTwoWayPatch, expectedResult, c.Description, "", schema)
}
for _, c := range customStrategicMergePatchRawTestCases {
original, expectedTwoWayPatch, _, expectedResult := twoWayRawTestCaseToJSONOrFail(t, c)
testPatchApplication(t, original, expectedTwoWayPatch, expectedResult, c.Description, c.ExpectedError)
for _, c := range customStrategicMergePatchRawTestCases {
original, expectedTwoWayPatch, _, expectedResult := twoWayRawTestCaseToJSONOrFail(t, c)
testPatchApplication(t, original, expectedTwoWayPatch, expectedResult, c.Description, c.ExpectedError, schema)
}
}
}
......@@ -2354,68 +2385,6 @@ mergingList:
},
},
{
Description: "replace non merging list nested in merging list with value conflict",
StrategicMergePatchRawTestCaseData: StrategicMergePatchRawTestCaseData{
Original: []byte(`
mergingList:
- name: 1
nonMergingList:
- name: 1
- name: 2
value: 2
- name: 2
`),
TwoWay: []byte(`
$setElementOrder/mergingList:
- name: 1
- name: 2
mergingList:
- name: 1
nonMergingList:
- name: 1
value: 1
`),
Modified: []byte(`
mergingList:
- name: 1
nonMergingList:
- name: 1
value: 1
- name: 2
`),
Current: []byte(`
mergingList:
- name: 1
other: a
nonMergingList:
- name: 1
value: c
- name: 2
other: b
`),
ThreeWay: []byte(`
$setElementOrder/mergingList:
- name: 1
- name: 2
mergingList:
- name: 1
nonMergingList:
- name: 1
value: 1
`),
Result: []byte(`
mergingList:
- name: 1
other: a
nonMergingList:
- name: 1
value: 1
- name: 2
other: b
`),
},
},
{
Description: "replace non merging list nested in merging list with deletion conflict",
StrategicMergePatchRawTestCaseData: StrategicMergePatchRawTestCaseData{
Original: []byte(`
......@@ -6041,14 +6010,16 @@ mergeItemPtr:
}
func TestStrategicMergePatch(t *testing.T) {
testStrategicMergePatchWithCustomArguments(t, "bad original",
"<THIS IS NOT JSON>", "{}", mergeItem, mergepatch.ErrBadJSONDoc)
testStrategicMergePatchWithCustomArguments(t, "bad patch",
"{}", "<THIS IS NOT JSON>", mergeItem, mergepatch.ErrBadJSONDoc)
testStrategicMergePatchWithCustomArguments(t, "bad struct",
testStrategicMergePatchWithCustomArgumentsUsingStruct(t, "bad struct",
"{}", "{}", []byte("<THIS IS NOT A STRUCT>"), mergepatch.ErrBadArgKind(struct{}{}, []byte{}))
testStrategicMergePatchWithCustomArguments(t, "nil struct",
"{}", "{}", nil, mergepatch.ErrBadArgKind(struct{}{}, nil))
mergeItemOpenapiSchema := PatchMetaFromOpenAPI{
Schema: sptest.GetSchemaOrDie(fakeMergeItemSchema, "mergeItem"),
}
schemas := []LookupPatchMeta{
mergeItemStructSchema,
mergeItemOpenapiSchema,
}
tc := StrategicMergePatchTestCases{}
err := yaml.Unmarshal(createStrategicMergePatchTestCaseData, &tc)
......@@ -6057,53 +6028,76 @@ func TestStrategicMergePatch(t *testing.T) {
return
}
for _, c := range tc.TestCases {
testTwoWayPatch(t, c)
testThreeWayPatch(t, c)
}
for _, schema := range schemas {
testStrategicMergePatchWithCustomArguments(t, "bad original",
"<THIS IS NOT JSON>", "{}", schema, mergepatch.ErrBadJSONDoc)
testStrategicMergePatchWithCustomArguments(t, "bad patch",
"{}", "<THIS IS NOT JSON>", schema, mergepatch.ErrBadJSONDoc)
testStrategicMergePatchWithCustomArguments(t, "nil struct",
"{}", "{}", nil, mergepatch.ErrBadArgKind(struct{}{}, nil))
for _, c := range tc.TestCases {
testTwoWayPatch(t, c, schema)
testThreeWayPatch(t, c, schema)
}
// run multiple times to exercise different map traversal orders
for i := 0; i < 10; i++ {
for _, c := range strategicMergePatchRawTestCases {
testTwoWayPatchForRawTestCase(t, c)
testThreeWayPatchForRawTestCase(t, c)
// run multiple times to exercise different map traversal orders
for i := 0; i < 10; i++ {
for _, c := range strategicMergePatchRawTestCases {
testTwoWayPatchForRawTestCase(t, c, schema)
testThreeWayPatchForRawTestCase(t, c, schema)
}
}
}
}
func testStrategicMergePatchWithCustomArguments(t *testing.T, description, original, patch string, dataStruct interface{}, err error) {
_, err2 := StrategicMergePatch([]byte(original), []byte(patch), dataStruct)
if err2 != err {
if err2 == nil {
t.Errorf("expected error: %s\ndid not occur in test case: %s", err, description)
func testStrategicMergePatchWithCustomArgumentsUsingStruct(t *testing.T, description, original, patch string, dataStruct interface{}, expected error) {
schema, actual := NewPatchMetaFromStruct(dataStruct)
// If actual is not nil, check error. If errors match, return.
if actual != nil {
checkErrorsEqual(t, description, expected, actual, schema)
return
}
testStrategicMergePatchWithCustomArguments(t, description, original, patch, schema, expected)
}
func testStrategicMergePatchWithCustomArguments(t *testing.T, description, original, patch string, schema LookupPatchMeta, expected error) {
_, actual := StrategicMergePatch([]byte(original), []byte(patch), schema)
checkErrorsEqual(t, description, expected, actual, schema)
}
func checkErrorsEqual(t *testing.T, description string, expected, actual error, schema LookupPatchMeta) {
if actual != expected {
if actual == nil {
t.Errorf("using %s expected error: %s\ndid not occur in test case: %s", getSchemaType(schema), expected, description)
return
}
if err == nil || err2.Error() != err.Error() {
t.Errorf("unexpected error: %s\noccurred in test case: %s", err2, description)
if expected == nil || actual.Error() != expected.Error() {
t.Errorf("using %s unexpected error: %s\noccurred in test case: %s", getSchemaType(schema), actual, description)
return
}
}
}
func testTwoWayPatch(t *testing.T, c StrategicMergePatchTestCase) {
original, expectedPatch, modified, expectedResult := twoWayTestCaseToJSONOrFail(t, c)
func testTwoWayPatch(t *testing.T, c StrategicMergePatchTestCase, schema LookupPatchMeta) {
original, expectedPatch, modified, expectedResult := twoWayTestCaseToJSONOrFail(t, c, schema)
actualPatch, err := CreateTwoWayMergePatch(original, modified, mergeItem)
actualPatch, err := CreateTwoWayMergePatchUsingLookupPatchMeta(original, modified, schema)
if err != nil {
t.Errorf("error: %s\nin test case: %s\ncannot create two way patch: %s:\n%s\n",
err, c.Description, original, mergepatch.ToYAMLOrError(c.StrategicMergePatchTestCaseData))
t.Errorf("using %s error: %s\nin test case: %s\ncannot create two way patch: %s:\n%s\n",
getSchemaType(schema), err, c.Description, original, mergepatch.ToYAMLOrError(c.StrategicMergePatchTestCaseData))
return
}
testPatchCreation(t, expectedPatch, actualPatch, c.Description)
testPatchApplication(t, original, actualPatch, expectedResult, c.Description, "")
testPatchApplication(t, original, actualPatch, expectedResult, c.Description, "", schema)
}
func testTwoWayPatchForRawTestCase(t *testing.T, c StrategicMergePatchRawTestCase) {
func testTwoWayPatchForRawTestCase(t *testing.T, c StrategicMergePatchRawTestCase, schema LookupPatchMeta) {
original, expectedPatch, modified, expectedResult := twoWayRawTestCaseToJSONOrFail(t, c)
actualPatch, err := CreateTwoWayMergePatch(original, modified, mergeItem)
actualPatch, err := CreateTwoWayMergePatchUsingLookupPatchMeta(original, modified, schema)
if err != nil {
t.Errorf("error: %s\nin test case: %s\ncannot create two way patch:\noriginal:%s\ntwoWay:%s\nmodified:%s\ncurrent:%s\nthreeWay:%s\nresult:%s\n",
err, c.Description, c.Original, c.TwoWay, c.Modified, c.Current, c.ThreeWay, c.Result)
......@@ -6111,18 +6105,18 @@ func testTwoWayPatchForRawTestCase(t *testing.T, c StrategicMergePatchRawTestCas
}
testPatchCreation(t, expectedPatch, actualPatch, c.Description)
testPatchApplication(t, original, actualPatch, expectedResult, c.Description, c.ExpectedError)
testPatchApplication(t, original, actualPatch, expectedResult, c.Description, c.ExpectedError, schema)
}
func twoWayTestCaseToJSONOrFail(t *testing.T, c StrategicMergePatchTestCase) ([]byte, []byte, []byte, []byte) {
func twoWayTestCaseToJSONOrFail(t *testing.T, c StrategicMergePatchTestCase, schema LookupPatchMeta) ([]byte, []byte, []byte, []byte) {
expectedResult := c.TwoWayResult
if expectedResult == nil {
expectedResult = c.Modified
}
return sortJsonOrFail(t, testObjectToJSONOrFail(t, c.Original), c.Description),
sortJsonOrFail(t, testObjectToJSONOrFail(t, c.TwoWay), c.Description),
sortJsonOrFail(t, testObjectToJSONOrFail(t, c.Modified), c.Description),
sortJsonOrFail(t, testObjectToJSONOrFail(t, expectedResult), c.Description)
return sortJsonOrFail(t, testObjectToJSONOrFail(t, c.Original), c.Description, schema),
sortJsonOrFail(t, testObjectToJSONOrFail(t, c.TwoWay), c.Description, schema),
sortJsonOrFail(t, testObjectToJSONOrFail(t, c.Modified), c.Description, schema),
sortJsonOrFail(t, testObjectToJSONOrFail(t, expectedResult), c.Description, schema)
}
func twoWayRawTestCaseToJSONOrFail(t *testing.T, c StrategicMergePatchRawTestCase) ([]byte, []byte, []byte, []byte) {
......@@ -6136,94 +6130,94 @@ func twoWayRawTestCaseToJSONOrFail(t *testing.T, c StrategicMergePatchRawTestCas
yamlToJSONOrError(t, expectedResult)
}
func testThreeWayPatch(t *testing.T, c StrategicMergePatchTestCase) {
original, modified, current, expected, result := threeWayTestCaseToJSONOrFail(t, c)
actual, err := CreateThreeWayMergePatch(original, modified, current, mergeItem, false)
func testThreeWayPatch(t *testing.T, c StrategicMergePatchTestCase, schema LookupPatchMeta) {
original, modified, current, expected, result := threeWayTestCaseToJSONOrFail(t, c, schema)
actual, err := CreateThreeWayMergePatch(original, modified, current, schema, false)
if err != nil {
if !mergepatch.IsConflict(err) {
t.Errorf("error: %s\nin test case: %s\ncannot create three way patch:\n%s\n",
err, c.Description, mergepatch.ToYAMLOrError(c.StrategicMergePatchTestCaseData))
t.Errorf("using %s error: %s\nin test case: %s\ncannot create three way patch:\n%s\n",
getSchemaType(schema), err, c.Description, mergepatch.ToYAMLOrError(c.StrategicMergePatchTestCaseData))
return
}
if !strings.Contains(c.Description, "conflict") {
t.Errorf("unexpected conflict: %s\nin test case: %s\ncannot create three way patch:\n%s\n",
err, c.Description, mergepatch.ToYAMLOrError(c.StrategicMergePatchTestCaseData))
t.Errorf("using %s unexpected conflict: %s\nin test case: %s\ncannot create three way patch:\n%s\n",
getSchemaType(schema), err, c.Description, mergepatch.ToYAMLOrError(c.StrategicMergePatchTestCaseData))
return
}
if len(c.Result) > 0 {
actual, err := CreateThreeWayMergePatch(original, modified, current, mergeItem, true)
actual, err := CreateThreeWayMergePatch(original, modified, current, schema, true)
if err != nil {
t.Errorf("error: %s\nin test case: %s\ncannot force three way patch application:\n%s\n",
err, c.Description, mergepatch.ToYAMLOrError(c.StrategicMergePatchTestCaseData))
t.Errorf("using %s error: %s\nin test case: %s\ncannot force three way patch application:\n%s\n",
getSchemaType(schema), err, c.Description, mergepatch.ToYAMLOrError(c.StrategicMergePatchTestCaseData))
return
}
testPatchCreation(t, expected, actual, c.Description)
testPatchApplication(t, current, actual, result, c.Description, "")
testPatchApplication(t, current, actual, result, c.Description, "", schema)
}
return
}
if strings.Contains(c.Description, "conflict") || len(c.Result) < 1 {
t.Errorf("error in test case: %s\nexpected conflict did not occur:\n%s\n",
c.Description, mergepatch.ToYAMLOrError(c.StrategicMergePatchTestCaseData))
t.Errorf("using %s error in test case: %s\nexpected conflict did not occur:\n%s\n",
getSchemaType(schema), c.Description, mergepatch.ToYAMLOrError(c.StrategicMergePatchTestCaseData))
return
}
testPatchCreation(t, expected, actual, c.Description)
testPatchApplication(t, current, actual, result, c.Description, "")
testPatchApplication(t, current, actual, result, c.Description, "", schema)
}
func testThreeWayPatchForRawTestCase(t *testing.T, c StrategicMergePatchRawTestCase) {
func testThreeWayPatchForRawTestCase(t *testing.T, c StrategicMergePatchRawTestCase, schema LookupPatchMeta) {
original, modified, current, expected, result := threeWayRawTestCaseToJSONOrFail(t, c)
actual, err := CreateThreeWayMergePatch(original, modified, current, mergeItem, false)
actual, err := CreateThreeWayMergePatch(original, modified, current, schema, false)
if err != nil {
if !mergepatch.IsConflict(err) {
t.Errorf("error: %s\nin test case: %s\ncannot create three way patch:\noriginal:%s\ntwoWay:%s\nmodified:%s\ncurrent:%s\nthreeWay:%s\nresult:%s\n",
err, c.Description, c.Original, c.TwoWay, c.Modified, c.Current, c.ThreeWay, c.Result)
t.Errorf("using %s error: %s\nin test case: %s\ncannot create three way patch:\noriginal:%s\ntwoWay:%s\nmodified:%s\ncurrent:%s\nthreeWay:%s\nresult:%s\n",
getSchemaType(schema), err, c.Description, c.Original, c.TwoWay, c.Modified, c.Current, c.ThreeWay, c.Result)
return
}
if !strings.Contains(c.Description, "conflict") {
t.Errorf("unexpected conflict: %s\nin test case: %s\ncannot create three way patch:\noriginal:%s\ntwoWay:%s\nmodified:%s\ncurrent:%s\nthreeWay:%s\nresult:%s\n",
err, c.Description, c.Original, c.TwoWay, c.Modified, c.Current, c.ThreeWay, c.Result)
t.Errorf("using %s unexpected conflict: %s\nin test case: %s\ncannot create three way patch:\noriginal:%s\ntwoWay:%s\nmodified:%s\ncurrent:%s\nthreeWay:%s\nresult:%s\n",
getSchemaType(schema), err, c.Description, c.Original, c.TwoWay, c.Modified, c.Current, c.ThreeWay, c.Result)
return
}
if len(c.Result) > 0 {
actual, err := CreateThreeWayMergePatch(original, modified, current, mergeItem, true)
actual, err := CreateThreeWayMergePatch(original, modified, current, schema, true)
if err != nil {
t.Errorf("error: %s\nin test case: %s\ncannot force three way patch application:\noriginal:%s\ntwoWay:%s\nmodified:%s\ncurrent:%s\nthreeWay:%s\nresult:%s\n",
err, c.Description, c.Original, c.TwoWay, c.Modified, c.Current, c.ThreeWay, c.Result)
t.Errorf("using %s error: %s\nin test case: %s\ncannot force three way patch application:\noriginal:%s\ntwoWay:%s\nmodified:%s\ncurrent:%s\nthreeWay:%s\nresult:%s\n",
getSchemaType(schema), err, c.Description, c.Original, c.TwoWay, c.Modified, c.Current, c.ThreeWay, c.Result)
return
}
testPatchCreation(t, expected, actual, c.Description)
testPatchApplication(t, current, actual, result, c.Description, c.ExpectedError)
testPatchApplication(t, current, actual, result, c.Description, c.ExpectedError, schema)
}
return
}
if strings.Contains(c.Description, "conflict") || len(c.Result) < 1 {
t.Errorf("error: %s\nin test case: %s\nexpected conflict did not occur:\noriginal:%s\ntwoWay:%s\nmodified:%s\ncurrent:%s\nthreeWay:%s\nresult:%s\n",
err, c.Description, c.Original, c.TwoWay, c.Modified, c.Current, c.ThreeWay, c.Result)
t.Errorf("using %s error: %s\nin test case: %s\nexpected conflict did not occur:\noriginal:%s\ntwoWay:%s\nmodified:%s\ncurrent:%s\nthreeWay:%s\nresult:%s\n",
getSchemaType(schema), err, c.Description, c.Original, c.TwoWay, c.Modified, c.Current, c.ThreeWay, c.Result)
return
}
testPatchCreation(t, expected, actual, c.Description)
testPatchApplication(t, current, actual, result, c.Description, c.ExpectedError)
testPatchApplication(t, current, actual, result, c.Description, c.ExpectedError, schema)
}
func threeWayTestCaseToJSONOrFail(t *testing.T, c StrategicMergePatchTestCase) ([]byte, []byte, []byte, []byte, []byte) {
return sortJsonOrFail(t, testObjectToJSONOrFail(t, c.Original), c.Description),
sortJsonOrFail(t, testObjectToJSONOrFail(t, c.Modified), c.Description),
sortJsonOrFail(t, testObjectToJSONOrFail(t, c.Current), c.Description),
sortJsonOrFail(t, testObjectToJSONOrFail(t, c.ThreeWay), c.Description),
sortJsonOrFail(t, testObjectToJSONOrFail(t, c.Result), c.Description)
func threeWayTestCaseToJSONOrFail(t *testing.T, c StrategicMergePatchTestCase, schema LookupPatchMeta) ([]byte, []byte, []byte, []byte, []byte) {
return sortJsonOrFail(t, testObjectToJSONOrFail(t, c.Original), c.Description, schema),
sortJsonOrFail(t, testObjectToJSONOrFail(t, c.Modified), c.Description, schema),
sortJsonOrFail(t, testObjectToJSONOrFail(t, c.Current), c.Description, schema),
sortJsonOrFail(t, testObjectToJSONOrFail(t, c.ThreeWay), c.Description, schema),
sortJsonOrFail(t, testObjectToJSONOrFail(t, c.Result), c.Description, schema)
}
func threeWayRawTestCaseToJSONOrFail(t *testing.T, c StrategicMergePatchRawTestCase) ([]byte, []byte, []byte, []byte, []byte) {
......@@ -6242,22 +6236,22 @@ func testPatchCreation(t *testing.T, expected, actual []byte, description string
}
}
func testPatchApplication(t *testing.T, original, patch, expected []byte, description, expectedError string) {
result, err := StrategicMergePatch(original, patch, mergeItem)
func testPatchApplication(t *testing.T, original, patch, expected []byte, description, expectedError string, schema LookupPatchMeta) {
result, err := StrategicMergePatchUsingLookupPatchMeta(original, patch, schema)
if len(expectedError) != 0 {
if err != nil && strings.Contains(err.Error(), expectedError) {
return
}
t.Errorf("expected error should contain:\n%s\nin test case: %s\nbut got:\n%s\n", expectedError, description, err)
t.Errorf("using %s expected error should contain:\n%s\nin test case: %s\nbut got:\n%s\n", getSchemaType(schema), expectedError, description, err)
}
if err != nil {
t.Errorf("error: %s\nin test case: %s\ncannot apply patch:\n%s\nto original:\n%s\n",
err, description, jsonToYAMLOrError(patch), jsonToYAMLOrError(original))
t.Errorf("using %s error: %s\nin test case: %s\ncannot apply patch:\n%s\nto original:\n%s\n",
getSchemaType(schema), err, description, jsonToYAMLOrError(patch), jsonToYAMLOrError(original))
return
}
if !reflect.DeepEqual(result, expected) {
format := "error in test case: %s\npatch application failed:\noriginal:\n%s\npatch:\n%s\nexpected:\n%s\ngot:\n%s\n"
format := "using error in test case: %s\npatch application failed:\noriginal:\n%s\npatch:\n%s\nexpected:\n%s\ngot:\n%s\n"
t.Errorf(format, description,
jsonToYAMLOrError(original), jsonToYAMLOrError(patch),
jsonToYAMLOrError(expected), jsonToYAMLOrError(result))
......@@ -6277,19 +6271,23 @@ func testObjectToJSONOrFail(t *testing.T, o map[string]interface{}) []byte {
return j
}
func sortJsonOrFail(t *testing.T, j []byte, description string) []byte {
func sortJsonOrFail(t *testing.T, j []byte, description string, schema LookupPatchMeta) []byte {
if j == nil {
return nil
}
r, err := sortMergeListsByName(j, mergeItem)
r, err := sortMergeListsByName(j, schema)
if err != nil {
t.Errorf("error: %s\nin test case: %s\ncannot sort object:\n%s\n", err, description, j)
t.Errorf("using %s error: %s\n in test case: %s\ncannot sort object:\n%s\n", getSchemaType(schema), err, description, j)
return nil
}
return r
}
func getSchemaType(schema LookupPatchMeta) string {
return reflect.TypeOf(schema).String()
}
func jsonToYAMLOrError(j []byte) string {
y, err := jsonToYAML(j)
if err != nil {
......@@ -6336,14 +6334,17 @@ func yamlToJSONOrError(t *testing.T, y []byte) []byte {
}
type PrecisionItem struct {
Name string
Int32 int32
Int64 int64
Float32 float32
Float64 float64
Name string `json:"name,omitempty"`
Int32 int32 `json:"int32,omitempty"`
Int64 int64 `json:"int64,omitempty"`
Float32 float32 `json:"float32,omitempty"`
Float64 float64 `json:"float64,omitempty"`
}
var precisionItem PrecisionItem
var (
precisionItem PrecisionItem
precisionItemStructSchema = PatchMetaFromStruct{T: GetTagStructTypeOrDie(precisionItem)}
)
func TestNumberConversion(t *testing.T) {
testcases := map[string]struct {
......@@ -6396,25 +6397,35 @@ func TestNumberConversion(t *testing.T) {
},
}
for k, tc := range testcases {
patch, err := CreateTwoWayMergePatch([]byte(tc.Old), []byte(tc.New), precisionItem)
if err != nil {
t.Errorf("%s: unexpected error %v", k, err)
continue
}
if tc.ExpectedPatch != string(patch) {
t.Errorf("%s: expected %s, got %s", k, tc.ExpectedPatch, string(patch))
continue
}
precisionItemOpenapiSchema := PatchMetaFromOpenAPI{
Schema: sptest.GetSchemaOrDie(fakePrecisionItemSchema, "precisionItem"),
}
precisionItemSchemas := []LookupPatchMeta{
precisionItemStructSchema,
precisionItemOpenapiSchema,
}
result, err := StrategicMergePatch([]byte(tc.Old), patch, precisionItem)
if err != nil {
t.Errorf("%s: unexpected error %v", k, err)
continue
}
if tc.ExpectedResult != string(result) {
t.Errorf("%s: expected %s, got %s", k, tc.ExpectedResult, string(result))
continue
for _, schema := range precisionItemSchemas {
for k, tc := range testcases {
patch, err := CreateTwoWayMergePatchUsingLookupPatchMeta([]byte(tc.Old), []byte(tc.New), schema)
if err != nil {
t.Errorf("using %s in testcase %s: unexpected error %v", getSchemaType(schema), k, err)
continue
}
if tc.ExpectedPatch != string(patch) {
t.Errorf("using %s in testcase %s: expected %s, got %s", getSchemaType(schema), k, tc.ExpectedPatch, string(patch))
continue
}
result, err := StrategicMergePatchUsingLookupPatchMeta([]byte(tc.Old), patch, schema)
if err != nil {
t.Errorf("using %s in testcase %s: unexpected error %v", getSchemaType(schema), k, err)
continue
}
if tc.ExpectedResult != string(result) {
t.Errorf("using %s in testcase %s: expected %s, got %s", getSchemaType(schema), k, tc.ExpectedResult, string(result))
continue
}
}
}
}
......@@ -6437,7 +6448,7 @@ replacingItem:
name: my-object
value: some-value
other: current-other
merginglist:
mergingList:
- name: 1
- name: 2
- name: 3
......@@ -6451,7 +6462,7 @@ replacingItem:
name: my-object
value: some-value
other: current-other
merginglist:
mergingList:
- name: 1
- name: 2
- name: 3
......@@ -6461,7 +6472,7 @@ replacingItem:
The: RawExtension
`),
TwoWay: []byte(`
merginglist:
mergingList:
- name: 1
- name: 2
- name: 3
......@@ -6474,7 +6485,7 @@ replacingItem:
name: my-object
value: some-value
other: current-other
merginglist:
mergingList:
- name: 1
- name: 2
- name: 3
......@@ -6493,7 +6504,7 @@ replacingItem:
name: my-object
value: some-value
other: current-other
merginglist:
mergingList:
- name: 1
- name: 2
- name: 3
......@@ -6511,7 +6522,7 @@ replacingItem:
name: my-object
value: some-value
other: current-other
merginglist:
mergingList:
- name: 1
replacingItem:
Some: Generic
......@@ -6523,7 +6534,7 @@ replacingItem:
name: my-object
value: some-value
other: current-other
merginglist:
mergingList:
- name: 1
- name: 3
replacingItem:
......@@ -6536,7 +6547,7 @@ replacingItem:
name: my-object
value: some-value
other: current-other
merginglist:
mergingList:
- name: 1
- name: 2
replacingItem:
......@@ -6545,10 +6556,10 @@ replacingItem:
The: RawExtension
`),
TwoWay: []byte(`
$setElementOrder/merginglist:
$setElementOrder/mergingList:
- name: 1
- name: 2
merginglist:
mergingList:
- name: 2
replacingItem:
Newly: Modified
......@@ -6559,7 +6570,7 @@ replacingItem:
name: my-object
value: some-value
other: current-other
merginglist:
mergingList:
- name: 1
- name: 2
replacingItem:
......@@ -6568,10 +6579,10 @@ replacingItem:
The: RawExtension
`),
ThreeWay: []byte(`
$setElementOrder/merginglist:
$setElementOrder/mergingList:
- name: 1
- name: 2
merginglist:
mergingList:
- name: 2
replacingItem:
Newly: Modified
......@@ -6582,7 +6593,7 @@ replacingItem:
name: my-object
value: some-value
other: current-other
merginglist:
mergingList:
- name: 1
- name: 2
- name: 3
......@@ -6596,9 +6607,19 @@ replacingItem:
}
func TestReplaceWithRawExtension(t *testing.T) {
for _, c := range replaceRawExtensionPatchTestCases {
testTwoWayPatchForRawTestCase(t, c)
testThreeWayPatchForRawTestCase(t, c)
mergeItemOpenapiSchema := PatchMetaFromOpenAPI{
Schema: sptest.GetSchemaOrDie(fakeMergeItemSchema, "mergeItem"),
}
schemas := []LookupPatchMeta{
mergeItemStructSchema,
mergeItemOpenapiSchema,
}
for _, schema := range schemas {
for _, c := range replaceRawExtensionPatchTestCases {
testTwoWayPatchForRawTestCase(t, c, schema)
testThreeWayPatchForRawTestCase(t, c, schema)
}
}
}
......@@ -6658,60 +6679,70 @@ func TestUnknownField(t *testing.T) {
},
}
mergeItemOpenapiSchema := PatchMetaFromOpenAPI{
Schema: sptest.GetSchemaOrDie(fakeMergeItemSchema, "mergeItem"),
}
schemas := []LookupPatchMeta{
mergeItemStructSchema,
mergeItemOpenapiSchema,
}
for _, k := range sets.StringKeySet(testcases).List() {
tc := testcases[k]
func() {
twoWay, err := CreateTwoWayMergePatch([]byte(tc.Original), []byte(tc.Modified), &MergeItem{})
if err != nil {
if len(tc.ExpectedTwoWayErr) == 0 {
t.Errorf("%s: error making two-way patch: %v", k, err)
for _, schema := range schemas {
func() {
twoWay, err := CreateTwoWayMergePatchUsingLookupPatchMeta([]byte(tc.Original), []byte(tc.Modified), schema)
if err != nil {
if len(tc.ExpectedTwoWayErr) == 0 {
t.Errorf("using %s in testcase %s: error making two-way patch: %v", getSchemaType(schema), k, err)
}
if !strings.Contains(err.Error(), tc.ExpectedTwoWayErr) {
t.Errorf("using %s in testcase %s: expected error making two-way patch to contain '%s', got %s", getSchemaType(schema), k, tc.ExpectedTwoWayErr, err)
}
return
}
if !strings.Contains(err.Error(), tc.ExpectedTwoWayErr) {
t.Errorf("%s: expected error making two-way patch to contain '%s', got %s", k, tc.ExpectedTwoWayErr, err)
}
return
}
if string(twoWay) != tc.ExpectedTwoWay {
t.Errorf("%s: expected two-way patch:\n\t%s\ngot\n\t%s", k, string(tc.ExpectedTwoWay), string(twoWay))
return
}
if string(twoWay) != tc.ExpectedTwoWay {
t.Errorf("using %s in testcase %s: expected two-way patch:\n\t%s\ngot\n\t%s", getSchemaType(schema), k, string(tc.ExpectedTwoWay), string(twoWay))
return
}
twoWayResult, err := StrategicMergePatch([]byte(tc.Original), twoWay, MergeItem{})
if err != nil {
t.Errorf("%s: error applying two-way patch: %v", k, err)
return
}
if string(twoWayResult) != tc.ExpectedTwoWayResult {
t.Errorf("%s: expected two-way result:\n\t%s\ngot\n\t%s", k, string(tc.ExpectedTwoWayResult), string(twoWayResult))
return
}
}()
twoWayResult, err := StrategicMergePatchUsingLookupPatchMeta([]byte(tc.Original), twoWay, schema)
if err != nil {
t.Errorf("using %s in testcase %s: error applying two-way patch: %v", getSchemaType(schema), k, err)
return
}
if string(twoWayResult) != tc.ExpectedTwoWayResult {
t.Errorf("using %s in testcase %s: expected two-way result:\n\t%s\ngot\n\t%s", getSchemaType(schema), k, string(tc.ExpectedTwoWayResult), string(twoWayResult))
return
}
}()
func() {
threeWay, err := CreateThreeWayMergePatch([]byte(tc.Original), []byte(tc.Modified), []byte(tc.Current), &MergeItem{}, false)
if err != nil {
if len(tc.ExpectedThreeWayErr) == 0 {
t.Errorf("%s: error making three-way patch: %v", k, err)
} else if !strings.Contains(err.Error(), tc.ExpectedThreeWayErr) {
t.Errorf("%s: expected error making three-way patch to contain '%s', got %s", k, tc.ExpectedThreeWayErr, err)
func() {
threeWay, err := CreateThreeWayMergePatch([]byte(tc.Original), []byte(tc.Modified), []byte(tc.Current), schema, false)
if err != nil {
if len(tc.ExpectedThreeWayErr) == 0 {
t.Errorf("using %s in testcase %s: error making three-way patch: %v", getSchemaType(schema), k, err)
} else if !strings.Contains(err.Error(), tc.ExpectedThreeWayErr) {
t.Errorf("using %s in testcase %s: expected error making three-way patch to contain '%s', got %s", getSchemaType(schema), k, tc.ExpectedThreeWayErr, err)
}
return
}
return
}
if string(threeWay) != tc.ExpectedThreeWay {
t.Errorf("%s: expected three-way patch:\n\t%s\ngot\n\t%s", k, string(tc.ExpectedThreeWay), string(threeWay))
return
}
if string(threeWay) != tc.ExpectedThreeWay {
t.Errorf("using %s in testcase %s: expected three-way patch:\n\t%s\ngot\n\t%s", getSchemaType(schema), k, string(tc.ExpectedThreeWay), string(threeWay))
return
}
threeWayResult, err := StrategicMergePatch([]byte(tc.Current), threeWay, MergeItem{})
if err != nil {
t.Errorf("%s: error applying three-way patch: %v", k, err)
return
} else if string(threeWayResult) != tc.ExpectedThreeWayResult {
t.Errorf("%s: expected three-way result:\n\t%s\ngot\n\t%s", k, string(tc.ExpectedThreeWayResult), string(threeWayResult))
return
}
}()
threeWayResult, err := StrategicMergePatch([]byte(tc.Current), threeWay, schema)
if err != nil {
t.Errorf("using %s in testcase %s: error applying three-way patch: %v", getSchemaType(schema), k, err)
return
} else if string(threeWayResult) != tc.ExpectedThreeWayResult {
t.Errorf("using %s in testcase %s: expected three-way result:\n\t%s\ngot\n\t%s", getSchemaType(schema), k, string(tc.ExpectedThreeWayResult), string(threeWayResult))
return
}
}()
}
}
}
{
"swagger": "2.0",
"info": {
"title": "StrategicMergePatchTestingMergeItem",
"version": "v1.9.0"
},
"paths": {},
"definitions": {
"mergeItem": {
"description": "MergeItem is type definition for testing strategic merge.",
"required": [],
"properties": {
"name": {
"description": "Name field.",
"type": "string"
},
"value": {
"description": "Value field.",
"type": "string"
},
"other": {
"description": "Other field.",
"type": "string"
},
"mergingList": {
"description": "MergingList field.",
"type": "array",
"items": {
"$ref": "#/definitions/mergeItem"
},
"x-kubernetes-patch-merge-key": "name",
"x-kubernetes-patch-strategy": "merge"
},
"nonMergingList": {
"description": "NonMergingList field.",
"type": "array",
"items": {
"$ref": "#/definitions/mergeItem"
}
},
"mergingIntList": {
"description": "MergingIntList field.",
"type": "array",
"items": {
"type": "integer",
"format": "int32"
},
"x-kubernetes-patch-strategy": "merge"
},
"nonMergingIntList": {
"description": "NonMergingIntList field.",
"type": "array",
"items": {
"type": "integer",
"format": "int32"
}
},
"mergeItemPtr": {
"description": "MergeItemPtr field.",
"$ref": "#/definitions/mergeItem",
"x-kubernetes-patch-merge-key": "name",
"x-kubernetes-patch-strategy": "merge"
},
"simpleMap": {
"description": "SimpleMap field.",
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"replacingItem": {
"description": "ReplacingItem field.",
"$ref": "#/definitions/io.k8s.apimachinery.pkg.runtime.RawExtension",
"x-kubernetes-patch-strategy": "replace"
},
"retainKeysMap": {
"description": "RetainKeysMap field.",
"$ref": "#/definitions/retainKeysMergeItem",
"x-kubernetes-patch-strategy": "retainKeys"
},
"retainKeysMergingList": {
"description": "RetainKeysMergingList field.",
"type": "array",
"items": {
"$ref": "#/definitions/mergeItem"
},
"x-kubernetes-patch-merge-key": "name",
"x-kubernetes-patch-strategy": "merge,retainKeys"
}
},
"x-kubernetes-group-version-kind": [
{
"group": "fake-group",
"kind": "mergeItem",
"version": "some-version"
}
]
},
"retainKeysMergeItem": {
"description": "RetainKeysMergeItem is type definition for testing strategic merge.",
"required": [],
"properties": {
"name": {
"description": "Name field.",
"type": "string"
},
"value": {
"description": "Value field.",
"type": "string"
},
"other": {
"description": "Other field.",
"type": "string"
},
"simpleMap": {
"description": "SimpleMap field.",
"additionalProperties": "object",
"items": {
"type": "string"
}
},
"mergingList": {
"description": "MergingList field.",
"type": "array",
"items": {
"$ref": "#/definitions/mergeItem"
},
"x-kubernetes-patch-merge-key": "name",
"x-kubernetes-patch-strategy": "merge"
},
"nonMergingList": {
"description": "NonMergingList field.",
"type": "array",
"items": {
"$ref": "#/definitions/mergeItem"
}
},
"mergingIntList": {
"description": "MergingIntList field.",
"type": "array",
"items": {
"type": "integer",
"format": "int32"
},
"x-kubernetes-patch-strategy": "merge"
}
},
"x-kubernetes-group-version-kind": [
{
"group": "fake-group",
"kind": "retainKeysMergeItem",
"version": "some-version"
}
]
},
"io.k8s.apimachinery.pkg.runtime.RawExtension": {
"description": "RawExtension is used to hold extensions in external versions.",
"required": [
"Raw"
],
"properties": {
"Raw": {
"description": "Raw is the underlying serialization of this object.",
"type": "string",
"format": "byte"
}
}
}
}
}
{
"swagger": "2.0",
"info": {
"title": "StrategicMergePatchTestingPrecisionItem",
"version": "v1.9.0"
},
"paths": {},
"definitions": {
"precisionItem": {
"description": "PrecisionItem is type definition for testing strategic merge.",
"required": [],
"properties": {
"name": {
"description": "Name field.",
"type": "string"
},
"int32": {
"description": "Int32 field.",
"type": "integer",
"format": "int32"
},
"int64": {
"description": "Int64 field.",
"type": "integer",
"format": "int64"
},
"float32": {
"description": "Float32 field.",
"type": "number",
"format": "float32"
},
"float64": {
"description": "Float64 field.",
"type": "number",
"format": "float64"
}
},
"x-kubernetes-group-version-kind": [
{
"group": "fake-group",
"kind": "precisionItem",
"version": "some-version"
}
]
}
}
}
package(default_visibility = ["//visibility:public"])
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = ["openapi.go"],
importpath = "k8s.io/apimachinery/pkg/util/strategicpatch/testing",
visibility = ["//visibility:public"],
deps = [
"//vendor/github.com/googleapis/gnostic/OpenAPIv2:go_default_library",
"//vendor/github.com/googleapis/gnostic/compiler:go_default_library",
"//vendor/gopkg.in/yaml.v2:go_default_library",
"//vendor/k8s.io/kube-openapi/pkg/util/proto:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
/*
Copyright 2017 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 testing
import (
"io/ioutil"
"os"
"sync"
"gopkg.in/yaml.v2"
"github.com/googleapis/gnostic/OpenAPIv2"
"github.com/googleapis/gnostic/compiler"
openapi "k8s.io/kube-openapi/pkg/util/proto"
)
// Fake opens and returns a openapi swagger from a file Path. It will
// parse only once and then return the same copy everytime.
type Fake struct {
Path string
once sync.Once
document *openapi_v2.Document
err error
}
// OpenAPISchema returns the openapi document and a potential error.
func (f *Fake) OpenAPISchema() (*openapi_v2.Document, error) {
f.once.Do(func() {
_, err := os.Stat(f.Path)
if err != nil {
f.err = err
return
}
spec, err := ioutil.ReadFile(f.Path)
if err != nil {
f.err = err
return
}
var info yaml.MapSlice
err = yaml.Unmarshal(spec, &info)
if err != nil {
f.err = err
return
}
f.document, f.err = openapi_v2.NewDocument(info, compiler.NewContext("$root", nil))
})
return f.document, f.err
}
func getSchema(f Fake, model string) (openapi.Schema, error) {
s, err := f.OpenAPISchema()
if err != nil {
return nil, err
}
m, err := openapi.NewOpenAPIData(s)
if err != nil {
return nil, err
}
return m.LookupModel(model), nil
}
// GetSchemaOrDie returns returns the openapi schema.
func GetSchemaOrDie(f Fake, model string) openapi.Schema {
s, err := getSchema(f, model)
if err != nil {
panic(err)
}
return s
}
/*
Copyright 2017 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 strategicpatch
import (
"errors"
"strings"
"k8s.io/apimachinery/pkg/util/mergepatch"
openapi "k8s.io/kube-openapi/pkg/util/proto"
)
const (
patchStrategyOpenapiextensionKey = "x-kubernetes-patch-strategy"
patchMergeKeyOpenapiextensionKey = "x-kubernetes-patch-merge-key"
)
type LookupPatchItem interface {
openapi.SchemaVisitor
Error() error
Path() *openapi.Path
}
type kindItem struct {
key string
path *openapi.Path
err error
patchmeta PatchMeta
subschema openapi.Schema
hasVisitKind bool
}
func NewKindItem(key string, path *openapi.Path) *kindItem {
return &kindItem{
key: key,
path: path,
}
}
var _ LookupPatchItem = &kindItem{}
func (item *kindItem) Error() error {
return item.err
}
func (item *kindItem) Path() *openapi.Path {
return item.path
}
func (item *kindItem) VisitPrimitive(schema *openapi.Primitive) {
item.err = errors.New("expected kind, but got primitive")
}
func (item *kindItem) VisitArray(schema *openapi.Array) {
item.err = errors.New("expected kind, but got slice")
}
func (item *kindItem) VisitMap(schema *openapi.Map) {
item.err = errors.New("expected kind, but got map")
}
func (item *kindItem) VisitReference(schema openapi.Reference) {
if !item.hasVisitKind {
schema.SubSchema().Accept(item)
}
}
func (item *kindItem) VisitKind(schema *openapi.Kind) {
subschema, ok := schema.Fields[item.key]
if !ok {
item.err = FieldNotFoundError{Path: schema.GetPath().String(), Field: item.key}
return
}
mergeKey, patchStrategies, err := parsePatchMetadata(subschema.GetExtensions())
if err != nil {
item.err = err
return
}
item.patchmeta = PatchMeta{
patchStrategies: patchStrategies,
patchMergeKey: mergeKey,
}
item.subschema = subschema
}
type sliceItem struct {
key string
path *openapi.Path
err error
patchmeta PatchMeta
subschema openapi.Schema
hasVisitKind bool
}
func NewSliceItem(key string, path *openapi.Path) *sliceItem {
return &sliceItem{
key: key,
path: path,
}
}
var _ LookupPatchItem = &sliceItem{}
func (item *sliceItem) Error() error {
return item.err
}
func (item *sliceItem) Path() *openapi.Path {
return item.path
}
func (item *sliceItem) VisitPrimitive(schema *openapi.Primitive) {
item.err = errors.New("expected slice, but got primitive")
}
func (item *sliceItem) VisitArray(schema *openapi.Array) {
if !item.hasVisitKind {
item.err = errors.New("expected visit kind first, then visit array")
}
subschema := schema.SubType
item.subschema = subschema
}
func (item *sliceItem) VisitMap(schema *openapi.Map) {
item.err = errors.New("expected slice, but got map")
}
func (item *sliceItem) VisitReference(schema openapi.Reference) {
if !item.hasVisitKind {
schema.SubSchema().Accept(item)
} else {
item.subschema = schema.SubSchema()
}
}
func (item *sliceItem) VisitKind(schema *openapi.Kind) {
subschema, ok := schema.Fields[item.key]
if !ok {
item.err = FieldNotFoundError{Path: schema.GetPath().String(), Field: item.key}
return
}
mergeKey, patchStrategies, err := parsePatchMetadata(subschema.GetExtensions())
if err != nil {
item.err = err
return
}
item.patchmeta = PatchMeta{
patchStrategies: patchStrategies,
patchMergeKey: mergeKey,
}
item.hasVisitKind = true
subschema.Accept(item)
}
func parsePatchMetadata(extensions map[string]interface{}) (string, []string, error) {
ps, foundPS := extensions[patchStrategyOpenapiextensionKey]
var patchStrategies []string
var mergeKey, patchStrategy string
var ok bool
if foundPS {
patchStrategy, ok = ps.(string)
if ok {
patchStrategies = strings.Split(patchStrategy, ",")
} else {
return "", nil, mergepatch.ErrBadArgType(patchStrategy, ps)
}
}
mk, foundMK := extensions[patchMergeKeyOpenapiextensionKey]
if foundMK {
mergeKey, ok = mk.(string)
if !ok {
return "", nil, mergepatch.ErrBadArgType(mergeKey, mk)
}
}
return mergeKey, patchStrategies, nil
}
......@@ -26,17 +26,14 @@ const (
// struct field given the struct type and the JSON name of the field.
// It returns field type, a slice of patch strategies, merge key and error.
// TODO: fix the returned errors to be introspectable.
func LookupPatchMetadata(t reflect.Type, jsonField string) (
func LookupPatchMetadataForStruct(t reflect.Type, jsonField string) (
elemType reflect.Type, patchStrategies []string, patchMergeKey string, e error) {
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
if t.Kind() == reflect.Map {
elemType = t.Elem()
return
}
if t.Kind() != reflect.Struct {
e = fmt.Errorf("merging an object in json but data type is not map or struct, instead is: %s",
e = fmt.Errorf("merging an object in json but data type is not struct, instead is: %s",
t.Kind().String())
return
}
......
......@@ -14,7 +14,7 @@ func TestLookupPtrToStruct(t *testing.T) {
Inner []Elem `json:"inner" patchStrategy:"merge" patchMergeKey:"key"`
}
outer := &Outer{}
elemType, patchStrategies, patchMergeKey, err := LookupPatchMetadata(reflect.TypeOf(outer), "inner")
elemType, patchStrategies, patchMergeKey, err := LookupPatchMetadataForStruct(reflect.TypeOf(outer), "inner")
if err != nil {
t.Fatal(err)
}
......
......@@ -1667,6 +1667,10 @@
"Rev": "39a7bf85c140f972372c2a0d1ee40adbf0c8bfe1"
},
{
"ImportPath": "k8s.io/kube-openapi/pkg/util/proto",
"Rev": "39a7bf85c140f972372c2a0d1ee40adbf0c8bfe1"
},
{
"ImportPath": "k8s.io/client-go/discovery",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
......
......@@ -697,6 +697,10 @@
{
"ImportPath": "k8s.io/kube-openapi/pkg/common",
"Rev": "39a7bf85c140f972372c2a0d1ee40adbf0c8bfe1"
},
{
"ImportPath": "k8s.io/kube-openapi/pkg/util/proto",
"Rev": "39a7bf85c140f972372c2a0d1ee40adbf0c8bfe1"
}
]
}
......@@ -1633,6 +1633,10 @@
{
"ImportPath": "k8s.io/kube-openapi/pkg/util",
"Rev": "39a7bf85c140f972372c2a0d1ee40adbf0c8bfe1"
},
{
"ImportPath": "k8s.io/kube-openapi/pkg/util/proto",
"Rev": "39a7bf85c140f972372c2a0d1ee40adbf0c8bfe1"
}
]
}
......@@ -1617,6 +1617,10 @@
{
"ImportPath": "k8s.io/kube-openapi/pkg/util",
"Rev": "39a7bf85c140f972372c2a0d1ee40adbf0c8bfe1"
},
{
"ImportPath": "k8s.io/kube-openapi/pkg/util/proto",
"Rev": "39a7bf85c140f972372c2a0d1ee40adbf0c8bfe1"
}
]
}
......@@ -965,6 +965,10 @@
{
"ImportPath": "k8s.io/kube-openapi/pkg/common",
"Rev": "39a7bf85c140f972372c2a0d1ee40adbf0c8bfe1"
},
{
"ImportPath": "k8s.io/kube-openapi/pkg/util/proto",
"Rev": "39a7bf85c140f972372c2a0d1ee40adbf0c8bfe1"
}
]
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment