mirror of
https://github.com/rancher/norman.git
synced 2025-08-01 23:41:24 +00:00
Major refactor of subcontexts
This commit is contained in:
parent
b62e043067
commit
722cedfe01
@ -147,17 +147,26 @@ func (j *JSONResponseWriter) addLinks(b *builder.Builder, schema *types.Schema,
|
||||
rawResource.Links["remove"] = self
|
||||
}
|
||||
|
||||
subContextVersion := context.Schemas.SubContextVersionForSchema(schema)
|
||||
for _, backRef := range context.Schemas.References(schema) {
|
||||
if !backRef.Schema.CanList(context) {
|
||||
continue
|
||||
}
|
||||
|
||||
if schema.SubContext == "" {
|
||||
if subContextVersion == nil {
|
||||
rawResource.Links[backRef.Schema.PluralName] = context.URLBuilder.FilterLink(backRef.Schema, backRef.FieldName, rawResource.ID)
|
||||
} else {
|
||||
rawResource.Links[backRef.Schema.PluralName] = context.URLBuilder.SubContextCollection(schema, rawResource.ID, backRef.Schema)
|
||||
}
|
||||
}
|
||||
|
||||
if subContextVersion != nil {
|
||||
for _, subSchema := range context.Schemas.SchemasForVersion(*subContextVersion) {
|
||||
if subSchema.CanList(context) {
|
||||
rawResource.Links[subSchema.PluralName] = context.URLBuilder.SubContextCollection(schema, rawResource.ID, subSchema)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newCollection(apiContext *types.APIContext) *types.GenericCollection {
|
||||
|
@ -1,12 +1,14 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/rancher/norman/api"
|
||||
"github.com/rancher/norman/store/crd"
|
||||
"github.com/rancher/norman/store/proxy"
|
||||
"github.com/rancher/norman/types"
|
||||
"github.com/rancher/norman/types/factory"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
@ -39,13 +41,19 @@ func main() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
store, err := crd.NewCRDStoreFromConfig(*kubeConfig)
|
||||
client, err := proxy.NewClientGetterFromConfig(*kubeConfig)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
crdFactory := crd.Factory{
|
||||
ClientGetter: client,
|
||||
}
|
||||
|
||||
Schemas.MustImportAndCustomize(&version, Foo{}, func(schema *types.Schema) {
|
||||
schema.Store = store
|
||||
if err := crdFactory.AssignStores(context.Background(), types.DefaultStorageContext, schema); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
|
||||
server := api.NewAPIServer()
|
||||
|
@ -24,6 +24,7 @@ var (
|
||||
ActionNotAvailable = ErrorCode{"ActionNotAvailable", 404}
|
||||
InvalidState = ErrorCode{"InvalidState", 422}
|
||||
ServerError = ErrorCode{"ServerError", 500}
|
||||
ClusterUnavailable = ErrorCode{"ClusterUnavailable", 503}
|
||||
PermissionDenied = ErrorCode{"PermissionDenied", 403}
|
||||
|
||||
MethodNotAllowed = ErrorCode{"MethodNotAllow", 405}
|
||||
|
107
parse/parse.go
107
parse/parse.go
@ -6,7 +6,10 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"sort"
|
||||
|
||||
"github.com/rancher/norman/api/builtin"
|
||||
"github.com/rancher/norman/httperror"
|
||||
"github.com/rancher/norman/types"
|
||||
"github.com/rancher/norman/urlbuilder"
|
||||
)
|
||||
@ -42,26 +45,23 @@ type URLParser func(schema *types.Schemas, url *url.URL) (ParsedURL, error)
|
||||
func DefaultURLParser(schemas *types.Schemas, url *url.URL) (ParsedURL, error) {
|
||||
result := ParsedURL{}
|
||||
|
||||
version := Version(schemas, url.Path)
|
||||
path := url.Path
|
||||
path = multiSlashRegexp.ReplaceAllString(path, "/")
|
||||
version, prefix, parts, subContext := parseVersionAndSubContext(schemas, path)
|
||||
|
||||
if version == nil {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
path := url.Path
|
||||
path = multiSlashRegexp.ReplaceAllString(path, "/")
|
||||
|
||||
parts := strings.SplitN(path[len(version.Path):], "/", 4)
|
||||
prefix, parts, subContext := parseSubContext(schemas, version, parts)
|
||||
|
||||
result.Version = version.Path
|
||||
result.SubContext = subContext
|
||||
result.SubContextPrefix = prefix
|
||||
result.Action, result.Method = parseAction(url)
|
||||
result.Query = url.Query()
|
||||
|
||||
result.Type = safeIndex(parts, 1)
|
||||
result.ID = safeIndex(parts, 2)
|
||||
result.Link = safeIndex(parts, 3)
|
||||
result.Type = safeIndex(parts, 0)
|
||||
result.ID = safeIndex(parts, 1)
|
||||
result.Link = safeIndex(parts, 2)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@ -121,11 +121,14 @@ func Parse(rw http.ResponseWriter, req *http.Request, schemas *types.Schemas, ur
|
||||
}
|
||||
|
||||
if result.Schema == nil {
|
||||
if result.Type != "" {
|
||||
err = httperror.NewAPIError(httperror.NotFound, "failed to find schema "+result.Type)
|
||||
}
|
||||
result.Method = http.MethodGet
|
||||
result.Type = "apiRoot"
|
||||
result.Schema = result.Schemas.Schema(&builtin.Version, "apiRoot")
|
||||
result.ID = result.Version.Path
|
||||
return result, nil
|
||||
return result, err
|
||||
}
|
||||
|
||||
result.Type = result.Schema.ID
|
||||
@ -137,29 +140,61 @@ func Parse(rw http.ResponseWriter, req *http.Request, schemas *types.Schemas, ur
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func parseSubContext(schemas *types.Schemas, version *types.APIVersion, parts []string) (string, []string, map[string]string) {
|
||||
subContext := ""
|
||||
result := map[string]string{}
|
||||
|
||||
for len(parts) > 3 && version != nil && parts[3] != "" {
|
||||
resourceType := parts[1]
|
||||
resourceID := parts[2]
|
||||
|
||||
if !version.SubContexts[resourceType] {
|
||||
break
|
||||
func versionsForPath(schemas *types.Schemas, path string) []types.APIVersion {
|
||||
var matchedVersion []types.APIVersion
|
||||
for _, version := range schemas.Versions() {
|
||||
if strings.HasPrefix(path, version.Path) {
|
||||
matchedVersion = append(matchedVersion, version)
|
||||
}
|
||||
}
|
||||
sort.Slice(matchedVersion, func(i, j int) bool {
|
||||
return len(matchedVersion[i].Path) > len(matchedVersion[j].Path)
|
||||
})
|
||||
return matchedVersion
|
||||
}
|
||||
|
||||
subSchema := schemas.Schema(version, parts[3])
|
||||
if subSchema == nil {
|
||||
break
|
||||
}
|
||||
func parseVersionAndSubContext(schemas *types.Schemas, path string) (*types.APIVersion, string, []string, map[string]string) {
|
||||
versions := versionsForPath(schemas, path)
|
||||
if len(versions) == 0 {
|
||||
return nil, "", nil, nil
|
||||
}
|
||||
version := &versions[0]
|
||||
|
||||
result[resourceType] = resourceID
|
||||
subContext = subContext + "/" + resourceType + "/" + resourceID
|
||||
parts = append(parts[:1], parts[3:]...)
|
||||
if strings.HasSuffix(path, "/") {
|
||||
path = path[:len(path)-1]
|
||||
}
|
||||
|
||||
return subContext, parts, result
|
||||
versionParts := strings.Split(version.Path, "/")
|
||||
pathParts := strings.Split(path, "/")
|
||||
paths := pathParts[len(versionParts):]
|
||||
|
||||
if !version.SubContext || len(versions) < 2 {
|
||||
return version, "", paths, nil
|
||||
}
|
||||
|
||||
if len(paths) < 2 {
|
||||
// Handle case like /v3/clusters/foo where /v3 and /v3/clusters are API versions.
|
||||
// In this situation you want the version to be /v3 and the path "clusters", "foo"
|
||||
return &versions[1], "", pathParts[len(versionParts)-1:], nil
|
||||
}
|
||||
|
||||
// Length is always >= 3
|
||||
|
||||
attrs := map[string]string{
|
||||
version.SubContextSchema: paths[0],
|
||||
}
|
||||
|
||||
for i, version := range versions {
|
||||
schema := schemas.Schema(&version, paths[1])
|
||||
if schema != nil {
|
||||
if i == 0 {
|
||||
break
|
||||
}
|
||||
return &version, "", paths[1:], attrs
|
||||
}
|
||||
}
|
||||
|
||||
return version, "/" + paths[0], paths[1:], attrs
|
||||
}
|
||||
|
||||
func DefaultResolver(typeName string, apiContext *types.APIContext) error {
|
||||
@ -223,20 +258,6 @@ func parseAction(url *url.URL) (string, string) {
|
||||
return action, ""
|
||||
}
|
||||
|
||||
func Version(schemas *types.Schemas, path string) *types.APIVersion {
|
||||
path = multiSlashRegexp.ReplaceAllString(path, "/")
|
||||
for _, version := range schemas.Versions() {
|
||||
if version.Path == "" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(path, version.Path) {
|
||||
return &version
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Body(req *http.Request) (map[string]interface{}, error) {
|
||||
req.ParseMultipartForm(maxFormSize)
|
||||
if req.MultipartForm != nil {
|
||||
|
@ -29,8 +29,8 @@ func (d *DefaultSubContextAttributeProvider) Create(apiContext *types.APIContext
|
||||
func (d *DefaultSubContextAttributeProvider) create(apiContext *types.APIContext, schema *types.Schema) map[string]string {
|
||||
result := map[string]string{}
|
||||
|
||||
for subContext, value := range apiContext.SubContext {
|
||||
subContextSchema := apiContext.Schemas.SubContext(subContext)
|
||||
for subContextSchemaID, value := range apiContext.SubContext {
|
||||
subContextSchema := apiContext.Schemas.Schema(nil, subContextSchemaID)
|
||||
if subContextSchema == nil {
|
||||
continue
|
||||
}
|
||||
|
@ -34,14 +34,10 @@ func Handler(apiContext *types.APIContext, _ types.RequestHandler) error {
|
||||
}
|
||||
|
||||
func getMatchingSchemas(apiContext *types.APIContext) []*types.Schema {
|
||||
apiVersions := apiContext.Request.URL.Query()["apiVersions"]
|
||||
resourceTypes := apiContext.Request.URL.Query()["resourceTypes"]
|
||||
|
||||
var schemas []*types.Schema
|
||||
for _, schema := range apiContext.Schemas.Schemas() {
|
||||
if !matches(apiVersions, schema.Version.Path) {
|
||||
continue
|
||||
}
|
||||
for _, schema := range apiContext.Schemas.SchemasForVersion(*apiContext.Version) {
|
||||
if !matches(resourceTypes, schema.ID) {
|
||||
continue
|
||||
}
|
||||
@ -151,9 +147,14 @@ func streamStore(ctx context.Context, eg *errgroup.Group, apiContext *types.APIC
|
||||
opts := parse.QueryOptions(apiContext, schema)
|
||||
events, err := schema.Store.Watch(apiContext, schema, &opts)
|
||||
if err != nil || events == nil {
|
||||
if err != nil {
|
||||
logrus.Errorf("failed on subscribe %s: %v", schema.ID, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Debugf("watching %s", schema.ID)
|
||||
|
||||
for e := range events {
|
||||
result <- e
|
||||
}
|
||||
|
@ -1,136 +0,0 @@
|
||||
package crd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/rancher/norman/store/proxy"
|
||||
"github.com/rancher/norman/types"
|
||||
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
|
||||
apiextclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
type Store struct {
|
||||
Factory *Factory
|
||||
k8sClient rest.Interface
|
||||
schemaStores map[string]types.Store
|
||||
}
|
||||
|
||||
func NewCRDStoreFromConfig(config rest.Config) (*Store, error) {
|
||||
dynamicConfig := config
|
||||
if dynamicConfig.NegotiatedSerializer == nil {
|
||||
configConfig := dynamic.ContentConfig()
|
||||
dynamicConfig.NegotiatedSerializer = configConfig.NegotiatedSerializer
|
||||
}
|
||||
|
||||
k8sClient, err := rest.UnversionedRESTClientFor(&dynamicConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
apiExtClient, err := clientset.NewForConfig(&dynamicConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewCRDStoreFromClients(apiExtClient, k8sClient), nil
|
||||
}
|
||||
|
||||
func NewCRDStoreFromClients(apiExtClientSet apiextclientset.Interface, k8sClient rest.Interface) *Store {
|
||||
return &Store{
|
||||
Factory: &Factory{
|
||||
APIExtClientSet: apiExtClientSet,
|
||||
},
|
||||
k8sClient: k8sClient,
|
||||
schemaStores: map[string]types.Store{},
|
||||
}
|
||||
}
|
||||
|
||||
func key(schema *types.Schema) string {
|
||||
if !strings.EqualFold(schema.BaseType, schema.ID) {
|
||||
return schema.Version.Path + "/" + schema.BaseType
|
||||
}
|
||||
|
||||
return schema.Version.Path + "/" + schema.ID
|
||||
}
|
||||
|
||||
func (c *Store) ByID(apiContext *types.APIContext, schema *types.Schema, id string) (map[string]interface{}, error) {
|
||||
store, ok := c.schemaStores[key(schema)]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
return store.ByID(apiContext, schema, id)
|
||||
}
|
||||
|
||||
func (c *Store) Delete(apiContext *types.APIContext, schema *types.Schema, id string) (map[string]interface{}, error) {
|
||||
store, ok := c.schemaStores[key(schema)]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
return store.Delete(apiContext, schema, id)
|
||||
}
|
||||
|
||||
func (c *Store) List(apiContext *types.APIContext, schema *types.Schema, opt *types.QueryOptions) ([]map[string]interface{}, error) {
|
||||
store, ok := c.schemaStores[key(schema)]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
return store.List(apiContext, schema, opt)
|
||||
}
|
||||
|
||||
func (c *Store) Watch(apiContext *types.APIContext, schema *types.Schema, opt *types.QueryOptions) (chan map[string]interface{}, error) {
|
||||
store, ok := c.schemaStores[key(schema)]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
return store.Watch(apiContext, schema, opt)
|
||||
}
|
||||
|
||||
func (c *Store) Update(apiContext *types.APIContext, schema *types.Schema, data map[string]interface{}, id string) (map[string]interface{}, error) {
|
||||
store, ok := c.schemaStores[key(schema)]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
return store.Update(apiContext, schema, data, id)
|
||||
}
|
||||
|
||||
func (c *Store) Create(apiContext *types.APIContext, schema *types.Schema, data map[string]interface{}) (map[string]interface{}, error) {
|
||||
store, ok := c.schemaStores[key(schema)]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
return store.Create(apiContext, schema, data)
|
||||
}
|
||||
|
||||
func (c *Store) AddSchemas(ctx context.Context, schemas ...*types.Schema) error {
|
||||
schemaStatus := map[*types.Schema]*apiext.CustomResourceDefinition{}
|
||||
var allSchemas []*types.Schema
|
||||
|
||||
for _, schema := range schemas {
|
||||
if schema.Store != nil || !schema.CanList(nil) || !strings.EqualFold(schema.BaseType, schema.ID) {
|
||||
continue
|
||||
}
|
||||
|
||||
schema.Store = c
|
||||
allSchemas = append(allSchemas, schema)
|
||||
}
|
||||
|
||||
schemaStatus, err := c.Factory.AddSchemas(ctx, allSchemas...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for schema, crd := range schemaStatus {
|
||||
c.schemaStores[key(schema)] = proxy.NewProxyStore(c.k8sClient,
|
||||
[]string{"apis"},
|
||||
crd.Spec.Group,
|
||||
crd.Spec.Version,
|
||||
crd.Status.AcceptedNames.Kind,
|
||||
crd.Status.AcceptedNames.Plural)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -5,6 +5,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"fmt"
|
||||
|
||||
"github.com/rancher/norman/store/proxy"
|
||||
"github.com/rancher/norman/types"
|
||||
"github.com/rancher/norman/types/convert"
|
||||
"github.com/sirupsen/logrus"
|
||||
@ -13,48 +16,58 @@ import (
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
type Factory struct {
|
||||
APIExtClientSet clientset.Interface
|
||||
ClientGetter proxy.ClientGetter
|
||||
}
|
||||
|
||||
func NewFactoryFromConfig(config rest.Config) (*Factory, error) {
|
||||
dynamicConfig := config
|
||||
if dynamicConfig.NegotiatedSerializer == nil {
|
||||
configConfig := dynamic.ContentConfig()
|
||||
dynamicConfig.NegotiatedSerializer = configConfig.NegotiatedSerializer
|
||||
func (c *Factory) AssignStores(ctx context.Context, storageContext types.StorageContext, schemas ...*types.Schema) error {
|
||||
schemaStatus, err := c.CreateCRDs(ctx, storageContext, schemas...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
apiExtClient, err := clientset.NewForConfig(&dynamicConfig)
|
||||
for _, schema := range schemas {
|
||||
crd, ok := schemaStatus[schema]
|
||||
if !ok {
|
||||
return fmt.Errorf("failed to create create/find CRD for %s", schema.ID)
|
||||
}
|
||||
|
||||
schema.Store = proxy.NewProxyStore(c.ClientGetter,
|
||||
storageContext,
|
||||
[]string{"apis"},
|
||||
crd.Spec.Group,
|
||||
crd.Spec.Version,
|
||||
crd.Status.AcceptedNames.Kind,
|
||||
crd.Status.AcceptedNames.Plural)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Factory) CreateCRDs(ctx context.Context, storageContext types.StorageContext, schemas ...*types.Schema) (map[*types.Schema]*apiext.CustomResourceDefinition, error) {
|
||||
schemaStatus := map[*types.Schema]*apiext.CustomResourceDefinition{}
|
||||
|
||||
apiClient, err := c.ClientGetter.APIExtClient(nil, storageContext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Factory{
|
||||
APIExtClientSet: apiExtClient,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Factory) AddSchemas(ctx context.Context, schemas ...*types.Schema) (map[*types.Schema]*apiext.CustomResourceDefinition, error) {
|
||||
schemaStatus := map[*types.Schema]*apiext.CustomResourceDefinition{}
|
||||
|
||||
ready, err := c.getReadyCRDs()
|
||||
ready, err := c.getReadyCRDs(apiClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, schema := range schemas {
|
||||
crd, err := c.createCRD(schema, ready)
|
||||
crd, err := c.createCRD(apiClient, schema, ready)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
schemaStatus[schema] = crd
|
||||
}
|
||||
|
||||
ready, err = c.getReadyCRDs()
|
||||
ready, err = c.getReadyCRDs(apiClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -63,7 +76,7 @@ func (c *Factory) AddSchemas(ctx context.Context, schemas ...*types.Schema) (map
|
||||
if readyCrd, ok := ready[crd.Name]; ok {
|
||||
schemaStatus[schema] = readyCrd
|
||||
} else {
|
||||
if err := c.waitCRD(ctx, crd.Name, schema, schemaStatus); err != nil {
|
||||
if err := c.waitCRD(ctx, apiClient, crd.Name, schema, schemaStatus); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@ -72,7 +85,7 @@ func (c *Factory) AddSchemas(ctx context.Context, schemas ...*types.Schema) (map
|
||||
return schemaStatus, nil
|
||||
}
|
||||
|
||||
func (c *Factory) waitCRD(ctx context.Context, crdName string, schema *types.Schema, schemaStatus map[*types.Schema]*apiext.CustomResourceDefinition) error {
|
||||
func (c *Factory) waitCRD(ctx context.Context, apiClient clientset.Interface, crdName string, schema *types.Schema, schemaStatus map[*types.Schema]*apiext.CustomResourceDefinition) error {
|
||||
logrus.Infof("Waiting for CRD %s to become available", crdName)
|
||||
defer logrus.Infof("Done waiting for CRD %s to become available", crdName)
|
||||
|
||||
@ -83,7 +96,7 @@ func (c *Factory) waitCRD(ctx context.Context, crdName string, schema *types.Sch
|
||||
}
|
||||
first = false
|
||||
|
||||
crd, err := c.APIExtClientSet.ApiextensionsV1beta1().CustomResourceDefinitions().Get(crdName, metav1.GetOptions{})
|
||||
crd, err := apiClient.ApiextensionsV1beta1().CustomResourceDefinitions().Get(crdName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -106,7 +119,7 @@ func (c *Factory) waitCRD(ctx context.Context, crdName string, schema *types.Sch
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Factory) createCRD(schema *types.Schema, ready map[string]*apiext.CustomResourceDefinition) (*apiext.CustomResourceDefinition, error) {
|
||||
func (c *Factory) createCRD(apiClient clientset.Interface, schema *types.Schema, ready map[string]*apiext.CustomResourceDefinition) (*apiext.CustomResourceDefinition, error) {
|
||||
plural := strings.ToLower(schema.PluralName)
|
||||
name := strings.ToLower(plural + "." + schema.Version.Group)
|
||||
|
||||
@ -136,15 +149,15 @@ func (c *Factory) createCRD(schema *types.Schema, ready map[string]*apiext.Custo
|
||||
}
|
||||
|
||||
logrus.Infof("Creating CRD %s", name)
|
||||
crd, err := c.APIExtClientSet.ApiextensionsV1beta1().CustomResourceDefinitions().Create(crd)
|
||||
crd, err := apiClient.ApiextensionsV1beta1().CustomResourceDefinitions().Create(crd)
|
||||
if errors.IsAlreadyExists(err) {
|
||||
return crd, nil
|
||||
}
|
||||
return crd, err
|
||||
}
|
||||
|
||||
func (c *Factory) getReadyCRDs() (map[string]*apiext.CustomResourceDefinition, error) {
|
||||
list, err := c.APIExtClientSet.ApiextensionsV1beta1().CustomResourceDefinitions().List(metav1.ListOptions{})
|
||||
func (c *Factory) getReadyCRDs(apiClient clientset.Interface) (map[string]*apiext.CustomResourceDefinition, error) {
|
||||
list, err := apiClient.ApiextensionsV1beta1().CustomResourceDefinitions().List(metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -7,6 +7,10 @@ import (
|
||||
type Store struct {
|
||||
}
|
||||
|
||||
func (e *Store) Context() types.StorageContext {
|
||||
return types.DefaultStorageContext
|
||||
}
|
||||
|
||||
func (e *Store) Delete(apiContext *types.APIContext, schema *types.Schema, id string) (map[string]interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
@ -8,6 +8,8 @@ import (
|
||||
"github.com/rancher/norman/types"
|
||||
"github.com/rancher/norman/types/convert"
|
||||
"github.com/rancher/norman/types/values"
|
||||
"github.com/sirupsen/logrus"
|
||||
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
@ -28,8 +30,57 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
type ClientGetter interface {
|
||||
Config(apiContext *types.APIContext, context types.StorageContext) (rest.Config, error)
|
||||
UnversionedClient(apiContext *types.APIContext, context types.StorageContext) (rest.Interface, error)
|
||||
APIExtClient(apiContext *types.APIContext, context types.StorageContext) (clientset.Interface, error)
|
||||
}
|
||||
|
||||
type simpleClientGetter struct {
|
||||
restConfig rest.Config
|
||||
client rest.Interface
|
||||
apiExtClient clientset.Interface
|
||||
}
|
||||
|
||||
func NewClientGetterFromConfig(config rest.Config) (ClientGetter, error) {
|
||||
dynamicConfig := config
|
||||
if dynamicConfig.NegotiatedSerializer == nil {
|
||||
configConfig := dynamic.ContentConfig()
|
||||
dynamicConfig.NegotiatedSerializer = configConfig.NegotiatedSerializer
|
||||
}
|
||||
|
||||
unversionedClient, err := rest.UnversionedRESTClientFor(&dynamicConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
apiExtClient, err := clientset.NewForConfig(&dynamicConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &simpleClientGetter{
|
||||
restConfig: config,
|
||||
client: unversionedClient,
|
||||
apiExtClient: apiExtClient,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *simpleClientGetter) Config(apiContext *types.APIContext, context types.StorageContext) (rest.Config, error) {
|
||||
return s.restConfig, nil
|
||||
}
|
||||
|
||||
func (s *simpleClientGetter) UnversionedClient(apiContext *types.APIContext, context types.StorageContext) (rest.Interface, error) {
|
||||
return s.client, nil
|
||||
}
|
||||
|
||||
func (s *simpleClientGetter) APIExtClient(apiContext *types.APIContext, context types.StorageContext) (clientset.Interface, error) {
|
||||
return s.apiExtClient, nil
|
||||
}
|
||||
|
||||
type Store struct {
|
||||
k8sClient rest.Interface
|
||||
clientGetter ClientGetter
|
||||
storageContext types.StorageContext
|
||||
prefix []string
|
||||
group string
|
||||
version string
|
||||
@ -38,11 +89,12 @@ type Store struct {
|
||||
authContext map[string]string
|
||||
}
|
||||
|
||||
func NewProxyStore(k8sClient rest.Interface,
|
||||
func NewProxyStore(clientGetter ClientGetter, storageContext types.StorageContext,
|
||||
prefix []string, group, version, kind, resourcePlural string) types.Store {
|
||||
return &errorStore{
|
||||
Store: &Store{
|
||||
k8sClient: k8sClient,
|
||||
clientGetter: clientGetter,
|
||||
storageContext: storageContext,
|
||||
prefix: prefix,
|
||||
group: group,
|
||||
version: version,
|
||||
@ -56,10 +108,11 @@ func NewProxyStore(k8sClient rest.Interface,
|
||||
}
|
||||
}
|
||||
|
||||
func NewRawProxyStore(k8sClient rest.Interface,
|
||||
func NewRawProxyStore(clientGetter ClientGetter, storageContext types.StorageContext,
|
||||
prefix []string, group, version, kind, resourcePlural string) *Store {
|
||||
return &Store{
|
||||
k8sClient: k8sClient,
|
||||
clientGetter: clientGetter,
|
||||
storageContext: storageContext,
|
||||
prefix: prefix,
|
||||
group: group,
|
||||
version: version,
|
||||
@ -83,6 +136,10 @@ func (p *Store) doAuthed(apiContext *types.APIContext, request *rest.Request) re
|
||||
return request.Do()
|
||||
}
|
||||
|
||||
func (p *Store) k8sClient(apiContext *types.APIContext) (rest.Interface, error) {
|
||||
return p.clientGetter.UnversionedClient(apiContext, p.storageContext)
|
||||
}
|
||||
|
||||
func (p *Store) ByID(apiContext *types.APIContext, schema *types.Schema, id string) (map[string]interface{}, error) {
|
||||
_, result, err := p.byID(apiContext, schema, id)
|
||||
return result, err
|
||||
@ -91,19 +148,33 @@ func (p *Store) ByID(apiContext *types.APIContext, schema *types.Schema, id stri
|
||||
func (p *Store) byID(apiContext *types.APIContext, schema *types.Schema, id string) (string, map[string]interface{}, error) {
|
||||
namespace, id := splitID(id)
|
||||
|
||||
req := p.common(namespace, p.k8sClient.Get()).
|
||||
k8sClient, err := p.k8sClient(apiContext)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
req := p.common(namespace, k8sClient.Get()).
|
||||
Name(id)
|
||||
|
||||
return p.singleResult(apiContext, schema, req)
|
||||
}
|
||||
|
||||
func (p *Store) Context() types.StorageContext {
|
||||
return p.storageContext
|
||||
}
|
||||
|
||||
func (p *Store) List(apiContext *types.APIContext, schema *types.Schema, opt *types.QueryOptions) ([]map[string]interface{}, error) {
|
||||
namespace := getNamespace(apiContext, opt)
|
||||
|
||||
req := p.common(namespace, p.k8sClient.Get())
|
||||
k8sClient, err := p.k8sClient(apiContext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := p.common(namespace, k8sClient.Get())
|
||||
|
||||
resultList := &unstructured.UnstructuredList{}
|
||||
err := req.Do().Into(resultList)
|
||||
err = req.Do().Into(resultList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -120,8 +191,13 @@ func (p *Store) List(apiContext *types.APIContext, schema *types.Schema, opt *ty
|
||||
func (p *Store) Watch(apiContext *types.APIContext, schema *types.Schema, opt *types.QueryOptions) (chan map[string]interface{}, error) {
|
||||
namespace := getNamespace(apiContext, opt)
|
||||
|
||||
k8sClient, err := p.k8sClient(apiContext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
timeout := int64(60 * 60)
|
||||
req := p.common(namespace, p.k8sClient.Get())
|
||||
req := p.common(namespace, k8sClient.Get())
|
||||
req.VersionedParams(&metav1.ListOptions{
|
||||
Watch: true,
|
||||
TimeoutSeconds: &timeout,
|
||||
@ -139,6 +215,7 @@ func (p *Store) Watch(apiContext *types.APIContext, schema *types.Schema, opt *t
|
||||
|
||||
go func() {
|
||||
<-apiContext.Request.Context().Done()
|
||||
logrus.Debugf("stopping watcher for %s", schema.ID)
|
||||
watcher.Stop()
|
||||
}()
|
||||
|
||||
@ -152,6 +229,7 @@ func (p *Store) Watch(apiContext *types.APIContext, schema *types.Schema, opt *t
|
||||
}
|
||||
result <- apiContext.AccessControl.Filter(apiContext, data.Object, p.authContext)
|
||||
}
|
||||
logrus.Debugf("closing watcher for %s", schema.ID)
|
||||
close(result)
|
||||
}()
|
||||
|
||||
@ -196,7 +274,12 @@ func (p *Store) Create(apiContext *types.APIContext, schema *types.Schema, data
|
||||
}
|
||||
}
|
||||
|
||||
req := p.common(namespace, p.k8sClient.Post()).
|
||||
k8sClient, err := p.k8sClient(apiContext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := p.common(namespace, k8sClient.Post()).
|
||||
Body(&unstructured.Unstructured{
|
||||
Object: data,
|
||||
})
|
||||
@ -219,8 +302,13 @@ func (p *Store) toInternal(mapper types.Mapper, data map[string]interface{}) {
|
||||
}
|
||||
|
||||
func (p *Store) Update(apiContext *types.APIContext, schema *types.Schema, data map[string]interface{}, id string) (map[string]interface{}, error) {
|
||||
k8sClient, err := p.k8sClient(apiContext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
namespace, id := splitID(id)
|
||||
req := p.common(namespace, p.k8sClient.Get()).
|
||||
req := p.common(namespace, k8sClient.Get()).
|
||||
Name(id)
|
||||
|
||||
resourceVersion, existing, err := p.singleResultRaw(apiContext, schema, req)
|
||||
@ -235,7 +323,7 @@ func (p *Store) Update(apiContext *types.APIContext, schema *types.Schema, data
|
||||
values.PutValue(existing, namespace, "metadata", "namespace")
|
||||
values.PutValue(existing, id, "metadata", "name")
|
||||
|
||||
req = p.common(namespace, p.k8sClient.Put()).
|
||||
req = p.common(namespace, k8sClient.Put()).
|
||||
Body(&unstructured.Unstructured{
|
||||
Object: existing,
|
||||
}).
|
||||
@ -246,16 +334,21 @@ func (p *Store) Update(apiContext *types.APIContext, schema *types.Schema, data
|
||||
}
|
||||
|
||||
func (p *Store) Delete(apiContext *types.APIContext, schema *types.Schema, id string) (map[string]interface{}, error) {
|
||||
k8sClient, err := p.k8sClient(apiContext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
namespace, id := splitID(id)
|
||||
|
||||
prop := metav1.DeletePropagationForeground
|
||||
req := p.common(namespace, p.k8sClient.Delete()).
|
||||
req := p.common(namespace, k8sClient.Delete()).
|
||||
Body(&metav1.DeleteOptions{
|
||||
PropagationPolicy: &prop,
|
||||
}).
|
||||
Name(id)
|
||||
|
||||
err := p.doAuthed(apiContext, req).Error()
|
||||
err = p.doAuthed(apiContext, req).Error()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -18,29 +18,33 @@ type Store struct {
|
||||
StreamTransformer StreamTransformerFunc
|
||||
}
|
||||
|
||||
func (t *Store) ByID(apiContext *types.APIContext, schema *types.Schema, id string) (map[string]interface{}, error) {
|
||||
data, err := t.Store.ByID(apiContext, schema, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if t.Transformer == nil {
|
||||
return data, nil
|
||||
}
|
||||
return t.Transformer(apiContext, data)
|
||||
func (s *Store) Context() types.StorageContext {
|
||||
return s.Store.Context()
|
||||
}
|
||||
|
||||
func (t *Store) Watch(apiContext *types.APIContext, schema *types.Schema, opt *types.QueryOptions) (chan map[string]interface{}, error) {
|
||||
c, err := t.Store.Watch(apiContext, schema, opt)
|
||||
func (s *Store) ByID(apiContext *types.APIContext, schema *types.Schema, id string) (map[string]interface{}, error) {
|
||||
data, err := s.Store.ByID(apiContext, schema, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if s.Transformer == nil {
|
||||
return data, nil
|
||||
}
|
||||
return s.Transformer(apiContext, data)
|
||||
}
|
||||
|
||||
func (s *Store) Watch(apiContext *types.APIContext, schema *types.Schema, opt *types.QueryOptions) (chan map[string]interface{}, error) {
|
||||
c, err := s.Store.Watch(apiContext, schema, opt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if t.StreamTransformer != nil {
|
||||
return t.StreamTransformer(apiContext, c)
|
||||
if s.StreamTransformer != nil {
|
||||
return s.StreamTransformer(apiContext, c)
|
||||
}
|
||||
|
||||
return convert.Chan(c, func(data map[string]interface{}) map[string]interface{} {
|
||||
item, err := t.Transformer(apiContext, data)
|
||||
item, err := s.Transformer(apiContext, data)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@ -48,23 +52,23 @@ func (t *Store) Watch(apiContext *types.APIContext, schema *types.Schema, opt *t
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (t *Store) List(apiContext *types.APIContext, schema *types.Schema, opt *types.QueryOptions) ([]map[string]interface{}, error) {
|
||||
data, err := t.Store.List(apiContext, schema, opt)
|
||||
func (s *Store) List(apiContext *types.APIContext, schema *types.Schema, opt *types.QueryOptions) ([]map[string]interface{}, error) {
|
||||
data, err := s.Store.List(apiContext, schema, opt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if t.ListTransformer != nil {
|
||||
return t.ListTransformer(apiContext, data)
|
||||
if s.ListTransformer != nil {
|
||||
return s.ListTransformer(apiContext, data)
|
||||
}
|
||||
|
||||
if t.Transformer == nil {
|
||||
if s.Transformer == nil {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
var result []map[string]interface{}
|
||||
for _, item := range data {
|
||||
item, err := t.Transformer(apiContext, item)
|
||||
item, err := s.Transformer(apiContext, item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -76,28 +80,28 @@ func (t *Store) List(apiContext *types.APIContext, schema *types.Schema, opt *ty
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (t *Store) Create(apiContext *types.APIContext, schema *types.Schema, data map[string]interface{}) (map[string]interface{}, error) {
|
||||
data, err := t.Store.Create(apiContext, schema, data)
|
||||
func (s *Store) Create(apiContext *types.APIContext, schema *types.Schema, data map[string]interface{}) (map[string]interface{}, error) {
|
||||
data, err := s.Store.Create(apiContext, schema, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if t.Transformer == nil {
|
||||
if s.Transformer == nil {
|
||||
return data, nil
|
||||
}
|
||||
return t.Transformer(apiContext, data)
|
||||
return s.Transformer(apiContext, data)
|
||||
}
|
||||
|
||||
func (t *Store) Update(apiContext *types.APIContext, schema *types.Schema, data map[string]interface{}, id string) (map[string]interface{}, error) {
|
||||
data, err := t.Store.Update(apiContext, schema, data, id)
|
||||
func (s *Store) Update(apiContext *types.APIContext, schema *types.Schema, data map[string]interface{}, id string) (map[string]interface{}, error) {
|
||||
data, err := s.Store.Update(apiContext, schema, data, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if t.Transformer == nil {
|
||||
if s.Transformer == nil {
|
||||
return data, nil
|
||||
}
|
||||
return t.Transformer(apiContext, data)
|
||||
return s.Transformer(apiContext, data)
|
||||
}
|
||||
|
||||
func (t *Store) Delete(apiContext *types.APIContext, schema *types.Schema, id string) (map[string]interface{}, error) {
|
||||
return t.Store.Delete(apiContext, schema, id)
|
||||
func (s *Store) Delete(apiContext *types.APIContext, schema *types.Schema, id string) (map[string]interface{}, error) {
|
||||
return s.Store.Delete(apiContext, schema, id)
|
||||
}
|
||||
|
@ -16,6 +16,10 @@ type StoreWrapper struct {
|
||||
store types.Store
|
||||
}
|
||||
|
||||
func (s *StoreWrapper) Context() types.StorageContext {
|
||||
return s.store.Context()
|
||||
}
|
||||
|
||||
func (s *StoreWrapper) ByID(apiContext *types.APIContext, schema *types.Schema, id string) (map[string]interface{}, error) {
|
||||
data, err := s.store.ByID(apiContext, schema, id)
|
||||
if err != nil {
|
||||
|
@ -131,10 +131,6 @@ func (s *Schemas) MustCustomizeType(version *APIVersion, obj interface{}, f func
|
||||
|
||||
f(schema)
|
||||
|
||||
if schema.SubContext != "" {
|
||||
s.schemasBySubContext[schema.SubContext] = schema
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
|
@ -29,28 +29,26 @@ type BackReference struct {
|
||||
|
||||
type Schemas struct {
|
||||
sync.Mutex
|
||||
typeNames map[reflect.Type]string
|
||||
schemasByPath map[string]map[string]*Schema
|
||||
schemasBySubContext map[string]*Schema
|
||||
mappers map[string]map[string][]Mapper
|
||||
references map[string][]BackReference
|
||||
embedded map[string]*Schema
|
||||
DefaultMappers MappersFactory
|
||||
DefaultPostMappers MappersFactory
|
||||
versions []APIVersion
|
||||
schemas []*Schema
|
||||
AddHook SchemaHook
|
||||
errors []error
|
||||
typeNames map[reflect.Type]string
|
||||
schemasByPath map[string]map[string]*Schema
|
||||
mappers map[string]map[string][]Mapper
|
||||
references map[string][]BackReference
|
||||
embedded map[string]*Schema
|
||||
DefaultMappers MappersFactory
|
||||
DefaultPostMappers MappersFactory
|
||||
versions []APIVersion
|
||||
schemas []*Schema
|
||||
AddHook SchemaHook
|
||||
errors []error
|
||||
}
|
||||
|
||||
func NewSchemas() *Schemas {
|
||||
return &Schemas{
|
||||
typeNames: map[reflect.Type]string{},
|
||||
schemasByPath: map[string]map[string]*Schema{},
|
||||
schemasBySubContext: map[string]*Schema{},
|
||||
mappers: map[string]map[string][]Mapper{},
|
||||
references: map[string][]BackReference{},
|
||||
embedded: map[string]*Schema{},
|
||||
typeNames: map[reflect.Type]string{},
|
||||
schemasByPath: map[string]map[string]*Schema{},
|
||||
mappers: map[string]map[string][]Mapper{},
|
||||
references: map[string][]BackReference{},
|
||||
embedded: map[string]*Schema{},
|
||||
}
|
||||
}
|
||||
|
||||
@ -62,12 +60,6 @@ func (s *Schemas) Err() error {
|
||||
return NewErrors(s.errors)
|
||||
}
|
||||
|
||||
func (s *Schemas) SubContext(subContext string) *Schema {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
return s.schemasBySubContext[subContext]
|
||||
}
|
||||
|
||||
func (s *Schemas) AddSchemas(schema *Schemas) *Schemas {
|
||||
for _, schema := range schema.Schemas() {
|
||||
s.AddSchema(*schema)
|
||||
@ -86,8 +78,6 @@ func (s *Schemas) doRemoveSchema(schema Schema) *Schemas {
|
||||
|
||||
s.removeReferences(&schema)
|
||||
|
||||
delete(s.schemasBySubContext, schema.SubContext)
|
||||
|
||||
if schema.Embed {
|
||||
s.removeEmbed(&schema)
|
||||
}
|
||||
@ -145,10 +135,6 @@ func (s *Schemas) doAddSchema(schema Schema) *Schemas {
|
||||
}
|
||||
}
|
||||
|
||||
if schema.SubContext != "" {
|
||||
s.schemasBySubContext[schema.SubContext] = &schema
|
||||
}
|
||||
|
||||
if schema.Embed {
|
||||
s.embed(&schema)
|
||||
}
|
||||
@ -343,6 +329,16 @@ func (s *Schemas) doSchema(version *APIVersion, name string, lock bool) *Schema
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Schemas) SubContextVersionForSchema(schema *Schema) *APIVersion {
|
||||
fullName := fmt.Sprintf("%s/schemas/%s", schema.Version.Path, schema.ID)
|
||||
for _, version := range s.Versions() {
|
||||
if version.SubContextSchema == fullName {
|
||||
return &version
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type multiErrors struct {
|
||||
errors []error
|
||||
}
|
||||
|
@ -186,7 +186,12 @@ type URLBuilder interface {
|
||||
ResourceLinkByID(schema *Schema, id string) string
|
||||
}
|
||||
|
||||
type StorageContext string
|
||||
|
||||
var DefaultStorageContext StorageContext
|
||||
|
||||
type Store interface {
|
||||
Context() StorageContext
|
||||
ByID(apiContext *APIContext, schema *Schema, id string) (map[string]interface{}, error)
|
||||
List(apiContext *APIContext, schema *Schema, opt *QueryOptions) ([]map[string]interface{}, error)
|
||||
Create(apiContext *APIContext, schema *Schema, data map[string]interface{}) (map[string]interface{}, error)
|
||||
|
@ -69,10 +69,11 @@ type Resource struct {
|
||||
}
|
||||
|
||||
type APIVersion struct {
|
||||
Group string `json:"group,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
SubContexts map[string]bool `json:"subContext,omitempty"`
|
||||
Group string `json:"group,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
SubContext bool `json:"subContext,omitempty"`
|
||||
SubContextSchema string `json:"filterField,omitempty"`
|
||||
}
|
||||
|
||||
type Namespaced struct{}
|
||||
@ -90,7 +91,6 @@ type Schema struct {
|
||||
PkgName string `json:"-"`
|
||||
Type string `json:"type,omitempty"`
|
||||
BaseType string `json:"baseType,omitempty"`
|
||||
SubContext string `json:"-,omitempty"`
|
||||
Links map[string]string `json:"links"`
|
||||
Version APIVersion `json:"version"`
|
||||
PluralName string `json:"pluralName,omitempty"`
|
||||
|
@ -122,7 +122,7 @@ func (u *urlBuilder) Collection(schema *types.Schema, versionOverride *types.API
|
||||
}
|
||||
|
||||
func (u *urlBuilder) SubContextCollection(subContext *types.Schema, contextName string, schema *types.Schema) string {
|
||||
return u.constructBasicURL(schema.Version, subContext.SubContext, contextName, u.getPluralName(schema))
|
||||
return u.constructBasicURL(subContext.Version, subContext.PluralName, contextName, u.getPluralName(schema))
|
||||
}
|
||||
|
||||
func (u *urlBuilder) Version(version types.APIVersion) string {
|
||||
|
Loading…
Reference in New Issue
Block a user