mirror of
https://github.com/rancher/steve.git
synced 2025-08-31 15:11:31 +00:00
Refactor
This commit is contained in:
73
pkg/server/cli/clicontext.go
Normal file
73
pkg/server/cli/clicontext.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
authcli "github.com/rancher/steve/pkg/auth/cli"
|
||||
"github.com/rancher/steve/pkg/server"
|
||||
"github.com/rancher/wrangler/pkg/kubeconfig"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
KubeConfig string
|
||||
HTTPSListenPort int
|
||||
HTTPListenPort int
|
||||
Namespace string
|
||||
|
||||
WebhookConfig authcli.WebhookConfig
|
||||
}
|
||||
|
||||
func (c *Config) MustServerConfig() *server.Server {
|
||||
cc, err := c.ToServerConfig()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return cc
|
||||
}
|
||||
|
||||
func (c *Config) ToServerConfig() (*server.Server, error) {
|
||||
restConfig, err := kubeconfig.GetNonInteractiveClientConfig(c.KubeConfig).ClientConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
auth, err := c.WebhookConfig.WebhookMiddleware()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &server.Server{
|
||||
Namespace: c.Namespace,
|
||||
RestConfig: restConfig,
|
||||
AuthMiddleware: auth,
|
||||
HTTPPort: c.HTTPListenPort,
|
||||
HTTPSPort: c.HTTPSListenPort,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func Flags(config *Config) []cli.Flag {
|
||||
flags := []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "kubeconfig",
|
||||
EnvVar: "KUBECONFIG",
|
||||
Destination: &config.KubeConfig,
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "https-listen-port",
|
||||
Value: 8443,
|
||||
Destination: &config.HTTPSListenPort,
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "http-listen-port",
|
||||
Value: 8080,
|
||||
Destination: &config.HTTPListenPort,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "namespace",
|
||||
EnvVar: "NAMESPACE",
|
||||
Value: "steve",
|
||||
Destination: &config.Namespace,
|
||||
},
|
||||
}
|
||||
|
||||
return append(flags, authcli.Flags(&config.WebhookConfig)...)
|
||||
}
|
89
pkg/server/config.go
Normal file
89
pkg/server/config.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/rancher/steve/pkg/auth"
|
||||
"github.com/rancher/steve/pkg/schema"
|
||||
"github.com/rancher/steve/pkg/schemaserver/types"
|
||||
"github.com/rancher/steve/pkg/server/router"
|
||||
"github.com/rancher/wrangler-api/pkg/generated/controllers/apiextensions.k8s.io"
|
||||
apiextensionsv1beta1 "github.com/rancher/wrangler-api/pkg/generated/controllers/apiextensions.k8s.io/v1beta1"
|
||||
"github.com/rancher/wrangler-api/pkg/generated/controllers/apiregistration.k8s.io"
|
||||
apiregistrationv1 "github.com/rancher/wrangler-api/pkg/generated/controllers/apiregistration.k8s.io/v1"
|
||||
"github.com/rancher/wrangler-api/pkg/generated/controllers/core"
|
||||
corev1 "github.com/rancher/wrangler-api/pkg/generated/controllers/core/v1"
|
||||
"github.com/rancher/wrangler-api/pkg/generated/controllers/rbac"
|
||||
rbacv1 "github.com/rancher/wrangler-api/pkg/generated/controllers/rbac/v1"
|
||||
"github.com/rancher/wrangler/pkg/start"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
*Controllers
|
||||
|
||||
RestConfig *rest.Config
|
||||
|
||||
Namespace string
|
||||
HTTPSPort int
|
||||
HTTPPort int
|
||||
BaseSchemas *types.APISchemas
|
||||
SchemaTemplates []schema.Template
|
||||
AuthMiddleware auth.Middleware
|
||||
Next http.Handler
|
||||
Router router.RouterFunc
|
||||
PostStartHooks []func() error
|
||||
StartHooks []StartHook
|
||||
}
|
||||
|
||||
type Controllers struct {
|
||||
K8s kubernetes.Interface
|
||||
Core corev1.Interface
|
||||
RBAC rbacv1.Interface
|
||||
API apiregistrationv1.Interface
|
||||
CRD apiextensionsv1beta1.Interface
|
||||
starters []start.Starter
|
||||
}
|
||||
|
||||
func NewController(cfg *rest.Config) (*Controllers, error) {
|
||||
c := &Controllers{}
|
||||
|
||||
core, err := core.NewFactoryFromConfig(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.starters = append(c.starters, core)
|
||||
|
||||
rbac, err := rbac.NewFactoryFromConfig(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.starters = append(c.starters, rbac)
|
||||
|
||||
api, err := apiregistration.NewFactoryFromConfig(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.starters = append(c.starters, api)
|
||||
|
||||
crd, err := apiextensions.NewFactoryFromConfig(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.starters = append(c.starters, crd)
|
||||
|
||||
c.K8s, err = kubernetes.NewForConfig(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.Core = core.Core().V1()
|
||||
c.RBAC = rbac.Rbac().V1()
|
||||
c.API = api.Apiregistration().V1()
|
||||
c.CRD = crd.Apiextensions().V1beta1()
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
type StartHook func(context.Context, *Server) error
|
@@ -1,29 +1,29 @@
|
||||
package publicapi
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/rancher/norman/v2/pkg/api"
|
||||
"github.com/rancher/norman/v2/pkg/auth"
|
||||
"github.com/rancher/norman/v2/pkg/types"
|
||||
"github.com/rancher/norman/v2/pkg/urlbuilder"
|
||||
"github.com/rancher/steve/pkg/accesscontrol"
|
||||
"github.com/rancher/steve/pkg/auth"
|
||||
k8sproxy "github.com/rancher/steve/pkg/proxy"
|
||||
"github.com/rancher/steve/pkg/resources/schema"
|
||||
"github.com/rancher/steve/pkg/schema"
|
||||
"github.com/rancher/steve/pkg/schemaserver/server"
|
||||
"github.com/rancher/steve/pkg/schemaserver/types"
|
||||
"github.com/rancher/steve/pkg/schemaserver/urlbuilder"
|
||||
"github.com/rancher/steve/pkg/server/router"
|
||||
"github.com/sirupsen/logrus"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
func NewHandler(cfg *rest.Config, sf schema.Factory, authMiddleware auth.Middleware, next http.Handler) (http.Handler, error) {
|
||||
func New(cfg *rest.Config, sf schema.Factory, authMiddleware auth.Middleware, next http.Handler, routerFunc router.RouterFunc) (http.Handler, error) {
|
||||
var (
|
||||
err error
|
||||
)
|
||||
|
||||
a := &apiServer{
|
||||
sf: sf,
|
||||
server: api.DefaultAPIServer(),
|
||||
server: server.DefaultAPIServer(),
|
||||
}
|
||||
a.server.AccessControl = accesscontrol.NewAccessControl()
|
||||
|
||||
@@ -33,18 +33,22 @@ func NewHandler(cfg *rest.Config, sf schema.Factory, authMiddleware auth.Middlew
|
||||
}
|
||||
|
||||
w := authMiddleware.Wrap
|
||||
return router.Routes(router.Handlers{
|
||||
handlers := router.Handlers{
|
||||
Next: next,
|
||||
K8sResource: w(a.apiHandler(k8sAPI)),
|
||||
GenericResource: w(a.apiHandler(nil)),
|
||||
K8sProxy: w(proxy),
|
||||
APIRoot: w(a.apiHandler(apiRoot)),
|
||||
}), nil
|
||||
}
|
||||
if routerFunc == nil {
|
||||
return router.Routes(handlers), nil
|
||||
}
|
||||
return routerFunc(handlers), nil
|
||||
}
|
||||
|
||||
type apiServer struct {
|
||||
sf schema.Factory
|
||||
server *api.Server
|
||||
server *server.Server
|
||||
}
|
||||
|
||||
func (a *apiServer) common(rw http.ResponseWriter, req *http.Request) (*types.APIRequest, bool) {
|
||||
|
@@ -1,10 +1,10 @@
|
||||
package publicapi
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/rancher/norman/v2/pkg/types"
|
||||
"github.com/rancher/steve/pkg/attributes"
|
||||
"github.com/rancher/steve/pkg/resources/schema"
|
||||
"github.com/rancher/steve/pkg/schema"
|
||||
"github.com/rancher/steve/pkg/schemaserver/types"
|
||||
runtimeschema "k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
@@ -24,7 +24,7 @@ func k8sAPI(sf schema.Factory, apiOp *types.APIRequest) {
|
||||
|
||||
nOrN := vars["nameorns"]
|
||||
if nOrN != "" {
|
||||
schema := apiOp.Schemas.Schema(apiOp.Type)
|
||||
schema := apiOp.Schemas.LookupSchema(apiOp.Type)
|
||||
if attributes.Namespaced(schema) {
|
||||
vars["namespace"] = nOrN
|
||||
} else {
|
||||
@@ -33,7 +33,7 @@ func k8sAPI(sf schema.Factory, apiOp *types.APIRequest) {
|
||||
}
|
||||
|
||||
if namespace := vars["namespace"]; namespace != "" {
|
||||
apiOp.Namespaces = []string{namespace}
|
||||
apiOp.Namespace = namespace
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -3,20 +3,19 @@ package apigroups
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/rancher/norman/v2/pkg/data"
|
||||
"github.com/rancher/norman/v2/pkg/store/empty"
|
||||
"github.com/rancher/norman/v2/pkg/types"
|
||||
"github.com/rancher/steve/pkg/schemaserver/store/empty"
|
||||
"github.com/rancher/steve/pkg/schemaserver/types"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/discovery"
|
||||
)
|
||||
|
||||
func Register(schemas *types.Schemas, discovery discovery.DiscoveryInterface) {
|
||||
schemas.MustImportAndCustomize(v1.APIGroup{}, func(schema *types.Schema) {
|
||||
func Register(schemas *types.APISchemas, discovery discovery.DiscoveryInterface) {
|
||||
schemas.MustImportAndCustomize(v1.APIGroup{}, func(schema *types.APISchema) {
|
||||
schema.CollectionMethods = []string{http.MethodGet}
|
||||
schema.ResourceMethods = []string{http.MethodGet}
|
||||
schema.Store = NewStore(discovery)
|
||||
schema.Formatter = func(request *types.APIRequest, resource *types.RawResource) {
|
||||
resource.ID = data.Object(resource.Values).String("name")
|
||||
resource.ID = resource.APIObject.Data().String("name")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -34,38 +33,32 @@ func NewStore(discovery discovery.DiscoveryInterface) types.Store {
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Store) ByID(apiOp *types.APIRequest, schema *types.Schema, id string) (types.APIObject, error) {
|
||||
groupList, err := e.discovery.ServerGroups()
|
||||
if err != nil {
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
|
||||
if id == "core" {
|
||||
id = ""
|
||||
}
|
||||
|
||||
for _, group := range groupList.Groups {
|
||||
if group.Name == id {
|
||||
return types.ToAPI(group), nil
|
||||
}
|
||||
}
|
||||
|
||||
return types.APIObject{}, nil
|
||||
func (e *Store) ByID(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) {
|
||||
return types.DefaultByID(e, apiOp, schema, id)
|
||||
}
|
||||
|
||||
func (e *Store) List(apiOp *types.APIRequest, schema *types.Schema, opt *types.QueryOptions) (types.APIObject, error) {
|
||||
groupList, err := e.discovery.ServerGroups()
|
||||
if err != nil {
|
||||
return types.APIObject{}, err
|
||||
func toAPIObject(schema *types.APISchema, group v1.APIGroup) types.APIObject {
|
||||
if group.Name == "" {
|
||||
group.Name = "core"
|
||||
}
|
||||
return types.APIObject{
|
||||
Type: schema.ID,
|
||||
ID: group.Name,
|
||||
Object: group,
|
||||
}
|
||||
|
||||
var result []interface{}
|
||||
}
|
||||
|
||||
func (e *Store) List(apiOp *types.APIRequest, schema *types.APISchema) (types.APIObjectList, error) {
|
||||
groupList, err := e.discovery.ServerGroups()
|
||||
if err != nil {
|
||||
return types.APIObjectList{}, err
|
||||
}
|
||||
|
||||
var result types.APIObjectList
|
||||
for _, item := range groupList.Groups {
|
||||
if item.Name == "" {
|
||||
item.Name = "core"
|
||||
}
|
||||
result = append(result, item)
|
||||
result.Objects = append(result.Objects, toAPIObject(schema, item))
|
||||
}
|
||||
|
||||
return types.ToAPI(result), nil
|
||||
return result, nil
|
||||
}
|
||||
|
@@ -1,9 +1,11 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"github.com/rancher/norman/v2/pkg/types"
|
||||
"github.com/rancher/steve/pkg/attributes"
|
||||
"github.com/rancher/steve/pkg/table"
|
||||
"github.com/rancher/steve/pkg/schema/table"
|
||||
"github.com/rancher/steve/pkg/schemaserver/types"
|
||||
"github.com/rancher/wrangler/pkg/schemas"
|
||||
"github.com/rancher/wrangler/pkg/schemas/mappers"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -22,12 +24,15 @@ var (
|
||||
)
|
||||
|
||||
type DefaultColumns struct {
|
||||
types.EmptyMapper
|
||||
mappers.EmptyMapper
|
||||
}
|
||||
|
||||
func (d *DefaultColumns) ModifySchema(schema *types.Schema, schemas *types.Schemas) error {
|
||||
if attributes.Columns(schema) == nil {
|
||||
attributes.SetColumns(schema, []table.Column{
|
||||
func (d *DefaultColumns) ModifySchema(schema *schemas.Schema, schemas *schemas.Schemas) error {
|
||||
as := &types.APISchema{
|
||||
Schema: schema,
|
||||
}
|
||||
if attributes.Columns(as) == nil {
|
||||
attributes.SetColumns(as, []table.Column{
|
||||
NameColumn,
|
||||
CreatedColumn,
|
||||
})
|
||||
|
111
pkg/server/resources/common/dynamiccolumns.go
Normal file
111
pkg/server/resources/common/dynamiccolumns.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/rancher/steve/pkg/attributes"
|
||||
"github.com/rancher/steve/pkg/schema/table"
|
||||
"github.com/rancher/steve/pkg/schemaserver/types"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
type DynamicColumns struct {
|
||||
client *rest.RESTClient
|
||||
}
|
||||
|
||||
func NewDynamicColumns(config *rest.Config) (*DynamicColumns, error) {
|
||||
c, err := newClient(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &DynamicColumns{
|
||||
client: c,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func hasGet(methods []string) bool {
|
||||
for _, method := range methods {
|
||||
if method == http.MethodGet {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (d *DynamicColumns) SetColumns(schema *types.APISchema) error {
|
||||
if attributes.Columns(schema) != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
gvr := attributes.GVR(schema)
|
||||
if gvr.Resource == "" {
|
||||
return nil
|
||||
}
|
||||
nsed := attributes.Namespaced(schema)
|
||||
|
||||
if !hasGet(schema.CollectionMethods) {
|
||||
return nil
|
||||
}
|
||||
|
||||
r := d.client.Get()
|
||||
if gvr.Group == "" {
|
||||
r.Prefix("api")
|
||||
} else {
|
||||
r.Prefix("apis", gvr.Group)
|
||||
}
|
||||
r.Prefix(gvr.Version)
|
||||
if nsed {
|
||||
r.Prefix("namespaces", "default")
|
||||
}
|
||||
r.Prefix(gvr.Resource)
|
||||
|
||||
obj, err := r.Do().Get()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t, ok := obj.(*metav1.Table)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
var cols []table.Column
|
||||
for _, cd := range t.ColumnDefinitions {
|
||||
cols = append(cols, table.Column{
|
||||
Name: cd.Name,
|
||||
Field: "metadata.computed.fields." + cd.Name,
|
||||
Type: cd.Type,
|
||||
Format: cd.Format,
|
||||
})
|
||||
}
|
||||
|
||||
if len(cols) > 0 {
|
||||
attributes.SetColumns(schema, cols)
|
||||
schema.Attributes["server-side-column"] = "true"
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func newClient(config *rest.Config) (*rest.RESTClient, error) {
|
||||
scheme := runtime.NewScheme()
|
||||
if err := metav1.AddMetaToScheme(scheme); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := metav1beta1.AddMetaToScheme(scheme); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config = rest.CopyConfig(config)
|
||||
config.UserAgent = rest.DefaultKubernetesUserAgent()
|
||||
config.AcceptContentTypes = "application/json;as=Table;v=v1beta1;g=meta.k8s.io"
|
||||
config.ContentType = "application/json;as=Table;v=v1beta1;g=meta.k8s.io"
|
||||
config.GroupVersion = &schema.GroupVersion{}
|
||||
config.NegotiatedSerializer = serializer.NewCodecFactory(scheme)
|
||||
config.APIPath = "/"
|
||||
return rest.RESTClientFor(config)
|
||||
}
|
@@ -1,25 +1,27 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"github.com/rancher/norman/v2/pkg/store/proxy"
|
||||
"github.com/rancher/norman/v2/pkg/types"
|
||||
"github.com/rancher/norman/v2/pkg/types/convert"
|
||||
"github.com/rancher/norman/v2/pkg/types/values"
|
||||
"github.com/rancher/steve/pkg/resources/schema"
|
||||
"github.com/rancher/steve/pkg/schema"
|
||||
"github.com/rancher/steve/pkg/schemaserver/types"
|
||||
"github.com/rancher/steve/pkg/server/store/proxy"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
)
|
||||
|
||||
func Register(collection *schema.Collection, clientGetter proxy.ClientGetter) error {
|
||||
collection.AddTemplate(&schema.Template{
|
||||
func DefaultTemplate(clientGetter proxy.ClientGetter) schema.Template {
|
||||
return schema.Template{
|
||||
Store: proxy.NewProxyStore(clientGetter),
|
||||
Formatter: Formatter,
|
||||
Mapper: &DefaultColumns{},
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func Formatter(request *types.APIRequest, resource *types.RawResource) {
|
||||
selfLink := convert.ToString(values.GetValueN(resource.Values, "metadata", "selfLink"))
|
||||
meta, err := meta.Accessor(resource.APIObject.Object)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
selfLink := meta.GetSelfLink()
|
||||
if selfLink == "" {
|
||||
return
|
||||
}
|
||||
|
@@ -5,15 +5,14 @@ import (
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
schema2 "k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
"github.com/rancher/norman/v2/pkg/store/empty"
|
||||
"github.com/rancher/norman/v2/pkg/types"
|
||||
"github.com/rancher/steve/pkg/accesscontrol"
|
||||
"github.com/rancher/steve/pkg/attributes"
|
||||
"github.com/rancher/steve/pkg/clustercache"
|
||||
"github.com/rancher/steve/pkg/schemaserver/store/empty"
|
||||
"github.com/rancher/steve/pkg/schemaserver/types"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
schema2 "k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -24,11 +23,11 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
func Register(schemas *types.Schemas, ccache clustercache.ClusterCache) {
|
||||
schemas.MustImportAndCustomize(Count{}, func(schema *types.Schema) {
|
||||
func Register(schemas *types.APISchemas, ccache clustercache.ClusterCache) {
|
||||
schemas.MustImportAndCustomize(Count{}, func(schema *types.APISchema) {
|
||||
schema.CollectionMethods = []string{http.MethodGet}
|
||||
schema.ResourceMethods = []string{http.MethodGet}
|
||||
schema.Attributes["access"] = accesscontrol.AccessListMap{
|
||||
schema.Attributes["access"] = accesscontrol.AccessListByVerb{
|
||||
"watch": accesscontrol.AccessList{
|
||||
{
|
||||
Namespace: "*",
|
||||
@@ -58,27 +57,39 @@ type Store struct {
|
||||
ccache clustercache.ClusterCache
|
||||
}
|
||||
|
||||
func (s *Store) ByID(apiOp *types.APIRequest, schema *types.Schema, id string) (types.APIObject, error) {
|
||||
c := s.getCount(apiOp)
|
||||
return types.ToAPI(c), nil
|
||||
func toAPIObject(c Count) types.APIObject {
|
||||
return types.APIObject{
|
||||
Type: "count",
|
||||
ID: c.ID,
|
||||
Object: c,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Store) List(apiOp *types.APIRequest, schema *types.Schema, opt *types.QueryOptions) (types.APIObject, error) {
|
||||
func (s *Store) ByID(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) {
|
||||
c := s.getCount(apiOp)
|
||||
return types.ToAPI([]interface{}{c}), nil
|
||||
return toAPIObject(c), nil
|
||||
}
|
||||
|
||||
func (s *Store) Watch(apiOp *types.APIRequest, schema *types.Schema, w types.WatchRequest) (chan types.APIEvent, error) {
|
||||
func (s *Store) List(apiOp *types.APIRequest, schema *types.APISchema) (types.APIObjectList, error) {
|
||||
c := s.getCount(apiOp)
|
||||
return types.APIObjectList{
|
||||
Objects: []types.APIObject{
|
||||
toAPIObject(c),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Store) Watch(apiOp *types.APIRequest, schema *types.APISchema, w types.WatchRequest) (chan types.APIEvent, error) {
|
||||
var (
|
||||
result = make(chan types.APIEvent, 100)
|
||||
counts map[string]ItemCount
|
||||
gvrToSchema = map[schema2.GroupVersionResource]*types.Schema{}
|
||||
gvrToSchema = map[schema2.GroupVersionResource]*types.APISchema{}
|
||||
countLock sync.Mutex
|
||||
)
|
||||
|
||||
counts = s.getCount(apiOp).Counts
|
||||
for id := range counts {
|
||||
schema := apiOp.Schemas.Schema(id)
|
||||
schema := apiOp.Schemas.LookupSchema(id)
|
||||
if schema == nil {
|
||||
continue
|
||||
}
|
||||
@@ -107,11 +118,6 @@ func (s *Store) Watch(apiOp *types.APIRequest, schema *types.Schema, w types.Wat
|
||||
return nil
|
||||
}
|
||||
|
||||
apiObj := apiOp.Filter(nil, schema, types.ToAPI(obj))
|
||||
if apiObj.IsNil() {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, namespace, revision, ok := getInfo(obj)
|
||||
if !ok {
|
||||
return nil
|
||||
@@ -151,7 +157,7 @@ func (s *Store) Watch(apiOp *types.APIRequest, schema *types.Schema, w types.Wat
|
||||
result <- types.APIEvent{
|
||||
Name: "resource.change",
|
||||
ResourceType: "counts",
|
||||
Object: types.ToAPI(Count{
|
||||
Object: toAPIObject(Count{
|
||||
ID: "count",
|
||||
Counts: countsCopy,
|
||||
}),
|
||||
@@ -170,8 +176,8 @@ func (s *Store) Watch(apiOp *types.APIRequest, schema *types.Schema, w types.Wat
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *Store) schemasToWatch(apiOp *types.APIRequest) (result []*types.Schema) {
|
||||
for _, schema := range apiOp.Schemas.Schemas() {
|
||||
func (s *Store) schemasToWatch(apiOp *types.APIRequest) (result []*types.APISchema) {
|
||||
for _, schema := range apiOp.Schemas.Schemas {
|
||||
if ignore[schema.ID] {
|
||||
continue
|
||||
}
|
||||
|
@@ -1,38 +1,28 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"github.com/rancher/norman/v2/pkg/store/apiroot"
|
||||
"github.com/rancher/norman/v2/pkg/subscribe"
|
||||
"github.com/rancher/norman/v2/pkg/types"
|
||||
"github.com/rancher/steve/pkg/accesscontrol"
|
||||
"github.com/rancher/steve/pkg/client"
|
||||
"github.com/rancher/steve/pkg/clustercache"
|
||||
"github.com/rancher/steve/pkg/resources/apigroups"
|
||||
"github.com/rancher/steve/pkg/resources/common"
|
||||
"github.com/rancher/steve/pkg/resources/core"
|
||||
"github.com/rancher/steve/pkg/resources/counts"
|
||||
"github.com/rancher/steve/pkg/resources/schema"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"github.com/rancher/steve/pkg/schema"
|
||||
"github.com/rancher/steve/pkg/schemaserver/store/apiroot"
|
||||
"github.com/rancher/steve/pkg/schemaserver/subscribe"
|
||||
"github.com/rancher/steve/pkg/schemaserver/types"
|
||||
"github.com/rancher/steve/pkg/server/resources/apigroups"
|
||||
"github.com/rancher/steve/pkg/server/resources/common"
|
||||
"github.com/rancher/steve/pkg/server/resources/counts"
|
||||
"k8s.io/client-go/discovery"
|
||||
)
|
||||
|
||||
func SchemaFactory(
|
||||
cf *client.Factory,
|
||||
as *accesscontrol.AccessStore,
|
||||
k8s kubernetes.Interface,
|
||||
ccache clustercache.ClusterCache,
|
||||
) (*schema.Collection, error) {
|
||||
baseSchema := types.EmptySchemas()
|
||||
collection := schema.NewCollection(baseSchema, as)
|
||||
|
||||
core.Register(collection)
|
||||
func DefaultSchemas(baseSchema *types.APISchemas, discovery discovery.DiscoveryInterface, ccache clustercache.ClusterCache) *types.APISchemas {
|
||||
counts.Register(baseSchema, ccache)
|
||||
subscribe.Register(baseSchema)
|
||||
apigroups.Register(baseSchema, k8s.Discovery())
|
||||
apigroups.Register(baseSchema, discovery)
|
||||
apiroot.Register(baseSchema, []string{"v1"}, []string{"proxy:/apis"})
|
||||
|
||||
if err := common.Register(collection, cf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return collection, nil
|
||||
return baseSchema
|
||||
}
|
||||
|
||||
func DefaultSchemaTemplates(cf *client.Factory) []schema.Template {
|
||||
return []schema.Template{
|
||||
common.DefaultTemplate(cf),
|
||||
}
|
||||
}
|
||||
|
@@ -6,28 +6,36 @@ import (
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type RouterFunc func(h Handlers) http.Handler
|
||||
|
||||
type Handlers struct {
|
||||
K8sResource http.Handler
|
||||
GenericResource http.Handler
|
||||
APIRoot http.Handler
|
||||
K8sProxy http.Handler
|
||||
Next http.Handler
|
||||
}
|
||||
|
||||
func Routes(h Handlers) http.Handler {
|
||||
m := mux.NewRouter()
|
||||
m.UseEncodedPath()
|
||||
m.StrictSlash(true)
|
||||
m.NotFoundHandler = h.K8sProxy
|
||||
|
||||
m.Path("/").Handler(h.APIRoot)
|
||||
m.Path("/").Handler(h.APIRoot).HeadersRegexp("Accepts", ".*json.*")
|
||||
m.Path("/{name:v1}").Handler(h.APIRoot)
|
||||
|
||||
m.Path("/v1/{type:schemas}/{name:.*}").Handler(h.GenericResource)
|
||||
m.Path("/v1/{group}.{version}.{resource}").Handler(h.K8sResource)
|
||||
m.Path("/v1/{group}.{version}.{resource}/{nameorns}").Handler(h.K8sResource)
|
||||
m.Path("/v1/{group}.{version}.{resource}/{namespace}/{name}").Handler(h.K8sResource)
|
||||
m.Path("/v1/{group}.{version}.{resource}/{nameorns}").Queries("action", "{action}").Handler(h.K8sResource)
|
||||
m.Path("/v1/{group}.{version}.{resource}/{namespace}/{name}").Queries("action", "{action}").Handler(h.K8sResource)
|
||||
m.Path("/v1/{type:schemas}/{name:.*}").Handler(h.GenericResource)
|
||||
m.Path("/v1/{type}").Handler(h.GenericResource)
|
||||
m.Path("/v1/{type}/{name}").Handler(h.GenericResource)
|
||||
m.PathPrefix("/api").Handler(h.K8sProxy)
|
||||
m.PathPrefix("/openapi").Handler(h.K8sProxy)
|
||||
m.PathPrefix("/version").Handler(h.K8sProxy)
|
||||
m.NotFoundHandler = h.Next
|
||||
|
||||
return m
|
||||
}
|
||||
|
@@ -2,127 +2,152 @@ package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/rancher/norman/pkg/auth"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/rancher/dynamiclistener/server"
|
||||
"github.com/rancher/dynamiclistener/storage/kubernetes"
|
||||
"github.com/rancher/dynamiclistener/storage/memory"
|
||||
"github.com/rancher/steve/pkg/accesscontrol"
|
||||
"github.com/rancher/steve/pkg/client"
|
||||
"github.com/rancher/steve/pkg/clustercache"
|
||||
"github.com/rancher/steve/pkg/controllers/schema"
|
||||
"github.com/rancher/steve/pkg/resources"
|
||||
"github.com/rancher/steve/pkg/server/publicapi"
|
||||
"github.com/rancher/wrangler-api/pkg/generated/controllers/apiextensions.k8s.io"
|
||||
"github.com/rancher/wrangler-api/pkg/generated/controllers/apiregistration.k8s.io"
|
||||
"github.com/rancher/wrangler-api/pkg/generated/controllers/core"
|
||||
rbaccontroller "github.com/rancher/wrangler-api/pkg/generated/controllers/rbac"
|
||||
"github.com/rancher/wrangler/pkg/generic"
|
||||
"github.com/rancher/wrangler/pkg/kubeconfig"
|
||||
schemacontroller "github.com/rancher/steve/pkg/controllers/schema"
|
||||
"github.com/rancher/steve/pkg/schema"
|
||||
"github.com/rancher/steve/pkg/schemaserver/types"
|
||||
"github.com/rancher/steve/pkg/server/handler"
|
||||
"github.com/rancher/steve/pkg/server/resources"
|
||||
v1 "github.com/rancher/wrangler-api/pkg/generated/controllers/core/v1"
|
||||
"github.com/rancher/wrangler/pkg/start"
|
||||
"github.com/sirupsen/logrus"
|
||||
schema2 "k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Kubeconfig string
|
||||
ListenAddress string
|
||||
WebhookKubeconfig string
|
||||
Authentication bool
|
||||
var ErrConfigRequired = errors.New("rest config is required")
|
||||
|
||||
func setDefaults(server *Server) error {
|
||||
if server.RestConfig == nil {
|
||||
return ErrConfigRequired
|
||||
}
|
||||
|
||||
if server.Namespace == "" {
|
||||
server.Namespace = "steve"
|
||||
}
|
||||
|
||||
if server.Controllers == nil {
|
||||
var err error
|
||||
server.Controllers, err = NewController(server.RestConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if server.Next == nil {
|
||||
server.Next = http.NotFoundHandler()
|
||||
}
|
||||
|
||||
if server.BaseSchemas == nil {
|
||||
server.BaseSchemas = types.EmptyAPISchemas()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Run(ctx context.Context, cfg Config) error {
|
||||
restConfig, err := kubeconfig.GetNonInteractiveClientConfig(cfg.Kubeconfig).ClientConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
func setup(ctx context.Context, server *Server) (http.Handler, *schema.Collection, error) {
|
||||
if err := setDefaults(server); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
restConfig.QPS = 100
|
||||
restConfig.Burst = 100
|
||||
|
||||
rbac, err := rbaccontroller.NewFactoryFromConfig(restConfig)
|
||||
cf, err := client.NewFactory(server.RestConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
core, err := core.NewFactoryFromConfig(restConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
k8s, err := kubernetes.NewForConfig(restConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
api, err := apiregistration.NewFactoryFromConfig(restConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
crd, err := apiextensions.NewFactoryFromConfig(restConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cf, err := client.NewFactory(restConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
ccache := clustercache.NewClusterCache(ctx, cf.DynamicClient())
|
||||
|
||||
sf := resources.SchemaFactory(cf,
|
||||
accesscontrol.NewAccessStore(rbac.Rbac().V1()),
|
||||
k8s,
|
||||
ccache,
|
||||
core.Core().V1().ConfigMap(),
|
||||
core.Core().V1().Secret())
|
||||
server.BaseSchemas = resources.DefaultSchemas(server.BaseSchemas, server.K8s.Discovery(), ccache)
|
||||
server.SchemaTemplates = append(server.SchemaTemplates, resources.DefaultSchemaTemplates(cf)...)
|
||||
|
||||
sync := schema.Register(ctx,
|
||||
k8s.Discovery(),
|
||||
crd.Apiextensions().V1beta1().CustomResourceDefinition(),
|
||||
api.Apiregistration().V1().APIService(),
|
||||
k8s.AuthorizationV1().SelfSubjectAccessReviews(),
|
||||
sf := schema.NewCollection(server.BaseSchemas, accesscontrol.NewAccessStore(server.RBAC))
|
||||
sync := schemacontroller.Register(ctx,
|
||||
server.K8s.Discovery(),
|
||||
server.CRD.CustomResourceDefinition(),
|
||||
server.API.APIService(),
|
||||
server.K8s.AuthorizationV1().SelfSubjectAccessReviews(),
|
||||
ccache,
|
||||
sf)
|
||||
|
||||
handler, err := publicapi.NewHandler(restConfig, sf)
|
||||
handler, err := handler.New(server.RestConfig, sf, server.AuthMiddleware, server.Next, server.Router)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
server.PostStartHooks = append(server.PostStartHooks, func() error {
|
||||
return sync()
|
||||
})
|
||||
|
||||
return handler, sf, nil
|
||||
}
|
||||
|
||||
func (c *Server) Handler(ctx context.Context) (http.Handler, error) {
|
||||
handler, sf, err := setup(ctx, c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, hook := range c.StartHooks {
|
||||
if err := hook(ctx, c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
for i := range c.SchemaTemplates {
|
||||
sf.AddTemplate(&c.SchemaTemplates[i])
|
||||
}
|
||||
|
||||
if err := start.All(ctx, 5, c.starters...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, hook := range c.PostStartHooks {
|
||||
if err := hook(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return handler, nil
|
||||
}
|
||||
|
||||
func ListenAndServe(ctx context.Context, secrets v1.SecretController, namespace string, handler http.Handler, httpsPort, httpPort int, opts *server.ListenOpts) error {
|
||||
var (
|
||||
err error
|
||||
)
|
||||
|
||||
if opts == nil {
|
||||
opts = &server.ListenOpts{}
|
||||
}
|
||||
|
||||
if opts.CA == nil || opts.CAKey == nil {
|
||||
opts.CA, opts.CAKey, err = kubernetes.LoadOrGenCA(secrets, namespace, "serving-ca")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if opts.Storage == nil {
|
||||
storage := kubernetes.Load(ctx, secrets, namespace, "service-cert", memory.New())
|
||||
opts.Storage = storage
|
||||
}
|
||||
|
||||
if err := server.ListenAndServe(ctx, httpsPort, httpPort, handler, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Server) ListenAndServe(ctx context.Context, httpsPort, httpPort int, opts *server.ListenOpts) error {
|
||||
handler, err := c.Handler(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cfg.Authentication {
|
||||
authMiddleware, err := auth.NewWebhookMiddleware(cfg.WebhookKubeconfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
handler = wrapHandler(handler, authMiddleware)
|
||||
}
|
||||
|
||||
for _, controllers := range []controllers{api, crd, rbac} {
|
||||
for gvk, controller := range controllers.Controllers() {
|
||||
ccache.AddController(gvk, controller.Informer())
|
||||
}
|
||||
}
|
||||
|
||||
if err := start.All(ctx, 5, api, crd, rbac); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := sync(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Infof("listening on %s", cfg.ListenAddress)
|
||||
return http.ListenAndServe(cfg.ListenAddress, handler)
|
||||
}
|
||||
|
||||
func wrapHandler(handler http.Handler, middleware func(http.ResponseWriter, *http.Request, http.Handler)) http.Handler {
|
||||
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
||||
middleware(resp, req, handler)
|
||||
})
|
||||
}
|
||||
|
||||
type controllers interface {
|
||||
Controllers() map[schema2.GroupVersionKind]*generic.Controller
|
||||
return ListenAndServe(ctx, c.Core.Secret(), c.Namespace, handler, httpsPort, httpPort, opts)
|
||||
}
|
||||
|
66
pkg/server/store/proxy/client.go
Normal file
66
pkg/server/store/proxy/client.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/rancher/steve/pkg/attributes"
|
||||
"github.com/rancher/steve/pkg/schemaserver/httperror"
|
||||
"github.com/rancher/steve/pkg/schemaserver/types"
|
||||
"github.com/rancher/wrangler/pkg/schemas/validation"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
type ClientFactory struct {
|
||||
cfg rest.Config
|
||||
client dynamic.Interface
|
||||
impersonate bool
|
||||
idToGVR map[string]schema.GroupVersionResource
|
||||
}
|
||||
|
||||
func NewClientFactory(cfg *rest.Config, impersonate bool) *ClientFactory {
|
||||
return &ClientFactory{
|
||||
impersonate: impersonate,
|
||||
cfg: *cfg,
|
||||
idToGVR: map[string]schema.GroupVersionResource{},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ClientFactory) Client(ctx *types.APIRequest, schema *types.APISchema) (dynamic.ResourceInterface, error) {
|
||||
gvr := attributes.GVR(schema)
|
||||
if gvr.Resource == "" {
|
||||
return nil, httperror.NewAPIError(validation.NotFound, "Failed to find gvr for "+schema.ID)
|
||||
}
|
||||
|
||||
user, ok := request.UserFrom(ctx.Request.Context())
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to find user context for client")
|
||||
}
|
||||
|
||||
client, err := p.getClient(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client.Resource(gvr), nil
|
||||
}
|
||||
|
||||
func (p *ClientFactory) getClient(user user.Info) (dynamic.Interface, error) {
|
||||
if p.impersonate {
|
||||
return p.client, nil
|
||||
}
|
||||
|
||||
if user.GetName() == "" {
|
||||
return nil, fmt.Errorf("failed to determine current user")
|
||||
}
|
||||
|
||||
newCfg := p.cfg
|
||||
newCfg.Impersonate.UserName = user.GetName()
|
||||
newCfg.Impersonate.Groups = user.GetGroups()
|
||||
newCfg.Impersonate.Extra = user.GetExtra()
|
||||
|
||||
return dynamic.NewForConfig(&newCfg)
|
||||
}
|
56
pkg/server/store/proxy/error_wrapper.go
Normal file
56
pkg/server/store/proxy/error_wrapper.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"github.com/rancher/steve/pkg/schemaserver/httperror"
|
||||
"github.com/rancher/steve/pkg/schemaserver/types"
|
||||
"github.com/rancher/wrangler/pkg/schemas/validation"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
)
|
||||
|
||||
type errorStore struct {
|
||||
types.Store
|
||||
}
|
||||
|
||||
func (e *errorStore) ByID(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) {
|
||||
data, err := e.Store.ByID(apiOp, schema, id)
|
||||
return data, translateError(err)
|
||||
}
|
||||
|
||||
func (e *errorStore) List(apiOp *types.APIRequest, schema *types.APISchema) (types.APIObjectList, error) {
|
||||
data, err := e.Store.List(apiOp, schema)
|
||||
return data, translateError(err)
|
||||
}
|
||||
|
||||
func (e *errorStore) Create(apiOp *types.APIRequest, schema *types.APISchema, data types.APIObject) (types.APIObject, error) {
|
||||
data, err := e.Store.Create(apiOp, schema, data)
|
||||
return data, translateError(err)
|
||||
|
||||
}
|
||||
|
||||
func (e *errorStore) Update(apiOp *types.APIRequest, schema *types.APISchema, data types.APIObject, id string) (types.APIObject, error) {
|
||||
data, err := e.Store.Update(apiOp, schema, data, id)
|
||||
return data, translateError(err)
|
||||
|
||||
}
|
||||
|
||||
func (e *errorStore) Delete(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) {
|
||||
data, err := e.Store.Delete(apiOp, schema, id)
|
||||
return data, translateError(err)
|
||||
|
||||
}
|
||||
|
||||
func (e *errorStore) Watch(apiOp *types.APIRequest, schema *types.APISchema, wr types.WatchRequest) (chan types.APIEvent, error) {
|
||||
data, err := e.Store.Watch(apiOp, schema, wr)
|
||||
return data, translateError(err)
|
||||
}
|
||||
|
||||
func translateError(err error) error {
|
||||
if apiError, ok := err.(errors.APIStatus); ok {
|
||||
status := apiError.Status()
|
||||
return httperror.NewAPIError(validation.ErrorCode{
|
||||
Status: int(status.Code),
|
||||
Code: string(status.Reason),
|
||||
}, status.Message)
|
||||
}
|
||||
return err
|
||||
}
|
303
pkg/server/store/proxy/proxy_store.go
Normal file
303
pkg/server/store/proxy/proxy_store.go
Normal file
@@ -0,0 +1,303 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rancher/steve/pkg/attributes"
|
||||
"github.com/rancher/steve/pkg/schemaserver/types"
|
||||
"github.com/rancher/wrangler/pkg/data"
|
||||
"github.com/rancher/wrangler/pkg/schemas/validation"
|
||||
"github.com/sirupsen/logrus"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
apitypes "k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/client-go/dynamic"
|
||||
)
|
||||
|
||||
var (
|
||||
lowerChars = regexp.MustCompile("[a-z]+")
|
||||
)
|
||||
|
||||
type ClientGetter interface {
|
||||
Client(ctx *types.APIRequest, schema *types.APISchema, namespace string) (dynamic.ResourceInterface, error)
|
||||
}
|
||||
|
||||
type Store struct {
|
||||
clientGetter ClientGetter
|
||||
}
|
||||
|
||||
func NewProxyStore(clientGetter ClientGetter) types.Store {
|
||||
return &errorStore{
|
||||
Store: &Store{
|
||||
clientGetter: clientGetter,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Store) ByID(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) {
|
||||
result, err := s.byID(apiOp, schema, id)
|
||||
return toAPI(schema, result), err
|
||||
}
|
||||
|
||||
func decodeParams(apiOp *types.APIRequest, target runtime.Object) error {
|
||||
return metav1.ParameterCodec.DecodeParameters(apiOp.Request.URL.Query(), metav1.SchemeGroupVersion, target)
|
||||
}
|
||||
|
||||
func toAPI(schema *types.APISchema, obj *unstructured.Unstructured) types.APIObject {
|
||||
if obj == nil {
|
||||
return types.APIObject{}
|
||||
}
|
||||
|
||||
gvr := attributes.GVR(schema)
|
||||
|
||||
id := obj.GetName()
|
||||
ns := obj.GetNamespace()
|
||||
if ns != "" {
|
||||
id = fmt.Sprintf("%s/%s", ns, id)
|
||||
}
|
||||
t := fmt.Sprintf("%s/%s/%s", gvr.Group, gvr.Version, gvr.Resource)
|
||||
return types.APIObject{
|
||||
Type: t,
|
||||
ID: id,
|
||||
Object: obj,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Store) byID(apiOp *types.APIRequest, schema *types.APISchema, id string) (*unstructured.Unstructured, error) {
|
||||
k8sClient, err := s.clientGetter.Client(apiOp, schema, apiOp.Namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
opts := metav1.GetOptions{}
|
||||
if err := decodeParams(apiOp, &opts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return k8sClient.Get(id, opts)
|
||||
}
|
||||
|
||||
func (s *Store) List(apiOp *types.APIRequest, schema *types.APISchema) (types.APIObjectList, error) {
|
||||
k8sClient, err := s.clientGetter.Client(apiOp, schema, apiOp.Namespace)
|
||||
if err != nil {
|
||||
return types.APIObjectList{}, err
|
||||
}
|
||||
|
||||
opts := metav1.ListOptions{}
|
||||
if err := decodeParams(apiOp, &opts); err != nil {
|
||||
return types.APIObjectList{}, nil
|
||||
}
|
||||
|
||||
resultList, err := k8sClient.List(opts)
|
||||
if err != nil {
|
||||
return types.APIObjectList{}, err
|
||||
}
|
||||
|
||||
result := types.APIObjectList{
|
||||
Revision: resultList.GetResourceVersion(),
|
||||
Continue: resultList.GetContinue(),
|
||||
}
|
||||
|
||||
for i := range resultList.Items {
|
||||
result.Objects = append(result.Objects, toAPI(schema, &resultList.Items[i]))
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func returnErr(err error, c chan types.APIEvent) {
|
||||
c <- types.APIEvent{
|
||||
Name: "resource.error",
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Store) listAndWatch(apiOp *types.APIRequest, k8sClient dynamic.ResourceInterface, schema *types.APISchema, w types.WatchRequest, result chan types.APIEvent) {
|
||||
rev := w.Revision
|
||||
if rev == "" {
|
||||
list, err := k8sClient.List(metav1.ListOptions{
|
||||
Limit: 1,
|
||||
})
|
||||
if err != nil {
|
||||
returnErr(errors.Wrapf(err, "failed to list %s", schema.ID), result)
|
||||
return
|
||||
}
|
||||
rev = list.GetResourceVersion()
|
||||
} else if rev == "-1" {
|
||||
rev = ""
|
||||
}
|
||||
|
||||
timeout := int64(60 * 30)
|
||||
watcher, err := k8sClient.Watch(metav1.ListOptions{
|
||||
Watch: true,
|
||||
TimeoutSeconds: &timeout,
|
||||
ResourceVersion: rev,
|
||||
})
|
||||
if err != nil {
|
||||
returnErr(errors.Wrapf(err, "stopping watch for %s: %v", schema.ID, err), result)
|
||||
return
|
||||
}
|
||||
defer watcher.Stop()
|
||||
logrus.Debugf("opening watcher for %s", schema.ID)
|
||||
|
||||
go func() {
|
||||
<-apiOp.Request.Context().Done()
|
||||
watcher.Stop()
|
||||
}()
|
||||
|
||||
for event := range watcher.ResultChan() {
|
||||
data := event.Object.(*unstructured.Unstructured)
|
||||
result <- s.toAPIEvent(apiOp, schema, event.Type, data)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Store) Watch(apiOp *types.APIRequest, schema *types.APISchema, w types.WatchRequest) (chan types.APIEvent, error) {
|
||||
k8sClient, err := s.clientGetter.Client(apiOp, schema, apiOp.Namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(chan types.APIEvent)
|
||||
go func() {
|
||||
s.listAndWatch(apiOp, k8sClient, schema, w, result)
|
||||
logrus.Debugf("closing watcher for %s", schema.ID)
|
||||
close(result)
|
||||
}()
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *Store) toAPIEvent(apiOp *types.APIRequest, schema *types.APISchema, et watch.EventType, obj *unstructured.Unstructured) types.APIEvent {
|
||||
name := types.ChangeAPIEvent
|
||||
switch et {
|
||||
case watch.Deleted:
|
||||
name = types.RemoveAPIEvent
|
||||
case watch.Added:
|
||||
name = types.CreateAPIEvent
|
||||
}
|
||||
|
||||
return types.APIEvent{
|
||||
Name: name,
|
||||
Revision: obj.GetResourceVersion(),
|
||||
Object: toAPI(schema, obj),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Store) Create(apiOp *types.APIRequest, schema *types.APISchema, params types.APIObject) (types.APIObject, error) {
|
||||
var (
|
||||
resp *unstructured.Unstructured
|
||||
)
|
||||
|
||||
input := params.Data()
|
||||
|
||||
if input == nil {
|
||||
input = data.Object{}
|
||||
}
|
||||
|
||||
name := types.Name(input)
|
||||
ns := types.Namespace(input)
|
||||
if name == "" && input.String("metadata", "generateName") == "" {
|
||||
input.SetNested(schema.ID[0:1]+"-", "metadata", "generatedName")
|
||||
}
|
||||
|
||||
gvk := attributes.GVK(schema)
|
||||
input["apiVersion"], input["kind"] = gvk.ToAPIVersionAndKind()
|
||||
|
||||
k8sClient, err := s.clientGetter.Client(apiOp, schema, ns)
|
||||
if err != nil {
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
|
||||
opts := metav1.CreateOptions{}
|
||||
if err := decodeParams(apiOp, &opts); err != nil {
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
|
||||
resp, err = k8sClient.Create(&unstructured.Unstructured{Object: input}, opts)
|
||||
return toAPI(schema, resp), err
|
||||
}
|
||||
|
||||
func (s *Store) Update(apiOp *types.APIRequest, schema *types.APISchema, params types.APIObject, id string) (types.APIObject, error) {
|
||||
var (
|
||||
err error
|
||||
input = params.Data()
|
||||
)
|
||||
|
||||
ns := types.Namespace(input)
|
||||
k8sClient, err := s.clientGetter.Client(apiOp, schema, ns)
|
||||
if err != nil {
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
|
||||
if apiOp.Method == http.MethodPatch {
|
||||
bytes, err := ioutil.ReadAll(io.LimitReader(apiOp.Request.Body, 2<<20))
|
||||
if err != nil {
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
|
||||
pType := apitypes.StrategicMergePatchType
|
||||
if apiOp.Request.Header.Get("content-type") == string(apitypes.JSONPatchType) {
|
||||
pType = apitypes.JSONPatchType
|
||||
}
|
||||
|
||||
opts := metav1.PatchOptions{}
|
||||
if err := decodeParams(apiOp, &opts); err != nil {
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
|
||||
resp, err := k8sClient.Patch(id, pType, bytes, opts)
|
||||
if err != nil {
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
|
||||
return toAPI(schema, resp), nil
|
||||
}
|
||||
|
||||
resourceVersion := input.String("metadata", "resourceVersion")
|
||||
if resourceVersion == "" {
|
||||
return types.APIObject{}, fmt.Errorf("metadata.resourceVersion is required for update")
|
||||
}
|
||||
|
||||
opts := metav1.UpdateOptions{}
|
||||
if err := decodeParams(apiOp, &opts); err != nil {
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
|
||||
resp, err := k8sClient.Update(&unstructured.Unstructured{Object: input}, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
|
||||
return toAPI(schema, resp), nil
|
||||
}
|
||||
|
||||
func (s *Store) Delete(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) {
|
||||
opts := metav1.DeleteOptions{}
|
||||
if err := decodeParams(apiOp, &opts); err != nil {
|
||||
return types.APIObject{}, nil
|
||||
}
|
||||
|
||||
k8sClient, err := s.clientGetter.Client(apiOp, schema, apiOp.Namespace)
|
||||
if err != nil {
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
|
||||
if err := k8sClient.Delete(id, &opts); err != nil {
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
|
||||
obj, err := s.byID(apiOp, schema, id)
|
||||
if err != nil {
|
||||
// ignore lookup error
|
||||
return types.APIObject{}, validation.ErrorCode{
|
||||
Status: http.StatusNoContent,
|
||||
}
|
||||
}
|
||||
return toAPI(schema, obj), nil
|
||||
}
|
Reference in New Issue
Block a user