1
0
mirror of https://github.com/rancher/steve.git synced 2025-08-31 15:11:31 +00:00
This commit is contained in:
Darren Shepherd
2020-01-30 22:37:59 -07:00
parent 19c6732de0
commit 8b42d0aff8
71 changed files with 4024 additions and 507 deletions

View 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
View 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

View File

@@ -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) {

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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,
})

View 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)
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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),
}
}

View File

@@ -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
}

View File

@@ -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)
}

View 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)
}

View 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
}

View 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
}