Unverified Commit de4225fa authored by Kubernetes Prow Robot's avatar Kubernetes Prow Robot Committed by GitHub

Merge pull request #74102 from…

Merge pull request #74102 from caesarxuchao/automated-cherry-pick-of-#73443-#73713-#73805-#74000-upstream-release-1.13 Automated cherry pick of #73443: update json-patch to pick up bug fixes
parents 4ef32ffb de5b047e
...@@ -1549,8 +1549,8 @@ ...@@ -1549,8 +1549,8 @@
}, },
{ {
"ImportPath": "github.com/evanphx/json-patch", "ImportPath": "github.com/evanphx/json-patch",
"Comment": "v4.0.0-3-g36442dbdb58521", "Comment": "v4.1.0-19-g5858425f75500d",
"Rev": "36442dbdb585210f8d5a1b45e67aa323c197d5c4" "Rev": "5858425f75500d40c52783dce87d085a483ce135"
}, },
{ {
"ImportPath": "github.com/exponent-io/jsonpath", "ImportPath": "github.com/exponent-io/jsonpath",
......
...@@ -129,6 +129,8 @@ func TestAddFlags(t *testing.T) { ...@@ -129,6 +129,8 @@ func TestAddFlags(t *testing.T) {
MaxMutatingRequestsInFlight: 200, MaxMutatingRequestsInFlight: 200,
RequestTimeout: time.Duration(2) * time.Minute, RequestTimeout: time.Duration(2) * time.Minute,
MinRequestTimeout: 1800, MinRequestTimeout: 1800,
JSONPatchMaxCopyBytes: int64(100 * 1024 * 1024),
MaxRequestBodyBytes: int64(100 * 1024 * 1024),
}, },
Admission: &kubeoptions.AdmissionOptions{ Admission: &kubeoptions.AdmissionOptions{
GenericAdmission: &apiserveroptions.AdmissionOptions{ GenericAdmission: &apiserveroptions.AdmissionOptions{
......
...@@ -384,7 +384,7 @@ ...@@ -384,7 +384,7 @@
}, },
{ {
"ImportPath": "github.com/evanphx/json-patch", "ImportPath": "github.com/evanphx/json-patch",
"Rev": "36442dbdb585210f8d5a1b45e67aa323c197d5c4" "Rev": "5858425f75500d40c52783dce87d085a483ce135"
}, },
{ {
"ImportPath": "github.com/ghodss/yaml", "ImportPath": "github.com/ghodss/yaml",
......
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
}, },
{ {
"ImportPath": "github.com/evanphx/json-patch", "ImportPath": "github.com/evanphx/json-patch",
"Rev": "36442dbdb585210f8d5a1b45e67aa323c197d5c4" "Rev": "5858425f75500d40c52783dce87d085a483ce135"
}, },
{ {
"ImportPath": "github.com/gogo/protobuf/proto", "ImportPath": "github.com/gogo/protobuf/proto",
......
...@@ -341,6 +341,17 @@ func NewTooManyRequestsError(message string) *StatusError { ...@@ -341,6 +341,17 @@ func NewTooManyRequestsError(message string) *StatusError {
}} }}
} }
// NewRequestEntityTooLargeError returns an error indicating that the request
// entity was too large.
func NewRequestEntityTooLargeError(message string) *StatusError {
return &StatusError{metav1.Status{
Status: metav1.StatusFailure,
Code: http.StatusRequestEntityTooLarge,
Reason: metav1.StatusReasonRequestEntityTooLarge,
Message: fmt.Sprintf("Request entity too large: %s", message),
}}
}
// NewGenericServerResponse returns a new error for server responses that are not in a recognizable form. // NewGenericServerResponse returns a new error for server responses that are not in a recognizable form.
func NewGenericServerResponse(code int, verb string, qualifiedResource schema.GroupResource, name, serverMessage string, retryAfterSeconds int, isUnexpectedResponse bool) *StatusError { func NewGenericServerResponse(code int, verb string, qualifiedResource schema.GroupResource, name, serverMessage string, retryAfterSeconds int, isUnexpectedResponse bool) *StatusError {
reason := metav1.StatusReasonUnknown reason := metav1.StatusReasonUnknown
...@@ -527,6 +538,19 @@ func IsTooManyRequests(err error) bool { ...@@ -527,6 +538,19 @@ func IsTooManyRequests(err error) bool {
return false return false
} }
// IsRequestEntityTooLargeError determines if err is an error which indicates
// the request entity is too large.
func IsRequestEntityTooLargeError(err error) bool {
if ReasonForError(err) == metav1.StatusReasonRequestEntityTooLarge {
return true
}
switch t := err.(type) {
case APIStatus:
return t.Status().Code == http.StatusRequestEntityTooLarge
}
return false
}
// IsUnexpectedServerError returns true if the server response was not in the expected API format, // IsUnexpectedServerError returns true if the server response was not in the expected API format,
// and may be the result of another HTTP actor. // and may be the result of another HTTP actor.
func IsUnexpectedServerError(err error) bool { func IsUnexpectedServerError(err error) bool {
......
...@@ -713,6 +713,10 @@ const ( ...@@ -713,6 +713,10 @@ const (
// Status code 406 // Status code 406
StatusReasonNotAcceptable StatusReason = "NotAcceptable" StatusReasonNotAcceptable StatusReason = "NotAcceptable"
// StatusReasonRequestEntityTooLarge means that the request entity is too large.
// Status code 413
StatusReasonRequestEntityTooLarge StatusReason = "RequestEntityTooLarge"
// StatusReasonUnsupportedMediaType means that the content type sent by the client is not acceptable // StatusReasonUnsupportedMediaType means that the content type sent by the client is not acceptable
// to the server - for instance, attempting to send protobuf for a resource that supports only json and yaml. // to the server - for instance, attempting to send protobuf for a resource that supports only json and yaml.
// API calls that return UnsupportedMediaType can never succeed. // API calls that return UnsupportedMediaType can never succeed.
......
...@@ -384,7 +384,7 @@ ...@@ -384,7 +384,7 @@
}, },
{ {
"ImportPath": "github.com/evanphx/json-patch", "ImportPath": "github.com/evanphx/json-patch",
"Rev": "36442dbdb585210f8d5a1b45e67aa323c197d5c4" "Rev": "5858425f75500d40c52783dce87d085a483ce135"
}, },
{ {
"ImportPath": "github.com/ghodss/yaml", "ImportPath": "github.com/ghodss/yaml",
......
...@@ -87,6 +87,10 @@ type APIGroupVersion struct { ...@@ -87,6 +87,10 @@ type APIGroupVersion struct {
// OpenAPIModels exposes the OpenAPI models to each individual handler. // OpenAPIModels exposes the OpenAPI models to each individual handler.
OpenAPIModels openapiproto.Models OpenAPIModels openapiproto.Models
// The limit on the request body size that would be accepted and decoded in a write request.
// 0 means no limit.
MaxRequestBodyBytes int64
} }
// InstallREST registers the REST handlers (storage, watch, proxy and redirect) into a restful Container. // InstallREST registers the REST handlers (storage, watch, proxy and redirect) into a restful Container.
......
...@@ -79,7 +79,7 @@ func createHandler(r rest.NamedCreater, scope RequestScope, admit admission.Inte ...@@ -79,7 +79,7 @@ func createHandler(r rest.NamedCreater, scope RequestScope, admit admission.Inte
} }
decoder := scope.Serializer.DecoderToVersion(s.Serializer, scope.HubGroupVersion) decoder := scope.Serializer.DecoderToVersion(s.Serializer, scope.HubGroupVersion)
body, err := readBody(req) body, err := limitedReadBody(req, scope.MaxRequestBodyBytes)
if err != nil { if err != nil {
scope.err(err, w, req) scope.err(err, w, req)
return return
......
...@@ -66,7 +66,7 @@ func DeleteResource(r rest.GracefulDeleter, allowsOptions bool, scope RequestSco ...@@ -66,7 +66,7 @@ func DeleteResource(r rest.GracefulDeleter, allowsOptions bool, scope RequestSco
options := &metav1.DeleteOptions{} options := &metav1.DeleteOptions{}
if allowsOptions { if allowsOptions {
body, err := readBody(req) body, err := limitedReadBody(req, scope.MaxRequestBodyBytes)
if err != nil { if err != nil {
scope.err(err, w, req) scope.err(err, w, req)
return return
...@@ -227,7 +227,7 @@ func DeleteCollection(r rest.CollectionDeleter, checkBody bool, scope RequestSco ...@@ -227,7 +227,7 @@ func DeleteCollection(r rest.CollectionDeleter, checkBody bool, scope RequestSco
options := &metav1.DeleteOptions{} options := &metav1.DeleteOptions{}
if checkBody { if checkBody {
body, err := readBody(req) body, err := limitedReadBody(req, scope.MaxRequestBodyBytes)
if err != nil { if err != nil {
scope.err(err, w, req) scope.err(err, w, req)
return return
......
...@@ -47,6 +47,11 @@ import ( ...@@ -47,6 +47,11 @@ import (
utiltrace "k8s.io/apiserver/pkg/util/trace" utiltrace "k8s.io/apiserver/pkg/util/trace"
) )
const (
// maximum number of operations a single json patch may contain.
maxJSONPatchOperations = 10000
)
// PatchResource returns a function that will handle a resource patch. // PatchResource returns a function that will handle a resource patch.
func PatchResource(r rest.Patcher, scope RequestScope, admit admission.Interface, patchTypes []string) http.HandlerFunc { func PatchResource(r rest.Patcher, scope RequestScope, admit admission.Interface, patchTypes []string) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) { return func(w http.ResponseWriter, req *http.Request) {
...@@ -88,7 +93,7 @@ func PatchResource(r rest.Patcher, scope RequestScope, admit admission.Interface ...@@ -88,7 +93,7 @@ func PatchResource(r rest.Patcher, scope RequestScope, admit admission.Interface
ctx := req.Context() ctx := req.Context()
ctx = request.WithNamespace(ctx, namespace) ctx = request.WithNamespace(ctx, namespace)
patchJS, err := readBody(req) patchJS, err := limitedReadBody(req, scope.MaxRequestBodyBytes)
if err != nil { if err != nil {
scope.err(err, w, req) scope.err(err, w, req)
return return
...@@ -287,6 +292,11 @@ func (p *jsonPatcher) applyJSPatch(versionedJS []byte) (patchedJS []byte, retErr ...@@ -287,6 +292,11 @@ func (p *jsonPatcher) applyJSPatch(versionedJS []byte) (patchedJS []byte, retErr
if err != nil { if err != nil {
return nil, errors.NewBadRequest(err.Error()) return nil, errors.NewBadRequest(err.Error())
} }
if len(patchObj) > maxJSONPatchOperations {
return nil, errors.NewRequestEntityTooLargeError(
fmt.Sprintf("The allowed maximum operations in a JSON patch is %d, got %d",
maxJSONPatchOperations, len(patchObj)))
}
patchedJS, err := patchObj.Apply(versionedJS) patchedJS, err := patchObj.Apply(versionedJS)
if err != nil { if err != nil {
return nil, errors.NewGenericServerResponse(http.StatusUnprocessableEntity, "", schema.GroupResource{}, "", err.Error(), 0, false) return nil, errors.NewGenericServerResponse(http.StatusUnprocessableEntity, "", schema.GroupResource{}, "", err.Error(), 0, false)
......
...@@ -20,6 +20,7 @@ import ( ...@@ -20,6 +20,7 @@ import (
"context" "context"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
...@@ -71,6 +72,8 @@ type RequestScope struct { ...@@ -71,6 +72,8 @@ type RequestScope struct {
// HubGroupVersion indicates what version objects read from etcd or incoming requests should be converted to for in-memory handling. // HubGroupVersion indicates what version objects read from etcd or incoming requests should be converted to for in-memory handling.
HubGroupVersion schema.GroupVersion HubGroupVersion schema.GroupVersion
MaxRequestBodyBytes int64
} }
func (scope *RequestScope) err(err error, w http.ResponseWriter, req *http.Request) { func (scope *RequestScope) err(err error, w http.ResponseWriter, req *http.Request) {
...@@ -324,9 +327,23 @@ func summarizeData(data []byte, maxLength int) string { ...@@ -324,9 +327,23 @@ func summarizeData(data []byte, maxLength int) string {
} }
} }
func readBody(req *http.Request) ([]byte, error) { func limitedReadBody(req *http.Request, limit int64) ([]byte, error) {
defer req.Body.Close() defer req.Body.Close()
return ioutil.ReadAll(req.Body) if limit <= 0 {
return ioutil.ReadAll(req.Body)
}
lr := &io.LimitedReader{
R: req.Body,
N: limit + 1,
}
data, err := ioutil.ReadAll(lr)
if err != nil {
return nil, err
}
if lr.N <= 0 {
return nil, errors.NewRequestEntityTooLargeError(fmt.Sprintf("limit is %d", limit))
}
return data, nil
} }
func parseTimeout(str string) time.Duration { func parseTimeout(str string) time.Duration {
......
...@@ -64,7 +64,7 @@ func UpdateResource(r rest.Updater, scope RequestScope, admit admission.Interfac ...@@ -64,7 +64,7 @@ func UpdateResource(r rest.Updater, scope RequestScope, admit admission.Interfac
ctx := req.Context() ctx := req.Context()
ctx = request.WithNamespace(ctx, namespace) ctx = request.WithNamespace(ctx, namespace)
body, err := readBody(req) body, err := limitedReadBody(req, scope.MaxRequestBodyBytes)
if err != nil { if err != nil {
scope.err(err, w, req) scope.err(err, w, req)
return return
......
...@@ -505,6 +505,8 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag ...@@ -505,6 +505,8 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
HubGroupVersion: schema.GroupVersion{Group: fqKindToRegister.Group, Version: runtime.APIVersionInternal}, HubGroupVersion: schema.GroupVersion{Group: fqKindToRegister.Group, Version: runtime.APIVersionInternal},
MetaGroupVersion: metav1.SchemeGroupVersion, MetaGroupVersion: metav1.SchemeGroupVersion,
MaxRequestBodyBytes: a.group.MaxRequestBodyBytes,
} }
if a.group.MetaGroupVersion != nil { if a.group.MetaGroupVersion != nil {
reqScope.MetaGroupVersion = *a.group.MetaGroupVersion reqScope.MetaGroupVersion = *a.group.MetaGroupVersion
......
...@@ -110,6 +110,7 @@ go_library( ...@@ -110,6 +110,7 @@ go_library(
"//vendor/github.com/coreos/go-systemd/daemon:go_default_library", "//vendor/github.com/coreos/go-systemd/daemon:go_default_library",
"//vendor/github.com/emicklei/go-restful:go_default_library", "//vendor/github.com/emicklei/go-restful:go_default_library",
"//vendor/github.com/emicklei/go-restful-swagger12:go_default_library", "//vendor/github.com/emicklei/go-restful-swagger12:go_default_library",
"//vendor/github.com/evanphx/json-patch:go_default_library",
"//vendor/github.com/go-openapi/spec:go_default_library", "//vendor/github.com/go-openapi/spec:go_default_library",
"//vendor/github.com/pborman/uuid:go_default_library", "//vendor/github.com/pborman/uuid:go_default_library",
"//vendor/golang.org/x/net/http2:go_default_library", "//vendor/golang.org/x/net/http2:go_default_library",
......
...@@ -26,9 +26,11 @@ import ( ...@@ -26,9 +26,11 @@ import (
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"sync/atomic"
"time" "time"
"github.com/emicklei/go-restful-swagger12" "github.com/emicklei/go-restful-swagger12"
jsonpatch "github.com/evanphx/json-patch"
"github.com/go-openapi/spec" "github.com/go-openapi/spec"
"github.com/pborman/uuid" "github.com/pborman/uuid"
"k8s.io/klog" "k8s.io/klog"
...@@ -157,6 +159,13 @@ type Config struct { ...@@ -157,6 +159,13 @@ type Config struct {
// If specified, long running requests such as watch will be allocated a random timeout between this value, and // If specified, long running requests such as watch will be allocated a random timeout between this value, and
// twice this value. Note that it is up to the request handlers to ignore or honor this timeout. In seconds. // twice this value. Note that it is up to the request handlers to ignore or honor this timeout. In seconds.
MinRequestTimeout int MinRequestTimeout int
// The limit on the total size increase all "copy" operations in a json
// patch may cause.
// This affects all places that applies json patch in the binary.
JSONPatchMaxCopyBytes int64
// The limit on the request body size that would be accepted and decoded in a write request.
// 0 means no limit.
MaxRequestBodyBytes int64
// MaxRequestsInFlight is the maximum number of parallel non-long-running requests. Every further // MaxRequestsInFlight is the maximum number of parallel non-long-running requests. Every further
// request has to wait. Applies only to non-mutating requests. // request has to wait. Applies only to non-mutating requests.
MaxRequestsInFlight int MaxRequestsInFlight int
...@@ -247,20 +256,36 @@ type AuthorizationInfo struct { ...@@ -247,20 +256,36 @@ type AuthorizationInfo struct {
// NewConfig returns a Config struct with the default values // NewConfig returns a Config struct with the default values
func NewConfig(codecs serializer.CodecFactory) *Config { func NewConfig(codecs serializer.CodecFactory) *Config {
return &Config{ return &Config{
Serializer: codecs, Serializer: codecs,
BuildHandlerChainFunc: DefaultBuildHandlerChain, BuildHandlerChainFunc: DefaultBuildHandlerChain,
HandlerChainWaitGroup: new(utilwaitgroup.SafeWaitGroup), HandlerChainWaitGroup: new(utilwaitgroup.SafeWaitGroup),
LegacyAPIGroupPrefixes: sets.NewString(DefaultLegacyAPIPrefix), LegacyAPIGroupPrefixes: sets.NewString(DefaultLegacyAPIPrefix),
DisabledPostStartHooks: sets.NewString(), DisabledPostStartHooks: sets.NewString(),
HealthzChecks: []healthz.HealthzChecker{healthz.PingHealthz, healthz.LogHealthz}, HealthzChecks: []healthz.HealthzChecker{healthz.PingHealthz, healthz.LogHealthz},
EnableIndex: true, EnableIndex: true,
EnableDiscovery: true, EnableDiscovery: true,
EnableProfiling: true, EnableProfiling: true,
EnableMetrics: true, EnableMetrics: true,
MaxRequestsInFlight: 400, MaxRequestsInFlight: 400,
MaxMutatingRequestsInFlight: 200, MaxMutatingRequestsInFlight: 200,
RequestTimeout: time.Duration(60) * time.Second, RequestTimeout: time.Duration(60) * time.Second,
MinRequestTimeout: 1800, MinRequestTimeout: 1800,
// 10MB is the recommended maximum client request size in bytes
// the etcd server should accept. See
// https://github.com/etcd-io/etcd/blob/release-3.3/etcdserver/server.go#L90.
// A request body might be encoded in json, and is converted to
// proto when persisted in etcd. Assuming the upper bound of
// the size ratio is 10:1, we set 100MB as the largest size
// increase the "copy" operations in a json patch may cause.
JSONPatchMaxCopyBytes: int64(100 * 1024 * 1024),
// 10MB is the recommended maximum client request size in bytes
// the etcd server should accept. See
// https://github.com/etcd-io/etcd/blob/release-3.3/etcdserver/server.go#L90.
// A request body might be encoded in json, and is converted to
// proto when persisted in etcd. Assuming the upper bound of
// the size ratio is 10:1, we set 100MB as the largest request
// body size to be accepted and decoded in a write request.
MaxRequestBodyBytes: int64(100 * 1024 * 1024),
EnableAPIResponseCompression: utilfeature.DefaultFeatureGate.Enabled(features.APIResponseCompression), EnableAPIResponseCompression: utilfeature.DefaultFeatureGate.Enabled(features.APIResponseCompression),
// Default to treating watch as a long-running operation // Default to treating watch as a long-running operation
...@@ -478,6 +503,20 @@ func (c completedConfig) New(name string, delegationTarget DelegationTarget) (*G ...@@ -478,6 +503,20 @@ func (c completedConfig) New(name string, delegationTarget DelegationTarget) (*G
DiscoveryGroupManager: discovery.NewRootAPIsHandler(c.DiscoveryAddresses, c.Serializer), DiscoveryGroupManager: discovery.NewRootAPIsHandler(c.DiscoveryAddresses, c.Serializer),
enableAPIResponseCompression: c.EnableAPIResponseCompression, enableAPIResponseCompression: c.EnableAPIResponseCompression,
maxRequestBodyBytes: c.MaxRequestBodyBytes,
}
for {
if c.JSONPatchMaxCopyBytes <= 0 {
break
}
existing := atomic.LoadInt64(&jsonpatch.AccumulatedCopySizeLimit)
if existing > 0 && existing < c.JSONPatchMaxCopyBytes {
break
}
if atomic.CompareAndSwapInt64(&jsonpatch.AccumulatedCopySizeLimit, existing, c.JSONPatchMaxCopyBytes) {
break
}
} }
for k, v := range delegationTarget.PostStartHooks() { for k, v := range delegationTarget.PostStartHooks() {
......
...@@ -158,6 +158,10 @@ type GenericAPIServer struct { ...@@ -158,6 +158,10 @@ type GenericAPIServer struct {
// HandlerChainWaitGroup allows you to wait for all chain handlers finish after the server shutdown. // HandlerChainWaitGroup allows you to wait for all chain handlers finish after the server shutdown.
HandlerChainWaitGroup *utilwaitgroup.SafeWaitGroup HandlerChainWaitGroup *utilwaitgroup.SafeWaitGroup
// The limit on the request body size that would be accepted and decoded in a write request.
// 0 means no limit.
maxRequestBodyBytes int64
} }
// DelegationTarget is an interface which allows for composition of API servers with top level handling that works // DelegationTarget is an interface which allows for composition of API servers with top level handling that works
...@@ -340,6 +344,7 @@ func (s *GenericAPIServer) installAPIResources(apiPrefix string, apiGroupInfo *A ...@@ -340,6 +344,7 @@ func (s *GenericAPIServer) installAPIResources(apiPrefix string, apiGroupInfo *A
apiGroupVersion.OptionsExternalVersion = apiGroupInfo.OptionsExternalVersion apiGroupVersion.OptionsExternalVersion = apiGroupInfo.OptionsExternalVersion
} }
apiGroupVersion.OpenAPIModels = openAPIGroupModels apiGroupVersion.OpenAPIModels = openAPIGroupModels
apiGroupVersion.MaxRequestBodyBytes = s.maxRequestBodyBytes
if err := apiGroupVersion.InstallREST(s.Handler.GoRestfulContainer); err != nil { if err := apiGroupVersion.InstallREST(s.Handler.GoRestfulContainer); err != nil {
return fmt.Errorf("unable to setup API %v: %v", apiGroupInfo, err) return fmt.Errorf("unable to setup API %v: %v", apiGroupInfo, err)
......
...@@ -42,7 +42,15 @@ type ServerRunOptions struct { ...@@ -42,7 +42,15 @@ type ServerRunOptions struct {
MaxMutatingRequestsInFlight int MaxMutatingRequestsInFlight int
RequestTimeout time.Duration RequestTimeout time.Duration
MinRequestTimeout int MinRequestTimeout int
TargetRAMMB int // We intentionally did not add a flag for this option. Users of the
// apiserver library can wire it to a flag.
JSONPatchMaxCopyBytes int64
// The limit on the request body size that would be accepted and
// decoded in a write request. 0 means no limit.
// We intentionally did not add a flag for this option. Users of the
// apiserver library can wire it to a flag.
MaxRequestBodyBytes int64
TargetRAMMB int
} }
func NewServerRunOptions() *ServerRunOptions { func NewServerRunOptions() *ServerRunOptions {
...@@ -52,6 +60,8 @@ func NewServerRunOptions() *ServerRunOptions { ...@@ -52,6 +60,8 @@ func NewServerRunOptions() *ServerRunOptions {
MaxMutatingRequestsInFlight: defaults.MaxMutatingRequestsInFlight, MaxMutatingRequestsInFlight: defaults.MaxMutatingRequestsInFlight,
RequestTimeout: defaults.RequestTimeout, RequestTimeout: defaults.RequestTimeout,
MinRequestTimeout: defaults.MinRequestTimeout, MinRequestTimeout: defaults.MinRequestTimeout,
JSONPatchMaxCopyBytes: defaults.JSONPatchMaxCopyBytes,
MaxRequestBodyBytes: defaults.MaxRequestBodyBytes,
} }
} }
...@@ -63,6 +73,8 @@ func (s *ServerRunOptions) ApplyTo(c *server.Config) error { ...@@ -63,6 +73,8 @@ func (s *ServerRunOptions) ApplyTo(c *server.Config) error {
c.MaxMutatingRequestsInFlight = s.MaxMutatingRequestsInFlight c.MaxMutatingRequestsInFlight = s.MaxMutatingRequestsInFlight
c.RequestTimeout = s.RequestTimeout c.RequestTimeout = s.RequestTimeout
c.MinRequestTimeout = s.MinRequestTimeout c.MinRequestTimeout = s.MinRequestTimeout
c.JSONPatchMaxCopyBytes = s.JSONPatchMaxCopyBytes
c.MaxRequestBodyBytes = s.MaxRequestBodyBytes
c.PublicAddress = s.AdvertiseAddress c.PublicAddress = s.AdvertiseAddress
return nil return nil
...@@ -107,10 +119,18 @@ func (s *ServerRunOptions) Validate() []error { ...@@ -107,10 +119,18 @@ func (s *ServerRunOptions) Validate() []error {
errors = append(errors, fmt.Errorf("--min-request-timeout can not be negative value")) errors = append(errors, fmt.Errorf("--min-request-timeout can not be negative value"))
} }
if s.JSONPatchMaxCopyBytes < 0 {
errors = append(errors, fmt.Errorf("--json-patch-max-copy-bytes can not be negative value"))
}
if s.MaxRequestBodyBytes < 0 {
errors = append(errors, fmt.Errorf("--max-resource-write-bytes can not be negative value"))
}
return errors return errors
} }
// AddFlags adds flags for a specific APIServer to the specified FlagSet // AddUniversalFlags adds flags for a specific APIServer to the specified FlagSet
func (s *ServerRunOptions) AddUniversalFlags(fs *pflag.FlagSet) { func (s *ServerRunOptions) AddUniversalFlags(fs *pflag.FlagSet) {
// Note: the weird ""+ in below lines seems to be the only way to get gofmt to // Note: the weird ""+ in below lines seems to be the only way to get gofmt to
// arrange these text blocks sensibly. Grrr. // arrange these text blocks sensibly. Grrr.
......
...@@ -40,6 +40,8 @@ func TestServerRunOptionsValidate(t *testing.T) { ...@@ -40,6 +40,8 @@ func TestServerRunOptionsValidate(t *testing.T) {
MaxMutatingRequestsInFlight: 200, MaxMutatingRequestsInFlight: 200,
RequestTimeout: time.Duration(2) * time.Minute, RequestTimeout: time.Duration(2) * time.Minute,
MinRequestTimeout: 1800, MinRequestTimeout: 1800,
JSONPatchMaxCopyBytes: 10 * 1024 * 1024,
MaxRequestBodyBytes: 10 * 1024 * 1024,
TargetRAMMB: -65536, TargetRAMMB: -65536,
}, },
expectErr: "--target-ram-mb can not be negative value", expectErr: "--target-ram-mb can not be negative value",
...@@ -53,6 +55,8 @@ func TestServerRunOptionsValidate(t *testing.T) { ...@@ -53,6 +55,8 @@ func TestServerRunOptionsValidate(t *testing.T) {
MaxMutatingRequestsInFlight: 200, MaxMutatingRequestsInFlight: 200,
RequestTimeout: time.Duration(2) * time.Minute, RequestTimeout: time.Duration(2) * time.Minute,
MinRequestTimeout: 1800, MinRequestTimeout: 1800,
JSONPatchMaxCopyBytes: 10 * 1024 * 1024,
MaxRequestBodyBytes: 10 * 1024 * 1024,
TargetRAMMB: 65536, TargetRAMMB: 65536,
}, },
expectErr: "--max-requests-inflight can not be negative value", expectErr: "--max-requests-inflight can not be negative value",
...@@ -66,6 +70,8 @@ func TestServerRunOptionsValidate(t *testing.T) { ...@@ -66,6 +70,8 @@ func TestServerRunOptionsValidate(t *testing.T) {
MaxMutatingRequestsInFlight: -200, MaxMutatingRequestsInFlight: -200,
RequestTimeout: time.Duration(2) * time.Minute, RequestTimeout: time.Duration(2) * time.Minute,
MinRequestTimeout: 1800, MinRequestTimeout: 1800,
JSONPatchMaxCopyBytes: 10 * 1024 * 1024,
MaxRequestBodyBytes: 10 * 1024 * 1024,
TargetRAMMB: 65536, TargetRAMMB: 65536,
}, },
expectErr: "--max-mutating-requests-inflight can not be negative value", expectErr: "--max-mutating-requests-inflight can not be negative value",
...@@ -79,6 +85,8 @@ func TestServerRunOptionsValidate(t *testing.T) { ...@@ -79,6 +85,8 @@ func TestServerRunOptionsValidate(t *testing.T) {
MaxMutatingRequestsInFlight: 200, MaxMutatingRequestsInFlight: 200,
RequestTimeout: -time.Duration(2) * time.Minute, RequestTimeout: -time.Duration(2) * time.Minute,
MinRequestTimeout: 1800, MinRequestTimeout: 1800,
JSONPatchMaxCopyBytes: 10 * 1024 * 1024,
MaxRequestBodyBytes: 10 * 1024 * 1024,
TargetRAMMB: 65536, TargetRAMMB: 65536,
}, },
expectErr: "--request-timeout can not be negative value", expectErr: "--request-timeout can not be negative value",
...@@ -92,11 +100,43 @@ func TestServerRunOptionsValidate(t *testing.T) { ...@@ -92,11 +100,43 @@ func TestServerRunOptionsValidate(t *testing.T) {
MaxMutatingRequestsInFlight: 200, MaxMutatingRequestsInFlight: 200,
RequestTimeout: time.Duration(2) * time.Minute, RequestTimeout: time.Duration(2) * time.Minute,
MinRequestTimeout: -1800, MinRequestTimeout: -1800,
JSONPatchMaxCopyBytes: 10 * 1024 * 1024,
MaxRequestBodyBytes: 10 * 1024 * 1024,
TargetRAMMB: 65536, TargetRAMMB: 65536,
}, },
expectErr: "--min-request-timeout can not be negative value", expectErr: "--min-request-timeout can not be negative value",
}, },
{ {
name: "Test when JSONPatchMaxCopyBytes is negative value",
testOptions: &ServerRunOptions{
AdvertiseAddress: net.ParseIP("192.168.10.10"),
CorsAllowedOriginList: []string{"10.10.10.100", "10.10.10.200"},
MaxRequestsInFlight: 400,
MaxMutatingRequestsInFlight: 200,
RequestTimeout: time.Duration(2) * time.Minute,
MinRequestTimeout: 1800,
JSONPatchMaxCopyBytes: -10 * 1024 * 1024,
MaxRequestBodyBytes: 10 * 1024 * 1024,
TargetRAMMB: 65536,
},
expectErr: "--json-patch-max-copy-bytes can not be negative value",
},
{
name: "Test when MaxRequestBodyBytes is negative value",
testOptions: &ServerRunOptions{
AdvertiseAddress: net.ParseIP("192.168.10.10"),
CorsAllowedOriginList: []string{"10.10.10.100", "10.10.10.200"},
MaxRequestsInFlight: 400,
MaxMutatingRequestsInFlight: 200,
RequestTimeout: time.Duration(2) * time.Minute,
MinRequestTimeout: 1800,
JSONPatchMaxCopyBytes: 10 * 1024 * 1024,
MaxRequestBodyBytes: -10 * 1024 * 1024,
TargetRAMMB: 65536,
},
expectErr: "--max-resource-write-bytes can not be negative value",
},
{
name: "Test when ServerRunOptions is valid", name: "Test when ServerRunOptions is valid",
testOptions: &ServerRunOptions{ testOptions: &ServerRunOptions{
AdvertiseAddress: net.ParseIP("192.168.10.10"), AdvertiseAddress: net.ParseIP("192.168.10.10"),
...@@ -105,6 +145,8 @@ func TestServerRunOptionsValidate(t *testing.T) { ...@@ -105,6 +145,8 @@ func TestServerRunOptionsValidate(t *testing.T) {
MaxMutatingRequestsInFlight: 200, MaxMutatingRequestsInFlight: 200,
RequestTimeout: time.Duration(2) * time.Minute, RequestTimeout: time.Duration(2) * time.Minute,
MinRequestTimeout: 1800, MinRequestTimeout: 1800,
JSONPatchMaxCopyBytes: 10 * 1024 * 1024,
MaxRequestBodyBytes: 10 * 1024 * 1024,
TargetRAMMB: 65536, TargetRAMMB: 65536,
}, },
}, },
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
}, },
{ {
"ImportPath": "github.com/evanphx/json-patch", "ImportPath": "github.com/evanphx/json-patch",
"Rev": "36442dbdb585210f8d5a1b45e67aa323c197d5c4" "Rev": "5858425f75500d40c52783dce87d085a483ce135"
}, },
{ {
"ImportPath": "github.com/gogo/protobuf/proto", "ImportPath": "github.com/gogo/protobuf/proto",
......
...@@ -56,7 +56,7 @@ ...@@ -56,7 +56,7 @@
}, },
{ {
"ImportPath": "github.com/evanphx/json-patch", "ImportPath": "github.com/evanphx/json-patch",
"Rev": "36442dbdb585210f8d5a1b45e67aa323c197d5c4" "Rev": "5858425f75500d40c52783dce87d085a483ce135"
}, },
{ {
"ImportPath": "github.com/gogo/protobuf/proto", "ImportPath": "github.com/gogo/protobuf/proto",
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
}, },
{ {
"ImportPath": "github.com/evanphx/json-patch", "ImportPath": "github.com/evanphx/json-patch",
"Rev": "36442dbdb585210f8d5a1b45e67aa323c197d5c4" "Rev": "5858425f75500d40c52783dce87d085a483ce135"
}, },
{ {
"ImportPath": "github.com/gogo/protobuf/proto", "ImportPath": "github.com/gogo/protobuf/proto",
......
...@@ -108,7 +108,7 @@ ...@@ -108,7 +108,7 @@
}, },
{ {
"ImportPath": "github.com/evanphx/json-patch", "ImportPath": "github.com/evanphx/json-patch",
"Rev": "36442dbdb585210f8d5a1b45e67aa323c197d5c4" "Rev": "5858425f75500d40c52783dce87d085a483ce135"
}, },
{ {
"ImportPath": "github.com/go-openapi/jsonpointer", "ImportPath": "github.com/go-openapi/jsonpointer",
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
}, },
{ {
"ImportPath": "github.com/evanphx/json-patch", "ImportPath": "github.com/evanphx/json-patch",
"Rev": "36442dbdb585210f8d5a1b45e67aa323c197d5c4" "Rev": "5858425f75500d40c52783dce87d085a483ce135"
}, },
{ {
"ImportPath": "github.com/gogo/protobuf/proto", "ImportPath": "github.com/gogo/protobuf/proto",
......
...@@ -100,7 +100,7 @@ ...@@ -100,7 +100,7 @@
}, },
{ {
"ImportPath": "github.com/evanphx/json-patch", "ImportPath": "github.com/evanphx/json-patch",
"Rev": "36442dbdb585210f8d5a1b45e67aa323c197d5c4" "Rev": "5858425f75500d40c52783dce87d085a483ce135"
}, },
{ {
"ImportPath": "github.com/go-openapi/jsonpointer", "ImportPath": "github.com/go-openapi/jsonpointer",
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
"Deps": [ "Deps": [
{ {
"ImportPath": "github.com/evanphx/json-patch", "ImportPath": "github.com/evanphx/json-patch",
"Rev": "36442dbdb585210f8d5a1b45e67aa323c197d5c4" "Rev": "5858425f75500d40c52783dce87d085a483ce135"
}, },
{ {
"ImportPath": "github.com/gogo/protobuf/proto", "ImportPath": "github.com/gogo/protobuf/proto",
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
}, },
{ {
"ImportPath": "github.com/evanphx/json-patch", "ImportPath": "github.com/evanphx/json-patch",
"Rev": "36442dbdb585210f8d5a1b45e67aa323c197d5c4" "Rev": "5858425f75500d40c52783dce87d085a483ce135"
}, },
{ {
"ImportPath": "github.com/gogo/protobuf/proto", "ImportPath": "github.com/gogo/protobuf/proto",
......
...@@ -11,6 +11,8 @@ go_test( ...@@ -11,6 +11,8 @@ go_test(
srcs = [ srcs = [
"apiserver_test.go", "apiserver_test.go",
"main_test.go", "main_test.go",
"max_json_patch_operations_test.go",
"max_request_body_bytes_test.go",
"patch_test.go", "patch_test.go",
"print_test.go", "print_test.go",
], ],
...@@ -19,6 +21,7 @@ go_test( ...@@ -19,6 +21,7 @@ go_test(
"integration", "integration",
], ],
deps = [ deps = [
"//cmd/kube-apiserver/app/options:go_default_library",
"//pkg/api/legacyscheme:go_default_library", "//pkg/api/legacyscheme:go_default_library",
"//pkg/api/testapi:go_default_library", "//pkg/api/testapi:go_default_library",
"//pkg/apis/core:go_default_library", "//pkg/apis/core:go_default_library",
......
/*
Copyright 2018 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 apiserver
import (
"fmt"
"strings"
"testing"
"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/kubernetes/cmd/kube-apiserver/app/options"
"k8s.io/kubernetes/test/integration/framework"
)
// Tests that the apiserver limits the number of operations in a json patch.
func TestMaxJSONPatchOperations(t *testing.T) {
stopCh := make(chan struct{})
defer close(stopCh)
clientSet, _ := framework.StartTestServer(t, stopCh, framework.TestServerSetup{
ModifyServerRunOptions: func(opts *options.ServerRunOptions) {
opts.GenericServerRunOptions.MaxRequestBodyBytes = 1024 * 1024
},
})
p := `{"op":"add","path":"/x","value":"y"}`
// maxJSONPatchOperations = 10000
hugePatch := []byte("[" + strings.Repeat(p+",", 10000) + p + "]")
c := clientSet.CoreV1().RESTClient()
// Create a secret so we can patch it.
secret := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
},
}
_, err := clientSet.CoreV1().Secrets("default").Create(secret)
if err != nil {
t.Fatal(err)
}
err = c.Patch(types.JSONPatchType).AbsPath(fmt.Sprintf("/api/v1/namespaces/default/secrets/test")).
Body(hugePatch).Do().Error()
if err == nil {
t.Fatalf("unexpected no error")
}
if !errors.IsRequestEntityTooLargeError(err) {
t.Errorf("expected requested entity too large err, got %v", err)
}
if !strings.Contains(err.Error(), "The allowed maximum operations in a JSON patch is") {
t.Errorf("expected the error message to be about maximum operations, got %v", err)
}
}
/*
Copyright 2018 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 apiserver
import (
"fmt"
"strings"
"testing"
"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/kubernetes/cmd/kube-apiserver/app/options"
"k8s.io/kubernetes/test/integration/framework"
)
// Tests that the apiserver limits the resource size in write operations.
func TestMaxResourceSize(t *testing.T) {
stopCh := make(chan struct{})
defer close(stopCh)
clientSet, _ := framework.StartTestServer(t, stopCh, framework.TestServerSetup{
ModifyServerRunOptions: func(opts *options.ServerRunOptions) {
opts.GenericServerRunOptions.MaxRequestBodyBytes = 1024 * 1024
},
})
hugeData := []byte(strings.Repeat("x", 1024*1024+1))
c := clientSet.CoreV1().RESTClient()
t.Run("Create should limit the request body size", func(t *testing.T) {
err := c.Post().AbsPath(fmt.Sprintf("/api/v1/namespaces/default/pods")).
Body(hugeData).Do().Error()
if err == nil {
t.Fatalf("unexpected no error")
}
if !errors.IsRequestEntityTooLargeError(err) {
t.Errorf("expected requested entity too large err, got %v", err)
}
})
// Create a secret so we can update/patch/delete it.
secret := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
},
}
_, err := clientSet.CoreV1().Secrets("default").Create(secret)
if err != nil {
t.Fatal(err)
}
t.Run("Update should limit the request body size", func(t *testing.T) {
err = c.Put().AbsPath(fmt.Sprintf("/api/v1/namespaces/default/secrets/test")).
Body(hugeData).Do().Error()
if err == nil {
t.Fatalf("unexpected no error")
}
if !errors.IsRequestEntityTooLargeError(err) {
t.Errorf("expected requested entity too large err, got %v", err)
}
})
t.Run("Patch should limit the request body size", func(t *testing.T) {
err = c.Patch(types.JSONPatchType).AbsPath(fmt.Sprintf("/api/v1/namespaces/default/secrets/test")).
Body(hugeData).Do().Error()
if err == nil {
t.Fatalf("unexpected no error")
}
if !errors.IsRequestEntityTooLargeError(err) {
t.Errorf("expected requested entity too large err, got %v", err)
}
})
t.Run("Delete should limit the request body size", func(t *testing.T) {
err = c.Delete().AbsPath(fmt.Sprintf("/api/v1/namespaces/default/secrets/test")).
Body(hugeData).Do().Error()
if err == nil {
t.Fatalf("unexpected no error")
}
if !errors.IsRequestEntityTooLargeError(err) {
t.Errorf("expected requested entity too large err, got %v", err)
}
})
}
...@@ -11,7 +11,6 @@ go_test( ...@@ -11,7 +11,6 @@ go_test(
srcs = [ srcs = [
"apiserver_test.go", "apiserver_test.go",
"main_test.go", "main_test.go",
"setup_test.go",
"webhook_test.go", "webhook_test.go",
], ],
tags = ["integration"], tags = ["integration"],
...@@ -43,7 +42,6 @@ go_test( ...@@ -43,7 +42,6 @@ go_test(
"//staging/src/k8s.io/sample-apiserver/pkg/apis/wardle/v1beta1:go_default_library", "//staging/src/k8s.io/sample-apiserver/pkg/apis/wardle/v1beta1:go_default_library",
"//staging/src/k8s.io/sample-apiserver/pkg/cmd/server:go_default_library", "//staging/src/k8s.io/sample-apiserver/pkg/cmd/server:go_default_library",
"//test/integration/framework:go_default_library", "//test/integration/framework:go_default_library",
"//vendor/github.com/pborman/uuid:go_default_library",
"//vendor/github.com/stretchr/testify/assert:go_default_library", "//vendor/github.com/stretchr/testify/assert:go_default_library",
], ],
) )
......
...@@ -30,6 +30,7 @@ import ( ...@@ -30,6 +30,7 @@ import (
"k8s.io/kubernetes/cmd/kube-apiserver/app/options" "k8s.io/kubernetes/cmd/kube-apiserver/app/options"
"k8s.io/kubernetes/pkg/master" "k8s.io/kubernetes/pkg/master"
"k8s.io/kubernetes/pkg/master/reconcilers" "k8s.io/kubernetes/pkg/master/reconcilers"
"k8s.io/kubernetes/test/integration/framework"
) )
func TestWebhookLoopback(t *testing.T) { func TestWebhookLoopback(t *testing.T) {
...@@ -40,7 +41,7 @@ func TestWebhookLoopback(t *testing.T) { ...@@ -40,7 +41,7 @@ func TestWebhookLoopback(t *testing.T) {
called := int32(0) called := int32(0)
client, _ := startTestServer(t, stopCh, TestServerSetup{ client, _ := framework.StartTestServer(t, stopCh, framework.TestServerSetup{
ModifyServerRunOptions: func(opts *options.ServerRunOptions) { ModifyServerRunOptions: func(opts *options.ServerRunOptions) {
}, },
ModifyServerConfig: func(config *master.Config) { ModifyServerConfig: func(config *master.Config) {
......
...@@ -12,6 +12,7 @@ go_library( ...@@ -12,6 +12,7 @@ go_library(
"master_utils.go", "master_utils.go",
"perf_utils.go", "perf_utils.go",
"serializer.go", "serializer.go",
"test_server.go",
"util.go", "util.go",
], ],
data = [ data = [
...@@ -19,6 +20,8 @@ go_library( ...@@ -19,6 +20,8 @@ go_library(
], ],
importpath = "k8s.io/kubernetes/test/integration/framework", importpath = "k8s.io/kubernetes/test/integration/framework",
deps = [ deps = [
"//cmd/kube-apiserver/app:go_default_library",
"//cmd/kube-apiserver/app/options:go_default_library",
"//pkg/api/legacyscheme:go_default_library", "//pkg/api/legacyscheme:go_default_library",
"//pkg/api/testapi:go_default_library", "//pkg/api/testapi:go_default_library",
"//pkg/apis/batch:go_default_library", "//pkg/apis/batch:go_default_library",
...@@ -57,6 +60,7 @@ go_library( ...@@ -57,6 +60,7 @@ go_library(
"//staging/src/k8s.io/client-go/informers:go_default_library", "//staging/src/k8s.io/client-go/informers:go_default_library",
"//staging/src/k8s.io/client-go/kubernetes:go_default_library", "//staging/src/k8s.io/client-go/kubernetes:go_default_library",
"//staging/src/k8s.io/client-go/rest:go_default_library", "//staging/src/k8s.io/client-go/rest:go_default_library",
"//staging/src/k8s.io/client-go/util/cert:go_default_library",
"//test/e2e/framework:go_default_library", "//test/e2e/framework:go_default_library",
"//test/utils:go_default_library", "//test/utils:go_default_library",
"//vendor/github.com/go-openapi/spec:go_default_library", "//vendor/github.com/go-openapi/spec:go_default_library",
......
...@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and ...@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package apiserver package framework
import ( import (
"io/ioutil" "io/ioutil"
...@@ -36,7 +36,6 @@ import ( ...@@ -36,7 +36,6 @@ import (
"k8s.io/kubernetes/cmd/kube-apiserver/app" "k8s.io/kubernetes/cmd/kube-apiserver/app"
"k8s.io/kubernetes/cmd/kube-apiserver/app/options" "k8s.io/kubernetes/cmd/kube-apiserver/app/options"
"k8s.io/kubernetes/pkg/master" "k8s.io/kubernetes/pkg/master"
"k8s.io/kubernetes/test/integration/framework"
) )
type TestServerSetup struct { type TestServerSetup struct {
...@@ -45,7 +44,7 @@ type TestServerSetup struct { ...@@ -45,7 +44,7 @@ type TestServerSetup struct {
} }
// startTestServer runs a kube-apiserver, optionally calling out to the setup.ModifyServerRunOptions and setup.ModifyServerConfig functions // startTestServer runs a kube-apiserver, optionally calling out to the setup.ModifyServerRunOptions and setup.ModifyServerConfig functions
func startTestServer(t *testing.T, stopCh <-chan struct{}, setup TestServerSetup) (client.Interface, *rest.Config) { func StartTestServer(t *testing.T, stopCh <-chan struct{}, setup TestServerSetup) (client.Interface, *rest.Config) {
certDir, _ := ioutil.TempDir("", "test-integration-"+t.Name()) certDir, _ := ioutil.TempDir("", "test-integration-"+t.Name())
go func() { go func() {
<-stopCh <-stopCh
...@@ -89,7 +88,7 @@ func startTestServer(t *testing.T, stopCh <-chan struct{}, setup TestServerSetup ...@@ -89,7 +88,7 @@ func startTestServer(t *testing.T, stopCh <-chan struct{}, setup TestServerSetup
kubeAPIServerOptions.SecureServing.ServerCert.CertDirectory = certDir kubeAPIServerOptions.SecureServing.ServerCert.CertDirectory = certDir
kubeAPIServerOptions.InsecureServing.BindPort = 0 kubeAPIServerOptions.InsecureServing.BindPort = 0
kubeAPIServerOptions.Etcd.StorageConfig.Prefix = path.Join("/", uuid.New(), "registry") kubeAPIServerOptions.Etcd.StorageConfig.Prefix = path.Join("/", uuid.New(), "registry")
kubeAPIServerOptions.Etcd.StorageConfig.ServerList = []string{framework.GetEtcdURL()} kubeAPIServerOptions.Etcd.StorageConfig.ServerList = []string{GetEtcdURL()}
kubeAPIServerOptions.ServiceClusterIPRange = *defaultServiceClusterIPRange kubeAPIServerOptions.ServiceClusterIPRange = *defaultServiceClusterIPRange
kubeAPIServerOptions.Authentication.RequestHeader.UsernameHeaders = []string{"X-Remote-User"} kubeAPIServerOptions.Authentication.RequestHeader.UsernameHeaders = []string{"X-Remote-User"}
kubeAPIServerOptions.Authentication.RequestHeader.GroupHeaders = []string{"X-Remote-Group"} kubeAPIServerOptions.Authentication.RequestHeader.GroupHeaders = []string{"X-Remote-Group"}
......
...@@ -3,6 +3,7 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library") ...@@ -3,6 +3,7 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library( go_library(
name = "go_default_library", name = "go_default_library",
srcs = [ srcs = [
"errors.go",
"merge.go", "merge.go",
"patch.go", "patch.go",
], ],
......
...@@ -25,6 +25,19 @@ go get -u github.com/evanphx/json-patch ...@@ -25,6 +25,19 @@ go get -u github.com/evanphx/json-patch
* [Comparing JSON documents](#comparing-json-documents) * [Comparing JSON documents](#comparing-json-documents)
* [Combine merge patches](#combine-merge-patches) * [Combine merge patches](#combine-merge-patches)
# Configuration
* There is a global configuration variable `jsonpatch.SupportNegativeIndices`.
This defaults to `true` and enables the non-standard practice of allowing
negative indices to mean indices starting at the end of an array. This
functionality can be disabled by setting `jsonpatch.SupportNegativeIndices =
false`.
* There is a global configuration variable `jsonpatch.AccumulatedCopySizeLimit`,
which limits the total size increase in bytes caused by "copy" operations in a
patch. It defaults to 0, which means there is no limit.
## Create and apply a merge patch ## Create and apply a merge patch
Given both an original JSON document and a modified JSON document, you can create Given both an original JSON document and a modified JSON document, you can create
a [Merge Patch](https://tools.ietf.org/html/rfc7396) document. a [Merge Patch](https://tools.ietf.org/html/rfc7396) document.
......
package jsonpatch
import "fmt"
// AccumulatedCopySizeError is an error type returned when the accumulated size
// increase caused by copy operations in a patch operation has exceeded the
// limit.
type AccumulatedCopySizeError struct {
limit int64
accumulated int64
}
// NewAccumulatedCopySizeError returns an AccumulatedCopySizeError.
func NewAccumulatedCopySizeError(l, a int64) *AccumulatedCopySizeError {
return &AccumulatedCopySizeError{limit: l, accumulated: a}
}
// Error implements the error interface.
func (a *AccumulatedCopySizeError) Error() string {
return fmt.Sprintf("Unable to complete the copy, the accumulated size increase of copy is %d, exceeding the limit %d", a.accumulated, a.limit)
}
// ArraySizeError is an error type returned when the array size has exceeded
// the limit.
type ArraySizeError struct {
limit int
size int
}
// NewArraySizeError returns an ArraySizeError.
func NewArraySizeError(l, s int) *ArraySizeError {
return &ArraySizeError{limit: l, size: s}
}
// Error implements the error interface.
func (a *ArraySizeError) Error() string {
return fmt.Sprintf("Unable to create array of size %d, limit is %d", a.size, a.limit)
}
...@@ -14,6 +14,16 @@ const ( ...@@ -14,6 +14,16 @@ const (
eAry eAry
) )
var (
// SupportNegativeIndices decides whether to support non-standard practice of
// allowing negative indices to mean indices starting at the end of an array.
// Default to true.
SupportNegativeIndices bool = true
// AccumulatedCopySizeLimit limits the total size increase in bytes caused by
// "copy" operations in a patch.
AccumulatedCopySizeLimit int64 = 0
)
type lazyNode struct { type lazyNode struct {
raw *json.RawMessage raw *json.RawMessage
doc partialDoc doc partialDoc
...@@ -61,6 +71,20 @@ func (n *lazyNode) UnmarshalJSON(data []byte) error { ...@@ -61,6 +71,20 @@ func (n *lazyNode) UnmarshalJSON(data []byte) error {
return nil return nil
} }
func deepCopy(src *lazyNode) (*lazyNode, int, error) {
if src == nil {
return nil, 0, nil
}
a, err := src.MarshalJSON()
if err != nil {
return nil, 0, err
}
sz := len(a)
ra := make(json.RawMessage, sz)
copy(ra, a)
return newLazyNode(&ra), sz, nil
}
func (n *lazyNode) intoDoc() (*partialDoc, error) { func (n *lazyNode) intoDoc() (*partialDoc, error) {
if n.which == eDoc { if n.which == eDoc {
return &n.doc, nil return &n.doc, nil
...@@ -342,35 +366,14 @@ func (d *partialDoc) remove(key string) error { ...@@ -342,35 +366,14 @@ func (d *partialDoc) remove(key string) error {
return nil return nil
} }
// set should only be used to implement the "replace" operation, so "key" must
// be an already existing index in "d".
func (d *partialArray) set(key string, val *lazyNode) error { func (d *partialArray) set(key string, val *lazyNode) error {
if key == "-" {
*d = append(*d, val)
return nil
}
idx, err := strconv.Atoi(key) idx, err := strconv.Atoi(key)
if err != nil { if err != nil {
return err return err
} }
(*d)[idx] = val
sz := len(*d)
if idx+1 > sz {
sz = idx + 1
}
ary := make([]*lazyNode, sz)
cur := *d
copy(ary, cur)
if idx >= len(ary) {
return fmt.Errorf("Unable to access invalid index: %d", idx)
}
ary[idx] = val
*d = ary
return nil return nil
} }
...@@ -385,17 +388,26 @@ func (d *partialArray) add(key string, val *lazyNode) error { ...@@ -385,17 +388,26 @@ func (d *partialArray) add(key string, val *lazyNode) error {
return err return err
} }
ary := make([]*lazyNode, len(*d)+1) sz := len(*d) + 1
ary := make([]*lazyNode, sz)
cur := *d cur := *d
if idx < -len(ary) || idx >= len(ary) { if idx >= len(ary) {
return fmt.Errorf("Unable to access invalid index: %d", idx) return fmt.Errorf("Unable to access invalid index: %d", idx)
} }
if idx < 0 { if SupportNegativeIndices {
idx += len(ary) if idx < -len(ary) {
return fmt.Errorf("Unable to access invalid index: %d", idx)
}
if idx < 0 {
idx += len(ary)
}
} }
copy(ary[0:idx], cur[0:idx]) copy(ary[0:idx], cur[0:idx])
ary[idx] = val ary[idx] = val
copy(ary[idx+1:], cur[idx:]) copy(ary[idx+1:], cur[idx:])
...@@ -426,11 +438,18 @@ func (d *partialArray) remove(key string) error { ...@@ -426,11 +438,18 @@ func (d *partialArray) remove(key string) error {
cur := *d cur := *d
if idx < -len(cur) || idx >= len(cur) { if idx >= len(cur) {
return fmt.Errorf("Unable to remove invalid index: %d", idx) return fmt.Errorf("Unable to access invalid index: %d", idx)
} }
if idx < 0 {
idx += len(cur) if SupportNegativeIndices {
if idx < -len(cur) {
return fmt.Errorf("Unable to access invalid index: %d", idx)
}
if idx < 0 {
idx += len(cur)
}
} }
ary := make([]*lazyNode, len(cur)-1) ary := make([]*lazyNode, len(cur)-1)
...@@ -511,7 +530,7 @@ func (p Patch) move(doc *container, op operation) error { ...@@ -511,7 +530,7 @@ func (p Patch) move(doc *container, op operation) error {
return fmt.Errorf("jsonpatch move operation does not apply: doc is missing destination path: %s", path) return fmt.Errorf("jsonpatch move operation does not apply: doc is missing destination path: %s", path)
} }
return con.set(key, val) return con.add(key, val)
} }
func (p Patch) test(doc *container, op operation) error { func (p Patch) test(doc *container, op operation) error {
...@@ -545,7 +564,7 @@ func (p Patch) test(doc *container, op operation) error { ...@@ -545,7 +564,7 @@ func (p Patch) test(doc *container, op operation) error {
return fmt.Errorf("Testing value %s failed", path) return fmt.Errorf("Testing value %s failed", path)
} }
func (p Patch) copy(doc *container, op operation) error { func (p Patch) copy(doc *container, op operation, accumulatedCopySize *int64) error {
from := op.from() from := op.from()
con, key := findObject(doc, from) con, key := findObject(doc, from)
...@@ -567,7 +586,16 @@ func (p Patch) copy(doc *container, op operation) error { ...@@ -567,7 +586,16 @@ func (p Patch) copy(doc *container, op operation) error {
return fmt.Errorf("jsonpatch copy operation does not apply: doc is missing destination path: %s", path) return fmt.Errorf("jsonpatch copy operation does not apply: doc is missing destination path: %s", path)
} }
return con.set(key, val) valCopy, sz, err := deepCopy(val)
if err != nil {
return err
}
(*accumulatedCopySize) += int64(sz)
if AccumulatedCopySizeLimit > 0 && *accumulatedCopySize > AccumulatedCopySizeLimit {
return NewAccumulatedCopySizeError(AccumulatedCopySizeLimit, *accumulatedCopySize)
}
return con.add(key, valCopy)
} }
// Equal indicates if 2 JSON documents have the same structural equality. // Equal indicates if 2 JSON documents have the same structural equality.
...@@ -620,6 +648,8 @@ func (p Patch) ApplyIndent(doc []byte, indent string) ([]byte, error) { ...@@ -620,6 +648,8 @@ func (p Patch) ApplyIndent(doc []byte, indent string) ([]byte, error) {
err = nil err = nil
var accumulatedCopySize int64
for _, op := range p { for _, op := range p {
switch op.kind() { switch op.kind() {
case "add": case "add":
...@@ -633,7 +663,7 @@ func (p Patch) ApplyIndent(doc []byte, indent string) ([]byte, error) { ...@@ -633,7 +663,7 @@ func (p Patch) ApplyIndent(doc []byte, indent string) ([]byte, error) {
case "test": case "test":
err = p.test(&pd, op) err = p.test(&pd, op)
case "copy": case "copy":
err = p.copy(&pd, op) err = p.copy(&pd, op, &accumulatedCopySize)
default: default:
err = fmt.Errorf("Unexpected kind: %s", op.kind()) err = fmt.Errorf("Unexpected kind: %s", op.kind())
} }
......
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