mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-07 11:13:48 +00:00
Support application/apply-patch+cbor in patch requests.
This commit is contained in:
parent
f01e0d64db
commit
37ed906a33
@ -325,7 +325,10 @@ func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
supportedTypes := []string{
|
||||
string(types.JSONPatchType),
|
||||
string(types.MergePatchType),
|
||||
string(types.ApplyPatchType),
|
||||
string(types.ApplyYAMLPatchType),
|
||||
}
|
||||
if utilfeature.TestOnlyFeatureGate.Enabled(features.TestOnlyCBORServingAndStorage) {
|
||||
supportedTypes = append(supportedTypes, string(types.ApplyCBORPatchType))
|
||||
}
|
||||
|
||||
var handlerFunc http.HandlerFunc
|
||||
|
@ -186,15 +186,16 @@ func ValidateUpdateOptions(options *metav1.UpdateOptions) field.ErrorList {
|
||||
|
||||
func ValidatePatchOptions(options *metav1.PatchOptions, patchType types.PatchType) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
if patchType != types.ApplyPatchType {
|
||||
if options.Force != nil {
|
||||
allErrs = append(allErrs, field.Forbidden(field.NewPath("force"), "may not be specified for non-apply patch"))
|
||||
}
|
||||
} else {
|
||||
switch patchType {
|
||||
case types.ApplyYAMLPatchType, types.ApplyCBORPatchType:
|
||||
if options.FieldManager == "" {
|
||||
// This field is defaulted to "kubectl" by kubectl, but HAS TO be explicitly set by controllers.
|
||||
allErrs = append(allErrs, field.Required(field.NewPath("fieldManager"), "is required for apply patch"))
|
||||
}
|
||||
default:
|
||||
if options.Force != nil {
|
||||
allErrs = append(allErrs, field.Forbidden(field.NewPath("force"), "may not be specified for non-apply patch"))
|
||||
}
|
||||
}
|
||||
allErrs = append(allErrs, ValidateFieldManager(options.FieldManager, field.NewPath("fieldManager"))...)
|
||||
allErrs = append(allErrs, ValidateDryRun(field.NewPath("dryRun"), options.DryRun)...)
|
||||
|
@ -141,12 +141,23 @@ func TestValidPatchOptions(t *testing.T) {
|
||||
Force: boolPtr(true),
|
||||
FieldManager: "kubectl",
|
||||
},
|
||||
patchType: types.ApplyPatchType,
|
||||
patchType: types.ApplyYAMLPatchType,
|
||||
}, {
|
||||
opts: metav1.PatchOptions{
|
||||
FieldManager: "kubectl",
|
||||
},
|
||||
patchType: types.ApplyPatchType,
|
||||
patchType: types.ApplyYAMLPatchType,
|
||||
}, {
|
||||
opts: metav1.PatchOptions{
|
||||
Force: boolPtr(true),
|
||||
FieldManager: "kubectl",
|
||||
},
|
||||
patchType: types.ApplyCBORPatchType,
|
||||
}, {
|
||||
opts: metav1.PatchOptions{
|
||||
FieldManager: "kubectl",
|
||||
},
|
||||
patchType: types.ApplyCBORPatchType,
|
||||
}, {
|
||||
opts: metav1.PatchOptions{},
|
||||
patchType: types.MergePatchType,
|
||||
@ -175,7 +186,12 @@ func TestInvalidPatchOptions(t *testing.T) {
|
||||
// missing manager
|
||||
{
|
||||
opts: metav1.PatchOptions{},
|
||||
patchType: types.ApplyPatchType,
|
||||
patchType: types.ApplyYAMLPatchType,
|
||||
},
|
||||
// missing manager
|
||||
{
|
||||
opts: metav1.PatchOptions{},
|
||||
patchType: types.ApplyCBORPatchType,
|
||||
},
|
||||
// force on non-apply
|
||||
{
|
||||
|
@ -25,5 +25,7 @@ const (
|
||||
JSONPatchType PatchType = "application/json-patch+json"
|
||||
MergePatchType PatchType = "application/merge-patch+json"
|
||||
StrategicMergePatchType PatchType = "application/strategic-merge-patch+json"
|
||||
ApplyPatchType PatchType = "application/apply-patch+yaml"
|
||||
ApplyPatchType PatchType = ApplyYAMLPatchType
|
||||
ApplyYAMLPatchType PatchType = "application/apply-patch+yaml"
|
||||
ApplyCBORPatchType PatchType = "application/apply-patch+cbor"
|
||||
)
|
||||
|
@ -35,9 +35,11 @@ import (
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/validation"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
cbor "k8s.io/apimachinery/pkg/runtime/serializer/cbor/direct"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/managedfields"
|
||||
"k8s.io/apimachinery/pkg/util/mergepatch"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/strategicpatch"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
@ -50,8 +52,10 @@ import (
|
||||
requestmetrics "k8s.io/apiserver/pkg/endpoints/handlers/metrics"
|
||||
"k8s.io/apiserver/pkg/endpoints/handlers/negotiation"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/apiserver/pkg/features"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
"k8s.io/apiserver/pkg/util/dryrun"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/component-base/tracing"
|
||||
)
|
||||
|
||||
@ -129,10 +133,25 @@ func PatchResource(r rest.Patcher, scope *RequestScope, admit admission.Interfac
|
||||
audit.LogRequestPatch(req.Context(), patchBytes)
|
||||
span.AddEvent("Recorded the audit event")
|
||||
|
||||
baseContentType := runtime.ContentTypeJSON
|
||||
if patchType == types.ApplyPatchType {
|
||||
var baseContentType string
|
||||
switch patchType {
|
||||
case types.ApplyYAMLPatchType:
|
||||
baseContentType = runtime.ContentTypeYAML
|
||||
case types.ApplyCBORPatchType:
|
||||
if !utilfeature.TestOnlyFeatureGate.Enabled(features.TestOnlyCBORServingAndStorage) {
|
||||
// This request should have already been rejected by the
|
||||
// Content-Type allowlist check. Return 500 because assumptions are
|
||||
// already broken and the feature is not GA.
|
||||
utilruntime.HandleErrorWithContext(req.Context(), nil, "The patch content-type allowlist check should have made this unreachable.")
|
||||
scope.err(errors.NewInternalError(errors.NewInternalError(fmt.Errorf("unexpected patch type: %v", patchType))), w, req)
|
||||
return
|
||||
}
|
||||
|
||||
baseContentType = runtime.ContentTypeCBOR
|
||||
default:
|
||||
baseContentType = runtime.ContentTypeJSON
|
||||
}
|
||||
|
||||
s, ok := runtime.SerializerInfoForMediaType(scope.Serializer.SupportedMediaTypes(), baseContentType)
|
||||
if !ok {
|
||||
scope.err(fmt.Errorf("no serializer defined for %v", baseContentType), w, req)
|
||||
@ -452,6 +471,20 @@ func (p *smpPatcher) createNewObject(_ context.Context) (runtime.Object, error)
|
||||
return nil, errors.NewNotFound(p.resource.GroupResource(), p.name)
|
||||
}
|
||||
|
||||
func newApplyPatcher(p *patcher, fieldManager *managedfields.FieldManager, unmarshalFn, unmarshalStrictFn func([]byte, interface{}) error) *applyPatcher {
|
||||
return &applyPatcher{
|
||||
fieldManager: fieldManager,
|
||||
patch: p.patchBytes,
|
||||
options: p.options,
|
||||
creater: p.creater,
|
||||
kind: p.kind,
|
||||
userAgent: p.userAgent,
|
||||
validationDirective: p.validationDirective,
|
||||
unmarshalFn: unmarshalFn,
|
||||
unmarshalStrictFn: unmarshalStrictFn,
|
||||
}
|
||||
}
|
||||
|
||||
type applyPatcher struct {
|
||||
patch []byte
|
||||
options *metav1.PatchOptions
|
||||
@ -460,6 +493,8 @@ type applyPatcher struct {
|
||||
fieldManager *managedfields.FieldManager
|
||||
userAgent string
|
||||
validationDirective string
|
||||
unmarshalFn func(data []byte, v interface{}) error
|
||||
unmarshalStrictFn func(data []byte, v interface{}) error
|
||||
}
|
||||
|
||||
func (p *applyPatcher) applyPatchToCurrentObject(requestContext context.Context, obj runtime.Object) (runtime.Object, error) {
|
||||
@ -472,7 +507,7 @@ func (p *applyPatcher) applyPatchToCurrentObject(requestContext context.Context,
|
||||
}
|
||||
|
||||
patchObj := &unstructured.Unstructured{Object: map[string]interface{}{}}
|
||||
if err := yaml.Unmarshal(p.patch, &patchObj.Object); err != nil {
|
||||
if err := p.unmarshalFn(p.patch, &patchObj.Object); err != nil {
|
||||
return nil, errors.NewBadRequest(fmt.Sprintf("error decoding YAML: %v", err))
|
||||
}
|
||||
|
||||
@ -484,7 +519,7 @@ func (p *applyPatcher) applyPatchToCurrentObject(requestContext context.Context,
|
||||
// TODO: spawn something to track deciding whether a fieldValidation=Strict
|
||||
// fatal error should return before an error from the apply operation
|
||||
if p.validationDirective == metav1.FieldValidationStrict || p.validationDirective == metav1.FieldValidationWarn {
|
||||
if err := yaml.UnmarshalStrict(p.patch, &map[string]interface{}{}); err != nil {
|
||||
if err := p.unmarshalStrictFn(p.patch, &map[string]interface{}{}); err != nil {
|
||||
if p.validationDirective == metav1.FieldValidationStrict {
|
||||
return nil, errors.NewBadRequest(fmt.Sprintf("error strict decoding YAML: %v", err))
|
||||
}
|
||||
@ -634,16 +669,21 @@ func (p *patcher) patchResource(ctx context.Context, scope *RequestScope) (runti
|
||||
fieldManager: scope.FieldManager,
|
||||
}
|
||||
// this case is unreachable if ServerSideApply is not enabled because we will have already rejected the content type
|
||||
case types.ApplyPatchType:
|
||||
p.mechanism = &applyPatcher{
|
||||
fieldManager: scope.FieldManager,
|
||||
patch: p.patchBytes,
|
||||
options: p.options,
|
||||
creater: p.creater,
|
||||
kind: p.kind,
|
||||
userAgent: p.userAgent,
|
||||
validationDirective: p.validationDirective,
|
||||
case types.ApplyYAMLPatchType:
|
||||
p.mechanism = newApplyPatcher(p, scope.FieldManager, yaml.Unmarshal, yaml.UnmarshalStrict)
|
||||
p.forceAllowCreate = true
|
||||
case types.ApplyCBORPatchType:
|
||||
if !utilfeature.TestOnlyFeatureGate.Enabled(features.TestOnlyCBORServingAndStorage) {
|
||||
utilruntime.HandleErrorWithContext(context.TODO(), nil, "CBOR apply requests should be rejected before reaching this point unless the feature gate is enabled.")
|
||||
return nil, false, fmt.Errorf("%v: unimplemented patch type", p.patchType)
|
||||
}
|
||||
|
||||
// The strict and non-strict funcs are the same here because any CBOR map with
|
||||
// duplicate keys is invalid and always rejected outright regardless of strictness
|
||||
// mode, and unknown field errors can't occur in practice because the type of the
|
||||
// destination value for unmarshaling an apply configuration is always
|
||||
// "unstructured".
|
||||
p.mechanism = newApplyPatcher(p, scope.FieldManager, cbor.Unmarshal, cbor.Unmarshal)
|
||||
p.forceAllowCreate = true
|
||||
default:
|
||||
return nil, false, fmt.Errorf("%v: unimplemented patch type", p.patchType)
|
||||
@ -670,7 +710,7 @@ func (p *patcher) patchResource(ctx context.Context, scope *RequestScope) (runti
|
||||
result, err := requestFunc()
|
||||
// If the object wasn't committed to storage because it's serialized size was too large,
|
||||
// it is safe to remove managedFields (which can be large) and try again.
|
||||
if isTooLargeError(err) && p.patchType != types.ApplyPatchType {
|
||||
if isTooLargeError(err) && p.patchType != types.ApplyYAMLPatchType && p.patchType != types.ApplyCBORPatchType {
|
||||
if _, accessorErr := meta.Accessor(p.restPatcher.New()); accessorErr == nil {
|
||||
p.updatedObjectInfo = rest.DefaultUpdatedObjectInfo(nil,
|
||||
p.applyPatch,
|
||||
|
@ -875,7 +875,10 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
|
||||
string(types.JSONPatchType),
|
||||
string(types.MergePatchType),
|
||||
string(types.StrategicMergePatchType),
|
||||
string(types.ApplyPatchType),
|
||||
string(types.ApplyYAMLPatchType),
|
||||
}
|
||||
if utilfeature.TestOnlyFeatureGate.Enabled(features.TestOnlyCBORServingAndStorage) {
|
||||
supportedTypes = append(supportedTypes, string(types.ApplyCBORPatchType))
|
||||
}
|
||||
handler := metrics.InstrumentRouteFunc(action.Verb, group, version, resource, subresource, requestScope, metrics.APIServerComponent, deprecated, removedRelease, restfulPatchResource(patcher, reqScope, admit, supportedTypes))
|
||||
handler = utilwarning.AddWarningsHandler(handler, warnings)
|
||||
|
@ -207,7 +207,7 @@ func TestGoldenRequest(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load fixture: %v", err)
|
||||
}
|
||||
if diff := cmp.Diff(got, want); diff != "" {
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Errorf("unexpected difference from expected bytes:\n%s", diff)
|
||||
}
|
||||
}))
|
||||
|
@ -25,12 +25,12 @@ import (
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/client-go/features"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/util/apply"
|
||||
"k8s.io/client-go/util/consistencydetector"
|
||||
"k8s.io/client-go/util/watchlist"
|
||||
"k8s.io/klog/v2"
|
||||
@ -340,10 +340,6 @@ func (c *dynamicResourceClient) Apply(ctx context.Context, name string, obj *uns
|
||||
if err := validateNamespaceWithOptionalName(c.namespace, name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
outBytes, err := runtime.Encode(unstructured.UnstructuredJSONScheme, obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
accessor, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -355,21 +351,16 @@ func (c *dynamicResourceClient) Apply(ctx context.Context, name string, obj *uns
|
||||
}
|
||||
patchOpts := opts.ToPatchOptions()
|
||||
|
||||
result := c.client.client.
|
||||
Patch(types.ApplyPatchType).
|
||||
AbsPath(append(c.makeURLSegments(name), subresources...)...).
|
||||
Body(outBytes).
|
||||
SpecificallyVersionedParams(&patchOpts, dynamicParameterCodec, versionV1).
|
||||
Do(ctx)
|
||||
if err := result.Error(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
retBytes, err := result.Raw()
|
||||
request, err := apply.NewRequest(c.client.client, obj.Object)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var out unstructured.Unstructured
|
||||
if err := runtime.DecodeInto(unstructured.UnstructuredJSONScheme, retBytes, &out); err != nil {
|
||||
if err := request.
|
||||
AbsPath(append(c.makeURLSegments(name), subresources...)...).
|
||||
SpecificallyVersionedParams(&patchOpts, dynamicParameterCodec, versionV1).
|
||||
Do(ctx).Into(&out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
|
@ -2,8 +2,8 @@ PATCH /apis/flops/v1alpha1/namespaces/mops/flips/mips/fin?force=true HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json
|
||||
Accept-Encoding: gzip
|
||||
Content-Length: 29
|
||||
Content-Length: 28
|
||||
Content-Type: application/apply-patch+yaml
|
||||
User-Agent: TestGoldenRequest
|
||||
|
||||
{"metadata":{"name":"mips"}}
|
||||
{"metadata":{"name":"mips"}}
|
@ -2,8 +2,8 @@ PATCH /apis/flops/v1alpha1/namespaces/mops/flips/mips/status?force=true HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json
|
||||
Accept-Encoding: gzip
|
||||
Content-Length: 29
|
||||
Content-Length: 28
|
||||
Content-Type: application/apply-patch+yaml
|
||||
User-Agent: TestGoldenRequest
|
||||
|
||||
{"metadata":{"name":"mips"}}
|
||||
{"metadata":{"name":"mips"}}
|
@ -18,7 +18,6 @@ package gentype
|
||||
|
||||
import (
|
||||
"context"
|
||||
json "encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
@ -27,6 +26,7 @@ import (
|
||||
types "k8s.io/apimachinery/pkg/types"
|
||||
watch "k8s.io/apimachinery/pkg/watch"
|
||||
rest "k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/util/apply"
|
||||
"k8s.io/client-go/util/consistencydetector"
|
||||
"k8s.io/client-go/util/watchlist"
|
||||
"k8s.io/klog/v2"
|
||||
@ -337,20 +337,21 @@ func (a *alsoApplier[T, C]) Apply(ctx context.Context, obj C, opts metav1.ApplyO
|
||||
return *new(T), fmt.Errorf("object provided to Apply must not be nil")
|
||||
}
|
||||
patchOpts := opts.ToPatchOptions()
|
||||
data, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return *new(T), err
|
||||
}
|
||||
if obj.GetName() == nil {
|
||||
return *new(T), fmt.Errorf("obj.Name must be provided to Apply")
|
||||
}
|
||||
err = a.client.client.Patch(types.ApplyPatchType).
|
||||
|
||||
request, err := apply.NewRequest(a.client.client, obj)
|
||||
if err != nil {
|
||||
return *new(T), err
|
||||
}
|
||||
|
||||
err = request.
|
||||
UseProtobufAsDefaultIfPreferred(a.client.prefersProtobuf).
|
||||
NamespaceIfScoped(a.client.namespace, a.client.namespace != "").
|
||||
Resource(a.client.resource).
|
||||
Name(*obj.GetName()).
|
||||
VersionedParams(&patchOpts, a.client.parameterCodec).
|
||||
Body(data).
|
||||
Do(ctx).
|
||||
Into(result)
|
||||
return result, err
|
||||
@ -362,24 +363,24 @@ func (a *alsoApplier[T, C]) ApplyStatus(ctx context.Context, obj C, opts metav1.
|
||||
return *new(T), fmt.Errorf("object provided to Apply must not be nil")
|
||||
}
|
||||
patchOpts := opts.ToPatchOptions()
|
||||
data, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return *new(T), err
|
||||
}
|
||||
|
||||
if obj.GetName() == nil {
|
||||
return *new(T), fmt.Errorf("obj.Name must be provided to Apply")
|
||||
}
|
||||
|
||||
request, err := apply.NewRequest(a.client.client, obj)
|
||||
if err != nil {
|
||||
return *new(T), err
|
||||
}
|
||||
|
||||
result := a.client.newObject()
|
||||
err = a.client.client.Patch(types.ApplyPatchType).
|
||||
err = request.
|
||||
UseProtobufAsDefaultIfPreferred(a.client.prefersProtobuf).
|
||||
NamespaceIfScoped(a.client.namespace, a.client.namespace != "").
|
||||
Resource(a.client.resource).
|
||||
Name(*obj.GetName()).
|
||||
SubResource("status").
|
||||
VersionedParams(&patchOpts, a.client.parameterCodec).
|
||||
Body(data).
|
||||
Do(ctx).
|
||||
Into(result)
|
||||
return result, err
|
||||
|
49
staging/src/k8s.io/client-go/util/apply/apply.go
Normal file
49
staging/src/k8s.io/client-go/util/apply/apply.go
Normal file
@ -0,0 +1,49 @@
|
||||
/*
|
||||
Copyright 2024 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 apply
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
cbor "k8s.io/apimachinery/pkg/runtime/serializer/cbor/direct"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/json"
|
||||
"k8s.io/client-go/features"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
// NewRequest builds a new server-side apply request. The provided apply configuration object will
|
||||
// be marshalled to the request's body using the default encoding, and the Content-Type header will
|
||||
// be set to application/apply-patch with the appropriate structured syntax name suffix (today,
|
||||
// either +yaml or +cbor, see
|
||||
// https://www.iana.org/assignments/media-type-structured-suffix/media-type-structured-suffix.xhtml).
|
||||
func NewRequest(client rest.Interface, applyConfiguration interface{}) (*rest.Request, error) {
|
||||
pt := types.ApplyYAMLPatchType
|
||||
marshal := json.Marshal
|
||||
|
||||
if features.TestOnlyFeatureGates.Enabled(features.TestOnlyClientAllowsCBOR) && features.TestOnlyFeatureGates.Enabled(features.TestOnlyClientPrefersCBOR) {
|
||||
pt = types.ApplyCBORPatchType
|
||||
marshal = cbor.Marshal
|
||||
}
|
||||
|
||||
body, err := marshal(applyConfiguration)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal apply configuration: %w", err)
|
||||
}
|
||||
|
||||
return client.Patch(pt).Body(body), nil
|
||||
}
|
@ -142,7 +142,6 @@ func (g *genClientForType) GenerateType(c *generator.Context, t *types.Type, w i
|
||||
"ApplyOptions": c.Universe.Type(types.Name{Package: "k8s.io/apimachinery/pkg/apis/meta/v1", Name: "ApplyOptions"}),
|
||||
"PatchType": c.Universe.Type(types.Name{Package: "k8s.io/apimachinery/pkg/types", Name: "PatchType"}),
|
||||
"PatchOptions": c.Universe.Type(types.Name{Package: "k8s.io/apimachinery/pkg/apis/meta/v1", Name: "PatchOptions"}),
|
||||
"jsonMarshal": c.Universe.Function(types.Name{Package: "encoding/json", Name: "Marshal"}),
|
||||
"context": c.Universe.Type(types.Name{Package: "context", Name: "Context"}),
|
||||
},
|
||||
}
|
||||
@ -172,11 +171,9 @@ func (g *genClientForType) GenerateType(c *generator.Context, t *types.Type, w i
|
||||
"ApplyOptions": c.Universe.Type(types.Name{Package: "k8s.io/apimachinery/pkg/apis/meta/v1", Name: "ApplyOptions"}),
|
||||
"UpdateOptions": c.Universe.Type(types.Name{Package: "k8s.io/apimachinery/pkg/apis/meta/v1", Name: "UpdateOptions"}),
|
||||
"PatchType": c.Universe.Type(types.Name{Package: "k8s.io/apimachinery/pkg/types", Name: "PatchType"}),
|
||||
"ApplyPatchType": c.Universe.Type(types.Name{Package: "k8s.io/apimachinery/pkg/types", Name: "ApplyPatchType"}),
|
||||
"watchInterface": c.Universe.Type(types.Name{Package: "k8s.io/apimachinery/pkg/watch", Name: "Interface"}),
|
||||
"RESTClientInterface": c.Universe.Type(types.Name{Package: "k8s.io/client-go/rest", Name: "Interface"}),
|
||||
"schemeParameterCodec": c.Universe.Variable(types.Name{Package: path.Join(g.clientsetPackage, "scheme"), Name: "ParameterCodec"}),
|
||||
"jsonMarshal": c.Universe.Function(types.Name{Package: "encoding/json", Name: "Marshal"}),
|
||||
"fmtErrorf": c.Universe.Function(types.Name{Package: "fmt", Name: "Errorf"}),
|
||||
"klogWarningf": c.Universe.Function(types.Name{Package: "k8s.io/klog/v2", Name: "Warningf"}),
|
||||
"context": c.Universe.Type(types.Name{Package: "context", Name: "Context"}),
|
||||
@ -186,6 +183,7 @@ func (g *genClientForType) GenerateType(c *generator.Context, t *types.Type, w i
|
||||
"CheckListFromCacheDataConsistencyIfRequested": c.Universe.Function(types.Name{Package: "k8s.io/client-go/util/consistencydetector", Name: "CheckListFromCacheDataConsistencyIfRequested"}),
|
||||
"CheckWatchListFromCacheDataConsistencyIfRequested": c.Universe.Function(types.Name{Package: "k8s.io/client-go/util/consistencydetector", Name: "CheckWatchListFromCacheDataConsistencyIfRequested"}),
|
||||
"PrepareWatchListOptionsFromListOptions": c.Universe.Function(types.Name{Package: "k8s.io/client-go/util/watchlist", Name: "PrepareWatchListOptionsFromListOptions"}),
|
||||
"applyNewRequest": c.Universe.Function(types.Name{Package: "k8s.io/client-go/util/apply", Name: "NewRequest"}),
|
||||
"Client": c.Universe.Type(types.Name{Package: "k8s.io/client-go/gentype", Name: "Client"}),
|
||||
"ClientWithList": c.Universe.Type(types.Name{Package: "k8s.io/client-go/gentype", Name: "ClientWithList"}),
|
||||
"ClientWithApply": c.Universe.Type(types.Name{Package: "k8s.io/client-go/gentype", Name: "ClientWithApply"}),
|
||||
@ -843,22 +841,21 @@ func (c *$.type|privatePlural$) $.verb$(ctx $.context|raw$, $.inputType|private$
|
||||
return nil, $.fmtErrorf|raw$("$.inputType|private$ provided to $.verb$ must not be nil")
|
||||
}
|
||||
patchOpts := opts.ToPatchOptions()
|
||||
data, err := $.jsonMarshal|raw$($.inputType|private$)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
name := $.inputType|private$.Name
|
||||
name := $.inputType|private$.Name
|
||||
if name == nil {
|
||||
return nil, $.fmtErrorf|raw$("$.inputType|private$.Name must be provided to $.verb$")
|
||||
}
|
||||
request, err := $.applyNewRequest|raw$(c.GetClient(), $.inputType|private$)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = &$.resultType|raw${}
|
||||
err = c.GetClient().Patch($.ApplyPatchType|raw$).
|
||||
err = request.
|
||||
$if .prefersProtobuf$UseProtobufAsDefault().$end$
|
||||
$if .namespaced$Namespace(c.GetNamespace()).$end$
|
||||
Resource("$.type|resource$").
|
||||
Name(*name).
|
||||
VersionedParams(&patchOpts, $.schemeParameterCodec|raw$).
|
||||
Body(data).
|
||||
Do(ctx).
|
||||
Into(result)
|
||||
return
|
||||
@ -873,20 +870,19 @@ func (c *$.type|privatePlural$) $.verb$(ctx $.context|raw$, $.type|private$Name
|
||||
return nil, $.fmtErrorf|raw$("$.inputType|private$ provided to $.verb$ must not be nil")
|
||||
}
|
||||
patchOpts := opts.ToPatchOptions()
|
||||
data, err := $.jsonMarshal|raw$($.inputType|private$)
|
||||
request, err := $.applyNewRequest|raw$(c.GetClient(), $.inputType|private$)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result = &$.resultType|raw${}
|
||||
err = c.GetClient().Patch($.ApplyPatchType|raw$).
|
||||
err = request.
|
||||
$if .prefersProtobuf$UseProtobufAsDefault().$end$
|
||||
$if .namespaced$Namespace(c.GetNamespace()).$end$
|
||||
Resource("$.type|resource$").
|
||||
Name($.type|private$Name).
|
||||
SubResource("$.subresourcePath$").
|
||||
VersionedParams(&patchOpts, $.schemeParameterCodec|raw$).
|
||||
Body(data).
|
||||
Do(ctx).
|
||||
Into(result)
|
||||
return
|
||||
|
@ -1033,7 +1033,7 @@ func TestPatchVeryLargeObject(t *testing.T) {
|
||||
}
|
||||
|
||||
// Applying to the same object should cause managedFields to go over the object size limit, and fail.
|
||||
_, err = client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
|
||||
_, err = client.CoreV1().RESTClient().Patch(types.ApplyYAMLPatchType).
|
||||
Namespace("default").
|
||||
Resource("configmaps").
|
||||
Name("large-patch-test-cm").
|
||||
@ -1053,6 +1053,79 @@ func TestPatchVeryLargeObject(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestPatchVeryLargeObjectCBORApply mirrors TestPatchVeryLargeObject using the +cbor structured
|
||||
// syntax suffix for application/apply-patch and with CBOR enabled.
|
||||
func TestPatchVeryLargeObjectCBORApply(t *testing.T) {
|
||||
framework.EnableCBORServingAndStorageForTest(t)
|
||||
framework.SetTestOnlyCBORClientFeatureGatesForTest(t, true, false)
|
||||
|
||||
client, closeFn := setup(t)
|
||||
defer closeFn()
|
||||
|
||||
cfg := &v1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "large-patch-test-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string]string{"k": "v"},
|
||||
}
|
||||
|
||||
// Create a small config map.
|
||||
if _, err := client.CoreV1().ConfigMaps(cfg.Namespace).Create(context.TODO(), cfg, metav1.CreateOptions{}); err != nil {
|
||||
t.Errorf("unable to create configMap: %v", err)
|
||||
}
|
||||
|
||||
patchString := `{"data":{"k":"v"`
|
||||
for i := 0; i < 9999; i++ {
|
||||
unique := fmt.Sprintf("this-key-is-very-long-so-as-to-create-a-very-large-serialized-fieldset-%v", i)
|
||||
patchString = fmt.Sprintf("%s,%q:%q", patchString, unique, "A")
|
||||
}
|
||||
patchString = fmt.Sprintf("%s}}", patchString)
|
||||
|
||||
// Should be able to update a small object to be near the object size limit.
|
||||
_, err := client.CoreV1().RESTClient().Patch(types.MergePatchType).
|
||||
AbsPath("/api/v1").
|
||||
Namespace(cfg.Namespace).
|
||||
Resource("configmaps").
|
||||
Name(cfg.Name).
|
||||
Body([]byte(patchString)).Do(context.TODO()).Get()
|
||||
if err != nil {
|
||||
t.Errorf("unable to patch configMap: %v", err)
|
||||
}
|
||||
|
||||
// Applying to the same object should cause managedFields to go over the object size limit, and fail.
|
||||
_, err = client.CoreV1().RESTClient().Patch(types.ApplyYAMLPatchType).
|
||||
Namespace("default").
|
||||
Resource("configmaps").
|
||||
Name("large-patch-test-cm").
|
||||
Param("fieldManager", "apply_test").
|
||||
Body([]byte(`{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": {
|
||||
"name": "large-patch-test-cm",
|
||||
"namespace": "default",
|
||||
}
|
||||
}`)).
|
||||
Do(context.TODO()).
|
||||
Get()
|
||||
if err == nil {
|
||||
t.Fatalf("expected to fail to update object using Apply patch, but succeeded")
|
||||
}
|
||||
|
||||
_, err = client.CoreV1().RESTClient().Patch(types.ApplyCBORPatchType).
|
||||
Namespace("default").
|
||||
Resource("configmaps").
|
||||
Name("large-patch-test-cm").
|
||||
Param("fieldManager", "apply_test").
|
||||
Body([]byte("\xa3\x4aapiVersion\x42v1\x44kind\x49ConfigMap\x48metadata\xa2\x44name\x53large-patch-test-cm\x49namespace\x47default")).
|
||||
Do(context.TODO()).
|
||||
Get()
|
||||
if err == nil {
|
||||
t.Fatalf("expected to fail to update object using Apply patch (cbor), but succeeded")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplyManagedFields makes sure that managedFields api does not change
|
||||
func TestApplyManagedFields(t *testing.T) {
|
||||
client, closeFn := setup(t)
|
||||
|
@ -18,7 +18,6 @@ package apiserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@ -28,8 +27,10 @@ import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/json"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
apiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
|
||||
"k8s.io/kubernetes/test/integration/etcd"
|
||||
"k8s.io/kubernetes/test/integration/framework"
|
||||
@ -92,6 +93,19 @@ func createMapping(groupVersion string, resource metav1.APIResource) (*meta.REST
|
||||
|
||||
// TestApplyStatus makes sure that applying the status works for all known types.
|
||||
func TestApplyStatus(t *testing.T) {
|
||||
testApplyStatus(t, func(testing.TB, *rest.Config) {})
|
||||
}
|
||||
|
||||
// TestApplyStatus makes sure that applying the status works for all known types.
|
||||
func TestApplyStatusWithCBOR(t *testing.T) {
|
||||
framework.EnableCBORServingAndStorageForTest(t)
|
||||
framework.SetTestOnlyCBORClientFeatureGatesForTest(t, true, true)
|
||||
testApplyStatus(t, func(t testing.TB, config *rest.Config) {
|
||||
config.Wrap(framework.AssertRequestResponseAsCBOR(t))
|
||||
})
|
||||
}
|
||||
|
||||
func testApplyStatus(t *testing.T, reconfigureClient func(testing.TB, *rest.Config)) {
|
||||
server, err := apiservertesting.StartTestServer(t, apiservertesting.NewDefaultTestServerOptions(), []string{"--disable-admission-plugins", "ServiceAccount,TaintNodesByCondition"}, framework.SharedEtcd())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@ -102,10 +116,6 @@ func TestApplyStatus(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dynamicClient, err := dynamic.NewForConfig(server.ClientConfig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// create CRDs so we can make sure that custom resources do not get lost
|
||||
etcd.CreateTestCRDs(t, apiextensionsclientset.NewForConfigOrDie(server.ClientConfig), false, etcd.GetCustomResourceDefinitionData()...)
|
||||
@ -134,7 +144,7 @@ func TestApplyStatus(t *testing.T) {
|
||||
// both spec and status get wiped for CSRs,
|
||||
// nothing is expected to be managed for it, skip it
|
||||
if mapping.Resource.Resource == "certificatesigningrequests" {
|
||||
t.Skip()
|
||||
t.SkipNow()
|
||||
}
|
||||
|
||||
status, ok := statusData[mapping.Resource]
|
||||
@ -159,6 +169,13 @@ func TestApplyStatus(t *testing.T) {
|
||||
// etcd test stub data doesn't contain apiVersion/kind (!), but apply requires it
|
||||
newObj.SetGroupVersionKind(mapping.GroupVersionKind)
|
||||
|
||||
dynamicClientConfig := rest.CopyConfig(server.ClientConfig)
|
||||
reconfigureClient(t, dynamicClientConfig)
|
||||
dynamicClient, err := dynamic.NewForConfig(dynamicClientConfig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rsc := dynamicClient.Resource(mapping.Resource).Namespace(namespace)
|
||||
// apply to create
|
||||
_, err = rsc.Apply(context.TODO(), name, &newObj, metav1.ApplyOptions{FieldManager: "create_test"})
|
||||
|
@ -47,11 +47,12 @@ import (
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
appsv1ac "k8s.io/client-go/applyconfigurations/apps/v1"
|
||||
autoscalingv1ac "k8s.io/client-go/applyconfigurations/autoscaling/v1"
|
||||
corev1ac "k8s.io/client-go/applyconfigurations/core/v1"
|
||||
metav1ac "k8s.io/client-go/applyconfigurations/meta/v1"
|
||||
"k8s.io/client-go/discovery"
|
||||
"k8s.io/client-go/gentype"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
clientscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
"k8s.io/client-go/rest"
|
||||
@ -61,7 +62,6 @@ import (
|
||||
"k8s.io/kubernetes/pkg/api/legacyscheme"
|
||||
"k8s.io/kubernetes/test/integration/framework"
|
||||
imageutils "k8s.io/kubernetes/test/utils/image"
|
||||
"k8s.io/kubernetes/test/utils/ktesting"
|
||||
wardlev1alpha1 "k8s.io/sample-apiserver/pkg/apis/wardle/v1alpha1"
|
||||
wardlev1alpha1client "k8s.io/sample-apiserver/pkg/generated/clientset/versioned/typed/wardle/v1alpha1"
|
||||
"k8s.io/utils/ptr"
|
||||
@ -71,7 +71,7 @@ func TestClient(t *testing.T) {
|
||||
result := kubeapiservertesting.StartTestServerOrDie(t, nil, framework.DefaultTestServerFlags(), framework.SharedEtcd())
|
||||
defer result.TearDownFn()
|
||||
|
||||
client := clientset.NewForConfigOrDie(result.ClientConfig)
|
||||
client := kubernetes.NewForConfigOrDie(result.ClientConfig)
|
||||
|
||||
info, err := client.Discovery().ServerVersion()
|
||||
if err != nil {
|
||||
@ -145,7 +145,7 @@ func TestAtomicPut(t *testing.T) {
|
||||
result := kubeapiservertesting.StartTestServerOrDie(t, nil, framework.DefaultTestServerFlags(), framework.SharedEtcd())
|
||||
defer result.TearDownFn()
|
||||
|
||||
c := clientset.NewForConfigOrDie(result.ClientConfig)
|
||||
c := kubernetes.NewForConfigOrDie(result.ClientConfig)
|
||||
|
||||
rcBody := v1.ReplicationController{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
@ -234,7 +234,7 @@ func TestPatch(t *testing.T) {
|
||||
result := kubeapiservertesting.StartTestServerOrDie(t, nil, framework.DefaultTestServerFlags(), framework.SharedEtcd())
|
||||
defer result.TearDownFn()
|
||||
|
||||
c := clientset.NewForConfigOrDie(result.ClientConfig)
|
||||
c := kubernetes.NewForConfigOrDie(result.ClientConfig)
|
||||
|
||||
name := "patchpod"
|
||||
resource := "pods"
|
||||
@ -353,7 +353,7 @@ func TestPatchWithCreateOnUpdate(t *testing.T) {
|
||||
result := kubeapiservertesting.StartTestServerOrDie(t, nil, framework.DefaultTestServerFlags(), framework.SharedEtcd())
|
||||
defer result.TearDownFn()
|
||||
|
||||
c := clientset.NewForConfigOrDie(result.ClientConfig)
|
||||
c := kubernetes.NewForConfigOrDie(result.ClientConfig)
|
||||
|
||||
endpointTemplate := &v1.Endpoints{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
@ -461,7 +461,7 @@ func TestAPIVersions(t *testing.T) {
|
||||
result := kubeapiservertesting.StartTestServerOrDie(t, nil, framework.DefaultTestServerFlags(), framework.SharedEtcd())
|
||||
defer result.TearDownFn()
|
||||
|
||||
c := clientset.NewForConfigOrDie(result.ClientConfig)
|
||||
c := kubernetes.NewForConfigOrDie(result.ClientConfig)
|
||||
|
||||
clientVersion := c.CoreV1().RESTClient().APIVersion().String()
|
||||
g, err := c.Discovery().ServerGroups()
|
||||
@ -483,7 +483,7 @@ func TestEventValidation(t *testing.T) {
|
||||
result := kubeapiservertesting.StartTestServerOrDie(t, nil, framework.DefaultTestServerFlags(), framework.SharedEtcd())
|
||||
defer result.TearDownFn()
|
||||
|
||||
client := clientset.NewForConfigOrDie(result.ClientConfig)
|
||||
client := kubernetes.NewForConfigOrDie(result.ClientConfig)
|
||||
|
||||
createNamespace := func(namespace string) string {
|
||||
if namespace == "" {
|
||||
@ -591,7 +591,7 @@ func TestEventCompatibility(t *testing.T) {
|
||||
result := kubeapiservertesting.StartTestServerOrDie(t, nil, framework.DefaultTestServerFlags(), framework.SharedEtcd())
|
||||
defer result.TearDownFn()
|
||||
|
||||
client := clientset.NewForConfigOrDie(result.ClientConfig)
|
||||
client := kubernetes.NewForConfigOrDie(result.ClientConfig)
|
||||
|
||||
coreevents := []*v1.Event{
|
||||
{
|
||||
@ -701,7 +701,7 @@ func TestSingleWatch(t *testing.T) {
|
||||
result := kubeapiservertesting.StartTestServerOrDie(t, nil, framework.DefaultTestServerFlags(), framework.SharedEtcd())
|
||||
defer result.TearDownFn()
|
||||
|
||||
client := clientset.NewForConfigOrDie(result.ClientConfig)
|
||||
client := kubernetes.NewForConfigOrDie(result.ClientConfig)
|
||||
|
||||
mkEvent := func(i int) *v1.Event {
|
||||
name := fmt.Sprintf("event-%v", i)
|
||||
@ -785,7 +785,7 @@ func TestMultiWatch(t *testing.T) {
|
||||
result := kubeapiservertesting.StartTestServerOrDie(t, nil, framework.DefaultTestServerFlags(), framework.SharedEtcd())
|
||||
defer result.TearDownFn()
|
||||
|
||||
client := clientset.NewForConfigOrDie(result.ClientConfig)
|
||||
client := kubernetes.NewForConfigOrDie(result.ClientConfig)
|
||||
|
||||
dummyEvent := func(i int) *v1.Event {
|
||||
name := fmt.Sprintf("unrelated-%v", i)
|
||||
@ -1014,7 +1014,7 @@ func TestApplyWithApplyConfiguration(t *testing.T) {
|
||||
testServer := kubeapiservertesting.StartTestServerOrDie(t, nil, framework.DefaultTestServerFlags(), framework.SharedEtcd())
|
||||
defer testServer.TearDownFn()
|
||||
|
||||
c := clientset.NewForConfigOrDie(testServer.ClientConfig)
|
||||
c := kubernetes.NewForConfigOrDie(testServer.ClientConfig)
|
||||
|
||||
// Test apply to spec
|
||||
obj, err := c.AppsV1().Deployments("default").Apply(context.TODO(), deployment, metav1.ApplyOptions{FieldManager: "test-mgr", Force: true})
|
||||
@ -1172,7 +1172,7 @@ func TestExtractModifyApply(t *testing.T) {
|
||||
|
||||
testServer := kubeapiservertesting.StartTestServerOrDie(t, nil, framework.DefaultTestServerFlags(), framework.SharedEtcd())
|
||||
defer testServer.TearDownFn()
|
||||
c := clientset.NewForConfigOrDie(testServer.ClientConfig)
|
||||
c := kubernetes.NewForConfigOrDie(testServer.ClientConfig)
|
||||
deploymentClient := c.AppsV1().Deployments("default")
|
||||
fieldMgr := "test-mgr"
|
||||
|
||||
@ -1244,7 +1244,7 @@ func TestExtractModifyApply(t *testing.T) {
|
||||
func TestExtractModifyApply_ForceOwnership(t *testing.T) {
|
||||
testServer := kubeapiservertesting.StartTestServerOrDie(t, nil, framework.DefaultTestServerFlags(), framework.SharedEtcd())
|
||||
defer testServer.TearDownFn()
|
||||
c := clientset.NewForConfigOrDie(testServer.ClientConfig)
|
||||
c := kubernetes.NewForConfigOrDie(testServer.ClientConfig)
|
||||
deploymentClient := c.AppsV1().Deployments("default")
|
||||
|
||||
// apply an initial state with one field manager
|
||||
@ -1364,7 +1364,7 @@ func TestClientCBOREnablement(t *testing.T) {
|
||||
// Generated clients for built-in types force Protobuf by default. They are tested here to
|
||||
// ensure that the CBOR client feature gates do not interfere with this.
|
||||
DoRequestWithProtobufPreferredGeneratedClient := func(t *testing.T, config *rest.Config) error {
|
||||
clientset, err := clientset.NewForConfig(config)
|
||||
clientset, err := kubernetes.NewForConfig(config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -1401,7 +1401,7 @@ func TestClientCBOREnablement(t *testing.T) {
|
||||
}
|
||||
|
||||
DoRequestWithGenericTypedClient := func(t *testing.T, config *rest.Config) error {
|
||||
clientset, err := clientset.NewForConfig(config)
|
||||
clientset, err := kubernetes.NewForConfig(config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -1794,8 +1794,6 @@ func TestClientCBOREnablement(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCBORWithTypedClient(t *testing.T) {
|
||||
ktesting.SetDefaultVerbosity(10) // todo
|
||||
|
||||
framework.EnableCBORServingAndStorageForTest(t)
|
||||
framework.SetTestOnlyCBORClientFeatureGatesForTest(t, true, true)
|
||||
|
||||
@ -1806,7 +1804,7 @@ func TestCBORWithTypedClient(t *testing.T) {
|
||||
|
||||
{
|
||||
// Setup using client with default config.
|
||||
clientset, err := clientset.NewForConfig(server.ClientConfig)
|
||||
clientset, err := kubernetes.NewForConfig(server.ClientConfig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -1825,7 +1823,7 @@ func TestCBORWithTypedClient(t *testing.T) {
|
||||
config.ContentType = ""
|
||||
config.AcceptContentTypes = ""
|
||||
config.Wrap(framework.AssertRequestResponseAsCBOR(t))
|
||||
clientset, err := clientset.NewForConfig(config)
|
||||
clientset, err := kubernetes.NewForConfig(config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -1933,4 +1931,56 @@ func TestCBORWithTypedClient(t *testing.T) {
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
config = rest.CopyConfig(server.ClientConfig)
|
||||
// Configuring a non-empty AcceptContentTypes avoids the "default to accepting Protobuf"
|
||||
// behavior from client-gen's --prefer-protobuf option, which is set when generating all of
|
||||
// the clients with ApplyScale.
|
||||
config.AcceptContentTypes = "application/cbor"
|
||||
config.Wrap(framework.AssertRequestResponseAsCBOR(t))
|
||||
clientset, err = kubernetes.NewForConfig(config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// for Apply, ApplyStatus, and ApplyScale
|
||||
rsClient := clientset.AppsV1().ReplicaSets(TestNamespace)
|
||||
rs, err := rsClient.Apply(
|
||||
context.TODO(),
|
||||
appsv1ac.ReplicaSet("test-cbor-typed-client", TestNamespace).
|
||||
WithSpec(appsv1ac.ReplicaSetSpec().
|
||||
WithReplicas(0).
|
||||
WithSelector(metav1ac.LabelSelector().WithMatchLabels(map[string]string{"foo": "bar"})).
|
||||
WithTemplate(corev1ac.PodTemplateSpec().
|
||||
WithLabels(map[string]string{"foo": "bar"}).
|
||||
WithSpec(corev1ac.PodSpec().
|
||||
WithContainers(corev1ac.Container().
|
||||
WithName("testing").
|
||||
WithImage("busybox"),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
metav1.ApplyOptions{FieldManager: "test-cbor-typed-client"},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := rsClient.ApplyScale(
|
||||
context.TODO(),
|
||||
rs.GetName(),
|
||||
autoscalingv1ac.Scale().WithSpec(autoscalingv1ac.ScaleSpec().WithReplicas(1)),
|
||||
metav1.ApplyOptions{
|
||||
FieldManager: "test-cbor-typed-client",
|
||||
DryRun: []string{metav1.DryRunAll},
|
||||
Force: true,
|
||||
},
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := rsClient.ApplyStatus(context.TODO(), appsv1ac.ReplicaSet(rs.GetName(), rs.GetNamespace()), metav1.ApplyOptions{FieldManager: "test-cbor-typed-client", DryRun: []string{metav1.DryRunAll}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
@ -302,7 +302,54 @@ func unstructuredToEvent(obj *unstructured.Unstructured) (*corev1.Event, error)
|
||||
}
|
||||
|
||||
func TestDynamicClientCBOREnablement(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
DoCreate := func(t *testing.T, config *rest.Config) error {
|
||||
client, err := dynamic.NewForConfig(config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = client.Resource(corev1.SchemeGroupVersion.WithResource("namespaces")).Create(
|
||||
context.TODO(),
|
||||
&unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "test-dynamic-client-cbor-enablement",
|
||||
},
|
||||
},
|
||||
},
|
||||
metav1.CreateOptions{DryRun: []string{metav1.DryRunAll}},
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
DoApply := func(t *testing.T, config *rest.Config) error {
|
||||
client, err := dynamic.NewForConfig(config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
name := "test-dynamic-client-cbor-enablement"
|
||||
_, err = client.Resource(corev1.SchemeGroupVersion.WithResource("namespaces")).Apply(
|
||||
context.TODO(),
|
||||
name,
|
||||
&unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Namespace",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": name,
|
||||
},
|
||||
},
|
||||
},
|
||||
metav1.ApplyOptions{
|
||||
FieldManager: "foo-bar",
|
||||
DryRun: []string{metav1.DryRunAll},
|
||||
},
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
serving bool
|
||||
allowed bool
|
||||
@ -312,6 +359,7 @@ func TestDynamicClientCBOREnablement(t *testing.T) {
|
||||
wantResponseContentType string
|
||||
wantResponseStatus int
|
||||
wantStatusError bool
|
||||
doRequest func(t *testing.T, config *rest.Config) error
|
||||
}{
|
||||
{
|
||||
name: "sends cbor accepts both gets cbor",
|
||||
@ -323,6 +371,7 @@ func TestDynamicClientCBOREnablement(t *testing.T) {
|
||||
wantResponseContentType: "application/cbor",
|
||||
wantResponseStatus: http.StatusCreated,
|
||||
wantStatusError: false,
|
||||
doRequest: DoCreate,
|
||||
},
|
||||
{
|
||||
name: "sends cbor accepts both gets 415",
|
||||
@ -334,6 +383,7 @@ func TestDynamicClientCBOREnablement(t *testing.T) {
|
||||
wantResponseContentType: "application/json",
|
||||
wantResponseStatus: http.StatusUnsupportedMediaType,
|
||||
wantStatusError: true,
|
||||
doRequest: DoCreate,
|
||||
},
|
||||
{
|
||||
name: "sends json accepts both gets cbor",
|
||||
@ -345,6 +395,7 @@ func TestDynamicClientCBOREnablement(t *testing.T) {
|
||||
wantResponseContentType: "application/cbor",
|
||||
wantResponseStatus: http.StatusCreated,
|
||||
wantStatusError: false,
|
||||
doRequest: DoCreate,
|
||||
},
|
||||
{
|
||||
name: "sends json accepts both gets json",
|
||||
@ -356,6 +407,7 @@ func TestDynamicClientCBOREnablement(t *testing.T) {
|
||||
wantResponseContentType: "application/json",
|
||||
wantResponseStatus: http.StatusCreated,
|
||||
wantStatusError: false,
|
||||
doRequest: DoCreate,
|
||||
},
|
||||
{
|
||||
name: "sends json accepts json gets json with serving enabled",
|
||||
@ -367,6 +419,7 @@ func TestDynamicClientCBOREnablement(t *testing.T) {
|
||||
wantResponseContentType: "application/json",
|
||||
wantResponseStatus: http.StatusCreated,
|
||||
wantStatusError: false,
|
||||
doRequest: DoCreate,
|
||||
},
|
||||
{
|
||||
name: "sends json accepts json gets json with serving disabled",
|
||||
@ -378,6 +431,7 @@ func TestDynamicClientCBOREnablement(t *testing.T) {
|
||||
wantResponseContentType: "application/json",
|
||||
wantResponseStatus: http.StatusCreated,
|
||||
wantStatusError: false,
|
||||
doRequest: DoCreate,
|
||||
},
|
||||
{
|
||||
name: "sends json without both gates enabled",
|
||||
@ -389,60 +443,92 @@ func TestDynamicClientCBOREnablement(t *testing.T) {
|
||||
wantResponseContentType: "application/json",
|
||||
wantResponseStatus: http.StatusCreated,
|
||||
wantStatusError: false,
|
||||
doRequest: DoCreate,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if tc.serving {
|
||||
{
|
||||
name: "apply sends cbor accepts both gets cbor",
|
||||
serving: true,
|
||||
allowed: true,
|
||||
preferred: true,
|
||||
wantRequestContentType: "application/apply-patch+cbor",
|
||||
wantRequestAccept: "application/json;q=0.9,application/cbor;q=1",
|
||||
wantResponseContentType: "application/cbor",
|
||||
wantResponseStatus: http.StatusCreated,
|
||||
wantStatusError: false,
|
||||
doRequest: DoApply,
|
||||
},
|
||||
{
|
||||
name: "apply sends json accepts both gets cbor",
|
||||
serving: true,
|
||||
allowed: true,
|
||||
preferred: false,
|
||||
wantRequestContentType: "application/apply-patch+yaml",
|
||||
wantRequestAccept: "application/json;q=0.9,application/cbor;q=1",
|
||||
wantResponseContentType: "application/cbor",
|
||||
wantResponseStatus: http.StatusCreated,
|
||||
wantStatusError: false,
|
||||
doRequest: DoApply,
|
||||
},
|
||||
{
|
||||
name: "apply sends cbor accepts both gets 415",
|
||||
serving: false,
|
||||
allowed: true,
|
||||
preferred: true,
|
||||
wantRequestContentType: "application/apply-patch+cbor",
|
||||
wantRequestAccept: "application/json;q=0.9,application/cbor;q=1",
|
||||
wantResponseContentType: "application/json",
|
||||
wantResponseStatus: http.StatusUnsupportedMediaType,
|
||||
wantStatusError: true,
|
||||
doRequest: DoApply,
|
||||
},
|
||||
}
|
||||
|
||||
for _, serving := range []bool{true, false} {
|
||||
t.Run(fmt.Sprintf("serving=%t", serving), func(t *testing.T) {
|
||||
if serving {
|
||||
framework.EnableCBORServingAndStorageForTest(t)
|
||||
}
|
||||
|
||||
server := kubeapiservertesting.StartTestServerOrDie(t, nil, framework.DefaultTestServerFlags(), framework.SharedEtcd())
|
||||
defer server.TearDownFn()
|
||||
|
||||
framework.SetTestOnlyCBORClientFeatureGatesForTest(t, tc.allowed, tc.preferred)
|
||||
for _, tc := range testCases {
|
||||
if serving != tc.serving {
|
||||
continue
|
||||
}
|
||||
|
||||
config := rest.CopyConfig(server.ClientConfig)
|
||||
config.Wrap(func(rt http.RoundTripper) http.RoundTripper {
|
||||
return roundTripperFunc(func(request *http.Request) (*http.Response, error) {
|
||||
response, err := rt.RoundTrip(request)
|
||||
if got := response.Request.Header.Get("Content-Type"); got != tc.wantRequestContentType {
|
||||
t.Errorf("want request content type %q, got %q", tc.wantRequestContentType, got)
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
framework.SetTestOnlyCBORClientFeatureGatesForTest(t, tc.allowed, tc.preferred)
|
||||
|
||||
config := rest.CopyConfig(server.ClientConfig)
|
||||
config.Wrap(func(rt http.RoundTripper) http.RoundTripper {
|
||||
return roundTripperFunc(func(request *http.Request) (*http.Response, error) {
|
||||
response, err := rt.RoundTrip(request)
|
||||
if got := response.Request.Header.Get("Content-Type"); got != tc.wantRequestContentType {
|
||||
t.Errorf("want request content type %q, got %q", tc.wantRequestContentType, got)
|
||||
}
|
||||
if got := response.Request.Header.Get("Accept"); got != tc.wantRequestAccept {
|
||||
t.Errorf("want request accept %q, got %q", tc.wantRequestAccept, got)
|
||||
}
|
||||
if got := response.Header.Get("Content-Type"); got != tc.wantResponseContentType {
|
||||
t.Errorf("want response content type %q, got %q", tc.wantResponseContentType, got)
|
||||
}
|
||||
if got := response.StatusCode; got != tc.wantResponseStatus {
|
||||
t.Errorf("want response status %d, got %d", tc.wantResponseStatus, got)
|
||||
}
|
||||
return response, err
|
||||
})
|
||||
})
|
||||
err := tc.doRequest(t, config)
|
||||
switch {
|
||||
case tc.wantStatusError && errors.IsUnsupportedMediaType(err):
|
||||
// ok
|
||||
case !tc.wantStatusError && err == nil:
|
||||
// ok
|
||||
default:
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if got := response.Request.Header.Get("Accept"); got != tc.wantRequestAccept {
|
||||
t.Errorf("want request accept %q, got %q", tc.wantRequestAccept, got)
|
||||
}
|
||||
if got := response.Header.Get("Content-Type"); got != tc.wantResponseContentType {
|
||||
t.Errorf("want response content type %q, got %q", tc.wantResponseContentType, got)
|
||||
}
|
||||
if got := response.StatusCode; got != tc.wantResponseStatus {
|
||||
t.Errorf("want response status %d, got %d", tc.wantResponseStatus, got)
|
||||
}
|
||||
return response, err
|
||||
})
|
||||
})
|
||||
client, err := dynamic.NewForConfig(config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = client.Resource(corev1.SchemeGroupVersion.WithResource("namespaces")).Create(
|
||||
context.TODO(),
|
||||
&unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "test-dynamic-client-cbor-enablement",
|
||||
},
|
||||
},
|
||||
},
|
||||
metav1.CreateOptions{DryRun: []string{metav1.DryRunAll}},
|
||||
)
|
||||
switch {
|
||||
case tc.wantStatusError && errors.IsUnsupportedMediaType(err):
|
||||
// ok
|
||||
case !tc.wantStatusError && err == nil:
|
||||
// ok
|
||||
default:
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer/cbor"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer/cbor/direct"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apiserver/pkg/features"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
@ -111,8 +112,6 @@ func EnableCBORServingAndStorageForTest(tb testing.TB) {
|
||||
// AssertRequestResponseAsCBOR returns a transport.WrapperFunc that will report a test error if a
|
||||
// non-empty request or response body contains data that does not appear to be CBOR-encoded.
|
||||
func AssertRequestResponseAsCBOR(t testing.TB) transport.WrapperFunc {
|
||||
recognizer := cbor.NewSerializer(runtime.NewScheme(), runtime.NewScheme())
|
||||
|
||||
unsupportedPatchContentTypes := sets.New(
|
||||
"application/json-patch+json",
|
||||
"application/merge-patch+json",
|
||||
@ -126,12 +125,11 @@ func AssertRequestResponseAsCBOR(t testing.TB) transport.WrapperFunc {
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
recognized, _, err := recognizer.RecognizesData(requestbody)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if len(requestbody) > 0 && !recognized {
|
||||
t.Errorf("non-cbor request: 0x%x", requestbody)
|
||||
if len(requestbody) > 0 {
|
||||
err = direct.Unmarshal(requestbody, new(interface{}))
|
||||
if err != nil {
|
||||
t.Errorf("non-cbor request: 0x%x", requestbody)
|
||||
}
|
||||
}
|
||||
request.Body = io.NopCloser(bytes.NewReader(requestbody))
|
||||
}
|
||||
@ -152,11 +150,10 @@ func AssertRequestResponseAsCBOR(t testing.TB) transport.WrapperFunc {
|
||||
Closer: response.Body,
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
recognized, _, err := recognizer.RecognizesData(buf.Bytes())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
if buf.Len() == 0 {
|
||||
return
|
||||
}
|
||||
if buf.Len() > 0 && !recognized {
|
||||
if err := direct.Unmarshal(buf.Bytes(), new(interface{})); err != nil {
|
||||
t.Errorf("non-cbor response: 0x%x", buf.Bytes())
|
||||
}
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user