Merge pull request #21909 from madhusudancs/scale-group-apiinstaller

Allow cross-group subresource registrations in the APIInstaller.
pull/6/head
Jeff Lowdermilk 2016-03-02 16:06:50 -08:00
commit 6390aef05d
4 changed files with 234 additions and 74 deletions

View File

@ -110,6 +110,63 @@ func (a *APIInstaller) NewWebService() *restful.WebService {
return ws
}
// getResourceKind returns the external group version kind registered for the given storage
// object. If the storage object is a subresource and has an override supplied for it, it returns
// the group version kind supplied in the override.
func (a *APIInstaller) getResourceKind(path string, storage rest.Storage) (unversioned.GroupVersionKind, error) {
if fqKindToRegister, ok := a.group.SubresourceGroupVersionKind[path]; ok {
return fqKindToRegister, nil
}
object := storage.New()
fqKinds, err := a.group.Typer.ObjectKinds(object)
if err != nil {
return unversioned.GroupVersionKind{}, err
}
// a given go type can have multiple potential fully qualified kinds. Find the one that corresponds with the group
// we're trying to register here
fqKindToRegister := unversioned.GroupVersionKind{}
for _, fqKind := range fqKinds {
if fqKind.Group == a.group.GroupVersion.Group {
fqKindToRegister = a.group.GroupVersion.WithKind(fqKind.Kind)
break
}
// TODO This keeps it doing what it was doing before, but it doesn't feel right.
if fqKind.Group == extensions.GroupName && fqKind.Kind == "ThirdPartyResourceData" {
fqKindToRegister = a.group.GroupVersion.WithKind(fqKind.Kind)
}
}
if fqKindToRegister.IsEmpty() {
return unversioned.GroupVersionKind{}, fmt.Errorf("unable to locate fully qualified kind for %v: found %v when registering for %v", reflect.TypeOf(object), fqKinds, a.group.GroupVersion)
}
return fqKindToRegister, nil
}
// restMapping returns rest mapper for the resource.
// Example REST paths that this mapper maps.
// 1. Resource only, no subresource:
// Resource Type: batch/v1.Job (input args: resource = "jobs")
// REST path: /apis/batch/v1/namespaces/{namespace}/job/{name}
// 2. Subresource and its parent belong to different API groups and/or versions:
// Resource Type: extensions/v1beta1.ReplicaSet (input args: resource = "replicasets")
// Subresource Type: autoscaling/v1.Scale
// REST path: /apis/extensions/v1beta1/namespaces/{namespace}/replicaset/{name}/scale
func (a *APIInstaller) restMapping(resource string) (*meta.RESTMapping, error) {
// subresources must have parent resources, and follow the namespacing rules of their parent.
// So get the storage of the resource (which is the parent resource in case of subresources)
storage, ok := a.group.Storage[resource]
if !ok {
return nil, fmt.Errorf("unable to locate the storage object for resource: %s", resource)
}
fqKindToRegister, err := a.getResourceKind(resource, storage)
if err != nil {
return nil, fmt.Errorf("unable to locate fully qualified kind for mapper resource %s: %v", resource, err)
}
return a.group.Mapper.RESTMapping(fqKindToRegister.GroupKind(), fqKindToRegister.Version)
}
func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storage, ws *restful.WebService, proxyHandler http.Handler) (*unversioned.APIResource, error) {
admit := a.group.Admit
context := a.group.Context
@ -119,88 +176,28 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
optionsExternalVersion = *a.group.OptionsExternalVersion
}
var resource, subresource string
switch parts := strings.Split(path, "/"); len(parts) {
case 2:
resource, subresource = parts[0], parts[1]
case 1:
resource = parts[0]
default:
// TODO: support deeper paths
return nil, fmt.Errorf("api_installer allows only one or two segment paths (resource or resource/subresource)")
}
hasSubresource := len(subresource) > 0
object := storage.New()
fqKinds, err := a.group.Typer.ObjectKinds(object)
resource, subresource, err := splitSubresource(path)
if err != nil {
return nil, err
}
// a given go type can have multiple potential fully qualified kinds. Find the one that corresponds with the group
// we're trying to register here
fqKindToRegister := unversioned.GroupVersionKind{}
for _, fqKind := range fqKinds {
if fqKind.Group == a.group.GroupVersion.Group {
fqKindToRegister = fqKind
break
}
// TODO This keeps it doing what it was doing before, but it doesn't feel right.
if fqKind.Group == extensions.GroupName && fqKind.Kind == "ThirdPartyResourceData" {
fqKindToRegister = fqKind
fqKindToRegister.Group = a.group.GroupVersion.Group
fqKindToRegister.Version = a.group.GroupVersion.Version
}
mapping, err := a.restMapping(resource)
if err != nil {
return nil, err
}
kind := fqKindToRegister.Kind
if fqKindToRegister.IsEmpty() {
return nil, fmt.Errorf("unable to locate fully qualified kind for %v: found %v when registering for %v", reflect.TypeOf(object), fqKinds, a.group.GroupVersion)
fqKindToRegister, err := a.getResourceKind(path, storage)
if err != nil {
return nil, err
}
versionedPtr, err := a.group.Creater.New(a.group.GroupVersion.WithKind(kind))
versionedPtr, err := a.group.Creater.New(fqKindToRegister)
if err != nil {
return nil, err
}
versionedObject := indirectArbitraryPointer(versionedPtr)
mapping, err := a.group.Mapper.RESTMapping(fqKindToRegister.GroupKind(), a.group.GroupVersion.Version)
if err != nil {
return nil, err
}
// subresources must have parent resources, and follow the namespacing rules of their parent
if hasSubresource {
parentStorage, ok := a.group.Storage[resource]
if !ok {
return nil, fmt.Errorf("subresources can only be declared when the parent is also registered: %s needs %s", path, resource)
}
parentObject := parentStorage.New()
parentFQKinds, err := a.group.Typer.ObjectKinds(parentObject)
if err != nil {
return nil, err
}
// a given go type can have multiple potential fully qualified kinds. Find the one that corresponds with the group
// we're trying to register here
parentFQKindToRegister := unversioned.GroupVersionKind{}
for _, fqKind := range parentFQKinds {
if fqKind.Group == a.group.GroupVersion.Group {
parentFQKindToRegister = fqKind
break
}
}
if parentFQKindToRegister.IsEmpty() {
return nil, fmt.Errorf("unable to locate fully qualified kind for %v: found %v when registering for %v", reflect.TypeOf(object), fqKinds, a.group.GroupVersion)
}
parentMapping, err := a.group.Mapper.RESTMapping(parentFQKindToRegister.GroupKind(), a.group.GroupVersion.Version)
if err != nil {
return nil, err
}
mapping.Scope = parentMapping.Scope
}
kind := fqKindToRegister.Kind
hasSubresource := len(subresource) > 0
// what verbs are supported by the storage, used to know what verbs we support per path
creater, isCreater := storage.(rest.Creater)
@ -329,7 +326,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
if ok {
resourceKind = kindProvider.Kind()
} else {
resourceKind = fqKindToRegister.Kind
resourceKind = kind
}
var apiResource unversioned.APIResource
@ -451,9 +448,10 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
Creater: a.group.Creater,
Convertor: a.group.Convertor,
// TODO: This seems wrong for cross-group subresources. It makes an assumption that a subresource and its parent are in the same group version. Revisit this.
Resource: a.group.GroupVersion.WithResource(resource),
Subresource: subresource,
Kind: a.group.GroupVersion.WithKind(kind),
Kind: fqKindToRegister,
}
for _, action := range actions {
reqScope.Namer = action.Namer
@ -948,3 +946,19 @@ var _ rest.StorageMetadata = defaultStorageMetadata{}
func (defaultStorageMetadata) ProducesMIMETypes(verb string) []string {
return nil
}
// splitSubresource checks if the given storage path is the path of a subresource and returns
// the resource and subresource components.
func splitSubresource(path string) (string, string, error) {
var resource, subresource string
switch parts := strings.Split(path, "/"); len(parts) {
case 2:
resource, subresource = parts[0], parts[1]
case 1:
resource = parts[0]
default:
// TODO: support deeper paths
return "", "", fmt.Errorf("api_installer allows only one or two segment paths (resource or resource/subresource)")
}
return resource, subresource, nil
}

View File

@ -96,6 +96,12 @@ type APIGroupVersion struct {
Context api.RequestContextMapper
MinRequestTimeout time.Duration
// SubresourceGroupVersionKind contains the GroupVersionKind overrides for each subresource that is
// accessible from this API group version. The GroupVersionKind is that of the external version of
// the subresource. The key of this map should be the path of the subresource. The keys here should
// match the keys in the Storage map above for subresources.
SubresourceGroupVersionKind map[string]unversioned.GroupVersionKind
}
type ProxyDialerFunc func(network, addr string) (net.Conn, error)

View File

@ -59,9 +59,12 @@ func convert(obj runtime.Object) (runtime.Object, error) {
// This creates fake API versions, similar to api/latest.go.
var testAPIGroup = "test.group"
var testAPIGroup2 = "test.group2"
var testInternalGroupVersion = unversioned.GroupVersion{Group: testAPIGroup, Version: runtime.APIVersionInternal}
var testGroupVersion = unversioned.GroupVersion{Group: testAPIGroup, Version: "version"}
var newGroupVersion = unversioned.GroupVersion{Group: testAPIGroup, Version: "version2"}
var testGroup2Version = unversioned.GroupVersion{Group: testAPIGroup2, Version: "version"}
var testInternalGroup2Version = unversioned.GroupVersion{Group: testAPIGroup2, Version: runtime.APIVersionInternal}
var prefix = "apis"
var grouplessGroupVersion = unversioned.GroupVersion{Group: "", Version: "v1"}
@ -99,6 +102,11 @@ func interfacesFor(version unversioned.GroupVersion) (*meta.VersionInterfaces, e
ObjectConvertor: api.Scheme,
MetadataAccessor: accessor,
}, nil
case testGroup2Version:
return &meta.VersionInterfaces{
ObjectConvertor: api.Scheme,
MetadataAccessor: accessor,
}, nil
default:
return nil, fmt.Errorf("unsupported storage version: %s (valid: %v)", version, groupVersions)
}
@ -139,11 +147,18 @@ func addTestTypes() {
}
api.Scheme.AddKnownTypes(testGroupVersion,
&apiservertesting.Simple{}, &apiservertesting.SimpleList{}, &ListOptions{},
&api.DeleteOptions{}, &apiservertesting.SimpleGetOptions{}, &apiservertesting.SimpleRoot{})
&api.DeleteOptions{}, &apiservertesting.SimpleGetOptions{}, &apiservertesting.SimpleRoot{},
&SimpleXGSubresource{})
api.Scheme.AddKnownTypes(testGroupVersion, &api.Pod{})
api.Scheme.AddKnownTypes(testInternalGroupVersion,
&apiservertesting.Simple{}, &apiservertesting.SimpleList{}, &api.ListOptions{},
&apiservertesting.SimpleGetOptions{}, &apiservertesting.SimpleRoot{})
&apiservertesting.SimpleGetOptions{}, &apiservertesting.SimpleRoot{},
&SimpleXGSubresource{})
// Register SimpleXGSubresource in both testGroupVersion and testGroup2Version, and also their
// their corresponding internal versions, to verify that the desired group version object is
// served in the tests.
api.Scheme.AddKnownTypes(testGroup2Version, &SimpleXGSubresource{})
api.Scheme.AddKnownTypes(testInternalGroup2Version, &SimpleXGSubresource{})
}
func addNewTestTypes() {
@ -3157,6 +3172,124 @@ func TestUpdateChecksAPIVersion(t *testing.T) {
}
}
// SimpleXGSubresource is a cross group subresource, i.e. the subresource does not belong to the
// same group as its parent resource.
type SimpleXGSubresource struct {
unversioned.TypeMeta `json:",inline"`
api.ObjectMeta `json:"metadata"`
SubresourceInfo string `json:"subresourceInfo,omitempty"`
Labels map[string]string `json:"labels,omitempty"`
}
func (obj *SimpleXGSubresource) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta }
type SimpleXGSubresourceRESTStorage struct {
item SimpleXGSubresource
}
func (storage *SimpleXGSubresourceRESTStorage) New() runtime.Object {
return &SimpleXGSubresource{}
}
func (storage *SimpleXGSubresourceRESTStorage) Get(ctx api.Context, id string) (runtime.Object, error) {
copied, err := api.Scheme.Copy(&storage.item)
if err != nil {
panic(err)
}
return copied, nil
}
func TestXGSubresource(t *testing.T) {
container := restful.NewContainer()
container.Router(restful.CurlyRouter{})
mux := container.ServeMux
itemID := "theID"
subresourceStorage := &SimpleXGSubresourceRESTStorage{
item: SimpleXGSubresource{
SubresourceInfo: "foo",
},
}
storage := map[string]rest.Storage{
"simple": &SimpleRESTStorage{},
"simple/subsimple": subresourceStorage,
}
group := APIGroupVersion{
Storage: storage,
RequestInfoResolver: newTestRequestInfoResolver(),
Creater: api.Scheme,
Convertor: api.Scheme,
Typer: api.Scheme,
Linker: selfLinker,
Mapper: namespaceMapper,
ParameterCodec: api.ParameterCodec,
Admit: admissionControl,
Context: requestContextMapper,
Root: "/" + prefix,
GroupVersion: testGroupVersion,
OptionsExternalVersion: &testGroupVersion,
Serializer: api.Codecs,
SubresourceGroupVersionKind: map[string]unversioned.GroupVersionKind{
"simple/subsimple": testGroup2Version.WithKind("SimpleXGSubresource"),
},
}
if err := (&group).InstallREST(container); err != nil {
panic(fmt.Sprintf("unable to install container %s: %v", group.GroupVersion, err))
}
ws := new(restful.WebService)
InstallSupport(mux, ws)
container.Add(ws)
handler := defaultAPIServer{mux, container}
server := httptest.NewServer(handler)
// TODO: Uncomment when fix #19254
// defer server.Close()
resp, err := http.Get(server.URL + "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + "/namespaces/default/simple/" + itemID + "/subsimple")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Fatalf("unexpected response: %#v", resp)
}
var itemOut SimpleXGSubresource
body, err := extractBody(resp, &itemOut)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
// Test if the returned object has the expected group, version and kind
// We are directly unmarshaling JSON here because TypeMeta cannot be decoded through the
// installed decoders. TypeMeta cannot be decoded because it is added to the ignored
// conversion type list in API scheme and hence cannot be converted from input type object
// to output type object. So it's values don't appear in the decoded output object.
decoder := json.NewDecoder(strings.NewReader(body))
var itemFromBody SimpleXGSubresource
err = decoder.Decode(&itemFromBody)
if err != nil {
t.Errorf("unexpected JSON decoding error: %v", err)
}
if want := fmt.Sprintf("%s/%s", testGroup2Version.Group, testGroup2Version.Version); itemFromBody.APIVersion != want {
t.Errorf("unexpected APIVersion got: %+v want: %+v", itemFromBody.APIVersion, want)
}
if itemFromBody.Kind != "SimpleXGSubresource" {
t.Errorf("unexpected Kind got: %+v want: SimpleXGSubresource", itemFromBody.Kind)
}
if itemOut.Name != subresourceStorage.item.Name {
t.Errorf("Unexpected data: %#v, expected %#v (%s)", itemOut, subresourceStorage.item, string(body))
}
}
func readBodyOrDie(r io.Reader) []byte {
body, err := ioutil.ReadAll(r)
if err != nil {

View File

@ -192,6 +192,12 @@ type APIGroupInfo struct {
NegotiatedSerializer runtime.NegotiatedSerializer
// ParameterCodec performs conversions for query parameters passed to API calls
ParameterCodec runtime.ParameterCodec
// SubresourceGroupVersionKind contains the GroupVersionKind overrides for each subresource that is
// accessible from this API group version. The GroupVersionKind is that of the external version of
// the subresource. The key of this map should be the path of the subresource. The keys here should
// match the keys in the Storage map above for subresources.
SubresourceGroupVersionKind map[string]unversioned.GroupVersionKind
}
// Config is a structure used to configure a GenericAPIServer.
@ -838,6 +844,7 @@ func (s *GenericAPIServer) getAPIGroupVersion(apiGroupInfo *APIGroupInfo, groupV
version.Creater = apiGroupInfo.Scheme
version.Convertor = apiGroupInfo.Scheme
version.Typer = apiGroupInfo.Scheme
version.SubresourceGroupVersionKind = apiGroupInfo.SubresourceGroupVersionKind
return version, err
}