Skip to content

Commit

Permalink
Adding tests for schemaDefinitions
Browse files Browse the repository at this point in the history
  • Loading branch information
MbolotSuse committed Jan 9, 2024
1 parent eaa28d9 commit 97bb45a
Show file tree
Hide file tree
Showing 4 changed files with 671 additions and 2 deletions.
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,15 @@ require (
github.com/urfave/cli v1.22.14
github.com/urfave/cli/v2 v2.25.7
golang.org/x/sync v0.3.0
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.27.4
k8s.io/apiextensions-apiserver v0.27.4
k8s.io/apimachinery v0.27.4
k8s.io/apiserver v0.27.4
k8s.io/client-go v12.0.0+incompatible
k8s.io/klog v1.0.0
k8s.io/kube-aggregator v0.27.4
k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f
)

require (
Expand Down Expand Up @@ -99,10 +101,8 @@ require (
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/component-base v0.27.4 // indirect
k8s.io/klog/v2 v2.100.1 // indirect
k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect
k8s.io/utils v0.0.0-20230209194617-a36077c30491 // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.1.2 // indirect
sigs.k8s.io/cli-utils v0.27.0 // indirect
Expand Down
325 changes: 325 additions & 0 deletions pkg/schema/definitions/handler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,325 @@
package definitions

import (
"fmt"
"testing"
"time"

openapi_v2 "github.com/google/gnostic/openapiv2"
"github.com/rancher/apiserver/pkg/apierror"
"github.com/rancher/apiserver/pkg/types"
wschemas "github.com/rancher/wrangler/pkg/schemas"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/version"
"k8s.io/client-go/discovery"
"k8s.io/client-go/openapi"
restclient "k8s.io/client-go/rest"
)

var globalRoleObject = 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",
},
},
},
}

func TestByID(t *testing.T) {
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")

tests := []struct {
name string
schemaName string
needsRefresh bool
openapiError error
serverGroupsResourcesErr error
useBadOpenApiDoc bool
unparseableGV bool
wantObject *types.APIObject
wantError bool
wantErrorCode *int
}{
{
name: "global role definition",
schemaName: "management.cattle.io.globalrole",
needsRefresh: true,
wantObject: &globalRoleObject,
},
{
name: "missing definition",
schemaName: "management.cattle.io.cluster",
needsRefresh: true,
wantError: true,
wantErrorCode: intPtr(404),
},
{
name: "not refreshed",
schemaName: "management.cattle.io.globalrole",
needsRefresh: false,
wantError: true,
wantErrorCode: intPtr(503),
},
{
name: "missing from model",
schemaName: "management.cattle.io.missingfrommodel",
needsRefresh: true,
wantError: true,
wantErrorCode: intPtr(503),
},
{
name: "refresh error - openapi doc unavailable",
schemaName: "management.cattle.io.globalrole",
needsRefresh: true,
openapiError: fmt.Errorf("server unavailable"),
wantError: true,
wantErrorCode: intPtr(500),
},
{
name: "refresh error - unable to parse openapi doc",
schemaName: "management.cattle.io.globalrole",
needsRefresh: true,
useBadOpenApiDoc: true,
wantError: true,
wantErrorCode: intPtr(500),
},
{
name: "refresh error - unable to retrieve groups and resources",
schemaName: "management.cattle.io.globalrole",
needsRefresh: true,
serverGroupsResourcesErr: fmt.Errorf("server not available"),
wantError: true,
wantErrorCode: intPtr(500),
},
{
name: "refresh error - unable to retrieve all groups and resources",
schemaName: "management.cattle.io.globalrole",
needsRefresh: true,
serverGroupsResourcesErr: &discovery.ErrGroupDiscoveryFailed{
Groups: map[schema.GroupVersion]error{
{
Group: "other.cattle.io",
Version: "v1",
}: fmt.Errorf("some group error"),
},
},
wantError: true,
wantErrorCode: intPtr(500),
},
{
name: "refresh error - unparesable gv",
schemaName: "management.cattle.io.globalrole",
needsRefresh: true,
unparseableGV: true,
wantError: true,
wantErrorCode: intPtr(500),
},
}

for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
client, err := buildDefaultDiscovery()
client.DocumentErr = test.openapiError
client.GroupResourcesErr = test.serverGroupsResourcesErr
if test.useBadOpenApiDoc {
schema := client.Document.Definitions.AdditionalProperties[0]
schema.Value.Type = &openapi_v2.TypeItem{
Value: []string{"multiple", "entries"},
}
}
if test.unparseableGV {
client.Resources = append(client.Resources, &metav1.APIResourceList{
GroupVersion: "not/parse/able",
})
}
require.Nil(t, err)
handler := SchemaDefinitionHandler{
client: client,
}
if !test.needsRefresh {
handler.lastRefresh = time.Now()
handler.refreshStale = time.Minute * 1
}
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{
Version: "v2",
},
},
}
resources := []*metav1.APIResourceList{
{
GroupVersion: schema.GroupVersion{
Group: "management.cattle.io",
Version: "v2",
}.String(),
APIResources: []metav1.APIResource{
{
Group: "management.cattle.io",
Kind: "GlobalRole",
Version: "v2",
},
},
},
{
GroupVersion: schema.GroupVersion{
Group: "management.cattle.io",
Version: "v1",
}.String(),
APIResources: []metav1.APIResource{
{
Group: "management.cattle.io",
Kind: "GlobalRole",
Version: "v2",
},
},
},
nil,
}
return &fakeDiscovery{
Groups: groups,
Resources: resources,
Document: document,
}, nil
}

type fakeDiscovery struct {
Groups []*metav1.APIGroup
Resources []*metav1.APIResourceList
Document *openapi_v2.Document
GroupResourcesErr error
DocumentErr error
}

// ServerGroupsAndResources is the only method we actually need for the test - just returns what is on the struct
func (f *fakeDiscovery) ServerGroupsAndResources() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) {
return f.Groups, f.Resources, f.GroupResourcesErr
}

// The rest of these methods are just here to conform to discovery.DiscoveryInterface
func (f *fakeDiscovery) RESTClient() restclient.Interface { return nil }
func (f *fakeDiscovery) ServerGroups() (*metav1.APIGroupList, error) { return nil, 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 }
Loading

0 comments on commit 97bb45a

Please sign in to comment.