mirror of
https://github.com/niusmallnan/steve.git
synced 2025-09-09 09:00:31 +00:00
Fix update/patch and add webhook auth
This commit is contained in:
16
main.go
16
main.go
@@ -23,18 +23,22 @@ func main() {
|
||||
app.Version = version.FriendlyVersion()
|
||||
app.Usage = ""
|
||||
app.Flags = []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "authentication",
|
||||
Destination: &config.Authentication,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "webhook-kubeconfig",
|
||||
EnvVar: "WEBHOOK_KUBECONFIG",
|
||||
Value: "webhook-kubeconfig.yaml",
|
||||
Destination: &config.WebhookKubeconfig,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "kubeconfig",
|
||||
EnvVar: "KUBECONFIG",
|
||||
Value: "",
|
||||
Destination: &config.Kubeconfig,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "namespace",
|
||||
EnvVar: "NAMESPACE",
|
||||
Value: "default",
|
||||
Destination: &config.Namespace,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "listen-address",
|
||||
EnvVar: "LISTEN_ADDRESS",
|
||||
|
@@ -6,13 +6,17 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/rancher/naok/pkg/attributes"
|
||||
schema2 "github.com/rancher/naok/pkg/resources/schema"
|
||||
"github.com/rancher/naok/pkg/resources/schema/converter"
|
||||
"github.com/rancher/norman/pkg/types"
|
||||
apiextcontrollerv1beta1 "github.com/rancher/wrangler-api/pkg/generated/controllers/apiextensions.k8s.io/v1beta1"
|
||||
v1 "github.com/rancher/wrangler-api/pkg/generated/controllers/apiregistration.k8s.io/v1"
|
||||
"github.com/sirupsen/logrus"
|
||||
authorizationv1 "k8s.io/api/authorization/v1"
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||
"k8s.io/client-go/discovery"
|
||||
authorizationv1client "k8s.io/client-go/kubernetes/typed/authorization/v1"
|
||||
apiv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
|
||||
)
|
||||
|
||||
@@ -27,6 +31,7 @@ type handler struct {
|
||||
schemas *schema2.Collection
|
||||
client discovery.DiscoveryInterface
|
||||
crd apiextcontrollerv1beta1.CustomResourceDefinitionClient
|
||||
ssar authorizationv1client.SelfSubjectAccessReviewInterface
|
||||
handler SchemasHandler
|
||||
}
|
||||
|
||||
@@ -34,6 +39,7 @@ func Register(ctx context.Context,
|
||||
discovery discovery.DiscoveryInterface,
|
||||
crd apiextcontrollerv1beta1.CustomResourceDefinitionController,
|
||||
apiService v1.APIServiceController,
|
||||
ssar authorizationv1client.SelfSubjectAccessReviewInterface,
|
||||
schemasHandler SchemasHandler,
|
||||
schemas *schema2.Collection) (init func() error) {
|
||||
|
||||
@@ -42,6 +48,7 @@ func Register(ctx context.Context,
|
||||
schemas: schemas,
|
||||
handler: schemasHandler,
|
||||
crd: crd,
|
||||
ssar: ssar,
|
||||
}
|
||||
|
||||
apiService.OnChange(ctx, "schema", h.OnChangeAPIService)
|
||||
@@ -75,6 +82,24 @@ func (h *handler) queueRefresh() {
|
||||
}()
|
||||
}
|
||||
|
||||
func isListWatchable(schema *types.Schema) bool {
|
||||
var (
|
||||
canList bool
|
||||
canWatch bool
|
||||
)
|
||||
|
||||
for _, verb := range attributes.Verbs(schema) {
|
||||
switch verb {
|
||||
case "list":
|
||||
canList = true
|
||||
case "watch":
|
||||
canWatch = true
|
||||
}
|
||||
}
|
||||
|
||||
return canList && canWatch
|
||||
}
|
||||
|
||||
func (h *handler) refreshAll() error {
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
@@ -89,7 +114,19 @@ func (h *handler) refreshAll() error {
|
||||
return err
|
||||
}
|
||||
|
||||
h.schemas.Reset(schemas)
|
||||
filteredSchemas := map[string]*types.Schema{}
|
||||
for id, schema := range schemas {
|
||||
if isListWatchable(schema) {
|
||||
if ok, err := h.allowed(schema); err != nil {
|
||||
return err
|
||||
} else if !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
filteredSchemas[id] = schema
|
||||
}
|
||||
|
||||
h.schemas.Reset(filteredSchemas)
|
||||
if h.handler != nil {
|
||||
return h.handler.OnSchemas(h.schemas)
|
||||
}
|
||||
@@ -97,6 +134,24 @@ func (h *handler) refreshAll() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *handler) allowed(schema *types.Schema) (bool, error) {
|
||||
gvr := attributes.GVR(schema)
|
||||
ssar, err := h.ssar.Create(&authorizationv1.SelfSubjectAccessReview{
|
||||
Spec: authorizationv1.SelfSubjectAccessReviewSpec{
|
||||
ResourceAttributes: &authorizationv1.ResourceAttributes{
|
||||
Verb: "list",
|
||||
Group: gvr.Group,
|
||||
Version: gvr.Version,
|
||||
Resource: gvr.Resource,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return ssar.Status.Allowed && !ssar.Status.Denied, nil
|
||||
}
|
||||
|
||||
func (h *handler) needToSync() bool {
|
||||
old := atomic.SwapInt32(&h.toSync, 0)
|
||||
return old == 1
|
||||
|
@@ -2,33 +2,32 @@ package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/rancher/norman/pkg/auth"
|
||||
"net/http"
|
||||
|
||||
"github.com/rancher/wrangler/pkg/generic"
|
||||
schema2 "k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
"github.com/rancher/naok/pkg/clustercache"
|
||||
|
||||
"github.com/rancher/wrangler-api/pkg/generated/controllers/core"
|
||||
|
||||
"github.com/rancher/naok/pkg/accesscontrol"
|
||||
"github.com/rancher/naok/pkg/client"
|
||||
"github.com/rancher/naok/pkg/clustercache"
|
||||
"github.com/rancher/naok/pkg/controllers/schema"
|
||||
"github.com/rancher/naok/pkg/resources"
|
||||
"github.com/rancher/naok/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"
|
||||
"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
|
||||
Namespace string
|
||||
ListenAddress string
|
||||
WebhookKubeconfig string
|
||||
Authentication bool
|
||||
}
|
||||
|
||||
func Run(ctx context.Context, cfg Config) error {
|
||||
@@ -37,6 +36,9 @@ func Run(ctx context.Context, cfg Config) error {
|
||||
return err
|
||||
}
|
||||
|
||||
restConfig.QPS = 100
|
||||
restConfig.Burst = 100
|
||||
|
||||
rbac, err := rbaccontroller.NewFactoryFromConfig(restConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -80,6 +82,7 @@ func Run(ctx context.Context, cfg Config) error {
|
||||
k8s.Discovery(),
|
||||
crd.Apiextensions().V1beta1().CustomResourceDefinition(),
|
||||
api.Apiregistration().V1().APIService(),
|
||||
k8s.AuthorizationV1().SelfSubjectAccessReviews(),
|
||||
ccache,
|
||||
sf)
|
||||
|
||||
@@ -88,6 +91,14 @@ func Run(ctx context.Context, cfg Config) error {
|
||||
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())
|
||||
@@ -106,6 +117,12 @@ func Run(ctx context.Context, cfg Config) error {
|
||||
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
|
||||
}
|
||||
|
2
vendor/github.com/rancher/norman/pkg/api/server.go
generated
vendored
2
vendor/github.com/rancher/norman/pkg/api/server.go
generated
vendored
@@ -195,6 +195,8 @@ func determineVerb(apiOp *types.APIRequest) Verb {
|
||||
return Get
|
||||
case http.MethodPost:
|
||||
return Create
|
||||
case http.MethodPatch:
|
||||
return Update
|
||||
case http.MethodPut:
|
||||
return Update
|
||||
case http.MethodDelete:
|
||||
|
93
vendor/github.com/rancher/norman/pkg/auth/filter.go
generated
vendored
Normal file
93
vendor/github.com/rancher/norman/pkg/auth/filter.go
generated
vendored
Normal file
@@ -0,0 +1,93 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
v1 "k8s.io/api/authentication/v1"
|
||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/apiserver/plugin/pkg/authenticator/token/webhook"
|
||||
)
|
||||
|
||||
type Authenticator interface {
|
||||
Authenticate(req *http.Request) (user.Info, bool, error)
|
||||
}
|
||||
|
||||
func NewWebhookAuthenticator(kubeConfigFile string) (Authenticator, error) {
|
||||
wh, err := webhook.New(kubeConfigFile, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &webhookAuth{
|
||||
auth: wh,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewWebhookMiddleware(kubeConfigFile string) (func(http.ResponseWriter, *http.Request, http.Handler), error) {
|
||||
auth, err := NewWebhookAuthenticator(kubeConfigFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ToMiddleware(auth), nil
|
||||
}
|
||||
|
||||
type webhookAuth struct {
|
||||
auth authenticator.Token
|
||||
}
|
||||
|
||||
func (w *webhookAuth) Authenticate(req *http.Request) (user.Info, bool, error) {
|
||||
token := req.Header.Get("Authorization")
|
||||
if strings.HasPrefix(token, "Bearer ") {
|
||||
token = strings.TrimPrefix(token, "Bearer ")
|
||||
} else {
|
||||
token = ""
|
||||
}
|
||||
|
||||
if token == "" {
|
||||
cookie, err := req.Cookie("R_SESS")
|
||||
if err != nil && err != http.ErrNoCookie {
|
||||
return nil, false, err
|
||||
} else if err != http.ErrNoCookie && len(cookie.Value) > 0 {
|
||||
token = "cookie://" + cookie.Value
|
||||
}
|
||||
}
|
||||
|
||||
if token == "" {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
resp, ok, err := w.auth.AuthenticateToken(req.Context(), token)
|
||||
if resp == nil {
|
||||
return nil, ok, err
|
||||
}
|
||||
return resp.User, ok, err
|
||||
}
|
||||
|
||||
func ToMiddleware(auth Authenticator) func(rw http.ResponseWriter, req *http.Request, next http.Handler) {
|
||||
return func(rw http.ResponseWriter, req *http.Request, next http.Handler) {
|
||||
info, ok, err := auth.Authenticate(req)
|
||||
if err != nil {
|
||||
rw.WriteHeader(http.StatusServiceUnavailable)
|
||||
rw.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if !ok {
|
||||
rw.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := request.WithUser(req.Context(), info)
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
req.Header.Set(v1.ImpersonateUserHeader, info.GetName())
|
||||
for _, group := range info.GetGroups() {
|
||||
req.Header.Set(v1.ImpersonateGroupHeader, group)
|
||||
}
|
||||
|
||||
next.ServeHTTP(rw, req)
|
||||
}
|
||||
}
|
2
vendor/github.com/rancher/norman/pkg/data/data.go
generated
vendored
2
vendor/github.com/rancher/norman/pkg/data/data.go
generated
vendored
@@ -22,7 +22,7 @@ func (o Object) Map(names ...string) Object {
|
||||
func (o Object) Slice(names ...string) (result []Object) {
|
||||
v := values.GetValueN(o, names...)
|
||||
for _, item := range convert.ToInterfaceSlice(v) {
|
||||
result = append(result, Object(convert.ToMapInterface(item)))
|
||||
result = append(result, convert.ToMapInterface(item))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
40
vendor/github.com/rancher/norman/pkg/store/proxy/proxy_store.go
generated
vendored
40
vendor/github.com/rancher/norman/pkg/store/proxy/proxy_store.go
generated
vendored
@@ -1,17 +1,22 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
types2 "k8s.io/apimachinery/pkg/types"
|
||||
|
||||
"github.com/rancher/norman/pkg/types/convert"
|
||||
|
||||
errors2 "github.com/pkg/errors"
|
||||
|
||||
"github.com/rancher/norman/pkg/types"
|
||||
"github.com/rancher/norman/pkg/types/convert/merge"
|
||||
"github.com/rancher/norman/pkg/types/values"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
@@ -260,31 +265,32 @@ func (s *Store) Update(apiOp *types.APIRequest, schema *types.Schema, params typ
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
resp, err := k8sClient.Get(id, metav1.GetOptions{})
|
||||
if apiOp.Method == http.MethodPatch {
|
||||
bytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
|
||||
resourceVersion, existing := resp.GetResourceVersion(), resp.Object
|
||||
existing = merge.APIUpdateMerge(schema.InternalSchema, apiOp.Schemas, existing, data, apiOp.Option("replace") == "true")
|
||||
|
||||
values.PutValue(existing, resourceVersion, "metadata", "resourceVersion")
|
||||
if len(apiOp.Namespaces) > 0 {
|
||||
values.PutValue(existing, apiOp.Namespaces[0], "metadata", "namespace")
|
||||
}
|
||||
values.PutValue(existing, id, "metadata", "name")
|
||||
|
||||
resp, err = k8sClient.Update(&unstructured.Unstructured{Object: existing}, metav1.UpdateOptions{})
|
||||
if errors.IsConflict(err) {
|
||||
continue
|
||||
} else if err != nil {
|
||||
resp, err := k8sClient.Patch(id, types2.StrategicMergePatchType, bytes, metav1.PatchOptions{})
|
||||
if err != nil {
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
|
||||
_, result, err = s.singleResult(apiOp, schema, resp)
|
||||
return types.ToAPI(result), err
|
||||
}
|
||||
|
||||
resourceVersion := convert.ToString(values.GetValueN(data, "metadata", "resourceVersion"))
|
||||
if resourceVersion == "" {
|
||||
return types.APIObject{}, fmt.Errorf("metadata.resourceVersion is required for update")
|
||||
}
|
||||
|
||||
resp, err := k8sClient.Update(&unstructured.Unstructured{Object: data}, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
return types.APIObject{}, err
|
||||
}
|
||||
|
||||
_, result, err = s.singleResult(apiOp, schema, resp)
|
||||
return types.ToAPI(result), err
|
||||
}
|
||||
|
||||
|
156
vendor/github.com/rancher/norman/pkg/types/convert/merge/merge.go
generated
vendored
156
vendor/github.com/rancher/norman/pkg/types/convert/merge/merge.go
generated
vendored
@@ -1,156 +0,0 @@
|
||||
package merge
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/rancher/norman/pkg/types"
|
||||
convert2 "github.com/rancher/norman/pkg/types/convert"
|
||||
definition2 "github.com/rancher/norman/pkg/types/definition"
|
||||
)
|
||||
|
||||
func APIUpdateMerge(schema *types.Schema, schemas *types.Schemas, dest, src map[string]interface{}, replace bool) map[string]interface{} {
|
||||
result := UpdateMerge(schema, schemas, dest, src, replace)
|
||||
if s, ok := dest["status"]; ok {
|
||||
result["status"] = s
|
||||
}
|
||||
if m, ok := dest["metadata"]; ok {
|
||||
result["metadata"] = mergeMetadata(convert2.ToMapInterface(m), convert2.ToMapInterface(src["metadata"]))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func UpdateMerge(schema *types.Schema, schemas *types.Schemas, dest, src map[string]interface{}, replace bool) map[string]interface{} {
|
||||
return mergeMaps("", nil, schema, schemas, replace, dest, src)
|
||||
}
|
||||
|
||||
func isProtected(k string) bool {
|
||||
if !strings.Contains(k, "cattle.io/") || (isField(k) && k != "field.cattle.io/creatorId") {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func isField(k string) bool {
|
||||
return strings.HasPrefix(k, "field.cattle.io/")
|
||||
}
|
||||
|
||||
func mergeProtected(dest, src map[string]interface{}) map[string]interface{} {
|
||||
if src == nil {
|
||||
return dest
|
||||
}
|
||||
|
||||
result := copyMap(dest)
|
||||
|
||||
for k, v := range src {
|
||||
if isProtected(k) {
|
||||
continue
|
||||
}
|
||||
result[k] = v
|
||||
}
|
||||
|
||||
for k := range dest {
|
||||
if isProtected(k) || isField(k) {
|
||||
continue
|
||||
}
|
||||
if _, ok := src[k]; !ok {
|
||||
delete(result, k)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func mergeMetadata(dest map[string]interface{}, src map[string]interface{}) map[string]interface{} {
|
||||
result := copyMap(dest)
|
||||
|
||||
labels := convert2.ToMapInterface(dest["labels"])
|
||||
srcLabels := convert2.ToMapInterface(src["labels"])
|
||||
labels = mergeProtected(labels, srcLabels)
|
||||
|
||||
annotations := convert2.ToMapInterface(dest["annotations"])
|
||||
srcAnnotation := convert2.ToMapInterface(src["annotations"])
|
||||
annotations = mergeProtected(annotations, srcAnnotation)
|
||||
|
||||
result["labels"] = labels
|
||||
result["annotations"] = annotations
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func merge(field, fieldType string, parentSchema, schema *types.Schema, schemas *types.Schemas, replace bool, dest, src interface{}) interface{} {
|
||||
if isMap(field, schema, schemas) {
|
||||
return src
|
||||
}
|
||||
|
||||
sm, smOk := src.(map[string]interface{})
|
||||
dm, dmOk := dest.(map[string]interface{})
|
||||
if smOk && dmOk {
|
||||
fieldType, fieldSchema := getSchema(field, fieldType, parentSchema, schema, schemas)
|
||||
return mergeMaps(fieldType, schema, fieldSchema, schemas, replace, dm, sm)
|
||||
}
|
||||
return src
|
||||
}
|
||||
|
||||
func getSchema(field, parentFieldType string, parentSchema, schema *types.Schema, schemas *types.Schemas) (string, *types.Schema) {
|
||||
if schema == nil {
|
||||
if definition2.IsMapType(parentFieldType) && parentSchema != nil {
|
||||
subType := definition2.SubType(parentFieldType)
|
||||
s := schemas.Schema(subType)
|
||||
if s != nil && s.InternalSchema != nil {
|
||||
s = s.InternalSchema
|
||||
}
|
||||
return subType, s
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
fieldType := schema.ResourceFields[field].Type
|
||||
s := schemas.Schema(fieldType)
|
||||
if s != nil && s.InternalSchema != nil {
|
||||
return fieldType, s.InternalSchema
|
||||
}
|
||||
return fieldType, s
|
||||
}
|
||||
|
||||
func isMap(field string, schema *types.Schema, schemas *types.Schemas) bool {
|
||||
if schema == nil {
|
||||
return false
|
||||
}
|
||||
f := schema.ResourceFields[field]
|
||||
mapType := definition2.IsMapType(f.Type)
|
||||
if !mapType {
|
||||
return false
|
||||
}
|
||||
|
||||
subType := definition2.SubType(f.Type)
|
||||
return schemas.Schema(subType) == nil
|
||||
}
|
||||
|
||||
func mergeMaps(fieldType string, parentSchema, schema *types.Schema, schemas *types.Schemas, replace bool, dest map[string]interface{}, src map[string]interface{}) map[string]interface{} {
|
||||
result := copyMapReplace(schema, dest, replace)
|
||||
for k, v := range src {
|
||||
result[k] = merge(k, fieldType, parentSchema, schema, schemas, replace, dest[k], v)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func copyMap(src map[string]interface{}) map[string]interface{} {
|
||||
result := map[string]interface{}{}
|
||||
for k, v := range src {
|
||||
result[k] = v
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func copyMapReplace(schema *types.Schema, src map[string]interface{}, replace bool) map[string]interface{} {
|
||||
result := map[string]interface{}{}
|
||||
for k, v := range src {
|
||||
if replace && schema != nil {
|
||||
f := schema.ResourceFields[k]
|
||||
if f.Update {
|
||||
continue
|
||||
}
|
||||
}
|
||||
result[k] = v
|
||||
}
|
||||
return result
|
||||
}
|
9
vendor/github.com/rancher/norman/pkg/urlbuilder/base.go
generated
vendored
9
vendor/github.com/rancher/norman/pkg/urlbuilder/base.go
generated
vendored
@@ -45,7 +45,14 @@ func getHost(r *http.Request, scheme string) string {
|
||||
func getScheme(r *http.Request) string {
|
||||
scheme := r.Header.Get(ForwardedProtoHeader)
|
||||
if scheme != "" {
|
||||
return scheme
|
||||
switch scheme {
|
||||
case "ws":
|
||||
return "http"
|
||||
case "wss":
|
||||
return "https"
|
||||
default:
|
||||
return scheme
|
||||
}
|
||||
} else if r.TLS != nil {
|
||||
return "https"
|
||||
}
|
||||
|
2
vendor/k8s.io/apimachinery/pkg/apis/meta/v1/types.go
generated
vendored
2
vendor/k8s.io/apimachinery/pkg/apis/meta/v1/types.go
generated
vendored
@@ -1081,7 +1081,7 @@ type LabelSelector struct {
|
||||
MatchLabels map[string]string `json:"matchLabels,omitempty" protobuf:"bytes,1,rep,name=matchLabels"`
|
||||
// matchExpressions is a list of label selector requirements. The requirements are ANDed.
|
||||
// +optional
|
||||
MatchExpressions []LabelSelectorRequirement `json:",omitempty" protobuf:"bytes,2,rep,name=matchExpressions"`
|
||||
MatchExpressions []LabelSelectorRequirement `json:"matchExpressions,omitempty" protobuf:"bytes,2,rep,name=matchExpressions"`
|
||||
}
|
||||
|
||||
// A label selector requirement is a selector that contains values, a key, and an operator that
|
||||
|
90
vendor/k8s.io/apiserver/pkg/authentication/authenticator/audagnostic.go
generated
vendored
Normal file
90
vendor/k8s.io/apiserver/pkg/authentication/authenticator/audagnostic.go
generated
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package authenticator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func authenticate(ctx context.Context, implicitAuds Audiences, authenticate func() (*Response, bool, error)) (*Response, bool, error) {
|
||||
targetAuds, ok := AudiencesFrom(ctx)
|
||||
// We can remove this once api audiences is never empty. That will probably
|
||||
// be N releases after TokenRequest is GA.
|
||||
if !ok {
|
||||
return authenticate()
|
||||
}
|
||||
auds := implicitAuds.Intersect(targetAuds)
|
||||
if len(auds) == 0 {
|
||||
return nil, false, nil
|
||||
}
|
||||
resp, ok, err := authenticate()
|
||||
if err != nil || !ok {
|
||||
return nil, false, err
|
||||
}
|
||||
if len(resp.Audiences) > 0 {
|
||||
// maybe the authenticator was audience aware after all.
|
||||
return nil, false, fmt.Errorf("audience agnostic authenticator wrapped an authenticator that returned audiences: %q", resp.Audiences)
|
||||
}
|
||||
resp.Audiences = auds
|
||||
return resp, true, nil
|
||||
}
|
||||
|
||||
type audAgnosticRequestAuthenticator struct {
|
||||
implicit Audiences
|
||||
delegate Request
|
||||
}
|
||||
|
||||
var _ = Request(&audAgnosticRequestAuthenticator{})
|
||||
|
||||
func (a *audAgnosticRequestAuthenticator) AuthenticateRequest(req *http.Request) (*Response, bool, error) {
|
||||
return authenticate(req.Context(), a.implicit, func() (*Response, bool, error) {
|
||||
return a.delegate.AuthenticateRequest(req)
|
||||
})
|
||||
}
|
||||
|
||||
// WrapAudienceAgnosticRequest wraps an audience agnostic request authenticator
|
||||
// to restrict its accepted audiences to a set of implicit audiences.
|
||||
func WrapAudienceAgnosticRequest(implicit Audiences, delegate Request) Request {
|
||||
return &audAgnosticRequestAuthenticator{
|
||||
implicit: implicit,
|
||||
delegate: delegate,
|
||||
}
|
||||
}
|
||||
|
||||
type audAgnosticTokenAuthenticator struct {
|
||||
implicit Audiences
|
||||
delegate Token
|
||||
}
|
||||
|
||||
var _ = Token(&audAgnosticTokenAuthenticator{})
|
||||
|
||||
func (a *audAgnosticTokenAuthenticator) AuthenticateToken(ctx context.Context, tok string) (*Response, bool, error) {
|
||||
return authenticate(ctx, a.implicit, func() (*Response, bool, error) {
|
||||
return a.delegate.AuthenticateToken(ctx, tok)
|
||||
})
|
||||
}
|
||||
|
||||
// WrapAudienceAgnosticToken wraps an audience agnostic token authenticator to
|
||||
// restrict its accepted audiences to a set of implicit audiences.
|
||||
func WrapAudienceAgnosticToken(implicit Audiences, delegate Token) Token {
|
||||
return &audAgnosticTokenAuthenticator{
|
||||
implicit: implicit,
|
||||
delegate: delegate,
|
||||
}
|
||||
}
|
63
vendor/k8s.io/apiserver/pkg/authentication/authenticator/audiences.go
generated
vendored
Normal file
63
vendor/k8s.io/apiserver/pkg/authentication/authenticator/audiences.go
generated
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package authenticator
|
||||
|
||||
import "context"
|
||||
|
||||
// Audiences is a container for the Audiences of a token.
|
||||
type Audiences []string
|
||||
|
||||
// The key type is unexported to prevent collisions
|
||||
type key int
|
||||
|
||||
const (
|
||||
// audiencesKey is the context key for request audiences.
|
||||
audiencesKey key = iota
|
||||
)
|
||||
|
||||
// WithAudiences returns a context that stores a request's expected audiences.
|
||||
func WithAudiences(ctx context.Context, auds Audiences) context.Context {
|
||||
return context.WithValue(ctx, audiencesKey, auds)
|
||||
}
|
||||
|
||||
// AudiencesFrom returns a request's expected audiences stored in the request context.
|
||||
func AudiencesFrom(ctx context.Context) (Audiences, bool) {
|
||||
auds, ok := ctx.Value(audiencesKey).(Audiences)
|
||||
return auds, ok
|
||||
}
|
||||
|
||||
// Has checks if Audiences contains a specific audiences.
|
||||
func (a Audiences) Has(taud string) bool {
|
||||
for _, aud := range a {
|
||||
if aud == taud {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Intersect intersects Audiences with a target Audiences and returns all
|
||||
// elements in both.
|
||||
func (a Audiences) Intersect(tauds Audiences) Audiences {
|
||||
selected := Audiences{}
|
||||
for _, taud := range tauds {
|
||||
if a.Has(taud) {
|
||||
selected = append(selected, taud)
|
||||
}
|
||||
}
|
||||
return selected
|
||||
}
|
80
vendor/k8s.io/apiserver/pkg/authentication/authenticator/interfaces.go
generated
vendored
Normal file
80
vendor/k8s.io/apiserver/pkg/authentication/authenticator/interfaces.go
generated
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
Copyright 2014 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package authenticator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
)
|
||||
|
||||
// Token checks a string value against a backing authentication store and
|
||||
// returns a Response or an error if the token could not be checked.
|
||||
type Token interface {
|
||||
AuthenticateToken(ctx context.Context, token string) (*Response, bool, error)
|
||||
}
|
||||
|
||||
// Request attempts to extract authentication information from a request and
|
||||
// returns a Response or an error if the request could not be checked.
|
||||
type Request interface {
|
||||
AuthenticateRequest(req *http.Request) (*Response, bool, error)
|
||||
}
|
||||
|
||||
// Password checks a username and password against a backing authentication
|
||||
// store and returns a Response or an error if the password could not be
|
||||
// checked.
|
||||
type Password interface {
|
||||
AuthenticatePassword(ctx context.Context, user, password string) (*Response, bool, error)
|
||||
}
|
||||
|
||||
// TokenFunc is a function that implements the Token interface.
|
||||
type TokenFunc func(ctx context.Context, token string) (*Response, bool, error)
|
||||
|
||||
// AuthenticateToken implements authenticator.Token.
|
||||
func (f TokenFunc) AuthenticateToken(ctx context.Context, token string) (*Response, bool, error) {
|
||||
return f(ctx, token)
|
||||
}
|
||||
|
||||
// RequestFunc is a function that implements the Request interface.
|
||||
type RequestFunc func(req *http.Request) (*Response, bool, error)
|
||||
|
||||
// AuthenticateRequest implements authenticator.Request.
|
||||
func (f RequestFunc) AuthenticateRequest(req *http.Request) (*Response, bool, error) {
|
||||
return f(req)
|
||||
}
|
||||
|
||||
// PasswordFunc is a function that implements the Password interface.
|
||||
type PasswordFunc func(ctx context.Context, user, password string) (*Response, bool, error)
|
||||
|
||||
// AuthenticatePassword implements authenticator.Password.
|
||||
func (f PasswordFunc) AuthenticatePassword(ctx context.Context, user, password string) (*Response, bool, error) {
|
||||
return f(ctx, user, password)
|
||||
}
|
||||
|
||||
// Response is the struct returned by authenticator interfaces upon successful
|
||||
// authentication. It contains information about whether the authenticator
|
||||
// authenticated the request, information about the context of the
|
||||
// authentication, and information about the authenticated user.
|
||||
type Response struct {
|
||||
// Audiences is the set of audiences the authenticator was able to validate
|
||||
// the token against. If the authenticator is not audience aware, this field
|
||||
// will be empty.
|
||||
Audiences Audiences
|
||||
// User is the UserInfo associated with the authentication context.
|
||||
User user.Info
|
||||
}
|
211
vendor/k8s.io/apiserver/pkg/util/webhook/authentication.go
generated
vendored
Normal file
211
vendor/k8s.io/apiserver/pkg/util/webhook/authentication.go
generated
vendored
Normal file
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
)
|
||||
|
||||
// AuthenticationInfoResolverWrapper can be used to inject Dial function to the
|
||||
// rest.Config generated by the resolver.
|
||||
type AuthenticationInfoResolverWrapper func(AuthenticationInfoResolver) AuthenticationInfoResolver
|
||||
|
||||
// NewDefaultAuthenticationInfoResolverWrapper builds a default authn resolver wrapper
|
||||
func NewDefaultAuthenticationInfoResolverWrapper(
|
||||
proxyTransport *http.Transport,
|
||||
kubeapiserverClientConfig *rest.Config) AuthenticationInfoResolverWrapper {
|
||||
|
||||
webhookAuthResolverWrapper := func(delegate AuthenticationInfoResolver) AuthenticationInfoResolver {
|
||||
return &AuthenticationInfoResolverDelegator{
|
||||
ClientConfigForFunc: func(server string) (*rest.Config, error) {
|
||||
if server == "kubernetes.default.svc" {
|
||||
return kubeapiserverClientConfig, nil
|
||||
}
|
||||
return delegate.ClientConfigFor(server)
|
||||
},
|
||||
ClientConfigForServiceFunc: func(serviceName, serviceNamespace string) (*rest.Config, error) {
|
||||
if serviceName == "kubernetes" && serviceNamespace == corev1.NamespaceDefault {
|
||||
return kubeapiserverClientConfig, nil
|
||||
}
|
||||
ret, err := delegate.ClientConfigForService(serviceName, serviceNamespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if proxyTransport != nil && proxyTransport.DialContext != nil {
|
||||
ret.Dial = proxyTransport.DialContext
|
||||
}
|
||||
return ret, err
|
||||
},
|
||||
}
|
||||
}
|
||||
return webhookAuthResolverWrapper
|
||||
}
|
||||
|
||||
// AuthenticationInfoResolver builds rest.Config base on the server or service
|
||||
// name and service namespace.
|
||||
type AuthenticationInfoResolver interface {
|
||||
// ClientConfigFor builds rest.Config based on the server.
|
||||
ClientConfigFor(server string) (*rest.Config, error)
|
||||
// ClientConfigForService builds rest.Config based on the serviceName and
|
||||
// serviceNamespace.
|
||||
ClientConfigForService(serviceName, serviceNamespace string) (*rest.Config, error)
|
||||
}
|
||||
|
||||
// AuthenticationInfoResolverDelegator implements AuthenticationInfoResolver.
|
||||
type AuthenticationInfoResolverDelegator struct {
|
||||
ClientConfigForFunc func(server string) (*rest.Config, error)
|
||||
ClientConfigForServiceFunc func(serviceName, serviceNamespace string) (*rest.Config, error)
|
||||
}
|
||||
|
||||
// ClientConfigFor returns client config for given server.
|
||||
func (a *AuthenticationInfoResolverDelegator) ClientConfigFor(server string) (*rest.Config, error) {
|
||||
return a.ClientConfigForFunc(server)
|
||||
}
|
||||
|
||||
// ClientConfigForService returns client config for given service.
|
||||
func (a *AuthenticationInfoResolverDelegator) ClientConfigForService(serviceName, serviceNamespace string) (*rest.Config, error) {
|
||||
return a.ClientConfigForServiceFunc(serviceName, serviceNamespace)
|
||||
}
|
||||
|
||||
type defaultAuthenticationInfoResolver struct {
|
||||
kubeconfig clientcmdapi.Config
|
||||
}
|
||||
|
||||
// NewDefaultAuthenticationInfoResolver generates an AuthenticationInfoResolver
|
||||
// that builds rest.Config based on the kubeconfig file. kubeconfigFile is the
|
||||
// path to the kubeconfig.
|
||||
func NewDefaultAuthenticationInfoResolver(kubeconfigFile string) (AuthenticationInfoResolver, error) {
|
||||
if len(kubeconfigFile) == 0 {
|
||||
return &defaultAuthenticationInfoResolver{}, nil
|
||||
}
|
||||
|
||||
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
|
||||
loadingRules.ExplicitPath = kubeconfigFile
|
||||
loader := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &clientcmd.ConfigOverrides{})
|
||||
clientConfig, err := loader.RawConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &defaultAuthenticationInfoResolver{kubeconfig: clientConfig}, nil
|
||||
}
|
||||
|
||||
func (c *defaultAuthenticationInfoResolver) ClientConfigFor(server string) (*rest.Config, error) {
|
||||
return c.clientConfig(server)
|
||||
}
|
||||
|
||||
func (c *defaultAuthenticationInfoResolver) ClientConfigForService(serviceName, serviceNamespace string) (*rest.Config, error) {
|
||||
return c.clientConfig(serviceName + "." + serviceNamespace + ".svc")
|
||||
}
|
||||
|
||||
func (c *defaultAuthenticationInfoResolver) clientConfig(target string) (*rest.Config, error) {
|
||||
// exact match
|
||||
if authConfig, ok := c.kubeconfig.AuthInfos[target]; ok {
|
||||
return restConfigFromKubeconfig(authConfig)
|
||||
}
|
||||
|
||||
// star prefixed match
|
||||
serverSteps := strings.Split(target, ".")
|
||||
for i := 1; i < len(serverSteps); i++ {
|
||||
nickName := "*." + strings.Join(serverSteps[i:], ".")
|
||||
if authConfig, ok := c.kubeconfig.AuthInfos[nickName]; ok {
|
||||
return restConfigFromKubeconfig(authConfig)
|
||||
}
|
||||
}
|
||||
|
||||
// if we're trying to hit the kube-apiserver and there wasn't an explicit config, use the in-cluster config
|
||||
if target == "kubernetes.default.svc" {
|
||||
// if we can find an in-cluster-config use that. If we can't, fall through.
|
||||
inClusterConfig, err := rest.InClusterConfig()
|
||||
if err == nil {
|
||||
return setGlobalDefaults(inClusterConfig), nil
|
||||
}
|
||||
}
|
||||
|
||||
// star (default) match
|
||||
if authConfig, ok := c.kubeconfig.AuthInfos["*"]; ok {
|
||||
return restConfigFromKubeconfig(authConfig)
|
||||
}
|
||||
|
||||
// use the current context from the kubeconfig if possible
|
||||
if len(c.kubeconfig.CurrentContext) > 0 {
|
||||
if currContext, ok := c.kubeconfig.Contexts[c.kubeconfig.CurrentContext]; ok {
|
||||
if len(currContext.AuthInfo) > 0 {
|
||||
if currAuth, ok := c.kubeconfig.AuthInfos[currContext.AuthInfo]; ok {
|
||||
return restConfigFromKubeconfig(currAuth)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// anonymous
|
||||
return setGlobalDefaults(&rest.Config{}), nil
|
||||
}
|
||||
|
||||
func restConfigFromKubeconfig(configAuthInfo *clientcmdapi.AuthInfo) (*rest.Config, error) {
|
||||
config := &rest.Config{}
|
||||
|
||||
// blindly overwrite existing values based on precedence
|
||||
if len(configAuthInfo.Token) > 0 {
|
||||
config.BearerToken = configAuthInfo.Token
|
||||
} else if len(configAuthInfo.TokenFile) > 0 {
|
||||
tokenBytes, err := ioutil.ReadFile(configAuthInfo.TokenFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.BearerToken = string(tokenBytes)
|
||||
config.BearerTokenFile = configAuthInfo.TokenFile
|
||||
}
|
||||
if len(configAuthInfo.Impersonate) > 0 {
|
||||
config.Impersonate = rest.ImpersonationConfig{
|
||||
UserName: configAuthInfo.Impersonate,
|
||||
Groups: configAuthInfo.ImpersonateGroups,
|
||||
Extra: configAuthInfo.ImpersonateUserExtra,
|
||||
}
|
||||
}
|
||||
if len(configAuthInfo.ClientCertificate) > 0 || len(configAuthInfo.ClientCertificateData) > 0 {
|
||||
config.CertFile = configAuthInfo.ClientCertificate
|
||||
config.CertData = configAuthInfo.ClientCertificateData
|
||||
config.KeyFile = configAuthInfo.ClientKey
|
||||
config.KeyData = configAuthInfo.ClientKeyData
|
||||
}
|
||||
if len(configAuthInfo.Username) > 0 || len(configAuthInfo.Password) > 0 {
|
||||
config.Username = configAuthInfo.Username
|
||||
config.Password = configAuthInfo.Password
|
||||
}
|
||||
if configAuthInfo.AuthProvider != nil {
|
||||
return nil, fmt.Errorf("auth provider not supported")
|
||||
}
|
||||
|
||||
return setGlobalDefaults(config), nil
|
||||
}
|
||||
|
||||
func setGlobalDefaults(config *rest.Config) *rest.Config {
|
||||
config.UserAgent = "kube-apiserver-admission"
|
||||
config.Timeout = 30 * time.Second
|
||||
|
||||
return config
|
||||
}
|
198
vendor/k8s.io/apiserver/pkg/util/webhook/client.go
generated
vendored
Normal file
198
vendor/k8s.io/apiserver/pkg/util/webhook/client.go
generated
vendored
Normal file
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
|
||||
"github.com/hashicorp/golang-lru"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultCacheSize = 200
|
||||
)
|
||||
|
||||
// ClientConfig defines parameters required for creating a hook client.
|
||||
type ClientConfig struct {
|
||||
Name string
|
||||
URL string
|
||||
CABundle []byte
|
||||
Service *ClientConfigService
|
||||
}
|
||||
|
||||
// ClientConfigService defines service discovery parameters of the webhook.
|
||||
type ClientConfigService struct {
|
||||
Name string
|
||||
Namespace string
|
||||
Path string
|
||||
}
|
||||
|
||||
// ClientManager builds REST clients to talk to webhooks. It caches the clients
|
||||
// to avoid duplicate creation.
|
||||
type ClientManager struct {
|
||||
authInfoResolver AuthenticationInfoResolver
|
||||
serviceResolver ServiceResolver
|
||||
negotiatedSerializer runtime.NegotiatedSerializer
|
||||
cache *lru.Cache
|
||||
}
|
||||
|
||||
// NewClientManager creates a clientManager.
|
||||
func NewClientManager(gv schema.GroupVersion, addToSchemaFunc func(s *runtime.Scheme) error) (ClientManager, error) {
|
||||
cache, err := lru.New(defaultCacheSize)
|
||||
if err != nil {
|
||||
return ClientManager{}, err
|
||||
}
|
||||
hookScheme := runtime.NewScheme()
|
||||
if err := addToSchemaFunc(hookScheme); err != nil {
|
||||
return ClientManager{}, err
|
||||
}
|
||||
return ClientManager{
|
||||
cache: cache,
|
||||
negotiatedSerializer: serializer.NegotiatedSerializerWrapper(runtime.SerializerInfo{
|
||||
Serializer: serializer.NewCodecFactory(hookScheme).LegacyCodec(gv),
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SetAuthenticationInfoResolverWrapper sets the
|
||||
// AuthenticationInfoResolverWrapper.
|
||||
func (cm *ClientManager) SetAuthenticationInfoResolverWrapper(wrapper AuthenticationInfoResolverWrapper) {
|
||||
if wrapper != nil {
|
||||
cm.authInfoResolver = wrapper(cm.authInfoResolver)
|
||||
}
|
||||
}
|
||||
|
||||
// SetAuthenticationInfoResolver sets the AuthenticationInfoResolver.
|
||||
func (cm *ClientManager) SetAuthenticationInfoResolver(resolver AuthenticationInfoResolver) {
|
||||
cm.authInfoResolver = resolver
|
||||
}
|
||||
|
||||
// SetServiceResolver sets the ServiceResolver.
|
||||
func (cm *ClientManager) SetServiceResolver(sr ServiceResolver) {
|
||||
if sr != nil {
|
||||
cm.serviceResolver = sr
|
||||
}
|
||||
}
|
||||
|
||||
// Validate checks if ClientManager is properly set up.
|
||||
func (cm *ClientManager) Validate() error {
|
||||
var errs []error
|
||||
if cm.negotiatedSerializer == nil {
|
||||
errs = append(errs, fmt.Errorf("the clientManager requires a negotiatedSerializer"))
|
||||
}
|
||||
if cm.serviceResolver == nil {
|
||||
errs = append(errs, fmt.Errorf("the clientManager requires a serviceResolver"))
|
||||
}
|
||||
if cm.authInfoResolver == nil {
|
||||
errs = append(errs, fmt.Errorf("the clientManager requires an authInfoResolver"))
|
||||
}
|
||||
return utilerrors.NewAggregate(errs)
|
||||
}
|
||||
|
||||
// HookClient get a RESTClient from the cache, or constructs one based on the
|
||||
// webhook configuration.
|
||||
func (cm *ClientManager) HookClient(cc ClientConfig) (*rest.RESTClient, error) {
|
||||
ccWithNoName := cc
|
||||
ccWithNoName.Name = ""
|
||||
cacheKey, err := json.Marshal(ccWithNoName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if client, ok := cm.cache.Get(string(cacheKey)); ok {
|
||||
return client.(*rest.RESTClient), nil
|
||||
}
|
||||
|
||||
complete := func(cfg *rest.Config) (*rest.RESTClient, error) {
|
||||
// Combine CAData from the config with any existing CA bundle provided
|
||||
if len(cfg.TLSClientConfig.CAData) > 0 {
|
||||
cfg.TLSClientConfig.CAData = append(cfg.TLSClientConfig.CAData, '\n')
|
||||
}
|
||||
cfg.TLSClientConfig.CAData = append(cfg.TLSClientConfig.CAData, cc.CABundle...)
|
||||
|
||||
cfg.ContentConfig.NegotiatedSerializer = cm.negotiatedSerializer
|
||||
cfg.ContentConfig.ContentType = runtime.ContentTypeJSON
|
||||
client, err := rest.UnversionedRESTClientFor(cfg)
|
||||
if err == nil {
|
||||
cm.cache.Add(string(cacheKey), client)
|
||||
}
|
||||
return client, err
|
||||
}
|
||||
|
||||
if cc.Service != nil {
|
||||
restConfig, err := cm.authInfoResolver.ClientConfigForService(cc.Service.Name, cc.Service.Namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg := rest.CopyConfig(restConfig)
|
||||
serverName := cc.Service.Name + "." + cc.Service.Namespace + ".svc"
|
||||
host := serverName + ":443"
|
||||
cfg.Host = "https://" + host
|
||||
cfg.APIPath = cc.Service.Path
|
||||
// Set the server name if not already set
|
||||
if len(cfg.TLSClientConfig.ServerName) == 0 {
|
||||
cfg.TLSClientConfig.ServerName = serverName
|
||||
}
|
||||
|
||||
delegateDialer := cfg.Dial
|
||||
if delegateDialer == nil {
|
||||
var d net.Dialer
|
||||
delegateDialer = d.DialContext
|
||||
}
|
||||
cfg.Dial = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
if addr == host {
|
||||
u, err := cm.serviceResolver.ResolveEndpoint(cc.Service.Namespace, cc.Service.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addr = u.Host
|
||||
}
|
||||
return delegateDialer(ctx, network, addr)
|
||||
}
|
||||
|
||||
return complete(cfg)
|
||||
}
|
||||
|
||||
if cc.URL == "" {
|
||||
return nil, &ErrCallingWebhook{WebhookName: cc.Name, Reason: errors.New("webhook configuration must have either service or URL")}
|
||||
}
|
||||
|
||||
u, err := url.Parse(cc.URL)
|
||||
if err != nil {
|
||||
return nil, &ErrCallingWebhook{WebhookName: cc.Name, Reason: fmt.Errorf("Unparsable URL: %v", err)}
|
||||
}
|
||||
|
||||
restConfig, err := cm.authInfoResolver.ClientConfigFor(u.Host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg := rest.CopyConfig(restConfig)
|
||||
cfg.Host = u.Scheme + "://" + u.Host
|
||||
cfg.APIPath = u.Path
|
||||
|
||||
return complete(cfg)
|
||||
}
|
34
vendor/k8s.io/apiserver/pkg/util/webhook/error.go
generated
vendored
Normal file
34
vendor/k8s.io/apiserver/pkg/util/webhook/error.go
generated
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package webhook
|
||||
|
||||
import "fmt"
|
||||
|
||||
// ErrCallingWebhook is returned for transport-layer errors calling webhooks. It
|
||||
// represents a failure to talk to the webhook, not the webhook rejecting a
|
||||
// request.
|
||||
type ErrCallingWebhook struct {
|
||||
WebhookName string
|
||||
Reason error
|
||||
}
|
||||
|
||||
func (e *ErrCallingWebhook) Error() string {
|
||||
if e.Reason != nil {
|
||||
return fmt.Sprintf("failed calling webhook %q: %v", e.WebhookName, e.Reason)
|
||||
}
|
||||
return fmt.Sprintf("failed calling webhook %q; no further details available", e.WebhookName)
|
||||
}
|
107
vendor/k8s.io/apiserver/pkg/util/webhook/gencerts.sh
generated
vendored
Normal file
107
vendor/k8s.io/apiserver/pkg/util/webhook/gencerts.sh
generated
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright 2017 The Kubernetes Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
set -e
|
||||
|
||||
# gencerts.sh generates the certificates for the webhook tests.
|
||||
#
|
||||
# It is not expected to be run often (there is no go generate rule), and mainly
|
||||
# exists for documentation purposes.
|
||||
|
||||
CN_BASE="webhook_tests"
|
||||
|
||||
cat > server.conf << EOF
|
||||
[req]
|
||||
req_extensions = v3_req
|
||||
distinguished_name = req_distinguished_name
|
||||
[req_distinguished_name]
|
||||
[ v3_req ]
|
||||
basicConstraints = CA:FALSE
|
||||
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
|
||||
extendedKeyUsage = clientAuth, serverAuth
|
||||
subjectAltName = @alt_names
|
||||
[alt_names]
|
||||
IP.1 = 127.0.0.1
|
||||
EOF
|
||||
|
||||
cat > client.conf << EOF
|
||||
[req]
|
||||
req_extensions = v3_req
|
||||
distinguished_name = req_distinguished_name
|
||||
[req_distinguished_name]
|
||||
[ v3_req ]
|
||||
basicConstraints = CA:FALSE
|
||||
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
|
||||
extendedKeyUsage = clientAuth, serverAuth
|
||||
subjectAltName = @alt_names
|
||||
[alt_names]
|
||||
IP.1 = 127.0.0.1
|
||||
EOF
|
||||
|
||||
# Create a certificate authority
|
||||
openssl genrsa -out caKey.pem 2048
|
||||
openssl req -x509 -new -nodes -key caKey.pem -days 100000 -out caCert.pem -subj "/CN=${CN_BASE}_ca"
|
||||
|
||||
# Create a second certificate authority
|
||||
openssl genrsa -out badCAKey.pem 2048
|
||||
openssl req -x509 -new -nodes -key badCAKey.pem -days 100000 -out badCACert.pem -subj "/CN=${CN_BASE}_ca"
|
||||
|
||||
# Create a server certiticate
|
||||
openssl genrsa -out serverKey.pem 2048
|
||||
openssl req -new -key serverKey.pem -out server.csr -subj "/CN=${CN_BASE}_server" -config server.conf
|
||||
openssl x509 -req -in server.csr -CA caCert.pem -CAkey caKey.pem -CAcreateserial -out serverCert.pem -days 100000 -extensions v3_req -extfile server.conf
|
||||
|
||||
# Create a client certiticate
|
||||
openssl genrsa -out clientKey.pem 2048
|
||||
openssl req -new -key clientKey.pem -out client.csr -subj "/CN=${CN_BASE}_client" -config client.conf
|
||||
openssl x509 -req -in client.csr -CA caCert.pem -CAkey caKey.pem -CAcreateserial -out clientCert.pem -days 100000 -extensions v3_req -extfile client.conf
|
||||
|
||||
outfile=certs_test.go
|
||||
|
||||
cat > $outfile << EOF
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// This file was generated using openssl by the gencerts.sh script
|
||||
// and holds raw certificates for the webhook tests.
|
||||
|
||||
package webhook
|
||||
EOF
|
||||
|
||||
for file in caKey caCert badCAKey badCACert serverKey serverCert clientKey clientCert; do
|
||||
data=$(cat ${file}.pem)
|
||||
echo "" >> $outfile
|
||||
echo "var $file = []byte(\`$data\`)" >> $outfile
|
||||
done
|
||||
|
||||
# Clean up after we're done.
|
||||
rm ./*.pem
|
||||
rm ./*.csr
|
||||
rm ./*.srl
|
||||
rm ./*.conf
|
46
vendor/k8s.io/apiserver/pkg/util/webhook/serviceresolver.go
generated
vendored
Normal file
46
vendor/k8s.io/apiserver/pkg/util/webhook/serviceresolver.go
generated
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// ServiceResolver knows how to convert a service reference into an actual location.
|
||||
type ServiceResolver interface {
|
||||
ResolveEndpoint(namespace, name string) (*url.URL, error)
|
||||
}
|
||||
|
||||
type defaultServiceResolver struct{}
|
||||
|
||||
// NewDefaultServiceResolver creates a new default server resolver.
|
||||
func NewDefaultServiceResolver() ServiceResolver {
|
||||
return &defaultServiceResolver{}
|
||||
}
|
||||
|
||||
// ResolveEndpoint constructs a service URL from a given namespace and name
|
||||
// note that the name and namespace are required and by default all created addresses use HTTPS scheme.
|
||||
// for example:
|
||||
// name=ross namespace=andromeda resolves to https://ross.andromeda.svc:443
|
||||
func (sr defaultServiceResolver) ResolveEndpoint(namespace, name string) (*url.URL, error) {
|
||||
if len(name) == 0 || len(namespace) == 0 {
|
||||
return nil, errors.New("cannot resolve an empty service name or namespace")
|
||||
}
|
||||
return &url.URL{Scheme: "https", Host: fmt.Sprintf("%s.%s.svc:443", name, namespace)}, nil
|
||||
}
|
101
vendor/k8s.io/apiserver/pkg/util/webhook/validation.go
generated
vendored
Normal file
101
vendor/k8s.io/apiserver/pkg/util/webhook/validation.go
generated
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/validation"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
// ValidateWebhookURL validates webhook's URL.
|
||||
func ValidateWebhookURL(fldPath *field.Path, URL string, forceHttps bool) field.ErrorList {
|
||||
var allErrors field.ErrorList
|
||||
const form = "; desired format: https://host[/path]"
|
||||
if u, err := url.Parse(URL); err != nil {
|
||||
allErrors = append(allErrors, field.Required(fldPath, "url must be a valid URL: "+err.Error()+form))
|
||||
} else {
|
||||
if forceHttps && u.Scheme != "https" {
|
||||
allErrors = append(allErrors, field.Invalid(fldPath, u.Scheme, "'https' is the only allowed URL scheme"+form))
|
||||
}
|
||||
if len(u.Host) == 0 {
|
||||
allErrors = append(allErrors, field.Invalid(fldPath, u.Host, "host must be provided"+form))
|
||||
}
|
||||
if u.User != nil {
|
||||
allErrors = append(allErrors, field.Invalid(fldPath, u.User.String(), "user information is not permitted in the URL"))
|
||||
}
|
||||
if len(u.Fragment) != 0 {
|
||||
allErrors = append(allErrors, field.Invalid(fldPath, u.Fragment, "fragments are not permitted in the URL"))
|
||||
}
|
||||
if len(u.RawQuery) != 0 {
|
||||
allErrors = append(allErrors, field.Invalid(fldPath, u.RawQuery, "query parameters are not permitted in the URL"))
|
||||
}
|
||||
}
|
||||
return allErrors
|
||||
}
|
||||
|
||||
func ValidateWebhookService(fldPath *field.Path, namespace, name string, path *string) field.ErrorList {
|
||||
var allErrors field.ErrorList
|
||||
|
||||
if len(name) == 0 {
|
||||
allErrors = append(allErrors, field.Required(fldPath.Child("name"), "service name is required"))
|
||||
}
|
||||
|
||||
if len(namespace) == 0 {
|
||||
allErrors = append(allErrors, field.Required(fldPath.Child("namespace"), "service namespace is required"))
|
||||
}
|
||||
|
||||
if path == nil {
|
||||
return allErrors
|
||||
}
|
||||
|
||||
// TODO: replace below with url.Parse + verifying that host is empty?
|
||||
|
||||
urlPath := *path
|
||||
if urlPath == "/" || len(urlPath) == 0 {
|
||||
return allErrors
|
||||
}
|
||||
if urlPath == "//" {
|
||||
allErrors = append(allErrors, field.Invalid(fldPath.Child("path"), urlPath, "segment[0] may not be empty"))
|
||||
return allErrors
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(urlPath, "/") {
|
||||
allErrors = append(allErrors, field.Invalid(fldPath.Child("path"), urlPath, "must start with a '/'"))
|
||||
}
|
||||
|
||||
urlPathToCheck := urlPath[1:]
|
||||
if strings.HasSuffix(urlPathToCheck, "/") {
|
||||
urlPathToCheck = urlPathToCheck[:len(urlPathToCheck)-1]
|
||||
}
|
||||
steps := strings.Split(urlPathToCheck, "/")
|
||||
for i, step := range steps {
|
||||
if len(step) == 0 {
|
||||
allErrors = append(allErrors, field.Invalid(fldPath.Child("path"), urlPath, fmt.Sprintf("segment[%d] may not be empty", i)))
|
||||
continue
|
||||
}
|
||||
failures := validation.IsDNS1123Subdomain(step)
|
||||
for _, failure := range failures {
|
||||
allErrors = append(allErrors, field.Invalid(fldPath.Child("path"), urlPath, fmt.Sprintf("segment[%d]: %v", i, failure)))
|
||||
}
|
||||
}
|
||||
|
||||
return allErrors
|
||||
}
|
120
vendor/k8s.io/apiserver/pkg/util/webhook/webhook.go
generated
vendored
Normal file
120
vendor/k8s.io/apiserver/pkg/util/webhook/webhook.go
generated
vendored
Normal file
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package webhook implements a generic HTTP webhook plugin.
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
"k8s.io/apimachinery/pkg/util/net"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
)
|
||||
|
||||
// defaultRequestTimeout is set for all webhook request. This is the absolute
|
||||
// timeout of the HTTP request, including reading the response body.
|
||||
const defaultRequestTimeout = 30 * time.Second
|
||||
|
||||
type GenericWebhook struct {
|
||||
RestClient *rest.RESTClient
|
||||
InitialBackoff time.Duration
|
||||
}
|
||||
|
||||
// NewGenericWebhook creates a new GenericWebhook from the provided kubeconfig file.
|
||||
func NewGenericWebhook(scheme *runtime.Scheme, codecFactory serializer.CodecFactory, kubeConfigFile string, groupVersions []schema.GroupVersion, initialBackoff time.Duration) (*GenericWebhook, error) {
|
||||
return newGenericWebhook(scheme, codecFactory, kubeConfigFile, groupVersions, initialBackoff, defaultRequestTimeout)
|
||||
}
|
||||
|
||||
func newGenericWebhook(scheme *runtime.Scheme, codecFactory serializer.CodecFactory, kubeConfigFile string, groupVersions []schema.GroupVersion, initialBackoff, requestTimeout time.Duration) (*GenericWebhook, error) {
|
||||
for _, groupVersion := range groupVersions {
|
||||
if !scheme.IsVersionRegistered(groupVersion) {
|
||||
return nil, fmt.Errorf("webhook plugin requires enabling extension resource: %s", groupVersion)
|
||||
}
|
||||
}
|
||||
|
||||
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
|
||||
loadingRules.ExplicitPath = kubeConfigFile
|
||||
loader := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &clientcmd.ConfigOverrides{})
|
||||
|
||||
clientConfig, err := loader.ClientConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Kubeconfigs can't set a timeout, this can only be set through a command line flag.
|
||||
//
|
||||
// https://github.com/kubernetes/client-go/blob/master/tools/clientcmd/overrides.go
|
||||
//
|
||||
// Set this to something reasonable so request to webhooks don't hang forever.
|
||||
clientConfig.Timeout = requestTimeout
|
||||
|
||||
codec := codecFactory.LegacyCodec(groupVersions...)
|
||||
clientConfig.ContentConfig.NegotiatedSerializer = serializer.NegotiatedSerializerWrapper(runtime.SerializerInfo{Serializer: codec})
|
||||
|
||||
restClient, err := rest.UnversionedRESTClientFor(clientConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &GenericWebhook{restClient, initialBackoff}, nil
|
||||
}
|
||||
|
||||
// WithExponentialBackoff will retry webhookFn() up to 5 times with exponentially increasing backoff when
|
||||
// it returns an error for which apierrors.SuggestsClientDelay() or apierrors.IsInternalError() returns true.
|
||||
func (g *GenericWebhook) WithExponentialBackoff(webhookFn func() rest.Result) rest.Result {
|
||||
var result rest.Result
|
||||
WithExponentialBackoff(g.InitialBackoff, func() error {
|
||||
result = webhookFn()
|
||||
return result.Error()
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
// WithExponentialBackoff will retry webhookFn() up to 5 times with exponentially increasing backoff when
|
||||
// it returns an error for which apierrors.SuggestsClientDelay() or apierrors.IsInternalError() returns true.
|
||||
func WithExponentialBackoff(initialBackoff time.Duration, webhookFn func() error) error {
|
||||
backoff := wait.Backoff{
|
||||
Duration: initialBackoff,
|
||||
Factor: 1.5,
|
||||
Jitter: 0.2,
|
||||
Steps: 5,
|
||||
}
|
||||
|
||||
var err error
|
||||
wait.ExponentialBackoff(backoff, func() (bool, error) {
|
||||
err = webhookFn()
|
||||
// these errors indicate a transient error that should be retried.
|
||||
if net.IsConnectionReset(err) || apierrors.IsInternalError(err) || apierrors.IsTimeout(err) || apierrors.IsTooManyRequests(err) {
|
||||
return false, nil
|
||||
}
|
||||
// if the error sends the Retry-After header, we respect it as an explicit confirmation we should retry.
|
||||
if _, shouldRetry := apierrors.SuggestsClientDelay(err); shouldRetry {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
return err
|
||||
}
|
177
vendor/k8s.io/apiserver/plugin/pkg/authenticator/token/webhook/webhook.go
generated
vendored
Normal file
177
vendor/k8s.io/apiserver/plugin/pkg/authenticator/token/webhook/webhook.go
generated
vendored
Normal file
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package webhook implements the authenticator.Token interface using HTTP webhooks.
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
authentication "k8s.io/api/authentication/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/util/webhook"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
authenticationclient "k8s.io/client-go/kubernetes/typed/authentication/v1beta1"
|
||||
"k8s.io/klog"
|
||||
)
|
||||
|
||||
var (
|
||||
groupVersions = []schema.GroupVersion{authentication.SchemeGroupVersion}
|
||||
)
|
||||
|
||||
const retryBackoff = 500 * time.Millisecond
|
||||
|
||||
// Ensure WebhookTokenAuthenticator implements the authenticator.Token interface.
|
||||
var _ authenticator.Token = (*WebhookTokenAuthenticator)(nil)
|
||||
|
||||
type WebhookTokenAuthenticator struct {
|
||||
tokenReview authenticationclient.TokenReviewInterface
|
||||
initialBackoff time.Duration
|
||||
implicitAuds authenticator.Audiences
|
||||
}
|
||||
|
||||
// NewFromInterface creates a webhook authenticator using the given tokenReview
|
||||
// client. It is recommend to wrap this authenticator with the token cache
|
||||
// authenticator implemented in
|
||||
// k8s.io/apiserver/pkg/authentication/token/cache.
|
||||
func NewFromInterface(tokenReview authenticationclient.TokenReviewInterface, implicitAuds authenticator.Audiences) (*WebhookTokenAuthenticator, error) {
|
||||
return newWithBackoff(tokenReview, retryBackoff, implicitAuds)
|
||||
}
|
||||
|
||||
// New creates a new WebhookTokenAuthenticator from the provided kubeconfig
|
||||
// file. It is recommend to wrap this authenticator with the token cache
|
||||
// authenticator implemented in
|
||||
// k8s.io/apiserver/pkg/authentication/token/cache.
|
||||
func New(kubeConfigFile string, implicitAuds authenticator.Audiences) (*WebhookTokenAuthenticator, error) {
|
||||
tokenReview, err := tokenReviewInterfaceFromKubeconfig(kubeConfigFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newWithBackoff(tokenReview, retryBackoff, implicitAuds)
|
||||
}
|
||||
|
||||
// newWithBackoff allows tests to skip the sleep.
|
||||
func newWithBackoff(tokenReview authenticationclient.TokenReviewInterface, initialBackoff time.Duration, implicitAuds authenticator.Audiences) (*WebhookTokenAuthenticator, error) {
|
||||
return &WebhookTokenAuthenticator{tokenReview, initialBackoff, implicitAuds}, nil
|
||||
}
|
||||
|
||||
// AuthenticateToken implements the authenticator.Token interface.
|
||||
func (w *WebhookTokenAuthenticator) AuthenticateToken(ctx context.Context, token string) (*authenticator.Response, bool, error) {
|
||||
// We take implicit audiences of the API server at WebhookTokenAuthenticator
|
||||
// construction time. The outline of how we validate audience here is:
|
||||
//
|
||||
// * if the ctx is not audience limited, don't do any audience validation.
|
||||
// * if ctx is audience-limited, add the audiences to the tokenreview spec
|
||||
// * if the tokenreview returns with audiences in the status that intersect
|
||||
// with the audiences in the ctx, copy into the response and return success
|
||||
// * if the tokenreview returns without an audience in the status, ensure
|
||||
// the ctx audiences intersect with the implicit audiences, and set the
|
||||
// intersection in the response.
|
||||
// * otherwise return unauthenticated.
|
||||
wantAuds, checkAuds := authenticator.AudiencesFrom(ctx)
|
||||
r := &authentication.TokenReview{
|
||||
Spec: authentication.TokenReviewSpec{
|
||||
Token: token,
|
||||
Audiences: wantAuds,
|
||||
},
|
||||
}
|
||||
var (
|
||||
result *authentication.TokenReview
|
||||
err error
|
||||
auds authenticator.Audiences
|
||||
)
|
||||
webhook.WithExponentialBackoff(w.initialBackoff, func() error {
|
||||
result, err = w.tokenReview.Create(r)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
// An error here indicates bad configuration or an outage. Log for debugging.
|
||||
klog.Errorf("Failed to make webhook authenticator request: %v", err)
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if checkAuds {
|
||||
gotAuds := w.implicitAuds
|
||||
if len(result.Status.Audiences) > 0 {
|
||||
gotAuds = result.Status.Audiences
|
||||
}
|
||||
auds = wantAuds.Intersect(gotAuds)
|
||||
if len(auds) == 0 {
|
||||
return nil, false, nil
|
||||
}
|
||||
}
|
||||
|
||||
r.Status = result.Status
|
||||
if !r.Status.Authenticated {
|
||||
var err error
|
||||
if len(r.Status.Error) != 0 {
|
||||
err = errors.New(r.Status.Error)
|
||||
}
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
var extra map[string][]string
|
||||
if r.Status.User.Extra != nil {
|
||||
extra = map[string][]string{}
|
||||
for k, v := range r.Status.User.Extra {
|
||||
extra[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return &authenticator.Response{
|
||||
User: &user.DefaultInfo{
|
||||
Name: r.Status.User.Username,
|
||||
UID: r.Status.User.UID,
|
||||
Groups: r.Status.User.Groups,
|
||||
Extra: extra,
|
||||
},
|
||||
Audiences: auds,
|
||||
}, true, nil
|
||||
}
|
||||
|
||||
// tokenReviewInterfaceFromKubeconfig builds a client from the specified kubeconfig file,
|
||||
// and returns a TokenReviewInterface that uses that client. Note that the client submits TokenReview
|
||||
// requests to the exact path specified in the kubeconfig file, so arbitrary non-API servers can be targeted.
|
||||
func tokenReviewInterfaceFromKubeconfig(kubeConfigFile string) (authenticationclient.TokenReviewInterface, error) {
|
||||
localScheme := runtime.NewScheme()
|
||||
if err := scheme.AddToScheme(localScheme); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := localScheme.SetVersionPriority(groupVersions...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gw, err := webhook.NewGenericWebhook(localScheme, scheme.Codecs, kubeConfigFile, groupVersions, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &tokenReviewClient{gw}, nil
|
||||
}
|
||||
|
||||
type tokenReviewClient struct {
|
||||
w *webhook.GenericWebhook
|
||||
}
|
||||
|
||||
func (t *tokenReviewClient) Create(tokenReview *authentication.TokenReview) (*authentication.TokenReview, error) {
|
||||
result := &authentication.TokenReview{}
|
||||
err := t.w.RestClient.Post().Body(tokenReview).Do().Into(result)
|
||||
return result, err
|
||||
}
|
11
vendor/modules.txt
vendored
11
vendor/modules.txt
vendored
@@ -55,12 +55,12 @@ github.com/rancher/norman/pkg/data
|
||||
github.com/rancher/norman/pkg/store/empty
|
||||
github.com/rancher/norman/pkg/types/values
|
||||
github.com/rancher/norman/pkg/api/builtin
|
||||
github.com/rancher/norman/pkg/auth
|
||||
github.com/rancher/norman/pkg/api
|
||||
github.com/rancher/norman/pkg/urlbuilder
|
||||
github.com/rancher/norman/pkg/httperror
|
||||
github.com/rancher/norman/pkg/types/slice
|
||||
github.com/rancher/norman/pkg/types/definition
|
||||
github.com/rancher/norman/pkg/types/convert/merge
|
||||
github.com/rancher/norman/pkg/api/writer
|
||||
github.com/rancher/norman/pkg/store/schema
|
||||
github.com/rancher/norman/pkg/api/access
|
||||
@@ -133,6 +133,8 @@ gopkg.in/yaml.v2
|
||||
# k8s.io/api v0.0.0 => ../kuberlite/staging/src/k8s.io/api
|
||||
k8s.io/api/rbac/v1
|
||||
k8s.io/api/core/v1
|
||||
k8s.io/api/authorization/v1
|
||||
k8s.io/api/authentication/v1
|
||||
k8s.io/api/admissionregistration/v1beta1
|
||||
k8s.io/api/apps/v1
|
||||
k8s.io/api/apps/v1beta1
|
||||
@@ -163,9 +165,7 @@ k8s.io/api/settings/v1alpha1
|
||||
k8s.io/api/storage/v1
|
||||
k8s.io/api/storage/v1alpha1
|
||||
k8s.io/api/storage/v1beta1
|
||||
k8s.io/api/authentication/v1
|
||||
k8s.io/api/authentication/v1beta1
|
||||
k8s.io/api/authorization/v1
|
||||
k8s.io/api/authorization/v1beta1
|
||||
# k8s.io/apiextensions-apiserver v0.0.0-20190409022649-727a075fdec8
|
||||
k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1
|
||||
@@ -227,7 +227,10 @@ k8s.io/apimachinery/pkg/apis/meta/v1beta1
|
||||
# k8s.io/apiserver v0.0.0-20190409021813-1ec86e4da56c
|
||||
k8s.io/apiserver/pkg/authentication/user
|
||||
k8s.io/apiserver/pkg/endpoints/request
|
||||
k8s.io/apiserver/pkg/authentication/authenticator
|
||||
k8s.io/apiserver/plugin/pkg/authenticator/token/webhook
|
||||
k8s.io/apiserver/pkg/apis/audit
|
||||
k8s.io/apiserver/pkg/util/webhook
|
||||
# k8s.io/client-go v11.0.1-0.20190409021438-1a26190bd76a+incompatible => ../kuberlite/staging/src/k8s.io/client-go
|
||||
k8s.io/client-go/dynamic
|
||||
k8s.io/client-go/rest
|
||||
@@ -235,6 +238,7 @@ k8s.io/client-go/dynamic/dynamicinformer
|
||||
k8s.io/client-go/tools/cache
|
||||
k8s.io/client-go/util/workqueue
|
||||
k8s.io/client-go/discovery
|
||||
k8s.io/client-go/kubernetes/typed/authorization/v1
|
||||
k8s.io/client-go/transport
|
||||
k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/informers/rbac/v1
|
||||
@@ -263,7 +267,6 @@ k8s.io/client-go/kubernetes/typed/apps/v1beta2
|
||||
k8s.io/client-go/kubernetes/typed/auditregistration/v1alpha1
|
||||
k8s.io/client-go/kubernetes/typed/authentication/v1
|
||||
k8s.io/client-go/kubernetes/typed/authentication/v1beta1
|
||||
k8s.io/client-go/kubernetes/typed/authorization/v1
|
||||
k8s.io/client-go/kubernetes/typed/authorization/v1beta1
|
||||
k8s.io/client-go/kubernetes/typed/autoscaling/v1
|
||||
k8s.io/client-go/kubernetes/typed/autoscaling/v2beta1
|
||||
|
Reference in New Issue
Block a user