mirror of
https://github.com/rancher/steve.git
synced 2025-08-02 07:12:36 +00:00
Refactor schema IDs and paths
This commit is contained in:
parent
0baa096865
commit
ad67c46055
86
pkg/controllers/schema/schemas.go
Normal file
86
pkg/controllers/schema/schemas.go
Normal file
@ -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
|
||||
}
|
@ -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
|
||||
|
@ -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 == "" {
|
23
pkg/resources/schema.go
Normal file
23
pkg/resources/schema.go
Normal file
@ -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
|
||||
}
|
83
pkg/resources/schema/collection.go
Normal file
83
pkg/resources/schema/collection.go
Normal file
@ -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
|
||||
}
|
||||
}
|
76
pkg/resources/schema/converter/discovery.go
Normal file
76
pkg/resources/schema/converter/discovery.go
Normal file
@ -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
|
||||
}
|
37
pkg/resources/schema/converter/k8stonorman.go
Normal file
37
pkg/resources/schema/converter/k8stonorman.go
Normal file
@ -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
|
||||
}
|
104
pkg/resources/schema/converter/openapi.go
Normal file
104
pkg/resources/schema/converter/openapi.go
Normal file
@ -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
|
||||
}
|
@ -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 {
|
112
pkg/resources/schema/factory.go
Normal file
112
pkg/resources/schema/factory.go
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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))
|
||||
}
|
||||
|
@ -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"],
|
||||
})
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user