diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_discovery_controller.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_discovery_controller.go index 9e663ac6cc9..fc61d5c19e7 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_discovery_controller.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_discovery_controller.go @@ -23,7 +23,7 @@ import ( "k8s.io/klog/v2" - apidiscoveryv2beta1 "k8s.io/api/apidiscovery/v2beta1" + apidiscoveryv2 "k8s.io/api/apidiscovery/v2" autoscaling "k8s.io/api/autoscaling/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" @@ -87,7 +87,7 @@ func (c *DiscoveryController) sync(version schema.GroupVersion) error { apiVersionsForDiscovery := []metav1.GroupVersionForDiscovery{} apiResourcesForDiscovery := []metav1.APIResource{} - aggregatedApiResourcesForDiscovery := []apidiscoveryv2beta1.APIResourceDiscovery{} + aggregatedAPIResourcesForDiscovery := []apidiscoveryv2.APIResourceDiscovery{} versionsForDiscoveryMap := map[metav1.GroupVersion]bool{} crds, err := c.crdLister.List(labels.Everything()) @@ -158,13 +158,13 @@ func (c *DiscoveryController) sync(version schema.GroupVersion) error { } if c.resourceManager != nil { - var scope apidiscoveryv2beta1.ResourceScope + var scope apidiscoveryv2.ResourceScope if crd.Spec.Scope == apiextensionsv1.NamespaceScoped { - scope = apidiscoveryv2beta1.ScopeNamespace + scope = apidiscoveryv2.ScopeNamespace } else { - scope = apidiscoveryv2beta1.ScopeCluster + scope = apidiscoveryv2.ScopeCluster } - apiResourceDiscovery := apidiscoveryv2beta1.APIResourceDiscovery{ + apiResourceDiscovery := apidiscoveryv2.APIResourceDiscovery{ Resource: crd.Status.AcceptedNames.Plural, SingularResource: crd.Status.AcceptedNames.Singular, Scope: scope, @@ -178,7 +178,7 @@ func (c *DiscoveryController) sync(version schema.GroupVersion) error { Categories: crd.Status.AcceptedNames.Categories, } if subresources != nil && subresources.Status != nil { - apiResourceDiscovery.Subresources = append(apiResourceDiscovery.Subresources, apidiscoveryv2beta1.APISubresourceDiscovery{ + apiResourceDiscovery.Subresources = append(apiResourceDiscovery.Subresources, apidiscoveryv2.APISubresourceDiscovery{ Subresource: "status", ResponseKind: &metav1.GroupVersionKind{ Group: version.Group, @@ -189,7 +189,7 @@ func (c *DiscoveryController) sync(version schema.GroupVersion) error { }) } if subresources != nil && subresources.Scale != nil { - apiResourceDiscovery.Subresources = append(apiResourceDiscovery.Subresources, apidiscoveryv2beta1.APISubresourceDiscovery{ + apiResourceDiscovery.Subresources = append(apiResourceDiscovery.Subresources, apidiscoveryv2.APISubresourceDiscovery{ Subresource: "scale", ResponseKind: &metav1.GroupVersionKind{ Group: autoscaling.GroupName, @@ -200,7 +200,7 @@ func (c *DiscoveryController) sync(version schema.GroupVersion) error { }) } - aggregatedApiResourcesForDiscovery = append(aggregatedApiResourcesForDiscovery, apiResourceDiscovery) + aggregatedAPIResourcesForDiscovery = append(aggregatedAPIResourcesForDiscovery, apiResourceDiscovery) } if subresources != nil && subresources.Status != nil { @@ -260,14 +260,14 @@ func (c *DiscoveryController) sync(version schema.GroupVersion) error { return apiResourcesForDiscovery }))) - sort.Slice(aggregatedApiResourcesForDiscovery[:], func(i, j int) bool { - return aggregatedApiResourcesForDiscovery[i].Resource < aggregatedApiResourcesForDiscovery[j].Resource + sort.Slice(aggregatedAPIResourcesForDiscovery, func(i, j int) bool { + return aggregatedAPIResourcesForDiscovery[i].Resource < aggregatedAPIResourcesForDiscovery[j].Resource }) if c.resourceManager != nil { - c.resourceManager.AddGroupVersion(version.Group, apidiscoveryv2beta1.APIVersionDiscovery{ - Freshness: apidiscoveryv2beta1.DiscoveryFreshnessCurrent, + c.resourceManager.AddGroupVersion(version.Group, apidiscoveryv2.APIVersionDiscovery{ + Freshness: apidiscoveryv2.DiscoveryFreshnessCurrent, Version: version.Version, - Resources: aggregatedApiResourcesForDiscovery, + Resources: aggregatedAPIResourcesForDiscovery, }) // Default priority for CRDs c.resourceManager.SetGroupVersionPriority(metav1.GroupVersion(version), 1000, 100) diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_discovery_controller_test.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_discovery_controller_test.go index d51b2694190..4ca778e7c2b 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_discovery_controller_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_discovery_controller_test.go @@ -22,7 +22,7 @@ import ( "time" "github.com/stretchr/testify/require" - apidiscoveryv2beta1 "k8s.io/api/apidiscovery/v2beta1" + apidiscoveryv2 "k8s.io/api/apidiscovery/v2" v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake" @@ -125,13 +125,13 @@ var coolBarCRD = &v1.CustomResourceDefinition{ }, } -var coolFooDiscovery apidiscoveryv2beta1.APIVersionDiscovery = apidiscoveryv2beta1.APIVersionDiscovery{ +var coolFooDiscovery apidiscoveryv2.APIVersionDiscovery = apidiscoveryv2.APIVersionDiscovery{ Version: "v1", - Freshness: apidiscoveryv2beta1.DiscoveryFreshnessCurrent, - Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ + Freshness: apidiscoveryv2.DiscoveryFreshnessCurrent, + Resources: []apidiscoveryv2.APIResourceDiscovery{ { Resource: "coolfoos", - Scope: apidiscoveryv2beta1.ScopeCluster, + Scope: apidiscoveryv2.ScopeCluster, SingularResource: "coolfoo", Verbs: []string{"delete", "deletecollection", "get", "list", "patch", "create", "update", "watch"}, ShortNames: []string{"foo"}, @@ -141,7 +141,7 @@ var coolFooDiscovery apidiscoveryv2beta1.APIVersionDiscovery = apidiscoveryv2bet Version: "v1", Kind: "CoolFoo", }, - Subresources: []apidiscoveryv2beta1.APISubresourceDiscovery{ + Subresources: []apidiscoveryv2.APISubresourceDiscovery{ { Subresource: "status", Verbs: []string{"get", "patch", "update"}, @@ -157,13 +157,13 @@ var coolFooDiscovery apidiscoveryv2beta1.APIVersionDiscovery = apidiscoveryv2bet }, } -var mergedDiscovery apidiscoveryv2beta1.APIVersionDiscovery = apidiscoveryv2beta1.APIVersionDiscovery{ +var mergedDiscovery apidiscoveryv2.APIVersionDiscovery = apidiscoveryv2.APIVersionDiscovery{ Version: "v1", - Freshness: apidiscoveryv2beta1.DiscoveryFreshnessCurrent, - Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ + Freshness: apidiscoveryv2.DiscoveryFreshnessCurrent, + Resources: []apidiscoveryv2.APIResourceDiscovery{ { Resource: "coolbars", - Scope: apidiscoveryv2beta1.ScopeCluster, + Scope: apidiscoveryv2.ScopeCluster, SingularResource: "coolbar", Verbs: []string{"delete", "deletecollection", "get", "list", "patch", "create", "update", "watch"}, ShortNames: []string{"bar"}, @@ -175,7 +175,7 @@ var mergedDiscovery apidiscoveryv2beta1.APIVersionDiscovery = apidiscoveryv2beta }, }, { Resource: "coolfoos", - Scope: apidiscoveryv2beta1.ScopeCluster, + Scope: apidiscoveryv2.ScopeCluster, SingularResource: "coolfoo", Verbs: []string{"delete", "deletecollection", "get", "list", "patch", "create", "update", "watch"}, ShortNames: []string{"foo"}, @@ -185,7 +185,7 @@ var mergedDiscovery apidiscoveryv2beta1.APIVersionDiscovery = apidiscoveryv2beta Version: "v1", Kind: "CoolFoo", }, - Subresources: []apidiscoveryv2beta1.APISubresourceDiscovery{ + Subresources: []apidiscoveryv2.APISubresourceDiscovery{ { Subresource: "status", Verbs: []string{"get", "patch", "update"}, diff --git a/staging/src/k8s.io/apiserver/pkg/apis/apidiscovery/v2/conversion.go b/staging/src/k8s.io/apiserver/pkg/apis/apidiscovery/v2/conversion.go new file mode 100644 index 00000000000..e74323e5ced --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/apis/apidiscovery/v2/conversion.go @@ -0,0 +1,226 @@ +/* +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. +*/ + +// This file was duplicated from the auto-generated file by conversion-gen in +// k8s.io/kubernetes/pkg/apis/apidiscovery Unlike most k8s types discovery is +// served by all apiservers and conversion is needed by all apiservers. The +// concept of internal/hub type does not exist for discovery as we work directly +// with the versioned types. + +// The conversion code here facilities conversion strictly between v2beta1 and +// v2 types. It is only necessary in k8s versions where mixed state could be +// possible before the full removal of the v2beta1 types. It is placed in this +// directory such that all apiservers can benefit from the conversion without +// having to implement their own if the client/server they're communicating with +// only supports one version. + +// Once the v2beta1 types are removed (intended for Kubernetes v1.33), this file +// will be removed. +package v2 + +import ( + unsafe "unsafe" + + v2 "k8s.io/api/apidiscovery/v2" + v2beta1 "k8s.io/api/apidiscovery/v2beta1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + conversion "k8s.io/apimachinery/pkg/conversion" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// RegisterConversions adds conversion functions to the given scheme. +// Public to allow building arbitrary schemes. +func RegisterConversions(s *runtime.Scheme) error { + if err := s.AddGeneratedConversionFunc((*v2beta1.APIGroupDiscovery)(nil), (*v2.APIGroupDiscovery)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convertv2beta1APIGroupDiscoveryTov2APIGroupDiscovery(a.(*v2beta1.APIGroupDiscovery), b.(*v2.APIGroupDiscovery), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*v2.APIGroupDiscovery)(nil), (*v2beta1.APIGroupDiscovery)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convertv2APIGroupDiscoveryTov2beta1APIGroupDiscovery(a.(*v2.APIGroupDiscovery), b.(*v2beta1.APIGroupDiscovery), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*v2beta1.APIGroupDiscoveryList)(nil), (*v2.APIGroupDiscoveryList)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convertv2beta1APIGroupDiscoveryListTov2APIGroupDiscoveryList(a.(*v2beta1.APIGroupDiscoveryList), b.(*v2.APIGroupDiscoveryList), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*v2.APIGroupDiscoveryList)(nil), (*v2beta1.APIGroupDiscoveryList)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convertv2APIGroupDiscoveryListTov2beta1APIGroupDiscoveryList(a.(*v2.APIGroupDiscoveryList), b.(*v2beta1.APIGroupDiscoveryList), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*v2beta1.APIResourceDiscovery)(nil), (*v2.APIResourceDiscovery)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convertv2beta1APIResourceDiscoveryTov2APIResourceDiscovery(a.(*v2beta1.APIResourceDiscovery), b.(*v2.APIResourceDiscovery), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*v2.APIResourceDiscovery)(nil), (*v2beta1.APIResourceDiscovery)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convertv2APIResourceDiscoveryTov2beta1APIResourceDiscovery(a.(*v2.APIResourceDiscovery), b.(*v2beta1.APIResourceDiscovery), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*v2beta1.APISubresourceDiscovery)(nil), (*v2.APISubresourceDiscovery)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convertv2beta1APISubresourceDiscoveryTov2APISubresourceDiscovery(a.(*v2beta1.APISubresourceDiscovery), b.(*v2.APISubresourceDiscovery), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*v2.APISubresourceDiscovery)(nil), (*v2beta1.APISubresourceDiscovery)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convertv2APISubresourceDiscoveryTov2beta1APISubresourceDiscovery(a.(*v2.APISubresourceDiscovery), b.(*v2beta1.APISubresourceDiscovery), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*v2beta1.APIVersionDiscovery)(nil), (*v2.APIVersionDiscovery)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convertv2beta1APIVersionDiscoveryTov2APIVersionDiscovery(a.(*v2beta1.APIVersionDiscovery), b.(*v2.APIVersionDiscovery), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*v2.APIVersionDiscovery)(nil), (*v2beta1.APIVersionDiscovery)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convertv2APIVersionDiscoveryTov2beta1APIVersionDiscovery(a.(*v2.APIVersionDiscovery), b.(*v2beta1.APIVersionDiscovery), scope) + }); err != nil { + return err + } + return nil +} + +func autoConvertv2beta1APIGroupDiscoveryTov2APIGroupDiscovery(in *v2beta1.APIGroupDiscovery, out *v2.APIGroupDiscovery, s conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + out.Versions = *(*[]v2.APIVersionDiscovery)(unsafe.Pointer(&in.Versions)) + return nil +} + +// Convertv2beta1APIGroupDiscoveryTov2APIGroupDiscovery is an autogenerated conversion function. +func Convertv2beta1APIGroupDiscoveryTov2APIGroupDiscovery(in *v2beta1.APIGroupDiscovery, out *v2.APIGroupDiscovery, s conversion.Scope) error { + return autoConvertv2beta1APIGroupDiscoveryTov2APIGroupDiscovery(in, out, s) +} + +func autoConvertv2APIGroupDiscoveryTov2beta1APIGroupDiscovery(in *v2.APIGroupDiscovery, out *v2beta1.APIGroupDiscovery, s conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + out.Versions = *(*[]v2beta1.APIVersionDiscovery)(unsafe.Pointer(&in.Versions)) + return nil +} + +// Convertv2APIGroupDiscoveryTov2beta1APIGroupDiscovery is an autogenerated conversion function. +func Convertv2APIGroupDiscoveryTov2beta1APIGroupDiscovery(in *v2.APIGroupDiscovery, out *v2beta1.APIGroupDiscovery, s conversion.Scope) error { + return autoConvertv2APIGroupDiscoveryTov2beta1APIGroupDiscovery(in, out, s) +} + +func autoConvertv2beta1APIGroupDiscoveryListTov2APIGroupDiscoveryList(in *v2beta1.APIGroupDiscoveryList, out *v2.APIGroupDiscoveryList, s conversion.Scope) error { + out.ListMeta = in.ListMeta + out.Items = *(*[]v2.APIGroupDiscovery)(unsafe.Pointer(&in.Items)) + return nil +} + +// Convertv2beta1APIGroupDiscoveryListTov2APIGroupDiscoveryList is an autogenerated conversion function. +func Convertv2beta1APIGroupDiscoveryListTov2APIGroupDiscoveryList(in *v2beta1.APIGroupDiscoveryList, out *v2.APIGroupDiscoveryList, s conversion.Scope) error { + return autoConvertv2beta1APIGroupDiscoveryListTov2APIGroupDiscoveryList(in, out, s) +} + +func autoConvertv2APIGroupDiscoveryListTov2beta1APIGroupDiscoveryList(in *v2.APIGroupDiscoveryList, out *v2beta1.APIGroupDiscoveryList, s conversion.Scope) error { + out.ListMeta = in.ListMeta + out.Items = *(*[]v2beta1.APIGroupDiscovery)(unsafe.Pointer(&in.Items)) + return nil +} + +// Convertv2APIGroupDiscoveryListTov2beta1APIGroupDiscoveryList is an autogenerated conversion function. +func Convertv2APIGroupDiscoveryListTov2beta1APIGroupDiscoveryList(in *v2.APIGroupDiscoveryList, out *v2beta1.APIGroupDiscoveryList, s conversion.Scope) error { + return autoConvertv2APIGroupDiscoveryListTov2beta1APIGroupDiscoveryList(in, out, s) +} + +func autoConvertv2beta1APIResourceDiscoveryTov2APIResourceDiscovery(in *v2beta1.APIResourceDiscovery, out *v2.APIResourceDiscovery, s conversion.Scope) error { + out.Resource = in.Resource + out.ResponseKind = (*v1.GroupVersionKind)(unsafe.Pointer(in.ResponseKind)) + out.Scope = v2.ResourceScope(in.Scope) + out.SingularResource = in.SingularResource + out.Verbs = *(*[]string)(unsafe.Pointer(&in.Verbs)) + out.ShortNames = *(*[]string)(unsafe.Pointer(&in.ShortNames)) + out.Categories = *(*[]string)(unsafe.Pointer(&in.Categories)) + out.Subresources = *(*[]v2.APISubresourceDiscovery)(unsafe.Pointer(&in.Subresources)) + return nil +} + +// Convertv2beta1APIResourceDiscoveryTov2APIResourceDiscovery is an autogenerated conversion function. +func Convertv2beta1APIResourceDiscoveryTov2APIResourceDiscovery(in *v2beta1.APIResourceDiscovery, out *v2.APIResourceDiscovery, s conversion.Scope) error { + return autoConvertv2beta1APIResourceDiscoveryTov2APIResourceDiscovery(in, out, s) +} + +func autoConvertv2APIResourceDiscoveryTov2beta1APIResourceDiscovery(in *v2.APIResourceDiscovery, out *v2beta1.APIResourceDiscovery, s conversion.Scope) error { + out.Resource = in.Resource + out.ResponseKind = (*v1.GroupVersionKind)(unsafe.Pointer(in.ResponseKind)) + out.Scope = v2beta1.ResourceScope(in.Scope) + out.SingularResource = in.SingularResource + out.Verbs = *(*[]string)(unsafe.Pointer(&in.Verbs)) + out.ShortNames = *(*[]string)(unsafe.Pointer(&in.ShortNames)) + out.Categories = *(*[]string)(unsafe.Pointer(&in.Categories)) + out.Subresources = *(*[]v2beta1.APISubresourceDiscovery)(unsafe.Pointer(&in.Subresources)) + return nil +} + +// Convertv2APIResourceDiscoveryTov2beta1APIResourceDiscovery is an autogenerated conversion function. +func Convertv2APIResourceDiscoveryTov2beta1APIResourceDiscovery(in *v2.APIResourceDiscovery, out *v2beta1.APIResourceDiscovery, s conversion.Scope) error { + return autoConvertv2APIResourceDiscoveryTov2beta1APIResourceDiscovery(in, out, s) +} + +func autoConvertv2beta1APISubresourceDiscoveryTov2APISubresourceDiscovery(in *v2beta1.APISubresourceDiscovery, out *v2.APISubresourceDiscovery, s conversion.Scope) error { + out.Subresource = in.Subresource + out.ResponseKind = (*v1.GroupVersionKind)(unsafe.Pointer(in.ResponseKind)) + out.AcceptedTypes = *(*[]v1.GroupVersionKind)(unsafe.Pointer(&in.AcceptedTypes)) + out.Verbs = *(*[]string)(unsafe.Pointer(&in.Verbs)) + return nil +} + +// Convertv2beta1APISubresourceDiscoveryTov2APISubresourceDiscovery is an autogenerated conversion function. +func Convertv2beta1APISubresourceDiscoveryTov2APISubresourceDiscovery(in *v2beta1.APISubresourceDiscovery, out *v2.APISubresourceDiscovery, s conversion.Scope) error { + return autoConvertv2beta1APISubresourceDiscoveryTov2APISubresourceDiscovery(in, out, s) +} + +func autoConvertv2APISubresourceDiscoveryTov2beta1APISubresourceDiscovery(in *v2.APISubresourceDiscovery, out *v2beta1.APISubresourceDiscovery, s conversion.Scope) error { + out.Subresource = in.Subresource + out.ResponseKind = (*v1.GroupVersionKind)(unsafe.Pointer(in.ResponseKind)) + out.AcceptedTypes = *(*[]v1.GroupVersionKind)(unsafe.Pointer(&in.AcceptedTypes)) + out.Verbs = *(*[]string)(unsafe.Pointer(&in.Verbs)) + return nil +} + +// Convertv2APISubresourceDiscoveryTov2beta1APISubresourceDiscovery is an autogenerated conversion function. +func Convertv2APISubresourceDiscoveryTov2beta1APISubresourceDiscovery(in *v2.APISubresourceDiscovery, out *v2beta1.APISubresourceDiscovery, s conversion.Scope) error { + return autoConvertv2APISubresourceDiscoveryTov2beta1APISubresourceDiscovery(in, out, s) +} + +func autoConvertv2beta1APIVersionDiscoveryTov2APIVersionDiscovery(in *v2beta1.APIVersionDiscovery, out *v2.APIVersionDiscovery, s conversion.Scope) error { + out.Version = in.Version + out.Resources = *(*[]v2.APIResourceDiscovery)(unsafe.Pointer(&in.Resources)) + out.Freshness = v2.DiscoveryFreshness(in.Freshness) + return nil +} + +// Convertv2beta1APIVersionDiscoveryTov2APIVersionDiscovery is an autogenerated conversion function. +func Convertv2beta1APIVersionDiscoveryTov2APIVersionDiscovery(in *v2beta1.APIVersionDiscovery, out *v2.APIVersionDiscovery, s conversion.Scope) error { + return autoConvertv2beta1APIVersionDiscoveryTov2APIVersionDiscovery(in, out, s) +} + +func autoConvertv2APIVersionDiscoveryTov2beta1APIVersionDiscovery(in *v2.APIVersionDiscovery, out *v2beta1.APIVersionDiscovery, s conversion.Scope) error { + out.Version = in.Version + out.Resources = *(*[]v2beta1.APIResourceDiscovery)(unsafe.Pointer(&in.Resources)) + out.Freshness = v2beta1.DiscoveryFreshness(in.Freshness) + return nil +} + +// Convertv2APIVersionDiscoveryTov2beta1APIVersionDiscovery is an autogenerated conversion function. +func Convertv2APIVersionDiscoveryTov2beta1APIVersionDiscovery(in *v2.APIVersionDiscovery, out *v2beta1.APIVersionDiscovery, s conversion.Scope) error { + return autoConvertv2APIVersionDiscoveryTov2beta1APIVersionDiscovery(in, out, s) +} diff --git a/staging/src/k8s.io/apiserver/pkg/apis/apidiscovery/v2/doc.go b/staging/src/k8s.io/apiserver/pkg/apis/apidiscovery/v2/doc.go new file mode 100644 index 00000000000..579dc0bb25e --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/apis/apidiscovery/v2/doc.go @@ -0,0 +1,19 @@ +/* +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. +*/ + +// +groupName=apidiscovery.k8s.io + +package v2 // import "k8s.io/apiserver/pkg/apis/apidiscovery/v2" diff --git a/staging/src/k8s.io/apiserver/pkg/apis/apidiscovery/v2/fuzzer_test.go b/staging/src/k8s.io/apiserver/pkg/apis/apidiscovery/v2/fuzzer_test.go new file mode 100644 index 00000000000..add2deeebe9 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/apis/apidiscovery/v2/fuzzer_test.go @@ -0,0 +1,87 @@ +/* +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 v2_test + +import ( + "reflect" + "testing" + + v2 "k8s.io/api/apidiscovery/v2" + v2beta1 "k8s.io/api/apidiscovery/v2beta1" + v2scheme "k8s.io/apiserver/pkg/apis/apidiscovery/v2" + v2beta1scheme "k8s.io/apiserver/pkg/apis/apidiscovery/v2beta1" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + + "github.com/google/go-cmp/cmp" + fuzz "github.com/google/gofuzz" + "github.com/stretchr/testify/require" +) + +func TestConversionRoundTrip(t *testing.T) { + scheme := runtime.NewScheme() + err := v2beta1scheme.AddToScheme(scheme) + require.NoError(t, err) + v2scheme.SchemeBuilder.Register(v2scheme.RegisterConversions) + err = v2scheme.AddToScheme(scheme) + require.NoError(t, err) + + fuzzer := fuzz.NewWithSeed(2374375) + + // v2 -> v2beta1 -> v2 + for i := 0; i < 100; i++ { + expected := &v2.APIGroupDiscoveryList{} + fuzzer.Fuzz(expected) + expected.TypeMeta = metav1.TypeMeta{ + Kind: "APIGroupDiscoveryList", + APIVersion: "apidiscovery.k8s.io/v2", + } + o, err := scheme.ConvertToVersion(expected, v2beta1.SchemeGroupVersion) + require.NoError(t, err) + v2beta1Type := o.(*v2beta1.APIGroupDiscoveryList) + + o2, err := scheme.ConvertToVersion(v2beta1Type, v2.SchemeGroupVersion) + require.NoError(t, err) + actual := o2.(*v2.APIGroupDiscoveryList) + + if !reflect.DeepEqual(expected, actual) { + t.Error(cmp.Diff(expected, actual)) + } + } + + // v2beta1 -> v2 -> v2beta1 + for i := 0; i < 100; i++ { + expected := &v2beta1.APIGroupDiscoveryList{} + fuzzer.Fuzz(expected) + expected.TypeMeta = metav1.TypeMeta{ + Kind: "APIGroupDiscoveryList", + APIVersion: "apidiscovery.k8s.io/v2beta1", + } + o, err := scheme.ConvertToVersion(expected, v2.SchemeGroupVersion) + require.NoError(t, err) + v2Type := o.(*v2.APIGroupDiscoveryList) + + o2, err := scheme.ConvertToVersion(v2Type, v2beta1.SchemeGroupVersion) + require.NoError(t, err) + actual := o2.(*v2beta1.APIGroupDiscoveryList) + + if !reflect.DeepEqual(expected, actual) { + t.Error(cmp.Diff(expected, actual)) + } + } +} diff --git a/staging/src/k8s.io/apiserver/pkg/apis/apidiscovery/v2/register.go b/staging/src/k8s.io/apiserver/pkg/apis/apidiscovery/v2/register.go new file mode 100644 index 00000000000..02c45b9cf8c --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/apis/apidiscovery/v2/register.go @@ -0,0 +1,39 @@ +/* +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 v2 + +import ( + apidiscoveryv2 "k8s.io/api/apidiscovery/v2" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// GroupName is the group name use in this package +const GroupName = "apidiscovery.k8s.io" + +// SchemeGroupVersion is group version used to register these objects +var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v2"} + +// Resource takes an unqualified resource and returns a Group qualified GroupResource +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} + +var ( + SchemeBuilder = &apidiscoveryv2.SchemeBuilder + // AddToScheme adds api to a scheme + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/staging/src/k8s.io/apiserver/pkg/apis/apidiscovery/v2beta1/doc.go b/staging/src/k8s.io/apiserver/pkg/apis/apidiscovery/v2beta1/doc.go new file mode 100644 index 00000000000..1a3142bad2e --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/apis/apidiscovery/v2beta1/doc.go @@ -0,0 +1,19 @@ +/* +Copyright 2022 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. +*/ + +// +groupName=apidiscovery.k8s.io + +package v2beta1 // import "k8s.io/apiserver/pkg/apis/apidiscovery/v2beta1" diff --git a/staging/src/k8s.io/apiserver/pkg/apis/apidiscovery/v2beta1/register.go b/staging/src/k8s.io/apiserver/pkg/apis/apidiscovery/v2beta1/register.go new file mode 100644 index 00000000000..fc57981927d --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/apis/apidiscovery/v2beta1/register.go @@ -0,0 +1,39 @@ +/* +Copyright 2022 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 v2beta1 + +import ( + apidiscoveryv2beta1 "k8s.io/api/apidiscovery/v2beta1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// GroupName is the group name use in this package +const GroupName = "apidiscovery.k8s.io" + +// SchemeGroupVersion is group version used to register these objects +var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v2beta1"} + +// Resource takes an unqualified resource and returns a Group qualified GroupResource +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} + +var ( + localSchemeBuilder = &apidiscoveryv2beta1.SchemeBuilder + // AddToScheme adds api to a scheme + AddToScheme = localSchemeBuilder.AddToScheme +) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/discovery/aggregated/etag.go b/staging/src/k8s.io/apiserver/pkg/endpoints/discovery/aggregated/etag.go index 0151f6c1085..09f8a616dda 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/discovery/aggregated/etag.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/discovery/aggregated/etag.go @@ -24,6 +24,7 @@ import ( "strconv" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" ) @@ -39,6 +40,7 @@ import ( func ServeHTTPWithETag( object runtime.Object, hash string, + targetGV schema.GroupVersion, serializer runtime.NegotiatedSerializer, w http.ResponseWriter, req *http.Request, @@ -64,7 +66,7 @@ func ServeHTTPWithETag( responsewriters.WriteObjectNegotiated( serializer, DiscoveryEndpointRestrictions, - AggregatedDiscoveryGV, + targetGV, w, req, http.StatusOK, diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/discovery/aggregated/fake.go b/staging/src/k8s.io/apiserver/pkg/endpoints/discovery/aggregated/fake.go index a819fe28fc3..e3c7fcfa7e5 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/discovery/aggregated/fake.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/discovery/aggregated/fake.go @@ -26,7 +26,7 @@ import ( "github.com/emicklei/go-restful/v3" "github.com/google/go-cmp/cmp" - apidiscoveryv2beta1 "k8s.io/api/apidiscovery/v2beta1" + apidiscoveryv2 "k8s.io/api/apidiscovery/v2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/wait" ) @@ -122,7 +122,7 @@ func (f *recorderResourceManager) SetGroupVersionPriority(gv metav1.GroupVersion }) } -func (f *recorderResourceManager) AddGroupVersion(groupName string, value apidiscoveryv2beta1.APIVersionDiscovery) { +func (f *recorderResourceManager) AddGroupVersion(groupName string, value apidiscoveryv2.APIVersionDiscovery) { f.lock.Lock() defer f.lock.Unlock() @@ -153,7 +153,7 @@ func (f *recorderResourceManager) RemoveGroupVersion(gv metav1.GroupVersion) { }) } -func (f *recorderResourceManager) SetGroups(values []apidiscoveryv2beta1.APIGroupDiscovery) { +func (f *recorderResourceManager) SetGroups(values []apidiscoveryv2.APIGroupDiscovery) { f.lock.Lock() defer f.lock.Unlock() diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/discovery/aggregated/handler.go b/staging/src/k8s.io/apiserver/pkg/endpoints/discovery/aggregated/handler.go index 254a223481d..f4676d29061 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/discovery/aggregated/handler.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/discovery/aggregated/handler.go @@ -17,15 +17,22 @@ limitations under the License. package aggregated import ( + "fmt" "net/http" "reflect" "sort" "sync" + apidiscoveryv2 "k8s.io/api/apidiscovery/v2" apidiscoveryv2beta1 "k8s.io/api/apidiscovery/v2beta1" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/version" + apidiscoveryv2conversion "k8s.io/apiserver/pkg/apis/apidiscovery/v2" + "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" + + "k8s.io/apiserver/pkg/endpoints/handlers/negotiation" "k8s.io/apiserver/pkg/endpoints/metrics" "sync/atomic" @@ -51,7 +58,7 @@ type ResourceManager interface { // Adds knowledge of the given groupversion to the discovery document // If it was already being tracked, updates the stored APIVersionDiscovery // Thread-safe - AddGroupVersion(groupName string, value apidiscoveryv2beta1.APIVersionDiscovery) + AddGroupVersion(groupName string, value apidiscoveryv2.APIVersionDiscovery) // Sets a priority to be used while sorting a specific group and // group-version. If two versions report different priorities for @@ -72,7 +79,7 @@ type ResourceManager interface { // Resets the manager's known list of group-versions and replaces them // with the given groups // Thread-Safe - SetGroups([]apidiscoveryv2beta1.APIGroupDiscovery) + SetGroups([]apidiscoveryv2.APIGroupDiscovery) // Returns the same resource manager using a different source // The source is used to decide how to de-duplicate groups. @@ -87,7 +94,7 @@ type resourceManager struct { *resourceDiscoveryManager } -func (rm resourceManager) AddGroupVersion(groupName string, value apidiscoveryv2beta1.APIVersionDiscovery) { +func (rm resourceManager) AddGroupVersion(groupName string, value apidiscoveryv2.APIVersionDiscovery) { rm.resourceDiscoveryManager.AddGroupVersion(rm.source, groupName, value) } func (rm resourceManager) SetGroupVersionPriority(gv metav1.GroupVersion, grouppriority, versionpriority int) { @@ -99,7 +106,7 @@ func (rm resourceManager) RemoveGroup(groupName string) { func (rm resourceManager) RemoveGroupVersion(gv metav1.GroupVersion) { rm.resourceDiscoveryManager.RemoveGroupVersion(rm.source, gv) } -func (rm resourceManager) SetGroups(groups []apidiscoveryv2beta1.APIGroupDiscovery) { +func (rm resourceManager) SetGroups(groups []apidiscoveryv2.APIGroupDiscovery) { rm.resourceDiscoveryManager.SetGroups(rm.source, groups) } @@ -133,7 +140,7 @@ type resourceDiscoveryManager struct { // Writes protected by the lock. // List of all apigroups & resources indexed by the resource manager lock sync.RWMutex - apiGroups map[groupKey]*apidiscoveryv2beta1.APIGroupDiscovery + apiGroups map[groupKey]*apidiscoveryv2.APIGroupDiscovery versionPriorities map[groupVersionKey]priorityInfo } @@ -144,8 +151,12 @@ type priorityInfo struct { func NewResourceManager(path string) ResourceManager { scheme := runtime.NewScheme() - codecs := serializer.NewCodecFactory(scheme) + // Register conversion for apidiscovery + apidiscoveryv2.SchemeBuilder.Register(apidiscoveryv2conversion.RegisterConversions) + utilruntime.Must(apidiscoveryv2.AddToScheme(scheme)) utilruntime.Must(apidiscoveryv2beta1.AddToScheme(scheme)) + + codecs := serializer.NewCodecFactory(scheme) rdm := &resourceDiscoveryManager{ serializer: codecs, versionPriorities: make(map[groupVersionKey]priorityInfo), @@ -181,7 +192,7 @@ func (rdm *resourceDiscoveryManager) SetGroupVersionPriority(source Source, gv m rdm.cache.Store(nil) } -func (rdm *resourceDiscoveryManager) SetGroups(source Source, groups []apidiscoveryv2beta1.APIGroupDiscovery) { +func (rdm *resourceDiscoveryManager) SetGroups(source Source, groups []apidiscoveryv2.APIGroupDiscovery) { rdm.lock.Lock() defer rdm.lock.Unlock() @@ -221,17 +232,17 @@ func (rdm *resourceDiscoveryManager) SetGroups(source Source, groups []apidiscov } } -func (rdm *resourceDiscoveryManager) AddGroupVersion(source Source, groupName string, value apidiscoveryv2beta1.APIVersionDiscovery) { +func (rdm *resourceDiscoveryManager) AddGroupVersion(source Source, groupName string, value apidiscoveryv2.APIVersionDiscovery) { rdm.lock.Lock() defer rdm.lock.Unlock() rdm.addGroupVersionLocked(source, groupName, value) } -func (rdm *resourceDiscoveryManager) addGroupVersionLocked(source Source, groupName string, value apidiscoveryv2beta1.APIVersionDiscovery) { +func (rdm *resourceDiscoveryManager) addGroupVersionLocked(source Source, groupName string, value apidiscoveryv2.APIVersionDiscovery) { if rdm.apiGroups == nil { - rdm.apiGroups = make(map[groupKey]*apidiscoveryv2beta1.APIGroupDiscovery) + rdm.apiGroups = make(map[groupKey]*apidiscoveryv2.APIGroupDiscovery) } key := groupKey{ @@ -264,11 +275,11 @@ func (rdm *resourceDiscoveryManager) addGroupVersionLocked(source Source, groupN } } else { - group := &apidiscoveryv2beta1.APIGroupDiscovery{ + group := &apidiscoveryv2.APIGroupDiscovery{ ObjectMeta: metav1.ObjectMeta{ Name: groupName, }, - Versions: []apidiscoveryv2beta1.APIVersionDiscovery{value}, + Versions: []apidiscoveryv2.APIVersionDiscovery{value}, } rdm.apiGroups[key] = group } @@ -354,12 +365,12 @@ func (rdm *resourceDiscoveryManager) RemoveGroup(source Source, groupName string // Prepares the api group list for serving by converting them from map into // list and sorting them according to insertion order -func (rdm *resourceDiscoveryManager) calculateAPIGroupsLocked() []apidiscoveryv2beta1.APIGroupDiscovery { +func (rdm *resourceDiscoveryManager) calculateAPIGroupsLocked() []apidiscoveryv2.APIGroupDiscovery { regenerationCounter.Inc() // Re-order the apiGroups by their priority. - groups := []apidiscoveryv2beta1.APIGroupDiscovery{} + groups := []apidiscoveryv2.APIGroupDiscovery{} - groupsToUse := map[string]apidiscoveryv2beta1.APIGroupDiscovery{} + groupsToUse := map[string]apidiscoveryv2.APIGroupDiscovery{} sourcesUsed := map[metav1.GroupVersion]Source{} for key, group := range rdm.apiGroups { @@ -475,7 +486,7 @@ func (rdm *resourceDiscoveryManager) fetchFromCache() *cachedGroupList { if cacheLoad != nil { return cacheLoad } - response := apidiscoveryv2beta1.APIGroupDiscoveryList{ + response := apidiscoveryv2.APIGroupDiscoveryList{ Items: rdm.calculateAPIGroupsLocked(), } etag, err := calculateETag(response) @@ -492,7 +503,13 @@ func (rdm *resourceDiscoveryManager) fetchFromCache() *cachedGroupList { } type cachedGroupList struct { - cachedResponse apidiscoveryv2beta1.APIGroupDiscoveryList + cachedResponse apidiscoveryv2.APIGroupDiscoveryList + // etag is calculated based on a SHA hash of only the JSON object. + // A response via different Accept encodings (eg: protobuf, json) will + // yield the same etag. This is okay because Accept is part of the Vary header. + // Per RFC7231 a client must only cache a response etag pair if the header field + // matches as indicated by the Vary field. Thus, protobuf and json and other Accept + // encodings will not be cached as the same response despite having the same etag. cachedResponseETag string } @@ -505,11 +522,30 @@ func (rdm *resourceDiscoveryManager) serveHTTP(resp http.ResponseWriter, req *ht response := cache.cachedResponse etag := cache.cachedResponseETag + mediaType, _, err := negotiation.NegotiateOutputMediaType(req, rdm.serializer, DiscoveryEndpointRestrictions) + if err != nil { + // Should never happen. wrapper.go will only proxy requests to this + // handler if the media type passes DiscoveryEndpointRestrictions + utilruntime.HandleError(err) + resp.WriteHeader(http.StatusInternalServerError) + return + } + var targetGV schema.GroupVersion + if mediaType.Convert == nil || + (mediaType.Convert.GroupVersion() != apidiscoveryv2.SchemeGroupVersion && + mediaType.Convert.GroupVersion() != apidiscoveryv2beta1.SchemeGroupVersion) { + utilruntime.HandleError(fmt.Errorf("expected aggregated discovery group version, got group: %s, version %s", mediaType.Convert.Group, mediaType.Convert.Version)) + resp.WriteHeader(http.StatusInternalServerError) + return + } + targetGV = mediaType.Convert.GroupVersion() + if len(etag) > 0 { // Use proper e-tag headers if one is available ServeHTTPWithETag( &response, etag, + targetGV, rdm.serializer, resp, req, @@ -520,7 +556,7 @@ func (rdm *resourceDiscoveryManager) serveHTTP(resp http.ResponseWriter, req *ht responsewriters.WriteObjectNegotiated( rdm.serializer, DiscoveryEndpointRestrictions, - AggregatedDiscoveryGV, + targetGV, resp, req, http.StatusOK, diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/discovery/aggregated/handler_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/discovery/aggregated/handler_test.go index 7fd3cd46567..2a41e4a93f0 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/discovery/aggregated/handler_test.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/discovery/aggregated/handler_test.go @@ -32,11 +32,15 @@ import ( fuzz "github.com/google/gofuzz" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + apidiscoveryv2 "k8s.io/api/apidiscovery/v2" apidiscoveryv2beta1 "k8s.io/api/apidiscovery/v2beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/version" + apidiscoveryv2conversion "k8s.io/apiserver/pkg/apis/apidiscovery/v2" discoveryendpoint "k8s.io/apiserver/pkg/endpoints/discovery/aggregated" ) @@ -46,21 +50,23 @@ var codecs = runtimeserializer.NewCodecFactory(scheme) const discoveryPath = "/apis" func init() { - // Add all builtin types to scheme - apidiscoveryv2beta1.AddToScheme(scheme) + // Register conversion for apidiscovery + apidiscoveryv2.SchemeBuilder.Register(apidiscoveryv2conversion.RegisterConversions) + utilruntime.Must(apidiscoveryv2.AddToScheme(scheme)) + utilruntime.Must(apidiscoveryv2beta1.AddToScheme(scheme)) codecs = runtimeserializer.NewCodecFactory(scheme) } -func fuzzAPIGroups(atLeastNumGroups, maxNumGroups int, seed int64) apidiscoveryv2beta1.APIGroupDiscoveryList { +func fuzzAPIGroups(atLeastNumGroups, maxNumGroups int, seed int64) apidiscoveryv2.APIGroupDiscoveryList { fuzzer := fuzz.NewWithSeed(seed) fuzzer.NumElements(atLeastNumGroups, maxNumGroups) fuzzer.NilChance(0) - fuzzer.Funcs(func(o *apidiscoveryv2beta1.APIGroupDiscovery, c fuzz.Continue) { + fuzzer.Funcs(func(o *apidiscoveryv2.APIGroupDiscovery, c fuzz.Continue) { c.FuzzNoCustom(o) // The ResourceManager will just not serve the group if its versions // list is empty - atLeastOne := apidiscoveryv2beta1.APIVersionDiscovery{} + atLeastOne := apidiscoveryv2.APIVersionDiscovery{} c.Fuzz(&atLeastOne) o.Versions = append(o.Versions, atLeastOne) sort.Slice(o.Versions[:], func(i, j int) bool { @@ -75,28 +81,58 @@ func fuzzAPIGroups(atLeastNumGroups, maxNumGroups int, seed int64) apidiscoveryv } }) - var apis []apidiscoveryv2beta1.APIGroupDiscovery + var apis []apidiscoveryv2.APIGroupDiscovery fuzzer.Fuzz(&apis) sort.Slice(apis[:], func(i, j int) bool { return apis[i].Name < apis[j].Name }) - return apidiscoveryv2beta1.APIGroupDiscoveryList{ + return apidiscoveryv2.APIGroupDiscoveryList{ TypeMeta: metav1.TypeMeta{ Kind: "APIGroupDiscoveryList", - APIVersion: "apidiscovery.k8s.io/v2beta1", + APIVersion: "apidiscovery.k8s.io/v2", }, Items: apis, } } -func fetchPath(handler http.Handler, acceptPrefix string, path string, etag string) (*http.Response, []byte, *apidiscoveryv2beta1.APIGroupDiscoveryList) { +func fetchPathV2Beta1(handler http.Handler, acceptPrefix string, path string, etag string) (*http.Response, []byte, *apidiscoveryv2beta1.APIGroupDiscoveryList) { + acceptSuffix := ";g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList" + r, bytes := fetchPathHelper(handler, acceptPrefix+acceptSuffix, path, etag) + var decoded *apidiscoveryv2beta1.APIGroupDiscoveryList + if len(bytes) > 0 { + decoded = &apidiscoveryv2beta1.APIGroupDiscoveryList{} + err := runtime.DecodeInto(codecs.UniversalDecoder(), bytes, decoded) + if err != nil { + panic(fmt.Sprintf("failed to decode response: %v", err)) + } + + } + return r, bytes, decoded +} + +func fetchPath(handler http.Handler, acceptPrefix string, path string, etag string) (*http.Response, []byte, *apidiscoveryv2.APIGroupDiscoveryList) { + acceptSuffix := ";g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList," + acceptSuffixV2Beta1 := ";g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList," + r, bytes := fetchPathHelper(handler, acceptPrefix+acceptSuffix+","+acceptPrefix+acceptSuffixV2Beta1, path, etag) + var decoded *apidiscoveryv2.APIGroupDiscoveryList + if len(bytes) > 0 { + decoded = &apidiscoveryv2.APIGroupDiscoveryList{} + err := runtime.DecodeInto(codecs.UniversalDecoder(), bytes, decoded) + if err != nil { + panic(fmt.Sprintf("failed to decode response: %v", err)) + } + } + return r, bytes, decoded +} + +func fetchPathHelper(handler http.Handler, accept string, path string, etag string) (*http.Response, []byte) { // Expect json-formatted apis group list w := httptest.NewRecorder() req := httptest.NewRequest("GET", discoveryPath, nil) // Ask for JSON response - req.Header.Set("Accept", acceptPrefix+";g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList") + req.Header.Set("Accept", accept) if etag != "" { // Quote provided etag if unquoted @@ -110,13 +146,7 @@ func fetchPath(handler http.Handler, acceptPrefix string, path string, etag stri handler.ServeHTTP(w, req) bytes := w.Body.Bytes() - var decoded *apidiscoveryv2beta1.APIGroupDiscoveryList - if len(bytes) > 0 { - decoded = &apidiscoveryv2beta1.APIGroupDiscoveryList{} - runtime.DecodeInto(codecs.UniversalDecoder(), bytes, decoded) - } - - return w.Result(), bytes, decoded + return w.Result(), bytes } // Add all builtin APIServices to the manager and check the output @@ -132,7 +162,7 @@ func TestBasicResponse(t *testing.T) { require.NoError(t, err, "json marshal should always succeed") assert.Equal(t, http.StatusOK, response.StatusCode, "response should be 200 OK") - assert.Equal(t, "application/json;g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList", response.Header.Get("Content-Type"), "Content-Type response header should be as requested in Accept header if supported") + assert.Equal(t, "application/json;g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList", response.Header.Get("Content-Type"), "Content-Type response header should be as requested in Accept header if supported") assert.NotEmpty(t, response.Header.Get("ETag"), "E-Tag should be set") assert.NoError(t, err, "decode should always succeed") @@ -149,11 +179,39 @@ func TestBasicResponseProtobuf(t *testing.T) { response, _, decoded := fetchPath(manager, "application/vnd.kubernetes.protobuf", discoveryPath, "") assert.Equal(t, http.StatusOK, response.StatusCode, "response should be 200 OK") - assert.Equal(t, "application/vnd.kubernetes.protobuf;g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList", response.Header.Get("Content-Type"), "Content-Type response header should be as requested in Accept header if supported") + assert.Equal(t, "application/vnd.kubernetes.protobuf;g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList", response.Header.Get("Content-Type"), "Content-Type response header should be as requested in Accept header if supported") assert.NotEmpty(t, response.Header.Get("ETag"), "E-Tag should be set") assert.EqualValues(t, &apis, decoded, "decoded value should equal input") } +// V2Beta1 should still be served +func TestV2Beta1SkewSupport(t *testing.T) { + manager := discoveryendpoint.NewResourceManager("apis") + + apis := fuzzAPIGroups(1, 3, 10) + manager.SetGroups(apis.Items) + + converted, err := scheme.ConvertToVersion(&apis, &schema.GroupVersion{Group: "apidiscovery.k8s.io", Version: "v2beta1"}) + if err != nil { + t.Fatal(err) + } + + v2beta1apis := converted.(*apidiscoveryv2beta1.APIGroupDiscoveryList) + + response, body, decoded := fetchPathV2Beta1(manager, "application/json", discoveryPath, "") + + jsonFormatted, err := json.Marshal(v2beta1apis) + require.NoError(t, err, "json marshal should always succeed") + + assert.Equal(t, http.StatusOK, response.StatusCode, "response should be 200 OK") + assert.Equal(t, "application/json;g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList", response.Header.Get("Content-Type"), "Content-Type response header should be as requested in Accept header if supported") + assert.NotEmpty(t, response.Header.Get("ETag"), "E-Tag should be set") + + assert.NoError(t, err, "decode should always succeed") + assert.EqualValues(t, v2beta1apis, decoded, "decoded value should equal input") + assert.Equal(t, string(jsonFormatted)+"\n", string(body), "response should be the api group list") +} + // Test that an etag associated with the service only depends on the apiresources // e.g.: Multiple services with the same contents should have the same etag. func TestEtagConsistent(t *testing.T) { @@ -348,7 +406,7 @@ func TestUpdateService(t *testing.T) { if err != nil { t.Error(err) } - var newapis apidiscoveryv2beta1.APIGroupDiscoveryList + var newapis apidiscoveryv2.APIGroupDiscoveryList err = json.Unmarshal(b, &newapis) if err != nil { t.Error(err) @@ -369,7 +427,7 @@ func TestUpdateService(t *testing.T) { func TestMultipleSources(t *testing.T) { type pair struct { manager discoveryendpoint.ResourceManager - apis apidiscoveryv2beta1.APIGroupDiscoveryList + apis apidiscoveryv2.APIGroupDiscoveryList } pairs := []pair{} @@ -388,7 +446,7 @@ func TestMultipleSources(t *testing.T) { pairs = append(pairs, pair{manager, apis}) } - expectedResult := []apidiscoveryv2beta1.APIGroupDiscovery{} + expectedResult := []apidiscoveryv2.APIGroupDiscovery{} groupCounter := 0 for _, p := range pairs { @@ -422,7 +480,7 @@ func TestSourcePrecedence(t *testing.T) { apis := fuzzAPIGroups(1, 3, int64(15)) for _, g := range apis.Items { for i, v := range g.Versions { - v.Freshness = apidiscoveryv2beta1.DiscoveryFreshnessCurrent + v.Freshness = apidiscoveryv2.DiscoveryFreshnessCurrent g.Versions[i] = v otherManager.AddGroupVersion(g.Name, v) } @@ -434,12 +492,12 @@ func TestSourcePrecedence(t *testing.T) { // Add the first groupversion under default. // No versions should appear in discovery document except this one overrideVersion := initialDocument.Items[0].Versions[0] - overrideVersion.Freshness = apidiscoveryv2beta1.DiscoveryFreshnessStale + overrideVersion.Freshness = apidiscoveryv2.DiscoveryFreshnessStale defaultManager.AddGroupVersion(initialDocument.Items[0].Name, overrideVersion) _, _, maskedDocument := fetchPath(defaultManager, "application/json", discoveryPath, "") masked := initialDocument.DeepCopy() - masked.Items[0].Versions[0].Freshness = apidiscoveryv2beta1.DiscoveryFreshnessStale + masked.Items[0].Versions[0].Freshness = apidiscoveryv2.DiscoveryFreshnessStale require.Equal(t, masked.Items, maskedDocument.Items) @@ -593,19 +651,19 @@ func TestAbuse(t *testing.T) { func TestVersionSortingNoPriority(t *testing.T) { manager := discoveryendpoint.NewResourceManager("apis") - manager.AddGroupVersion("default", apidiscoveryv2beta1.APIVersionDiscovery{ + manager.AddGroupVersion("default", apidiscoveryv2.APIVersionDiscovery{ Version: "v1alpha1", }) - manager.AddGroupVersion("default", apidiscoveryv2beta1.APIVersionDiscovery{ + manager.AddGroupVersion("default", apidiscoveryv2.APIVersionDiscovery{ Version: "v2beta1", }) - manager.AddGroupVersion("default", apidiscoveryv2beta1.APIVersionDiscovery{ + manager.AddGroupVersion("default", apidiscoveryv2.APIVersionDiscovery{ Version: "v1", }) - manager.AddGroupVersion("default", apidiscoveryv2beta1.APIVersionDiscovery{ + manager.AddGroupVersion("default", apidiscoveryv2.APIVersionDiscovery{ Version: "v1beta1", }) - manager.AddGroupVersion("default", apidiscoveryv2beta1.APIVersionDiscovery{ + manager.AddGroupVersion("default", apidiscoveryv2.APIVersionDiscovery{ Version: "v2", }) @@ -625,11 +683,11 @@ func TestVersionSortingNoPriority(t *testing.T) { func TestVersionSortingWithPriority(t *testing.T) { manager := discoveryendpoint.NewResourceManager("apis") - manager.AddGroupVersion("default", apidiscoveryv2beta1.APIVersionDiscovery{ + manager.AddGroupVersion("default", apidiscoveryv2.APIVersionDiscovery{ Version: "v1", }) manager.SetGroupVersionPriority(metav1.GroupVersion{Group: "default", Version: "v1"}, 1000, 100) - manager.AddGroupVersion("default", apidiscoveryv2beta1.APIVersionDiscovery{ + manager.AddGroupVersion("default", apidiscoveryv2.APIVersionDiscovery{ Version: "v1alpha1", }) manager.SetGroupVersionPriority(metav1.GroupVersion{Group: "default", Version: "v1alpha1"}, 1000, 200) @@ -648,15 +706,15 @@ func TestVersionSortingWithPriority(t *testing.T) { func TestGroupVersionSortingConflictingPriority(t *testing.T) { manager := discoveryendpoint.NewResourceManager("apis") - manager.AddGroupVersion("default", apidiscoveryv2beta1.APIVersionDiscovery{ + manager.AddGroupVersion("default", apidiscoveryv2.APIVersionDiscovery{ Version: "v1", }) manager.SetGroupVersionPriority(metav1.GroupVersion{Group: "default", Version: "v1"}, 1000, 100) - manager.AddGroupVersion("test", apidiscoveryv2beta1.APIVersionDiscovery{ + manager.AddGroupVersion("test", apidiscoveryv2.APIVersionDiscovery{ Version: "v1alpha1", }) manager.SetGroupVersionPriority(metav1.GroupVersion{Group: "test", Version: "v1alpha1"}, 500, 100) - manager.AddGroupVersion("test", apidiscoveryv2beta1.APIVersionDiscovery{ + manager.AddGroupVersion("test", apidiscoveryv2.APIVersionDiscovery{ Version: "v1alpha2", }) manager.SetGroupVersionPriority(metav1.GroupVersion{Group: "test", Version: "v1alpha1"}, 2000, 100) @@ -679,17 +737,17 @@ func TestStatelessGroupPriorityMinimum(t *testing.T) { stableGroup := "stable.example.com" experimentalGroup := "experimental.example.com" - manager.AddGroupVersion(stableGroup, apidiscoveryv2beta1.APIVersionDiscovery{ + manager.AddGroupVersion(stableGroup, apidiscoveryv2.APIVersionDiscovery{ Version: "v1", }) manager.SetGroupVersionPriority(metav1.GroupVersion{Group: stableGroup, Version: "v1"}, 1000, 100) - manager.AddGroupVersion(experimentalGroup, apidiscoveryv2beta1.APIVersionDiscovery{ + manager.AddGroupVersion(experimentalGroup, apidiscoveryv2.APIVersionDiscovery{ Version: "v1", }) manager.SetGroupVersionPriority(metav1.GroupVersion{Group: experimentalGroup, Version: "v1"}, 100, 100) - manager.AddGroupVersion(experimentalGroup, apidiscoveryv2beta1.APIVersionDiscovery{ + manager.AddGroupVersion(experimentalGroup, apidiscoveryv2.APIVersionDiscovery{ Version: "v1alpha1", }) manager.SetGroupVersionPriority(metav1.GroupVersion{Group: experimentalGroup, Version: "v1alpha1"}, 10000, 100) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/discovery/aggregated/negotiation.go b/staging/src/k8s.io/apiserver/pkg/endpoints/discovery/aggregated/negotiation.go index 9e58dad8542..9900021a432 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/discovery/aggregated/negotiation.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/discovery/aggregated/negotiation.go @@ -20,8 +20,6 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" ) -var AggregatedDiscoveryGV = schema.GroupVersion{Group: "apidiscovery.k8s.io", Version: "v2beta1"} - // Interface is from "k8s.io/apiserver/pkg/endpoints/handlers/negotiation" // DiscoveryEndpointRestrictions allows requests to /apis to provide a Content Negotiation GVK for aggregated discovery. @@ -39,7 +37,7 @@ func (discoveryEndpointRestrictions) AllowsStreamSchema(s string) bool { return // IsAggregatedDiscoveryGVK checks if a provided GVK is the GVK for serving aggregated discovery. func IsAggregatedDiscoveryGVK(gvk *schema.GroupVersionKind) bool { if gvk != nil { - return gvk.Group == "apidiscovery.k8s.io" && gvk.Version == "v2beta1" && gvk.Kind == "APIGroupDiscoveryList" + return gvk.Group == "apidiscovery.k8s.io" && (gvk.Version == "v2beta1" || gvk.Version == "v2") && gvk.Kind == "APIGroupDiscoveryList" } return false } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/discovery/aggregated/wrapper.go b/staging/src/k8s.io/apiserver/pkg/endpoints/discovery/aggregated/wrapper.go index 8516c154c88..25fe90fa4ba 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/discovery/aggregated/wrapper.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/discovery/aggregated/wrapper.go @@ -19,8 +19,10 @@ package aggregated import ( "net/http" + apidiscoveryv2 "k8s.io/api/apidiscovery/v2" apidiscoveryv2beta1 "k8s.io/api/apidiscovery/v2beta1" "k8s.io/apimachinery/pkg/runtime/serializer" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" "github.com/emicklei/go-restful/v3" "k8s.io/apimachinery/pkg/runtime" @@ -69,10 +71,11 @@ func (wrapped *WrappedHandler) GenerateWebService(prefix string, returnType inte // WrapAggregatedDiscoveryToHandler wraps a handler with an option to // emit the aggregated discovery by passing in the aggregated // discovery type in content negotiation headers: eg: (Accept: -// application/json;v=v2beta1;g=apidiscovery.k8s.io;as=APIGroupDiscoveryList) +// application/json;v=v2;g=apidiscovery.k8s.io;as=APIGroupDiscoveryList) func WrapAggregatedDiscoveryToHandler(handler http.Handler, aggHandler http.Handler) *WrappedHandler { scheme := runtime.NewScheme() - apidiscoveryv2beta1.AddToScheme(scheme) + utilruntime.Must(apidiscoveryv2.AddToScheme(scheme)) + utilruntime.Must(apidiscoveryv2beta1.AddToScheme(scheme)) codecs := serializer.NewCodecFactory(scheme) return &WrappedHandler{codecs, handler, aggHandler} } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/discovery/aggregated/wrapper_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/discovery/aggregated/wrapper_test.go index 6ad58d7ea53..0cfa4992fb4 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/discovery/aggregated/wrapper_test.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/discovery/aggregated/wrapper_test.go @@ -32,8 +32,11 @@ import ( const discoveryPath = "/apis" const jsonAccept = "application/json" const protobufAccept = "application/vnd.kubernetes.protobuf" -const aggregatedAcceptSuffix = ";g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList" +const aggregatedV2Beta1AcceptSuffix = ";g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList" +const aggregatedAcceptSuffix = ";g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList" +const aggregatedV2Beta1JSONAccept = jsonAccept + aggregatedV2Beta1AcceptSuffix +const aggregatedV2Beta1ProtoAccept = protobufAccept + aggregatedV2Beta1AcceptSuffix const aggregatedJSONAccept = jsonAccept + aggregatedAcceptSuffix const aggregatedProtoAccept = protobufAccept + aggregatedAcceptSuffix @@ -75,6 +78,12 @@ func TestAggregationEnabled(t *testing.T) { // Empty accept headers are valid and should be handled by the unaggregated handler accept: "", expected: "unaggregated", + }, { + accept: aggregatedV2Beta1JSONAccept, + expected: "aggregated", + }, { + accept: aggregatedV2Beta1ProtoAccept, + expected: "aggregated", }, { accept: aggregatedJSONAccept, expected: "aggregated", diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/groupversion.go b/staging/src/k8s.io/apiserver/pkg/endpoints/groupversion.go index 0ce06ab1069..a60485d20c1 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/groupversion.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/groupversion.go @@ -22,7 +22,7 @@ import ( restful "github.com/emicklei/go-restful/v3" - apidiscoveryv2beta1 "k8s.io/api/apidiscovery/v2beta1" + apidiscoveryv2 "k8s.io/api/apidiscovery/v2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -107,7 +107,7 @@ type APIGroupVersion struct { // InstallREST registers the REST handlers (storage, watch, proxy and redirect) into a restful Container. // It is expected that the provided path root prefix will serve all operations. Root MUST NOT end // in a slash. -func (g *APIGroupVersion) InstallREST(container *restful.Container) ([]apidiscoveryv2beta1.APIResourceDiscovery, []*storageversion.ResourceInfo, error) { +func (g *APIGroupVersion) InstallREST(container *restful.Container) ([]apidiscoveryv2.APIResourceDiscovery, []*storageversion.ResourceInfo, error) { prefix := path.Join(g.Root, g.GroupVersion.Group, g.GroupVersion.Version) installer := &APIInstaller{ group: g, diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go b/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go index 5315b470ea4..0a0fdde0d78 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go @@ -28,7 +28,7 @@ import ( restful "github.com/emicklei/go-restful/v3" "sigs.k8s.io/structured-merge-diff/v4/fieldpath" - apidiscoveryv2beta1 "k8s.io/api/apidiscovery/v2beta1" + apidiscoveryv2 "k8s.io/api/apidiscovery/v2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/conversion" "k8s.io/apimachinery/pkg/runtime" @@ -71,8 +71,8 @@ type action struct { AllNamespaces bool // true iff the action is namespaced but works on aggregate result for all namespaces } -func ConvertGroupVersionIntoToDiscovery(list []metav1.APIResource) ([]apidiscoveryv2beta1.APIResourceDiscovery, error) { - var apiResourceList []apidiscoveryv2beta1.APIResourceDiscovery +func ConvertGroupVersionIntoToDiscovery(list []metav1.APIResource) ([]apidiscoveryv2.APIResourceDiscovery, error) { + var apiResourceList []apidiscoveryv2.APIResourceDiscovery parentResources := make(map[string]int) // Loop through all top-level resources @@ -82,14 +82,14 @@ func ConvertGroupVersionIntoToDiscovery(list []metav1.APIResource) ([]apidiscove continue } - var scope apidiscoveryv2beta1.ResourceScope + var scope apidiscoveryv2.ResourceScope if r.Namespaced { - scope = apidiscoveryv2beta1.ScopeNamespace + scope = apidiscoveryv2.ScopeNamespace } else { - scope = apidiscoveryv2beta1.ScopeCluster + scope = apidiscoveryv2.ScopeCluster } - resource := apidiscoveryv2beta1.APIResourceDiscovery{ + resource := apidiscoveryv2.APIResourceDiscovery{ Resource: r.Name, Scope: scope, ResponseKind: &metav1.GroupVersionKind{ @@ -116,17 +116,17 @@ func ConvertGroupVersionIntoToDiscovery(list []metav1.APIResource) ([]apidiscove continue } - var scope apidiscoveryv2beta1.ResourceScope + var scope apidiscoveryv2.ResourceScope if r.Namespaced { - scope = apidiscoveryv2beta1.ScopeNamespace + scope = apidiscoveryv2.ScopeNamespace } else { - scope = apidiscoveryv2beta1.ScopeCluster + scope = apidiscoveryv2.ScopeCluster } parentidx, exists := parentResources[split[0]] if !exists { // If a subresource exists without a parent, create a parent - apiResourceList = append(apiResourceList, apidiscoveryv2beta1.APIResourceDiscovery{ + apiResourceList = append(apiResourceList, apidiscoveryv2.APIResourceDiscovery{ Resource: split[0], Scope: scope, // avoid nil panics in v0.26.0-v0.26.3 client-go clients @@ -142,7 +142,7 @@ func ConvertGroupVersionIntoToDiscovery(list []metav1.APIResource) ([]apidiscove // } - subresource := apidiscoveryv2beta1.APISubresourceDiscovery{ + subresource := apidiscoveryv2.APISubresourceDiscovery{ Subresource: split[1], Verbs: r.Verbs, // avoid nil panics in v0.26.0-v0.26.3 client-go clients diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/installer_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/installer_test.go index 03d06f90947..f47a0b3d3a6 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/installer_test.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/installer_test.go @@ -20,7 +20,7 @@ import ( "testing" "github.com/stretchr/testify/require" - apidiscoveryv2beta1 "k8s.io/api/apidiscovery/v2beta1" + apidiscoveryv2 "k8s.io/api/apidiscovery/v2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -106,7 +106,7 @@ func TestConvertAPIResourceToDiscovery(t *testing.T) { tests := []struct { name string resources []metav1.APIResource - wantAPIResourceDiscovery []apidiscoveryv2beta1.APIResourceDiscovery + wantAPIResourceDiscovery []apidiscoveryv2.APIResourceDiscovery wantErr bool }{ { @@ -121,10 +121,10 @@ func TestConvertAPIResourceToDiscovery(t *testing.T) { Verbs: []string{"create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"}, }, }, - wantAPIResourceDiscovery: []apidiscoveryv2beta1.APIResourceDiscovery{ + wantAPIResourceDiscovery: []apidiscoveryv2.APIResourceDiscovery{ { Resource: "pods", - Scope: apidiscoveryv2beta1.ScopeNamespace, + Scope: apidiscoveryv2.ScopeNamespace, ResponseKind: &metav1.GroupVersionKind{ Kind: "Pod", }, @@ -146,10 +146,10 @@ func TestConvertAPIResourceToDiscovery(t *testing.T) { Verbs: []string{"create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"}, }, }, - wantAPIResourceDiscovery: []apidiscoveryv2beta1.APIResourceDiscovery{ + wantAPIResourceDiscovery: []apidiscoveryv2.APIResourceDiscovery{ { Resource: "cronjobs", - Scope: apidiscoveryv2beta1.ScopeNamespace, + Scope: apidiscoveryv2.ScopeNamespace, ResponseKind: &metav1.GroupVersionKind{ Group: "batch", Version: "v1", @@ -182,10 +182,10 @@ func TestConvertAPIResourceToDiscovery(t *testing.T) { Verbs: []string{"create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"}, }, }, - wantAPIResourceDiscovery: []apidiscoveryv2beta1.APIResourceDiscovery{ + wantAPIResourceDiscovery: []apidiscoveryv2.APIResourceDiscovery{ { Resource: "cronjobs", - Scope: apidiscoveryv2beta1.ScopeNamespace, + Scope: apidiscoveryv2.ScopeNamespace, ResponseKind: &metav1.GroupVersionKind{ Group: "batch", Version: "v1", @@ -193,7 +193,7 @@ func TestConvertAPIResourceToDiscovery(t *testing.T) { }, ShortNames: []string{"cj"}, Verbs: []string{"create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"}, - Subresources: []apidiscoveryv2beta1.APISubresourceDiscovery{{ + Subresources: []apidiscoveryv2.APISubresourceDiscovery{{ Subresource: "status", ResponseKind: &metav1.GroupVersionKind{ Group: "batch", @@ -245,10 +245,10 @@ func TestConvertAPIResourceToDiscovery(t *testing.T) { Verbs: []string{"create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"}, }, }, - wantAPIResourceDiscovery: []apidiscoveryv2beta1.APIResourceDiscovery{ + wantAPIResourceDiscovery: []apidiscoveryv2.APIResourceDiscovery{ { Resource: "cronjobs", - Scope: apidiscoveryv2beta1.ScopeNamespace, + Scope: apidiscoveryv2.ScopeNamespace, ResponseKind: &metav1.GroupVersionKind{ Group: "batch", Version: "v1", @@ -256,7 +256,7 @@ func TestConvertAPIResourceToDiscovery(t *testing.T) { }, ShortNames: []string{"cj"}, Verbs: []string{"create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"}, - Subresources: []apidiscoveryv2beta1.APISubresourceDiscovery{{ + Subresources: []apidiscoveryv2.APISubresourceDiscovery{{ Subresource: "status", ResponseKind: &metav1.GroupVersionKind{ Group: "batch", @@ -267,7 +267,7 @@ func TestConvertAPIResourceToDiscovery(t *testing.T) { }}, }, { Resource: "deployments", - Scope: apidiscoveryv2beta1.ScopeNamespace, + Scope: apidiscoveryv2.ScopeNamespace, ResponseKind: &metav1.GroupVersionKind{ Group: "apps", Version: "v1", @@ -275,7 +275,7 @@ func TestConvertAPIResourceToDiscovery(t *testing.T) { }, ShortNames: []string{"deploy"}, Verbs: []string{"create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"}, - Subresources: []apidiscoveryv2beta1.APISubresourceDiscovery{{ + Subresources: []apidiscoveryv2.APISubresourceDiscovery{{ Subresource: "status", ResponseKind: &metav1.GroupVersionKind{ Group: "apps", @@ -298,13 +298,13 @@ func TestConvertAPIResourceToDiscovery(t *testing.T) { Verbs: []string{"create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"}, }, }, - wantAPIResourceDiscovery: []apidiscoveryv2beta1.APIResourceDiscovery{ + wantAPIResourceDiscovery: []apidiscoveryv2.APIResourceDiscovery{ { Resource: "cronjobs", - Scope: apidiscoveryv2beta1.ScopeNamespace, + Scope: apidiscoveryv2.ScopeNamespace, // populated to avoid nil panics ResponseKind: &metav1.GroupVersionKind{}, - Subresources: []apidiscoveryv2beta1.APISubresourceDiscovery{{ + Subresources: []apidiscoveryv2.APISubresourceDiscovery{{ Subresource: "status", ResponseKind: &metav1.GroupVersionKind{ Group: "batch", @@ -327,13 +327,13 @@ func TestConvertAPIResourceToDiscovery(t *testing.T) { Verbs: []string{"create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"}, }, }, - wantAPIResourceDiscovery: []apidiscoveryv2beta1.APIResourceDiscovery{ + wantAPIResourceDiscovery: []apidiscoveryv2.APIResourceDiscovery{ { Resource: "cronjobs", - Scope: apidiscoveryv2beta1.ScopeNamespace, + Scope: apidiscoveryv2.ScopeNamespace, // populated to avoid nil panics ResponseKind: &metav1.GroupVersionKind{}, - Subresources: []apidiscoveryv2beta1.APISubresourceDiscovery{{ + Subresources: []apidiscoveryv2.APISubresourceDiscovery{{ Subresource: "status", // populated to avoid nil panics ResponseKind: &metav1.GroupVersionKind{}, @@ -364,7 +364,7 @@ func TestConvertAPIResourceToDiscovery(t *testing.T) { Verbs: []string{"create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"}, }, }, - wantAPIResourceDiscovery: []apidiscoveryv2beta1.APIResourceDiscovery{}, + wantAPIResourceDiscovery: []apidiscoveryv2.APIResourceDiscovery{}, wantErr: true, }, { @@ -378,10 +378,10 @@ func TestConvertAPIResourceToDiscovery(t *testing.T) { Verbs: []string{"create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"}, }, }, - wantAPIResourceDiscovery: []apidiscoveryv2beta1.APIResourceDiscovery{ + wantAPIResourceDiscovery: []apidiscoveryv2.APIResourceDiscovery{ { Resource: "nodes", - Scope: apidiscoveryv2beta1.ScopeCluster, + Scope: apidiscoveryv2.ScopeCluster, ResponseKind: &metav1.GroupVersionKind{ Kind: "Node", }, @@ -401,10 +401,10 @@ func TestConvertAPIResourceToDiscovery(t *testing.T) { Verbs: []string{"create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"}, }, }, - wantAPIResourceDiscovery: []apidiscoveryv2beta1.APIResourceDiscovery{ + wantAPIResourceDiscovery: []apidiscoveryv2.APIResourceDiscovery{ { Resource: "nodes", - Scope: apidiscoveryv2beta1.ScopeNamespace, + Scope: apidiscoveryv2.ScopeNamespace, ResponseKind: &metav1.GroupVersionKind{ Kind: "Node", }, @@ -424,11 +424,11 @@ func TestConvertAPIResourceToDiscovery(t *testing.T) { Verbs: []string{"create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"}, }, }, - wantAPIResourceDiscovery: []apidiscoveryv2beta1.APIResourceDiscovery{ + wantAPIResourceDiscovery: []apidiscoveryv2.APIResourceDiscovery{ { Resource: "nodes", SingularResource: "node", - Scope: apidiscoveryv2beta1.ScopeCluster, + Scope: apidiscoveryv2.ScopeCluster, ResponseKind: &metav1.GroupVersionKind{ Kind: "Node", }, diff --git a/staging/src/k8s.io/apiserver/pkg/server/genericapiserver.go b/staging/src/k8s.io/apiserver/pkg/server/genericapiserver.go index 53bed80fa94..6066c0fcbab 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/genericapiserver.go +++ b/staging/src/k8s.io/apiserver/pkg/server/genericapiserver.go @@ -29,7 +29,7 @@ import ( systemd "github.com/coreos/go-systemd/v22/daemon" "golang.org/x/time/rate" - apidiscoveryv2beta1 "k8s.io/api/apidiscovery/v2beta1" + apidiscoveryv2 "k8s.io/api/apidiscovery/v2" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -756,8 +756,8 @@ func (s *GenericAPIServer) installAPIResources(apiPrefix string, apiGroupInfo *A if apiPrefix == APIGroupPrefix { s.AggregatedDiscoveryGroupManager.AddGroupVersion( groupVersion.Group, - apidiscoveryv2beta1.APIVersionDiscovery{ - Freshness: apidiscoveryv2beta1.DiscoveryFreshnessCurrent, + apidiscoveryv2.APIVersionDiscovery{ + Freshness: apidiscoveryv2.DiscoveryFreshnessCurrent, Version: groupVersion.Version, Resources: discoveryAPIResources, }, @@ -766,8 +766,8 @@ func (s *GenericAPIServer) installAPIResources(apiPrefix string, apiGroupInfo *A // There is only one group version for legacy resources, priority can be defaulted to 0. s.AggregatedLegacyDiscoveryGroupManager.AddGroupVersion( groupVersion.Group, - apidiscoveryv2beta1.APIVersionDiscovery{ - Freshness: apidiscoveryv2beta1.DiscoveryFreshnessCurrent, + apidiscoveryv2.APIVersionDiscovery{ + Freshness: apidiscoveryv2.DiscoveryFreshnessCurrent, Version: groupVersion.Version, Resources: discoveryAPIResources, }, diff --git a/staging/src/k8s.io/client-go/discovery/aggregated_discovery.go b/staging/src/k8s.io/client-go/discovery/aggregated_discovery.go index f72c42051b9..f5eaaedab3a 100644 --- a/staging/src/k8s.io/client-go/discovery/aggregated_discovery.go +++ b/staging/src/k8s.io/client-go/discovery/aggregated_discovery.go @@ -19,7 +19,8 @@ package discovery import ( "fmt" - apidiscovery "k8s.io/api/apidiscovery/v2beta1" + apidiscovery "k8s.io/api/apidiscovery/v2" + apidiscoveryv2beta1 "k8s.io/api/apidiscovery/v2beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" ) @@ -154,3 +155,124 @@ func convertAPISubresource(parent metav1.APIResource, in apidiscovery.APISubreso result.Verbs = in.Verbs return result, nil } + +// Please note the functions below will be removed in v1.33. They facilitate conversion +// between the deprecated type apidiscoveryv2beta1.APIGroupDiscoveryList. + +// SplitGroupsAndResourcesV2Beta1 transforms "aggregated" discovery top-level structure into +// the previous "unaggregated" discovery groups and resources. +// Deprecated: Please use SplitGroupsAndResources +func SplitGroupsAndResourcesV2Beta1(aggregatedGroups apidiscoveryv2beta1.APIGroupDiscoveryList) ( + *metav1.APIGroupList, + map[schema.GroupVersion]*metav1.APIResourceList, + map[schema.GroupVersion]error) { + // Aggregated group list will contain the entirety of discovery, including + // groups, versions, and resources. GroupVersions marked "stale" are failed. + groups := []*metav1.APIGroup{} + failedGVs := map[schema.GroupVersion]error{} + resourcesByGV := map[schema.GroupVersion]*metav1.APIResourceList{} + for _, aggGroup := range aggregatedGroups.Items { + group, resources, failed := convertAPIGroupv2beta1(aggGroup) + groups = append(groups, group) + for gv, resourceList := range resources { + resourcesByGV[gv] = resourceList + } + for gv, err := range failed { + failedGVs[gv] = err + } + } + // Transform slice of groups to group list before returning. + groupList := &metav1.APIGroupList{} + groupList.Groups = make([]metav1.APIGroup, 0, len(groups)) + for _, group := range groups { + groupList.Groups = append(groupList.Groups, *group) + } + return groupList, resourcesByGV, failedGVs +} + +// convertAPIGroupv2beta1 tranforms an "aggregated" APIGroupDiscovery to an "legacy" APIGroup, +// also returning the map of APIResourceList for resources within GroupVersions. +func convertAPIGroupv2beta1(g apidiscoveryv2beta1.APIGroupDiscovery) ( + *metav1.APIGroup, + map[schema.GroupVersion]*metav1.APIResourceList, + map[schema.GroupVersion]error) { + // Iterate through versions to convert to group and resources. + group := &metav1.APIGroup{} + gvResources := map[schema.GroupVersion]*metav1.APIResourceList{} + failedGVs := map[schema.GroupVersion]error{} + group.Name = g.ObjectMeta.Name + for _, v := range g.Versions { + gv := schema.GroupVersion{Group: g.Name, Version: v.Version} + if v.Freshness == apidiscoveryv2beta1.DiscoveryFreshnessStale { + failedGVs[gv] = StaleGroupVersionError{gv: gv} + continue + } + version := metav1.GroupVersionForDiscovery{} + version.GroupVersion = gv.String() + version.Version = v.Version + group.Versions = append(group.Versions, version) + // PreferredVersion is first non-stale Version + if group.PreferredVersion == (metav1.GroupVersionForDiscovery{}) { + group.PreferredVersion = version + } + resourceList := &metav1.APIResourceList{} + resourceList.GroupVersion = gv.String() + for _, r := range v.Resources { + resource, err := convertAPIResourcev2beta1(r) + if err == nil { + resourceList.APIResources = append(resourceList.APIResources, resource) + } + // Subresources field in new format get transformed into full APIResources. + // It is possible a partial result with an error was returned to be used + // as the parent resource for the subresource. + for _, subresource := range r.Subresources { + sr, err := convertAPISubresourcev2beta1(resource, subresource) + if err == nil { + resourceList.APIResources = append(resourceList.APIResources, sr) + } + } + } + gvResources[gv] = resourceList + } + return group, gvResources, failedGVs +} + +// convertAPIResource tranforms a APIResourceDiscovery to an APIResource. We are +// resilient to missing GVK, since this resource might be the parent resource +// for a subresource. If the parent is missing a GVK, it is not returned in +// discovery, and the subresource MUST have the GVK. +func convertAPIResourcev2beta1(in apidiscoveryv2beta1.APIResourceDiscovery) (metav1.APIResource, error) { + result := metav1.APIResource{ + Name: in.Resource, + SingularName: in.SingularResource, + Namespaced: in.Scope == apidiscoveryv2beta1.ScopeNamespace, + Verbs: in.Verbs, + ShortNames: in.ShortNames, + Categories: in.Categories, + } + // Can return partial result with error, which can be the parent for a + // subresource. Do not add this result to the returned discovery resources. + if in.ResponseKind == nil || (*in.ResponseKind) == emptyKind { + return result, fmt.Errorf("discovery resource %s missing GVK", in.Resource) + } + result.Group = in.ResponseKind.Group + result.Version = in.ResponseKind.Version + result.Kind = in.ResponseKind.Kind + return result, nil +} + +// convertAPISubresource tranforms a APISubresourceDiscovery to an APIResource. +func convertAPISubresourcev2beta1(parent metav1.APIResource, in apidiscoveryv2beta1.APISubresourceDiscovery) (metav1.APIResource, error) { + result := metav1.APIResource{} + if in.ResponseKind == nil || (*in.ResponseKind) == emptyKind { + return result, fmt.Errorf("subresource %s/%s missing GVK", parent.Name, in.Subresource) + } + result.Name = fmt.Sprintf("%s/%s", parent.Name, in.Subresource) + result.SingularName = parent.SingularName + result.Namespaced = parent.Namespaced + result.Group = in.ResponseKind.Group + result.Version = in.ResponseKind.Version + result.Kind = in.ResponseKind.Kind + result.Verbs = in.Verbs + return result, nil +} diff --git a/staging/src/k8s.io/client-go/discovery/aggregated_discovery_test.go b/staging/src/k8s.io/client-go/discovery/aggregated_discovery_test.go index a5004804c1a..c9cb43d2d65 100644 --- a/staging/src/k8s.io/client-go/discovery/aggregated_discovery_test.go +++ b/staging/src/k8s.io/client-go/discovery/aggregated_discovery_test.go @@ -20,7 +20,8 @@ import ( "testing" "github.com/stretchr/testify/assert" - apidiscovery "k8s.io/api/apidiscovery/v2beta1" + apidiscovery "k8s.io/api/apidiscovery/v2" + apidiscoveryv2beta1 "k8s.io/api/apidiscovery/v2beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" ) @@ -961,3 +962,941 @@ func TestSplitGroupsAndResources(t *testing.T) { assert.Equal(t, test.expectedGVResources, resourcesByGV) } } + +// Duplicated from test above. Remove after 1.33 +func TestSplitGroupsAndResourcesV2Beta1(t *testing.T) { + tests := []struct { + name string + agg apidiscoveryv2beta1.APIGroupDiscoveryList + expectedGroups metav1.APIGroupList + expectedGVResources map[schema.GroupVersion]*metav1.APIResourceList + expectedFailedGVs map[schema.GroupVersion]error + }{ + { + name: "Aggregated discovery: core/v1 group and pod resource", + agg: apidiscoveryv2beta1.APIGroupDiscoveryList{ + Items: []apidiscoveryv2beta1.APIGroupDiscovery{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "", + }, + Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ + { + Version: "v1", + Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ + { + Resource: "pods", + ResponseKind: &metav1.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "Pod", + }, + Scope: apidiscoveryv2beta1.ScopeNamespace, + }, + }, + }, + }, + }, + }, + }, + expectedGroups: metav1.APIGroupList{ + Groups: []metav1.APIGroup{ + { + Name: "", + Versions: []metav1.GroupVersionForDiscovery{ + { + GroupVersion: "v1", + Version: "v1", + }, + }, + PreferredVersion: metav1.GroupVersionForDiscovery{ + GroupVersion: "v1", + Version: "v1", + }, + }, + }, + }, + expectedGVResources: map[schema.GroupVersion]*metav1.APIResourceList{ + {Group: "", Version: "v1"}: { + GroupVersion: "v1", + APIResources: []metav1.APIResource{ + { + Name: "pods", + Namespaced: true, + Group: "", + Version: "v1", + Kind: "Pod", + }, + }, + }, + }, + expectedFailedGVs: map[schema.GroupVersion]error{}, + }, + { + name: "Aggregated discovery: 1 group/1 resources at /api, 1 group/2 versions/1 resources at /apis", + agg: apidiscoveryv2beta1.APIGroupDiscoveryList{ + Items: []apidiscoveryv2beta1.APIGroupDiscovery{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "apps", + }, + Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ + { + Version: "v2", + Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ + { + Resource: "deployments", + ResponseKind: &metav1.GroupVersionKind{ + Group: "apps", + Version: "v2", + Kind: "Deployment", + }, + Scope: apidiscoveryv2beta1.ScopeNamespace, + }, + }, + }, + { + Version: "v1", + Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ + { + Resource: "deployments", + ResponseKind: &metav1.GroupVersionKind{ + Group: "apps", + Version: "v1", + Kind: "Deployment", + }, + Scope: apidiscoveryv2beta1.ScopeNamespace, + }, + }, + }, + }, + }, + }, + }, + expectedGroups: metav1.APIGroupList{ + Groups: []metav1.APIGroup{ + { + Name: "apps", + Versions: []metav1.GroupVersionForDiscovery{ + { + GroupVersion: "apps/v2", + Version: "v2", + }, + { + GroupVersion: "apps/v1", + Version: "v1", + }, + }, + PreferredVersion: metav1.GroupVersionForDiscovery{ + GroupVersion: "apps/v2", + Version: "v2", + }, + }, + }, + }, + expectedGVResources: map[schema.GroupVersion]*metav1.APIResourceList{ + {Group: "apps", Version: "v1"}: { + GroupVersion: "apps/v1", + APIResources: []metav1.APIResource{ + { + Name: "deployments", + Namespaced: true, + Group: "apps", + Version: "v1", + Kind: "Deployment", + }, + }, + }, + {Group: "apps", Version: "v2"}: { + GroupVersion: "apps/v2", + APIResources: []metav1.APIResource{ + { + Name: "deployments", + Namespaced: true, + Group: "apps", + Version: "v2", + Kind: "Deployment", + }, + }, + }, + }, + expectedFailedGVs: map[schema.GroupVersion]error{}, + }, + { + name: "Aggregated discovery: 1 group/2 resources at /api, 1 group/2 resources at /apis", + agg: apidiscoveryv2beta1.APIGroupDiscoveryList{ + Items: []apidiscoveryv2beta1.APIGroupDiscovery{ + { + Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ + { + Version: "v1", + Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ + { + Resource: "pods", + ResponseKind: &metav1.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "Pod", + }, + Scope: apidiscoveryv2beta1.ScopeNamespace, + }, + { + Resource: "services", + ResponseKind: &metav1.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "Service", + }, + Scope: apidiscoveryv2beta1.ScopeNamespace, + }, + }, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "apps", + }, + Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ + { + Version: "v1", + Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ + { + Resource: "deployments", + ResponseKind: &metav1.GroupVersionKind{ + Group: "apps", + Version: "v1", + Kind: "Deployment", + }, + Scope: apidiscoveryv2beta1.ScopeNamespace, + }, + { + Resource: "statefulsets", + ResponseKind: &metav1.GroupVersionKind{ + Group: "apps", + Version: "v1", + Kind: "StatefulSet", + }, + Scope: apidiscoveryv2beta1.ScopeNamespace, + }, + }, + }, + }, + }, + }, + }, + expectedGroups: metav1.APIGroupList{ + Groups: []metav1.APIGroup{ + { + Name: "", + Versions: []metav1.GroupVersionForDiscovery{ + { + GroupVersion: "v1", + Version: "v1", + }, + }, + PreferredVersion: metav1.GroupVersionForDiscovery{ + GroupVersion: "v1", + Version: "v1", + }, + }, + { + Name: "apps", + Versions: []metav1.GroupVersionForDiscovery{ + { + GroupVersion: "apps/v1", + Version: "v1", + }, + }, + PreferredVersion: metav1.GroupVersionForDiscovery{ + GroupVersion: "apps/v1", + Version: "v1", + }, + }, + }, + }, + expectedGVResources: map[schema.GroupVersion]*metav1.APIResourceList{ + {Group: "", Version: "v1"}: { + GroupVersion: "v1", + APIResources: []metav1.APIResource{ + { + Name: "pods", + Namespaced: true, + Group: "", + Version: "v1", + Kind: "Pod", + }, + { + Name: "services", + Namespaced: true, + Group: "", + Version: "v1", + Kind: "Service", + }, + }, + }, + {Group: "apps", Version: "v1"}: { + GroupVersion: "apps/v1", + APIResources: []metav1.APIResource{ + { + Name: "deployments", + Namespaced: true, + Group: "apps", + Version: "v1", + Kind: "Deployment", + }, + { + Name: "statefulsets", + Namespaced: true, + Group: "apps", + Version: "v1", + Kind: "StatefulSet", + }, + }, + }, + }, + expectedFailedGVs: map[schema.GroupVersion]error{}, + }, + { + name: "Aggregated discovery: multiple groups with cluster-scoped resources", + agg: apidiscoveryv2beta1.APIGroupDiscoveryList{ + Items: []apidiscoveryv2beta1.APIGroupDiscovery{ + { + Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ + { + Version: "v1", + Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ + { + Resource: "pods", + ResponseKind: &metav1.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "Pod", + }, + Scope: apidiscoveryv2beta1.ScopeNamespace, + }, + { + Resource: "namespaces", + ResponseKind: &metav1.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "Namespace", + }, + Scope: apidiscoveryv2beta1.ScopeCluster, + }, + }, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "rbac.authorization.k8s.io", + }, + Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ + { + Version: "v1", + Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ + { + Resource: "roles", + ResponseKind: &metav1.GroupVersionKind{ + Group: "rbac.authorization.k8s.io", + Version: "v1", + Kind: "Role", + }, + Scope: apidiscoveryv2beta1.ScopeCluster, + }, + { + Resource: "clusterroles", + ResponseKind: &metav1.GroupVersionKind{ + Group: "rbac.authorization.k8s.io", + Version: "v1", + Kind: "ClusterRole", + }, + Scope: apidiscoveryv2beta1.ScopeCluster, + }, + }, + }, + }, + }, + }, + }, + expectedGroups: metav1.APIGroupList{ + Groups: []metav1.APIGroup{ + { + Name: "", + Versions: []metav1.GroupVersionForDiscovery{ + { + GroupVersion: "v1", + Version: "v1", + }, + }, + PreferredVersion: metav1.GroupVersionForDiscovery{ + GroupVersion: "v1", + Version: "v1", + }, + }, + { + Name: "rbac.authorization.k8s.io", + Versions: []metav1.GroupVersionForDiscovery{ + { + GroupVersion: "rbac.authorization.k8s.io/v1", + Version: "v1", + }, + }, + PreferredVersion: metav1.GroupVersionForDiscovery{ + GroupVersion: "rbac.authorization.k8s.io/v1", + Version: "v1", + }, + }, + }, + }, + expectedGVResources: map[schema.GroupVersion]*metav1.APIResourceList{ + {Group: "", Version: "v1"}: { + GroupVersion: "v1", + APIResources: []metav1.APIResource{ + { + Name: "pods", + Namespaced: true, + Group: "", + Version: "v1", + Kind: "Pod", + }, + { + Name: "namespaces", + Namespaced: false, + Group: "", + Version: "v1", + Kind: "Namespace", + }, + }, + }, + {Group: "rbac.authorization.k8s.io", Version: "v1"}: { + GroupVersion: "rbac.authorization.k8s.io/v1", + APIResources: []metav1.APIResource{ + { + Name: "roles", + Namespaced: false, + Group: "rbac.authorization.k8s.io", + Version: "v1", + Kind: "Role", + }, + { + Name: "clusterroles", + Namespaced: false, + Group: "rbac.authorization.k8s.io", + Version: "v1", + Kind: "ClusterRole", + }, + }, + }, + }, + expectedFailedGVs: map[schema.GroupVersion]error{}, + }, + { + name: "Aggregated discovery with single subresource", + agg: apidiscoveryv2beta1.APIGroupDiscoveryList{ + Items: []apidiscoveryv2beta1.APIGroupDiscovery{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "apps", + }, + Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ + { + Version: "v1", + Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ + { + Resource: "deployments", + ResponseKind: &metav1.GroupVersionKind{ + Group: "apps", + Version: "v1", + Kind: "Deployment", + }, + Scope: apidiscoveryv2beta1.ScopeNamespace, + SingularResource: "deployment", + ShortNames: []string{"deploy"}, + Verbs: []string{"parentverb1", "parentverb2", "parentverb3", "parentverb4"}, + Categories: []string{"all", "testcategory"}, + Subresources: []apidiscoveryv2beta1.APISubresourceDiscovery{ + { + Subresource: "scale", + ResponseKind: &metav1.GroupVersionKind{ + Group: "apps", + Version: "v1", + Kind: "Deployment", + }, + Verbs: []string{"get", "patch", "update"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + expectedGroups: metav1.APIGroupList{ + Groups: []metav1.APIGroup{ + { + Name: "apps", + Versions: []metav1.GroupVersionForDiscovery{ + { + GroupVersion: "apps/v1", + Version: "v1", + }, + }, + PreferredVersion: metav1.GroupVersionForDiscovery{ + GroupVersion: "apps/v1", + Version: "v1", + }, + }, + }, + }, + expectedGVResources: map[schema.GroupVersion]*metav1.APIResourceList{ + {Group: "apps", Version: "v1"}: { + GroupVersion: "apps/v1", + APIResources: []metav1.APIResource{ + { + Name: "deployments", + SingularName: "deployment", + Namespaced: true, + Group: "apps", + Version: "v1", + Kind: "Deployment", + Verbs: []string{"parentverb1", "parentverb2", "parentverb3", "parentverb4"}, + ShortNames: []string{"deploy"}, + Categories: []string{"all", "testcategory"}, + }, + { + Name: "deployments/scale", + SingularName: "deployment", + Namespaced: true, + Group: "apps", + Version: "v1", + Kind: "Deployment", + Verbs: []string{"get", "patch", "update"}, + }, + }, + }, + }, + expectedFailedGVs: map[schema.GroupVersion]error{}, + }, + { + name: "Aggregated discovery with single subresource and parent missing GVK", + agg: apidiscoveryv2beta1.APIGroupDiscoveryList{ + Items: []apidiscoveryv2beta1.APIGroupDiscovery{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "external.metrics.k8s.io", + }, + Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ + { + Version: "v1beta1", + Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ + { + // resilient to nil GVK for parent + Resource: "*", + Scope: apidiscoveryv2beta1.ScopeNamespace, + SingularResource: "", + Subresources: []apidiscoveryv2beta1.APISubresourceDiscovery{ + { + Subresource: "other-external-metric", + ResponseKind: &metav1.GroupVersionKind{ + Kind: "MetricValueList", + }, + Verbs: []string{"get"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + expectedGroups: metav1.APIGroupList{ + Groups: []metav1.APIGroup{ + { + Name: "external.metrics.k8s.io", + Versions: []metav1.GroupVersionForDiscovery{ + { + GroupVersion: "external.metrics.k8s.io/v1beta1", + Version: "v1beta1", + }, + }, + PreferredVersion: metav1.GroupVersionForDiscovery{ + GroupVersion: "external.metrics.k8s.io/v1beta1", + Version: "v1beta1", + }, + }, + }, + }, + expectedGVResources: map[schema.GroupVersion]*metav1.APIResourceList{ + {Group: "external.metrics.k8s.io", Version: "v1beta1"}: { + GroupVersion: "external.metrics.k8s.io/v1beta1", + APIResources: []metav1.APIResource{ + // Since parent GVK was nil, it is NOT returned--only the subresource. + { + Name: "*/other-external-metric", + SingularName: "", + Namespaced: true, + Group: "", + Version: "", + Kind: "MetricValueList", + Verbs: []string{"get"}, + }, + }, + }, + }, + expectedFailedGVs: map[schema.GroupVersion]error{}, + }, + { + name: "Aggregated discovery with single subresource and parent empty GVK", + agg: apidiscoveryv2beta1.APIGroupDiscoveryList{ + Items: []apidiscoveryv2beta1.APIGroupDiscovery{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "external.metrics.k8s.io", + }, + Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ + { + Version: "v1beta1", + Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ + { + // resilient to empty GVK for parent + Resource: "*", + Scope: apidiscoveryv2beta1.ScopeNamespace, + SingularResource: "", + ResponseKind: &metav1.GroupVersionKind{}, + Subresources: []apidiscoveryv2beta1.APISubresourceDiscovery{ + { + Subresource: "other-external-metric", + ResponseKind: &metav1.GroupVersionKind{ + Kind: "MetricValueList", + }, + Verbs: []string{"get"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + expectedGroups: metav1.APIGroupList{ + Groups: []metav1.APIGroup{ + { + Name: "external.metrics.k8s.io", + Versions: []metav1.GroupVersionForDiscovery{ + { + GroupVersion: "external.metrics.k8s.io/v1beta1", + Version: "v1beta1", + }, + }, + PreferredVersion: metav1.GroupVersionForDiscovery{ + GroupVersion: "external.metrics.k8s.io/v1beta1", + Version: "v1beta1", + }, + }, + }, + }, + expectedGVResources: map[schema.GroupVersion]*metav1.APIResourceList{ + {Group: "external.metrics.k8s.io", Version: "v1beta1"}: { + GroupVersion: "external.metrics.k8s.io/v1beta1", + APIResources: []metav1.APIResource{ + // Since parent GVK was nil, it is NOT returned--only the subresource. + { + Name: "*/other-external-metric", + SingularName: "", + Namespaced: true, + Group: "", + Version: "", + Kind: "MetricValueList", + Verbs: []string{"get"}, + }, + }, + }, + }, + expectedFailedGVs: map[schema.GroupVersion]error{}, + }, + { + name: "Aggregated discovery with multiple subresources", + agg: apidiscoveryv2beta1.APIGroupDiscoveryList{ + Items: []apidiscoveryv2beta1.APIGroupDiscovery{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "apps", + }, + Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ + { + Version: "v1", + Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ + { + Resource: "deployments", + ResponseKind: &metav1.GroupVersionKind{ + Group: "apps", + Version: "v1", + Kind: "Deployment", + }, + Scope: apidiscoveryv2beta1.ScopeNamespace, + SingularResource: "deployment", + Subresources: []apidiscoveryv2beta1.APISubresourceDiscovery{ + { + Subresource: "scale", + ResponseKind: &metav1.GroupVersionKind{ + Group: "apps", + Version: "v1", + Kind: "Deployment", + }, + Verbs: []string{"get", "patch", "update"}, + }, + { + Subresource: "status", + ResponseKind: &metav1.GroupVersionKind{ + Group: "apps", + Version: "v1", + Kind: "Deployment", + }, + Verbs: []string{"get", "patch", "update"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + expectedGroups: metav1.APIGroupList{ + Groups: []metav1.APIGroup{ + { + Name: "apps", + Versions: []metav1.GroupVersionForDiscovery{ + { + GroupVersion: "apps/v1", + Version: "v1", + }, + }, + PreferredVersion: metav1.GroupVersionForDiscovery{ + GroupVersion: "apps/v1", + Version: "v1", + }, + }, + }, + }, + expectedGVResources: map[schema.GroupVersion]*metav1.APIResourceList{ + {Group: "apps", Version: "v1"}: { + GroupVersion: "apps/v1", + APIResources: []metav1.APIResource{ + { + Name: "deployments", + SingularName: "deployment", + Namespaced: true, + Group: "apps", + Version: "v1", + Kind: "Deployment", + }, + { + Name: "deployments/scale", + SingularName: "deployment", + Namespaced: true, + Group: "apps", + Version: "v1", + Kind: "Deployment", + Verbs: []string{"get", "patch", "update"}, + }, + { + Name: "deployments/status", + SingularName: "deployment", + Namespaced: true, + Group: "apps", + Version: "v1", + Kind: "Deployment", + Verbs: []string{"get", "patch", "update"}, + }, + }, + }, + }, + expectedFailedGVs: map[schema.GroupVersion]error{}, + }, + { + name: "Aggregated discovery: single failed GV at /api", + agg: apidiscoveryv2beta1.APIGroupDiscoveryList{ + Items: []apidiscoveryv2beta1.APIGroupDiscovery{ + { + Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ + { + Version: "v1", + Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ + { + Resource: "pods", + ResponseKind: &metav1.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "Pod", + }, + Scope: apidiscoveryv2beta1.ScopeNamespace, + }, + { + Resource: "services", + ResponseKind: &metav1.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "Service", + }, + Scope: apidiscoveryv2beta1.ScopeNamespace, + }, + }, + Freshness: apidiscoveryv2beta1.DiscoveryFreshnessStale, + }, + }, + }, + }, + }, + // Single core Group/Version is stale, so no Version within Group. + expectedGroups: metav1.APIGroupList{ + Groups: []metav1.APIGroup{{Name: ""}}, + }, + // Single core Group/Version is stale, so there are no expected resources. + expectedGVResources: map[schema.GroupVersion]*metav1.APIResourceList{}, + expectedFailedGVs: map[schema.GroupVersion]error{ + {Group: "", Version: "v1"}: StaleGroupVersionError{gv: schema.GroupVersion{Group: "", Version: "v1"}}, + }, + }, + { + name: "Aggregated discovery: single failed GV at /apis", + agg: apidiscoveryv2beta1.APIGroupDiscoveryList{ + Items: []apidiscoveryv2beta1.APIGroupDiscovery{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "apps", + }, + Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ + { + Version: "v1", + Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ + { + Resource: "deployments", + ResponseKind: &metav1.GroupVersionKind{ + Group: "apps", + Version: "v1", + Kind: "Deployment", + }, + Scope: apidiscoveryv2beta1.ScopeNamespace, + }, + { + Resource: "statefulsets", + ResponseKind: &metav1.GroupVersionKind{ + Group: "apps", + Version: "v1", + Kind: "StatefulSets", + }, + Scope: apidiscoveryv2beta1.ScopeNamespace, + }, + }, + Freshness: apidiscoveryv2beta1.DiscoveryFreshnessStale, + }, + }, + }, + }, + }, + // Single apps/v1 Group/Version is stale, so no Version within Group. + expectedGroups: metav1.APIGroupList{ + Groups: []metav1.APIGroup{{Name: "apps"}}, + }, + // Single apps/v1 Group/Version is stale, so there are no expected resources. + expectedGVResources: map[schema.GroupVersion]*metav1.APIResourceList{}, + expectedFailedGVs: map[schema.GroupVersion]error{ + {Group: "apps", Version: "v1"}: StaleGroupVersionError{gv: schema.GroupVersion{Group: "apps", Version: "v1"}}, + }, + }, + { + name: "Aggregated discovery: 1 group/2 versions/1 failed GV at /apis", + agg: apidiscoveryv2beta1.APIGroupDiscoveryList{ + Items: []apidiscoveryv2beta1.APIGroupDiscovery{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "apps", + }, + Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ + // Stale v2 should report failed GV. + { + Version: "v2", + Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ + { + Resource: "daemonsets", + ResponseKind: &metav1.GroupVersionKind{ + Group: "apps", + Version: "v2", + Kind: "DaemonSets", + }, + Scope: apidiscoveryv2beta1.ScopeNamespace, + }, + }, + Freshness: apidiscoveryv2beta1.DiscoveryFreshnessStale, + }, + { + Version: "v1", + Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ + { + Resource: "deployments", + ResponseKind: &metav1.GroupVersionKind{ + Group: "apps", + Version: "v1", + Kind: "Deployment", + }, + Scope: apidiscoveryv2beta1.ScopeNamespace, + }, + }, + }, + }, + }, + }, + }, + // Only apps/v1 is non-stale expected Group/Version + expectedGroups: metav1.APIGroupList{ + Groups: []metav1.APIGroup{ + { + Name: "apps", + Versions: []metav1.GroupVersionForDiscovery{ + { + GroupVersion: "apps/v1", + Version: "v1", + }, + }, + // PreferredVersion must be apps/v1 + PreferredVersion: metav1.GroupVersionForDiscovery{ + GroupVersion: "apps/v1", + Version: "v1", + }, + }, + }, + }, + // Only apps/v1 resources expected. + expectedGVResources: map[schema.GroupVersion]*metav1.APIResourceList{ + {Group: "apps", Version: "v1"}: { + GroupVersion: "apps/v1", + APIResources: []metav1.APIResource{ + { + Name: "deployments", + Namespaced: true, + Group: "apps", + Version: "v1", + Kind: "Deployment", + }, + }, + }, + }, + expectedFailedGVs: map[schema.GroupVersion]error{ + {Group: "apps", Version: "v2"}: StaleGroupVersionError{gv: schema.GroupVersion{Group: "apps", Version: "v2"}}, + }, + }, + } + + for _, test := range tests { + apiGroups, resourcesByGV, failedGVs := SplitGroupsAndResourcesV2Beta1(test.agg) + assert.Equal(t, test.expectedFailedGVs, failedGVs) + assert.Equal(t, test.expectedGroups, *apiGroups) + assert.Equal(t, test.expectedGVResources, resourcesByGV) + } +} diff --git a/staging/src/k8s.io/client-go/discovery/cached/disk/cached_discovery_test.go b/staging/src/k8s.io/client-go/discovery/cached/disk/cached_discovery_test.go index e6c7a03af76..f27f2a4331c 100644 --- a/staging/src/k8s.io/client-go/discovery/cached/disk/cached_discovery_test.go +++ b/staging/src/k8s.io/client-go/discovery/cached/disk/cached_discovery_test.go @@ -30,7 +30,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - apidiscovery "k8s.io/api/apidiscovery/v2beta1" + apidiscovery "k8s.io/api/apidiscovery/v2" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -643,7 +643,7 @@ func TestCachedDiscoveryClientAggregatedServerGroups(t *testing.T) { return } // Content-type is "aggregated" discovery format. - w.Header().Set("Content-Type", discovery.AcceptV2Beta1) + w.Header().Set("Content-Type", discovery.AcceptV2) w.WriteHeader(http.StatusOK) w.Write(output) })) diff --git a/staging/src/k8s.io/client-go/discovery/cached/memory/memcache_test.go b/staging/src/k8s.io/client-go/discovery/cached/memory/memcache_test.go index a86ece331a9..dbd7b46cde6 100644 --- a/staging/src/k8s.io/client-go/discovery/cached/memory/memcache_test.go +++ b/staging/src/k8s.io/client-go/discovery/cached/memory/memcache_test.go @@ -28,7 +28,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - apidiscovery "k8s.io/api/apidiscovery/v2beta1" + apidiscovery "k8s.io/api/apidiscovery/v2" errorsutil "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -1118,7 +1118,7 @@ func TestAggregatedMemCacheGroupsAndMaybeResources(t *testing.T) { output, err := json.Marshal(agg) require.NoError(t, err) // Content-type is "aggregated" discovery format. - w.Header().Set("Content-Type", discovery.AcceptV2Beta1) + w.Header().Set("Content-Type", discovery.AcceptV2) w.WriteHeader(http.StatusOK) w.Write(output) })) @@ -1161,6 +1161,7 @@ func TestAggregatedMemCacheGroupsAndMaybeResources(t *testing.T) { memClient.Invalidate() assert.False(t, memClient.Fresh()) apiGroupList, _, _, err = memClient.GroupsAndMaybeResources() + require.NoError(t, err) // Test the expected groups are returned for the aggregated format. actualGroupNames = sets.NewString(groupNamesFromList(apiGroupList)...) diff --git a/staging/src/k8s.io/client-go/discovery/discovery_client.go b/staging/src/k8s.io/client-go/discovery/discovery_client.go index df0e0f9974e..ef14fee5f09 100644 --- a/staging/src/k8s.io/client-go/discovery/discovery_client.go +++ b/staging/src/k8s.io/client-go/discovery/discovery_client.go @@ -33,7 +33,8 @@ import ( "github.com/golang/protobuf/proto" openapi_v2 "github.com/google/gnostic-models/openapiv2" - apidiscovery "k8s.io/api/apidiscovery/v2beta1" + apidiscoveryv2 "k8s.io/api/apidiscovery/v2" + apidiscoveryv2beta1 "k8s.io/api/apidiscovery/v2beta1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -64,12 +65,14 @@ const ( // MUST be ordered (g, v, as) for server in "Accept" header (BUT we are resilient // to ordering when comparing returned values in "Content-Type" header). AcceptV2Beta1 = runtime.ContentTypeJSON + ";" + "g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList" + AcceptV2 = runtime.ContentTypeJSON + ";" + "g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList" // Prioritize aggregated discovery by placing first in the order of discovery accept types. - acceptDiscoveryFormats = AcceptV2Beta1 + "," + AcceptV1 + acceptDiscoveryFormats = AcceptV2 + "," + AcceptV2Beta1 + "," + AcceptV1 ) // Aggregated discovery content-type GVK. var v2Beta1GVK = schema.GroupVersionKind{Group: "apidiscovery.k8s.io", Version: "v2beta1", Kind: "APIGroupDiscoveryList"} +var v2GVK = schema.GroupVersionKind{Group: "apidiscovery.k8s.io", Version: "v2", Kind: "APIGroupDiscoveryList"} // DiscoveryInterface holds the methods that discover server-supported API groups, // versions and resources. @@ -265,13 +268,20 @@ func (d *DiscoveryClient) downloadLegacy() ( var resourcesByGV map[schema.GroupVersion]*metav1.APIResourceList // Based on the content-type server responded with: aggregated or unaggregated. - if isGVK, _ := ContentTypeIsGVK(responseContentType, v2Beta1GVK); isGVK { - var aggregatedDiscovery apidiscovery.APIGroupDiscoveryList + if isGVK, _ := ContentTypeIsGVK(responseContentType, v2GVK); isGVK { + var aggregatedDiscovery apidiscoveryv2.APIGroupDiscoveryList err = json.Unmarshal(body, &aggregatedDiscovery) if err != nil { return nil, nil, nil, err } apiGroupList, resourcesByGV, failedGVs = SplitGroupsAndResources(aggregatedDiscovery) + } else if isGVK, _ := ContentTypeIsGVK(responseContentType, v2Beta1GVK); isGVK { + var aggregatedDiscovery apidiscoveryv2beta1.APIGroupDiscoveryList + err = json.Unmarshal(body, &aggregatedDiscovery) + if err != nil { + return nil, nil, nil, err + } + apiGroupList, resourcesByGV, failedGVs = SplitGroupsAndResourcesV2Beta1(aggregatedDiscovery) } else { // Default is unaggregated discovery v1. var v metav1.APIVersions @@ -317,13 +327,20 @@ func (d *DiscoveryClient) downloadAPIs() ( failedGVs := map[schema.GroupVersion]error{} var resourcesByGV map[schema.GroupVersion]*metav1.APIResourceList // Based on the content-type server responded with: aggregated or unaggregated. - if isGVK, _ := ContentTypeIsGVK(responseContentType, v2Beta1GVK); isGVK { - var aggregatedDiscovery apidiscovery.APIGroupDiscoveryList + if isGVK, _ := ContentTypeIsGVK(responseContentType, v2GVK); isGVK { + var aggregatedDiscovery apidiscoveryv2.APIGroupDiscoveryList err = json.Unmarshal(body, &aggregatedDiscovery) if err != nil { return nil, nil, nil, err } apiGroupList, resourcesByGV, failedGVs = SplitGroupsAndResources(aggregatedDiscovery) + } else if isGVK, _ := ContentTypeIsGVK(responseContentType, v2Beta1GVK); isGVK { + var aggregatedDiscovery apidiscoveryv2beta1.APIGroupDiscoveryList + err = json.Unmarshal(body, &aggregatedDiscovery) + if err != nil { + return nil, nil, nil, err + } + apiGroupList, resourcesByGV, failedGVs = SplitGroupsAndResourcesV2Beta1(aggregatedDiscovery) } else { // Default is unaggregated discovery v1. err = json.Unmarshal(body, apiGroupList) diff --git a/staging/src/k8s.io/client-go/discovery/discovery_client_test.go b/staging/src/k8s.io/client-go/discovery/discovery_client_test.go index db96ba3bb8c..87e6c95aa91 100644 --- a/staging/src/k8s.io/client-go/discovery/discovery_client_test.go +++ b/staging/src/k8s.io/client-go/discovery/discovery_client_test.go @@ -32,7 +32,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" golangproto "google.golang.org/protobuf/proto" - apidiscovery "k8s.io/api/apidiscovery/v2beta1" + apidiscovery "k8s.io/api/apidiscovery/v2" + apidiscoveryv2beta1 "k8s.io/api/apidiscovery/v2beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -58,7 +59,8 @@ func TestGetServerVersion(t *testing.T) { } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - w.Write(output) + _, err = w.Write(output) + require.NoError(t, err) })) defer server.Close() client := NewDiscoveryClientForConfigOrDie(&restclient.Config{Host: server.URL}) @@ -104,7 +106,8 @@ func TestGetServerGroupsWithV1Server(t *testing.T) { } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - w.Write(output) + _, err = w.Write(output) + require.NoError(t, err) })) defer server.Close() client := NewDiscoveryClientForConfigOrDie(&restclient.Config{Host: server.URL}) @@ -144,7 +147,8 @@ func TestDiscoveryToleratesMissingCoreGroup(t *testing.T) { } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - w.Write(output) + _, err = w.Write(output) + require.NoError(t, err) })) defer server.Close() client := NewDiscoveryClientForConfigOrDie(&restclient.Config{Host: server.URL}) @@ -180,7 +184,8 @@ func TestDiscoveryFailsWhenNonCoreGroupsMissing(t *testing.T) { } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - w.Write(output) + _, err = w.Write(output) + require.NoError(t, err) })) defer server.Close() client := NewDiscoveryClientForConfigOrDie(&restclient.Config{Host: server.URL}) @@ -380,7 +385,8 @@ func TestGetServerResourcesForGroupVersion(t *testing.T) { } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - w.Write(output) + _, err = w.Write(output) + require.NoError(t, err) })) defer server.Close() for _, test := range tests { @@ -1294,6 +1300,8 @@ func TestAggregatedServerGroups(t *testing.T) { for _, test := range tests { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + var output []byte + var err error var agg *apidiscovery.APIGroupDiscoveryList switch req.URL.Path { case "/api": @@ -1304,13 +1312,14 @@ func TestAggregatedServerGroups(t *testing.T) { w.WriteHeader(http.StatusNotFound) return } - output, err := json.Marshal(agg) + output, err = json.Marshal(agg) require.NoError(t, err) // Content-Type is "aggregated" discovery format. Add extra parameter // to ensure we are resilient to these extra parameters. - w.Header().Set("Content-Type", AcceptV2Beta1+"; charset=utf-8") + w.Header().Set("Content-Type", AcceptV2+"; charset=utf-8") w.WriteHeader(http.StatusOK) - w.Write(output) + _, err = w.Write(output) + require.NoError(t, err) })) defer server.Close() client := NewDiscoveryClientForConfigOrDie(&restclient.Config{Host: server.URL}) @@ -1338,7 +1347,9 @@ func TestAggregatedServerGroupsAndResources(t *testing.T) { tests := []struct { name string corev1 *apidiscovery.APIGroupDiscoveryList + corev1DiscoveryBeta *apidiscoveryv2beta1.APIGroupDiscoveryList apis *apidiscovery.APIGroupDiscoveryList + apisDiscoveryBeta *apidiscoveryv2beta1.APIGroupDiscoveryList expectedGroupNames []string expectedGroupVersions []string expectedGVKs []string @@ -1368,6 +1379,28 @@ func TestAggregatedServerGroupsAndResources(t *testing.T) { }, }, }, + corev1DiscoveryBeta: &apidiscoveryv2beta1.APIGroupDiscoveryList{ + Items: []apidiscoveryv2beta1.APIGroupDiscovery{ + { + Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ + { + Version: "v1", + Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ + { + Resource: "pods", + ResponseKind: &metav1.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "Pod", + }, + Scope: apidiscoveryv2beta1.ScopeNamespace, + }, + }, + }, + }, + }, + }, + }, apis: &apidiscovery.APIGroupDiscoveryList{ Items: []apidiscovery.APIGroupDiscovery{ { @@ -1393,6 +1426,31 @@ func TestAggregatedServerGroupsAndResources(t *testing.T) { }, }, }, + apisDiscoveryBeta: &apidiscoveryv2beta1.APIGroupDiscoveryList{ + Items: []apidiscoveryv2beta1.APIGroupDiscovery{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "apps", + }, + Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ + { + Version: "v1", + Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ + { + Resource: "deployments", + ResponseKind: &metav1.GroupVersionKind{ + Group: "apps", + Version: "v1", + Kind: "Deployment", + }, + Scope: apidiscoveryv2beta1.ScopeNamespace, + }, + }, + }, + }, + }, + }, + }, expectedGroupNames: []string{"", "apps"}, expectedGroupVersions: []string{"v1", "apps/v1"}, expectedGVKs: []string{ @@ -1424,6 +1482,28 @@ func TestAggregatedServerGroupsAndResources(t *testing.T) { }, }, }, + corev1DiscoveryBeta: &apidiscoveryv2beta1.APIGroupDiscoveryList{ + Items: []apidiscoveryv2beta1.APIGroupDiscovery{ + { + Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ + { + Version: "v1", + Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ + { + Resource: "pods", + ResponseKind: &metav1.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "Pod", + }, + Scope: apidiscoveryv2beta1.ScopeNamespace, + }, + }, + }, + }, + }, + }, + }, apis: &apidiscovery.APIGroupDiscoveryList{ Items: []apidiscovery.APIGroupDiscovery{ { @@ -1463,6 +1543,45 @@ func TestAggregatedServerGroupsAndResources(t *testing.T) { }, }, }, + apisDiscoveryBeta: &apidiscoveryv2beta1.APIGroupDiscoveryList{ + Items: []apidiscoveryv2beta1.APIGroupDiscovery{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "apps", + }, + Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ + { + Version: "v1", + Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ + { + Resource: "deployments", + ResponseKind: &metav1.GroupVersionKind{ + Group: "apps", + Version: "v1", + Kind: "Deployment", + }, + Scope: apidiscoveryv2beta1.ScopeNamespace, + }, + }, + }, + { + Version: "v2", + Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ + { + Resource: "deployments", + ResponseKind: &metav1.GroupVersionKind{ + Group: "apps", + Version: "v2", + Kind: "Deployment", + }, + Scope: apidiscoveryv2beta1.ScopeNamespace, + }, + }, + }, + }, + }, + }, + }, expectedGroupNames: []string{"", "apps"}, expectedGroupVersions: []string{"v1", "apps/v1", "apps/v2"}, expectedGVKs: []string{ @@ -1495,6 +1614,28 @@ func TestAggregatedServerGroupsAndResources(t *testing.T) { }, }, }, + corev1DiscoveryBeta: &apidiscoveryv2beta1.APIGroupDiscoveryList{ + Items: []apidiscoveryv2beta1.APIGroupDiscovery{ + { + Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ + { + Version: "v1", + Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ + { + Resource: "pods", + ResponseKind: &metav1.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "Pod", + }, + Scope: apidiscoveryv2beta1.ScopeNamespace, + }, + }, + }, + }, + }, + }, + }, apis: &apidiscovery.APIGroupDiscoveryList{ Items: []apidiscovery.APIGroupDiscovery{ { @@ -1535,6 +1676,46 @@ func TestAggregatedServerGroupsAndResources(t *testing.T) { }, }, }, + apisDiscoveryBeta: &apidiscoveryv2beta1.APIGroupDiscoveryList{ + Items: []apidiscoveryv2beta1.APIGroupDiscovery{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "apps", + }, + Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ + { + Version: "v1", + Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ + { + Resource: "deployments", + ResponseKind: &metav1.GroupVersionKind{ + Group: "apps", + Version: "v1", + Kind: "Deployment", + }, + Scope: apidiscoveryv2beta1.ScopeNamespace, + }, + }, + }, + { + Version: "v2", + Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ + { + Resource: "deployments", + ResponseKind: &metav1.GroupVersionKind{ + Group: "apps", + Version: "v2", + Kind: "Deployment", + }, + Scope: apidiscoveryv2beta1.ScopeNamespace, + }, + }, + Freshness: apidiscoveryv2beta1.DiscoveryFreshnessStale, + }, + }, + }, + }, + }, expectedGroupNames: []string{"", "apps"}, expectedGroupVersions: []string{"v1", "apps/v1"}, expectedGVKs: []string{ @@ -1576,6 +1757,37 @@ func TestAggregatedServerGroupsAndResources(t *testing.T) { }, }, }, + corev1DiscoveryBeta: &apidiscoveryv2beta1.APIGroupDiscoveryList{ + Items: []apidiscoveryv2beta1.APIGroupDiscovery{ + { + Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ + { + Version: "v1", + Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ + { + Resource: "pods", + ResponseKind: &metav1.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "Pod", + }, + Scope: apidiscoveryv2beta1.ScopeNamespace, + }, + { + Resource: "services", + ResponseKind: &metav1.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "Service", + }, + Scope: apidiscoveryv2beta1.ScopeNamespace, + }, + }, + }, + }, + }, + }, + }, apis: &apidiscovery.APIGroupDiscoveryList{ Items: []apidiscovery.APIGroupDiscovery{ { @@ -1635,6 +1847,65 @@ func TestAggregatedServerGroupsAndResources(t *testing.T) { }, }, }, + apisDiscoveryBeta: &apidiscoveryv2beta1.APIGroupDiscoveryList{ + Items: []apidiscoveryv2beta1.APIGroupDiscovery{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "apps", + }, + Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ + // Stale "v2" version not included. + { + Version: "v2", + Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ + { + Resource: "deployments", + ResponseKind: &metav1.GroupVersionKind{ + Group: "apps", + Version: "v2", + Kind: "Deployment", + }, + Scope: apidiscoveryv2beta1.ScopeNamespace, + }, + { + Resource: "statefulsets", + ResponseKind: &metav1.GroupVersionKind{ + Group: "apps", + Version: "v2", + Kind: "StatefulSet", + }, + Scope: apidiscoveryv2beta1.ScopeNamespace, + }, + }, + Freshness: apidiscoveryv2beta1.DiscoveryFreshnessStale, + }, + { + Version: "v1", + Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ + { + Resource: "deployments", + ResponseKind: &metav1.GroupVersionKind{ + Group: "apps", + Version: "v1", + Kind: "Deployment", + }, + Scope: apidiscoveryv2beta1.ScopeNamespace, + }, + { + Resource: "statefulsets", + ResponseKind: &metav1.GroupVersionKind{ + Group: "apps", + Version: "v1", + Kind: "StatefulSet", + }, + Scope: apidiscoveryv2beta1.ScopeNamespace, + }, + }, + }, + }, + }, + }, + }, expectedGroupNames: []string{"", "apps"}, expectedGroupVersions: []string{"v1", "apps/v1"}, expectedGVKs: []string{ @@ -1678,6 +1949,37 @@ func TestAggregatedServerGroupsAndResources(t *testing.T) { }, }, }, + corev1DiscoveryBeta: &apidiscoveryv2beta1.APIGroupDiscoveryList{ + Items: []apidiscoveryv2beta1.APIGroupDiscovery{ + { + Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ + { + Version: "v1", + Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ + { + Resource: "pods", + ResponseKind: &metav1.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "Pod", + }, + Scope: apidiscoveryv2beta1.ScopeNamespace, + }, + { + Resource: "services", + ResponseKind: &metav1.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "Service", + }, + Scope: apidiscoveryv2beta1.ScopeNamespace, + }, + }, + }, + }, + }, + }, + }, apis: &apidiscovery.APIGroupDiscoveryList{ Items: []apidiscovery.APIGroupDiscovery{ { @@ -1767,6 +2069,95 @@ func TestAggregatedServerGroupsAndResources(t *testing.T) { }, }, }, + apisDiscoveryBeta: &apidiscoveryv2beta1.APIGroupDiscoveryList{ + Items: []apidiscoveryv2beta1.APIGroupDiscovery{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "apps", + }, + Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ + { + Version: "v1", + Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ + { + Resource: "deployments", + ResponseKind: &metav1.GroupVersionKind{ + Group: "apps", + Version: "v1", + Kind: "Deployment", + }, + Scope: apidiscoveryv2beta1.ScopeNamespace, + }, + { + Resource: "statefulsets", + ResponseKind: &metav1.GroupVersionKind{ + Group: "apps", + Version: "v1", + Kind: "StatefulSet", + }, + Scope: apidiscoveryv2beta1.ScopeNamespace, + }, + }, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "batch", + }, + Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ + // Stale Group/Version is not included + { + Version: "v1", + Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ + { + Resource: "jobs", + ResponseKind: &metav1.GroupVersionKind{ + Group: "batch", + Version: "v1", + Kind: "Job", + }, + Scope: apidiscoveryv2beta1.ScopeNamespace, + }, + { + Resource: "cronjobs", + ResponseKind: &metav1.GroupVersionKind{ + Group: "batch", + Version: "v1", + Kind: "CronJob", + }, + Scope: apidiscoveryv2beta1.ScopeNamespace, + }, + }, + Freshness: apidiscoveryv2beta1.DiscoveryFreshnessStale, + }, + { + Version: "v1beta1", + Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ + { + Resource: "jobs", + ResponseKind: &metav1.GroupVersionKind{ + Group: "batch", + Version: "v1beta1", + Kind: "Job", + }, + Scope: apidiscoveryv2beta1.ScopeNamespace, + }, + { + Resource: "cronjobs", + ResponseKind: &metav1.GroupVersionKind{ + Group: "batch", + Version: "v1beta1", + Kind: "CronJob", + }, + Scope: apidiscoveryv2beta1.ScopeNamespace, + }, + }, + }, + }, + }, + }, + }, expectedGroupNames: []string{"", "apps", "batch"}, expectedGroupVersions: []string{"v1", "apps/v1", "batch/v1beta1"}, expectedGVKs: []string{ @@ -1780,8 +2171,9 @@ func TestAggregatedServerGroupsAndResources(t *testing.T) { expectedFailedGVs: []string{"batch/v1"}, }, { - name: "Aggregated discovery: /api returns nothing, 2 groups/2 resources at /apis", - corev1: &apidiscovery.APIGroupDiscoveryList{}, + name: "Aggregated discovery: /api returns nothing, 2 groups/2 resources at /apis", + corev1: &apidiscovery.APIGroupDiscoveryList{}, + corev1DiscoveryBeta: &apidiscoveryv2beta1.APIGroupDiscoveryList{}, apis: &apidiscovery.APIGroupDiscoveryList{ Items: []apidiscovery.APIGroupDiscovery{ { @@ -1871,6 +2263,95 @@ func TestAggregatedServerGroupsAndResources(t *testing.T) { }, }, }, + apisDiscoveryBeta: &apidiscoveryv2beta1.APIGroupDiscoveryList{ + Items: []apidiscoveryv2beta1.APIGroupDiscovery{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "apps", + }, + Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ + { + Version: "v1", + Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ + { + Resource: "deployments", + ResponseKind: &metav1.GroupVersionKind{ + Group: "apps", + Version: "v1", + Kind: "Deployment", + }, + Scope: apidiscoveryv2beta1.ScopeNamespace, + }, + { + Resource: "statefulsets", + ResponseKind: &metav1.GroupVersionKind{ + Group: "apps", + Version: "v1", + Kind: "StatefulSet", + }, + Scope: apidiscoveryv2beta1.ScopeNamespace, + }, + }, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "batch", + }, + Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ + { + Version: "v1", + Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ + { + Resource: "jobs", + ResponseKind: &metav1.GroupVersionKind{ + Group: "batch", + Version: "v1", + Kind: "Job", + }, + Scope: apidiscoveryv2beta1.ScopeNamespace, + }, + { + Resource: "cronjobs", + ResponseKind: &metav1.GroupVersionKind{ + Group: "batch", + Version: "v1", + Kind: "CronJob", + }, + Scope: apidiscoveryv2beta1.ScopeNamespace, + }, + }, + }, + { + // Stale "v1beta1" not included. + Version: "v1beta1", + Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ + { + Resource: "jobs", + ResponseKind: &metav1.GroupVersionKind{ + Group: "batch", + Version: "v1beta1", + Kind: "Job", + }, + Scope: apidiscoveryv2beta1.ScopeNamespace, + }, + { + Resource: "cronjobs", + ResponseKind: &metav1.GroupVersionKind{ + Group: "batch", + Version: "v1beta1", + Kind: "CronJob", + }, + Scope: apidiscoveryv2beta1.ScopeNamespace, + }, + }, + Freshness: apidiscoveryv2beta1.DiscoveryFreshnessStale, + }, + }, + }, + }, + }, expectedGroupNames: []string{"apps", "batch"}, expectedGroupVersions: []string{"apps/v1", "batch/v1"}, expectedGVKs: []string{ @@ -1883,61 +2364,84 @@ func TestAggregatedServerGroupsAndResources(t *testing.T) { }, } + // Ensure that client can parse both V2Beta1 and V2 types from server + serverAccepts := []string{AcceptV2Beta1, AcceptV2} for _, test := range tests { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - var agg *apidiscovery.APIGroupDiscoveryList - switch req.URL.Path { - case "/api": - agg = test.corev1 - case "/apis": - agg = test.apis - default: - w.WriteHeader(http.StatusNotFound) - return + for _, accept := range serverAccepts { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + var output []byte + var err error + if accept == AcceptV2 { + var agg *apidiscovery.APIGroupDiscoveryList + switch req.URL.Path { + case "/api": + agg = test.corev1 + case "/apis": + agg = test.apis + default: + w.WriteHeader(http.StatusNotFound) + return + } + output, err = json.Marshal(agg) + require.NoError(t, err) + } else { + var agg *apidiscoveryv2beta1.APIGroupDiscoveryList + switch req.URL.Path { + case "/api": + agg = test.corev1DiscoveryBeta + case "/apis": + agg = test.apisDiscoveryBeta + default: + w.WriteHeader(http.StatusNotFound) + return + } + output, err = json.Marshal(&agg) + require.NoError(t, err) + } + // Content-Type is "aggregated" discovery format. Add extra parameter + // to ensure we are resilient to these extra parameters. + w.Header().Set("Content-Type", accept+"; charset=utf-8") + w.WriteHeader(http.StatusOK) + _, err = w.Write(output) + require.NoError(t, err) + + })) + defer server.Close() + client := NewDiscoveryClientForConfigOrDie(&restclient.Config{Host: server.URL}) + apiGroups, resources, err := client.ServerGroupsAndResources() + if len(test.expectedFailedGVs) > 0 { + require.Error(t, err) + expectedFailedGVs := sets.NewString(test.expectedFailedGVs...) + actualFailedGVs := sets.NewString(failedGroupVersions(err)...) + assert.True(t, expectedFailedGVs.Equal(actualFailedGVs), + "%s: Expected Failed GVs (%s), got (%s)", test.name, expectedFailedGVs, actualFailedGVs) + } else { + require.NoError(t, err) } - output, err := json.Marshal(agg) - require.NoError(t, err) - // Content-type is "aggregated" discovery format. Add extra parameter - // to ensure we are resilient to these extra parameters. - w.Header().Set("Content-Type", AcceptV2Beta1+"; charset=utf-8") - w.WriteHeader(http.StatusOK) - w.Write(output) - })) - defer server.Close() - client := NewDiscoveryClientForConfigOrDie(&restclient.Config{Host: server.URL}) - apiGroups, resources, err := client.ServerGroupsAndResources() - if len(test.expectedFailedGVs) > 0 { - require.Error(t, err) - expectedFailedGVs := sets.NewString(test.expectedFailedGVs...) - actualFailedGVs := sets.NewString(failedGroupVersions(err)...) - assert.True(t, expectedFailedGVs.Equal(actualFailedGVs), - "%s: Expected Failed GVs (%s), got (%s)", test.name, expectedFailedGVs, actualFailedGVs) - } else { - require.NoError(t, err) + // Test the expected groups are returned for the aggregated format. + expectedGroupNames := sets.NewString(test.expectedGroupNames...) + actualGroupNames := sets.NewString(groupNames(apiGroups)...) + assert.True(t, expectedGroupNames.Equal(actualGroupNames), + "%s: Expected GVKs (%s), got (%s)", test.name, expectedGroupNames.List(), actualGroupNames.List()) + // If the core V1 group is returned from /api, it should be the first group. + if expectedGroupNames.Has("") { + assert.True(t, len(apiGroups) > 0) + actualFirstGroup := apiGroups[0] + assert.True(t, len(actualFirstGroup.Versions) > 0) + actualFirstGroupVersion := actualFirstGroup.Versions[0].GroupVersion + assert.Equal(t, "v1", actualFirstGroupVersion) + } + // Test the expected group/versions are returned from the aggregated discovery. + expectedGroupVersions := sets.NewString(test.expectedGroupVersions...) + actualGroupVersions := sets.NewString(groupVersions(resources)...) + assert.True(t, expectedGroupVersions.Equal(actualGroupVersions), + "%s: Expected GroupVersions(%s), got (%s)", test.name, expectedGroupVersions.List(), actualGroupVersions.List()) + // Test the expected GVKs are returned from the aggregated discovery. + expectedGVKs := sets.NewString(test.expectedGVKs...) + actualGVKs := sets.NewString(groupVersionKinds(resources)...) + assert.True(t, expectedGVKs.Equal(actualGVKs), + "%s: Expected GVKs (%s), got (%s)", test.name, expectedGVKs.List(), actualGVKs.List()) } - // Test the expected groups are returned for the aggregated format. - expectedGroupNames := sets.NewString(test.expectedGroupNames...) - actualGroupNames := sets.NewString(groupNames(apiGroups)...) - assert.True(t, expectedGroupNames.Equal(actualGroupNames), - "%s: Expected GVKs (%s), got (%s)", test.name, expectedGroupNames.List(), actualGroupNames.List()) - // If the core V1 group is returned from /api, it should be the first group. - if expectedGroupNames.Has("") { - assert.True(t, len(apiGroups) > 0) - actualFirstGroup := apiGroups[0] - assert.True(t, len(actualFirstGroup.Versions) > 0) - actualFirstGroupVersion := actualFirstGroup.Versions[0].GroupVersion - assert.Equal(t, "v1", actualFirstGroupVersion) - } - // Test the expected group/versions are returned from the aggregated discovery. - expectedGroupVersions := sets.NewString(test.expectedGroupVersions...) - actualGroupVersions := sets.NewString(groupVersions(resources)...) - assert.True(t, expectedGroupVersions.Equal(actualGroupVersions), - "%s: Expected GroupVersions(%s), got (%s)", test.name, expectedGroupVersions.List(), actualGroupVersions.List()) - // Test the expected GVKs are returned from the aggregated discovery. - expectedGVKs := sets.NewString(test.expectedGVKs...) - actualGVKs := sets.NewString(groupVersionKinds(resources)...) - assert.True(t, expectedGVKs.Equal(actualGVKs), - "%s: Expected GVKs (%s), got (%s)", test.name, expectedGVKs.List(), actualGVKs.List()) } } @@ -2023,8 +2527,10 @@ func TestAggregatedServerGroupsAndResourcesWithErrors(t *testing.T) { for _, test := range tests { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - var agg *apidiscovery.APIGroupDiscoveryList + var output []byte + var err error var status int + var agg *apidiscovery.APIGroupDiscoveryList switch req.URL.Path { case "/api": agg = test.corev1 @@ -2036,15 +2542,17 @@ func TestAggregatedServerGroupsAndResourcesWithErrors(t *testing.T) { w.WriteHeader(http.StatusNotFound) return } - output, err := json.Marshal(agg) + output, err = json.Marshal(agg) require.NoError(t, err) - // Content-type is "aggregated" discovery format. Add extra parameter + // Content-Type is "aggregated" discovery format. Add extra parameter // to ensure we are resilient to these extra parameters. - w.Header().Set("Content-Type", AcceptV2Beta1+"; charset=utf-8") + w.Header().Set("Content-Type", AcceptV2+"; charset=utf-8") w.WriteHeader(status) - w.Write(output) + _, err = w.Write(output) + require.NoError(t, err) })) defer server.Close() + client := NewDiscoveryClientForConfigOrDie(&restclient.Config{Host: server.URL}) apiGroups, resources, err := client.ServerGroupsAndResources() if test.expectedErr { @@ -2635,6 +3143,8 @@ func TestAggregatedServerPreferredResources(t *testing.T) { for _, test := range tests { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + var output []byte + var err error var agg *apidiscovery.APIGroupDiscoveryList switch req.URL.Path { case "/api": @@ -2645,13 +3155,14 @@ func TestAggregatedServerPreferredResources(t *testing.T) { w.WriteHeader(http.StatusNotFound) return } - output, err := json.Marshal(agg) + output, err = json.Marshal(agg) require.NoError(t, err) - // Content-type is "aggregated" discovery format. Add extra parameter + // Content-Type is "aggregated" discovery format. Add extra parameter // to ensure we are resilient to these extra parameters. - w.Header().Set("Content-Type", AcceptV2Beta1+"; charset=utf-8") + w.Header().Set("Content-Type", AcceptV2+"; charset=utf-8") w.WriteHeader(http.StatusOK) - w.Write(output) + _, err = w.Write(output) + require.NoError(t, err) })) defer server.Close() client := NewDiscoveryClientForConfigOrDie(&restclient.Config{Host: server.URL}) @@ -2674,7 +3185,7 @@ func TestAggregatedServerPreferredResources(t *testing.T) { } func TestDiscoveryContentTypeVersion(t *testing.T) { - v2beta1 := schema.GroupVersionKind{Group: "apidiscovery.k8s.io", Version: "v2beta1", Kind: "APIGroupDiscoveryList"} + v2 := schema.GroupVersionKind{Group: "apidiscovery.k8s.io", Version: "v2", Kind: "APIGroupDiscoveryList"} tests := []struct { contentType string gvk schema.GroupVersionKind @@ -2682,59 +3193,59 @@ func TestDiscoveryContentTypeVersion(t *testing.T) { expectErr bool }{ { - contentType: "application/json; g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList", - gvk: v2beta1, + contentType: "application/json; g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList", + gvk: v2, match: true, expectErr: false, }, { // content-type parameters are not in correct order, but comparison ignores order. - contentType: "application/json; v=v2beta1;as=APIGroupDiscoveryList;g=apidiscovery.k8s.io", - gvk: v2beta1, + contentType: "application/json; v=v2;as=APIGroupDiscoveryList;g=apidiscovery.k8s.io", + gvk: v2, match: true, expectErr: false, }, { // content-type parameters are not in correct order, but comparison ignores order. - contentType: "application/json; as=APIGroupDiscoveryList;g=apidiscovery.k8s.io;v=v2beta1", - gvk: v2beta1, + contentType: "application/json; as=APIGroupDiscoveryList;g=apidiscovery.k8s.io;v=v2", + gvk: v2, match: true, expectErr: false, }, { // Ignores extra parameter "charset=utf-8" - contentType: "application/json; g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList;charset=utf-8", - gvk: v2beta1, + contentType: "application/json; g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList;charset=utf-8", + gvk: v2, match: true, expectErr: false, }, { contentType: "application/json", - gvk: v2beta1, + gvk: v2, match: false, expectErr: false, }, { contentType: "application/json; charset=UTF-8", - gvk: v2beta1, + gvk: v2, match: false, expectErr: false, }, { contentType: "text/json", - gvk: v2beta1, + gvk: v2, match: false, expectErr: false, }, { contentType: "text/html", - gvk: v2beta1, + gvk: v2, match: false, expectErr: false, }, { contentType: "", - gvk: v2beta1, + gvk: v2, match: false, expectErr: true, }, diff --git a/staging/src/k8s.io/kube-aggregator/pkg/apiserver/handler_discovery.go b/staging/src/k8s.io/kube-aggregator/pkg/apiserver/handler_discovery.go index 953e93b754b..5e2f4f93c4b 100644 --- a/staging/src/k8s.io/kube-aggregator/pkg/apiserver/handler_discovery.go +++ b/staging/src/k8s.io/kube-aggregator/pkg/apiserver/handler_discovery.go @@ -23,10 +23,15 @@ import ( "sync" "time" + apidiscoveryv2 "k8s.io/api/apidiscovery/v2" apidiscoveryv2beta1 "k8s.io/api/apidiscovery/v2beta1" + apidiscoveryv2conversion "k8s.io/apiserver/pkg/apis/apidiscovery/v2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/apiserver/pkg/authentication/user" @@ -34,11 +39,11 @@ import ( discoveryendpoint "k8s.io/apiserver/pkg/endpoints/discovery/aggregated" "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/client-go/discovery" - "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/util/workqueue" "k8s.io/klog/v2" apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1" "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1/helper" + "k8s.io/kube-aggregator/pkg/apiserver/scheme" ) var APIRegistrationGroupVersion metav1.GroupVersion = metav1.GroupVersion{Group: "apiregistration.k8s.io", Version: "v1"} @@ -54,6 +59,12 @@ var v2Beta1GVK = schema.GroupVersionKind{ Kind: "APIGroupDiscoveryList", } +var v2GVK = schema.GroupVersionKind{ + Group: "apidiscovery.k8s.io", + Version: "v2", + Kind: "APIGroupDiscoveryList", +} + // Given a list of APIServices and proxyHandlers for contacting them, // DiscoveryManager caches a list of discovery documents for each server @@ -96,6 +107,9 @@ type discoveryManager struct { // Merged handler which stores all known groupversions mergedDiscoveryHandler discoveryendpoint.ResourceManager + + // Codecs is the serializer used for decoding aggregated apiserver responses + codecs serializer.CodecFactory } // Version of Service/Spec with relevant fields for use as a cache key @@ -129,7 +143,7 @@ func newServiceKey(service apiregistrationv1.ServiceReference) serviceKey { type cachedResult struct { // Currently cached discovery document for this service // Map from group name to version name to - discovery map[metav1.GroupVersion]apidiscoveryv2beta1.APIVersionDiscovery + discovery map[metav1.GroupVersion]apidiscoveryv2.APIVersionDiscovery // ETag hash of the cached discoveryDocument etag string @@ -172,11 +186,19 @@ var _ DiscoveryAggregationController = &discoveryManager{} func NewDiscoveryManager( target discoveryendpoint.ResourceManager, ) DiscoveryAggregationController { + discoveryScheme := runtime.NewScheme() + // Register conversion for apidiscovery + apidiscoveryv2.SchemeBuilder.Register(apidiscoveryv2conversion.RegisterConversions) + utilruntime.Must(apidiscoveryv2.AddToScheme(discoveryScheme)) + utilruntime.Must(apidiscoveryv2beta1.AddToScheme(discoveryScheme)) + codecs := serializer.NewCodecFactory(discoveryScheme) + return &discoveryManager{ mergedDiscoveryHandler: target, apiServices: make(map[string]groupVersionInfo), cachedResults: make(map[serviceKey]cachedResult), dirtyAPIServiceQueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "discovery-manager"), + codecs: codecs, } } @@ -214,7 +236,7 @@ func (dm *discoveryManager) fetchFreshDiscoveryForService(gv metav1.GroupVersion Path: req.URL.Path, IsResourceRequest: false, })) - req.Header.Add("Accept", discovery.AcceptV2Beta1) + req.Header.Add("Accept", discovery.AcceptV2+","+discovery.AcceptV2Beta1) if exists && len(cached.etag) > 0 { req.Header.Add("If-None-Match", cached.etag) @@ -228,6 +250,8 @@ func (dm *discoveryManager) fetchFreshDiscoveryForService(gv metav1.GroupVersion handler.ServeHTTP(writer, req) isV2Beta1GVK, _ := discovery.ContentTypeIsGVK(writer.Header().Get("Content-Type"), v2Beta1GVK) + isV2GVK, _ := discovery.ContentTypeIsGVK(writer.Header().Get("Content-Type"), v2GVK) + switch { case writer.respCode == http.StatusNotModified: // Keep old entry, update timestamp @@ -242,16 +266,16 @@ func (dm *discoveryManager) fetchFreshDiscoveryForService(gv metav1.GroupVersion case writer.respCode == http.StatusServiceUnavailable: return nil, fmt.Errorf("service %s returned non-success response code: %v", info.service.String(), writer.respCode) - case writer.respCode == http.StatusOK && isV2Beta1GVK: - parsed := &apidiscoveryv2beta1.APIGroupDiscoveryList{} - if err := runtime.DecodeInto(scheme.Codecs.UniversalDecoder(), writer.data, parsed); err != nil { + case writer.respCode == http.StatusOK && (isV2GVK || isV2Beta1GVK): + parsed := &apidiscoveryv2.APIGroupDiscoveryList{} + if err := runtime.DecodeInto(dm.codecs.UniversalDecoder(), writer.data, parsed); err != nil { return nil, err } klog.V(3).Infof("DiscoveryManager: Successfully downloaded discovery for %s", info.service.String()) // Convert discovery info into a map for convenient lookup later - discoMap := map[metav1.GroupVersion]apidiscoveryv2beta1.APIVersionDiscovery{} + discoMap := map[metav1.GroupVersion]apidiscoveryv2.APIVersionDiscovery{} for _, g := range parsed.Items { for _, v := range g.Versions { discoMap[metav1.GroupVersion{Group: g.Name, Version: v.Version}] = v @@ -330,7 +354,7 @@ func (dm *discoveryManager) fetchFreshDiscoveryForService(gv metav1.GroupVersion } klog.V(3).Infof("DiscoveryManager: Successfully downloaded legacy discovery for %s", info.service.String()) - discoMap := map[metav1.GroupVersion]apidiscoveryv2beta1.APIVersionDiscovery{ + discoMap := map[metav1.GroupVersion]apidiscoveryv2.APIVersionDiscovery{ // Convert old-style APIGroupList to new information gv: { Version: gv.Version, @@ -366,7 +390,7 @@ func (dm *discoveryManager) syncAPIService(apiServiceName string) error { // Lookup last cached result for this apiservice's service. cached, err := dm.fetchFreshDiscoveryForService(mgv, info) - var entry apidiscoveryv2beta1.APIVersionDiscovery + var entry apidiscoveryv2.APIVersionDiscovery // Extract the APIService's specific resource information from the // groupversion @@ -377,7 +401,7 @@ func (dm *discoveryManager) syncAPIService(apiServiceName string) error { // Just use empty GV to mark that GV exists, but no resources. // Also mark that it is stale to indicate the fetch failed // TODO: Maybe also stick in a status for the version the error? - entry = apidiscoveryv2beta1.APIVersionDiscovery{ + entry = apidiscoveryv2.APIVersionDiscovery{ Version: gv.Version, } } else { @@ -388,7 +412,7 @@ func (dm *discoveryManager) syncAPIService(apiServiceName string) error { } else { // Successfully fetched discovery information from the server, but // the server did not include this groupversion? - entry = apidiscoveryv2beta1.APIVersionDiscovery{ + entry = apidiscoveryv2.APIVersionDiscovery{ Version: gv.Version, } } @@ -397,9 +421,9 @@ func (dm *discoveryManager) syncAPIService(apiServiceName string) error { // The entry's staleness depends upon if `fetchFreshDiscoveryForService` // returned an error or not. if err == nil { - entry.Freshness = apidiscoveryv2beta1.DiscoveryFreshnessCurrent + entry.Freshness = apidiscoveryv2.DiscoveryFreshnessCurrent } else { - entry.Freshness = apidiscoveryv2beta1.DiscoveryFreshnessStale + entry.Freshness = apidiscoveryv2.DiscoveryFreshnessStale } dm.mergedDiscoveryHandler.AddGroupVersion(gv.Group, entry) diff --git a/staging/src/k8s.io/kube-aggregator/pkg/apiserver/handler_discovery_test.go b/staging/src/k8s.io/kube-aggregator/pkg/apiserver/handler_discovery_test.go index 5f39805a551..863ac49c566 100644 --- a/staging/src/k8s.io/kube-aggregator/pkg/apiserver/handler_discovery_test.go +++ b/staging/src/k8s.io/kube-aggregator/pkg/apiserver/handler_discovery_test.go @@ -33,10 +33,12 @@ import ( fuzz "github.com/google/gofuzz" "github.com/stretchr/testify/require" + apidiscoveryv2 "k8s.io/api/apidiscovery/v2" apidiscoveryv2beta1 "k8s.io/api/apidiscovery/v2beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + apidiscoveryv2scheme "k8s.io/apiserver/pkg/apis/apidiscovery/v2" "k8s.io/apiserver/pkg/endpoints" "k8s.io/apiserver/pkg/endpoints/discovery" discoveryendpoint "k8s.io/apiserver/pkg/endpoints/discovery/aggregated" @@ -68,31 +70,31 @@ func TestBasic(t *testing.T) { service3 := discoveryendpoint.NewResourceManager("apis") apiGroup1 := fuzzAPIGroups(2, 5, 25) apiGroup2 := fuzzAPIGroups(2, 5, 50) - apiGroup3 := apidiscoveryv2beta1.APIGroupDiscoveryList{Items: []apidiscoveryv2beta1.APIGroupDiscovery{ + apiGroup3 := apidiscoveryv2.APIGroupDiscoveryList{Items: []apidiscoveryv2.APIGroupDiscovery{ { ObjectMeta: metav1.ObjectMeta{Name: "weird.example.com"}, - Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ + Versions: []apidiscoveryv2.APIVersionDiscovery{ { Version: "v1", Freshness: "Current", - Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ + Resources: []apidiscoveryv2.APIResourceDiscovery{ { Resource: "parent-missing-kind", - Subresources: []apidiscoveryv2beta1.APISubresourceDiscovery{ + Subresources: []apidiscoveryv2.APISubresourceDiscovery{ {Subresource: "subresource-missing-kind"}, }, }, { Resource: "parent-empty-kind", ResponseKind: &metav1.GroupVersionKind{}, - Subresources: []apidiscoveryv2beta1.APISubresourceDiscovery{ + Subresources: []apidiscoveryv2.APISubresourceDiscovery{ {Subresource: "subresource-empty-kind", ResponseKind: &metav1.GroupVersionKind{}}, }, }, { Resource: "parent-with-kind", ResponseKind: &metav1.GroupVersionKind{Kind: "ParentWithKind"}, - Subresources: []apidiscoveryv2beta1.APISubresourceDiscovery{ + Subresources: []apidiscoveryv2.APISubresourceDiscovery{ {Subresource: "subresource-with-kind", ResponseKind: &metav1.GroupVersionKind{Kind: "SubresourceWithKind"}}, }, }, @@ -101,32 +103,32 @@ func TestBasic(t *testing.T) { }, }, }} - apiGroup3WithFixup := apidiscoveryv2beta1.APIGroupDiscoveryList{Items: []apidiscoveryv2beta1.APIGroupDiscovery{ + apiGroup3WithFixup := apidiscoveryv2.APIGroupDiscoveryList{Items: []apidiscoveryv2.APIGroupDiscovery{ { ObjectMeta: metav1.ObjectMeta{Name: "weird.example.com"}, - Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ + Versions: []apidiscoveryv2.APIVersionDiscovery{ { Version: "v1", Freshness: "Current", - Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ + Resources: []apidiscoveryv2.APIResourceDiscovery{ { Resource: "parent-missing-kind", ResponseKind: &metav1.GroupVersionKind{}, // defaulted by aggregator - Subresources: []apidiscoveryv2beta1.APISubresourceDiscovery{ + Subresources: []apidiscoveryv2.APISubresourceDiscovery{ {Subresource: "subresource-missing-kind", ResponseKind: &metav1.GroupVersionKind{}}, // defaulted by aggregator }, }, { Resource: "parent-empty-kind", ResponseKind: &metav1.GroupVersionKind{}, - Subresources: []apidiscoveryv2beta1.APISubresourceDiscovery{ + Subresources: []apidiscoveryv2.APISubresourceDiscovery{ {Subresource: "subresource-empty-kind", ResponseKind: &metav1.GroupVersionKind{}}, }, }, { Resource: "parent-with-kind", ResponseKind: &metav1.GroupVersionKind{Kind: "ParentWithKind"}, - Subresources: []apidiscoveryv2beta1.APISubresourceDiscovery{ + Subresources: []apidiscoveryv2.APISubresourceDiscovery{ {Subresource: "subresource-with-kind", ResponseKind: &metav1.GroupVersionKind{Kind: "SubresourceWithKind"}}, }, }, @@ -217,7 +219,7 @@ func TestBasic(t *testing.T) { checkAPIGroups(t, apiGroup3WithFixup, parsed) } -func checkAPIGroups(t *testing.T, api apidiscoveryv2beta1.APIGroupDiscoveryList, response *apidiscoveryv2beta1.APIGroupDiscoveryList) { +func checkAPIGroups(t *testing.T, api apidiscoveryv2.APIGroupDiscoveryList, response *apidiscoveryv2.APIGroupDiscoveryList) { t.Helper() if len(response.Items) < len(api.Items) { t.Errorf("expected to check for at least %d groups, only have %d groups in response", len(api.Items), len(response.Items)) @@ -281,10 +283,10 @@ func TestInitialRunHasAllAPIServices(t *testing.T) { t.Fatalf("unexpected status code %d", response.StatusCode) } - apiGroup := apidiscoveryv2beta1.APIGroupDiscoveryList{Items: []apidiscoveryv2beta1.APIGroupDiscovery{ + apiGroup := apidiscoveryv2.APIGroupDiscoveryList{Items: []apidiscoveryv2.APIGroupDiscovery{ { ObjectMeta: metav1.ObjectMeta{Name: "stable.example.com"}, - Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ + Versions: []apidiscoveryv2.APIVersionDiscovery{ { Version: "v1", Freshness: "Stale", @@ -348,6 +350,71 @@ func TestServiceGC(t *testing.T) { require.Equal(t, 1, getCacheLen()) } +// TestV2Beta1Skew tests that aggregated apiservers that only serve V2Beta1 +// are still supported +func TestV2Beta1Skew(t *testing.T) { + apiGroup := apidiscoveryv2.APIGroupDiscoveryList{Items: []apidiscoveryv2.APIGroupDiscovery{ + { + ObjectMeta: metav1.ObjectMeta{Name: "stable.example.com"}, + Versions: []apidiscoveryv2.APIVersionDiscovery{ + { + Version: "v1", + Freshness: "Current", + Resources: []apidiscoveryv2.APIResourceDiscovery{ + { + Resource: "parent-with-kind", + ResponseKind: &metav1.GroupVersionKind{Kind: "ParentWithKind"}, + Subresources: []apidiscoveryv2.APISubresourceDiscovery{ + {Subresource: "subresource-with-kind", ResponseKind: &metav1.GroupVersionKind{Kind: "SubresourceWithKind"}}, + }, + }, + }, + }, + }, + }, + }} + + aggregatedResourceManager := discoveryendpoint.NewResourceManager("apis") + + aggregatedManager := newDiscoveryManager(aggregatedResourceManager) + + aggregatedManager.AddAPIService(&apiregistrationv1.APIService{ + ObjectMeta: metav1.ObjectMeta{ + Name: "v1.stable.example.com", + }, + Spec: apiregistrationv1.APIServiceSpec{ + Group: "stable.example.com", + Version: "v1", + Service: &apiregistrationv1.ServiceReference{ + Name: "test-service", + }, + }, + }, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Force a v2beta1 response from the aggregated apiserver + v2b := apidiscoveryv2beta1.APIGroupDiscoveryList{} + err := apidiscoveryv2scheme.Convertv2APIGroupDiscoveryListTov2beta1APIGroupDiscoveryList(&apiGroup, &v2b, nil) + require.NoError(t, err) + converted, err := json.Marshal(v2b) + require.NoError(t, err) + w.Header().Set("Content-Type", "application/json;"+"g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList") + w.WriteHeader(200) + _, err = w.Write(converted) + require.NoError(t, err) + })) + testCtx, cancel := context.WithCancel(context.Background()) + defer cancel() + + go aggregatedManager.Run(testCtx.Done(), nil) + require.True(t, waitForQueueComplete(testCtx.Done(), aggregatedManager)) + + response, _, parsed := fetchPath(aggregatedResourceManager, "") + if response.StatusCode != 200 { + t.Fatalf("unexpected status code %d", response.StatusCode) + } + + checkAPIGroups(t, apiGroup, parsed) +} + // Test that a handler associated with an APIService gets pinged after the // APIService has been marked as dirty func TestDirty(t *testing.T) { @@ -642,34 +709,34 @@ func TestLegacyFallbackNoCache(t *testing.T) { // includes the legacy resources _, _, doc := fetchPath(aggregatedResourceManager, "") - mustConvert := func(r []metav1.APIResource) []apidiscoveryv2beta1.APIResourceDiscovery { + mustConvert := func(r []metav1.APIResource) []apidiscoveryv2.APIResourceDiscovery { converted, err := endpoints.ConvertGroupVersionIntoToDiscovery(r) require.NoError(t, err) return converted } - expectAggregatedDiscovery := []apidiscoveryv2beta1.APIGroupDiscovery{{ + expectAggregatedDiscovery := []apidiscoveryv2.APIGroupDiscovery{{ ObjectMeta: metav1.ObjectMeta{ Name: "stable.example.com", }, - Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ + Versions: []apidiscoveryv2.APIVersionDiscovery{ { Version: "v1", Resources: mustConvert([]metav1.APIResource{resources["v1"]}), - Freshness: apidiscoveryv2beta1.DiscoveryFreshnessCurrent, + Freshness: apidiscoveryv2.DiscoveryFreshnessCurrent, }, { Version: "v1beta1", Resources: mustConvert([]metav1.APIResource{resources["v1beta1"]}), - Freshness: apidiscoveryv2beta1.DiscoveryFreshnessCurrent, + Freshness: apidiscoveryv2.DiscoveryFreshnessCurrent, }, { Version: "v2alpha1", - Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ + Resources: []apidiscoveryv2.APIResourceDiscovery{ { Resource: "parent-without-kind", ResponseKind: &metav1.GroupVersionKind{}, // defaulted Scope: "Cluster", - Subresources: []apidiscoveryv2beta1.APISubresourceDiscovery{ + Subresources: []apidiscoveryv2.APISubresourceDiscovery{ { Subresource: "subresource", ResponseKind: &metav1.GroupVersionKind{Kind: "Subresource"}, @@ -684,7 +751,7 @@ func TestLegacyFallbackNoCache(t *testing.T) { Resource: "missing-parent", ResponseKind: &metav1.GroupVersionKind{}, // defaulted Scope: "Cluster", - Subresources: []apidiscoveryv2beta1.APISubresourceDiscovery{ + Subresources: []apidiscoveryv2.APISubresourceDiscovery{ { Subresource: "subresource-without-parent", ResponseKind: &metav1.GroupVersionKind{Kind: "SubresourceWithoutParent"}, @@ -692,12 +759,12 @@ func TestLegacyFallbackNoCache(t *testing.T) { }, }, }, - Freshness: apidiscoveryv2beta1.DiscoveryFreshnessCurrent, + Freshness: apidiscoveryv2.DiscoveryFreshnessCurrent, }, { Version: "v1alpha1", Resources: mustConvert([]metav1.APIResource{resources["v1alpha1"]}), - Freshness: apidiscoveryv2beta1.DiscoveryFreshnessCurrent, + Freshness: apidiscoveryv2.DiscoveryFreshnessCurrent, }, }, }} @@ -782,16 +849,16 @@ func testLegacyFallbackWithCustomRootHandler(t *testing.T, rootHandlerFn func(ht converted, err := endpoints.ConvertGroupVersionIntoToDiscovery([]metav1.APIResource{resource}) require.NoError(t, err) - require.Equal(t, []apidiscoveryv2beta1.APIGroupDiscovery{ + require.Equal(t, []apidiscoveryv2.APIGroupDiscovery{ { ObjectMeta: metav1.ObjectMeta{ Name: resource.Group, }, - Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ + Versions: []apidiscoveryv2.APIVersionDiscovery{ { Version: resource.Version, Resources: converted, - Freshness: apidiscoveryv2beta1.DiscoveryFreshnessCurrent, + Freshness: apidiscoveryv2.DiscoveryFreshnessCurrent, }, }, }, @@ -859,15 +926,15 @@ func TestAPIServiceStale(t *testing.T) { // At this point external services have synced. Check if discovery document // lists the APIService group version as Stale. _, _, doc := fetchPath(aggregatedResourceManager, "") - require.Equal(t, []apidiscoveryv2beta1.APIGroupDiscovery{ + require.Equal(t, []apidiscoveryv2.APIGroupDiscovery{ { ObjectMeta: metav1.ObjectMeta{ Name: "stable.example.com", }, - Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ + Versions: []apidiscoveryv2.APIVersionDiscovery{ { Version: "v1", - Freshness: apidiscoveryv2beta1.DiscoveryFreshnessStale, + Freshness: apidiscoveryv2.DiscoveryFreshnessStale, }, }, }, @@ -934,16 +1001,16 @@ func TestNotModified(t *testing.T) { } // copied from staging/src/k8s.io/apiserver/pkg/endpoints/discovery/v2/handler_test.go -func fuzzAPIGroups(atLeastNumGroups, maxNumGroups int, seed int64) apidiscoveryv2beta1.APIGroupDiscoveryList { +func fuzzAPIGroups(atLeastNumGroups, maxNumGroups int, seed int64) apidiscoveryv2.APIGroupDiscoveryList { fuzzer := fuzz.NewWithSeed(seed) fuzzer.NumElements(atLeastNumGroups, maxNumGroups) fuzzer.NilChance(0) - fuzzer.Funcs(func(o *apidiscoveryv2beta1.APIGroupDiscovery, c fuzz.Continue) { + fuzzer.Funcs(func(o *apidiscoveryv2.APIGroupDiscovery, c fuzz.Continue) { c.FuzzNoCustom(o) // The ResourceManager will just not serve the group if its versions // list is empty - atLeastOne := apidiscoveryv2beta1.APIVersionDiscovery{} + atLeastOne := apidiscoveryv2.APIVersionDiscovery{} c.Fuzz(&atLeastOne) o.Versions = append(o.Versions, atLeastOne) @@ -958,10 +1025,10 @@ func fuzzAPIGroups(atLeastNumGroups, maxNumGroups int, seed int64) apidiscoveryv } }) - var apis []apidiscoveryv2beta1.APIGroupDiscovery + var apis []apidiscoveryv2.APIGroupDiscovery fuzzer.Fuzz(&apis) - return apidiscoveryv2beta1.APIGroupDiscoveryList{ + return apidiscoveryv2.APIGroupDiscoveryList{ TypeMeta: metav1.TypeMeta{ Kind: "APIGroupDiscoveryList", APIVersion: "v1", @@ -972,13 +1039,13 @@ func fuzzAPIGroups(atLeastNumGroups, maxNumGroups int, seed int64) apidiscoveryv } // copied from staging/src/k8s.io/apiserver/pkg/endpoints/discovery/v2/handler_test.go -func fetchPath(handler http.Handler, etag string) (*http.Response, []byte, *apidiscoveryv2beta1.APIGroupDiscoveryList) { +func fetchPath(handler http.Handler, etag string) (*http.Response, []byte, *apidiscoveryv2.APIGroupDiscoveryList) { // Expect json-formatted apis group list w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/apis", nil) // Ask for JSON response - req.Header.Set("Accept", runtime.ContentTypeJSON+";g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList") + req.Header.Set("Accept", runtime.ContentTypeJSON+";g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList,"+runtime.ContentTypeJSON+";g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList") if etag != "" { // Quote provided etag if unquoted @@ -992,9 +1059,9 @@ func fetchPath(handler http.Handler, etag string) (*http.Response, []byte, *apid handler.ServeHTTP(w, req) bytes := w.Body.Bytes() - var decoded *apidiscoveryv2beta1.APIGroupDiscoveryList + var decoded *apidiscoveryv2.APIGroupDiscoveryList if len(bytes) > 0 { - decoded = &apidiscoveryv2beta1.APIGroupDiscoveryList{} + decoded = &apidiscoveryv2.APIGroupDiscoveryList{} runtime.DecodeInto(scheme.Codecs.UniversalDecoder(), bytes, decoded) } diff --git a/test/e2e/apimachinery/aggregated_discovery.go b/test/e2e/apimachinery/aggregated_discovery.go index 01dfde73b6d..180b1b8b8ae 100644 --- a/test/e2e/apimachinery/aggregated_discovery.go +++ b/test/e2e/apimachinery/aggregated_discovery.go @@ -22,7 +22,7 @@ import ( "fmt" "time" - apidiscoveryv2beta1 "k8s.io/api/apidiscovery/v2beta1" + apidiscoveryv2 "k8s.io/api/apidiscovery/v2" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apiextensionclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" "k8s.io/apiextensions-apiserver/test/integration/fixtures" @@ -143,7 +143,7 @@ var _ = SIGDescribe("AggregatedDiscovery", func() { }, } - const aggregatedAccept = "application/json;g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList" + const aggregatedAccept = "application/json;g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList" /* Release : v1.30 @@ -156,7 +156,7 @@ var _ = SIGDescribe("AggregatedDiscovery", func() { framework.Failf("Failed to get raw aggregated discovery document") } - groupList := apidiscoveryv2beta1.APIGroupDiscoveryList{} + groupList := apidiscoveryv2.APIGroupDiscoveryList{} err = json.Unmarshal(d, &groupList) if err != nil { framework.Failf("Failed to parse discovery: %v", err) @@ -174,7 +174,7 @@ var _ = SIGDescribe("AggregatedDiscovery", func() { framework.Failf("Failed to get raw aggregated discovery document") } - groupListLegacy := apidiscoveryv2beta1.APIGroupDiscoveryList{} + groupListLegacy := apidiscoveryv2.APIGroupDiscoveryList{} err = json.Unmarshal(d2, &groupListLegacy) if err != nil { framework.Failf("Failed to parse discovery: %v", err) @@ -238,7 +238,7 @@ var _ = SIGDescribe("AggregatedDiscovery", func() { framework.Failf("Failed to get raw aggregated discovery document") } - groupList := apidiscoveryv2beta1.APIGroupDiscoveryList{} + groupList := apidiscoveryv2.APIGroupDiscoveryList{} err = json.Unmarshal(d, &groupList) if err != nil { framework.Failf("Failed to parse discovery: %v", err) @@ -392,7 +392,7 @@ func isGVPresent(gvs *metav1.APIGroupList, gv schema.GroupVersion) bool { return false } -func isGVRPresentAPIDiscovery(apidiscovery apidiscoveryv2beta1.APIGroupDiscoveryList, gvr schema.GroupVersionResource) bool { +func isGVRPresentAPIDiscovery(apidiscovery apidiscoveryv2.APIGroupDiscoveryList, gvr schema.GroupVersionResource) bool { for _, group := range apidiscovery.Items { if gvr.Group == group.Name { for _, version := range group.Versions { diff --git a/test/integration/apiserver/discovery/discovery_test.go b/test/integration/apiserver/discovery/discovery_test.go index 03f79cf5343..c63d1299418 100644 --- a/test/integration/apiserver/discovery/discovery_test.go +++ b/test/integration/apiserver/discovery/discovery_test.go @@ -28,7 +28,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/require" - apidiscoveryv2beta1 "k8s.io/api/apidiscovery/v2beta1" + apidiscoveryv2 "k8s.io/api/apidiscovery/v2" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apiextensions "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" "k8s.io/apimachinery/pkg/api/meta" @@ -79,14 +79,14 @@ var ( codecs = runtimeserializer.NewCodecFactory(scheme) serialize runtime.NegotiatedSerializer - basicTestGroup = apidiscoveryv2beta1.APIGroupDiscovery{ + basicTestGroup = apidiscoveryv2.APIGroupDiscovery{ ObjectMeta: metav1.ObjectMeta{ Name: "stable.example.com", }, - Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ + Versions: []apidiscoveryv2.APIVersionDiscovery{ { Version: "v1", - Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ + Resources: []apidiscoveryv2.APIResourceDiscovery{ { Resource: "jobs", Verbs: []string{"create", "list", "watch", "delete"}, @@ -94,19 +94,19 @@ var ( Categories: []string{"all"}, }, }, - Freshness: apidiscoveryv2beta1.DiscoveryFreshnessCurrent, + Freshness: apidiscoveryv2.DiscoveryFreshnessCurrent, }, }, } - basicTestGroupWithFixup = apidiscoveryv2beta1.APIGroupDiscovery{ + basicTestGroupWithFixup = apidiscoveryv2.APIGroupDiscovery{ ObjectMeta: metav1.ObjectMeta{ Name: "stable.example.com", }, - Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ + Versions: []apidiscoveryv2.APIVersionDiscovery{ { Version: "v1", - Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ + Resources: []apidiscoveryv2.APIResourceDiscovery{ { Resource: "jobs", Verbs: []string{"create", "list", "watch", "delete"}, @@ -116,19 +116,19 @@ var ( ResponseKind: &metav1.GroupVersionKind{}, }, }, - Freshness: apidiscoveryv2beta1.DiscoveryFreshnessCurrent, + Freshness: apidiscoveryv2.DiscoveryFreshnessCurrent, }, }, } - basicTestGroupStale = apidiscoveryv2beta1.APIGroupDiscovery{ + basicTestGroupStale = apidiscoveryv2.APIGroupDiscovery{ ObjectMeta: metav1.ObjectMeta{ Name: "stable.example.com", }, - Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ + Versions: []apidiscoveryv2.APIVersionDiscovery{ { Version: "v1", - Freshness: apidiscoveryv2beta1.DiscoveryFreshnessStale, + Freshness: apidiscoveryv2.DiscoveryFreshnessStale, }, }, } @@ -193,7 +193,7 @@ func TestReadinessAggregatedAPIServiceDiscovery(t *testing.T) { // Create a resource manager whichs serves our GroupVersion resourceManager := discoveryendpoint.NewResourceManager("apis") - resourceManager.SetGroups([]apidiscoveryv2beta1.APIGroupDiscovery{basicTestGroup}) + resourceManager.SetGroups([]apidiscoveryv2.APIGroupDiscovery{basicTestGroup}) apiServiceWaitCh := make(chan struct{}) @@ -290,7 +290,7 @@ func TestAggregatedAPIServiceDiscovery(t *testing.T) { // Create a resource manager whichs serves our GroupVersion resourceManager := discoveryendpoint.NewResourceManager("apis") - resourceManager.SetGroups([]apidiscoveryv2beta1.APIGroupDiscovery{basicTestGroup}) + resourceManager.SetGroups([]apidiscoveryv2.APIGroupDiscovery{basicTestGroup}) // Install our ResourceManager as an Aggregated APIService to the // test server @@ -371,7 +371,7 @@ func runTestCases(t *testing.T, cases []testCase) { }) require.NoError(t, err, "v1 discovery must reset between tests: "+diff) - err = WaitForResultWithCondition(ctx, client, func(result apidiscoveryv2beta1.APIGroupDiscoveryList) bool { + err = WaitForResultWithCondition(ctx, client, func(result apidiscoveryv2.APIGroupDiscoveryList) bool { diff = cmp.Diff(originalV2, result) return reflect.DeepEqual(result, originalV2) }) @@ -393,6 +393,7 @@ func TestCRD(t *testing.T) { applyCRD(makeCRDSpec(stableGroup, "Foo", false, []string{"v1", "v1alpha1", "v1beta1", "v2"})), waitForGroupVersionsV1([]metav1.GroupVersion{stableV1, stableV1alpha1, stableV1beta1, stableV2}), waitForGroupVersionsV2([]metav1.GroupVersion{stableV1, stableV1alpha1, stableV1beta1, stableV2}), + waitForGroupVersionsV2Beta1([]metav1.GroupVersion{stableV1, stableV1alpha1, stableV1beta1, stableV2}), }, }, { @@ -402,6 +403,7 @@ func TestCRD(t *testing.T) { applyCRD(makeCRDSpec(stableGroup, "Foo", false, []string{"v1", "v1alpha1", "v1beta1", "v2"})), waitForGroupVersionsV1([]metav1.GroupVersion{stableV1, stableV1alpha1, stableV1beta1, stableV2}), waitForGroupVersionsV2([]metav1.GroupVersion{stableV1, stableV1alpha1, stableV1beta1, stableV2}), + waitForGroupVersionsV2Beta1([]metav1.GroupVersion{stableV1, stableV1alpha1, stableV1beta1, stableV2}), deleteObject{ GroupVersionResource: metav1.GroupVersionResource(apiextensionsv1.SchemeGroupVersion.WithResource("customresourcedefinitions")), Name: "foos.stable.example.com", @@ -469,6 +471,7 @@ func TestCRD(t *testing.T) { // Wait for GV to appear in both discovery documents waitForGroupVersionsV1([]metav1.GroupVersion{stableV2, stableV1alpha1}), waitForGroupVersionsV2([]metav1.GroupVersion{stableV2, stableV1alpha1}), + waitForGroupVersionsV2Beta1([]metav1.GroupVersion{stableV2, stableV1alpha1}), applyAPIService( apiregistrationv1.APIServiceSpec{ @@ -487,11 +490,13 @@ func TestCRD(t *testing.T) { // We should now have stable v1 available waitForGroupVersionsV1([]metav1.GroupVersion{stableV1}), waitForGroupVersionsV2([]metav1.GroupVersion{stableV1}), + waitForGroupVersionsV2Beta1([]metav1.GroupVersion{stableV1}), // The CRD group-versions not served by the aggregated // apiservice should still be availablee waitForGroupVersionsV1([]metav1.GroupVersion{stableV2, stableV1alpha1}), waitForGroupVersionsV2([]metav1.GroupVersion{stableV2, stableV1alpha1}), + waitForGroupVersionsV2Beta1([]metav1.GroupVersion{stableV2, stableV1alpha1}), // Remove API service. Show we have switched to CRD deleteObject{ @@ -502,9 +507,11 @@ func TestCRD(t *testing.T) { // Show that we still have stable v1 since it is in the CRD waitForGroupVersionsV1([]metav1.GroupVersion{stableV2, stableV1alpha1}), waitForGroupVersionsV2([]metav1.GroupVersion{stableV2, stableV1alpha1}), + waitForGroupVersionsV2Beta1([]metav1.GroupVersion{stableV2, stableV1alpha1}), waitForAbsentGroupVersionsV1([]metav1.GroupVersion{stableV1}), waitForAbsentGroupVersionsV2([]metav1.GroupVersion{stableV1}), + waitForAbsentGroupVersionsV2Beta1([]metav1.GroupVersion{stableV1}), }, }, { @@ -635,9 +642,9 @@ func TestFreshness(t *testing.T) { } switch entry.Freshness { - case apidiscoveryv2beta1.DiscoveryFreshnessCurrent: + case apidiscoveryv2.DiscoveryFreshnessCurrent: // Skip - case apidiscoveryv2beta1.DiscoveryFreshnessStale: + case apidiscoveryv2.DiscoveryFreshnessStale: staleGVs = append(staleGVs, targetGv) default: return fmt.Errorf("unrecognized freshness '%v' on gv '%v'", entry.Freshness, targetGv) @@ -704,7 +711,7 @@ func TestFreshness(t *testing.T) { // Shows a group for which multiple APIServices specify a GroupPriorityMinimum, // it is sorted the same in both versions of discovery -func TestGroupPriorty(t *testing.T) { +func TestGroupPriority(t *testing.T) { defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AggregatedDiscoveryEndpoint, true)() makeApiServiceSpec := func(gv metav1.GroupVersion, groupPriorityMin, versionPriority int) apiregistrationv1.APIServiceSpec { diff --git a/test/integration/apiserver/discovery/framework.go b/test/integration/apiserver/discovery/framework.go index f47f3771be4..0ed6dd49889 100644 --- a/test/integration/apiserver/discovery/framework.go +++ b/test/integration/apiserver/discovery/framework.go @@ -36,6 +36,7 @@ import ( "k8s.io/client-go/kubernetes" aggregator "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset" + apidiscoveryv2 "k8s.io/api/apidiscovery/v2" apidiscoveryv2beta1 "k8s.io/api/apidiscovery/v2beta1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -43,7 +44,8 @@ import ( ) const acceptV1JSON = "application/json" -const acceptV2JSON = "application/json;g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList" +const acceptV2Beta1JSON = "application/json;g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList" +const acceptV2JSON = "application/json;g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList" const maxTimeout = 10 * time.Second @@ -93,9 +95,15 @@ type waitForAbsentGroupVersionsV1 []metav1.GroupVersion // Wait for groupversions to appear in v2 discovery type waitForGroupVersionsV2 []metav1.GroupVersion +// Wait for groupversions to appear in v2beta1 discovery +type waitForGroupVersionsV2Beta1 []metav1.GroupVersion + // Wait for groupversions to disappear from v2 discovery type waitForAbsentGroupVersionsV2 []metav1.GroupVersion +// Wait for groupversions to disappear from v2beta1 discovery +type waitForAbsentGroupVersionsV2Beta1 []metav1.GroupVersion + type waitForStaleGroupVersionsV2 []metav1.GroupVersion type waitForFreshGroupVersionsV2 []metav1.GroupVersion @@ -144,10 +152,11 @@ func (a applyAPIService) Cleanup(ctx context.Context, client testClient) error { return err } - err = wait.PollWithContext( + err = wait.PollUntilContextTimeout( ctx, 250*time.Millisecond, maxTimeout, + true, func(ctx context.Context) (done bool, err error) { _, err = client.ApiregistrationV1().APIServices().Get(ctx, name, metav1.GetOptions{}) if err == nil { @@ -212,10 +221,11 @@ func (a applyCRD) Cleanup(ctx context.Context, client testClient) error { return err } - err = wait.PollWithContext( + err = wait.PollUntilContextTimeout( ctx, 250*time.Millisecond, maxTimeout, + true, func(ctx context.Context) (done bool, err error) { _, err = client.ApiextensionsV1().CustomResourceDefinitions().Get(ctx, name, metav1.GetOptions{}) if err == nil { @@ -248,9 +258,9 @@ func (d deleteObject) Do(ctx context.Context, client testClient) error { } func (w waitForStaleGroupVersionsV2) Do(ctx context.Context, client testClient) error { - err := WaitForResultWithCondition(ctx, client, func(result apidiscoveryv2beta1.APIGroupDiscoveryList) bool { + err := WaitForResultWithCondition(ctx, client, func(result apidiscoveryv2.APIGroupDiscoveryList) bool { for _, gv := range w { - if info := FindGroupVersionV2(result, gv); info == nil || info.Freshness != apidiscoveryv2beta1.DiscoveryFreshnessStale { + if info := FindGroupVersionV2(result, gv); info == nil || info.Freshness != apidiscoveryv2.DiscoveryFreshnessStale { return false } } @@ -265,9 +275,9 @@ func (w waitForStaleGroupVersionsV2) Do(ctx context.Context, client testClient) } func (w waitForFreshGroupVersionsV2) Do(ctx context.Context, client testClient) error { - err := WaitForResultWithCondition(ctx, client, func(result apidiscoveryv2beta1.APIGroupDiscoveryList) bool { + err := WaitForResultWithCondition(ctx, client, func(result apidiscoveryv2.APIGroupDiscoveryList) bool { for _, gv := range w { - if info := FindGroupVersionV2(result, gv); info == nil || info.Freshness != apidiscoveryv2beta1.DiscoveryFreshnessCurrent { + if info := FindGroupVersionV2(result, gv); info == nil || info.Freshness != apidiscoveryv2.DiscoveryFreshnessCurrent { return false } } @@ -282,7 +292,7 @@ func (w waitForFreshGroupVersionsV2) Do(ctx context.Context, client testClient) } func (w waitForGroupVersionsV2) Do(ctx context.Context, client testClient) error { - err := WaitForResultWithCondition(ctx, client, func(result apidiscoveryv2beta1.APIGroupDiscoveryList) bool { + err := WaitForResultWithCondition(ctx, client, func(result apidiscoveryv2.APIGroupDiscoveryList) bool { for _, gv := range w { if FindGroupVersionV2(result, gv) == nil { return false @@ -298,8 +308,25 @@ func (w waitForGroupVersionsV2) Do(ctx context.Context, client testClient) error return nil } +func (w waitForGroupVersionsV2Beta1) Do(ctx context.Context, client testClient) error { + err := WaitForV2Beta1ResultWithCondition(ctx, client, func(result apidiscoveryv2beta1.APIGroupDiscoveryList) bool { + for _, gv := range w { + if FindGroupVersionV2Beta1(result, gv) == nil { + return false + } + } + + return true + }) + + if err != nil { + return fmt.Errorf("waiting for groupversions v2 (%v): %w", w, err) + } + return nil +} + func (w waitForAbsentGroupVersionsV2) Do(ctx context.Context, client testClient) error { - err := WaitForResultWithCondition(ctx, client, func(result apidiscoveryv2beta1.APIGroupDiscoveryList) bool { + err := WaitForResultWithCondition(ctx, client, func(result apidiscoveryv2.APIGroupDiscoveryList) bool { for _, gv := range w { if FindGroupVersionV2(result, gv) != nil { return false @@ -315,6 +342,23 @@ func (w waitForAbsentGroupVersionsV2) Do(ctx context.Context, client testClient) return nil } +func (w waitForAbsentGroupVersionsV2Beta1) Do(ctx context.Context, client testClient) error { + err := WaitForV2Beta1ResultWithCondition(ctx, client, func(result apidiscoveryv2beta1.APIGroupDiscoveryList) bool { + for _, gv := range w { + if FindGroupVersionV2Beta1(result, gv) != nil { + return false + } + } + + return true + }) + + if err != nil { + return fmt.Errorf("waiting for absent groupversions v2 (%v): %w", w, err) + } + return nil +} + func (w waitForGroupVersionsV1) Do(ctx context.Context, client testClient) error { err := WaitForV1GroupsWithCondition(ctx, client, func(result metav1.APIGroupList) bool { for _, gv := range w { @@ -429,7 +473,7 @@ func (w waitForResourcesAbsentV1) Do(ctx context.Context, client testClient) err } func (w waitForResourcesV2) Do(ctx context.Context, client testClient) error { - err := WaitForResultWithCondition(ctx, client, func(result apidiscoveryv2beta1.APIGroupDiscoveryList) bool { + err := WaitForResultWithCondition(ctx, client, func(result apidiscoveryv2.APIGroupDiscoveryList) bool { for _, gvr := range w { if info := FindGroupVersionV2(result, metav1.GroupVersion{Group: gvr.Group, Version: gvr.Version}); info == nil { return false @@ -458,7 +502,7 @@ func (w waitForResourcesV2) Do(ctx context.Context, client testClient) error { } func (w waitForResourcesAbsentV2) Do(ctx context.Context, client testClient) error { - err := WaitForResultWithCondition(ctx, client, func(result apidiscoveryv2beta1.APIGroupDiscoveryList) bool { + err := WaitForResultWithCondition(ctx, client, func(result apidiscoveryv2.APIGroupDiscoveryList) bool { for _, gvr := range w { if info := FindGroupVersionV2(result, metav1.GroupVersion{Group: gvr.Group, Version: gvr.Version}); info == nil { return false @@ -484,7 +528,7 @@ func (i inlineAction) Do(ctx context.Context, client testClient) error { return i(ctx, client) } -func FetchV2Discovery(ctx context.Context, client testClient) (apidiscoveryv2beta1.APIGroupDiscoveryList, error) { +func FetchV2Discovery(ctx context.Context, client testClient) (apidiscoveryv2.APIGroupDiscoveryList, error) { result, err := client. Discovery(). RESTClient(). @@ -494,6 +538,29 @@ func FetchV2Discovery(ctx context.Context, client testClient) (apidiscoveryv2bet Do(ctx). Raw() + if err != nil { + return apidiscoveryv2.APIGroupDiscoveryList{}, fmt.Errorf("failed to fetch v2 discovery: %w", err) + } + + groupList := apidiscoveryv2.APIGroupDiscoveryList{} + err = json.Unmarshal(result, &groupList) + if err != nil { + return apidiscoveryv2.APIGroupDiscoveryList{}, fmt.Errorf("failed to parse v2 discovery: %w", err) + } + + return groupList, nil +} + +func FetchV2Beta1Discovery(ctx context.Context, client testClient) (apidiscoveryv2beta1.APIGroupDiscoveryList, error) { + result, err := client. + Discovery(). + RESTClient(). + Get(). + AbsPath("/apis"). + SetHeader("Accept", acceptV2Beta1JSON). + Do(ctx). + Raw() + if err != nil { return apidiscoveryv2beta1.APIGroupDiscoveryList{}, fmt.Errorf("failed to fetch v2 discovery: %w", err) } @@ -562,7 +629,7 @@ func FetchV1DiscoveryResource(ctx context.Context, client testClient, gv metav1. } func WaitForGroupsAbsent(ctx context.Context, client testClient, groups ...string) error { - return WaitForResultWithCondition(ctx, client, func(groupList apidiscoveryv2beta1.APIGroupDiscoveryList) bool { + return WaitForResultWithCondition(ctx, client, func(groupList apidiscoveryv2.APIGroupDiscoveryList) bool { for _, searchGroup := range groups { for _, docGroup := range groupList.Items { if docGroup.Name == searchGroup { @@ -598,8 +665,8 @@ func WaitForRootPaths(t *testing.T, ctx context.Context, client testClient, requ }) } -func WaitForGroups(ctx context.Context, client testClient, groups ...apidiscoveryv2beta1.APIGroupDiscovery) error { - return WaitForResultWithCondition(ctx, client, func(groupList apidiscoveryv2beta1.APIGroupDiscoveryList) bool { +func WaitForGroups(ctx context.Context, client testClient, groups ...apidiscoveryv2.APIGroupDiscovery) error { + return WaitForResultWithCondition(ctx, client, func(groupList apidiscoveryv2.APIGroupDiscoveryList) bool { for _, searchGroup := range groups { found := false for _, docGroup := range groupList.Items { @@ -616,13 +683,14 @@ func WaitForGroups(ctx context.Context, client testClient, groups ...apidiscover }) } -func WaitForResultWithCondition(ctx context.Context, client testClient, condition func(result apidiscoveryv2beta1.APIGroupDiscoveryList) bool) error { +func WaitForResultWithCondition(ctx context.Context, client testClient, condition func(result apidiscoveryv2.APIGroupDiscoveryList) bool) error { // Keep repeatedly fetching document from aggregator. // Check to see if it contains our service within a reasonable amount of time - return wait.PollWithContext( + return wait.PollUntilContextTimeout( ctx, 250*time.Millisecond, maxTimeout, + true, func(ctx context.Context) (done bool, err error) { groupList, err := FetchV2Discovery(ctx, client) if err != nil { @@ -637,13 +705,36 @@ func WaitForResultWithCondition(ctx context.Context, client testClient, conditio }) } +func WaitForV2Beta1ResultWithCondition(ctx context.Context, client testClient, condition func(result apidiscoveryv2beta1.APIGroupDiscoveryList) bool) error { + // Keep repeatedly fetching document from aggregator. + // Check to see if it contains our service within a reasonable amount of time + return wait.PollUntilContextTimeout( + ctx, + 250*time.Millisecond, + maxTimeout, + true, + func(ctx context.Context) (done bool, err error) { + groupList, err := FetchV2Beta1Discovery(ctx, client) + if err != nil { + return false, err + } + + if condition(groupList) { + return true, nil + } + + return false, nil + }) +} + func WaitForV1GroupsWithCondition(ctx context.Context, client testClient, condition func(result metav1.APIGroupList) bool) error { // Keep repeatedly fetching document from aggregator. // Check to see if it contains our service within a reasonable amount of time - return wait.PollWithContext( + return wait.PollUntilContextTimeout( ctx, 250*time.Millisecond, maxTimeout, + true, func(ctx context.Context) (done bool, err error) { groupList, err := FetchV1DiscoveryGroups(ctx, client) @@ -662,10 +753,11 @@ func WaitForV1GroupsWithCondition(ctx context.Context, client testClient, condit func WaitForV1ResourcesWithCondition(ctx context.Context, client testClient, gv metav1.GroupVersion, condition func(result metav1.APIResourceList) bool) error { // Keep repeatedly fetching document from aggregator. // Check to see if it contains our service within a reasonable amount of time - return wait.PollWithContext( + return wait.PollUntilContextTimeout( ctx, 250*time.Millisecond, maxTimeout, + true, func(ctx context.Context) (done bool, err error) { resourceList, err := FetchV1DiscoveryResource(ctx, client, gv) @@ -697,7 +789,23 @@ func FindGroupVersionV1(discovery metav1.APIGroupList, gv metav1.GroupVersion) b return false } -func FindGroupVersionV2(discovery apidiscoveryv2beta1.APIGroupDiscoveryList, gv metav1.GroupVersion) *apidiscoveryv2beta1.APIVersionDiscovery { +func FindGroupVersionV2(discovery apidiscoveryv2.APIGroupDiscoveryList, gv metav1.GroupVersion) *apidiscoveryv2.APIVersionDiscovery { + for _, documentGroup := range discovery.Items { + if documentGroup.Name != gv.Group { + continue + } + + for _, documentVersion := range documentGroup.Versions { + if documentVersion.Version == gv.Version { + return &documentVersion + } + } + } + + return nil +} + +func FindGroupVersionV2Beta1(discovery apidiscoveryv2beta1.APIGroupDiscoveryList, gv metav1.GroupVersion) *apidiscoveryv2beta1.APIVersionDiscovery { for _, documentGroup := range discovery.Items { if documentGroup.Name != gv.Group { continue