This commit is contained in:
Darren Shepherd
2020-01-30 22:01:21 -07:00
parent 9184741c57
commit 19c6732de0
2380 changed files with 587 additions and 759101 deletions

121
pkg/schema/collection.go Normal file
View File

@@ -0,0 +1,121 @@
package schema
import (
"strings"
"github.com/rancher/norman/v2/pkg/data"
"github.com/rancher/norman/v2/pkg/types"
"github.com/rancher/steve/pkg/accesscontrol"
"github.com/rancher/steve/pkg/attributes"
"github.com/rancher/steve/pkg/table"
"github.com/rancher/wrangler/pkg/name"
"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
ByGVK(gvr schema.GroupVersionKind) string
}
type Collection struct {
toSync int32
baseSchema *types.Schemas
schemas map[string]*types.Schema
templates map[string]*Template
byGVR map[schema.GroupVersionResource]string
byGVK map[schema.GroupVersionKind]string
as *accesscontrol.AccessStore
}
type Template struct {
Group string
Kind string
ID string
RegisterType interface{}
Customize func(*types.Schema)
Formatter types.Formatter
Store types.Store
StoreFactory func(types.Store) types.Store
Mapper types.Mapper
Columns []table.Column
ComputedColumns func(data.Object)
}
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{},
byGVK: map[schema.GroupVersionKind]string{},
as: access,
}
}
func (c *Collection) Reset(schemas map[string]*types.Schema) {
byGVK := map[schema.GroupVersionKind]string{}
byGVR := map[schema.GroupVersionResource]string{}
for _, s := range schemas {
gvr := attributes.GVR(s)
if gvr.Resource != "" {
byGVR[gvr] = s.ID
}
gvk := attributes.GVK(s)
if gvk.Kind != "" {
byGVK[gvk] = s.ID
}
}
c.schemas = schemas
c.byGVR = byGVR
c.byGVK = byGVK
}
func (c *Collection) Schema(id string) *types.Schema {
return c.schemas[id]
}
func (c *Collection) IDs() (result []string) {
seen := map[string]bool{}
for _, id := range c.byGVR {
if seen[id] {
continue
}
seen[id] = true
result = append(result, id)
}
return
}
func (c *Collection) ByGVR(gvr schema.GroupVersionResource) string {
id, ok := c.byGVR[gvr]
if ok {
return id
}
gvr.Resource = name.GuessPluralName(strings.ToLower(gvr.Resource))
return c.byGVK[schema.GroupVersionKind{
Group: gvr.Group,
Version: gvr.Version,
Kind: gvr.Resource,
}]
}
func (c *Collection) ByGVK(gvk schema.GroupVersionKind) string {
return c.byGVK[gvk]
}
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
}
}

103
pkg/schema/converter/crd.go Normal file
View File

@@ -0,0 +1,103 @@
package converter
import (
"github.com/rancher/norman/v2/pkg/types"
"github.com/rancher/steve/pkg/attributes"
"github.com/rancher/steve/pkg/table"
"github.com/rancher/wrangler-api/pkg/generated/controllers/apiextensions.k8s.io/v1beta1"
beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
)
var (
staticFields = map[string]types.Field{
"apiVersion": {
Type: "string",
},
"kind": {
Type: "string",
},
"metadata": {
Type: "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta",
},
}
)
func AddCustomResources(crd v1beta1.CustomResourceDefinitionClient, schemas map[string]*types.Schema) error {
crds, err := crd.List(metav1.ListOptions{})
if err != nil {
return nil
}
for _, crd := range crds.Items {
if crd.Status.AcceptedNames.Plural == "" {
continue
}
var columns []table.Column
for _, col := range crd.Spec.AdditionalPrinterColumns {
columns = append(columns, table.Column{
Name: col.Name,
Field: col.JSONPath,
Type: col.Type,
})
}
group, kind := crd.Spec.Group, crd.Status.AcceptedNames.Kind
if crd.Spec.Version != "" {
forVersion(&crd, group, crd.Spec.Version, kind, schemas, crd.Spec.AdditionalPrinterColumns, columns)
}
for _, version := range crd.Spec.Versions {
forVersion(&crd, group, version.Name, kind, schemas, crd.Spec.AdditionalPrinterColumns, columns)
}
}
return nil
}
func forVersion(crd *beta1.CustomResourceDefinition, group, version, kind string, schemas map[string]*types.Schema, columnDefs []beta1.CustomResourceColumnDefinition, columns []table.Column) {
var versionColumns []table.Column
for _, col := range columnDefs {
versionColumns = append(versionColumns, table.Column{
Name: col.Name,
Field: col.JSONPath,
Type: col.Type,
Format: col.Format,
})
}
if len(versionColumns) == 0 {
versionColumns = columns
}
id := GVKToSchemaID(schema.GroupVersionKind{
Group: group,
Version: version,
Kind: kind,
})
schema := schemas[id]
if schema == nil {
return
}
if len(columns) > 0 {
attributes.SetColumns(schema, columns)
}
if crd.Spec.Validation != nil && crd.Spec.Validation.OpenAPIV3Schema != nil {
if fieldsSchema := modelV3ToSchema(id, crd.Spec.Validation.OpenAPIV3Schema, schemas); fieldsSchema != nil {
for k, v := range staticFields {
fieldsSchema.ResourceFields[k] = v
}
for k, v := range fieldsSchema.ResourceFields {
if schema.ResourceFields == nil {
schema.ResourceFields = map[string]types.Field{}
}
if _, ok := schema.ResourceFields[k]; !ok {
schema.ResourceFields[k] = v
}
}
}
}
}

View File

@@ -0,0 +1,92 @@
package converter
import (
"strings"
"github.com/rancher/norman/v2/pkg/types"
"github.com/rancher/steve/pkg/attributes"
"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"
)
var (
preferredGroups = map[string]string{
"extensions": "apps",
}
)
func AddDiscovery(client discovery.DiscoveryInterface, schemas map[string]*types.Schema) error {
logrus.Info("Refreshing all schemas")
groups, resourceLists, err := client.ServerGroupsAndResources()
if err != nil {
return err
}
versions := indexVersions(groups)
var errs []error
for _, resourceList := range resourceLists {
gv, err := schema.ParseGroupVersion(resourceList.GroupVersion)
if err != nil {
errs = append(errs, err)
}
if err := refresh(gv, versions, resourceList, schemas); err != nil {
errs = append(errs, err)
}
}
return merr.NewErrors(errs...)
}
func indexVersions(groups []*metav1.APIGroup) map[string]string {
result := map[string]string{}
for _, group := range groups {
result[group.Name] = group.PreferredVersion.Version
}
return result
}
func refresh(gv schema.GroupVersion, groupToPreferredVersion map[string]string, 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{
ID: GVKToSchemaID(gvk),
Type: "schema",
Dynamic: true,
}
attributes.SetGVK(schema, gvk)
}
schema.PluralName = GVRToPluralName(gvr)
attributes.SetAPIResource(schema, resource)
if preferredVersion := groupToPreferredVersion[gv.Group]; preferredVersion != "" && preferredVersion != gv.Version {
attributes.SetPreferredVersion(schema, preferredVersion)
}
if group := preferredGroups[gv.Group]; group != "" {
attributes.SetPreferredGroup(schema, group)
}
schemas[schema.ID] = schema
}
return nil
}

View File

@@ -0,0 +1,43 @@
package converter
import (
"fmt"
"strings"
"github.com/rancher/norman/v2/pkg/types"
"github.com/rancher/wrangler-api/pkg/generated/controllers/apiextensions.k8s.io/v1beta1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/discovery"
)
func GVKToSchemaID(gvk schema.GroupVersionKind) string {
if gvk.Group == "" {
return strings.ToLower(fmt.Sprintf("core.%s.%s", gvk.Version, gvk.Kind))
}
return strings.ToLower(fmt.Sprintf("%s.%s.%s", gvk.Group, gvk.Version, gvk.Kind))
}
func GVRToPluralName(gvr schema.GroupVersionResource) string {
if gvr.Group == "" {
return fmt.Sprintf("core.%s.%s", gvr.Version, gvr.Resource)
}
return fmt.Sprintf("%s.%s.%s", gvr.Group, gvr.Version, gvr.Resource)
}
func ToSchemas(crd v1beta1.CustomResourceDefinitionClient, 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
}
if err := AddCustomResources(crd, result); err != nil {
return nil, err
}
return result, nil
}

View File

@@ -0,0 +1,109 @@
package converter
import (
"github.com/rancher/norman/v2/pkg/types"
"github.com/rancher/norman/v2/pkg/types/convert"
"github.com/rancher/steve/pkg/attributes"
"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" || v.Type == "integer" {
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:
sub := v.SubSchema()
if p, ok := sub.(*proto.Primitive); ok {
f.Type = p.Type
} else {
f.Type = sub.GetPath().String()
}
case *proto.Arbitrary:
default:
logrus.Errorf("unknown type: %v", schema)
f.Type = "json"
}
return f
}

View File

@@ -0,0 +1,37 @@
package schema
import (
"fmt"
"github.com/rancher/norman/v2/pkg/data"
"github.com/rancher/norman/v2/pkg/types"
)
func newDefaultMapper() types.Mapper {
return &defaultMapper{}
}
type defaultMapper struct {
types.EmptyMapper
}
func (d *defaultMapper) FromInternal(data data.Object) {
if data["kind"] != "" && data["apiVersion"] != "" {
if t, ok := data["type"]; ok && data != nil {
data["_type"] = t
}
}
if _, ok := data["id"]; ok || data == nil {
return
}
name := types.Name(data)
namespace := types.Namespace(data)
if namespace == "" {
data["id"] = name
} else {
data["id"] = fmt.Sprintf("%s/%s", namespace, name)
}
}

134
pkg/schema/factory.go Normal file
View File

@@ -0,0 +1,134 @@
package schema
import (
"fmt"
"net/http"
"github.com/rancher/norman/v2/pkg/api/builtin"
"github.com/rancher/norman/v2/pkg/types"
"github.com/rancher/steve/pkg/accesscontrol"
"github.com/rancher/steve/pkg/attributes"
"github.com/rancher/steve/pkg/table"
"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(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 _, template := range c.templates {
if template.RegisterType != nil {
s, err := result.Import(template.RegisterType)
if err != nil {
return nil, err
}
c.applyTemplates(result, s)
}
}
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)
s.ResourceMethods = append(s.ResourceMethods, http.MethodPatch)
}
if verbAccess.AnyVerb("create") {
s.CollectionMethods = append(s.CollectionMethods, http.MethodPost)
}
c.applyTemplates(result, s)
if err := result.AddSchema(*s); err != nil {
return nil, err
}
}
return result, nil
}
func (c *Collection) applyTemplates(schemas *types.Schemas, 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 t.Mapper != nil {
schemas.AddMapper(schema.ID, t.Mapper)
}
if schema.Formatter == nil {
schema.Formatter = t.Formatter
}
if schema.Store == nil {
if t.StoreFactory == nil {
schema.Store = t.Store
} else {
schema.Store = t.StoreFactory(templates[2].Store)
}
}
if t.Customize != nil {
t.Customize(schema)
}
if len(t.Columns) > 0 {
schemas.AddMapper(schema.ID, table.NewColumns(t.ComputedColumns, t.Columns...))
}
}
}

View File

@@ -0,0 +1,54 @@
package table
import (
"github.com/rancher/steve/pkg/attributes"
types2 "github.com/rancher/steve/pkg/schemaserver/types"
"github.com/rancher/wrangler/pkg/data"
types "github.com/rancher/wrangler/pkg/schemas"
"github.com/rancher/wrangler/pkg/schemas/mappers"
)
type Column struct {
Name string `json:"name,omitempty"`
Field string `json:"field,omitempty"`
Type string `json:"type,omitempty"`
Format string `json:"format,omitempty"`
}
type Table struct {
Columns []Column
Computed func(data.Object)
}
type ColumnMapper struct {
definition Table
mappers.EmptyMapper
}
func NewColumns(computed func(data.Object), columns ...Column) *ColumnMapper {
return &ColumnMapper{
definition: Table{
Columns: columns,
Computed: computed,
},
}
}
func (t *ColumnMapper) FromInternal(d data.Object) {
if t.definition.Computed != nil {
t.definition.Computed(d)
}
}
func (t *ColumnMapper) ModifySchema(schema *types.Schema, schemas *types.Schemas) error {
as := &types2.APISchema{
Schema: schema,
}
cols := t.definition.Columns
columnObj := attributes.Columns(as)
if columns, ok := columnObj.([]Column); ok {
cols = append(columns, cols...)
}
attributes.SetColumns(as, cols)
return nil
}