mirror of
https://github.com/rancher/steve.git
synced 2025-08-07 17:33:30 +00:00
In the original implementation of the definition handler, the resource list was checked for preferred version, and this overrode the preferred version of the overall group. However, this logic was inaccurate and did not use the group as the source of truth on the preferred version like it should have.
356 lines
10 KiB
Go
356 lines
10 KiB
Go
package definitions
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
|
|
openapi_v2 "github.com/google/gnostic-models/openapiv2"
|
|
"github.com/rancher/apiserver/pkg/apierror"
|
|
"github.com/rancher/apiserver/pkg/types"
|
|
wschemas "github.com/rancher/wrangler/v2/pkg/schemas"
|
|
"github.com/stretchr/testify/require"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/version"
|
|
"k8s.io/client-go/discovery"
|
|
"k8s.io/client-go/openapi"
|
|
restclient "k8s.io/client-go/rest"
|
|
"k8s.io/kube-openapi/pkg/util/proto"
|
|
)
|
|
|
|
func TestRefresh(t *testing.T) {
|
|
defaultDocument, err := openapi_v2.ParseDocument([]byte(openapi_raw))
|
|
require.NoError(t, err)
|
|
defaultModels, err := proto.NewOpenAPIData(defaultDocument)
|
|
require.NoError(t, err)
|
|
defaultSchemaToModel := map[string]string{
|
|
"management.cattle.io.globalrole": "io.cattle.management.v1.GlobalRole",
|
|
"noversion.cattle.io.resource": "io.cattle.noversion.v2.Resource",
|
|
"missinggroup.cattle.io.resource": "io.cattle.missinggroup.v2.Resource",
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
openapiError error
|
|
serverGroupsErr error
|
|
useBadOpenApiDoc bool
|
|
nilGroups bool
|
|
wantModels *proto.Models
|
|
wantSchemaToModel map[string]string
|
|
wantError bool
|
|
}{
|
|
{
|
|
name: "success",
|
|
wantModels: &defaultModels,
|
|
wantSchemaToModel: defaultSchemaToModel,
|
|
},
|
|
{
|
|
name: "error - openapi doc unavailable",
|
|
openapiError: fmt.Errorf("server unavailable"),
|
|
wantError: true,
|
|
},
|
|
{
|
|
name: "error - unable to parse openapi doc",
|
|
useBadOpenApiDoc: true,
|
|
wantError: true,
|
|
},
|
|
{
|
|
name: "error - unable to retrieve groups and resources",
|
|
serverGroupsErr: fmt.Errorf("server not available"),
|
|
wantError: true,
|
|
},
|
|
{
|
|
name: "no groups or error from server",
|
|
nilGroups: true,
|
|
wantModels: &defaultModels,
|
|
wantSchemaToModel: map[string]string{
|
|
"management.cattle.io.globalrole": "io.cattle.management.v2.GlobalRole",
|
|
"noversion.cattle.io.resource": "io.cattle.noversion.v2.Resource",
|
|
"missinggroup.cattle.io.resource": "io.cattle.missinggroup.v2.Resource",
|
|
},
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
test := test
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
client, err := buildDefaultDiscovery()
|
|
client.DocumentErr = test.openapiError
|
|
client.GroupsErr = test.serverGroupsErr
|
|
if test.useBadOpenApiDoc {
|
|
schema := client.Document.Definitions.AdditionalProperties[0]
|
|
schema.Value.Type = &openapi_v2.TypeItem{
|
|
Value: []string{"multiple", "entries"},
|
|
}
|
|
}
|
|
if test.nilGroups {
|
|
client.Groups = nil
|
|
}
|
|
require.Nil(t, err)
|
|
handler := SchemaDefinitionHandler{
|
|
client: client,
|
|
}
|
|
err = handler.Refresh()
|
|
if test.wantError {
|
|
require.Error(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
require.Equal(t, test.wantModels, handler.models)
|
|
require.Equal(t, test.wantSchemaToModel, handler.schemaToModel)
|
|
})
|
|
|
|
}
|
|
}
|
|
|
|
func Test_byID(t *testing.T) {
|
|
defaultDocument, err := openapi_v2.ParseDocument([]byte(openapi_raw))
|
|
require.NoError(t, err)
|
|
defaultModels, err := proto.NewOpenAPIData(defaultDocument)
|
|
require.NoError(t, err)
|
|
defaultSchemaToModel := map[string]string{
|
|
"management.cattle.io.globalrole": "io.cattle.management.v2.GlobalRole",
|
|
}
|
|
schemas := types.EmptyAPISchemas()
|
|
addBaseSchema := func(names ...string) {
|
|
for _, name := range names {
|
|
schemas.MustAddSchema(types.APISchema{
|
|
Schema: &wschemas.Schema{
|
|
ID: name,
|
|
CollectionMethods: []string{"get"},
|
|
ResourceMethods: []string{"get"},
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
intPtr := func(input int) *int {
|
|
return &input
|
|
}
|
|
|
|
addBaseSchema("management.cattle.io.globalrole", "management.cattle.io.missingfrommodel", "management.cattle.io.notakind")
|
|
|
|
tests := []struct {
|
|
name string
|
|
schemaName string
|
|
models *proto.Models
|
|
schemaToModel map[string]string
|
|
wantObject *types.APIObject
|
|
wantError bool
|
|
wantErrorCode *int
|
|
}{
|
|
{
|
|
name: "global role definition",
|
|
schemaName: "management.cattle.io.globalrole",
|
|
models: &defaultModels,
|
|
schemaToModel: defaultSchemaToModel,
|
|
wantObject: &types.APIObject{
|
|
ID: "management.cattle.io.globalrole",
|
|
Type: "schemaDefinition",
|
|
Object: schemaDefinition{
|
|
DefinitionType: "io.cattle.management.v2.GlobalRole",
|
|
Definitions: map[string]definition{
|
|
"io.cattle.management.v2.GlobalRole": {
|
|
ResourceFields: map[string]definitionField{
|
|
"apiVersion": {
|
|
Type: "string",
|
|
Description: "The APIVersion of this resource",
|
|
},
|
|
"kind": {
|
|
Type: "string",
|
|
Description: "The kind",
|
|
},
|
|
"metadata": {
|
|
Type: "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta",
|
|
Description: "The metadata",
|
|
},
|
|
"spec": {
|
|
Type: "io.cattle.management.v2.GlobalRole.spec", Description: "The spec for the project",
|
|
},
|
|
},
|
|
Type: "io.cattle.management.v2.GlobalRole",
|
|
Description: "A Global Role V2 provides Global Permissions in Rancher",
|
|
},
|
|
"io.cattle.management.v2.GlobalRole.spec": {
|
|
ResourceFields: map[string]definitionField{
|
|
"clusterName": {
|
|
Type: "string",
|
|
Description: "The name of the cluster",
|
|
Required: true,
|
|
},
|
|
"displayName": {
|
|
Type: "string",
|
|
Description: "The UI readable name",
|
|
Required: true,
|
|
},
|
|
"newField": {
|
|
Type: "string",
|
|
Description: "A new field not present in v1",
|
|
},
|
|
"notRequired": {
|
|
Type: "boolean",
|
|
Description: "Some field that isn't required",
|
|
},
|
|
},
|
|
Type: "io.cattle.management.v2.GlobalRole.spec",
|
|
Description: "The spec for the project",
|
|
},
|
|
"io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta": {
|
|
ResourceFields: map[string]definitionField{
|
|
"annotations": {
|
|
Type: "map",
|
|
SubType: "string",
|
|
Description: "annotations of the resource",
|
|
},
|
|
"name": {
|
|
Type: "string",
|
|
SubType: "",
|
|
Description: "name of the resource",
|
|
},
|
|
},
|
|
Type: "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta",
|
|
Description: "Object Metadata",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "missing definition",
|
|
schemaName: "management.cattle.io.cluster",
|
|
models: &defaultModels,
|
|
schemaToModel: defaultSchemaToModel,
|
|
wantError: true,
|
|
wantErrorCode: intPtr(404),
|
|
},
|
|
{
|
|
name: "not refreshed",
|
|
schemaName: "management.cattle.io.globalrole",
|
|
wantError: true,
|
|
wantErrorCode: intPtr(503),
|
|
},
|
|
{
|
|
name: "has schema, missing from model",
|
|
schemaName: "management.cattle.io.missingfrommodel",
|
|
models: &defaultModels,
|
|
schemaToModel: defaultSchemaToModel,
|
|
wantError: true,
|
|
wantErrorCode: intPtr(503),
|
|
},
|
|
{
|
|
name: "has schema, model is not a kind",
|
|
schemaName: "management.cattle.io.notakind",
|
|
models: &defaultModels,
|
|
schemaToModel: map[string]string{
|
|
"management.cattle.io.notakind": "io.management.cattle.NotAKind",
|
|
},
|
|
wantError: true,
|
|
wantErrorCode: intPtr(500),
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
test := test
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
handler := SchemaDefinitionHandler{
|
|
models: test.models,
|
|
schemaToModel: test.schemaToModel,
|
|
}
|
|
request := types.APIRequest{
|
|
Schemas: schemas,
|
|
Name: test.schemaName,
|
|
}
|
|
response, err := handler.byIDHandler(&request)
|
|
if test.wantError {
|
|
require.Error(t, err)
|
|
if test.wantErrorCode != nil {
|
|
require.True(t, apierror.IsAPIError(err))
|
|
apiErr, _ := err.(*apierror.APIError)
|
|
require.Equal(t, *test.wantErrorCode, apiErr.Code.Status)
|
|
}
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.Equal(t, *test.wantObject, response)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func buildDefaultDiscovery() (*fakeDiscovery, error) {
|
|
document, err := openapi_v2.ParseDocument([]byte(openapi_raw))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to parse openapi document %w", err)
|
|
}
|
|
groups := []metav1.APIGroup{
|
|
{
|
|
Name: "management.cattle.io",
|
|
PreferredVersion: metav1.GroupVersionForDiscovery{
|
|
GroupVersion: "management.cattle.io/v2",
|
|
Version: "v1",
|
|
},
|
|
Versions: []metav1.GroupVersionForDiscovery{
|
|
{
|
|
GroupVersion: "management.cattle.io/v1",
|
|
Version: "v1",
|
|
},
|
|
{
|
|
GroupVersion: "management.cattle.io/v2",
|
|
Version: "v2",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "noversion.cattle.io",
|
|
Versions: []metav1.GroupVersionForDiscovery{
|
|
{
|
|
GroupVersion: "noversion.cattle.io/v1",
|
|
Version: "v1",
|
|
},
|
|
{
|
|
GroupVersion: "noversion.cattle.io/v2",
|
|
Version: "v2",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
return &fakeDiscovery{
|
|
Groups: &metav1.APIGroupList{
|
|
Groups: groups,
|
|
},
|
|
Document: document,
|
|
}, nil
|
|
}
|
|
|
|
type fakeDiscovery struct {
|
|
Groups *metav1.APIGroupList
|
|
Document *openapi_v2.Document
|
|
GroupsErr error
|
|
DocumentErr error
|
|
}
|
|
|
|
// ServerGroups is the only method that needs to be mocked
|
|
func (f *fakeDiscovery) ServerGroups() (*metav1.APIGroupList, error) {
|
|
return f.Groups, f.GroupsErr
|
|
}
|
|
|
|
// The rest of these methods are just here to conform to discovery.DiscoveryInterface
|
|
func (f *fakeDiscovery) ServerGroupsAndResources() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) {
|
|
return nil, nil, nil
|
|
}
|
|
|
|
func (f *fakeDiscovery) RESTClient() restclient.Interface { return nil }
|
|
func (f *fakeDiscovery) ServerResourcesForGroupVersion(groupVersion string) (*metav1.APIResourceList, error) {
|
|
return nil, nil
|
|
}
|
|
func (f *fakeDiscovery) ServerPreferredResources() ([]*metav1.APIResourceList, error) {
|
|
return nil, nil
|
|
}
|
|
func (f *fakeDiscovery) ServerPreferredNamespacedResources() ([]*metav1.APIResourceList, error) {
|
|
return nil, nil
|
|
}
|
|
func (f *fakeDiscovery) ServerVersion() (*version.Info, error) { return nil, nil }
|
|
func (f *fakeDiscovery) OpenAPISchema() (*openapi_v2.Document, error) {
|
|
return f.Document, f.DocumentErr
|
|
}
|
|
func (f *fakeDiscovery) OpenAPIV3() openapi.Client { return nil }
|
|
func (f *fakeDiscovery) WithLegacy() discovery.DiscoveryInterface { return f }
|