diff --git a/discovery/aggregated_discovery.go b/discovery/aggregated_discovery.go index f72c4205..f5eaaeda 100644 --- a/discovery/aggregated_discovery.go +++ b/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/discovery/aggregated_discovery_test.go b/discovery/aggregated_discovery_test.go index a5004804..c9cb43d2 100644 --- a/discovery/aggregated_discovery_test.go +++ b/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/discovery/cached/disk/cached_discovery_test.go b/discovery/cached/disk/cached_discovery_test.go index e6c7a03a..f27f2a43 100644 --- a/discovery/cached/disk/cached_discovery_test.go +++ b/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/discovery/cached/memory/memcache_test.go b/discovery/cached/memory/memcache_test.go index a86ece33..dbd7b46c 100644 --- a/discovery/cached/memory/memcache_test.go +++ b/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/discovery/discovery_client.go b/discovery/discovery_client.go index df0e0f99..ef14fee5 100644 --- a/discovery/discovery_client.go +++ b/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/discovery/discovery_client_test.go b/discovery/discovery_client_test.go index db96ba3b..87e6c95a 100644 --- a/discovery/discovery_client_test.go +++ b/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, },