mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-25 12:43:23 +00:00
support openapi in apply
This commit is contained in:
parent
f1ad84a2c3
commit
0b0004e0c0
@ -86,6 +86,7 @@ go_library(
|
|||||||
"//pkg/kubectl/cmd/templates:go_default_library",
|
"//pkg/kubectl/cmd/templates:go_default_library",
|
||||||
"//pkg/kubectl/cmd/util:go_default_library",
|
"//pkg/kubectl/cmd/util:go_default_library",
|
||||||
"//pkg/kubectl/cmd/util/editor: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/explain:go_default_library",
|
||||||
"//pkg/kubectl/metricsutil:go_default_library",
|
"//pkg/kubectl/metricsutil:go_default_library",
|
||||||
"//pkg/kubectl/plugins: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/portforward:go_default_library",
|
||||||
"//vendor/k8s.io/client-go/tools/remotecommand: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/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",
|
"//vendor/k8s.io/utils/exec:go_default_library",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@ -214,6 +216,7 @@ go_test(
|
|||||||
"//pkg/kubectl:go_default_library",
|
"//pkg/kubectl:go_default_library",
|
||||||
"//pkg/kubectl/cmd/testing:go_default_library",
|
"//pkg/kubectl/cmd/testing:go_default_library",
|
||||||
"//pkg/kubectl/cmd/util: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/plugins:go_default_library",
|
||||||
"//pkg/kubectl/resource:go_default_library",
|
"//pkg/kubectl/resource:go_default_library",
|
||||||
"//pkg/kubectl/scheme: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/intstr:go_default_library",
|
||||||
"//vendor/k8s.io/apimachinery/pkg/util/sets: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: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/apimachinery/pkg/util/wait:go_default_library",
|
||||||
"//vendor/k8s.io/client-go/dynamic:go_default_library",
|
"//vendor/k8s.io/client-go/dynamic:go_default_library",
|
||||||
"//vendor/k8s.io/client-go/rest: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/sets"
|
||||||
"k8s.io/apimachinery/pkg/util/strategicpatch"
|
"k8s.io/apimachinery/pkg/util/strategicpatch"
|
||||||
"k8s.io/apimachinery/pkg/util/wait"
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
|
oapi "k8s.io/kube-openapi/pkg/util/proto"
|
||||||
api "k8s.io/kubernetes/pkg/apis/core"
|
api "k8s.io/kubernetes/pkg/apis/core"
|
||||||
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
|
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
|
||||||
"k8s.io/kubernetes/pkg/kubectl"
|
"k8s.io/kubernetes/pkg/kubectl"
|
||||||
"k8s.io/kubernetes/pkg/kubectl/cmd/templates"
|
"k8s.io/kubernetes/pkg/kubectl/cmd/templates"
|
||||||
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
|
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/resource"
|
||||||
"k8s.io/kubernetes/pkg/kubectl/scheme"
|
"k8s.io/kubernetes/pkg/kubectl/scheme"
|
||||||
"k8s.io/kubernetes/pkg/kubectl/util/i18n"
|
"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().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().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().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.AddDryRunFlag(cmd)
|
||||||
cmdutil.AddPrinterFlags(cmd)
|
cmdutil.AddPrinterFlags(cmd)
|
||||||
cmdutil.AddRecordFlag(cmd)
|
cmdutil.AddRecordFlag(cmd)
|
||||||
@ -193,6 +196,14 @@ func RunApply(f cmdutil.Factory, cmd *cobra.Command, out, errOut io.Writer, opti
|
|||||||
return err
|
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()
|
cmdNamespace, enforceNamespace, err := f.DefaultNamespace()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -312,9 +323,10 @@ func RunApply(f cmdutil.Factory, cmd *cobra.Command, out, errOut io.Writer, opti
|
|||||||
cascade: options.Cascade,
|
cascade: options.Cascade,
|
||||||
timeout: options.Timeout,
|
timeout: options.Timeout,
|
||||||
gracePeriod: options.GracePeriod,
|
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 {
|
if err != nil {
|
||||||
return cmdutil.AddSourceToErr(fmt.Sprintf("applying patch:\n%s\nto:\n%v\nfor:", patchBytes, info), info.Source, err)
|
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
|
cascade bool
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
gracePeriod int
|
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.
|
// Serialize the current configuration of the object from the server.
|
||||||
current, err := runtime.Encode(p.encoder, obj)
|
current, err := runtime.Encode(p.encoder, obj)
|
||||||
if err != nil {
|
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)
|
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 patchType types.PatchType
|
||||||
var patch []byte
|
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:"
|
createPatchErrFormat := "creating patch with:\noriginal:\n%s\nmodified:\n%s\ncurrent:\n%s\nfor:"
|
||||||
switch {
|
// Try to use openapi first if the openapi spec is available and can successfully calculate the patch.
|
||||||
case runtime.IsNotRegisteredError(err):
|
// Otherwise, fall back to baked-in types.
|
||||||
// fall back to generic JSON merge patch
|
if p.openapiSchema != nil {
|
||||||
patchType = types.MergePatchType
|
if schema = p.openapiSchema.LookupResource(p.mapping.GroupVersionKind); schema != nil {
|
||||||
preconditions := []mergepatch.PreconditionFunc{mergepatch.RequireKeyUnchanged("apiVersion"),
|
lookupPatchMeta = strategicpatch.PatchMetaFromOpenAPI{Schema: schema}
|
||||||
mergepatch.RequireKeyUnchanged("kind"), mergepatch.RequireMetadataKeyUnchanged("name")}
|
if openapiPatch, err := strategicpatch.CreateThreeWayMergePatch(original, modified, current, lookupPatchMeta, p.overwrite); err != nil {
|
||||||
patch, err = jsonmergepatch.CreateThreeWayJSONMergePatch(original, modified, current, preconditions...)
|
fmt.Fprintf(errOut, "warning: error calculating patch from openapi spec: %v\n", err)
|
||||||
if err != nil {
|
} else {
|
||||||
if mergepatch.IsPreconditionFailed(err) {
|
patchType = types.StrategicMergePatchType
|
||||||
return nil, nil, fmt.Errorf("%s", "At least one of apiVersion, kind and name was changed")
|
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:
|
if patch == nil {
|
||||||
// Compute a three way strategic merge patch to send to server.
|
// Create the versioned struct from the type defined in the restmapping
|
||||||
patchType = types.StrategicMergePatchType
|
// (which is the API version we'll be submitting the patch to)
|
||||||
patch, err = strategicpatch.CreateThreeWayMergePatch(original, modified, current, versionedObject, p.overwrite)
|
versionedObject, err := scheme.Scheme.New(p.mapping.GroupVersionKind)
|
||||||
if err != nil {
|
switch {
|
||||||
return nil, nil, cmdutil.AddSourceToErr(fmt.Sprintf(createPatchErrFormat, original, modified, current), source, err)
|
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
|
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
|
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++ {
|
for i := 1; i <= maxPatchRetry && errors.IsConflict(err); i++ {
|
||||||
if i > triesBeforeBackOff {
|
if i > triesBeforeBackOff {
|
||||||
p.backOff.Sleep(backOffPeriod)
|
p.backOff.Sleep(backOffPeriod)
|
||||||
@ -634,7 +670,7 @@ func (p *patcher) patch(current runtime.Object, modified []byte, source, namespa
|
|||||||
if getErr != nil {
|
if getErr != nil {
|
||||||
return nil, nil, getErr
|
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 {
|
if err != nil && p.force {
|
||||||
patchBytes, patchObject, err = p.deleteAndCreate(modified, namespace, name)
|
patchBytes, patchObject, err = p.deleteAndCreate(modified, namespace, name)
|
||||||
|
@ -19,10 +19,12 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@ -33,15 +35,33 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"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/client-go/rest/fake"
|
||||||
"k8s.io/kubernetes/pkg/api/testapi"
|
"k8s.io/kubernetes/pkg/api/testapi"
|
||||||
api "k8s.io/kubernetes/pkg/apis/core"
|
api "k8s.io/kubernetes/pkg/apis/core"
|
||||||
"k8s.io/kubernetes/pkg/apis/extensions"
|
"k8s.io/kubernetes/pkg/apis/extensions"
|
||||||
cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing"
|
cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing"
|
||||||
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
|
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
|
||||||
|
"k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi"
|
||||||
"k8s.io/kubernetes/pkg/printers"
|
"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) {
|
func TestApplyExtraArgsFail(t *testing.T) {
|
||||||
buf := bytes.NewBuffer([]byte{})
|
buf := bytes.NewBuffer([]byte{})
|
||||||
errBuf := bytes.NewBuffer([]byte{})
|
errBuf := bytes.NewBuffer([]byte{})
|
||||||
@ -410,38 +430,44 @@ func TestApplyObject(t *testing.T) {
|
|||||||
nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC)
|
nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC)
|
||||||
pathRC := "/namespaces/test/replicationcontrollers/" + nameRC
|
pathRC := "/namespaces/test/replicationcontrollers/" + nameRC
|
||||||
|
|
||||||
f, tf, _, _ := cmdtesting.NewAPIFactory()
|
for _, fn := range testingOpenAPISchemaFns {
|
||||||
tf.Printer = &testPrinter{}
|
f, tf, _, _ := cmdtesting.NewAPIFactory()
|
||||||
tf.UnstructuredClient = &fake.RESTClient{
|
tf.Printer = &testPrinter{}
|
||||||
NegotiatedSerializer: unstructuredSerializer,
|
tf.UnstructuredClient = &fake.RESTClient{
|
||||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
NegotiatedSerializer: unstructuredSerializer,
|
||||||
switch p, m := req.URL.Path, req.Method; {
|
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||||
case p == pathRC && m == "GET":
|
switch p, m := req.URL.Path, req.Method; {
|
||||||
bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
|
case p == pathRC && m == "GET":
|
||||||
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodyRC}, nil
|
bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
|
||||||
case p == pathRC && m == "PATCH":
|
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodyRC}, nil
|
||||||
validatePatchApplication(t, req)
|
case p == pathRC && m == "PATCH":
|
||||||
bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
|
validatePatchApplication(t, req)
|
||||||
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodyRC}, nil
|
bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
|
||||||
default:
|
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodyRC}, nil
|
||||||
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
default:
|
||||||
return nil, nil
|
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
||||||
}
|
return nil, nil
|
||||||
}),
|
}
|
||||||
}
|
}),
|
||||||
tf.Namespace = "test"
|
}
|
||||||
buf := bytes.NewBuffer([]byte{})
|
tf.OpenAPISchemaFunc = fn
|
||||||
errBuf := bytes.NewBuffer([]byte{})
|
tf.Namespace = "test"
|
||||||
|
buf := bytes.NewBuffer([]byte{})
|
||||||
|
errBuf := bytes.NewBuffer([]byte{})
|
||||||
|
|
||||||
cmd := NewCmdApply("kubectl", f, buf, errBuf)
|
cmd := NewCmdApply("kubectl", f, buf, errBuf)
|
||||||
cmd.Flags().Set("filename", filenameRC)
|
cmd.Flags().Set("filename", filenameRC)
|
||||||
cmd.Flags().Set("output", "name")
|
cmd.Flags().Set("output", "name")
|
||||||
cmd.Run(cmd, []string{})
|
cmd.Run(cmd, []string{})
|
||||||
|
|
||||||
// uses the name from the file, not the response
|
// uses the name from the file, not the response
|
||||||
expectRC := "replicationcontroller/" + nameRC + "\n"
|
expectRC := "replicationcontroller/" + nameRC + "\n"
|
||||||
if buf.String() != expectRC {
|
if buf.String() != expectRC {
|
||||||
t.Fatalf("unexpected output: %s\nexpected: %s", 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)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
f, tf, _, _ := cmdtesting.NewAPIFactory()
|
for _, fn := range testingOpenAPISchemaFns {
|
||||||
tf.Printer = &printers.YAMLPrinter{}
|
f, tf, _, _ := cmdtesting.NewAPIFactory()
|
||||||
tf.UnstructuredClient = &fake.RESTClient{
|
tf.Printer = &printers.YAMLPrinter{}
|
||||||
NegotiatedSerializer: unstructuredSerializer,
|
tf.UnstructuredClient = &fake.RESTClient{
|
||||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
NegotiatedSerializer: unstructuredSerializer,
|
||||||
switch p, m := req.URL.Path, req.Method; {
|
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||||
case p == pathRC && m == "GET":
|
switch p, m := req.URL.Path, req.Method; {
|
||||||
bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
|
case p == pathRC && m == "GET":
|
||||||
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodyRC}, nil
|
bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
|
||||||
case p == pathRC && m == "PATCH":
|
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodyRC}, nil
|
||||||
validatePatchApplication(t, req)
|
case p == pathRC && m == "PATCH":
|
||||||
bodyRC := ioutil.NopCloser(bytes.NewReader(postPatchData))
|
validatePatchApplication(t, req)
|
||||||
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodyRC}, nil
|
bodyRC := ioutil.NopCloser(bytes.NewReader(postPatchData))
|
||||||
default:
|
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodyRC}, nil
|
||||||
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
default:
|
||||||
return nil, nil
|
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
||||||
}
|
return nil, nil
|
||||||
}),
|
}
|
||||||
}
|
}),
|
||||||
tf.Namespace = "test"
|
}
|
||||||
buf := bytes.NewBuffer([]byte{})
|
tf.OpenAPISchemaFunc = fn
|
||||||
errBuf := bytes.NewBuffer([]byte{})
|
tf.Namespace = "test"
|
||||||
|
buf := bytes.NewBuffer([]byte{})
|
||||||
|
errBuf := bytes.NewBuffer([]byte{})
|
||||||
|
|
||||||
cmd := NewCmdApply("kubectl", f, buf, errBuf)
|
cmd := NewCmdApply("kubectl", f, buf, errBuf)
|
||||||
cmd.Flags().Set("filename", filenameRC)
|
cmd.Flags().Set("filename", filenameRC)
|
||||||
cmd.Flags().Set("output", "yaml")
|
cmd.Flags().Set("output", "yaml")
|
||||||
cmd.Run(cmd, []string{})
|
cmd.Run(cmd, []string{})
|
||||||
|
|
||||||
if !strings.Contains(buf.String(), "name: test-rc") {
|
if !strings.Contains(buf.String(), "name: test-rc") {
|
||||||
t.Fatalf("unexpected output: %s\nexpected to contain: %s", 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") {
|
if !strings.Contains(buf.String(), "post-patch: value") {
|
||||||
t.Fatalf("unexpected output: %s\nexpected to contain: %s", 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)
|
nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC)
|
||||||
pathRC := "/namespaces/test/replicationcontrollers/" + nameRC
|
pathRC := "/namespaces/test/replicationcontrollers/" + nameRC
|
||||||
|
|
||||||
firstPatch := true
|
for _, fn := range testingOpenAPISchemaFns {
|
||||||
retry := false
|
firstPatch := true
|
||||||
getCount := 0
|
retry := false
|
||||||
f, tf, _, _ := cmdtesting.NewAPIFactory()
|
getCount := 0
|
||||||
tf.Printer = &testPrinter{}
|
f, tf, _, _ := cmdtesting.NewAPIFactory()
|
||||||
tf.UnstructuredClient = &fake.RESTClient{
|
tf.Printer = &testPrinter{}
|
||||||
NegotiatedSerializer: unstructuredSerializer,
|
tf.UnstructuredClient = &fake.RESTClient{
|
||||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
NegotiatedSerializer: unstructuredSerializer,
|
||||||
switch p, m := req.URL.Path, req.Method; {
|
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||||
case p == pathRC && m == "GET":
|
switch p, m := req.URL.Path, req.Method; {
|
||||||
getCount++
|
case p == pathRC && m == "GET":
|
||||||
bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
|
getCount++
|
||||||
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodyRC}, nil
|
bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
|
||||||
case p == pathRC && m == "PATCH":
|
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodyRC}, nil
|
||||||
if firstPatch {
|
case p == pathRC && m == "PATCH":
|
||||||
firstPatch = false
|
if firstPatch {
|
||||||
statusErr := kubeerr.NewConflict(schema.GroupResource{Group: "", Resource: "rc"}, "test-rc", fmt.Errorf("the object has been modified. Please apply at first."))
|
firstPatch = false
|
||||||
bodyBytes, _ := json.Marshal(statusErr)
|
statusErr := kubeerr.NewConflict(schema.GroupResource{Group: "", Resource: "rc"}, "test-rc", fmt.Errorf("the object has been modified. Please apply at first."))
|
||||||
bodyErr := ioutil.NopCloser(bytes.NewReader(bodyBytes))
|
bodyBytes, _ := json.Marshal(statusErr)
|
||||||
return &http.Response{StatusCode: http.StatusConflict, Header: defaultHeader(), Body: bodyErr}, nil
|
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))
|
tf.OpenAPISchemaFunc = fn
|
||||||
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodyRC}, nil
|
tf.Namespace = "test"
|
||||||
default:
|
buf := bytes.NewBuffer([]byte{})
|
||||||
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
errBuf := bytes.NewBuffer([]byte{})
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
tf.Namespace = "test"
|
|
||||||
buf := bytes.NewBuffer([]byte{})
|
|
||||||
errBuf := bytes.NewBuffer([]byte{})
|
|
||||||
|
|
||||||
cmd := NewCmdApply("kubectl", f, buf, errBuf)
|
cmd := NewCmdApply("kubectl", f, buf, errBuf)
|
||||||
cmd.Flags().Set("filename", filenameRC)
|
cmd.Flags().Set("filename", filenameRC)
|
||||||
cmd.Flags().Set("output", "name")
|
cmd.Flags().Set("output", "name")
|
||||||
cmd.Run(cmd, []string{})
|
cmd.Run(cmd, []string{})
|
||||||
|
|
||||||
if !retry || getCount != 2 {
|
if !retry || getCount != 2 {
|
||||||
t.Fatalf("apply didn't retry when get conflict error")
|
t.Fatalf("apply didn't retry when get conflict error")
|
||||||
}
|
}
|
||||||
|
|
||||||
// uses the name from the file, not the response
|
// uses the name from the file, not the response
|
||||||
expectRC := "replicationcontroller/" + nameRC + "\n"
|
expectRC := "replicationcontroller/" + nameRC + "\n"
|
||||||
if buf.String() != expectRC {
|
if buf.String() != expectRC {
|
||||||
t.Fatalf("unexpected output: %s\nexpected: %s", 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)
|
nameSVC, currentSVC := readAndAnnotateService(t, filenameSVC)
|
||||||
pathSVC := "/namespaces/test/services/" + nameSVC
|
pathSVC := "/namespaces/test/services/" + nameSVC
|
||||||
|
|
||||||
f, tf, _, _ := cmdtesting.NewAPIFactory()
|
for _, fn := range testingOpenAPISchemaFns {
|
||||||
tf.Printer = &testPrinter{}
|
f, tf, _, _ := cmdtesting.NewAPIFactory()
|
||||||
tf.UnstructuredClient = &fake.RESTClient{
|
tf.Printer = &testPrinter{}
|
||||||
NegotiatedSerializer: unstructuredSerializer,
|
tf.UnstructuredClient = &fake.RESTClient{
|
||||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
NegotiatedSerializer: unstructuredSerializer,
|
||||||
switch p, m := req.URL.Path, req.Method; {
|
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||||
case p == pathRC && m == "GET":
|
switch p, m := req.URL.Path, req.Method; {
|
||||||
bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
|
case p == pathRC && m == "GET":
|
||||||
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodyRC}, nil
|
bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
|
||||||
case p == pathRC && m == "PATCH":
|
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodyRC}, nil
|
||||||
validatePatchApplication(t, req)
|
case p == pathRC && m == "PATCH":
|
||||||
bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
|
validatePatchApplication(t, req)
|
||||||
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodyRC}, nil
|
bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
|
||||||
case p == pathSVC && m == "GET":
|
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodyRC}, nil
|
||||||
bodySVC := ioutil.NopCloser(bytes.NewReader(currentSVC))
|
case p == pathSVC && m == "GET":
|
||||||
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodySVC}, nil
|
bodySVC := ioutil.NopCloser(bytes.NewReader(currentSVC))
|
||||||
case p == pathSVC && m == "PATCH":
|
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodySVC}, nil
|
||||||
validatePatchApplication(t, req)
|
case p == pathSVC && m == "PATCH":
|
||||||
bodySVC := ioutil.NopCloser(bytes.NewReader(currentSVC))
|
validatePatchApplication(t, req)
|
||||||
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodySVC}, nil
|
bodySVC := ioutil.NopCloser(bytes.NewReader(currentSVC))
|
||||||
default:
|
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodySVC}, nil
|
||||||
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
default:
|
||||||
return nil, nil
|
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
||||||
}
|
return nil, nil
|
||||||
}),
|
}
|
||||||
}
|
}),
|
||||||
tf.Namespace = "test"
|
}
|
||||||
buf := bytes.NewBuffer([]byte{})
|
tf.OpenAPISchemaFunc = fn
|
||||||
errBuf := bytes.NewBuffer([]byte{})
|
tf.Namespace = "test"
|
||||||
|
buf := bytes.NewBuffer([]byte{})
|
||||||
|
errBuf := bytes.NewBuffer([]byte{})
|
||||||
|
|
||||||
cmd := NewCmdApply("kubectl", f, buf, errBuf)
|
cmd := NewCmdApply("kubectl", f, buf, errBuf)
|
||||||
if asList {
|
if asList {
|
||||||
cmd.Flags().Set("filename", filenameRCSVC)
|
cmd.Flags().Set("filename", filenameRCSVC)
|
||||||
} else {
|
} else {
|
||||||
cmd.Flags().Set("filename", filenameRC)
|
cmd.Flags().Set("filename", filenameRC)
|
||||||
cmd.Flags().Set("filename", filenameSVC)
|
cmd.Flags().Set("filename", filenameSVC)
|
||||||
}
|
}
|
||||||
cmd.Flags().Set("output", "name")
|
cmd.Flags().Set("output", "name")
|
||||||
|
|
||||||
cmd.Run(cmd, []string{})
|
cmd.Run(cmd, []string{})
|
||||||
|
|
||||||
// Names should come from the REST response, NOT the files
|
// Names should come from the REST response, NOT the files
|
||||||
expectRC := "replicationcontroller/" + nameRC + "\n"
|
expectRC := "replicationcontroller/" + nameRC + "\n"
|
||||||
expectSVC := "service/" + nameSVC + "\n"
|
expectSVC := "service/" + nameSVC + "\n"
|
||||||
// Test both possible orders since output is non-deterministic.
|
// Test both possible orders since output is non-deterministic.
|
||||||
expectOne := expectRC + expectSVC
|
expectOne := expectRC + expectSVC
|
||||||
expectTwo := expectSVC + expectRC
|
expectTwo := expectSVC + expectRC
|
||||||
if buf.String() != expectOne && buf.String() != expectTwo {
|
if buf.String() != expectOne && buf.String() != expectTwo {
|
||||||
t.Fatalf("unexpected output: %s\nexpected: %s OR %s", buf.String(), expectOne, 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
|
verifiedPatch := false
|
||||||
deploymentBytes := readDeploymentFromFile(t, filenameDeployObjServerside)
|
deploymentBytes := readDeploymentFromFile(t, filenameDeployObjServerside)
|
||||||
|
|
||||||
f, tf, _, _ := cmdtesting.NewTestFactory()
|
for _, fn := range testingOpenAPISchemaFns {
|
||||||
tf.UnstructuredClient = &fake.RESTClient{
|
f, tf, _, _ := cmdtesting.NewAPIFactory()
|
||||||
NegotiatedSerializer: unstructuredSerializer,
|
tf.UnstructuredClient = &fake.RESTClient{
|
||||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
NegotiatedSerializer: unstructuredSerializer,
|
||||||
switch p, m := req.URL.Path, req.Method; {
|
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||||
case p == deploymentPath && m == "GET":
|
switch p, m := req.URL.Path, req.Method; {
|
||||||
body := ioutil.NopCloser(bytes.NewReader(deploymentBytes))
|
case p == deploymentPath && m == "GET":
|
||||||
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: body}, nil
|
body := ioutil.NopCloser(bytes.NewReader(deploymentBytes))
|
||||||
case p == deploymentPath && m == "PATCH":
|
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: body}, nil
|
||||||
patch, err := ioutil.ReadAll(req.Body)
|
case p == deploymentPath && m == "PATCH":
|
||||||
if err != nil {
|
patch, err := ioutil.ReadAll(req.Body)
|
||||||
t.Fatal(err)
|
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
|
||||||
|
|
||||||
|
// 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{})
|
||||||
|
|
||||||
patchMap := map[string]interface{}{}
|
cmd := NewCmdApply("kubectl", f, buf, errBuf)
|
||||||
if err := json.Unmarshal(patch, &patchMap); err != nil {
|
cmd.Flags().Set("filename", filenameDeployObjClientside)
|
||||||
t.Fatal(err)
|
cmd.Flags().Set("output", "name")
|
||||||
}
|
|
||||||
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
|
cmd.Run(cmd, []string{})
|
||||||
// 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{})
|
|
||||||
|
|
||||||
cmd := NewCmdApply("kubectl", f, buf, errBuf)
|
expected := "deployment/" + deploymentName + "\n"
|
||||||
cmd.Flags().Set("filename", filenameDeployObjClientside)
|
if buf.String() != expected {
|
||||||
cmd.Flags().Set("output", "name")
|
t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expected)
|
||||||
|
}
|
||||||
cmd.Run(cmd, []string{})
|
if errBuf.String() != "" {
|
||||||
|
t.Fatalf("unexpected error output: %s", errBuf.String())
|
||||||
expected := "deployment/" + deploymentName + "\n"
|
}
|
||||||
if buf.String() != expected {
|
if !verifiedPatch {
|
||||||
t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expected)
|
t.Fatal("No server-side patch call detected")
|
||||||
}
|
}
|
||||||
if !verifiedPatch {
|
|
||||||
t.Fatal("No server-side patch call detected")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -827,53 +876,58 @@ func TestUnstructuredApply(t *testing.T) {
|
|||||||
|
|
||||||
verifiedPatch := false
|
verifiedPatch := false
|
||||||
|
|
||||||
f, tf, _, _ := cmdtesting.NewAPIFactory()
|
for _, fn := range testingOpenAPISchemaFns {
|
||||||
tf.Printer = &testPrinter{}
|
f, tf, _, _ := cmdtesting.NewAPIFactory()
|
||||||
tf.UnstructuredClient = &fake.RESTClient{
|
tf.Printer = &testPrinter{}
|
||||||
NegotiatedSerializer: unstructuredSerializer,
|
tf.UnstructuredClient = &fake.RESTClient{
|
||||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
NegotiatedSerializer: unstructuredSerializer,
|
||||||
switch p, m := req.URL.Path, req.Method; {
|
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||||
case p == path && m == "GET":
|
switch p, m := req.URL.Path, req.Method; {
|
||||||
body := ioutil.NopCloser(bytes.NewReader(curr))
|
case p == path && m == "GET":
|
||||||
return &http.Response{
|
body := ioutil.NopCloser(bytes.NewReader(curr))
|
||||||
StatusCode: 200,
|
return &http.Response{
|
||||||
Header: defaultHeader(),
|
StatusCode: 200,
|
||||||
Body: body}, nil
|
Header: defaultHeader(),
|
||||||
case p == path && m == "PATCH":
|
Body: body}, nil
|
||||||
contentType := req.Header.Get("Content-Type")
|
case p == path && m == "PATCH":
|
||||||
if contentType != "application/merge-patch+json" {
|
contentType := req.Header.Get("Content-Type")
|
||||||
t.Fatalf("Unexpected Content-Type: %s", contentType)
|
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
|
}
|
||||||
|
tf.OpenAPISchemaFunc = fn
|
||||||
|
tf.Namespace = "test"
|
||||||
|
buf := bytes.NewBuffer([]byte{})
|
||||||
|
errBuf := bytes.NewBuffer([]byte{})
|
||||||
|
|
||||||
body := ioutil.NopCloser(bytes.NewReader(curr))
|
cmd := NewCmdApply("kubectl", f, buf, errBuf)
|
||||||
return &http.Response{
|
cmd.Flags().Set("filename", filenameWidgetClientside)
|
||||||
StatusCode: 200,
|
cmd.Flags().Set("output", "name")
|
||||||
Header: defaultHeader(),
|
cmd.Run(cmd, []string{})
|
||||||
Body: body}, nil
|
|
||||||
default:
|
|
||||||
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
|
||||||
tf.Namespace = "test"
|
expected := "widget/" + name + "\n"
|
||||||
buf := bytes.NewBuffer([]byte{})
|
if buf.String() != expected {
|
||||||
errBuf := bytes.NewBuffer([]byte{})
|
t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expected)
|
||||||
|
}
|
||||||
cmd := NewCmdApply("kubectl", f, buf, errBuf)
|
if errBuf.String() != "" {
|
||||||
cmd.Flags().Set("filename", filenameWidgetClientside)
|
t.Fatalf("unexpected error output: %s", errBuf.String())
|
||||||
cmd.Flags().Set("output", "name")
|
}
|
||||||
cmd.Run(cmd, []string{})
|
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 !verifiedPatch {
|
|
||||||
t.Fatal("No server-side patch call detected")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -890,76 +944,81 @@ func TestUnstructuredIdempotentApply(t *testing.T) {
|
|||||||
|
|
||||||
verifiedPatch := false
|
verifiedPatch := false
|
||||||
|
|
||||||
f, tf, _, _ := cmdtesting.NewAPIFactory()
|
for _, fn := range testingOpenAPISchemaFns {
|
||||||
tf.Printer = &testPrinter{}
|
f, tf, _, _ := cmdtesting.NewAPIFactory()
|
||||||
tf.UnstructuredClient = &fake.RESTClient{
|
tf.Printer = &testPrinter{}
|
||||||
NegotiatedSerializer: unstructuredSerializer,
|
tf.UnstructuredClient = &fake.RESTClient{
|
||||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
NegotiatedSerializer: unstructuredSerializer,
|
||||||
switch p, m := req.URL.Path, req.Method; {
|
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||||
case p == path && m == "GET":
|
switch p, m := req.URL.Path, req.Method; {
|
||||||
body := ioutil.NopCloser(bytes.NewReader(serversideData))
|
case p == path && m == "GET":
|
||||||
return &http.Response{
|
body := ioutil.NopCloser(bytes.NewReader(serversideData))
|
||||||
StatusCode: 200,
|
return &http.Response{
|
||||||
Header: defaultHeader(),
|
StatusCode: 200,
|
||||||
Body: body}, nil
|
Header: defaultHeader(),
|
||||||
case p == path && m == "PATCH":
|
Body: body}, nil
|
||||||
// In idempotent updates, kubectl sends a logically empty
|
case p == path && m == "PATCH":
|
||||||
// request body with the PATCH request.
|
// In idempotent updates, kubectl sends a logically empty
|
||||||
// Should look like this:
|
// request body with the PATCH request.
|
||||||
// Request Body: {"metadata":{"annotations":{}}}
|
// Should look like this:
|
||||||
|
// Request Body: {"metadata":{"annotations":{}}}
|
||||||
|
|
||||||
patch, err := ioutil.ReadAll(req.Body)
|
patch, err := ioutil.ReadAll(req.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType := req.Header.Get("Content-Type")
|
||||||
|
if contentType != "application/merge-patch+json" {
|
||||||
|
t.Fatalf("Unexpected Content-Type: %s", contentType)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
annotationsMap := walkMapPath(t, patchMap, []string{"metadata", "annotations"})
|
||||||
|
if len(annotationsMap) != 0 {
|
||||||
|
t.Fatalf("Unexpected Patch. Found unexpected annotation: %s", patch)
|
||||||
|
}
|
||||||
|
|
||||||
|
verifiedPatch = true
|
||||||
|
|
||||||
|
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{})
|
||||||
|
|
||||||
contentType := req.Header.Get("Content-Type")
|
cmd := NewCmdApply("kubectl", f, buf, errBuf)
|
||||||
if contentType != "application/merge-patch+json" {
|
cmd.Flags().Set("filename", filenameWidgetClientside)
|
||||||
t.Fatalf("Unexpected Content-Type: %s", contentType)
|
cmd.Flags().Set("output", "name")
|
||||||
}
|
cmd.Run(cmd, []string{})
|
||||||
|
|
||||||
patchMap := map[string]interface{}{}
|
expected := "widget/widget\n"
|
||||||
if err := json.Unmarshal(patch, &patchMap); err != nil {
|
if buf.String() != expected {
|
||||||
t.Fatal(err)
|
t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expected)
|
||||||
}
|
}
|
||||||
if len(patchMap) != 1 {
|
if errBuf.String() != "" {
|
||||||
t.Fatalf("Unexpected Patch. Has more than 1 entry. path: %s", patch)
|
t.Fatalf("unexpected error output: %s", errBuf.String())
|
||||||
}
|
}
|
||||||
|
if !verifiedPatch {
|
||||||
annotationsMap := walkMapPath(t, patchMap, []string{"metadata", "annotations"})
|
t.Fatal("No server-side patch call detected")
|
||||||
if len(annotationsMap) != 0 {
|
}
|
||||||
t.Fatalf("Unexpected Patch. Found unexpected annotation: %s", patch)
|
|
||||||
}
|
|
||||||
|
|
||||||
verifiedPatch = true
|
|
||||||
|
|
||||||
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.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{})
|
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1093,8 +1152,6 @@ func TestForceApply(t *testing.T) {
|
|||||||
nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC)
|
nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC)
|
||||||
pathRC := "/namespaces/test/replicationcontrollers/" + nameRC
|
pathRC := "/namespaces/test/replicationcontrollers/" + nameRC
|
||||||
pathRCList := "/namespaces/test/replicationcontrollers"
|
pathRCList := "/namespaces/test/replicationcontrollers"
|
||||||
deleted := false
|
|
||||||
counts := map[string]int{}
|
|
||||||
expected := map[string]int{
|
expected := map[string]int{
|
||||||
"getOk": 9,
|
"getOk": 9,
|
||||||
"getNotFound": 1,
|
"getNotFound": 1,
|
||||||
@ -1105,84 +1162,92 @@ func TestForceApply(t *testing.T) {
|
|||||||
"post": 1,
|
"post": 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
f, tf, _, _ := cmdtesting.NewAPIFactory()
|
for _, fn := range testingOpenAPISchemaFns {
|
||||||
tf.Printer = &testPrinter{}
|
deleted := false
|
||||||
tf.UnstructuredClient = &fake.RESTClient{
|
counts := map[string]int{}
|
||||||
NegotiatedSerializer: unstructuredSerializer,
|
f, tf, _, _ := cmdtesting.NewAPIFactory()
|
||||||
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
tf.Printer = &testPrinter{}
|
||||||
switch p, m := req.URL.Path, req.Method; {
|
tf.UnstructuredClient = &fake.RESTClient{
|
||||||
case strings.HasSuffix(p, pathRC) && m == "GET":
|
NegotiatedSerializer: unstructuredSerializer,
|
||||||
if deleted {
|
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||||
counts["getNotFound"]++
|
switch p, m := req.URL.Path, req.Method; {
|
||||||
return &http.Response{StatusCode: 404, Header: defaultHeader(), Body: ioutil.NopCloser(bytes.NewReader([]byte{}))}, nil
|
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
|
||||||
}
|
}
|
||||||
counts["getOk"]++
|
}),
|
||||||
bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC))
|
}
|
||||||
return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodyRC}, nil
|
tf.OpenAPISchemaFunc = fn
|
||||||
case strings.HasSuffix(p, pathRCList) && m == "GET":
|
tf.Client = tf.UnstructuredClient
|
||||||
counts["getList"]++
|
tf.ClientConfig = &restclient.Config{}
|
||||||
rcObj := readUnstructuredFromFile(t, filenameRC)
|
tf.Namespace = "test"
|
||||||
list := &unstructured.UnstructuredList{
|
buf := bytes.NewBuffer([]byte{})
|
||||||
Object: map[string]interface{}{
|
errBuf := bytes.NewBuffer([]byte{})
|
||||||
"apiVersion": "v1",
|
|
||||||
"kind": "ReplicationControllerList",
|
cmd := NewCmdApply("kubectl", f, buf, errBuf)
|
||||||
},
|
cmd.Flags().Set("filename", filenameRC)
|
||||||
Items: []unstructured.Unstructured{*rcObj},
|
cmd.Flags().Set("output", "name")
|
||||||
}
|
cmd.Flags().Set("force", "true")
|
||||||
listBytes, err := runtime.Encode(testapi.Default.Codec(), list)
|
cmd.Run(cmd, []string{})
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
for method, exp := range expected {
|
||||||
}
|
if exp != counts[method] {
|
||||||
bodyRCList := ioutil.NopCloser(bytes.NewReader(listBytes))
|
t.Errorf("Unexpected amount of %q API calls, wanted %v got %v", method, exp, counts[method])
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}),
|
}
|
||||||
}
|
|
||||||
tf.Client = tf.UnstructuredClient
|
|
||||||
tf.ClientConfig = defaultClientConfig()
|
|
||||||
tf.Namespace = "test"
|
|
||||||
buf := bytes.NewBuffer([]byte{})
|
|
||||||
errBuf := bytes.NewBuffer([]byte{})
|
|
||||||
|
|
||||||
cmd := NewCmdApply("kubectl", f, buf, errBuf)
|
if expected := "replicationcontroller/" + nameRC + "\n"; buf.String() != expected {
|
||||||
cmd.Flags().Set("filename", filenameRC)
|
t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expected)
|
||||||
cmd.Flags().Set("output", "name")
|
}
|
||||||
cmd.Flags().Set("force", "true")
|
if errBuf.String() != "" {
|
||||||
cmd.Run(cmd, []string{})
|
t.Fatalf("unexpected error output: %s", errBuf.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])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if expected := "replicationcontroller/" + nameRC + "\n"; buf.String() != expected {
|
|
||||||
t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expected)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user