k8s.io/apiserver@v0.31.1/pkg/cel/library/ip_test.go (about)

     1  /*
     2  Copyright 2023 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package library_test
    18  
    19  import (
    20  	"net/netip"
    21  	"regexp"
    22  	"testing"
    23  
    24  	"github.com/google/cel-go/cel"
    25  	"github.com/google/cel-go/common/types"
    26  	"github.com/google/cel-go/common/types/ref"
    27  	"github.com/stretchr/testify/require"
    28  	"k8s.io/apimachinery/pkg/util/sets"
    29  	apiservercel "k8s.io/apiserver/pkg/cel"
    30  	"k8s.io/apiserver/pkg/cel/library"
    31  )
    32  
    33  func testIP(t *testing.T, expr string, expectResult ref.Val, expectRuntimeErr string, expectCompileErrs []string) {
    34  	env, err := cel.NewEnv(
    35  		library.IP(),
    36  		library.CIDR(),
    37  	)
    38  	if err != nil {
    39  		t.Fatalf("%v", err)
    40  	}
    41  
    42  	compiled, issues := env.Compile(expr)
    43  
    44  	if len(expectCompileErrs) > 0 {
    45  		missingCompileErrs := []string{}
    46  		matchedCompileErrs := sets.New[int]()
    47  		for _, expectedCompileErr := range expectCompileErrs {
    48  			compiledPattern, err := regexp.Compile(expectedCompileErr)
    49  			if err != nil {
    50  				t.Fatalf("failed to compile expected err regex: %v", err)
    51  			}
    52  
    53  			didMatch := false
    54  
    55  			for i, compileError := range issues.Errors() {
    56  				if compiledPattern.Match([]byte(compileError.Message)) {
    57  					didMatch = true
    58  					matchedCompileErrs.Insert(i)
    59  				}
    60  			}
    61  
    62  			if !didMatch {
    63  				missingCompileErrs = append(missingCompileErrs, expectedCompileErr)
    64  			} else if len(matchedCompileErrs) != len(issues.Errors()) {
    65  				unmatchedErrs := []cel.Error{}
    66  				for i, issue := range issues.Errors() {
    67  					if !matchedCompileErrs.Has(i) {
    68  						unmatchedErrs = append(unmatchedErrs, *issue)
    69  					}
    70  				}
    71  				require.Empty(t, unmatchedErrs, "unexpected compilation errors")
    72  			}
    73  		}
    74  
    75  		require.Empty(t, missingCompileErrs, "expected compilation errors")
    76  		return
    77  	} else if len(issues.Errors()) > 0 {
    78  		t.Fatalf("%v", issues.Errors())
    79  	}
    80  
    81  	prog, err := env.Program(compiled)
    82  	if err != nil {
    83  		t.Fatalf("%v", err)
    84  	}
    85  	res, _, err := prog.Eval(map[string]interface{}{})
    86  	if len(expectRuntimeErr) > 0 {
    87  		if err == nil {
    88  			t.Fatalf("no runtime error thrown. Expected: %v", expectRuntimeErr)
    89  		} else if expectRuntimeErr != err.Error() {
    90  			t.Fatalf("unexpected err: %v", err)
    91  		}
    92  	} else if err != nil {
    93  		t.Fatalf("%v", err)
    94  	} else if expectResult != nil {
    95  		converted := res.Equal(expectResult).Value().(bool)
    96  		require.True(t, converted, "expectation not equal to output")
    97  	} else {
    98  		t.Fatal("expected result must not be nil")
    99  	}
   100  }
   101  
   102  func TestIP(t *testing.T) {
   103  	ipv4Addr, _ := netip.ParseAddr("192.168.0.1")
   104  	int4 := types.Int(4)
   105  
   106  	ipv6Addr, _ := netip.ParseAddr("2001:db8::68")
   107  	int6 := types.Int(6)
   108  
   109  	trueVal := types.Bool(true)
   110  	falseVal := types.Bool(false)
   111  
   112  	cases := []struct {
   113  		name              string
   114  		expr              string
   115  		expectResult      ref.Val
   116  		expectRuntimeErr  string
   117  		expectCompileErrs []string
   118  	}{
   119  		{
   120  			name:         "parse ipv4",
   121  			expr:         `ip("192.168.0.1")`,
   122  			expectResult: apiservercel.IP{Addr: ipv4Addr},
   123  		},
   124  		{
   125  			name:             "parse invalid ipv4",
   126  			expr:             `ip("192.168.0.1.0")`,
   127  			expectRuntimeErr: "IP Address \"192.168.0.1.0\" parse error during conversion from string: ParseAddr(\"192.168.0.1.0\"): IPv4 address too long",
   128  		},
   129  		{
   130  			name:         "isIP valid ipv4",
   131  			expr:         `isIP("192.168.0.1")`,
   132  			expectResult: trueVal,
   133  		},
   134  		{
   135  			name:         "isIP invalid ipv4",
   136  			expr:         `isIP("192.168.0.1.0")`,
   137  			expectResult: falseVal,
   138  		},
   139  		{
   140  			name:         "ip.isCanonical valid ipv4",
   141  			expr:         `ip.isCanonical("127.0.0.1")`,
   142  			expectResult: trueVal,
   143  		},
   144  		{
   145  			name:             "ip.isCanonical invalid ipv4",
   146  			expr:             `ip.isCanonical("127.0.0.1.0")`,
   147  			expectRuntimeErr: "IP Address \"127.0.0.1.0\" parse error during conversion from string: ParseAddr(\"127.0.0.1.0\"): IPv4 address too long",
   148  		},
   149  		{
   150  			name:         "ipv4 family",
   151  			expr:         `ip("192.168.0.1").family()`,
   152  			expectResult: int4,
   153  		},
   154  		{
   155  			name:         "ipv4 isUnspecified true",
   156  			expr:         `ip("0.0.0.0").isUnspecified()`,
   157  			expectResult: trueVal,
   158  		},
   159  		{
   160  			name:         "ipv4 isUnspecified false",
   161  			expr:         `ip("127.0.0.1").isUnspecified()`,
   162  			expectResult: falseVal,
   163  		},
   164  		{
   165  			name:         "ipv4 isLoopback true",
   166  			expr:         `ip("127.0.0.1").isLoopback()`,
   167  			expectResult: trueVal,
   168  		},
   169  		{
   170  			name:         "ipv4 isLoopback false",
   171  			expr:         `ip("1.2.3.4").isLoopback()`,
   172  			expectResult: falseVal,
   173  		},
   174  		{
   175  			name:         "ipv4 isLinkLocalMulticast true",
   176  			expr:         `ip("224.0.0.1").isLinkLocalMulticast()`,
   177  			expectResult: trueVal,
   178  		},
   179  		{
   180  			name:         "ipv4 isLinkLocalMulticast false",
   181  			expr:         `ip("224.0.1.1").isLinkLocalMulticast()`,
   182  			expectResult: falseVal,
   183  		},
   184  		{
   185  			name:         "ipv4 isLinkLocalUnicast true",
   186  			expr:         `ip("169.254.169.254").isLinkLocalUnicast()`,
   187  			expectResult: trueVal,
   188  		},
   189  		{
   190  			name:         "ipv4 isLinkLocalUnicast false",
   191  			expr:         `ip("192.168.0.1").isLinkLocalUnicast()`,
   192  			expectResult: falseVal,
   193  		},
   194  		{
   195  			name:         "ipv4 isGlobalUnicast true",
   196  			expr:         `ip("192.168.0.1").isGlobalUnicast()`,
   197  			expectResult: trueVal,
   198  		},
   199  		{
   200  			name:         "ipv4 isGlobalUnicast false",
   201  			expr:         `ip("255.255.255.255").isGlobalUnicast()`,
   202  			expectResult: falseVal,
   203  		},
   204  		{
   205  			name:         "parse ipv6",
   206  			expr:         `ip("2001:db8::68")`,
   207  			expectResult: apiservercel.IP{Addr: ipv6Addr},
   208  		},
   209  		{
   210  			name:             "parse invalid ipv6",
   211  			expr:             `ip("2001:db8:::68")`,
   212  			expectRuntimeErr: "IP Address \"2001:db8:::68\" parse error during conversion from string: ParseAddr(\"2001:db8:::68\"): each colon-separated field must have at least one digit (at \":68\")",
   213  		},
   214  		{
   215  			name:         "isIP valid ipv6",
   216  			expr:         `isIP("2001:db8::68")`,
   217  			expectResult: trueVal,
   218  		},
   219  		{
   220  			name:         "isIP invalid ipv4",
   221  			expr:         `isIP("2001:db8:::68")`,
   222  			expectResult: falseVal,
   223  		},
   224  		{
   225  			name:         "ip.isCanonical valid ipv6",
   226  			expr:         `ip.isCanonical("2001:db8::68")`,
   227  			expectResult: trueVal,
   228  		},
   229  		{
   230  			name:         "ip.isCanonical non-canonical ipv6",
   231  			expr:         `ip.isCanonical("2001:DB8::68")`,
   232  			expectResult: falseVal,
   233  		},
   234  		{
   235  			name:             "ip.isCanonical invalid ipv6",
   236  			expr:             `ip.isCanonical("2001:db8:::68")`,
   237  			expectRuntimeErr: "IP Address \"2001:db8:::68\" parse error during conversion from string: ParseAddr(\"2001:db8:::68\"): each colon-separated field must have at least one digit (at \":68\")",
   238  		},
   239  		{
   240  			name:         "ipv6 family",
   241  			expr:         `ip("2001:db8::68").family()`,
   242  			expectResult: int6,
   243  		},
   244  		{
   245  			name:         "ipv6 isUnspecified true",
   246  			expr:         `ip("::").isUnspecified()`,
   247  			expectResult: trueVal,
   248  		},
   249  		{
   250  			name:         "ipv6 isUnspecified false",
   251  			expr:         `ip("::1").isUnspecified()`,
   252  			expectResult: falseVal,
   253  		},
   254  		{
   255  			name:         "ipv6 isLoopback true",
   256  			expr:         `ip("::1").isLoopback()`,
   257  			expectResult: trueVal,
   258  		},
   259  		{
   260  			name:         "ipv6 isLoopback false",
   261  			expr:         `ip("2001:db8::abcd").isLoopback()`,
   262  			expectResult: falseVal,
   263  		},
   264  		{
   265  			name:         "ipv6 isLinkLocalMulticast true",
   266  			expr:         `ip("ff02::1").isLinkLocalMulticast()`,
   267  			expectResult: trueVal,
   268  		},
   269  		{
   270  			name:         "ipv6 isLinkLocalMulticast false",
   271  			expr:         `ip("fd00::1").isLinkLocalMulticast()`,
   272  			expectResult: falseVal,
   273  		},
   274  		{
   275  			name:         "ipv6 isLinkLocalUnicast true",
   276  			expr:         `ip("fe80::1").isLinkLocalUnicast()`,
   277  			expectResult: trueVal,
   278  		},
   279  		{
   280  			name:         "ipv6 isLinkLocalUnicast false",
   281  			expr:         `ip("fd80::1").isLinkLocalUnicast()`,
   282  			expectResult: falseVal,
   283  		},
   284  		{
   285  			name:         "ipv6 isGlobalUnicast true",
   286  			expr:         `ip("2001:db8::abcd").isGlobalUnicast()`,
   287  			expectResult: trueVal,
   288  		},
   289  		{
   290  			name:         "ipv6 isGlobalUnicast false",
   291  			expr:         `ip("ff00::1").isGlobalUnicast()`,
   292  			expectResult: falseVal,
   293  		},
   294  		{
   295  			name:              "passing cidr into isIP returns compile error",
   296  			expr:              `isIP(cidr("192.168.0.0/24"))`,
   297  			expectCompileErrs: []string{"found no matching overload for 'isIP' applied to '\\(net.CIDR\\)'"},
   298  		},
   299  		{
   300  			name:         "converting an IP address to a string",
   301  			expr:         `string(ip("192.168.0.1"))`,
   302  			expectResult: types.String("192.168.0.1"),
   303  		},
   304  		{
   305  			name:         "type of IP is net.IP",
   306  			expr:         `type(ip("192.168.0.1")) == net.IP`,
   307  			expectResult: trueVal,
   308  		},
   309  	}
   310  
   311  	for _, tc := range cases {
   312  		t.Run(tc.name, func(t *testing.T) {
   313  			testIP(t, tc.expr, tc.expectResult, tc.expectRuntimeErr, tc.expectCompileErrs)
   314  		})
   315  	}
   316  }