From ad67c460557990c40a00d640479a4bfd0a431899 Mon Sep 17 00:00:00 2001 From: Darren Shepherd Date: Tue, 13 Aug 2019 16:36:03 -0700 Subject: [PATCH] Refactor schema IDs and paths --- pkg/controllers/schema/schemas.go | 86 ++++++ pkg/proxy/proxy.go | 30 +- pkg/resources/{ => common}/formatter.go | 11 +- pkg/resources/schema.go | 23 ++ pkg/resources/schema/collection.go | 83 +++++ pkg/resources/schema/converter/discovery.go | 76 +++++ pkg/resources/schema/converter/k8stonorman.go | 37 +++ pkg/resources/schema/converter/openapi.go | 104 +++++++ .../schema/defaultmapper.go} | 8 +- pkg/resources/schema/factory.go | 112 +++++++ pkg/schemas/rbac.go | 75 ----- pkg/schemas/schemas.go | 285 ------------------ pkg/server/api.go | 54 +--- pkg/server/routes.go | 21 +- pkg/server/server.go | 17 +- 15 files changed, 595 insertions(+), 427 deletions(-) create mode 100644 pkg/controllers/schema/schemas.go rename pkg/resources/{ => common}/formatter.go (61%) create mode 100644 pkg/resources/schema.go create mode 100644 pkg/resources/schema/collection.go create mode 100644 pkg/resources/schema/converter/discovery.go create mode 100644 pkg/resources/schema/converter/k8stonorman.go create mode 100644 pkg/resources/schema/converter/openapi.go rename pkg/{server/mapper.go => resources/schema/defaultmapper.go} (82%) create mode 100644 pkg/resources/schema/factory.go delete mode 100644 pkg/schemas/rbac.go delete mode 100644 pkg/schemas/schemas.go diff --git a/pkg/controllers/schema/schemas.go b/pkg/controllers/schema/schemas.go new file mode 100644 index 0000000..bbfa371 --- /dev/null +++ b/pkg/controllers/schema/schemas.go @@ -0,0 +1,86 @@ +package schema + +import ( + "context" + "sync" + "sync/atomic" + "time" + + schema2 "github.com/rancher/naok/pkg/resources/schema" + "github.com/rancher/naok/pkg/resources/schema/converter" + apiextcontrollerv1beta1 "github.com/rancher/wrangler-api/pkg/generated/controllers/apiextensions.k8s.io/v1beta1" + v1 "github.com/rancher/wrangler-api/pkg/generated/controllers/apiregistration.k8s.io/v1" + "github.com/sirupsen/logrus" + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + "k8s.io/client-go/discovery" + apiv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1" +) + +type handler struct { + sync.Mutex + + toSync int32 + schemas *schema2.Collection + client discovery.DiscoveryInterface +} + +func Register(ctx context.Context, + discovery discovery.DiscoveryInterface, + crd apiextcontrollerv1beta1.CustomResourceDefinitionController, + apiService v1.APIServiceController, + schemas *schema2.Collection) { + + h := &handler{ + client: discovery, + schemas: schemas, + } + + apiService.OnChange(ctx, "schema", h.OnChangeAPIService) + crd.OnChange(ctx, "schema", h.OnChangeCRD) +} + +func (h *handler) OnChangeCRD(key string, crd *v1beta1.CustomResourceDefinition) (*v1beta1.CustomResourceDefinition, error) { + return crd, h.queueRefresh() +} + +func (h *handler) OnChangeAPIService(key string, api *apiv1.APIService) (*apiv1.APIService, error) { + return api, h.queueRefresh() +} + +func (h *handler) queueRefresh() error { + atomic.StoreInt32(&h.toSync, 1) + + go func() { + time.Sleep(500 * time.Millisecond) + if err := h.refreshAll(); err != nil { + logrus.Errorf("failed to sync schemas: %v", err) + atomic.StoreInt32(&h.toSync, 1) + } + }() + + return nil +} + +func (h *handler) refreshAll() error { + h.Lock() + defer h.Unlock() + + if !h.needToSync() { + return nil + } + + logrus.Info("Refreshing all schemas") + schemas, err := converter.ToSchemas(h.client) + if err != nil { + return err + } + + h.schemas.Reset(schemas) + + return nil +} + +func (h *handler) needToSync() bool { + old := atomic.SwapInt32(&h.toSync, 0) + return old == 1 +} diff --git a/pkg/proxy/proxy.go b/pkg/proxy/proxy.go index 4e1a9b4..e97044c 100644 --- a/pkg/proxy/proxy.go +++ b/pkg/proxy/proxy.go @@ -50,11 +50,35 @@ func Handler(prefix string, cfg *rest.Config) (http.Handler, error) { proxy.UpgradeTransport = upgradeTransport proxy.UseRequestLocation = true - if len(prefix) > 2 { - return stripLeaveSlash(prefix, proxy), nil + handler := setHost(target.Host, proxy) + + if len(target.Path) > 1 { + handler = prependPath(target.Path[:len(target.Path)-1], handler) } - return proxy, nil + if len(prefix) > 2 { + return stripLeaveSlash(prefix, handler), nil + } + + return handler, nil +} + +func setHost(host string, h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + req.Host = host + h.ServeHTTP(w, req) + }) +} + +func prependPath(prefix string, h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + if len(req.URL.Path) > 1 { + req.URL.Path = prefix + req.URL.Path + } else { + req.URL.Path = prefix + } + h.ServeHTTP(w, req) + }) } // like http.StripPrefix, but always leaves an initial slash. (so that our diff --git a/pkg/resources/formatter.go b/pkg/resources/common/formatter.go similarity index 61% rename from pkg/resources/formatter.go rename to pkg/resources/common/formatter.go index 96768f2..97bba1f 100644 --- a/pkg/resources/formatter.go +++ b/pkg/resources/common/formatter.go @@ -1,11 +1,20 @@ -package resources +package common import ( + "github.com/rancher/naok/pkg/resources/schema" + "github.com/rancher/norman/pkg/store/proxy" "github.com/rancher/norman/pkg/types" "github.com/rancher/norman/pkg/types/convert" "github.com/rancher/norman/pkg/types/values" ) +func Register(collection *schema.Collection, clientGetter proxy.ClientGetter) { + collection.AddTemplate(&schema.Template{ + Store: proxy.NewProxyStore(clientGetter), + Formatter: Formatter, + }) +} + func Formatter(request *types.APIRequest, resource *types.RawResource) { selfLink := convert.ToString(values.GetValueN(resource.Values, "metadata", "selfLink")) if selfLink == "" { diff --git a/pkg/resources/schema.go b/pkg/resources/schema.go new file mode 100644 index 0000000..1c9e66b --- /dev/null +++ b/pkg/resources/schema.go @@ -0,0 +1,23 @@ +package resources + +import ( + "github.com/rancher/naok/pkg/accesscontrol" + "github.com/rancher/naok/pkg/counts" + "github.com/rancher/naok/pkg/resources/common" + "github.com/rancher/naok/pkg/resources/schema" + "github.com/rancher/norman/pkg/store/proxy" + "github.com/rancher/norman/pkg/subscribe" + "github.com/rancher/norman/pkg/types" +) + +func SchemaFactory(getter proxy.ClientGetter, as *accesscontrol.AccessStore) *schema.Collection { + baseSchema := types.EmptySchemas() + collection := schema.NewCollection(baseSchema, as) + + counts.Register(baseSchema) + subscribe.Register(baseSchema) + + common.Register(collection, getter) + + return collection +} diff --git a/pkg/resources/schema/collection.go b/pkg/resources/schema/collection.go new file mode 100644 index 0000000..0d23009 --- /dev/null +++ b/pkg/resources/schema/collection.go @@ -0,0 +1,83 @@ +package schema + +import ( + "strings" + + "github.com/rancher/naok/pkg/accesscontrol" + "github.com/rancher/naok/pkg/attributes" + "github.com/rancher/norman/pkg/types" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/authentication/user" +) + +type Factory interface { + Schemas(user user.Info) (*types.Schemas, error) + ByGVR(gvr schema.GroupVersionResource) string +} + +type Collection struct { + toSync int32 + baseSchema *types.Schemas + schemas map[string]*types.Schema + templates map[string]*Template + byGVR map[schema.GroupVersionResource]string + + as *accesscontrol.AccessStore +} + +type Template struct { + Group string + Kind string + ID string + Formatter types.Formatter + Store types.Store + Mapper types.Mapper +} + +func NewCollection(baseSchema *types.Schemas, access *accesscontrol.AccessStore) *Collection { + return &Collection{ + baseSchema: baseSchema, + schemas: map[string]*types.Schema{}, + templates: map[string]*Template{}, + byGVR: map[schema.GroupVersionResource]string{}, + as: access, + } +} + +func (c *Collection) Reset(schemas map[string]*types.Schema) { + byGVR := map[schema.GroupVersionResource]string{} + + for _, s := range schemas { + gvr := attributes.GVR(s) + if gvr.Resource != "" { + gvr.Resource = strings.ToLower(gvr.Resource) + byGVR[gvr] = s.ID + } + + kind := attributes.Kind(s) + if kind != "" { + gvr.Resource = strings.ToLower(kind) + byGVR[gvr] = s.ID + } + } + + c.schemas = schemas + c.byGVR = byGVR +} + +func (c *Collection) ByGVR(gvr schema.GroupVersionResource) string { + gvr.Resource = strings.ToLower(gvr.Resource) + return c.byGVR[gvr] +} + +func (c *Collection) AddTemplate(template *Template) { + if template.Kind != "" { + c.templates[template.Group+"/"+template.Kind] = template + } + if template.ID != "" { + c.templates[template.ID] = template + } + if template.Kind == "" && template.Group == "" && template.ID == "" { + c.templates[""] = template + } +} diff --git a/pkg/resources/schema/converter/discovery.go b/pkg/resources/schema/converter/discovery.go new file mode 100644 index 0000000..71d6faa --- /dev/null +++ b/pkg/resources/schema/converter/discovery.go @@ -0,0 +1,76 @@ +package converter + +import ( + "strings" + + "github.com/rancher/naok/pkg/attributes" + "github.com/rancher/norman/pkg/types" + "github.com/rancher/wrangler/pkg/merr" + "github.com/sirupsen/logrus" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/discovery" +) + +func AddDiscovery(client discovery.DiscoveryInterface, schemas map[string]*types.Schema) error { + logrus.Info("Refreshing all schemas") + + _, resourceLists, err := client.ServerGroupsAndResources() + if err != nil { + return err + } + + var errs []error + for _, resourceList := range resourceLists { + gv, err := schema.ParseGroupVersion(resourceList.GroupVersion) + if err != nil { + errs = append(errs, err) + } + + if err := refresh(gv, resourceList, schemas); err != nil { + errs = append(errs, err) + } + } + + return merr.NewErrors(errs...) +} + +func refresh(gv schema.GroupVersion, resources *metav1.APIResourceList, schemas map[string]*types.Schema) error { + for _, resource := range resources.APIResources { + if strings.Contains(resource.Name, "/") { + continue + } + + gvk := schema.GroupVersionKind{ + Group: gv.Group, + Version: gv.Version, + Kind: resource.Kind, + } + + gvr := gvk.GroupVersion().WithResource(resource.Name) + + logrus.Infof("APIVersion %s/%s Kind %s", gvk.Group, gvk.Version, gvk.Kind) + + schema := schemas[gvkToSchemaID(gvk)] + if schema == nil { + schema = &types.Schema{ + Type: "schema", + Dynamic: true, + } + attributes.SetGVK(schema, gvk) + } + + schema.PluralName = resource.Name + attributes.SetAPIResource(schema, resource) + + // switch ID to be GVR, not GVK + if schema.ID != "" { + delete(schemas, schema.ID) + } + + schema.ID = GVRToSchemaID(gvr) + schemas[schema.ID] = schema + } + + return nil +} diff --git a/pkg/resources/schema/converter/k8stonorman.go b/pkg/resources/schema/converter/k8stonorman.go new file mode 100644 index 0000000..f5a74b8 --- /dev/null +++ b/pkg/resources/schema/converter/k8stonorman.go @@ -0,0 +1,37 @@ +package converter + +import ( + "fmt" + + "github.com/rancher/norman/pkg/types" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/discovery" +) + +func gvkToSchemaID(gvk schema.GroupVersionKind) string { + if gvk.Group == "" { + return fmt.Sprintf("apis/core/%s/%s", gvk.Version, gvk.Kind) + } + return fmt.Sprintf("apis/%s/%s/%s", gvk.Group, gvk.Version, gvk.Kind) +} + +func GVRToSchemaID(gvk schema.GroupVersionResource) string { + if gvk.Group == "" { + return fmt.Sprintf("apis/core/%s/%s", gvk.Version, gvk.Resource) + } + return fmt.Sprintf("apis/%s/%s/%s", gvk.Group, gvk.Version, gvk.Resource) +} + +func ToSchemas(client discovery.DiscoveryInterface) (map[string]*types.Schema, error) { + result := map[string]*types.Schema{} + + if err := AddOpenAPI(client, result); err != nil { + return nil, err + } + + if err := AddDiscovery(client, result); err != nil { + return nil, err + } + + return result, nil +} diff --git a/pkg/resources/schema/converter/openapi.go b/pkg/resources/schema/converter/openapi.go new file mode 100644 index 0000000..7651546 --- /dev/null +++ b/pkg/resources/schema/converter/openapi.go @@ -0,0 +1,104 @@ +package converter + +import ( + "github.com/rancher/naok/pkg/attributes" + "github.com/rancher/norman/pkg/types" + "github.com/rancher/norman/pkg/types/convert" + "github.com/sirupsen/logrus" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/discovery" + "k8s.io/kube-openapi/pkg/util/proto" +) + +func modelToSchema(modelName string, k *proto.Kind) *types.Schema { + s := types.Schema{ + ID: modelName, + Type: "schema", + ResourceFields: map[string]types.Field{}, + Attributes: map[string]interface{}{}, + Description: k.GetDescription(), + Dynamic: true, + } + + for fieldName, schemaField := range k.Fields { + s.ResourceFields[fieldName] = toField(schemaField) + } + + for _, fieldName := range k.RequiredFields { + if f, ok := s.ResourceFields[fieldName]; ok { + f.Required = true + s.ResourceFields[fieldName] = f + } + } + + if ms, ok := k.Extensions["x-kubernetes-group-version-kind"].([]interface{}); ok { + for _, mv := range ms { + if m, ok := mv.(map[interface{}]interface{}); ok { + gvk := schema.GroupVersionKind{ + Group: convert.ToString(m["group"]), + Version: convert.ToString(m["version"]), + Kind: convert.ToString(m["kind"]), + } + + s.ID = gvkToSchemaID(gvk) + attributes.SetGVK(&s, gvk) + } + } + } + + return &s +} + +func AddOpenAPI(client discovery.DiscoveryInterface, schemas map[string]*types.Schema) error { + openapi, err := client.OpenAPISchema() + if err != nil { + return err + } + + models, err := proto.NewOpenAPIData(openapi) + if err != nil { + return err + } + + for _, modelName := range models.ListModels() { + model := models.LookupModel(modelName) + if k, ok := model.(*proto.Kind); ok { + schema := modelToSchema(modelName, k) + schemas[schema.ID] = schema + } + } + + return nil +} + +func toField(schema proto.Schema) types.Field { + f := types.Field{ + Description: schema.GetDescription(), + Nullable: true, + Create: true, + Update: true, + } + switch v := schema.(type) { + case *proto.Array: + f.Type = "array[" + toField(v.SubType).Type + "]" + case *proto.Primitive: + if v.Type == "number" { + f.Type = "int" + } else { + f.Type = v.Type + } + case *proto.Map: + f.Type = "map[" + toField(v.SubType).Type + "]" + case *proto.Kind: + parts := v.Path.Get() + f.Type = parts[len(parts)-1] + case proto.Reference: + f.Type = v.SubSchema().GetPath().String() + case *proto.Arbitrary: + default: + logrus.Errorf("unknown type: %v", schema) + f.Type = "json" + } + + return f +} diff --git a/pkg/server/mapper.go b/pkg/resources/schema/defaultmapper.go similarity index 82% rename from pkg/server/mapper.go rename to pkg/resources/schema/defaultmapper.go index 832554a..16b399b 100644 --- a/pkg/server/mapper.go +++ b/pkg/resources/schema/defaultmapper.go @@ -1,4 +1,4 @@ -package server +package schema import ( "fmt" @@ -17,8 +17,10 @@ type defaultMapper struct { } func (d *defaultMapper) FromInternal(data map[string]interface{}) { - if t, ok := data["type"]; ok { - data["_type"] = t + if data["kind"] != "" && data["apiVersion"] != "" { + if t, ok := data["type"]; ok && data != nil { + data["_type"] = t + } } if _, ok := data["id"]; ok || data == nil { diff --git a/pkg/resources/schema/factory.go b/pkg/resources/schema/factory.go new file mode 100644 index 0000000..18f35f2 --- /dev/null +++ b/pkg/resources/schema/factory.go @@ -0,0 +1,112 @@ +package schema + +import ( + "fmt" + "net/http" + + "github.com/rancher/naok/pkg/accesscontrol" + "github.com/rancher/naok/pkg/attributes" + "github.com/rancher/norman/pkg/api/builtin" + "github.com/rancher/norman/pkg/types" + "k8s.io/apiserver/pkg/authentication/user" +) + +func newSchemas() (*types.Schemas, error) { + s, err := types.NewSchemas(builtin.Schemas) + if err != nil { + return nil, err + } + s.DefaultMapper = func() types.Mapper { + return newDefaultMapper() + } + + return s, nil +} + +func (c *Collection) Schemas(user user.Info) (*types.Schemas, error) { + access := c.as.AccessFor(user) + return c.schemasForSubject("", access) +} + +func (c *Collection) schemasForSubject(subjectKey string, access *accesscontrol.AccessSet) (*types.Schemas, error) { + result, err := newSchemas() + if err != nil { + return nil, err + } + + if _, err := result.AddSchemas(c.baseSchema); err != nil { + return nil, err + } + + for _, s := range c.schemas { + gr := attributes.GR(s) + + if gr.Resource == "" { + if err := result.AddSchema(*s); err != nil { + return nil, err + } + continue + } + + verbs := attributes.Verbs(s) + verbAccess := accesscontrol.AccessListMap{} + + for _, verb := range verbs { + a := access.AccessListFor(verb, gr) + if len(a) > 0 { + verbAccess[verb] = a + } + } + + if len(verbAccess) == 0 { + continue + } + + s = s.DeepCopy() + attributes.SetAccess(s, verbAccess) + if verbAccess.AnyVerb("list", "get") { + s.ResourceMethods = append(s.ResourceMethods, http.MethodGet) + s.CollectionMethods = append(s.CollectionMethods, http.MethodGet) + } + if verbAccess.AnyVerb("delete") { + s.ResourceMethods = append(s.ResourceMethods, http.MethodDelete) + } + if verbAccess.AnyVerb("update") { + s.ResourceMethods = append(s.ResourceMethods, http.MethodPut) + } + if verbAccess.AnyVerb("create") { + s.CollectionMethods = append(s.CollectionMethods, http.MethodPost) + } + + c.applyTemplates(s) + + if err := result.AddSchema(*s); err != nil { + return nil, err + } + } + + return result, nil +} + +func (c *Collection) applyTemplates(schema *types.Schema) { + templates := []*Template{ + c.templates[schema.ID], + c.templates[fmt.Sprintf("%s/%s", attributes.Group(schema), attributes.Kind(schema))], + c.templates[""], + } + + for _, t := range templates { + if t == nil { + continue + } + if schema.Mapper == nil { + schema.Mapper = t.Mapper + } + if schema.Formatter == nil { + schema.Formatter = t.Formatter + } + if schema.Store == nil { + schema.Store = t.Store + } + } +} diff --git a/pkg/schemas/rbac.go b/pkg/schemas/rbac.go deleted file mode 100644 index b3396d8..0000000 --- a/pkg/schemas/rbac.go +++ /dev/null @@ -1,75 +0,0 @@ -package schemas - -import ( - "net/http" - - "github.com/rancher/naok/pkg/accesscontrol" - "github.com/rancher/naok/pkg/attributes" - "github.com/rancher/norman/pkg/api/builtin" - "github.com/rancher/norman/pkg/types" -) - -func DefaultSchemaFactory() (*types.Schemas, error) { - return types.NewSchemas(builtin.Schemas) -} - -func (s *schemas) Schemas(subjectKey string, access *accesscontrol.AccessSet, schemasFactory func() (*types.Schemas, error)) (*types.Schemas, error) { - cached, ok := s.access.Load(subjectKey) - if ok { - return cached.(*types.Schemas), nil - } - - if schemasFactory == nil { - schemasFactory = DefaultSchemaFactory - } - - result, err := schemasFactory() - if err != nil { - return nil, err - } - - for _, s := range s.openSchemas { - if err := result.AddSchema(*s); err != nil { - return nil, err - } - } - - for _, s := range s.schemas { - gr := attributes.GR(s) - - verbs := attributes.Verbs(s) - verbAccess := accesscontrol.AccessListMap{} - - for _, verb := range verbs { - a := access.AccessListFor(verb, gr) - if len(a) > 0 { - verbAccess[verb] = a - } - } - - if len(verbAccess) == 0 { - continue - } - - s = s.DeepCopy() - attributes.SetAccess(s, verbAccess) - if verbAccess.AnyVerb("list", "get") { - s.ResourceMethods = append(s.ResourceMethods, http.MethodGet) - s.CollectionMethods = append(s.CollectionMethods, http.MethodGet) - } - if verbAccess.AnyVerb("delete") { - s.ResourceMethods = append(s.ResourceMethods, http.MethodDelete) - } - if verbAccess.AnyVerb("update") { - s.ResourceMethods = append(s.ResourceMethods, http.MethodPut) - } - if verbAccess.AnyVerb("create") { - s.CollectionMethods = append(s.CollectionMethods, http.MethodPost) - } - if err := result.AddSchema(*s); err != nil { - return nil, err - } - } - - return result, nil -} diff --git a/pkg/schemas/schemas.go b/pkg/schemas/schemas.go deleted file mode 100644 index 225b62e..0000000 --- a/pkg/schemas/schemas.go +++ /dev/null @@ -1,285 +0,0 @@ -package schemas - -import ( - "context" - "fmt" - "strings" - "sync" - "sync/atomic" - "time" - - openapi_v2 "github.com/googleapis/gnostic/OpenAPIv2" - "github.com/rancher/naok/pkg/accesscontrol" - "github.com/rancher/naok/pkg/attributes" - "github.com/rancher/naok/pkg/resources" - "github.com/rancher/norman/pkg/store/proxy" - "github.com/rancher/norman/pkg/types" - "github.com/rancher/norman/pkg/types/convert" - apiextcontrollerv1beta1 "github.com/rancher/wrangler-api/pkg/generated/controllers/apiextensions.k8s.io/v1beta1" - v1 "github.com/rancher/wrangler-api/pkg/generated/controllers/apiregistration.k8s.io/v1" - "github.com/rancher/wrangler/pkg/merr" - "github.com/sirupsen/logrus" - "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/discovery" - apiv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1" - "k8s.io/kube-openapi/pkg/util/proto" -) - -type SchemaFactory interface { - Schemas(subjectKey string, access *accesscontrol.AccessSet, schemasFactory func() (*types.Schemas, error)) (*types.Schemas, error) - ByGVR(gvr schema.GroupVersionResource) string -} - -type handler struct { - formatter types.Formatter - schemas *schemas - schemaStore types.Store - client discovery.DiscoveryInterface -} - -type schemas struct { - toSync int32 - - sync.Mutex - apiGroups []*metav1.APIGroup - gvkToName map[schema.GroupVersionKind]string - gvrToName map[schema.GroupVersionResource]string - openSchemas map[string]*types.Schema - schemas map[string]*types.Schema - access sync.Map -} - -func (s *schemas) reset() { - s.apiGroups = nil - s.gvkToName = map[schema.GroupVersionKind]string{} - s.gvrToName = map[schema.GroupVersionResource]string{} - s.openSchemas = map[string]*types.Schema{} - s.schemas = map[string]*types.Schema{} - s.access.Range(func(key, value interface{}) bool { - s.access.Delete(key) - return true - }) -} - -func (s *schemas) ByGVR(gvr schema.GroupVersionResource) string { - return s.gvrToName[gvr] -} - -func Register(ctx context.Context, clientGetter proxy.ClientGetter, discovery discovery.DiscoveryInterface, crd apiextcontrollerv1beta1.CustomResourceDefinitionController, - apiService v1.APIServiceController) SchemaFactory { - - h := &handler{ - formatter: resources.Formatter, - client: discovery, - schemas: &schemas{}, - schemaStore: proxy.NewProxyStore(clientGetter), - } - apiService.OnChange(ctx, "schema", h.OnChangeAPIService) - crd.OnChange(ctx, "schema", h.OnChangeCRD) - - return h.schemas -} - -func (h *handler) OnChangeCRD(key string, crd *v1beta1.CustomResourceDefinition) (*v1beta1.CustomResourceDefinition, error) { - return crd, h.queueRefresh() -} - -func (h *handler) OnChangeAPIService(key string, api *apiv1.APIService) (*apiv1.APIService, error) { - return api, h.queueRefresh() -} - -func schemaID(gvk schema.GroupVersionKind) string { - if gvk.Group == "" { - return fmt.Sprintf("io.k8s.api.core.%s.%s", gvk.Version, gvk.Kind) - } - return fmt.Sprintf("io.k8s.api.%s.%s.%s", gvk.Group, gvk.Version, gvk.Kind) -} - -func (h *handler) queueRefresh() error { - atomic.StoreInt32(&h.schemas.toSync, 1) - - go func() { - time.Sleep(500 * time.Millisecond) - if err := h.refreshAll(); err != nil { - logrus.Errorf("failed to sync schemas: %v", err) - } - }() - - return nil -} - -func (h *handler) refreshAll() error { - h.schemas.Lock() - defer h.schemas.Unlock() - - if !h.needToSync() { - return nil - } - - logrus.Info("Refreshing all schemas") - - groups, resourceLists, err := h.client.ServerGroupsAndResources() - if err != nil { - return err - } - - openapi, err := h.client.OpenAPISchema() - if err != nil { - return err - } - - h.schemas.reset() - h.schemas.apiGroups = groups - - if err := populate(openapi, h.schemas); err != nil { - return err - } - - var errs []error - for _, resourceList := range resourceLists { - gv, err := schema.ParseGroupVersion(resourceList.GroupVersion) - if err != nil { - errs = append(errs, err) - } - - if err := h.refresh(gv, resourceList); err != nil { - errs = append(errs, err) - } - } - - return merr.NewErrors(errs...) -} - -func (h *handler) needToSync() bool { - old := atomic.SwapInt32(&h.schemas.toSync, 0) - return old == 1 -} - -func (h *handler) refresh(gv schema.GroupVersion, resources *metav1.APIResourceList) error { - for _, resource := range resources.APIResources { - if strings.Contains(resource.Name, "/") { - continue - } - - gvk := schema.GroupVersionKind{ - Group: gv.Group, - Version: gv.Version, - Kind: resource.Kind, - } - gvr := gvk.GroupVersion().WithResource(resource.Name) - - logrus.Infof("APIVersion %s/%s Kind %s", gvk.Group, gvk.Version, gvk.Kind) - - schema := h.schemas.openSchemas[h.schemas.gvkToName[gvk]] - if schema == nil { - schema = &types.Schema{ - ID: schemaID(gvk), - Type: "schema", - Dynamic: true, - } - attributes.SetGVK(schema, gvk) - } - - schema.PluralName = resource.Name - attributes.SetAPIResource(schema, resource) - schema.Store = h.schemaStore - schema.Formatter = h.formatter - - h.schemas.schemas[schema.ID] = schema - h.schemas.gvkToName[gvk] = schema.ID - h.schemas.gvrToName[gvr] = schema.ID - } - - return nil -} - -func toField(schema proto.Schema) types.Field { - f := types.Field{ - Description: schema.GetDescription(), - Nullable: true, - Create: true, - Update: true, - } - switch v := schema.(type) { - case *proto.Array: - f.Type = "array[" + toField(v.SubType).Type + "]" - case *proto.Primitive: - if v.Type == "number" { - f.Type = "int" - } else { - f.Type = v.Type - } - case *proto.Map: - f.Type = "map[" + toField(v.SubType).Type + "]" - case *proto.Kind: - parts := v.Path.Get() - f.Type = parts[len(parts)-1] - case proto.Reference: - f.Type = v.SubSchema().GetPath().String() - case *proto.Arbitrary: - default: - logrus.Errorf("unknown type: %v", schema) - f.Type = "json" - } - - return f -} - -func modelToSchema(modelName string, k *proto.Kind, schemas *schemas) { - s := types.Schema{ - ID: modelName, - Type: "schema", - ResourceFields: map[string]types.Field{}, - Attributes: map[string]interface{}{}, - Description: k.GetDescription(), - Dynamic: true, - } - - for fieldName, schemaField := range k.Fields { - s.ResourceFields[fieldName] = toField(schemaField) - } - - for _, fieldName := range k.RequiredFields { - if f, ok := s.ResourceFields[fieldName]; ok { - f.Required = true - s.ResourceFields[fieldName] = f - } - } - - if ms, ok := k.Extensions["x-kubernetes-group-version-kind"].([]interface{}); ok { - for _, mv := range ms { - if m, ok := mv.(map[interface{}]interface{}); ok { - - gvk := schema.GroupVersionKind{ - Group: convert.ToString(m["group"]), - Version: convert.ToString(m["version"]), - Kind: convert.ToString(m["kind"]), - } - - attributes.SetGVK(&s, gvk) - - schemas.gvkToName[gvk] = s.ID - } - } - } - - schemas.openSchemas[s.ID] = &s -} - -func populate(openapi *openapi_v2.Document, schemas *schemas) error { - models, err := proto.NewOpenAPIData(openapi) - if err != nil { - return err - } - - for _, modelName := range models.ListModels() { - model := models.LookupModel(modelName) - if k, ok := model.(*proto.Kind); ok { - modelToSchema(modelName, k, schemas) - } - } - - return nil -} diff --git a/pkg/server/api.go b/pkg/server/api.go index 0901d34..a3cfac4 100644 --- a/pkg/server/api.go +++ b/pkg/server/api.go @@ -2,40 +2,31 @@ package server import ( "net/http" - - "github.com/rancher/naok/pkg/counts" + "strings" "github.com/gorilla/mux" "github.com/rancher/naok/pkg/accesscontrol" "github.com/rancher/naok/pkg/attributes" k8sproxy "github.com/rancher/naok/pkg/proxy" - "github.com/rancher/naok/pkg/schemas" + "github.com/rancher/naok/pkg/resources/schema" "github.com/rancher/norman/pkg/api" - "github.com/rancher/norman/pkg/store/proxy" - "github.com/rancher/norman/pkg/subscribe" "github.com/rancher/norman/pkg/types" "github.com/rancher/norman/pkg/urlbuilder" "k8s.io/apiserver/pkg/authentication/user" "k8s.io/client-go/rest" ) -func newAPIServer(cfg *rest.Config, cf proxy.ClientGetter, as *accesscontrol.AccessStore, sf schemas.SchemaFactory) (http.Handler, error) { +func newAPIServer(cfg *rest.Config, sf schema.Factory) (http.Handler, error) { var ( err error ) a := &apiServer{ - Router: mux.NewRouter(), - cf: cf, - as: as, - sf: sf, - server: api.NewAPIServer(), - baseSchemas: types.EmptySchemas(), + Router: mux.NewRouter(), + sf: sf, + server: api.NewAPIServer(), } - counts.Register(a.baseSchemas) - subscribe.Register(a.baseSchemas) - a.Router.NotFoundHandler, err = k8sproxy.Handler("/", cfg) if err != nil { return nil, err @@ -48,22 +39,8 @@ func newAPIServer(cfg *rest.Config, cf proxy.ClientGetter, as *accesscontrol.Acc type apiServer struct { *mux.Router - cf proxy.ClientGetter - as *accesscontrol.AccessStore - sf schemas.SchemaFactory - server *api.Server - baseSchemas *types.Schemas -} - -func (a *apiServer) newSchemas() (*types.Schemas, error) { - schemas, err := schemas.DefaultSchemaFactory() - if err != nil { - return nil, err - } - - schemas.DefaultMapper = newDefaultMapper - schemas.AddSchemas(a.baseSchemas) - return schemas, nil + sf schema.Factory + server *api.Server } func (a *apiServer) common(rw http.ResponseWriter, req *http.Request) (*types.APIRequest, bool) { @@ -72,8 +49,7 @@ func (a *apiServer) common(rw http.ResponseWriter, req *http.Request) (*types.AP Groups: []string{"system:masters"}, } - accessSet := a.as.AccessFor(user) - schemas, err := a.sf.Schemas("", accessSet, a.newSchemas) + schemas, err := a.sf.Schemas(user) if err != nil { rw.Write([]byte(err.Error())) rw.WriteHeader(http.StatusInternalServerError) @@ -96,14 +72,8 @@ func (a *apiServer) common(rw http.ResponseWriter, req *http.Request) (*types.AP func (a *apiServer) Schema(base string, schema *types.Schema) string { gvr := attributes.GVR(schema) - - if gvr.Group == "" && gvr.Version != "" && gvr.Resource != "" { - return urlbuilder.ConstructBasicURL(base, gvr.Version, gvr.Resource) + if gvr.Resource == "" { + return urlbuilder.ConstructBasicURL(base, "v1", schema.PluralName) } - - if gvr.Resource != "" { - return urlbuilder.ConstructBasicURL(base, "v1", "apis", gvr.Group, gvr.Version, gvr.Resource) - } - - return urlbuilder.ConstructBasicURL(base, "v1", schema.PluralName) + return urlbuilder.ConstructBasicURL(base, "v1", strings.ToLower(schema.ID)) } diff --git a/pkg/server/routes.go b/pkg/server/routes.go index b666d5d..0d4e056 100644 --- a/pkg/server/routes.go +++ b/pkg/server/routes.go @@ -12,18 +12,12 @@ import ( type APIFunc func(*types.APIRequest) func (a *apiServer) routes() error { - a.Path("/v1/{type:schemas}").Handler(a.handle(nil)) - a.Path("/v1/{type:schemas}/{name}").Handler(a.handle(nil)) - a.Path("/v1/{type:subscribe}").Handler(a.handle(nil)) - a.Path("/v1/{type:counts}").Handler(a.handle(nil)) - a.Path("/v1/{type:counts}/{name}").Handler(a.handle(nil)) - - a.Path("/{version:v1}/{resource}").Handler(a.handle(a.k8sAPI)) - a.Path("/{version:v1}/{resource}/{nameorns}").Handler(a.handle(a.k8sAPI)) - a.Path("/{version:v1}/{resource}/{namespace}/{name}").Handler(a.handle(a.k8sAPI)) - + a.Path("/v1/{type}").Handler(a.handle(nil)) + a.Path("/v1/{type:schemas}/{name:.*}").Handler(a.handle(nil)) + a.Path("/v1/{type}/{name}").Handler(a.handle(nil)) a.Path("/v1/apis/{group}/{version}/{resource}").Handler(a.handle(a.k8sAPI)) a.Path("/v1/apis/{group}/{version}/{resource}/{nameorns}").Handler(a.handle(a.k8sAPI)) + a.Path("/v1/apis/{group}/{version}/{resource}/{namespace}/{name}").Handler(a.handle(a.k8sAPI)) return nil } @@ -46,10 +40,15 @@ func (a *apiServer) api(rw http.ResponseWriter, req *http.Request, apiFunc APIFu func (a *apiServer) k8sAPI(apiOp *types.APIRequest) { vars := mux.Vars(apiOp.Request) + group := vars["group"] + if group == "core" { + group = "" + } + apiOp.Name = vars["name"] apiOp.Type = a.sf.ByGVR(schema.GroupVersionResource{ Version: vars["version"], - Group: vars["group"], + Group: group, Resource: vars["resource"], }) diff --git a/pkg/server/server.go b/pkg/server/server.go index 775631f..e77d995 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -4,9 +4,12 @@ import ( "context" "net/http" + "github.com/rancher/naok/pkg/resources" + + "github.com/rancher/naok/pkg/controllers/schema" + "github.com/rancher/naok/pkg/accesscontrol" "github.com/rancher/naok/pkg/client" - "github.com/rancher/naok/pkg/schemas" "github.com/rancher/wrangler-api/pkg/generated/controllers/apiextensions.k8s.io" "github.com/rancher/wrangler-api/pkg/generated/controllers/apiregistration.k8s.io" rbaccontroller "github.com/rancher/wrangler-api/pkg/generated/controllers/rbac" @@ -74,17 +77,17 @@ func startAPI(ctx context.Context, listenAddress string, restConfig *rest.Config return nil, err } - sf := schemas.Register(ctx, - cf, + as := accesscontrol.NewAccessStore(rbac.Rbac().V1()) + sf := resources.SchemaFactory(cf, as) + + schema.Register(ctx, k8s.Discovery(), crd.Apiextensions().V1beta1().CustomResourceDefinition(), api.Apiregistration().V1().APIService(), - ) - - as := accesscontrol.NewAccessStore(rbac.Rbac().V1()) + sf) return func() error { - handler, err := newAPIServer(restConfig, cf, as, sf) + handler, err := newAPIServer(restConfig, sf) if err != nil { return err }