mirror of
https://github.com/kubevela/kubevela.git
synced 2026-08-23 22:46:53 +00:00
Feat: support impersonation for application in apiserver (#4000)
Signed-off-by: Somefive <yd219913@alibaba-inc.com>
(cherry picked from commit 10dc83b60a)
Co-authored-by: Somefive <yd219913@alibaba-inc.com>
This commit is contained in:
co-authored by
Somefive
parent
e4fa5a5cf1
commit
eb386ce9f7
@@ -0,0 +1,23 @@
|
||||
{{ if .Values.authentication.enabled }}
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: kubevela:x-definition:reader
|
||||
rules:
|
||||
- apiGroups: [ "core.oam.dev" ]
|
||||
resources: [ "componentdefinitions", "traitdefinitions", "workloaddefinitions", "workflowstepdefinitions", "policydefinitions", "definitionrevisions" ]
|
||||
verbs: [ "get", "list", "watch" ]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: kubevela:x-definition:reader-binding
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: kubevela:x-definition:reader
|
||||
subjects:
|
||||
- kind: Group
|
||||
name: kubevela:x-definition:reader
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
{{ end }}
|
||||
@@ -19,7 +19,6 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
@@ -29,10 +28,12 @@ import (
|
||||
restfulspec "github.com/emicklei/go-restful-openapi/v2"
|
||||
"github.com/go-openapi/spec"
|
||||
"github.com/google/uuid"
|
||||
flag "github.com/spf13/pflag"
|
||||
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/config"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/utils/log"
|
||||
"github.com/oam-dev/kubevela/pkg/features"
|
||||
"github.com/oam-dev/kubevela/version"
|
||||
)
|
||||
|
||||
@@ -50,7 +51,7 @@ func main() {
|
||||
flag.BoolVar(&s.serverConfig.DisableStatisticCronJob, "disable-statistic-cronJob", false, "close the system statistic info calculating cronJob")
|
||||
flag.Float64Var(&s.serverConfig.KubeQPS, "kube-api-qps", 100, "the qps for kube clients. Low qps may lead to low throughput. High qps may give stress to api-server.")
|
||||
flag.IntVar(&s.serverConfig.KubeBurst, "kube-api-burst", 300, "the burst for kube clients. Recommend setting it qps*3.")
|
||||
|
||||
features.APIServerMutableFeatureGate.AddFlag(flag.CommandLine)
|
||||
flag.Parse()
|
||||
|
||||
if len(os.Args) > 2 && os.Args[1] == "build-swagger" {
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/domain/model"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/infrastructure/datastore"
|
||||
apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/interfaces/api/dto/v1"
|
||||
apiserverutils "github.com/oam-dev/kubevela/pkg/apiserver/utils"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/utils/bcode"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/utils/log"
|
||||
"github.com/oam-dev/kubevela/pkg/utils"
|
||||
@@ -552,6 +553,7 @@ func (p *rbacServiceImpl) CheckPerm(resource string, actions ...string) func(req
|
||||
bcode.ReturnError(req, res, bcode.ErrForbidden)
|
||||
return
|
||||
}
|
||||
apiserverutils.SetUsernameAndProjectInRequestContext(req, userName, projectName)
|
||||
chain.ProcessFilter(req, res)
|
||||
}
|
||||
return f
|
||||
|
||||
@@ -17,6 +17,7 @@ limitations under the License.
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -27,8 +28,10 @@ import (
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/domain/repository"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/infrastructure/datastore"
|
||||
apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/interfaces/api/dto/v1"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/utils"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/utils/bcode"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/utils/log"
|
||||
"github.com/oam-dev/kubevela/pkg/auth"
|
||||
"github.com/oam-dev/kubevela/pkg/multicluster"
|
||||
)
|
||||
|
||||
@@ -117,6 +120,9 @@ func (dt *targetServiceImpl) DeleteTarget(ctx context.Context, targetName string
|
||||
if err = repository.DeleteTargetNamespace(ctx, dt.K8sClient, ddt.Cluster.ClusterName, ddt.Cluster.Namespace, targetName); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = managePrivilegesForTarget(ctx, dt.K8sClient, ddt, true); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = dt.Store.Delete(ctx, target); err != nil {
|
||||
if errors.Is(err, datastore.ErrRecordNotExist) {
|
||||
return bcode.ErrTargetNotExist
|
||||
@@ -142,6 +148,9 @@ func (dt *targetServiceImpl) CreateTarget(ctx context.Context, req apisv1.Create
|
||||
if err := repository.CreateTargetNamespace(ctx, dt.K8sClient, req.Cluster.ClusterName, req.Cluster.Namespace, req.Name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := managePrivilegesForTarget(ctx, dt.K8sClient, &target, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err := repository.CreateTarget(ctx, dt.Store, &target)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -227,3 +236,22 @@ func (dt *targetServiceImpl) convertFromTargetModel(ctx context.Context, target
|
||||
}
|
||||
return targetBase
|
||||
}
|
||||
|
||||
// managePrivilegesForTarget grant or revoke privileges for target
|
||||
func managePrivilegesForTarget(ctx context.Context, cli client.Client, target *model.Target, revoke bool) error {
|
||||
if target.Cluster == nil {
|
||||
return nil
|
||||
}
|
||||
p := &auth.ScopedPrivilege{Cluster: target.Cluster.ClusterName, Namespace: target.Cluster.Namespace}
|
||||
identity := &auth.Identity{Groups: []string{utils.KubeVelaProjectGroupPrefix + target.Project}}
|
||||
writer := &bytes.Buffer{}
|
||||
f, msg := auth.GrantPrivileges, "GrantPrivileges"
|
||||
if revoke {
|
||||
f, msg = auth.RevokePrivileges, "RevokePrivileges"
|
||||
}
|
||||
if err := f(ctx, cli, []auth.PrivilegeDescription{p}, identity, writer); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Logger.Debugf("%s: %s", msg, writer.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/config"
|
||||
|
||||
apiConfig "github.com/oam-dev/kubevela/pkg/apiserver/config"
|
||||
"github.com/oam-dev/kubevela/pkg/auth"
|
||||
"github.com/oam-dev/kubevela/pkg/cue/packages"
|
||||
"github.com/oam-dev/kubevela/pkg/multicluster"
|
||||
"github.com/oam-dev/kubevela/pkg/oam/discoverymapper"
|
||||
@@ -40,6 +41,18 @@ func SetKubeClient(c client.Client) {
|
||||
kubeClient = c
|
||||
}
|
||||
|
||||
func setKubeConfig(conf *rest.Config) (err error) {
|
||||
if conf == nil {
|
||||
conf, err = config.GetConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
kubeConfig = conf
|
||||
kubeConfig.Wrap(auth.NewImpersonatingRoundTripper)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetKubeConfig generate the kube config from the config of apiserver
|
||||
func SetKubeConfig(c apiConfig.Config) error {
|
||||
conf, err := config.GetConfig()
|
||||
@@ -49,7 +62,7 @@ func SetKubeConfig(c apiConfig.Config) error {
|
||||
kubeConfig = conf
|
||||
kubeConfig.Burst = c.KubeBurst
|
||||
kubeConfig.QPS = float32(c.KubeQPS)
|
||||
return nil
|
||||
return setKubeConfig(kubeConfig)
|
||||
}
|
||||
|
||||
// GetKubeClient create and return kube runtime client
|
||||
|
||||
@@ -105,6 +105,7 @@ func (s *restServer) buildIoCContainer() error {
|
||||
return fmt.Errorf("fail to provides the datastore bean to the container: %w", err)
|
||||
}
|
||||
|
||||
kubeClient = utils.NewAuthApplicationClient(kubeClient)
|
||||
if err := s.beanContainer.ProvideWithName("kubeClient", kubeClient); err != nil {
|
||||
return fmt.Errorf("fail to provides the kubeClient bean to the container: %w", err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
Copyright 2022 The KubeVela 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 utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/emicklei/go-restful/v3"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/domain/model"
|
||||
"github.com/oam-dev/kubevela/pkg/features"
|
||||
)
|
||||
|
||||
// KubeVelaProjectGroupPrefix the prefix kubevela project
|
||||
const KubeVelaProjectGroupPrefix = "kubevela:project:"
|
||||
|
||||
// ContextWithUserInfo extract user from context (parse username and project) for impersonation
|
||||
func ContextWithUserInfo(ctx context.Context) context.Context {
|
||||
if !features.APIServerFeatureGate.Enabled(features.APIServerEnableImpersonation) {
|
||||
return ctx
|
||||
}
|
||||
userInfo := &user.DefaultInfo{Name: user.Anonymous}
|
||||
if username, ok := UsernameFrom(ctx); ok {
|
||||
userInfo.Name = username
|
||||
}
|
||||
if project, ok := ProjectFrom(ctx); ok {
|
||||
userInfo.Groups = []string{KubeVelaProjectGroupPrefix + project}
|
||||
}
|
||||
if userInfo.Name == model.DefaultAdminUserName && !features.APIServerFeatureGate.Enabled(features.APIServerEnableAdminImpersonation) {
|
||||
return ctx
|
||||
}
|
||||
return request.WithUser(ctx, userInfo)
|
||||
}
|
||||
|
||||
// SetUsernameAndProjectInRequestContext .
|
||||
func SetUsernameAndProjectInRequestContext(req *restful.Request, userName string, projectName string) {
|
||||
ctx := req.Request.Context()
|
||||
ctx = WithUsername(ctx, userName)
|
||||
ctx = WithProject(ctx, projectName)
|
||||
req.Request = req.Request.WithContext(ctx)
|
||||
}
|
||||
|
||||
// NewAuthApplicationClient will carry UserInfo for mutating requests related to application automatically
|
||||
func NewAuthApplicationClient(cli client.Client) client.Client {
|
||||
return &authAppClient{Client: cli}
|
||||
}
|
||||
|
||||
type authAppClient struct {
|
||||
client.Client
|
||||
}
|
||||
|
||||
// Status .
|
||||
func (c *authAppClient) Status() client.StatusWriter {
|
||||
return &authAppStatusClient{StatusWriter: c.Client.Status()}
|
||||
}
|
||||
|
||||
// Create .
|
||||
func (c *authAppClient) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error {
|
||||
if _, ok := obj.(*v1beta1.Application); ok {
|
||||
ctx = ContextWithUserInfo(ctx)
|
||||
}
|
||||
return c.Client.Create(ctx, obj, opts...)
|
||||
}
|
||||
|
||||
// Delete .
|
||||
func (c *authAppClient) Delete(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error {
|
||||
if _, ok := obj.(*v1beta1.Application); ok {
|
||||
ctx = ContextWithUserInfo(ctx)
|
||||
}
|
||||
return c.Client.Delete(ctx, obj, opts...)
|
||||
}
|
||||
|
||||
// Update .
|
||||
func (c *authAppClient) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error {
|
||||
if _, ok := obj.(*v1beta1.Application); ok {
|
||||
ctx = ContextWithUserInfo(ctx)
|
||||
}
|
||||
return c.Client.Update(ctx, obj, opts...)
|
||||
}
|
||||
|
||||
// Patch .
|
||||
func (c *authAppClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error {
|
||||
if _, ok := obj.(*v1beta1.Application); ok {
|
||||
ctx = ContextWithUserInfo(ctx)
|
||||
}
|
||||
return c.Client.Patch(ctx, obj, patch, opts...)
|
||||
}
|
||||
|
||||
type authAppStatusClient struct {
|
||||
client.StatusWriter
|
||||
}
|
||||
|
||||
// Update .
|
||||
func (c *authAppStatusClient) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error {
|
||||
if _, ok := obj.(*v1beta1.Application); ok {
|
||||
ctx = ContextWithUserInfo(ctx)
|
||||
}
|
||||
return c.StatusWriter.Update(ctx, obj, opts...)
|
||||
}
|
||||
|
||||
// Patch .
|
||||
func (c *authAppStatusClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error {
|
||||
if _, ok := obj.(*v1beta1.Application); ok {
|
||||
ctx = ContextWithUserInfo(ctx)
|
||||
}
|
||||
return c.StatusWriter.Patch(ctx, obj, patch, opts...)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
Copyright 2022 The KubeVela 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 utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type contextKey int
|
||||
|
||||
const (
|
||||
projectKey contextKey = iota
|
||||
usernameKey
|
||||
)
|
||||
|
||||
// WithProject carries project in context
|
||||
func WithProject(parent context.Context, project string) context.Context {
|
||||
return context.WithValue(parent, projectKey, project)
|
||||
}
|
||||
|
||||
// ProjectFrom extract project from context
|
||||
func ProjectFrom(ctx context.Context) (string, bool) {
|
||||
project, ok := ctx.Value(projectKey).(string)
|
||||
return project, ok
|
||||
}
|
||||
|
||||
// WithUsername carries username in context
|
||||
func WithUsername(parent context.Context, username string) context.Context {
|
||||
return context.WithValue(parent, usernameKey, username)
|
||||
}
|
||||
|
||||
// UsernameFrom extract username from context
|
||||
func UsernameFrom(ctx context.Context) (string, bool) {
|
||||
username, ok := ctx.Value(usernameKey).(string)
|
||||
return username, ok
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"github.com/xlab/treeprint"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/utils/strings/slices"
|
||||
@@ -325,6 +326,23 @@ func mergeSubjects(src []rbacv1.Subject, merge []rbacv1.Subject) []rbacv1.Subjec
|
||||
return subs
|
||||
}
|
||||
|
||||
func removeSubjects(src []rbacv1.Subject, toRemove []rbacv1.Subject) []rbacv1.Subject {
|
||||
var subs []rbacv1.Subject
|
||||
for _, sub := range src {
|
||||
add := true
|
||||
for _, t := range toRemove {
|
||||
if reflect.DeepEqual(t, sub) {
|
||||
add = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if add {
|
||||
subs = append(subs, sub)
|
||||
}
|
||||
}
|
||||
return subs
|
||||
}
|
||||
|
||||
// GrantPrivileges grant privileges to identity
|
||||
func GrantPrivileges(ctx context.Context, cli client.Client, privileges []PrivilegeDescription, identity *Identity, writer io.Writer) error {
|
||||
subs := identity.Subjects()
|
||||
@@ -372,3 +390,59 @@ func GrantPrivileges(ctx context.Context, cli client.Client, privileges []Privil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RevokePrivileges revoke privileges (notice that the revoking process only deletes bond subject in the
|
||||
// RoleBinding/ClusterRoleBinding, it does not ensure the identity's other related privileges are removed to
|
||||
// prevent identity from accessing)
|
||||
func RevokePrivileges(ctx context.Context, cli client.Client, privileges []PrivilegeDescription, identity *Identity, writer io.Writer) error {
|
||||
subs := identity.Subjects()
|
||||
if len(subs) == 0 {
|
||||
return fmt.Errorf("failed to find RBAC subjects in identity")
|
||||
}
|
||||
for _, p := range privileges {
|
||||
cluster := p.GetCluster()
|
||||
_ctx := multicluster.ContextWithClusterName(ctx, cluster)
|
||||
binding := p.GetRoleBinding(subs)
|
||||
kind, key := "ClusterRoleBinding", binding.GetName()
|
||||
if binding.GetNamespace() != "" {
|
||||
kind, key = "RoleBinding", binding.GetNamespace()+"/"+binding.GetName()
|
||||
}
|
||||
var err error
|
||||
remove := false
|
||||
var toDel client.Object
|
||||
switch bindingObj := binding.(type) {
|
||||
case *rbacv1.RoleBinding:
|
||||
obj := &rbacv1.RoleBinding{}
|
||||
if err = cli.Get(_ctx, client.ObjectKeyFromObject(bindingObj), obj); err == nil {
|
||||
bindingObj.Subjects = removeSubjects(obj.Subjects, bindingObj.Subjects)
|
||||
remove = len(bindingObj.Subjects) == 0
|
||||
toDel = obj
|
||||
}
|
||||
case *rbacv1.ClusterRoleBinding:
|
||||
obj := &rbacv1.ClusterRoleBinding{}
|
||||
if err = cli.Get(_ctx, client.ObjectKeyFromObject(bindingObj), obj); err == nil {
|
||||
bindingObj.Subjects = removeSubjects(obj.Subjects, bindingObj.Subjects)
|
||||
remove = len(bindingObj.Subjects) == 0
|
||||
toDel = obj
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
if !kerrors.IsNotFound(err) {
|
||||
return fmt.Errorf("failed to fetch %s %s in cluster %s: %w", kind, key, cluster, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if remove {
|
||||
if err = cli.Delete(_ctx, toDel); err != nil {
|
||||
return fmt.Errorf("failed to delete %s %s in cluster %s: %w", kind, key, cluster, err)
|
||||
}
|
||||
} else {
|
||||
res, err := utils.CreateOrUpdate(_ctx, cli, binding)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update %s %s in cluster %s: %w", kind, key, cluster, err)
|
||||
}
|
||||
_, _ = fmt.Fprintf(writer, "%s %s %s in cluster %s.\n", kind, key, res, cluster)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
Copyright 2022 The KubeVela 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 features
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/component-base/featuregate"
|
||||
)
|
||||
|
||||
var (
|
||||
// APIServerMutableFeatureGate is a mutable version of APIServerFeatureGate
|
||||
APIServerMutableFeatureGate featuregate.MutableFeatureGate = featuregate.NewFeatureGate()
|
||||
|
||||
// APIServerFeatureGate is a shared global FeatureGate for apiserver.
|
||||
APIServerFeatureGate featuregate.FeatureGate = APIServerMutableFeatureGate
|
||||
)
|
||||
|
||||
const (
|
||||
// APIServerEnableImpersonation whether to enable impersonation for APIServer
|
||||
APIServerEnableImpersonation featuregate.Feature = "EnableImpersonation"
|
||||
// APIServerEnableAdminImpersonation whether to disable User admin impersonation for APIServer
|
||||
APIServerEnableAdminImpersonation featuregate.Feature = "EnableAdminImpersonation"
|
||||
)
|
||||
|
||||
func init() {
|
||||
runtime.Must(APIServerMutableFeatureGate.Add(map[featuregate.Feature]featuregate.FeatureSpec{
|
||||
APIServerEnableImpersonation: {Default: false, PreRelease: featuregate.Alpha},
|
||||
APIServerEnableAdminImpersonation: {Default: false, PreRelease: featuregate.Alpha},
|
||||
}))
|
||||
}
|
||||
Reference in New Issue
Block a user