mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-11 13:02:14 +00:00
Adjust CEL cost calculation and versioning for authorization library
This commit is contained in:
parent
be2e32fa3e
commit
83bd512861
@ -20,12 +20,6 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/selection"
|
||||
genericfeatures "k8s.io/apiserver/pkg/features"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
@ -34,19 +28,25 @@ import (
|
||||
celtypes "github.com/google/cel-go/common/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"k8s.io/utils/pointer"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/selection"
|
||||
"k8s.io/apimachinery/pkg/util/version"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
celconfig "k8s.io/apiserver/pkg/apis/cel"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
apiservercel "k8s.io/apiserver/pkg/cel"
|
||||
"k8s.io/apiserver/pkg/cel/environment"
|
||||
genericfeatures "k8s.io/apiserver/pkg/features"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||
"k8s.io/utils/pointer"
|
||||
)
|
||||
|
||||
type condition struct {
|
||||
@ -182,6 +182,9 @@ func TestFilter(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
v130 := version.MajorMinor(1, 30)
|
||||
v131 := version.MajorMinor(1, 31)
|
||||
|
||||
var nilUnstructured *unstructured.Unstructured
|
||||
cases := []struct {
|
||||
name string
|
||||
@ -195,6 +198,8 @@ func TestFilter(t *testing.T) {
|
||||
namespaceObject *corev1.Namespace
|
||||
strictCost bool
|
||||
enableSelectors bool
|
||||
|
||||
compatibilityVersion *version.Version
|
||||
}{
|
||||
{
|
||||
name: "valid syntax for object",
|
||||
@ -494,6 +499,38 @@ func TestFilter(t *testing.T) {
|
||||
APIVersion: "*",
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "test authorizer error using fieldSelector with 1.30 compatibility",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
Expression: "authorizer.group('apps').resource('deployments').fieldSelector('foo=bar').labelSelector('apple=banana').subresource('status').namespace('test').name('backend').check('create').allowed()",
|
||||
},
|
||||
},
|
||||
attributes: newValidAttribute(&podObject, false),
|
||||
results: []EvaluationResult{
|
||||
{
|
||||
Error: fmt.Errorf("fieldSelector"),
|
||||
},
|
||||
},
|
||||
authorizer: newAuthzAllowMatch(authorizer.AttributesRecord{
|
||||
ResourceRequest: true,
|
||||
APIGroup: "apps",
|
||||
Resource: "deployments",
|
||||
Subresource: "status",
|
||||
Namespace: "test",
|
||||
Name: "backend",
|
||||
Verb: "create",
|
||||
APIVersion: "*",
|
||||
FieldSelectorRequirements: fields.Requirements{
|
||||
{Operator: "=", Field: "foo", Value: "bar"},
|
||||
},
|
||||
LabelSelectorRequirements: labels.Requirements{
|
||||
*simpleLabelSelector,
|
||||
},
|
||||
}),
|
||||
enableSelectors: true,
|
||||
compatibilityVersion: v130,
|
||||
},
|
||||
{
|
||||
name: "test authorizer allow resource check with all fields",
|
||||
validations: []ExpressionAccessor{
|
||||
@ -523,7 +560,8 @@ func TestFilter(t *testing.T) {
|
||||
*simpleLabelSelector,
|
||||
},
|
||||
}),
|
||||
enableSelectors: true,
|
||||
enableSelectors: true,
|
||||
compatibilityVersion: v131,
|
||||
},
|
||||
{
|
||||
name: "test authorizer allow resource check with parse failures",
|
||||
@ -550,7 +588,8 @@ func TestFilter(t *testing.T) {
|
||||
FieldSelectorParsingErr: errors.New("invalid selector: 'foo badoperator bar'; can't understand 'foo badoperator bar'"),
|
||||
LabelSelectorParsingErr: errors.New("unable to parse requirement: found 'badoperator', expected: in, notin, =, ==, !=, gt, lt"),
|
||||
}),
|
||||
enableSelectors: true,
|
||||
enableSelectors: true,
|
||||
compatibilityVersion: v131,
|
||||
},
|
||||
{
|
||||
name: "test authorizer allow resource check with all fields, without gate",
|
||||
@ -575,6 +614,7 @@ func TestFilter(t *testing.T) {
|
||||
Verb: "create",
|
||||
APIVersion: "*",
|
||||
}),
|
||||
compatibilityVersion: v131,
|
||||
},
|
||||
{
|
||||
name: "test authorizer not allowed resource check one incorrect field",
|
||||
@ -837,9 +877,13 @@ func TestFilter(t *testing.T) {
|
||||
if tc.testPerCallLimit == 0 {
|
||||
tc.testPerCallLimit = celconfig.PerCallLimit
|
||||
}
|
||||
env, err := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), tc.strictCost).Extend(
|
||||
compatibilityVersion := tc.compatibilityVersion
|
||||
if compatibilityVersion == nil {
|
||||
compatibilityVersion = environment.DefaultCompatibilityVersion()
|
||||
}
|
||||
env, err := environment.MustBaseEnvSet(compatibilityVersion, tc.strictCost).Extend(
|
||||
environment.VersionedOptions{
|
||||
IntroducedVersion: environment.DefaultCompatibilityVersion(),
|
||||
IntroducedVersion: compatibilityVersion,
|
||||
ProgramOptions: []celgo.ProgramOption{celgo.CostLimit(tc.testPerCallLimit)},
|
||||
},
|
||||
)
|
||||
@ -868,12 +912,16 @@ func TestFilter(t *testing.T) {
|
||||
}
|
||||
require.Equal(t, len(evalResults), len(tc.results))
|
||||
for i, result := range tc.results {
|
||||
if result.EvalResult != evalResults[i].EvalResult {
|
||||
t.Errorf("Expected result '%v' but got '%v'", result.EvalResult, evalResults[i].EvalResult)
|
||||
}
|
||||
if result.Error != nil && !strings.Contains(evalResults[i].Error.Error(), result.Error.Error()) {
|
||||
t.Errorf("Expected result '%v' but got '%v'", result.Error, evalResults[i].Error)
|
||||
}
|
||||
if result.Error == nil && evalResults[i].Error != nil {
|
||||
t.Errorf("Expected result '%v' but got error '%v'", result.EvalResult, evalResults[i].Error)
|
||||
continue
|
||||
}
|
||||
if result.EvalResult != evalResults[i].EvalResult {
|
||||
t.Errorf("Expected result '%v' but got '%v'", result.EvalResult, evalResults[i].EvalResult)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/google/cel-go/cel"
|
||||
"github.com/google/cel-go/checker"
|
||||
@ -30,6 +31,8 @@ import (
|
||||
"k8s.io/apimachinery/pkg/util/version"
|
||||
celconfig "k8s.io/apiserver/pkg/apis/cel"
|
||||
"k8s.io/apiserver/pkg/cel/library"
|
||||
genericfeatures "k8s.io/apiserver/pkg/features"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
utilversion "k8s.io/apiserver/pkg/util/version"
|
||||
)
|
||||
|
||||
@ -146,6 +149,38 @@ var baseOptsWithoutStrictCost = []VersionedOptions{
|
||||
library.Format(),
|
||||
},
|
||||
},
|
||||
// Authz selectors
|
||||
{
|
||||
IntroducedVersion: version.MajorMinor(1, 31),
|
||||
FeatureEnabled: func() bool {
|
||||
enabled := utilfeature.DefaultFeatureGate.Enabled(genericfeatures.AuthorizeWithSelectors)
|
||||
authzSelectorsLibraryInit.Do(func() {
|
||||
// Record the first time feature enablement was checked for this library.
|
||||
// This is checked from integration tests to ensure no cached cel envs
|
||||
// are constructed before feature enablement is effectively set.
|
||||
authzSelectorsLibraryEnabled.Store(enabled)
|
||||
// Uncomment to debug where the first initialization is coming from if needed.
|
||||
// debug.PrintStack()
|
||||
})
|
||||
return enabled
|
||||
},
|
||||
EnvOptions: []cel.EnvOption{
|
||||
library.AuthzSelectors(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var (
|
||||
authzSelectorsLibraryInit sync.Once
|
||||
authzSelectorsLibraryEnabled atomic.Value
|
||||
)
|
||||
|
||||
// AuthzSelectorsLibraryEnabled returns whether the AuthzSelectors library was enabled when it was constructed.
|
||||
// If it has not been contructed yet, this returns `false, false`.
|
||||
// This is solely for the benefit of the integration tests making sure feature gates get correctly parsed before AuthzSelector ever has to check for enablement.
|
||||
func AuthzSelectorsLibraryEnabled() (enabled, constructed bool) {
|
||||
enabled, constructed = authzSelectorsLibraryEnabled.Load().(bool)
|
||||
return
|
||||
}
|
||||
|
||||
var StrictCostOpt = VersionedOptions{
|
||||
|
@ -175,7 +175,15 @@ type VersionedOptions struct {
|
||||
//
|
||||
// Optional.
|
||||
RemovedVersion *version.Version
|
||||
|
||||
// FeatureEnabled returns true if these options are enabled by feature gates,
|
||||
// and returns false if these options are not enabled due to feature gates.
|
||||
//
|
||||
// This takes priority over IntroducedVersion / RemovedVersion for the NewExpressions environment.
|
||||
//
|
||||
// The StoredExpressions environment ignores this function.
|
||||
//
|
||||
// Optional.
|
||||
FeatureEnabled func() bool
|
||||
// EnvOptions provides CEL EnvOptions. This may be used to add a cel.Variable, a
|
||||
// cel.Library, or to enable other CEL EnvOptions such as language settings.
|
||||
//
|
||||
@ -210,7 +218,7 @@ type VersionedOptions struct {
|
||||
// making multiple calls to Extend.
|
||||
func (e *EnvSet) Extend(options ...VersionedOptions) (*EnvSet, error) {
|
||||
if len(options) > 0 {
|
||||
newExprOpts, err := e.filterAndBuildOpts(e.newExpressions, e.compatibilityVersion, options)
|
||||
newExprOpts, err := e.filterAndBuildOpts(e.newExpressions, e.compatibilityVersion, true, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -218,7 +226,7 @@ func (e *EnvSet) Extend(options ...VersionedOptions) (*EnvSet, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
storedExprOpt, err := e.filterAndBuildOpts(e.storedExpressions, version.MajorMinor(math.MaxUint, math.MaxUint), options)
|
||||
storedExprOpt, err := e.filterAndBuildOpts(e.storedExpressions, version.MajorMinor(math.MaxUint, math.MaxUint), false, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -231,13 +239,26 @@ func (e *EnvSet) Extend(options ...VersionedOptions) (*EnvSet, error) {
|
||||
return e, nil
|
||||
}
|
||||
|
||||
func (e *EnvSet) filterAndBuildOpts(base *cel.Env, compatVer *version.Version, opts []VersionedOptions) (cel.EnvOption, error) {
|
||||
func (e *EnvSet) filterAndBuildOpts(base *cel.Env, compatVer *version.Version, honorFeatureGateEnablement bool, opts []VersionedOptions) (cel.EnvOption, error) {
|
||||
var envOpts []cel.EnvOption
|
||||
var progOpts []cel.ProgramOption
|
||||
var declTypes []*apiservercel.DeclType
|
||||
|
||||
for _, opt := range opts {
|
||||
var allowedByFeatureGate, allowedByVersion bool
|
||||
if opt.FeatureEnabled != nil && honorFeatureGateEnablement {
|
||||
// Feature-gate-enabled libraries must follow compatible default feature enablement.
|
||||
// Enabling alpha features in their first release enables libraries the previous API server is unaware of.
|
||||
allowedByFeatureGate = opt.FeatureEnabled()
|
||||
if !allowedByFeatureGate {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if compatVer.AtLeast(opt.IntroducedVersion) && (opt.RemovedVersion == nil || compatVer.LessThan(opt.RemovedVersion)) {
|
||||
allowedByVersion = true
|
||||
}
|
||||
|
||||
if allowedByFeatureGate || allowedByVersion {
|
||||
envOpts = append(envOpts, opt.EnvOptions...)
|
||||
progOpts = append(progOpts, opt.ProgramOptions...)
|
||||
declTypes = append(declTypes, opt.DeclTypes...)
|
||||
|
@ -19,12 +19,13 @@ package library
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
genericfeatures "k8s.io/apiserver/pkg/features"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/google/cel-go/cel"
|
||||
"github.com/google/cel-go/common/types"
|
||||
@ -198,6 +199,30 @@ import (
|
||||
// Examples:
|
||||
//
|
||||
// authorizer.group('').resource('pods').namespace('default').check('create').error()
|
||||
//
|
||||
// fieldSelector
|
||||
//
|
||||
// Takes a string field selector, parses it to field selector requirements, and includes it in the authorization check.
|
||||
// If the field selector does not parse successfully, no field selector requirements are included in the authorization check.
|
||||
// Added in Kubernetes 1.31+, Authz library version 1.
|
||||
//
|
||||
// <ResourceCheck>.fieldSelector(<string>) <ResourceCheck>
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// authorizer.group('').resource('pods').fieldSelector('spec.nodeName=mynode').check('list').allowed()
|
||||
//
|
||||
// labelSelector (added in v1, Kubernetes 1.31+)
|
||||
//
|
||||
// Takes a string label selector, parses it to label selector requirements, and includes it in the authorization check.
|
||||
// If the label selector does not parse successfully, no label selector requirements are included in the authorization check.
|
||||
// Added in Kubernetes 1.31+, Authz library version 1.
|
||||
//
|
||||
// <ResourceCheck>.labelSelector(<string>) <ResourceCheck>
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// authorizer.group('').resource('pods').labelSelector('app=example').check('list').allowed()
|
||||
func Authz() cel.EnvOption {
|
||||
return cel.Lib(authzLib)
|
||||
}
|
||||
@ -226,12 +251,6 @@ var authzLibraryDecls = map[string][]cel.FunctionOpt{
|
||||
"subresource": {
|
||||
cel.MemberOverload("resourcecheck_subresource", []*cel.Type{ResourceCheckType, cel.StringType}, ResourceCheckType,
|
||||
cel.BinaryBinding(resourceCheckSubresource))},
|
||||
"fieldSelector": {
|
||||
cel.MemberOverload("authorizer_fieldselector", []*cel.Type{ResourceCheckType, cel.StringType}, ResourceCheckType,
|
||||
cel.BinaryBinding(resourceCheckFieldSelector))},
|
||||
"labelSelector": {
|
||||
cel.MemberOverload("authorizer_labelselector", []*cel.Type{ResourceCheckType, cel.StringType}, ResourceCheckType,
|
||||
cel.BinaryBinding(resourceCheckLabelSelector))},
|
||||
"namespace": {
|
||||
cel.MemberOverload("resourcecheck_namespace", []*cel.Type{ResourceCheckType, cel.StringType}, ResourceCheckType,
|
||||
cel.BinaryBinding(resourceCheckNamespace))},
|
||||
@ -269,6 +288,66 @@ func (*authz) ProgramOptions() []cel.ProgramOption {
|
||||
return []cel.ProgramOption{}
|
||||
}
|
||||
|
||||
// AuthzSelectors provides a CEL function library extension for adding fieldSelector and
|
||||
// labelSelector filters to authorization checks. This requires the Authz library.
|
||||
// See documentation of the Authz library for use and availability of the authorizer variable.
|
||||
//
|
||||
// fieldSelector
|
||||
//
|
||||
// Takes a string field selector, parses it to field selector requirements, and includes it in the authorization check.
|
||||
// If the field selector does not parse successfully, no field selector requirements are included in the authorization check.
|
||||
// Added in Kubernetes 1.31+.
|
||||
//
|
||||
// <ResourceCheck>.fieldSelector(<string>) <ResourceCheck>
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// authorizer.group('').resource('pods').fieldSelector('spec.nodeName=mynode').check('list').allowed()
|
||||
//
|
||||
// labelSelector
|
||||
//
|
||||
// Takes a string label selector, parses it to label selector requirements, and includes it in the authorization check.
|
||||
// If the label selector does not parse successfully, no label selector requirements are included in the authorization check.
|
||||
// Added in Kubernetes 1.31+.
|
||||
//
|
||||
// <ResourceCheck>.labelSelector(<string>) <ResourceCheck>
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// authorizer.group('').resource('pods').labelSelector('app=example').check('list').allowed()
|
||||
func AuthzSelectors() cel.EnvOption {
|
||||
return cel.Lib(authzSelectorsLib)
|
||||
}
|
||||
|
||||
var authzSelectorsLib = &authzSelectors{}
|
||||
|
||||
type authzSelectors struct{}
|
||||
|
||||
func (*authzSelectors) LibraryName() string {
|
||||
return "k8s.authzSelectors"
|
||||
}
|
||||
|
||||
var authzSelectorsLibraryDecls = map[string][]cel.FunctionOpt{
|
||||
"fieldSelector": {
|
||||
cel.MemberOverload("authorizer_fieldselector", []*cel.Type{ResourceCheckType, cel.StringType}, ResourceCheckType,
|
||||
cel.BinaryBinding(resourceCheckFieldSelector))},
|
||||
"labelSelector": {
|
||||
cel.MemberOverload("authorizer_labelselector", []*cel.Type{ResourceCheckType, cel.StringType}, ResourceCheckType,
|
||||
cel.BinaryBinding(resourceCheckLabelSelector))},
|
||||
}
|
||||
|
||||
func (*authzSelectors) CompileOptions() []cel.EnvOption {
|
||||
options := make([]cel.EnvOption, 0, len(authzSelectorsLibraryDecls))
|
||||
for name, overloads := range authzSelectorsLibraryDecls {
|
||||
options = append(options, cel.Function(name, overloads...))
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
func (*authzSelectors) ProgramOptions() []cel.ProgramOption {
|
||||
return []cel.ProgramOption{}
|
||||
}
|
||||
|
||||
func authorizerPath(arg1, arg2 ref.Val) ref.Val {
|
||||
authz, ok := arg1.(authorizerVal)
|
||||
if !ok {
|
||||
|
@ -55,6 +55,25 @@ type CostEstimator struct {
|
||||
SizeEstimator checker.CostEstimator
|
||||
}
|
||||
|
||||
const (
|
||||
// shortest repeatable selector requirement that allocates a values slice is 2 characters: k,
|
||||
selectorLengthToRequirementCount = float64(.5)
|
||||
// the expensive parts to represent each requirement are a struct and a values slice
|
||||
costPerRequirement = float64(common.ListCreateBaseCost + common.StructCreateBaseCost)
|
||||
)
|
||||
|
||||
// a selector consists of a list of requirements held in a slice
|
||||
var baseSelectorCost = checker.CostEstimate{Min: common.ListCreateBaseCost, Max: common.ListCreateBaseCost}
|
||||
|
||||
func selectorCostEstimate(selectorLength checker.SizeEstimate) checker.CostEstimate {
|
||||
parseCost := selectorLength.MultiplyByCostFactor(common.StringTraversalCostFactor)
|
||||
|
||||
requirementCount := selectorLength.MultiplyByCostFactor(selectorLengthToRequirementCount)
|
||||
requirementCost := requirementCount.MultiplyByCostFactor(costPerRequirement)
|
||||
|
||||
return baseSelectorCost.Add(parseCost).Add(requirementCost)
|
||||
}
|
||||
|
||||
func (l *CostEstimator) CallCost(function, overloadId string, args []ref.Val, result ref.Val) *uint64 {
|
||||
switch function {
|
||||
case "check":
|
||||
@ -66,6 +85,13 @@ func (l *CostEstimator) CallCost(function, overloadId string, args []ref.Val, re
|
||||
// All authorization builder and accessor functions have a nominal cost
|
||||
cost := uint64(1)
|
||||
return &cost
|
||||
case "fieldSelector", "labelSelector":
|
||||
// field and label selector parse is a string parse into a structured set of requirements
|
||||
if len(args) >= 2 {
|
||||
selectorLength := actualSize(args[1])
|
||||
cost := selectorCostEstimate(checker.SizeEstimate{Min: selectorLength, Max: selectorLength})
|
||||
return &cost.Max
|
||||
}
|
||||
case "isSorted", "sum", "max", "min", "indexOf", "lastIndexOf":
|
||||
var cost uint64
|
||||
if len(args) > 0 {
|
||||
@ -227,6 +253,11 @@ func (l *CostEstimator) EstimateCallCost(function, overloadId string, target *ch
|
||||
case "serviceAccount", "path", "group", "resource", "subresource", "namespace", "name", "allowed", "reason", "error", "errored":
|
||||
// All authorization builder and accessor functions have a nominal cost
|
||||
return &checker.CallEstimate{CostEstimate: checker.CostEstimate{Min: 1, Max: 1}}
|
||||
case "fieldSelector", "labelSelector":
|
||||
// field and label selector parse is a string parse into a structured set of requirements
|
||||
if len(args) == 1 {
|
||||
return &checker.CallEstimate{CostEstimate: selectorCostEstimate(l.sizeEstimate(args[0]))}
|
||||
}
|
||||
case "isSorted", "sum", "max", "min", "indexOf", "lastIndexOf":
|
||||
if target != nil {
|
||||
// Charge 1 cost for comparing each element in the list
|
||||
|
@ -629,6 +629,18 @@ func TestAuthzLibrary(t *testing.T) {
|
||||
expectEstimatedCost: checker.CostEstimate{Min: 6, Max: 6},
|
||||
expectRuntimeCost: 6,
|
||||
},
|
||||
{
|
||||
name: "fieldSelector",
|
||||
expr: "authorizer.group('').resource('pods').fieldSelector('spec.nodeName=example-node-name.fully.qualified.domain.name.example.com')",
|
||||
expectEstimatedCost: checker.CostEstimate{Min: 1821, Max: 1821},
|
||||
expectRuntimeCost: 1821, // authorizer(1) + group(1) + resource(1) + fieldSelector(10 + ceil(71/2)*50=1800 + ceil(71*.1)=8)
|
||||
},
|
||||
{
|
||||
name: "labelSelector",
|
||||
expr: "authorizer.group('').resource('pods').labelSelector('spec.nodeName=example-node-name.fully.qualified.domain.name.example.com')",
|
||||
expectEstimatedCost: checker.CostEstimate{Min: 1821, Max: 1821},
|
||||
expectRuntimeCost: 1821, // authorizer(1) + group(1) + resource(1) + fieldSelector(10 + ceil(71/2)*50=1800 + ceil(71*.1)=8)
|
||||
},
|
||||
{
|
||||
name: "path check allowed",
|
||||
expr: "authorizer.path('/healthz').check('get').allowed()",
|
||||
@ -1064,6 +1076,7 @@ func testCost(t *testing.T, expr string, expectEsimatedCost checker.CostEstimate
|
||||
Regex(),
|
||||
Lists(),
|
||||
Authz(),
|
||||
AuthzSelectors(),
|
||||
Quantity(),
|
||||
ext.Sets(),
|
||||
IP(),
|
||||
|
@ -26,7 +26,7 @@ import (
|
||||
|
||||
func TestLibraryCompatibility(t *testing.T) {
|
||||
var libs []map[string][]cel.FunctionOpt
|
||||
libs = append(libs, authzLibraryDecls, listsLibraryDecls, regexLibraryDecls, urlLibraryDecls, quantityLibraryDecls, ipLibraryDecls, cidrLibraryDecls)
|
||||
libs = append(libs, authzLibraryDecls, listsLibraryDecls, regexLibraryDecls, urlLibraryDecls, quantityLibraryDecls, ipLibraryDecls, cidrLibraryDecls, formatLibraryDecls, authzSelectorsLibraryDecls)
|
||||
functionNames := sets.New[string]()
|
||||
for _, lib := range libs {
|
||||
for name := range lib {
|
||||
@ -49,6 +49,8 @@ func TestLibraryCompatibility(t *testing.T) {
|
||||
"add", "asApproximateFloat", "asInteger", "compareTo", "isGreaterThan", "isInteger", "isLessThan", "isQuantity", "quantity", "sign", "sub",
|
||||
// Kubernetes <1.30>:
|
||||
"ip", "family", "isUnspecified", "isLoopback", "isLinkLocalMulticast", "isLinkLocalUnicast", "isGlobalUnicast", "ip.isCanonical", "isIP", "cidr", "containsIP", "containsCIDR", "masked", "prefixLength", "isCIDR", "string",
|
||||
// Kubernetes <1.31>:
|
||||
"fieldSelector", "labelSelector", "validate", "format.named",
|
||||
// Kubernetes <1.??>:
|
||||
)
|
||||
|
||||
|
292
test/integration/apiserver/cel/authorizerselector/helper.go
Normal file
292
test/integration/apiserver/cel/authorizerselector/helper.go
Normal file
@ -0,0 +1,292 @@
|
||||
/*
|
||||
Copyright 2024 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 authorizerselector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
|
||||
resourcev1alpha2 "k8s.io/api/resource/v1alpha2"
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
extclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apiserver/pkg/cel/environment"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
apiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
|
||||
"k8s.io/kubernetes/test/integration/framework"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
func RunAuthzSelectorsLibraryTests(t *testing.T, featureEnabled bool) {
|
||||
if _, initialized := environment.AuthzSelectorsLibraryEnabled(); initialized {
|
||||
// This ensures CEL environments don't get initialized during init(),
|
||||
// before they can be informed by configured feature gates.
|
||||
// If this check fails, uncomment the debug.PrintStack() when the authz selectors
|
||||
// library is first initialized to find the culprit, and modify it to be lazily initialized on first use.
|
||||
t.Fatalf("authz selector library was initialized before feature gates were finalized (possibly from an init() or package variable)")
|
||||
}
|
||||
|
||||
// Start the server with the desired feature enablement
|
||||
server, err := apiservertesting.StartTestServer(t, nil, []string{
|
||||
fmt.Sprintf("--feature-gates=AuthorizeWithSelectors=%v", featureEnabled),
|
||||
"--runtime-config=resource.k8s.io/v1alpha2=true",
|
||||
}, framework.SharedEtcd())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer server.TearDownFn()
|
||||
|
||||
// Ensure the authz selectors library was initialzed and saw the right feature enablement
|
||||
if gotEnabled, initialized := environment.AuthzSelectorsLibraryEnabled(); !initialized {
|
||||
t.Fatalf("authz selector library was not initialized during API server construction")
|
||||
} else if gotEnabled != featureEnabled {
|
||||
t.Fatalf("authz selector library enabled=%v, expected %v", gotEnabled, featureEnabled)
|
||||
}
|
||||
|
||||
// Attempt to create API objects using the fieldSelector and labelSelector authorizer functions,
|
||||
// and ensure they are only allowed when the feature is enabled.
|
||||
|
||||
c, err := kubernetes.NewForConfig(server.ClientConfig)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create clientset: %v", err)
|
||||
}
|
||||
crdClient, err := extclientset.NewForConfig(server.ClientConfig)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create clientset: %v", err)
|
||||
}
|
||||
|
||||
boolFieldSelectorExpression := `type(authorizer.group('').resource('').fieldSelector('')) == string`
|
||||
stringFieldSelectorExpression := boolFieldSelectorExpression + ` ? 'yes' : 'no'`
|
||||
fieldSelectorErrorSubstring := `undeclared reference to 'fieldSelector'`
|
||||
|
||||
testcases := []struct {
|
||||
name string
|
||||
createObject func() error
|
||||
expectErrorsWhenEnabled []*regexp.Regexp
|
||||
expectErrorsWhenDisabled []*regexp.Regexp
|
||||
}{
|
||||
{
|
||||
name: "ValidatingAdmissionPolicy",
|
||||
createObject: func() error {
|
||||
obj := &admissionregistrationv1.ValidatingAdmissionPolicy{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-with-variables"},
|
||||
Spec: admissionregistrationv1.ValidatingAdmissionPolicySpec{
|
||||
MatchConstraints: &admissionregistrationv1.MatchResources{
|
||||
ResourceRules: []admissionregistrationv1.NamedRuleWithOperations{{
|
||||
RuleWithOperations: admissionregistrationv1.RuleWithOperations{
|
||||
Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.OperationAll},
|
||||
Rule: admissionregistrationv1.Rule{APIGroups: []string{"example.com"}, APIVersions: []string{"*"}, Resources: []string{"*"}}}}}},
|
||||
Validations: []admissionregistrationv1.Validation{{
|
||||
Expression: boolFieldSelectorExpression,
|
||||
MessageExpression: stringFieldSelectorExpression}},
|
||||
AuditAnnotations: []admissionregistrationv1.AuditAnnotation{{Key: "test", ValueExpression: stringFieldSelectorExpression}},
|
||||
MatchConditions: []admissionregistrationv1.MatchCondition{{Name: "test", Expression: boolFieldSelectorExpression}},
|
||||
Variables: []admissionregistrationv1.Variable{{Name: "test", Expression: boolFieldSelectorExpression}}}}
|
||||
_, err := c.AdmissionregistrationV1().ValidatingAdmissionPolicies().Create(context.TODO(), obj, metav1.CreateOptions{})
|
||||
return err
|
||||
},
|
||||
expectErrorsWhenEnabled: []*regexp.Regexp{
|
||||
// authorizer is not available to messageExpression
|
||||
regexp.MustCompile(`spec\.validations\[0\]\.messageExpression:.*undeclared reference to 'authorizer'`),
|
||||
},
|
||||
expectErrorsWhenDisabled: []*regexp.Regexp{
|
||||
regexp.MustCompile(`spec\.validations\[0\]\.expression:.*` + fieldSelectorErrorSubstring),
|
||||
// authorizer is not available to messageExpression
|
||||
regexp.MustCompile(`spec\.validations\[0\]\.messageExpression:.*undeclared reference to 'authorizer'`),
|
||||
regexp.MustCompile(`spec\.auditAnnotations\[0\]\.valueExpression:.*` + fieldSelectorErrorSubstring),
|
||||
regexp.MustCompile(`spec\.matchConditions\[0\]\.expression:.*` + fieldSelectorErrorSubstring),
|
||||
regexp.MustCompile(`spec\.variables\[0\]\.expression:.*` + fieldSelectorErrorSubstring),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ValidatingWebhookConfiguration",
|
||||
createObject: func() error {
|
||||
obj := &admissionregistrationv1.ValidatingWebhookConfiguration{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test"},
|
||||
Webhooks: []admissionregistrationv1.ValidatingWebhook{{
|
||||
Name: "test.example.com",
|
||||
ClientConfig: admissionregistrationv1.WebhookClientConfig{URL: ptr.To("https://127.0.0.1")},
|
||||
AdmissionReviewVersions: []string{"v1"},
|
||||
SideEffects: ptr.To(admissionregistrationv1.SideEffectClassNone),
|
||||
Rules: []admissionregistrationv1.RuleWithOperations{{
|
||||
Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.OperationAll},
|
||||
Rule: admissionregistrationv1.Rule{APIGroups: []string{"example.com"}, APIVersions: []string{"*"}, Resources: []string{"*"}}}},
|
||||
MatchConditions: []admissionregistrationv1.MatchCondition{{Name: "test", Expression: boolFieldSelectorExpression}}}}}
|
||||
_, err := c.AdmissionregistrationV1().ValidatingWebhookConfigurations().Create(context.TODO(), obj, metav1.CreateOptions{})
|
||||
return err
|
||||
},
|
||||
expectErrorsWhenDisabled: []*regexp.Regexp{
|
||||
regexp.MustCompile(`webhooks\[0\]\.matchConditions\[0\]\.expression:.*` + fieldSelectorErrorSubstring),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "MutatingWebhookConfiguration",
|
||||
createObject: func() error {
|
||||
obj := &admissionregistrationv1.MutatingWebhookConfiguration{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test"},
|
||||
Webhooks: []admissionregistrationv1.MutatingWebhook{{
|
||||
Name: "test.example.com",
|
||||
ClientConfig: admissionregistrationv1.WebhookClientConfig{URL: ptr.To("https://127.0.0.1")},
|
||||
AdmissionReviewVersions: []string{"v1"},
|
||||
SideEffects: ptr.To(admissionregistrationv1.SideEffectClassNone),
|
||||
Rules: []admissionregistrationv1.RuleWithOperations{{
|
||||
Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.OperationAll},
|
||||
Rule: admissionregistrationv1.Rule{APIGroups: []string{"example.com"}, APIVersions: []string{"*"}, Resources: []string{"*"}}}},
|
||||
MatchConditions: []admissionregistrationv1.MatchCondition{{Name: "test", Expression: boolFieldSelectorExpression}}}}}
|
||||
_, err := c.AdmissionregistrationV1().MutatingWebhookConfigurations().Create(context.TODO(), obj, metav1.CreateOptions{})
|
||||
return err
|
||||
},
|
||||
expectErrorsWhenDisabled: []*regexp.Regexp{
|
||||
regexp.MustCompile(`webhooks\[0\]\.matchConditions\[0\]\.expression:.*` + fieldSelectorErrorSubstring),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ResourceClaimParameters",
|
||||
createObject: func() error {
|
||||
obj := &resourcev1alpha2.ResourceClaimParameters{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test"},
|
||||
DriverRequests: []resourcev1alpha2.DriverRequests{{
|
||||
DriverName: "example.com",
|
||||
Requests: []resourcev1alpha2.ResourceRequest{{
|
||||
ResourceRequestModel: resourcev1alpha2.ResourceRequestModel{
|
||||
NamedResources: &resourcev1alpha2.NamedResourcesRequest{Selector: boolFieldSelectorExpression}}}}}}}
|
||||
_, err := c.ResourceV1alpha2().ResourceClaimParameters("default").Create(context.TODO(), obj, metav1.CreateOptions{})
|
||||
return err
|
||||
},
|
||||
// authorizer is not available to resource APIs
|
||||
expectErrorsWhenEnabled: []*regexp.Regexp{regexp.MustCompile(`driverRequests\[0\]\.requests\[0\]\.namedResources\.selector:.*undeclared reference to 'authorizer'`)},
|
||||
expectErrorsWhenDisabled: []*regexp.Regexp{regexp.MustCompile(`driverRequests\[0\]\.requests\[0\]\.namedResources\.selector:.*undeclared reference to 'authorizer'`)},
|
||||
},
|
||||
{
|
||||
name: "CustomResourceDefinition - rule",
|
||||
createObject: func() error {
|
||||
obj := &apiextensionsv1.CustomResourceDefinition{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "crontabs.apis.example.com"},
|
||||
Spec: apiextensionsv1.CustomResourceDefinitionSpec{
|
||||
Group: "apis.example.com",
|
||||
Scope: apiextensionsv1.NamespaceScoped,
|
||||
Names: apiextensionsv1.CustomResourceDefinitionNames{Plural: "crontabs", Singular: "crontab", Kind: "CronTab", ListKind: "CronTabList"},
|
||||
Versions: []apiextensionsv1.CustomResourceDefinitionVersion{{
|
||||
Name: "v1beta1",
|
||||
Served: true,
|
||||
Storage: true,
|
||||
Schema: &apiextensionsv1.CustomResourceValidation{
|
||||
OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
|
||||
Type: "object",
|
||||
Properties: map[string]apiextensionsv1.JSONSchemaProps{
|
||||
"spec": {
|
||||
Type: "object",
|
||||
XValidations: apiextensionsv1.ValidationRules{{Rule: boolFieldSelectorExpression}}}}}}}}}}
|
||||
_, err := crdClient.ApiextensionsV1().CustomResourceDefinitions().Create(context.TODO(), obj, metav1.CreateOptions{})
|
||||
return err
|
||||
},
|
||||
// authorizer is not available to CRD validation
|
||||
expectErrorsWhenEnabled: []*regexp.Regexp{regexp.MustCompile(`x-kubernetes-validations\[0\]\.rule:.*undeclared reference to 'authorizer'`)},
|
||||
expectErrorsWhenDisabled: []*regexp.Regexp{regexp.MustCompile(`x-kubernetes-validations\[0\]\.rule:.*undeclared reference to 'authorizer'`)},
|
||||
},
|
||||
{
|
||||
name: "CustomResourceDefinition - messageExpression",
|
||||
createObject: func() error {
|
||||
obj := &apiextensionsv1.CustomResourceDefinition{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "crontabs.apis.example.com"},
|
||||
Spec: apiextensionsv1.CustomResourceDefinitionSpec{
|
||||
Group: "apis.example.com",
|
||||
Scope: apiextensionsv1.NamespaceScoped,
|
||||
Names: apiextensionsv1.CustomResourceDefinitionNames{Plural: "crontabs", Singular: "crontab", Kind: "CronTab", ListKind: "CronTabList"},
|
||||
Versions: []apiextensionsv1.CustomResourceDefinitionVersion{{
|
||||
Name: "v1beta1",
|
||||
Served: true,
|
||||
Storage: true,
|
||||
Schema: &apiextensionsv1.CustomResourceValidation{
|
||||
OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
|
||||
Type: "object",
|
||||
Properties: map[string]apiextensionsv1.JSONSchemaProps{
|
||||
"spec": {
|
||||
Type: "object",
|
||||
XValidations: apiextensionsv1.ValidationRules{{Rule: `self == oldSelf`, MessageExpression: stringFieldSelectorExpression}}}}}}}}}}
|
||||
_, err := crdClient.ApiextensionsV1().CustomResourceDefinitions().Create(context.TODO(), obj, metav1.CreateOptions{})
|
||||
return err
|
||||
},
|
||||
// authorizer is not available to CRD validation
|
||||
expectErrorsWhenEnabled: []*regexp.Regexp{regexp.MustCompile(`x-kubernetes-validations\[0\]\.messageExpression:.*undeclared reference to 'authorizer'`)},
|
||||
expectErrorsWhenDisabled: []*regexp.Regexp{regexp.MustCompile(`x-kubernetes-validations\[0\]\.messageExpression:.*undeclared reference to 'authorizer'`)},
|
||||
},
|
||||
}
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := tc.createObject()
|
||||
|
||||
var expectedErrors []*regexp.Regexp
|
||||
if featureEnabled {
|
||||
expectedErrors = tc.expectErrorsWhenEnabled
|
||||
} else {
|
||||
expectedErrors = tc.expectErrorsWhenDisabled
|
||||
}
|
||||
|
||||
switch {
|
||||
case len(expectedErrors) == 0 && err == nil:
|
||||
// success
|
||||
case len(expectedErrors) == 0 && err != nil:
|
||||
t.Fatalf("expected success, got error:\n%s", strings.Join(sets.List(getCauses(t, err)), "\n\n"))
|
||||
case len(expectedErrors) > 0 && err == nil:
|
||||
t.Fatalf("expected error, got success")
|
||||
case len(expectedErrors) > 0 && err != nil:
|
||||
// make sure errors match expectations
|
||||
actualCauses := getCauses(t, err)
|
||||
for _, expectCause := range expectedErrors {
|
||||
found := false
|
||||
for _, cause := range actualCauses.UnsortedList() {
|
||||
if expectCause.MatchString(cause) {
|
||||
actualCauses.Delete(cause)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("missing error matching %s", expectCause)
|
||||
}
|
||||
}
|
||||
if len(actualCauses) > 0 {
|
||||
t.Errorf("unexpected errors:\n%s", strings.Join(sets.List(actualCauses), "\n\n"))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func getCauses(t *testing.T, err error) sets.Set[string] {
|
||||
t.Helper()
|
||||
status, ok := err.(apierrors.APIStatus)
|
||||
if !ok {
|
||||
t.Fatalf("expected API status error, got %#v", err)
|
||||
}
|
||||
if len(status.Status().Details.Causes) == 0 {
|
||||
t.Fatalf("expected API status error with causes, got %#v", err)
|
||||
}
|
||||
causes := sets.New[string]()
|
||||
for _, cause := range status.Status().Details.Causes {
|
||||
causes.Insert(cause.Field + ": " + cause.Message)
|
||||
}
|
||||
return causes
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
/*
|
||||
Copyright 2024 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 selectordisabled
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"k8s.io/kubernetes/test/integration/apiserver/cel/authorizerselector"
|
||||
)
|
||||
|
||||
// TestAuthzSelectorsLibraryDisabled ensures that the authzselectors library feature disablement works properly.
|
||||
// CEL envs and compilers cached per process mean this must be the only test in this package.
|
||||
func TestAuthzSelectorsLibraryDisabled(t *testing.T) {
|
||||
authorizerselector.RunAuthzSelectorsLibraryTests(t, false)
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
/*
|
||||
Copyright 2024 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 selectordisabled
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"k8s.io/kubernetes/test/integration/framework"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
framework.EtcdMain(m.Run)
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
/*
|
||||
Copyright 2024 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 selectorenabled
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"k8s.io/kubernetes/test/integration/apiserver/cel/authorizerselector"
|
||||
)
|
||||
|
||||
// TestAuthzSelectorsLibraryEnabled ensures that the authzselectors library feature enablement works properly.
|
||||
// CEL envs and compilers cached per process mean this must be the only test in this package.
|
||||
func TestAuthzSelectorsLibraryEnabled(t *testing.T) {
|
||||
authorizerselector.RunAuthzSelectorsLibraryTests(t, true)
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
/*
|
||||
Copyright 2024 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 selectorenabled
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"k8s.io/kubernetes/test/integration/framework"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
framework.EtcdMain(m.Run)
|
||||
}
|
Loading…
Reference in New Issue
Block a user