1517 lines
50 KiB
Go
1517 lines
50 KiB
Go
/*
|
|
Copyright 2022 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 library
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/google/cel-go/common/types/ref"
|
|
|
|
"github.com/google/cel-go/cel"
|
|
"github.com/google/cel-go/checker"
|
|
"github.com/google/cel-go/common"
|
|
"github.com/google/cel-go/common/ast"
|
|
"github.com/google/cel-go/common/types"
|
|
"github.com/google/cel-go/ext"
|
|
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
|
|
|
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
|
apiservercel "k8s.io/apiserver/pkg/cel"
|
|
)
|
|
|
|
const (
|
|
intListLiteral = "[1, 2, 3, 4, 5]"
|
|
uintListLiteral = "[uint(1), uint(2), uint(3), uint(4), uint(5)]"
|
|
doubleListLiteral = "[1.0, 2.0, 3.0, 4.0, 5.0]"
|
|
boolListLiteral = "[false, true, false, true, false]"
|
|
stringListLiteral = "['012345678901', '012345678901', '012345678901', '012345678901', '012345678901']"
|
|
bytesListLiteral = "[bytes('012345678901'), bytes('012345678901'), bytes('012345678901'), bytes('012345678901'), bytes('012345678901')]"
|
|
durationListLiteral = "[duration('1s'), duration('2s'), duration('3s'), duration('4s'), duration('5s')]"
|
|
timestampListLiteral = "[timestamp('2011-01-01T00:00:00.000+01:00'), timestamp('2011-01-02T00:00:00.000+01:00'), " +
|
|
"timestamp('2011-01-03T00:00:00.000+01:00'), timestamp('2011-01-04T00:00:00.000+01:00'), " +
|
|
"timestamp('2011-01-05T00:00:00.000+01:00')]"
|
|
stringLiteral = "'01234567890123456789012345678901234567890123456789'"
|
|
)
|
|
|
|
type comparableCost struct {
|
|
comparableLiteral string
|
|
expectedEstimatedCost checker.CostEstimate
|
|
expectedRuntimeCost uint64
|
|
|
|
param string
|
|
}
|
|
|
|
func TestListsCost(t *testing.T) {
|
|
cases := []struct {
|
|
opts []string
|
|
costs []comparableCost
|
|
}{
|
|
{
|
|
opts: []string{".sum()"},
|
|
// 10 cost for the list declaration, the rest is the due to the function call
|
|
costs: []comparableCost{
|
|
{
|
|
comparableLiteral: intListLiteral,
|
|
expectedEstimatedCost: checker.CostEstimate{Min: 15, Max: 15}, expectedRuntimeCost: 15,
|
|
},
|
|
{
|
|
comparableLiteral: uintListLiteral,
|
|
expectedEstimatedCost: checker.CostEstimate{Min: 20, Max: 20}, expectedRuntimeCost: 20, // +5 for casts
|
|
},
|
|
{
|
|
comparableLiteral: doubleListLiteral,
|
|
expectedEstimatedCost: checker.CostEstimate{Min: 15, Max: 15}, expectedRuntimeCost: 15,
|
|
},
|
|
{
|
|
comparableLiteral: durationListLiteral,
|
|
expectedEstimatedCost: checker.CostEstimate{Min: 20, Max: 20}, expectedRuntimeCost: 20, // +5 for casts
|
|
},
|
|
},
|
|
},
|
|
{
|
|
opts: []string{".isSorted()", ".max()", ".min()"},
|
|
// 10 cost for the list declaration, the rest is the due to the function call
|
|
costs: []comparableCost{
|
|
{
|
|
comparableLiteral: intListLiteral,
|
|
expectedEstimatedCost: checker.CostEstimate{Min: 15, Max: 15}, expectedRuntimeCost: 15,
|
|
},
|
|
{
|
|
comparableLiteral: uintListLiteral,
|
|
expectedEstimatedCost: checker.CostEstimate{Min: 20, Max: 20}, expectedRuntimeCost: 20, // +5 for numeric casts
|
|
},
|
|
{
|
|
comparableLiteral: doubleListLiteral,
|
|
expectedEstimatedCost: checker.CostEstimate{Min: 15, Max: 15}, expectedRuntimeCost: 15,
|
|
},
|
|
{
|
|
comparableLiteral: boolListLiteral,
|
|
expectedEstimatedCost: checker.CostEstimate{Min: 15, Max: 15}, expectedRuntimeCost: 15,
|
|
},
|
|
{
|
|
comparableLiteral: stringListLiteral,
|
|
expectedEstimatedCost: checker.CostEstimate{Min: 15, Max: 25}, expectedRuntimeCost: 15, // +5 for string comparisons
|
|
},
|
|
{
|
|
comparableLiteral: bytesListLiteral,
|
|
expectedEstimatedCost: checker.CostEstimate{Min: 25, Max: 35}, expectedRuntimeCost: 25, // +10 for casts from string to byte, +5 for byte comparisons
|
|
},
|
|
{
|
|
comparableLiteral: durationListLiteral,
|
|
expectedEstimatedCost: checker.CostEstimate{Min: 20, Max: 20}, expectedRuntimeCost: 20, // +5 for numeric casts
|
|
},
|
|
{
|
|
comparableLiteral: timestampListLiteral,
|
|
expectedEstimatedCost: checker.CostEstimate{Min: 20, Max: 20}, expectedRuntimeCost: 20, // +5 for casts
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for _, tc := range cases {
|
|
for _, op := range tc.opts {
|
|
for _, typ := range tc.costs {
|
|
t.Run(typ.comparableLiteral+op, func(t *testing.T) {
|
|
e := typ.comparableLiteral + op
|
|
testCost(t, e, typ.expectedEstimatedCost, typ.expectedRuntimeCost)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIndexOfCost(t *testing.T) {
|
|
cases := []struct {
|
|
opts []string
|
|
costs []comparableCost
|
|
}{
|
|
{
|
|
opts: []string{".indexOf(%s)", ".lastIndexOf(%s)"},
|
|
// 10 cost for the list declaration, the rest is the due to the function call
|
|
costs: []comparableCost{
|
|
{
|
|
comparableLiteral: intListLiteral, param: "3",
|
|
expectedEstimatedCost: checker.CostEstimate{Min: 15, Max: 15}, expectedRuntimeCost: 15,
|
|
},
|
|
{
|
|
comparableLiteral: uintListLiteral, param: "uint(3)",
|
|
expectedEstimatedCost: checker.CostEstimate{Min: 21, Max: 21}, expectedRuntimeCost: 21, // +5 for numeric casts
|
|
},
|
|
{
|
|
comparableLiteral: doubleListLiteral, param: "3.0",
|
|
expectedEstimatedCost: checker.CostEstimate{Min: 15, Max: 15}, expectedRuntimeCost: 15,
|
|
},
|
|
{
|
|
comparableLiteral: boolListLiteral, param: "true",
|
|
expectedEstimatedCost: checker.CostEstimate{Min: 15, Max: 15}, expectedRuntimeCost: 15,
|
|
},
|
|
{
|
|
comparableLiteral: stringListLiteral, param: "'x'",
|
|
expectedEstimatedCost: checker.CostEstimate{Min: 15, Max: 25}, expectedRuntimeCost: 15, // +5 for string comparisons
|
|
},
|
|
{
|
|
comparableLiteral: bytesListLiteral, param: "bytes('x')",
|
|
expectedEstimatedCost: checker.CostEstimate{Min: 26, Max: 36}, expectedRuntimeCost: 26, // +11 for casts from string to byte, +5 for byte comparisons
|
|
},
|
|
{
|
|
comparableLiteral: durationListLiteral, param: "duration('3s')",
|
|
expectedEstimatedCost: checker.CostEstimate{Min: 21, Max: 21}, expectedRuntimeCost: 21, // +6 for casts from duration to byte
|
|
},
|
|
{
|
|
comparableLiteral: timestampListLiteral, param: "timestamp('2011-01-03T00:00:00.000+01:00')",
|
|
expectedEstimatedCost: checker.CostEstimate{Min: 21, Max: 21}, expectedRuntimeCost: 21, // +6 for casts from timestamp to byte
|
|
},
|
|
|
|
// index of operations are also defined for strings
|
|
{
|
|
comparableLiteral: stringLiteral, param: "'123'",
|
|
expectedEstimatedCost: checker.CostEstimate{Min: 5, Max: 5}, expectedRuntimeCost: 5,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for _, tc := range cases {
|
|
for _, op := range tc.opts {
|
|
for _, typ := range tc.costs {
|
|
opWithParam := fmt.Sprintf(op, typ.param)
|
|
t.Run(typ.comparableLiteral+opWithParam, func(t *testing.T) {
|
|
e := typ.comparableLiteral + opWithParam
|
|
testCost(t, e, typ.expectedEstimatedCost, typ.expectedRuntimeCost)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestURLsCost(t *testing.T) {
|
|
cases := []struct {
|
|
ops []string
|
|
expectEsimatedCost checker.CostEstimate
|
|
expectRuntimeCost uint64
|
|
}{
|
|
{
|
|
ops: []string{".getScheme()", ".getHostname()", ".getHost()", ".getPort()", ".getEscapedPath()", ".getQuery()"},
|
|
expectEsimatedCost: checker.CostEstimate{Min: 4, Max: 4},
|
|
expectRuntimeCost: 4,
|
|
},
|
|
{
|
|
ops: []string{" == url('https:://kubernetes.io/')"},
|
|
expectEsimatedCost: checker.CostEstimate{Min: 7, Max: 9},
|
|
expectRuntimeCost: 7,
|
|
},
|
|
{
|
|
ops: []string{" == url('http://x.b')"},
|
|
expectEsimatedCost: checker.CostEstimate{Min: 5, Max: 5},
|
|
expectRuntimeCost: 5,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
for _, op := range tc.ops {
|
|
t.Run("url."+op, func(t *testing.T) {
|
|
testCost(t, "url('https:://kubernetes.io/')"+op, tc.expectEsimatedCost, tc.expectRuntimeCost)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIPCost(t *testing.T) {
|
|
ipv4 := "ip('192.168.0.1')"
|
|
ipv4BaseEstimatedCost := checker.CostEstimate{Min: 2, Max: 2}
|
|
ipv4BaseRuntimeCost := uint64(2)
|
|
|
|
ipv6 := "ip('2001:db8:3333:4444:5555:6666:7777:8888')"
|
|
ipv6BaseEstimatedCost := checker.CostEstimate{Min: 4, Max: 4}
|
|
ipv6BaseRuntimeCost := uint64(4)
|
|
|
|
testCases := []struct {
|
|
ops []string
|
|
expectEsimatedCost func(checker.CostEstimate) checker.CostEstimate
|
|
expectRuntimeCost func(uint64) uint64
|
|
}{
|
|
{
|
|
// For just parsing the IP, the cost is expected to be the base.
|
|
ops: []string{""},
|
|
expectEsimatedCost: func(c checker.CostEstimate) checker.CostEstimate { return c },
|
|
expectRuntimeCost: func(c uint64) uint64 { return c },
|
|
},
|
|
{
|
|
ops: []string{".family()", ".isUnspecified()", ".isLoopback()", ".isLinkLocalMulticast()", ".isLinkLocalUnicast()", ".isGlobalUnicast()"},
|
|
// For most other operations, the cost is expected to be the base + 1.
|
|
expectEsimatedCost: func(c checker.CostEstimate) checker.CostEstimate {
|
|
return checker.CostEstimate{Min: c.Min + 1, Max: c.Max + 1}
|
|
},
|
|
expectRuntimeCost: func(c uint64) uint64 { return c + 1 },
|
|
},
|
|
{
|
|
ops: []string{" == ip('192.168.0.1')"},
|
|
// For most other operations, the cost is expected to be the base + 1.
|
|
expectEsimatedCost: func(c checker.CostEstimate) checker.CostEstimate {
|
|
return c.Add(ipv4BaseEstimatedCost).Add(checker.CostEstimate{Min: 1, Max: 1})
|
|
},
|
|
expectRuntimeCost: func(c uint64) uint64 { return c + ipv4BaseRuntimeCost + 1 },
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
for _, op := range tc.ops {
|
|
t.Run(ipv4+op, func(t *testing.T) {
|
|
testCost(t, ipv4+op, tc.expectEsimatedCost(ipv4BaseEstimatedCost), tc.expectRuntimeCost(ipv4BaseRuntimeCost))
|
|
})
|
|
|
|
t.Run(ipv6+op, func(t *testing.T) {
|
|
testCost(t, ipv6+op, tc.expectEsimatedCost(ipv6BaseEstimatedCost), tc.expectRuntimeCost(ipv6BaseRuntimeCost))
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIPIsCanonicalCost(t *testing.T) {
|
|
testCases := []struct {
|
|
op string
|
|
expectEsimatedCost checker.CostEstimate
|
|
expectRuntimeCost uint64
|
|
}{
|
|
{
|
|
op: "ip.isCanonical('192.168.0.1')",
|
|
expectEsimatedCost: checker.CostEstimate{Min: 3, Max: 3},
|
|
expectRuntimeCost: 3,
|
|
},
|
|
{
|
|
op: "ip.isCanonical('2001:db8:3333:4444:5555:6666:7777:8888')",
|
|
expectEsimatedCost: checker.CostEstimate{Min: 8, Max: 8},
|
|
expectRuntimeCost: 8,
|
|
},
|
|
{
|
|
op: "ip.isCanonical('2001:db8::abcd')",
|
|
expectEsimatedCost: checker.CostEstimate{Min: 3, Max: 3},
|
|
expectRuntimeCost: 3,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.op, func(t *testing.T) {
|
|
testCost(t, tc.op, tc.expectEsimatedCost, tc.expectRuntimeCost)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCIDRCost(t *testing.T) {
|
|
ipv4 := "cidr('192.168.0.0/16')"
|
|
ipv4BaseEstimatedCost := checker.CostEstimate{Min: 2, Max: 2}
|
|
ipv4BaseRuntimeCost := uint64(2)
|
|
|
|
ipv6 := "cidr('2001:db8::/32')"
|
|
ipv6BaseEstimatedCost := checker.CostEstimate{Min: 2, Max: 2}
|
|
ipv6BaseRuntimeCost := uint64(2)
|
|
|
|
type testCase struct {
|
|
ops []string
|
|
expectEsimatedCost func(checker.CostEstimate) checker.CostEstimate
|
|
expectRuntimeCost func(uint64) uint64
|
|
}
|
|
|
|
cases := []testCase{
|
|
{
|
|
// For just parsing the IP, the cost is expected to be the base.
|
|
ops: []string{""},
|
|
expectEsimatedCost: func(c checker.CostEstimate) checker.CostEstimate { return c },
|
|
expectRuntimeCost: func(c uint64) uint64 { return c },
|
|
},
|
|
{
|
|
ops: []string{".ip()", ".prefixLength()", ".masked()"},
|
|
// For most other operations, the cost is expected to be the base + 1.
|
|
expectEsimatedCost: func(c checker.CostEstimate) checker.CostEstimate {
|
|
return checker.CostEstimate{Min: c.Min + 1, Max: c.Max + 1}
|
|
},
|
|
expectRuntimeCost: func(c uint64) uint64 { return c + 1 },
|
|
},
|
|
{
|
|
ops: []string{" == cidr('2001:db8::/32')"},
|
|
// For most other operations, the cost is expected to be the base + 1.
|
|
expectEsimatedCost: func(c checker.CostEstimate) checker.CostEstimate {
|
|
return c.Add(ipv6BaseEstimatedCost).Add(checker.CostEstimate{Min: 1, Max: 1})
|
|
},
|
|
expectRuntimeCost: func(c uint64) uint64 { return c + ipv6BaseRuntimeCost + 1 },
|
|
},
|
|
}
|
|
|
|
//nolint:gocritic
|
|
ipv4Cases := append(cases, []testCase{
|
|
{
|
|
ops: []string{".containsCIDR(cidr('192.0.0.0/30'))"},
|
|
expectEsimatedCost: func(c checker.CostEstimate) checker.CostEstimate {
|
|
return checker.CostEstimate{Min: c.Min + 5, Max: c.Max + 9}
|
|
},
|
|
expectRuntimeCost: func(c uint64) uint64 { return c + 5 },
|
|
},
|
|
{
|
|
ops: []string{".containsCIDR(cidr('192.168.0.0/16'))"},
|
|
expectEsimatedCost: func(c checker.CostEstimate) checker.CostEstimate {
|
|
return checker.CostEstimate{Min: c.Min + 5, Max: c.Max + 9}
|
|
},
|
|
expectRuntimeCost: func(c uint64) uint64 { return c + 5 },
|
|
},
|
|
{
|
|
ops: []string{".containsCIDR('192.0.0.0/30')"},
|
|
expectEsimatedCost: func(c checker.CostEstimate) checker.CostEstimate {
|
|
return checker.CostEstimate{Min: c.Min + 5, Max: c.Max + 9}
|
|
},
|
|
expectRuntimeCost: func(c uint64) uint64 { return c + 5 },
|
|
},
|
|
{
|
|
ops: []string{".containsCIDR('192.168.0.0/16')"},
|
|
expectEsimatedCost: func(c checker.CostEstimate) checker.CostEstimate {
|
|
return checker.CostEstimate{Min: c.Min + 5, Max: c.Max + 9}
|
|
},
|
|
expectRuntimeCost: func(c uint64) uint64 { return c + 5 },
|
|
},
|
|
{
|
|
ops: []string{".containsIP(ip('192.0.0.1'))"},
|
|
expectEsimatedCost: func(c checker.CostEstimate) checker.CostEstimate {
|
|
return checker.CostEstimate{Min: c.Min + 2, Max: c.Max + 5}
|
|
},
|
|
expectRuntimeCost: func(c uint64) uint64 { return c + 2 },
|
|
},
|
|
{
|
|
ops: []string{".containsIP(ip('192.169.0.1'))"},
|
|
expectEsimatedCost: func(c checker.CostEstimate) checker.CostEstimate {
|
|
return checker.CostEstimate{Min: c.Min + 3, Max: c.Max + 6}
|
|
},
|
|
expectRuntimeCost: func(c uint64) uint64 { return c + 3 },
|
|
},
|
|
{
|
|
ops: []string{".containsIP(ip('192.169.169.250'))"},
|
|
expectEsimatedCost: func(c checker.CostEstimate) checker.CostEstimate {
|
|
return checker.CostEstimate{Min: c.Min + 3, Max: c.Max + 6}
|
|
},
|
|
expectRuntimeCost: func(c uint64) uint64 { return c + 3 },
|
|
},
|
|
{
|
|
ops: []string{".containsIP('192.0.0.1')"},
|
|
expectEsimatedCost: func(c checker.CostEstimate) checker.CostEstimate {
|
|
return checker.CostEstimate{Min: c.Min + 2, Max: c.Max + 5}
|
|
},
|
|
expectRuntimeCost: func(c uint64) uint64 { return c + 2 },
|
|
},
|
|
{
|
|
ops: []string{".containsIP('192.169.0.1')"},
|
|
expectEsimatedCost: func(c checker.CostEstimate) checker.CostEstimate {
|
|
return checker.CostEstimate{Min: c.Min + 3, Max: c.Max + 6}
|
|
},
|
|
expectRuntimeCost: func(c uint64) uint64 { return c + 3 },
|
|
},
|
|
}...)
|
|
|
|
//nolint:gocritic
|
|
ipv6Cases := append(cases, []testCase{
|
|
{
|
|
ops: []string{".containsCIDR(cidr('2001:db8::/126'))"},
|
|
// For operations like checking if an IP is in a CIDR, the cost is expected to higher.
|
|
expectEsimatedCost: func(c checker.CostEstimate) checker.CostEstimate {
|
|
return checker.CostEstimate{Min: c.Min + 5, Max: c.Max + 9}
|
|
},
|
|
expectRuntimeCost: func(c uint64) uint64 { return c + 5 },
|
|
},
|
|
{
|
|
ops: []string{".containsCIDR(cidr('2001:db8::/32'))"},
|
|
// For operations like checking if an IP is in a CIDR, the cost is expected to higher.
|
|
expectEsimatedCost: func(c checker.CostEstimate) checker.CostEstimate {
|
|
return checker.CostEstimate{Min: c.Min + 5, Max: c.Max + 9}
|
|
},
|
|
expectRuntimeCost: func(c uint64) uint64 { return c + 5 },
|
|
},
|
|
{
|
|
ops: []string{".containsCIDR('2001:db8::/126')"},
|
|
// For operations like checking if an IP is in a CIDR, the cost is expected to higher.
|
|
expectEsimatedCost: func(c checker.CostEstimate) checker.CostEstimate {
|
|
return checker.CostEstimate{Min: c.Min + 5, Max: c.Max + 9}
|
|
},
|
|
expectRuntimeCost: func(c uint64) uint64 { return c + 5 },
|
|
},
|
|
{
|
|
ops: []string{".containsCIDR('2001:db8::/32')"},
|
|
// For operations like checking if an IP is in a CIDR, the cost is expected to higher.
|
|
expectEsimatedCost: func(c checker.CostEstimate) checker.CostEstimate {
|
|
return checker.CostEstimate{Min: c.Min + 5, Max: c.Max + 9}
|
|
},
|
|
expectRuntimeCost: func(c uint64) uint64 { return c + 5 },
|
|
},
|
|
{
|
|
ops: []string{".containsIP(ip('2001:db8:3333:4444:5555:6666:7777:8888'))"},
|
|
// For operations like checking if an IP is in a CIDR, the cost is expected to higher.
|
|
expectEsimatedCost: func(c checker.CostEstimate) checker.CostEstimate {
|
|
return checker.CostEstimate{Min: c.Min + 5, Max: c.Max + 8}
|
|
},
|
|
expectRuntimeCost: func(c uint64) uint64 { return c + 5 },
|
|
},
|
|
{
|
|
ops: []string{".containsIP(ip('2001:db8::1'))"},
|
|
// For operations like checking if an IP is in a CIDR, the cost is expected to higher.
|
|
expectEsimatedCost: func(c checker.CostEstimate) checker.CostEstimate {
|
|
return checker.CostEstimate{Min: c.Min + 3, Max: c.Max + 6}
|
|
},
|
|
expectRuntimeCost: func(c uint64) uint64 { return c + 3 },
|
|
},
|
|
{
|
|
ops: []string{".containsIP('2001:db8:3333:4444:5555:6666:7777:8888')"},
|
|
// For operations like checking if an IP is in a CIDR, the cost is expected to higher.
|
|
expectEsimatedCost: func(c checker.CostEstimate) checker.CostEstimate {
|
|
return checker.CostEstimate{Min: c.Min + 5, Max: c.Max + 8}
|
|
},
|
|
expectRuntimeCost: func(c uint64) uint64 { return c + 5 },
|
|
},
|
|
{
|
|
ops: []string{".containsIP('2001:db8::1')"},
|
|
// For operations like checking if an IP is in a CIDR, the cost is expected to higher.
|
|
expectEsimatedCost: func(c checker.CostEstimate) checker.CostEstimate {
|
|
return checker.CostEstimate{Min: c.Min + 3, Max: c.Max + 6}
|
|
},
|
|
expectRuntimeCost: func(c uint64) uint64 { return c + 3 },
|
|
},
|
|
}...)
|
|
|
|
for _, tc := range ipv4Cases {
|
|
for _, op := range tc.ops {
|
|
t.Run(ipv4+op, func(t *testing.T) {
|
|
testCost(t, ipv4+op, tc.expectEsimatedCost(ipv4BaseEstimatedCost), tc.expectRuntimeCost(ipv4BaseRuntimeCost))
|
|
})
|
|
}
|
|
}
|
|
|
|
for _, tc := range ipv6Cases {
|
|
for _, op := range tc.ops {
|
|
t.Run(ipv6+op, func(t *testing.T) {
|
|
testCost(t, ipv6+op, tc.expectEsimatedCost(ipv6BaseEstimatedCost), tc.expectRuntimeCost(ipv6BaseRuntimeCost))
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestStringLibrary(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
expr string
|
|
expectEsimatedCost checker.CostEstimate
|
|
expectRuntimeCost uint64
|
|
}{
|
|
{
|
|
name: "lowerAscii",
|
|
expr: "'ABCDEFGHIJ abcdefghij'.lowerAscii()",
|
|
expectEsimatedCost: checker.CostEstimate{Min: 3, Max: 3},
|
|
expectRuntimeCost: 3,
|
|
},
|
|
{
|
|
name: "lowerAsciiEquals",
|
|
expr: "'ABCDEFGHIJ abcdefghij'.lowerAscii() == 'abcdefghij ABCDEFGHIJ'.lowerAscii()",
|
|
expectEsimatedCost: checker.CostEstimate{Min: 7, Max: 9},
|
|
expectRuntimeCost: 9,
|
|
},
|
|
{
|
|
name: "upperAscii",
|
|
expr: "'ABCDEFGHIJ abcdefghij'.upperAscii()",
|
|
expectEsimatedCost: checker.CostEstimate{Min: 3, Max: 3},
|
|
expectRuntimeCost: 3,
|
|
},
|
|
{
|
|
name: "upperAsciiEquals",
|
|
expr: "'ABCDEFGHIJ abcdefghij'.upperAscii() == 'abcdefghij ABCDEFGHIJ'.upperAscii()",
|
|
expectEsimatedCost: checker.CostEstimate{Min: 7, Max: 9},
|
|
expectRuntimeCost: 9,
|
|
},
|
|
{
|
|
name: "quote",
|
|
expr: "strings.quote('ABCDEFGHIJ abcdefghij')",
|
|
expectEsimatedCost: checker.CostEstimate{Min: 3, Max: 3},
|
|
expectRuntimeCost: 3,
|
|
},
|
|
{
|
|
name: "quoteEquals",
|
|
expr: "strings.quote('ABCDEFGHIJ abcdefghij') == strings.quote('ABCDEFGHIJ abcdefghij')",
|
|
expectEsimatedCost: checker.CostEstimate{Min: 7, Max: 11},
|
|
expectRuntimeCost: 9,
|
|
},
|
|
{
|
|
name: "replace",
|
|
expr: "'abc 123 def 123'.replace('123', '456')",
|
|
expectEsimatedCost: checker.CostEstimate{Min: 3, Max: 3},
|
|
expectRuntimeCost: 3,
|
|
},
|
|
{
|
|
name: "replace between all chars",
|
|
expr: "'abc 123 def 123'.replace('', 'x')",
|
|
expectEsimatedCost: checker.CostEstimate{Min: 3, Max: 3},
|
|
expectRuntimeCost: 3,
|
|
},
|
|
{
|
|
name: "replace with empty",
|
|
expr: "'abc 123 def 123'.replace('123', '')",
|
|
expectEsimatedCost: checker.CostEstimate{Min: 3, Max: 3},
|
|
expectRuntimeCost: 3,
|
|
},
|
|
{
|
|
name: "replace with limit",
|
|
expr: "'abc 123 def 123'.replace('123', '456', 1)",
|
|
expectEsimatedCost: checker.CostEstimate{Min: 3, Max: 3},
|
|
expectRuntimeCost: 3,
|
|
},
|
|
{
|
|
name: "split",
|
|
expr: "'abc 123 def 123'.split(' ')",
|
|
expectEsimatedCost: checker.CostEstimate{Min: 3, Max: 3},
|
|
expectRuntimeCost: 3,
|
|
},
|
|
{
|
|
name: "split with limit",
|
|
expr: "'abc 123 def 123'.split(' ', 1)",
|
|
expectEsimatedCost: checker.CostEstimate{Min: 3, Max: 3},
|
|
expectRuntimeCost: 3,
|
|
},
|
|
{
|
|
name: "substring",
|
|
expr: "'abc 123 def 123'.substring(5)",
|
|
expectEsimatedCost: checker.CostEstimate{Min: 2, Max: 2},
|
|
expectRuntimeCost: 2,
|
|
},
|
|
{
|
|
name: "substring with end",
|
|
expr: "'abc 123 def 123'.substring(5, 8)",
|
|
expectEsimatedCost: checker.CostEstimate{Min: 2, Max: 2},
|
|
expectRuntimeCost: 2,
|
|
},
|
|
{
|
|
name: "trim",
|
|
expr: "' abc 123 def 123 '.trim()",
|
|
expectEsimatedCost: checker.CostEstimate{Min: 2, Max: 2},
|
|
expectRuntimeCost: 2,
|
|
},
|
|
{
|
|
name: "join with separator",
|
|
expr: "['aa', 'bb', 'cc', 'd', 'e', 'f', 'g', 'h', 'i', 'j'].join(' ')",
|
|
expectEsimatedCost: checker.CostEstimate{Min: 11, Max: 23},
|
|
expectRuntimeCost: 15,
|
|
},
|
|
{
|
|
name: "join",
|
|
expr: "['aa', 'bb', 'cc', 'd', 'e', 'f', 'g', 'h', 'i', 'j'].join()",
|
|
expectEsimatedCost: checker.CostEstimate{Min: 10, Max: 22},
|
|
expectRuntimeCost: 13,
|
|
},
|
|
{
|
|
name: "find",
|
|
expr: "'abc 123 def 123'.find('123')",
|
|
expectEsimatedCost: checker.CostEstimate{Min: 2, Max: 2},
|
|
expectRuntimeCost: 2,
|
|
},
|
|
{
|
|
name: "findAll",
|
|
expr: "'abc 123 def 123'.findAll('123')",
|
|
expectEsimatedCost: checker.CostEstimate{Min: 2, Max: 2},
|
|
expectRuntimeCost: 2,
|
|
},
|
|
{
|
|
name: "findAll with limit",
|
|
expr: "'abc 123 def 123'.findAll('123', 1)",
|
|
expectEsimatedCost: checker.CostEstimate{Min: 2, Max: 2},
|
|
expectRuntimeCost: 2,
|
|
},
|
|
{
|
|
name: "jsonpatch.escapeKey",
|
|
expr: "jsonpatch.escapeKey('abc/def~ abc/def~')",
|
|
expectEsimatedCost: checker.CostEstimate{Min: 2, Max: 2},
|
|
expectRuntimeCost: 2,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
testCost(t, tc.expr, tc.expectEsimatedCost, tc.expectRuntimeCost)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAuthzLibrary(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
expr string
|
|
expectEstimatedCost checker.CostEstimate
|
|
expectRuntimeCost uint64
|
|
}{
|
|
{
|
|
name: "path",
|
|
expr: "authorizer.path('/healthz')",
|
|
expectEstimatedCost: checker.CostEstimate{Min: 2, Max: 2},
|
|
expectRuntimeCost: 2,
|
|
},
|
|
{
|
|
name: "resource",
|
|
expr: "authorizer.group('apps').resource('deployments').subresource('status').namespace('test').name('backend')",
|
|
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()",
|
|
expectEstimatedCost: checker.CostEstimate{Min: 350003, Max: 350003},
|
|
expectRuntimeCost: 350003,
|
|
},
|
|
{
|
|
name: "resource check allowed",
|
|
expr: "authorizer.group('apps').resource('deployments').subresource('status').namespace('test').name('backend').check('create').allowed()",
|
|
expectEstimatedCost: checker.CostEstimate{Min: 350007, Max: 350007},
|
|
expectRuntimeCost: 350007,
|
|
},
|
|
{
|
|
name: "resource check reason",
|
|
expr: "authorizer.group('apps').resource('deployments').subresource('status').namespace('test').name('backend').check('create').allowed()",
|
|
expectEstimatedCost: checker.CostEstimate{Min: 350007, Max: 350007},
|
|
expectRuntimeCost: 350007,
|
|
},
|
|
{
|
|
name: "resource check errored",
|
|
expr: "authorizer.group('apps').resource('deployments').subresource('status').namespace('test').name('backend').check('create').errored()",
|
|
expectEstimatedCost: checker.CostEstimate{Min: 350007, Max: 350007},
|
|
expectRuntimeCost: 350007,
|
|
},
|
|
{
|
|
name: "resource check error",
|
|
expr: "authorizer.group('apps').resource('deployments').subresource('status').namespace('test').name('backend').check('create').error()",
|
|
expectEstimatedCost: checker.CostEstimate{Min: 350007, Max: 350007},
|
|
expectRuntimeCost: 350007,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
testCost(t, tc.expr, tc.expectEstimatedCost, tc.expectRuntimeCost)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestQuantityCost(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
expr string
|
|
expectEstimatedCost checker.CostEstimate
|
|
expectRuntimeCost uint64
|
|
}{
|
|
{
|
|
name: "path",
|
|
expr: `quantity("12Mi")`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 1, Max: 1},
|
|
expectRuntimeCost: 1,
|
|
},
|
|
{
|
|
name: "isQuantity",
|
|
expr: `isQuantity("20")`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 1, Max: 1},
|
|
expectRuntimeCost: 1,
|
|
},
|
|
{
|
|
name: "isQuantity_megabytes",
|
|
expr: `isQuantity("20M")`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 1, Max: 1},
|
|
expectRuntimeCost: 1,
|
|
},
|
|
{
|
|
name: "equality_reflexivity",
|
|
expr: `quantity("200M") == quantity("200M")`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 3, Max: 3},
|
|
expectRuntimeCost: 3,
|
|
},
|
|
{
|
|
name: "equality_symmetry",
|
|
expr: `quantity("200M") == quantity("0.2G") && quantity("0.2G") == quantity("200M")`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 3, Max: 6},
|
|
expectRuntimeCost: 6,
|
|
},
|
|
{
|
|
name: "equality_transitivity",
|
|
expr: `quantity("2M") == quantity("0.002G") && quantity("2000k") == quantity("2M") && quantity("0.002G") == quantity("2000k")`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 3, Max: 9},
|
|
expectRuntimeCost: 9,
|
|
},
|
|
{
|
|
name: "quantity_less",
|
|
expr: `quantity("50M").isLessThan(quantity("50Mi"))`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 3, Max: 3},
|
|
expectRuntimeCost: 3,
|
|
},
|
|
{
|
|
name: "quantity_greater",
|
|
expr: `quantity("50Mi").isGreaterThan(quantity("50M"))`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 3, Max: 3},
|
|
expectRuntimeCost: 3,
|
|
},
|
|
{
|
|
name: "compare_equal",
|
|
expr: `quantity("200M").compareTo(quantity("0.2G")) > 0`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 4, Max: 4},
|
|
expectRuntimeCost: 4,
|
|
},
|
|
{
|
|
name: "add_quantity",
|
|
expr: `quantity("50k").add(quantity("20")) == quantity("50.02k")`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 5, Max: 5},
|
|
expectRuntimeCost: 5,
|
|
},
|
|
{
|
|
name: "sub_quantity",
|
|
expr: `quantity("50k").sub(quantity("20")) == quantity("49.98k")`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 5, Max: 5},
|
|
expectRuntimeCost: 5,
|
|
},
|
|
{
|
|
name: "sub_int",
|
|
expr: `quantity("50k").sub(20) == quantity("49980")`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 4, Max: 4},
|
|
expectRuntimeCost: 4,
|
|
},
|
|
{
|
|
name: "arith_chain_1",
|
|
expr: `quantity("50k").add(20).sub(quantity("100k")).asInteger() > 0`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 6, Max: 6},
|
|
expectRuntimeCost: 6,
|
|
},
|
|
{
|
|
name: "arith_chain",
|
|
expr: `quantity("50k").add(20).sub(quantity("100k")).sub(-50000).asInteger() > 0`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 7, Max: 7},
|
|
expectRuntimeCost: 7,
|
|
},
|
|
{
|
|
name: "as_integer",
|
|
expr: `quantity("50k").asInteger() > 0`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 3, Max: 3},
|
|
expectRuntimeCost: 3,
|
|
},
|
|
{
|
|
name: "is_integer",
|
|
expr: `quantity("50").isInteger()`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 2, Max: 2},
|
|
expectRuntimeCost: 2,
|
|
},
|
|
{
|
|
name: "as_float",
|
|
expr: `quantity("50.703k").asApproximateFloat() > 0.0`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 3, Max: 3},
|
|
expectRuntimeCost: 3,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
testCost(t, tc.expr, tc.expectEstimatedCost, tc.expectRuntimeCost)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNameFormatCost(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
expr string
|
|
expectEstimatedCost checker.CostEstimate
|
|
expectRuntimeCost uint64
|
|
}{
|
|
{
|
|
name: "format.named",
|
|
expr: `format.named("dns1123subdomain")`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 1, Max: 1},
|
|
expectRuntimeCost: 1,
|
|
},
|
|
{
|
|
name: "format.dns1123Subdomain.validate",
|
|
expr: `format.named("dns1123Subdomain").value().validate("my-name")`,
|
|
// Estimated cost doesnt know value at runtime so it is
|
|
// using an estimated maximum regex length
|
|
expectEstimatedCost: checker.CostEstimate{Min: 34, Max: 34},
|
|
expectRuntimeCost: 17,
|
|
},
|
|
{
|
|
name: "format.dns1123label.validate",
|
|
expr: `format.named("dns1123Label").value().validate("my-name")`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 34, Max: 34},
|
|
expectRuntimeCost: 10,
|
|
},
|
|
{
|
|
name: "format.dns1123label.validate",
|
|
expr: `format.named("dns1123Label").value().validate("my-name")`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 34, Max: 34},
|
|
expectRuntimeCost: 10,
|
|
},
|
|
{
|
|
name: "format.dns1123label.validate",
|
|
expr: `format.named("dns1123Label").value() == format.named("dns1123Label").value()`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 5, Max: 11},
|
|
expectRuntimeCost: 5,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
testCost(t, tc.expr, tc.expectEstimatedCost, tc.expectRuntimeCost)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSetsCost(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
expr string
|
|
expectEstimatedCost checker.CostEstimate
|
|
expectRuntimeCost uint64
|
|
}{
|
|
{
|
|
name: "sets",
|
|
expr: `sets.contains([], [])`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 21, Max: 21},
|
|
expectRuntimeCost: 21,
|
|
},
|
|
{
|
|
expr: `sets.contains([1], [])`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 21, Max: 21},
|
|
expectRuntimeCost: 21,
|
|
},
|
|
{
|
|
expr: `sets.contains([1], [1])`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 22, Max: 22},
|
|
expectRuntimeCost: 22,
|
|
},
|
|
{
|
|
expr: `sets.contains([1], [1, 1])`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 23, Max: 23},
|
|
expectRuntimeCost: 23,
|
|
},
|
|
{
|
|
expr: `sets.contains([1, 1], [1])`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 23, Max: 23},
|
|
expectRuntimeCost: 23,
|
|
},
|
|
{
|
|
expr: `sets.contains([2, 1], [1])`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 23, Max: 23},
|
|
expectRuntimeCost: 23,
|
|
},
|
|
{
|
|
expr: `sets.contains([1, 2, 3, 4], [2, 3])`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 29, Max: 29},
|
|
expectRuntimeCost: 29,
|
|
},
|
|
{
|
|
expr: `sets.contains([1], [1.0, 1])`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 23, Max: 23},
|
|
expectRuntimeCost: 23,
|
|
},
|
|
{
|
|
expr: `sets.contains([1, 2], [2u, 2.0])`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 25, Max: 25},
|
|
expectRuntimeCost: 25,
|
|
},
|
|
{
|
|
expr: `sets.contains([1, 2u], [2, 2.0])`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 25, Max: 25},
|
|
expectRuntimeCost: 25,
|
|
},
|
|
{
|
|
expr: `sets.contains([1, 2.0, 3u], [1.0, 2u, 3])`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 30, Max: 30},
|
|
expectRuntimeCost: 30,
|
|
},
|
|
{
|
|
expr: `sets.contains([[1], [2, 3]], [[2, 3.0]])`,
|
|
// 10 for each list creation, top-level list sizes are 2, 1
|
|
expectEstimatedCost: checker.CostEstimate{Min: 53, Max: 53},
|
|
expectRuntimeCost: 53,
|
|
},
|
|
{
|
|
expr: `!sets.contains([1], [2])`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 23, Max: 23},
|
|
expectRuntimeCost: 23,
|
|
},
|
|
{
|
|
expr: `!sets.contains([1], [1, 2])`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 24, Max: 24},
|
|
expectRuntimeCost: 24,
|
|
},
|
|
{
|
|
expr: `!sets.contains([1], ["1", 1])`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 24, Max: 24},
|
|
expectRuntimeCost: 24,
|
|
},
|
|
{
|
|
expr: `!sets.contains([1], [1.1, 1u])`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 24, Max: 24},
|
|
expectRuntimeCost: 24,
|
|
},
|
|
|
|
// set equivalence (note the cost factor is higher as it's basically two contains checks)
|
|
{
|
|
expr: `sets.equivalent([], [])`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 21, Max: 21},
|
|
expectRuntimeCost: 21,
|
|
},
|
|
{
|
|
expr: `sets.equivalent([1], [1])`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 23, Max: 23},
|
|
expectRuntimeCost: 23,
|
|
},
|
|
{
|
|
expr: `sets.equivalent([1], [1, 1])`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 25, Max: 25},
|
|
expectRuntimeCost: 25,
|
|
},
|
|
{
|
|
expr: `sets.equivalent([1, 1], [1])`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 25, Max: 25},
|
|
expectRuntimeCost: 25,
|
|
},
|
|
{
|
|
expr: `sets.equivalent([1], [1u, 1.0])`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 25, Max: 25},
|
|
expectRuntimeCost: 25,
|
|
},
|
|
{
|
|
expr: `sets.equivalent([1], [1u, 1.0])`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 25, Max: 25},
|
|
expectRuntimeCost: 25,
|
|
},
|
|
{
|
|
expr: `sets.equivalent([1, 2, 3], [3u, 2.0, 1])`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 39, Max: 39},
|
|
expectRuntimeCost: 39,
|
|
},
|
|
{
|
|
expr: `sets.equivalent([[1.0], [2, 3]], [[1], [2, 3.0]])`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 69, Max: 69},
|
|
expectRuntimeCost: 69,
|
|
},
|
|
{
|
|
expr: `!sets.equivalent([2, 1], [1])`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 26, Max: 26},
|
|
expectRuntimeCost: 26,
|
|
},
|
|
{
|
|
expr: `!sets.equivalent([1], [1, 2])`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 26, Max: 26},
|
|
expectRuntimeCost: 26,
|
|
},
|
|
{
|
|
expr: `!sets.equivalent([1, 2], [2u, 2, 2.0])`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 34, Max: 34},
|
|
expectRuntimeCost: 34,
|
|
},
|
|
{
|
|
expr: `!sets.equivalent([1, 2], [1u, 2, 2.3])`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 34, Max: 34},
|
|
expectRuntimeCost: 34,
|
|
},
|
|
{
|
|
expr: `sets.intersects([1], [1])`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 22, Max: 22},
|
|
expectRuntimeCost: 22,
|
|
},
|
|
{
|
|
expr: `sets.intersects([1], [1, 1])`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 23, Max: 23},
|
|
expectRuntimeCost: 23,
|
|
},
|
|
{
|
|
expr: `sets.intersects([1, 1], [1])`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 23, Max: 23},
|
|
expectRuntimeCost: 23,
|
|
},
|
|
{
|
|
expr: `sets.intersects([2, 1], [1])`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 23, Max: 23},
|
|
expectRuntimeCost: 23,
|
|
},
|
|
{
|
|
expr: `sets.intersects([1], [1, 2])`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 23, Max: 23},
|
|
expectRuntimeCost: 23,
|
|
},
|
|
{
|
|
expr: `sets.intersects([1], [1.0, 2])`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 23, Max: 23},
|
|
expectRuntimeCost: 23,
|
|
},
|
|
{
|
|
expr: `sets.intersects([1, 2], [2u, 2, 2.0])`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 27, Max: 27},
|
|
expectRuntimeCost: 27,
|
|
},
|
|
{
|
|
expr: `sets.intersects([1, 2], [1u, 2, 2.3])`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 27, Max: 27},
|
|
expectRuntimeCost: 27,
|
|
},
|
|
{
|
|
expr: `sets.intersects([[1], [2, 3]], [[1, 2], [2, 3.0]])`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 65, Max: 65},
|
|
expectRuntimeCost: 65,
|
|
},
|
|
{
|
|
expr: `!sets.intersects([], [])`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 22, Max: 22},
|
|
expectRuntimeCost: 22,
|
|
},
|
|
{
|
|
expr: `!sets.intersects([1], [])`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 22, Max: 22},
|
|
expectRuntimeCost: 22,
|
|
},
|
|
{
|
|
expr: `!sets.intersects([1], [2])`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 23, Max: 23},
|
|
expectRuntimeCost: 23,
|
|
},
|
|
{
|
|
expr: `!sets.intersects([1], ["1", 2])`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 24, Max: 24},
|
|
expectRuntimeCost: 24,
|
|
},
|
|
{
|
|
expr: `!sets.intersects([1], [1.1, 2u])`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 24, Max: 24},
|
|
expectRuntimeCost: 24,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
testCost(t, tc.expr, tc.expectEstimatedCost, tc.expectRuntimeCost)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSemverCost(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
expr string
|
|
expectEstimatedCost checker.CostEstimate
|
|
expectRuntimeCost uint64
|
|
}{
|
|
{
|
|
name: "semver",
|
|
expr: `semver("1.0.0")`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 1, Max: 1},
|
|
expectRuntimeCost: 1,
|
|
},
|
|
{
|
|
name: "semver long input",
|
|
expr: `semver("1234.56789012345.67890123456789")`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 4, Max: 4},
|
|
expectRuntimeCost: 4,
|
|
},
|
|
{
|
|
name: "isSemver",
|
|
expr: `isSemver("1.0.0")`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 1, Max: 1},
|
|
expectRuntimeCost: 1,
|
|
},
|
|
{
|
|
name: "isSemver long input",
|
|
expr: `isSemver("1234.56789012345.67890123456789")`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 4, Max: 4},
|
|
expectRuntimeCost: 4,
|
|
},
|
|
// major(), minor(), patch()
|
|
{
|
|
name: "major",
|
|
expr: `semver("1.2.3").major()`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 2, Max: 2},
|
|
expectRuntimeCost: 2,
|
|
},
|
|
{
|
|
name: "minor",
|
|
expr: `semver("1.2.3").minor()`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 2, Max: 2},
|
|
expectRuntimeCost: 2,
|
|
},
|
|
{
|
|
name: "patch",
|
|
expr: `semver("1.2.3").patch()`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 2, Max: 2},
|
|
expectRuntimeCost: 2,
|
|
},
|
|
// isLessThan
|
|
{
|
|
name: "isLessThan",
|
|
expr: `semver("1.0.0").isLessThan(semver("1.1.0"))`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 3, Max: 3},
|
|
expectRuntimeCost: 3,
|
|
},
|
|
// isGreaterThan
|
|
{
|
|
name: "isGreaterThan",
|
|
expr: `semver("1.1.0").isGreaterThan(semver("1.0.0"))`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 3, Max: 3},
|
|
expectRuntimeCost: 3,
|
|
},
|
|
// compareTo
|
|
{
|
|
name: "compareTo",
|
|
expr: `semver("1.0.0").compareTo(semver("1.2.3"))`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 3, Max: 3},
|
|
expectRuntimeCost: 3,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
testCost(t, tc.expr, tc.expectEstimatedCost, tc.expectRuntimeCost)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTwoVariableComprehensionCost(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
expr string
|
|
expectEstimatedCost checker.CostEstimate
|
|
expectRuntimeCost uint64
|
|
}{
|
|
{
|
|
name: "map all",
|
|
expr: `{'a': 1, 'b': 2}.all(k, v, v > 0)`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 37, Max: 41},
|
|
expectRuntimeCost: 41,
|
|
},
|
|
{
|
|
name: "map exists",
|
|
expr: `{'a': 1, 'b': 2}.exists(k, v, v > 0)`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 39, Max: 43},
|
|
expectRuntimeCost: 40,
|
|
},
|
|
{
|
|
name: "map existsOne",
|
|
expr: `{'a': 1, 'b': 2}.existsOne(k, v, v > 0)`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 38, Max: 40},
|
|
expectRuntimeCost: 40,
|
|
},
|
|
{
|
|
name: "map transformMap",
|
|
expr: `{'a': 1, 'b': 2}.transformMap(k, v, v + 1)`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 71, Max: 71},
|
|
expectRuntimeCost: 71,
|
|
},
|
|
{
|
|
name: "map transformMap with filter",
|
|
expr: `{'a': 1, 'b': 2}.transformMap(k, v, v < 5, v + 1)`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 67, Max: 75},
|
|
expectRuntimeCost: 75,
|
|
},
|
|
{
|
|
name: "map transformMapEntry",
|
|
expr: `{'a': 1, 'b': 2}.transformMapEntry(k, v, {k: v + 1})`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 131, Max: 131},
|
|
expectRuntimeCost: 131,
|
|
},
|
|
{
|
|
name: "map transformMapEntry with filter",
|
|
expr: `{'a': 1, 'b': 2}.transformMapEntry(k, v, v < 5, {k: v + 1})`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 67, Max: 135},
|
|
expectRuntimeCost: 135,
|
|
},
|
|
|
|
{
|
|
name: "list all",
|
|
expr: `[1, 2].all(i, v, v > 0)`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 17, Max: 21},
|
|
expectRuntimeCost: 21,
|
|
},
|
|
{
|
|
name: "list exists",
|
|
expr: `[1, 2].exists(i, v, v > 0)`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 19, Max: 23},
|
|
expectRuntimeCost: 20,
|
|
},
|
|
{
|
|
name: "list existsOne",
|
|
expr: `[1, 2].existsOne(i, v, v > 0)`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 18, Max: 20},
|
|
expectRuntimeCost: 20,
|
|
},
|
|
{
|
|
name: "list transformList",
|
|
expr: `[1, 2].transformList(i, v, v + 1)`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 49, Max: 49},
|
|
expectRuntimeCost: 49,
|
|
},
|
|
{
|
|
name: "list transformList with filter",
|
|
expr: `[1, 2].transformList(i, v, v < 5, v + 1)`,
|
|
expectEstimatedCost: checker.CostEstimate{Min: 27, Max: 53},
|
|
expectRuntimeCost: 53,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
testCost(t, tc.expr, tc.expectEstimatedCost, tc.expectRuntimeCost)
|
|
})
|
|
}
|
|
}
|
|
|
|
func testCost(t *testing.T, expr string, expectEsimatedCost checker.CostEstimate, expectRuntimeCost uint64) {
|
|
originalPanicOnUnknown := panicOnUnknown
|
|
panicOnUnknown = true
|
|
t.Cleanup(func() { panicOnUnknown = originalPanicOnUnknown })
|
|
|
|
est := &CostEstimator{SizeEstimator: &testCostEstimator{}}
|
|
env, err := cel.NewEnv(
|
|
ext.Strings(ext.StringsVersion(2)),
|
|
URLs(),
|
|
Regex(),
|
|
Lists(),
|
|
Authz(),
|
|
AuthzSelectors(),
|
|
Quantity(),
|
|
ext.Sets(),
|
|
IP(),
|
|
CIDR(),
|
|
Format(),
|
|
JSONPatch(),
|
|
cel.OptionalTypes(),
|
|
// cel-go v0.17.7 introduced CostEstimatorOptions.
|
|
// Previous the presence has a cost of 0 but cel fixed it to 1. We still set to 0 here to avoid breaking changes.
|
|
cel.CostEstimatorOptions(checker.PresenceTestHasCost(false)),
|
|
ext.TwoVarComprehensions(),
|
|
SemverLib(SemverVersion(1)),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("%v", err)
|
|
}
|
|
env, err = env.Extend(cel.Variable("authorizer", AuthorizerType))
|
|
if err != nil {
|
|
t.Fatalf("%v", err)
|
|
}
|
|
compiled, issues := env.Compile(expr)
|
|
if len(issues.Errors()) > 0 {
|
|
var errList []string
|
|
for _, issue := range issues.Errors() {
|
|
errList = append(errList, issue.ToDisplayString(common.NewTextSource(expr)))
|
|
}
|
|
t.Fatalf("%v", errList)
|
|
}
|
|
estCost, err := env.EstimateCost(compiled, est)
|
|
if err != nil {
|
|
t.Fatalf("%v", err)
|
|
}
|
|
if estCost.Min != expectEsimatedCost.Min || estCost.Max != expectEsimatedCost.Max {
|
|
t.Errorf("Expected estimated cost of %d..%d but got %d..%d", expectEsimatedCost.Min, expectEsimatedCost.Max, estCost.Min, estCost.Max)
|
|
}
|
|
prog, err := env.Program(compiled, cel.CostTracking(est))
|
|
if err != nil {
|
|
t.Fatalf("%v", err)
|
|
}
|
|
_, details, err := prog.Eval(map[string]interface{}{"authorizer": NewAuthorizerVal(nil, alwaysAllowAuthorizer{})})
|
|
if err != nil {
|
|
t.Fatalf("%v", err)
|
|
}
|
|
cost := details.ActualCost()
|
|
if *cost != expectRuntimeCost {
|
|
t.Errorf("Expected cost of %d but got %d", expectRuntimeCost, *cost)
|
|
}
|
|
}
|
|
|
|
func TestSize(t *testing.T) {
|
|
exactSize := func(size int) checker.SizeEstimate {
|
|
return checker.SizeEstimate{Min: uint64(size), Max: uint64(size)}
|
|
}
|
|
exactSizes := func(sizes ...int) []checker.SizeEstimate {
|
|
results := make([]checker.SizeEstimate, len(sizes))
|
|
for i, size := range sizes {
|
|
results[i] = exactSize(size)
|
|
}
|
|
return results
|
|
}
|
|
cases := []struct {
|
|
name string
|
|
function string
|
|
overload string
|
|
targetSize checker.SizeEstimate
|
|
argSizes []checker.SizeEstimate
|
|
expectSize checker.SizeEstimate
|
|
}{
|
|
{
|
|
name: "replace empty with char",
|
|
function: "replace",
|
|
targetSize: exactSize(3), // e.g. abc
|
|
argSizes: exactSizes(0, 1), // e.g. replace "" with "_"
|
|
expectSize: exactSize(7), // e.g. _a_b_c_
|
|
},
|
|
{
|
|
name: "maybe replace char with empty",
|
|
function: "replace",
|
|
targetSize: exactSize(3),
|
|
argSizes: exactSizes(1, 0),
|
|
expectSize: checker.SizeEstimate{Min: 0, Max: 3},
|
|
},
|
|
{
|
|
name: "maybe replace repeated",
|
|
function: "replace",
|
|
targetSize: exactSize(4),
|
|
argSizes: exactSizes(2, 4),
|
|
expectSize: checker.SizeEstimate{Min: 4, Max: 8},
|
|
},
|
|
{
|
|
name: "maybe replace empty",
|
|
function: "replace",
|
|
targetSize: exactSize(4),
|
|
argSizes: []checker.SizeEstimate{{Min: 0, Max: 1}, {Min: 0, Max: 2}},
|
|
expectSize: checker.SizeEstimate{Min: 0, Max: 14}, // len(__a__a__a__a__) == 14
|
|
},
|
|
{
|
|
name: "replace non-empty size range, maybe larger",
|
|
function: "replace",
|
|
targetSize: exactSize(4),
|
|
argSizes: []checker.SizeEstimate{{Min: 1, Max: 1}, {Min: 1, Max: 2}},
|
|
expectSize: checker.SizeEstimate{Min: 4, Max: 8},
|
|
},
|
|
{
|
|
name: "replace non-empty size range, maybe smaller",
|
|
function: "replace",
|
|
targetSize: exactSize(4),
|
|
argSizes: []checker.SizeEstimate{{Min: 1, Max: 2}, {Min: 1, Max: 1}},
|
|
expectSize: checker.SizeEstimate{Min: 2, Max: 4},
|
|
},
|
|
}
|
|
|
|
originalPanicOnUnknown := panicOnUnknown
|
|
panicOnUnknown = true
|
|
t.Cleanup(func() { panicOnUnknown = originalPanicOnUnknown })
|
|
|
|
est := &CostEstimator{SizeEstimator: &testCostEstimator{}}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
var targetNode checker.AstNode = testNode{size: tc.targetSize}
|
|
argNodes := make([]checker.AstNode, len(tc.argSizes))
|
|
for i, arg := range tc.argSizes {
|
|
argNodes[i] = testNode{size: arg}
|
|
}
|
|
result := est.EstimateCallCost(tc.function, tc.overload, &targetNode, argNodes)
|
|
if result.ResultSize == nil {
|
|
t.Fatalf("Expected ResultSize but got none")
|
|
}
|
|
if *result.ResultSize != tc.expectSize {
|
|
t.Fatalf("Expected %+v but got %+v", tc.expectSize, *result.ResultSize)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestTypeEquality ensures that cost is tested for all custom types used by Kubernetes libraries.
|
|
func TestTypeEquality(t *testing.T) {
|
|
examples := map[string]ref.Val{
|
|
// Add example ref.Val's for custom types in Kubernetes here:
|
|
"kubernetes.authorization.Authorizer": authorizerVal{},
|
|
"kubernetes.authorization.PathCheck": pathCheckVal{},
|
|
"kubernetes.authorization.GroupCheck": groupCheckVal{},
|
|
"kubernetes.authorization.ResourceCheck": resourceCheckVal{},
|
|
"kubernetes.authorization.Decision": decisionVal{},
|
|
"kubernetes.URL": apiservercel.URL{},
|
|
"kubernetes.Quantity": apiservercel.Quantity{},
|
|
"net.IP": apiservercel.IP{},
|
|
"net.CIDR": apiservercel.CIDR{},
|
|
"kubernetes.NamedFormat": apiservercel.Format{},
|
|
"kubernetes.Semver": apiservercel.Semver{},
|
|
}
|
|
|
|
originalPanicOnUnknown := panicOnUnknown
|
|
panicOnUnknown = true
|
|
t.Cleanup(func() { panicOnUnknown = originalPanicOnUnknown })
|
|
est := &CostEstimator{SizeEstimator: &testCostEstimator{}}
|
|
|
|
for _, lib := range KnownLibraries() {
|
|
for _, kt := range lib.Types() {
|
|
t.Run(kt.TypeName(), func(t *testing.T) {
|
|
typeNode := testNode{size: checker.SizeEstimate{Min: 10, Max: 100}, typ: kt}
|
|
est.EstimateCallCost("_==_", "", nil, []checker.AstNode{typeNode, typeNode})
|
|
ex, ok := examples[kt.TypeName()]
|
|
if !ok {
|
|
t.Errorf("missing example for type: %s", kt.TypeName())
|
|
}
|
|
est.CallCost("_==_", "", []ref.Val{ex, ex}, nil)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
type testNode struct {
|
|
size checker.SizeEstimate
|
|
typ *types.Type
|
|
}
|
|
|
|
var _ checker.AstNode = (*testNode)(nil)
|
|
|
|
func (t testNode) Path() []string {
|
|
return nil // not needed
|
|
}
|
|
|
|
func (t testNode) Type() *types.Type {
|
|
return t.typ // not needed
|
|
}
|
|
|
|
func (t testNode) Expr() ast.Expr {
|
|
return nil // not needed
|
|
}
|
|
|
|
func (t testNode) ComputedSize() *checker.SizeEstimate {
|
|
return &t.size
|
|
}
|
|
|
|
type testCostEstimator struct {
|
|
}
|
|
|
|
func (t *testCostEstimator) EstimateSize(element checker.AstNode) *checker.SizeEstimate {
|
|
expr, err := cel.TypeToExprType(element.Type())
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
switch expr.GetPrimitive() {
|
|
case exprpb.Type_STRING:
|
|
return &checker.SizeEstimate{Min: 0, Max: 12}
|
|
case exprpb.Type_BYTES:
|
|
return &checker.SizeEstimate{Min: 0, Max: 12}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (t *testCostEstimator) EstimateCallCost(function, overloadId string, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate {
|
|
return nil
|
|
}
|
|
|
|
type alwaysAllowAuthorizer struct{}
|
|
|
|
func (f alwaysAllowAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
|
|
return authorizer.DecisionAllow, "", nil
|
|
}
|