diff --git a/dynamic/golden_test.go b/dynamic/golden_test.go index e9f94e9e..d19c785e 100644 --- a/dynamic/golden_test.go +++ b/dynamic/golden_test.go @@ -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) } })) diff --git a/dynamic/simple.go b/dynamic/simple.go index b4767140..6c729982 100644 --- a/dynamic/simple.go +++ b/dynamic/simple.go @@ -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 diff --git a/dynamic/testdata/TestGoldenRequest/apply b/dynamic/testdata/TestGoldenRequest/apply index 9f6385a7..69916bf5 100755 --- a/dynamic/testdata/TestGoldenRequest/apply +++ b/dynamic/testdata/TestGoldenRequest/apply @@ -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"}} \ No newline at end of file diff --git a/dynamic/testdata/TestGoldenRequest/applystatus b/dynamic/testdata/TestGoldenRequest/applystatus index ce69f166..98a6ce44 100755 --- a/dynamic/testdata/TestGoldenRequest/applystatus +++ b/dynamic/testdata/TestGoldenRequest/applystatus @@ -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"}} \ No newline at end of file diff --git a/gentype/type.go b/gentype/type.go index 267f9111..e6ed6aae 100644 --- a/gentype/type.go +++ b/gentype/type.go @@ -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 diff --git a/go.mod b/go.mod index 5e7862f6..4f7d0a58 100644 --- a/go.mod +++ b/go.mod @@ -28,7 +28,7 @@ require ( google.golang.org/protobuf v1.34.2 gopkg.in/evanphx/json-patch.v4 v4.12.0 k8s.io/api v0.0.0-20241030034125-271c79cac830 - k8s.io/apimachinery v0.0.0-20241025000453-124c262107b0 + k8s.io/apimachinery v0.0.0-20241030172525-c56b2072eeef k8s.io/klog/v2 v2.130.1 k8s.io/kube-openapi v0.0.0-20240827152857-f7e401e7b4c2 k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 diff --git a/go.sum b/go.sum index 8d8da139..36cfd083 100644 --- a/go.sum +++ b/go.sum @@ -157,8 +157,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= k8s.io/api v0.0.0-20241030034125-271c79cac830 h1:zu5fMzQV/xp3VSln7QIaHNb2ghUQ3YJngYo1y52wpsc= k8s.io/api v0.0.0-20241030034125-271c79cac830/go.mod h1:aywKYMtpaiAtWkkKBmWO/iXvZmEmVXmBwQwOuQqkP0c= -k8s.io/apimachinery v0.0.0-20241025000453-124c262107b0 h1:6dJVqURMs0HNPdaaIJ0UqpwB39zuTdaMxYMTNKXiAis= -k8s.io/apimachinery v0.0.0-20241025000453-124c262107b0/go.mod h1:y/FzDt/GaPgPceo5rJcCtD4qW5l8SwtbzESSMGEY6P8= +k8s.io/apimachinery v0.0.0-20241030172525-c56b2072eeef h1:rg539iNcCy2bp+skJ2t30U9/m5du8y8NUxOgtEJ3tFM= +k8s.io/apimachinery v0.0.0-20241030172525-c56b2072eeef/go.mod h1:y/FzDt/GaPgPceo5rJcCtD4qW5l8SwtbzESSMGEY6P8= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20240827152857-f7e401e7b4c2 h1:GKE9U8BH16uynoxQii0auTjmmmuZ3O0LFMN6S0lPPhI= diff --git a/kubernetes/typed/apps/v1/deployment.go b/kubernetes/typed/apps/v1/deployment.go index 98665eb9..cc06ccf3 100644 --- a/kubernetes/typed/apps/v1/deployment.go +++ b/kubernetes/typed/apps/v1/deployment.go @@ -20,7 +20,6 @@ package v1 import ( context "context" - json "encoding/json" fmt "fmt" appsv1 "k8s.io/api/apps/v1" @@ -32,6 +31,7 @@ import ( applyconfigurationsautoscalingv1 "k8s.io/client-go/applyconfigurations/autoscaling/v1" gentype "k8s.io/client-go/gentype" scheme "k8s.io/client-go/kubernetes/scheme" + apply "k8s.io/client-go/util/apply" ) // DeploymentsGetter has a method to return a DeploymentInterface. @@ -120,20 +120,19 @@ func (c *deployments) ApplyScale(ctx context.Context, deploymentName string, sca return nil, fmt.Errorf("scale provided to ApplyScale must not be nil") } patchOpts := opts.ToPatchOptions() - data, err := json.Marshal(scale) + request, err := apply.NewRequest(c.GetClient(), scale) if err != nil { return nil, err } result = &autoscalingv1.Scale{} - err = c.GetClient().Patch(types.ApplyPatchType). + err = request. UseProtobufAsDefault(). Namespace(c.GetNamespace()). Resource("deployments"). Name(deploymentName). SubResource("scale"). VersionedParams(&patchOpts, scheme.ParameterCodec). - Body(data). Do(ctx). Into(result) return diff --git a/kubernetes/typed/apps/v1/replicaset.go b/kubernetes/typed/apps/v1/replicaset.go index b3efc668..db0fed95 100644 --- a/kubernetes/typed/apps/v1/replicaset.go +++ b/kubernetes/typed/apps/v1/replicaset.go @@ -20,7 +20,6 @@ package v1 import ( context "context" - json "encoding/json" fmt "fmt" appsv1 "k8s.io/api/apps/v1" @@ -32,6 +31,7 @@ import ( applyconfigurationsautoscalingv1 "k8s.io/client-go/applyconfigurations/autoscaling/v1" gentype "k8s.io/client-go/gentype" scheme "k8s.io/client-go/kubernetes/scheme" + apply "k8s.io/client-go/util/apply" ) // ReplicaSetsGetter has a method to return a ReplicaSetInterface. @@ -120,20 +120,19 @@ func (c *replicaSets) ApplyScale(ctx context.Context, replicaSetName string, sca return nil, fmt.Errorf("scale provided to ApplyScale must not be nil") } patchOpts := opts.ToPatchOptions() - data, err := json.Marshal(scale) + request, err := apply.NewRequest(c.GetClient(), scale) if err != nil { return nil, err } result = &autoscalingv1.Scale{} - err = c.GetClient().Patch(types.ApplyPatchType). + err = request. UseProtobufAsDefault(). Namespace(c.GetNamespace()). Resource("replicasets"). Name(replicaSetName). SubResource("scale"). VersionedParams(&patchOpts, scheme.ParameterCodec). - Body(data). Do(ctx). Into(result) return diff --git a/kubernetes/typed/apps/v1/statefulset.go b/kubernetes/typed/apps/v1/statefulset.go index e135c340..e52cc615 100644 --- a/kubernetes/typed/apps/v1/statefulset.go +++ b/kubernetes/typed/apps/v1/statefulset.go @@ -20,7 +20,6 @@ package v1 import ( context "context" - json "encoding/json" fmt "fmt" appsv1 "k8s.io/api/apps/v1" @@ -32,6 +31,7 @@ import ( applyconfigurationsautoscalingv1 "k8s.io/client-go/applyconfigurations/autoscaling/v1" gentype "k8s.io/client-go/gentype" scheme "k8s.io/client-go/kubernetes/scheme" + apply "k8s.io/client-go/util/apply" ) // StatefulSetsGetter has a method to return a StatefulSetInterface. @@ -120,20 +120,19 @@ func (c *statefulSets) ApplyScale(ctx context.Context, statefulSetName string, s return nil, fmt.Errorf("scale provided to ApplyScale must not be nil") } patchOpts := opts.ToPatchOptions() - data, err := json.Marshal(scale) + request, err := apply.NewRequest(c.GetClient(), scale) if err != nil { return nil, err } result = &autoscalingv1.Scale{} - err = c.GetClient().Patch(types.ApplyPatchType). + err = request. UseProtobufAsDefault(). Namespace(c.GetNamespace()). Resource("statefulsets"). Name(statefulSetName). SubResource("scale"). VersionedParams(&patchOpts, scheme.ParameterCodec). - Body(data). Do(ctx). Into(result) return diff --git a/kubernetes/typed/apps/v1beta2/statefulset.go b/kubernetes/typed/apps/v1beta2/statefulset.go index 879d86bc..c71e9349 100644 --- a/kubernetes/typed/apps/v1beta2/statefulset.go +++ b/kubernetes/typed/apps/v1beta2/statefulset.go @@ -20,7 +20,6 @@ package v1beta2 import ( context "context" - json "encoding/json" fmt "fmt" appsv1beta2 "k8s.io/api/apps/v1beta2" @@ -30,6 +29,7 @@ import ( applyconfigurationsappsv1beta2 "k8s.io/client-go/applyconfigurations/apps/v1beta2" gentype "k8s.io/client-go/gentype" scheme "k8s.io/client-go/kubernetes/scheme" + apply "k8s.io/client-go/util/apply" ) // StatefulSetsGetter has a method to return a StatefulSetInterface. @@ -118,20 +118,19 @@ func (c *statefulSets) ApplyScale(ctx context.Context, statefulSetName string, s return nil, fmt.Errorf("scale provided to ApplyScale must not be nil") } patchOpts := opts.ToPatchOptions() - data, err := json.Marshal(scale) + request, err := apply.NewRequest(c.GetClient(), scale) if err != nil { return nil, err } result = &appsv1beta2.Scale{} - err = c.GetClient().Patch(types.ApplyPatchType). + err = request. UseProtobufAsDefault(). Namespace(c.GetNamespace()). Resource("statefulsets"). Name(statefulSetName). SubResource("scale"). VersionedParams(&patchOpts, scheme.ParameterCodec). - Body(data). Do(ctx). Into(result) return diff --git a/kubernetes/typed/extensions/v1beta1/deployment.go b/kubernetes/typed/extensions/v1beta1/deployment.go index d7ca65b1..1bcf3cbc 100644 --- a/kubernetes/typed/extensions/v1beta1/deployment.go +++ b/kubernetes/typed/extensions/v1beta1/deployment.go @@ -20,7 +20,6 @@ package v1beta1 import ( context "context" - json "encoding/json" fmt "fmt" extensionsv1beta1 "k8s.io/api/extensions/v1beta1" @@ -30,6 +29,7 @@ import ( applyconfigurationsextensionsv1beta1 "k8s.io/client-go/applyconfigurations/extensions/v1beta1" gentype "k8s.io/client-go/gentype" scheme "k8s.io/client-go/kubernetes/scheme" + apply "k8s.io/client-go/util/apply" ) // DeploymentsGetter has a method to return a DeploymentInterface. @@ -118,20 +118,19 @@ func (c *deployments) ApplyScale(ctx context.Context, deploymentName string, sca return nil, fmt.Errorf("scale provided to ApplyScale must not be nil") } patchOpts := opts.ToPatchOptions() - data, err := json.Marshal(scale) + request, err := apply.NewRequest(c.GetClient(), scale) if err != nil { return nil, err } result = &extensionsv1beta1.Scale{} - err = c.GetClient().Patch(types.ApplyPatchType). + err = request. UseProtobufAsDefault(). Namespace(c.GetNamespace()). Resource("deployments"). Name(deploymentName). SubResource("scale"). VersionedParams(&patchOpts, scheme.ParameterCodec). - Body(data). Do(ctx). Into(result) return diff --git a/kubernetes/typed/extensions/v1beta1/replicaset.go b/kubernetes/typed/extensions/v1beta1/replicaset.go index 4640521a..f918be41 100644 --- a/kubernetes/typed/extensions/v1beta1/replicaset.go +++ b/kubernetes/typed/extensions/v1beta1/replicaset.go @@ -20,7 +20,6 @@ package v1beta1 import ( context "context" - json "encoding/json" fmt "fmt" extensionsv1beta1 "k8s.io/api/extensions/v1beta1" @@ -30,6 +29,7 @@ import ( applyconfigurationsextensionsv1beta1 "k8s.io/client-go/applyconfigurations/extensions/v1beta1" gentype "k8s.io/client-go/gentype" scheme "k8s.io/client-go/kubernetes/scheme" + apply "k8s.io/client-go/util/apply" ) // ReplicaSetsGetter has a method to return a ReplicaSetInterface. @@ -118,20 +118,19 @@ func (c *replicaSets) ApplyScale(ctx context.Context, replicaSetName string, sca return nil, fmt.Errorf("scale provided to ApplyScale must not be nil") } patchOpts := opts.ToPatchOptions() - data, err := json.Marshal(scale) + request, err := apply.NewRequest(c.GetClient(), scale) if err != nil { return nil, err } result = &extensionsv1beta1.Scale{} - err = c.GetClient().Patch(types.ApplyPatchType). + err = request. UseProtobufAsDefault(). Namespace(c.GetNamespace()). Resource("replicasets"). Name(replicaSetName). SubResource("scale"). VersionedParams(&patchOpts, scheme.ParameterCodec). - Body(data). Do(ctx). Into(result) return diff --git a/util/apply/apply.go b/util/apply/apply.go new file mode 100644 index 00000000..c135f559 --- /dev/null +++ b/util/apply/apply.go @@ -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 +}