mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-04 09:49:50 +00:00
Wire client feature gates affecting RESTClient content config.
This commit is contained in:
parent
8fb9622b88
commit
67b9dc1f3e
@ -17,6 +17,8 @@ limitations under the License.
|
|||||||
package rest
|
package rest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
@ -24,9 +26,11 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/munnerz/goautoneg"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
clientfeatures "k8s.io/client-go/features"
|
||||||
"k8s.io/client-go/util/flowcontrol"
|
"k8s.io/client-go/util/flowcontrol"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -115,7 +119,7 @@ func NewRESTClient(baseURL *url.URL, versionedAPIPath string, config ClientConte
|
|||||||
return &RESTClient{
|
return &RESTClient{
|
||||||
base: &base,
|
base: &base,
|
||||||
versionedAPIPath: versionedAPIPath,
|
versionedAPIPath: versionedAPIPath,
|
||||||
content: config,
|
content: scrubCBORContentConfigIfDisabled(config),
|
||||||
createBackoffMgr: readExpBackoffConfig,
|
createBackoffMgr: readExpBackoffConfig,
|
||||||
rateLimiter: rateLimiter,
|
rateLimiter: rateLimiter,
|
||||||
|
|
||||||
@ -123,6 +127,45 @@ func NewRESTClient(baseURL *url.URL, versionedAPIPath string, config ClientConte
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func scrubCBORContentConfigIfDisabled(content ClientContentConfig) ClientContentConfig {
|
||||||
|
if clientfeatures.TestOnlyFeatureGates.Enabled(clientfeatures.TestOnlyClientAllowsCBOR) {
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
if mediatype, _, err := mime.ParseMediaType(content.ContentType); err == nil && mediatype == "application/cbor" {
|
||||||
|
content.ContentType = "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
clauses := goautoneg.ParseAccept(content.AcceptContentTypes)
|
||||||
|
scrubbed := false
|
||||||
|
for i, clause := range clauses {
|
||||||
|
if clause.Type == "application" && clause.SubType == "cbor" {
|
||||||
|
scrubbed = true
|
||||||
|
clauses[i].SubType = "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !scrubbed {
|
||||||
|
// No application/cbor in AcceptContentTypes, nothing more to do.
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := make([]string, 0, len(clauses))
|
||||||
|
for _, clause := range clauses {
|
||||||
|
// ParseAccept does not store the parameter "q" in Params.
|
||||||
|
params := clause.Params
|
||||||
|
if clause.Q < 1 { // omit q=1, it's the default
|
||||||
|
if params == nil {
|
||||||
|
params = make(map[string]string, 1)
|
||||||
|
}
|
||||||
|
params["q"] = strconv.FormatFloat(clause.Q, 'g', 3, 32)
|
||||||
|
}
|
||||||
|
parts = append(parts, mime.FormatMediaType(fmt.Sprintf("%s/%s", clause.Type, clause.SubType), params))
|
||||||
|
}
|
||||||
|
content.AcceptContentTypes = strings.Join(parts, ",")
|
||||||
|
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
// GetRateLimiter returns rate limiter for a given client, or nil if it's called on a nil client
|
// GetRateLimiter returns rate limiter for a given client, or nil if it's called on a nil client
|
||||||
func (c *RESTClient) GetRateLimiter() flowcontrol.RateLimiter {
|
func (c *RESTClient) GetRateLimiter() flowcontrol.RateLimiter {
|
||||||
if c == nil {
|
if c == nil {
|
||||||
|
@ -160,6 +160,9 @@ func NewRequest(c *RESTClient) *Request {
|
|||||||
contentTypeNotSet := len(contentConfig.ContentType) == 0
|
contentTypeNotSet := len(contentConfig.ContentType) == 0
|
||||||
if contentTypeNotSet {
|
if contentTypeNotSet {
|
||||||
contentConfig.ContentType = "application/json"
|
contentConfig.ContentType = "application/json"
|
||||||
|
if clientfeatures.TestOnlyFeatureGates.Enabled(clientfeatures.TestOnlyClientAllowsCBOR) && clientfeatures.TestOnlyFeatureGates.Enabled(clientfeatures.TestOnlyClientPrefersCBOR) {
|
||||||
|
contentConfig.ContentType = "application/cbor"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
r := &Request{
|
r := &Request{
|
||||||
|
@ -20,6 +20,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"net/http"
|
||||||
"reflect"
|
"reflect"
|
||||||
rt "runtime"
|
rt "runtime"
|
||||||
"strings"
|
"strings"
|
||||||
@ -28,9 +29,12 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
|
|
||||||
appsv1 "k8s.io/api/apps/v1"
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
eventsv1 "k8s.io/api/events/v1"
|
eventsv1 "k8s.io/api/events/v1"
|
||||||
|
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||||
|
apiextensionsv1client "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1"
|
||||||
"k8s.io/apimachinery/pkg/api/equality"
|
"k8s.io/apimachinery/pkg/api/equality"
|
||||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
"k8s.io/apimachinery/pkg/api/resource"
|
"k8s.io/apimachinery/pkg/api/resource"
|
||||||
@ -45,14 +49,22 @@ import (
|
|||||||
appsv1ac "k8s.io/client-go/applyconfigurations/apps/v1"
|
appsv1ac "k8s.io/client-go/applyconfigurations/apps/v1"
|
||||||
corev1ac "k8s.io/client-go/applyconfigurations/core/v1"
|
corev1ac "k8s.io/client-go/applyconfigurations/core/v1"
|
||||||
metav1ac "k8s.io/client-go/applyconfigurations/meta/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"
|
clientset "k8s.io/client-go/kubernetes"
|
||||||
"k8s.io/utils/pointer"
|
clientscheme "k8s.io/client-go/kubernetes/scheme"
|
||||||
|
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||||
|
"k8s.io/client-go/rest"
|
||||||
|
"k8s.io/client-go/util/retry"
|
||||||
utilversion "k8s.io/component-base/version"
|
utilversion "k8s.io/component-base/version"
|
||||||
kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
|
kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
|
||||||
"k8s.io/kubernetes/pkg/api/legacyscheme"
|
"k8s.io/kubernetes/pkg/api/legacyscheme"
|
||||||
"k8s.io/kubernetes/test/integration/framework"
|
"k8s.io/kubernetes/test/integration/framework"
|
||||||
imageutils "k8s.io/kubernetes/test/utils/image"
|
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"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestClient(t *testing.T) {
|
func TestClient(t *testing.T) {
|
||||||
@ -147,7 +159,7 @@ func TestAtomicPut(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Spec: v1.ReplicationControllerSpec{
|
Spec: v1.ReplicationControllerSpec{
|
||||||
Replicas: pointer.Int32(0),
|
Replicas: ptr.To(int32(0)),
|
||||||
Selector: map[string]string{
|
Selector: map[string]string{
|
||||||
"foo": "bar",
|
"foo": "bar",
|
||||||
},
|
},
|
||||||
@ -1347,3 +1359,494 @@ func TestExtractModifyApply_ForceOwnership(t *testing.T) {
|
|||||||
t.Errorf("createMgrExtracted apply configuration did not match expected, got:\n%s\n", cmp.Diff(expectedCreateExtracted, createMgrExtracted))
|
t.Errorf("createMgrExtracted apply configuration did not match expected, got:\n%s\n", cmp.Diff(expectedCreateExtracted, createMgrExtracted))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGeneratedClientCBOREnablement(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)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = clientset.CoreV1().Namespaces().Create(
|
||||||
|
context.TODO(),
|
||||||
|
&v1.Namespace{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test-generated-client-cbor-enablement",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
metav1.CreateOptions{DryRun: []string{metav1.DryRunAll}},
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
DoRequestWithGeneratedClient := func(t *testing.T, config *rest.Config) error {
|
||||||
|
// This is using a generated client from sample-apiserver because it is generated
|
||||||
|
// without --prefer-protobuf. For convenience, the test serves the API as a CRD with
|
||||||
|
// a permissive schema instead of running a real aggregated sample-apiserver.
|
||||||
|
wardleClient, err := wardlev1alpha1client.NewForConfig(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = wardleClient.Fischers().Create(
|
||||||
|
context.TODO(),
|
||||||
|
&wardlev1alpha1.Fischer{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "test-generated-client-cbor-enablement"},
|
||||||
|
},
|
||||||
|
metav1.CreateOptions{DryRun: []string{metav1.DryRunAll}},
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type testCase struct {
|
||||||
|
name string
|
||||||
|
served bool
|
||||||
|
allowed bool
|
||||||
|
preferred bool
|
||||||
|
configuredContentType string
|
||||||
|
configuredAccept string
|
||||||
|
wantRequestContentType string
|
||||||
|
wantRequestAccept string
|
||||||
|
wantResponseContentType string
|
||||||
|
wantResponseStatus int
|
||||||
|
wantStatusError bool
|
||||||
|
doRequest func(t *testing.T, config *rest.Config) error
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []testCase{
|
||||||
|
{
|
||||||
|
name: "cbor allowed and preferred client forces protobuf",
|
||||||
|
served: true,
|
||||||
|
allowed: true,
|
||||||
|
preferred: true,
|
||||||
|
wantRequestContentType: "application/vnd.kubernetes.protobuf",
|
||||||
|
wantRequestAccept: "application/vnd.kubernetes.protobuf,application/json",
|
||||||
|
wantResponseContentType: "application/vnd.kubernetes.protobuf",
|
||||||
|
wantResponseStatus: http.StatusCreated,
|
||||||
|
wantStatusError: false,
|
||||||
|
doRequest: DoRequestWithProtobufPreferredGeneratedClient,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cbor allowed and not preferred client forces protobuf",
|
||||||
|
served: true,
|
||||||
|
allowed: true,
|
||||||
|
preferred: false,
|
||||||
|
wantRequestContentType: "application/vnd.kubernetes.protobuf",
|
||||||
|
wantRequestAccept: "application/vnd.kubernetes.protobuf,application/json",
|
||||||
|
wantResponseContentType: "application/vnd.kubernetes.protobuf",
|
||||||
|
wantResponseStatus: http.StatusCreated,
|
||||||
|
wantStatusError: false,
|
||||||
|
doRequest: DoRequestWithProtobufPreferredGeneratedClient,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cbor not allowed and not preferred client forces protobuf",
|
||||||
|
served: true,
|
||||||
|
allowed: false,
|
||||||
|
preferred: false,
|
||||||
|
wantRequestContentType: "application/vnd.kubernetes.protobuf",
|
||||||
|
wantRequestAccept: "application/vnd.kubernetes.protobuf,application/json",
|
||||||
|
wantResponseContentType: "application/vnd.kubernetes.protobuf",
|
||||||
|
wantResponseStatus: http.StatusCreated,
|
||||||
|
wantStatusError: false,
|
||||||
|
doRequest: DoRequestWithProtobufPreferredGeneratedClient,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cbor not allowed and preferred client forces protobuf",
|
||||||
|
served: true,
|
||||||
|
allowed: false,
|
||||||
|
preferred: true,
|
||||||
|
wantRequestContentType: "application/vnd.kubernetes.protobuf",
|
||||||
|
wantRequestAccept: "application/vnd.kubernetes.protobuf,application/json",
|
||||||
|
wantResponseContentType: "application/vnd.kubernetes.protobuf",
|
||||||
|
wantResponseStatus: http.StatusCreated,
|
||||||
|
wantStatusError: false,
|
||||||
|
doRequest: DoRequestWithProtobufPreferredGeneratedClient,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fully disabled",
|
||||||
|
served: true,
|
||||||
|
allowed: false,
|
||||||
|
preferred: false,
|
||||||
|
wantRequestContentType: "application/json",
|
||||||
|
wantRequestAccept: "application/json, */*",
|
||||||
|
wantResponseContentType: "application/json",
|
||||||
|
wantResponseStatus: http.StatusCreated,
|
||||||
|
wantStatusError: false,
|
||||||
|
doRequest: DoRequestWithGeneratedClient,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "send json accept both get json",
|
||||||
|
served: true,
|
||||||
|
allowed: true,
|
||||||
|
preferred: false,
|
||||||
|
wantRequestContentType: "application/json",
|
||||||
|
wantRequestAccept: "application/json, */*",
|
||||||
|
wantResponseContentType: "application/json",
|
||||||
|
wantResponseStatus: http.StatusCreated,
|
||||||
|
wantStatusError: false,
|
||||||
|
doRequest: DoRequestWithGeneratedClient,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "send json accept both get json",
|
||||||
|
served: false,
|
||||||
|
allowed: true,
|
||||||
|
preferred: false,
|
||||||
|
wantRequestContentType: "application/json",
|
||||||
|
wantRequestAccept: "application/json, */*",
|
||||||
|
wantResponseContentType: "application/json",
|
||||||
|
wantResponseStatus: http.StatusCreated,
|
||||||
|
wantStatusError: false,
|
||||||
|
doRequest: DoRequestWithGeneratedClient,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "send cbor accept both get cbor",
|
||||||
|
served: true,
|
||||||
|
allowed: true,
|
||||||
|
preferred: true,
|
||||||
|
wantRequestContentType: "application/cbor",
|
||||||
|
wantRequestAccept: "application/cbor, */*",
|
||||||
|
wantResponseContentType: "application/cbor",
|
||||||
|
wantResponseStatus: http.StatusCreated,
|
||||||
|
wantStatusError: false,
|
||||||
|
doRequest: DoRequestWithGeneratedClient,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "send cbor accept both get 415",
|
||||||
|
served: false,
|
||||||
|
allowed: true,
|
||||||
|
preferred: true,
|
||||||
|
wantRequestContentType: "application/cbor",
|
||||||
|
wantRequestAccept: "application/cbor, */*",
|
||||||
|
wantResponseContentType: "application/json",
|
||||||
|
wantResponseStatus: http.StatusUnsupportedMediaType,
|
||||||
|
wantStatusError: true,
|
||||||
|
doRequest: DoRequestWithGeneratedClient,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "both gates required to send cbor",
|
||||||
|
served: true,
|
||||||
|
allowed: false,
|
||||||
|
preferred: true,
|
||||||
|
wantRequestContentType: "application/json",
|
||||||
|
wantRequestAccept: "application/json, */*",
|
||||||
|
wantResponseContentType: "application/json",
|
||||||
|
wantResponseStatus: http.StatusCreated,
|
||||||
|
wantStatusError: false,
|
||||||
|
doRequest: DoRequestWithGeneratedClient,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "actively configured cbor",
|
||||||
|
served: true,
|
||||||
|
allowed: true,
|
||||||
|
preferred: false,
|
||||||
|
configuredContentType: "application/cbor",
|
||||||
|
configuredAccept: "application/cbor",
|
||||||
|
wantRequestContentType: "application/cbor",
|
||||||
|
wantRequestAccept: "application/cbor",
|
||||||
|
wantResponseContentType: "application/cbor",
|
||||||
|
wantResponseStatus: http.StatusCreated,
|
||||||
|
wantStatusError: false,
|
||||||
|
doRequest: DoRequestWithGeneratedClient,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "force disable actively configured cbor",
|
||||||
|
served: true,
|
||||||
|
allowed: false,
|
||||||
|
preferred: false,
|
||||||
|
configuredContentType: "application/cbor",
|
||||||
|
configuredAccept: "application/cbor",
|
||||||
|
wantRequestContentType: "application/json",
|
||||||
|
wantRequestAccept: "application/json",
|
||||||
|
wantResponseContentType: "application/json",
|
||||||
|
wantResponseStatus: http.StatusCreated,
|
||||||
|
wantStatusError: false,
|
||||||
|
doRequest: DoRequestWithGeneratedClient,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "actively configured cbor with two accepted media types",
|
||||||
|
served: true,
|
||||||
|
allowed: true,
|
||||||
|
preferred: false,
|
||||||
|
configuredContentType: "application/cbor",
|
||||||
|
configuredAccept: "application/cbor;q=0.9,example/foo;q=0.8",
|
||||||
|
wantRequestContentType: "application/cbor",
|
||||||
|
wantRequestAccept: "application/cbor;q=0.9,example/foo;q=0.8",
|
||||||
|
wantResponseContentType: "application/cbor",
|
||||||
|
wantResponseStatus: http.StatusCreated,
|
||||||
|
wantStatusError: false,
|
||||||
|
doRequest: DoRequestWithGeneratedClient,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "force disable actively configured cbor with two accepted media types",
|
||||||
|
served: true,
|
||||||
|
allowed: false,
|
||||||
|
preferred: false,
|
||||||
|
configuredContentType: "application/cbor",
|
||||||
|
configuredAccept: "application/cbor;q=0.9,example/foo;q=0.8",
|
||||||
|
wantRequestContentType: "application/json",
|
||||||
|
wantRequestAccept: "application/json; q=0.9,example/foo; q=0.8",
|
||||||
|
wantResponseContentType: "application/json",
|
||||||
|
wantResponseStatus: http.StatusCreated,
|
||||||
|
wantStatusError: false,
|
||||||
|
doRequest: DoRequestWithGeneratedClient,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, served := range []bool{true, false} {
|
||||||
|
t.Run(fmt.Sprintf("served=%t", served), func(t *testing.T) {
|
||||||
|
// Batch test cases with their server configuration instead of starting and stopping
|
||||||
|
// a new apiserver for each test case.
|
||||||
|
if served {
|
||||||
|
framework.EnableCBORServingAndStorageForTest(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
server := kubeapiservertesting.StartTestServerOrDie(t, nil, framework.DefaultTestServerFlags(), framework.SharedEtcd())
|
||||||
|
defer server.TearDownFn()
|
||||||
|
|
||||||
|
apiextensionsClient, err := apiextensionsv1client.NewForConfig(server.ClientConfig)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
crd, err := apiextensionsClient.CustomResourceDefinitions().Create(
|
||||||
|
context.TODO(),
|
||||||
|
&apiextensionsv1.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: fmt.Sprintf("fischers.%s", wardlev1alpha1.SchemeGroupVersion.Group)},
|
||||||
|
Spec: apiextensionsv1.CustomResourceDefinitionSpec{
|
||||||
|
Group: wardlev1alpha1.SchemeGroupVersion.Group,
|
||||||
|
Versions: []apiextensionsv1.CustomResourceDefinitionVersion{{
|
||||||
|
Name: wardlev1alpha1.SchemeGroupVersion.Version,
|
||||||
|
Served: true,
|
||||||
|
Storage: true,
|
||||||
|
Schema: &apiextensionsv1.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
|
||||||
|
XPreserveUnknownFields: ptr.To(true),
|
||||||
|
Type: "object",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
Names: apiextensionsv1.CustomResourceDefinitionNames{
|
||||||
|
Plural: "fischers",
|
||||||
|
Singular: "fischer",
|
||||||
|
Kind: "Fischer",
|
||||||
|
ListKind: "FischerList",
|
||||||
|
},
|
||||||
|
Scope: apiextensionsv1.ClusterScoped,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
metav1.CreateOptions{},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait to see cr in discovery
|
||||||
|
discoveryClient, err := discovery.NewDiscoveryClientForConfig(server.ClientConfig)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := wait.PollUntilContextTimeout(context.TODO(), 100*time.Millisecond, 5*time.Second, true, func(context.Context) (done bool, err error) {
|
||||||
|
resources, err := discoveryClient.ServerResourcesForGroupVersion(wardlev1alpha1.SchemeGroupVersion.String())
|
||||||
|
if err != nil {
|
||||||
|
if apierrors.IsNotFound(err) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
for _, resource := range resources.APIResources {
|
||||||
|
if resource.Name == crd.Spec.Names.Plural {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
if tc.served != served {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
framework.SetTestOnlyCBORClientFeatureGatesForTest(t, tc.allowed, tc.preferred)
|
||||||
|
|
||||||
|
config := rest.CopyConfig(server.ClientConfig)
|
||||||
|
config.ContentType = tc.configuredContentType
|
||||||
|
config.AcceptContentTypes = tc.configuredAccept
|
||||||
|
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 && apierrors.IsUnsupportedMediaType(err):
|
||||||
|
// ok
|
||||||
|
case !tc.wantStatusError && err == nil:
|
||||||
|
// ok
|
||||||
|
default:
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCBORWithTypedClient(t *testing.T) {
|
||||||
|
ktesting.SetDefaultVerbosity(10) // todo
|
||||||
|
|
||||||
|
framework.EnableCBORServingAndStorageForTest(t)
|
||||||
|
framework.SetTestOnlyCBORClientFeatureGatesForTest(t, true, true)
|
||||||
|
|
||||||
|
server := kubeapiservertesting.StartTestServerOrDie(t, nil, framework.DefaultTestServerFlags(), framework.SharedEtcd())
|
||||||
|
t.Cleanup(server.TearDownFn)
|
||||||
|
|
||||||
|
const TestNamespace = "test-cbor-typed-client"
|
||||||
|
|
||||||
|
{
|
||||||
|
// Setup using client with default config.
|
||||||
|
clientset, err := clientset.NewForConfig(server.ClientConfig)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := clientset.CoreV1().Namespaces().Delete(context.TODO(), TestNamespace, metav1.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if _, err := clientset.CoreV1().Namespaces().Create(context.TODO(), &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: TestNamespace}}, metav1.CreateOptions{}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config := rest.CopyConfig(server.ClientConfig)
|
||||||
|
// Content negotiation controlled by client feature gates.
|
||||||
|
config.ContentType = ""
|
||||||
|
config.AcceptContentTypes = ""
|
||||||
|
config.Wrap(framework.AssertRequestResponseAsCBOR(t))
|
||||||
|
clientset, err := clientset.NewForConfig(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should be identical to
|
||||||
|
// https://github.com/kubernetes/kubernetes/blob/9ec52fc06395e6ac2fd7a947d6b9fbd3f1bbacb3/staging/src/k8s.io/client-go/kubernetes/typed/core/v1/namespace.go#L64-L72
|
||||||
|
// minus the PrefersProtobuf option, which overrides content negotiation to Protobuf on a
|
||||||
|
// per-request basis.
|
||||||
|
var secretClient corev1client.SecretInterface = gentype.NewClientWithListAndApply[*v1.Secret, *v1.SecretList, *corev1ac.SecretApplyConfiguration](
|
||||||
|
"secrets",
|
||||||
|
clientset.CoreV1().RESTClient(),
|
||||||
|
clientscheme.ParameterCodec,
|
||||||
|
TestNamespace,
|
||||||
|
func() *v1.Secret { return &v1.Secret{} },
|
||||||
|
func() *v1.SecretList { return &v1.SecretList{} },
|
||||||
|
)
|
||||||
|
|
||||||
|
secret, err := secretClient.Create(context.TODO(), &v1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test-secret",
|
||||||
|
},
|
||||||
|
}, metav1.CreateOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
w, err := secretClient.Watch(context.TODO(), metav1.ListOptions{ResourceVersion: secret.ResourceVersion, FieldSelector: fmt.Sprintf("metadata.name=%s", secret.GetName())})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer w.Stop()
|
||||||
|
|
||||||
|
// do a real update to observe a watch event
|
||||||
|
if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error {
|
||||||
|
s, err := secretClient.Get(context.TODO(), secret.GetName(), metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if s.Annotations == nil {
|
||||||
|
s.Annotations = map[string]string{}
|
||||||
|
}
|
||||||
|
s.Annotations["foo"] = "bar"
|
||||||
|
_, err = secretClient.Update(context.TODO(), s, metav1.UpdateOptions{})
|
||||||
|
return err
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var seen bool
|
||||||
|
timeout := time.After(5 * time.Second)
|
||||||
|
for !seen {
|
||||||
|
select {
|
||||||
|
case e, ok := <-w.ResultChan():
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("watch closed without receiving expected event")
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Type == watch.Error {
|
||||||
|
t.Fatalf("watch received unexpected error event: %v", apierrors.FromObject(e.Object))
|
||||||
|
}
|
||||||
|
|
||||||
|
if ns, ok := e.Object.(*v1.Secret); ok && ns.GetAnnotations()["foo"] == "bar" {
|
||||||
|
// observed update
|
||||||
|
seen = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case <-timeout:
|
||||||
|
t.Fatal("timed out waiting for event")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := secretClient.Delete(context.TODO(), secret.GetName(), metav1.DeleteOptions{DryRun: []string{metav1.DryRunAll}}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := secretClient.DeleteCollection(context.TODO(), metav1.DeleteOptions{}, metav1.ListOptions{LabelSelector: "a,!a"}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := secretClient.Get(context.TODO(), secret.GetName(), metav1.GetOptions{}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := secretClient.List(context.TODO(), metav1.ListOptions{}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// for UpdateStatus
|
||||||
|
nsClient := gentype.NewClientWithListAndApply[*v1.Namespace, *v1.NamespaceList, *corev1ac.NamespaceApplyConfiguration](
|
||||||
|
"namespaces",
|
||||||
|
clientset.CoreV1().RESTClient(),
|
||||||
|
clientscheme.ParameterCodec,
|
||||||
|
"",
|
||||||
|
func() *v1.Namespace { return &v1.Namespace{} },
|
||||||
|
func() *v1.NamespaceList { return &v1.NamespaceList{} },
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error {
|
||||||
|
ns, err := nsClient.Get(context.TODO(), TestNamespace, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = nsClient.UpdateStatus(context.TODO(), ns, metav1.UpdateOptions{DryRun: []string{metav1.DryRunAll}})
|
||||||
|
return err
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user