Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions api/v1/installation_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,8 @@ type CalicoNetworkSpec struct {
// NodeAddressAutodetection provides configuration options for auto-detecting node addresses. At most one option
// can be used. If no detection option is specified, then IP auto detection will be disabled for this address family and IPs
// must be specified directly on the Node resource.
//
// +kubebuilder:validation:XValidation:rule="[has(self.firstFound) && self.firstFound == true, has(self.kubernetes), has(self.interface) && size(self.interface) > 0, has(self.skipInterface) && size(self.skipInterface) > 0, has(self.canReach) && size(self.canReach) > 0, has(self.cidrs) && size(self.cidrs) > 0].filter(x, x).size() <= 1",message="no more than one autodetection method can be specified"
type NodeAddressAutodetection struct {
// FirstFound uses default interface matching parameters to select an interface, performing best-effort
// filtering based on well-known interface names.
Expand Down
7 changes: 7 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,11 @@ require (
sigs.k8s.io/yaml v1.6.0
)

require github.com/google/cel-go v0.26.0

require (
al.essio.dev/pkg/shellescape v1.5.1 // indirect
cel.dev/expr v0.24.0 // indirect
dario.cat/mergo v1.0.2 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/BurntSushi/toml v1.5.0 // indirect
Expand All @@ -67,6 +70,7 @@ require (
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
github.com/Masterminds/squirrel v1.5.4 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/armon/go-radix v1.0.0 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 // indirect
Expand Down Expand Up @@ -168,6 +172,7 @@ require (
github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/cobra v1.10.1 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/stoewer/go-strcase v1.3.1 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/vbatts/tar-split v0.12.1 // indirect
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
Expand All @@ -179,6 +184,7 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 // indirect
golang.org/x/mod v0.32.0 // indirect
golang.org/x/oauth2 v0.33.0 // indirect
golang.org/x/sync v0.19.0 // indirect
Expand All @@ -189,6 +195,7 @@ require (
golang.org/x/tools v0.41.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 // indirect
google.golang.org/grpc v1.75.1 // indirect
google.golang.org/protobuf v1.36.10 // indirect
Expand Down
13 changes: 13 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho=
al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
Expand All @@ -26,6 +28,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwFc0ih8puEVAOFGELsn1zoIIYdxvML7mDxA=
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g=
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
Expand Down Expand Up @@ -207,6 +211,8 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI=
github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM=
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
Expand Down Expand Up @@ -411,7 +417,11 @@ github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs=
github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
Expand All @@ -420,6 +430,9 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
Expand Down
145 changes: 145 additions & 0 deletions pkg/controller/installation/cel_validation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// Copyright (c) 2026 Tigera, Inc. All rights reserved.

// 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 installation

import (
"context"
"encoding/json"
"path/filepath"
"runtime"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
operator "github.com/tigera/operator/api/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/utils/ptr"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
)

var _ = Describe("Installation CRD CEL validation", Serial, func() {
var (
testEnv *envtest.Environment
c client.Client
ctx context.Context
)

BeforeEach(func() {
ctx = context.Background()

_, thisFile, _, ok := runtime.Caller(0)
Expect(ok).To(BeTrue())
crdDir := filepath.Join(filepath.Dir(thisFile), "..", "..", "..", "config", "crd", "bases")

testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{crdDir},
ErrorIfCRDPathMissing: true,
}
cfg, err := testEnv.Start()
Expect(err).NotTo(HaveOccurred())
DeferCleanup(func() { _ = testEnv.Stop() })

Expect(operator.AddToScheme(scheme.Scheme)).To(Succeed())
c, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
Expect(err).NotTo(HaveOccurred())
})

newInstallation := func(v4 *operator.NodeAddressAutodetection) *operator.Installation {
return &operator.Installation{
ObjectMeta: metav1.ObjectMeta{Name: "default"},
Spec: operator.InstallationSpec{
CalicoNetwork: &operator.CalicoNetworkSpec{
NodeAddressAutodetectionV4: v4,
},
},
}
}

Describe("NodeAddressAutodetection", func() {
AfterEach(func() {
inst := &operator.Installation{ObjectMeta: metav1.ObjectMeta{Name: "default"}}
err := c.Delete(ctx, inst)
if err != nil {
GinkgoLogr.Error(err, "Failed to delete Installation in AfterEach")
}
})

DescribeTable("should allow single or no methods",
func(v4 *operator.NodeAddressAutodetection) {
Expect(c.Create(ctx, newInstallation(v4))).To(Succeed())
},
Entry("empty", &operator.NodeAddressAutodetection{}),
Entry("nil", nil),
Entry("firstFound", &operator.NodeAddressAutodetection{FirstFound: ptr.To(true)}),
Entry("interface", &operator.NodeAddressAutodetection{Interface: "eth0"}),
Entry("skipInterface", &operator.NodeAddressAutodetection{SkipInterface: "docker.*"}),
Entry("canReach", &operator.NodeAddressAutodetection{CanReach: "8.8.8.8"}),
Entry("cidrs", &operator.NodeAddressAutodetection{CIDRS: []string{"10.0.0.0/8"}}),
Entry("kubernetes", &operator.NodeAddressAutodetection{Kubernetes: ptr.To(operator.NodeInternalIP)}),
)

DescribeTable("should reject multiple methods",
func(v4 *operator.NodeAddressAutodetection) {
err := c.Create(ctx, newInstallation(v4))
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("no more than one autodetection method"))
},
Entry("firstFound + interface", &operator.NodeAddressAutodetection{
FirstFound: ptr.To(true),
Interface: "eth0",
}),
Entry("interface + canReach", &operator.NodeAddressAutodetection{
Interface: "eth0",
CanReach: "8.8.8.8",
}),
Entry("kubernetes + cidrs", &operator.NodeAddressAutodetection{
Kubernetes: ptr.To(operator.NodeInternalIP),
CIDRS: []string{"10.0.0.0/8"},
}),
Entry("three methods", &operator.NodeAddressAutodetection{
FirstFound: ptr.To(true),
Interface: "eth0",
CanReach: "8.8.8.8",
}),
Entry("skipInterface + canReach", &operator.NodeAddressAutodetection{
SkipInterface: "docker.*",
CanReach: "8.8.8.8",
}),
)

It("should reject a merge patch that adds a second method", func() {
inst := newInstallation(&operator.NodeAddressAutodetection{FirstFound: ptr.To(true)})
Expect(c.Create(ctx, inst)).To(Succeed())

// Merge patch adds interface alongside firstFound — the exact
// scenario that motivated this CEL rule.
patch, err := json.Marshal(map[string]any{
"spec": map[string]any{
"calicoNetwork": map[string]any{
"nodeAddressAutodetectionV4": map[string]any{
"interface": "eth0",
},
},
},
})
Expect(err).NotTo(HaveOccurred())
err = c.Patch(ctx, inst, client.RawPatch(types.MergePatchType, patch))
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("no more than one autodetection method"))
})
})
})
32 changes: 32 additions & 0 deletions pkg/imports/crds/operator/operator.tigera.io_installations.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1475,6 +1475,14 @@ spec:
the given regex.
type: string
type: object
x-kubernetes-validations:
- message: no more than one autodetection method can be specified
rule:
"[has(self.firstFound) && self.firstFound == true, has(self.kubernetes),
has(self.interface) && size(self.interface) > 0, has(self.skipInterface)
&& size(self.skipInterface) > 0, has(self.canReach) && size(self.canReach)
> 0, has(self.cidrs) && size(self.cidrs) > 0].filter(x, x).size()
<= 1"
nodeAddressAutodetectionV6:
description: |-
NodeAddressAutodetectionV6 specifies an approach to automatically detect node IPv6 addresses. If not specified,
Expand Down Expand Up @@ -1515,6 +1523,14 @@ spec:
the given regex.
type: string
type: object
x-kubernetes-validations:
- message: no more than one autodetection method can be specified
rule:
"[has(self.firstFound) && self.firstFound == true, has(self.kubernetes),
has(self.interface) && size(self.interface) > 0, has(self.skipInterface)
&& size(self.skipInterface) > 0, has(self.canReach) && size(self.canReach)
> 0, has(self.cidrs) && size(self.cidrs) > 0].filter(x, x).size()
<= 1"
sysctl:
description: Sysctl configures sysctl parameters for tuning plugin
items:
Expand Down Expand Up @@ -10298,6 +10314,14 @@ spec:
the given regex.
type: string
type: object
x-kubernetes-validations:
- message: no more than one autodetection method can be specified
rule:
"[has(self.firstFound) && self.firstFound == true,
has(self.kubernetes), has(self.interface) && size(self.interface)
> 0, has(self.skipInterface) && size(self.skipInterface)
> 0, has(self.canReach) && size(self.canReach) > 0, has(self.cidrs)
&& size(self.cidrs) > 0].filter(x, x).size() <= 1"
nodeAddressAutodetectionV6:
description: |-
NodeAddressAutodetectionV6 specifies an approach to automatically detect node IPv6 addresses. If not specified,
Expand Down Expand Up @@ -10338,6 +10362,14 @@ spec:
the given regex.
type: string
type: object
x-kubernetes-validations:
- message: no more than one autodetection method can be specified
rule:
"[has(self.firstFound) && self.firstFound == true,
has(self.kubernetes), has(self.interface) && size(self.interface)
> 0, has(self.skipInterface) && size(self.skipInterface)
> 0, has(self.canReach) && size(self.canReach) > 0, has(self.cidrs)
&& size(self.cidrs) > 0].filter(x, x).size() <= 1"
sysctl:
description:
Sysctl configures sysctl parameters for tuning
Expand Down
Loading