From 2053544aa0611ba3cde2880f16342f5c7606134b Mon Sep 17 00:00:00 2001 From: Anurag Saxena Date: Sun, 31 May 2026 21:45:36 -0400 Subject: [PATCH 1/2] CORENET-7206: Add OpenShift Tests Extension (OTE) framework for ingress-node-firewall Co-Authored-By: Claude Opus 4.6 --- Dockerfile.openshift | 6 +++ Makefile | 9 ++++ go.mod | 47 +++++++++++++++++++- go.sum | 37 +++++++++++++--- manifests/stable/image-references | 3 ++ test/Makefile | 47 ++++++++++++++++++++ test/cmd/main.go | 68 +++++++++++++++++++++++++++++ test/e2e/cli.go | 53 +++++++++++++++++++++++ test/e2e/util.go | 13 ++++++ test/otp/infw.go | 71 +++++++++++++++++++++++++++++++ 10 files changed, 348 insertions(+), 6 deletions(-) create mode 100644 test/Makefile create mode 100644 test/cmd/main.go create mode 100644 test/e2e/cli.go create mode 100644 test/e2e/util.go create mode 100644 test/otp/infw.go diff --git a/Dockerfile.openshift b/Dockerfile.openshift index 73e309105..d0e5f777a 100644 --- a/Dockerfile.openshift +++ b/Dockerfile.openshift @@ -14,13 +14,19 @@ COPY controllers/ controllers/ COPY pkg/ pkg/ COPY vendor/ vendor/ COPY bindata/manifests/ bindata/manifests/ +COPY test/ test/ # Build RUN CGO_ENABLED=0 GO111MODULE=on go build -a -mod=vendor -o manager main.go +# Build extended tests +RUN go mod vendor && make -C test build-e2e-tests && \ + gzip test/bin/ingress-node-firewall-tests + FROM registry.ci.openshift.org/ocp/4.22:base-rhel9 WORKDIR / COPY --from=builder /workspace/manager . COPY --from=builder /workspace/bindata/manifests /bindata/manifests +COPY --from=builder /workspace/test/bin/ingress-node-firewall-tests.gz /usr/bin/ ENTRYPOINT ["/manager"] diff --git a/Makefile b/Makefile index 628b17588..f7440d8e4 100644 --- a/Makefile +++ b/Makefile @@ -480,3 +480,12 @@ podman-build-daemon: ## Build the daemon image with podman. To change location, .PHONY: podman-push-daemon podman-push-daemon: ## Push the daemon image with docker. To change location, specify DAEMON_IMG=. podman push ${DAEMON_IMG} + +##@ Extended Tests (OTE) +.PHONY: build-e2e-tests +build-e2e-tests: ## Build the extended e2e test binary for OpenShift + $(MAKE) -C test build-e2e-tests + +.PHONY: clean-e2e-tests +clean-e2e-tests: ## Clean the extended e2e test artifacts + $(MAKE) -C test clean diff --git a/go.mod b/go.mod index 33ee76c11..043d4f3ff 100644 --- a/go.mod +++ b/go.mod @@ -13,10 +13,13 @@ require ( github.com/google/gopacket v1.1.19 github.com/kennygrant/sanitize v1.2.4 github.com/onsi/ginkgo v1.16.5 + github.com/onsi/ginkgo/v2 v2.23.3 github.com/onsi/gomega v1.37.0 + github.com/openshift-eng/openshift-tests-extension v0.0.0-20251218142942-7ecc8801b9df github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.22.0 github.com/prometheus/common v0.63.0 + github.com/spf13/cobra v1.8.1 github.com/vishvananda/netlink v1.3.1-0.20250206174618-62fb240731fa golang.org/x/sys v0.32.0 gopkg.in/mcuadros/go-syslog.v2 v2.3.0 @@ -32,8 +35,10 @@ require ( ) require ( + cel.dev/expr v0.18.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver v1.5.0 // indirect + github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect @@ -47,16 +52,20 @@ require ( github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.1.3 // indirect + github.com/google/cel-go v0.22.0 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/gofuzz v1.2.0 // indirect + github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/huandu/xstrings v1.3.2 // indirect github.com/imdario/mergo v0.3.16 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kylelemons/godebug v1.1.0 // indirect @@ -72,6 +81,7 @@ require ( github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/stoewer/go-strcase v1.3.0 // indirect github.com/vishvananda/netns v0.0.4 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/otel v1.32.0 // indirect @@ -79,13 +89,17 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/crypto v0.36.0 // indirect + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/net v0.37.0 // indirect - golang.org/x/oauth2 v0.25.0 // indirect + golang.org/x/oauth2 v0.27.0 // indirect golang.org/x/sync v0.12.0 // indirect golang.org/x/term v0.30.0 // indirect golang.org/x/text v0.23.0 // indirect golang.org/x/time v0.7.0 // indirect + golang.org/x/tools v0.30.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d // indirect google.golang.org/protobuf v1.36.5 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect @@ -97,3 +111,34 @@ require ( sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) + +// Replace directives for OTE framework +replace ( + github.com/onsi/ginkgo/v2 => github.com/openshift/onsi-ginkgo/v2 v2.6.1-0.20241205171354-8006f302fd12 + k8s.io/api => k8s.io/api v0.32.3 + k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.32.3 + k8s.io/apimachinery => k8s.io/apimachinery v0.32.3 + k8s.io/apiserver => k8s.io/apiserver v0.32.3 + k8s.io/cli-runtime => k8s.io/cli-runtime v0.32.3 + k8s.io/client-go => k8s.io/client-go v0.32.3 + k8s.io/cloud-provider => k8s.io/cloud-provider v0.32.3 + k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.32.3 + k8s.io/code-generator => k8s.io/code-generator v0.32.3 + k8s.io/component-base => k8s.io/component-base v0.32.3 + k8s.io/component-helpers => k8s.io/component-helpers v0.32.3 + k8s.io/controller-manager => k8s.io/controller-manager v0.32.3 + k8s.io/cri-api => k8s.io/cri-api v0.32.3 + k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.32.3 + k8s.io/dynamic-resource-allocation => k8s.io/dynamic-resource-allocation v0.32.3 + k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.32.3 + k8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.32.3 + k8s.io/kube-proxy => k8s.io/kube-proxy v0.32.3 + k8s.io/kube-scheduler => k8s.io/kube-scheduler v0.32.3 + k8s.io/kubectl => k8s.io/kubectl v0.32.3 + k8s.io/kubelet => k8s.io/kubelet v0.32.3 + k8s.io/legacy-cloud-providers => k8s.io/legacy-cloud-providers v0.32.3 + k8s.io/metrics => k8s.io/metrics v0.32.3 + k8s.io/mount-utils => k8s.io/mount-utils v0.32.3 + k8s.io/pod-security-admission => k8s.io/pod-security-admission v0.32.3 + k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.32.3 +) diff --git a/go.sum b/go.sum index 50df3ef57..5ac0aac34 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,13 @@ +cel.dev/expr v0.18.0 h1:CJ6drgk+Hf96lkLikr4rFf19WrU0BOWEihyZnI2TAzo= +cel.dev/expr v0.18.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60= github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= +github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= +github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -16,6 +20,7 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cilium/ebpf v0.18.0 h1:OsSwqS4y+gQHxaKgg2U/+Fev834kdnsQbtzRnbVC6Gs= github.com/cilium/ebpf v0.18.0/go.mod h1:vmsAT73y4lW2b4peE+qcOqw6MxvWQdC+LiU5gd/xyo4= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -45,7 +50,6 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6 h1:teYtXy9B7y5lHTp8V9KPxpYRAVA7dozigQcMiBust1s= github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= @@ -64,6 +68,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.22.0 h1:b3FJZxpiv1vTMo2/5RDUqAHPxkT8mmMfJIrq1llbf7g= +github.com/google/cel-go v0.22.0/go.mod h1:BuznPXXfQDpXKWQ9sPW3TzlAJN5zzFe+i9tIs0yC4s8= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -88,6 +94,8 @@ github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= @@ -136,12 +144,14 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/ginkgo/v2 v2.23.3 h1:edHxnszytJ4lD9D5Jjc4tiDkPBZ3siDeJJkUZJJVkp0= -github.com/onsi/ginkgo/v2 v2.23.3/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= +github.com/openshift-eng/openshift-tests-extension v0.0.0-20251218142942-7ecc8801b9df h1:/KiCxPFpkZN4HErfAX5tyhn6G3ziPFbkGswHVAZKY5Q= +github.com/openshift-eng/openshift-tests-extension v0.0.0-20251218142942-7ecc8801b9df/go.mod h1:6gkP5f2HL0meusT0Aim8icAspcD1cG055xxBZ9yC68M= +github.com/openshift/onsi-ginkgo/v2 v2.6.1-0.20241205171354-8006f302fd12 h1:AKx/w1qpS8We43bsRgf8Nll3CGlDHpr/WAXvuedTNZI= +github.com/openshift/onsi-ginkgo/v2 v2.6.1-0.20241205171354-8006f302fd12/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -157,11 +167,21 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= +github.com/stoewer/go-strcase v1.3.0/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/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/vishvananda/netlink v1.3.1-0.20250206174618-62fb240731fa h1:iAhToRwOrdk+pKzclvLM7nKZhsg8f7dVrgkFccDUbUw= @@ -187,6 +207,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -199,8 +221,8 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= -golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= -golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= +golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -242,6 +264,10 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 h1:YcyjlL1PRr2Q17/I0dPk2JmYS5CDXfcdb2Z3YRioEbw= +google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:OCdP9MfskevB/rbYvHTsXTtKC+3bHWajPdoKgjcYkfo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d h1:xJJRGY7TJcvIlpSrN3K6LAWgNFUILlO+OMAqtg9aqnw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -265,6 +291,7 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls= diff --git a/manifests/stable/image-references b/manifests/stable/image-references index 309987058..4390f13b5 100644 --- a/manifests/stable/image-references +++ b/manifests/stable/image-references @@ -4,6 +4,9 @@ apiVersion: image.openshift.io/v1 spec: tags: - name: ingress-node-firewall + annotations: + testextension.redhat.io/component: openshift/payload/ingress-node-firewall + testextension.redhat.io/binary: /usr/bin/ingress-node-firewall-tests.gz from: kind: DockerImage name: quay.io/openshift/origin-ingress-node-firewall:latest diff --git a/test/Makefile b/test/Makefile new file mode 100644 index 000000000..9d5cede0d --- /dev/null +++ b/test/Makefile @@ -0,0 +1,47 @@ +# test/Makefile - Build targets for ingress-node-firewall extended tests + +SHELL := /bin/bash + +# Binary name +BINARY_NAME := ingress-node-firewall-tests + +# Build directory +BUILD_DIR := bin +BINARY_PATH := $(BUILD_DIR)/$(BINARY_NAME) + +# Go build flags +GO := go +GOFLAGS ?= +LDFLAGS := -w -s + +# Version information +VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "unknown") +GIT_COMMIT ?= $(shell git rev-parse HEAD 2>/dev/null || echo "unknown") +BUILD_DATE ?= $(shell date -u +"%Y-%m-%dT%H:%M:%SZ") + +# LDFLAGS with version info +LDFLAGS += -X github.com/openshift/ingress-node-firewall/test/version.Version=$(VERSION) +LDFLAGS += -X github.com/openshift/ingress-node-firewall/test/version.GitCommit=$(GIT_COMMIT) +LDFLAGS += -X github.com/openshift/ingress-node-firewall/test/version.BuildDate=$(BUILD_DATE) + +.PHONY: all +all: build-e2e-tests + +.PHONY: build-e2e-tests +build-e2e-tests: ## Build the extended e2e test binary (static, ART compliant) + @echo "Building $(BINARY_NAME)..." + @mkdir -p $(BUILD_DIR) + CGO_ENABLED=0 GO_COMPLIANCE_POLICY="exempt_all" $(GO) build \ + -a -mod=mod \ + -ldflags "$(LDFLAGS)" \ + -o $(BINARY_PATH) ./cmd/main.go + @echo "Built $(BINARY_PATH)" + +.PHONY: clean +clean: ## Clean build artifacts + @echo "Cleaning test build artifacts..." + @rm -rf $(BUILD_DIR) + +.PHONY: help +help: ## Display this help + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) diff --git a/test/cmd/main.go b/test/cmd/main.go new file mode 100644 index 000000000..e1ed1ec52 --- /dev/null +++ b/test/cmd/main.go @@ -0,0 +1,68 @@ +package main + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + + "github.com/openshift-eng/openshift-tests-extension/pkg/cmd" + e "github.com/openshift-eng/openshift-tests-extension/pkg/extension" + et "github.com/openshift-eng/openshift-tests-extension/pkg/extension/extensiontests" + g "github.com/openshift-eng/openshift-tests-extension/pkg/ginkgo" + + _ "github.com/openshift/ingress-node-firewall/test/otp" +) + +func main() { + registry := e.NewRegistry() + ext := e.NewExtension("openshift", "payload", "ingress-node-firewall") + + ext.AddSuite(e.Suite{ + Name: "openshift/ingress-node-firewall/conformance/parallel", + Parents: []string{"openshift/conformance/parallel"}, + Qualifiers: []string{`[sig-network][JIRA:Networking] ingress-node-firewall && !labels.exists(l, l=="Serial") && !labels.exists(l, l=="Slow")`}, + }) + + ext.AddSuite(e.Suite{ + Name: "openshift/ingress-node-firewall/conformance/serial", + Parents: []string{"openshift/conformance/serial"}, + Qualifiers: []string{`[sig-network][JIRA:Networking] ingress-node-firewall && labels.exists(l, l=="Serial")`}, + }) + + ext.AddSuite(e.Suite{ + Name: "openshift/ingress-node-firewall/optional/slow", + Parents: []string{}, + Qualifiers: []string{`[sig-network][JIRA:Networking] ingress-node-firewall && labels.exists(l, l=="Slow")`}, + }) + + ext.AddSuite(e.Suite{ + Name: "openshift/ingress-node-firewall/all", + Parents: []string{}, + Qualifiers: []string{`[sig-network][JIRA:Networking] ingress-node-firewall`}, + }) + + specs, err := g.BuildExtensionTestSpecsFromOpenShiftGinkgoSuite() + if err != nil { + fmt.Fprintf(os.Stderr, "error: couldn't build extension test specs from ginkgo: %v\n", err) + os.Exit(1) + } + + specs.Walk(func(spec *et.ExtensionTestSpec) { + spec.Lifecycle = et.LifecycleBlocking + }) + + ext.AddSpecs(specs) + registry.Register(ext) + + rootCmd := &cobra.Command{ + Use: "ingress-node-firewall-tests", + Short: "OpenShift extended tests for ingress-node-firewall", + } + + rootCmd.AddCommand(cmd.DefaultExtensionCommands(registry)...) + + if err := rootCmd.Execute(); err != nil { + os.Exit(1) + } +} diff --git a/test/e2e/cli.go b/test/e2e/cli.go new file mode 100644 index 000000000..147defb2d --- /dev/null +++ b/test/e2e/cli.go @@ -0,0 +1,53 @@ +package e2e + +import ( + "bytes" + "context" + "fmt" + "os/exec" + "strings" +) + +// OCClient provides helper methods for executing oc commands +type OCClient struct { + kubeconfig string +} + +// NewOCClient creates a new OCClient +func NewOCClient(kubeconfig string) *OCClient { + if kubeconfig == "" { + kubeconfig = GetKubeconfig() + } + return &OCClient{ + kubeconfig: kubeconfig, + } +} + +// Run executes an oc command and returns the output +func (c *OCClient) Run(ctx context.Context, args ...string) (string, error) { + cmdArgs := append([]string{"--kubeconfig", c.kubeconfig}, args...) + cmd := exec.CommandContext(ctx, "oc", cmdArgs...) + + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + err := cmd.Run() + if err != nil { + return "", fmt.Errorf("command failed: %v, stderr: %s", err, stderr.String()) + } + + return strings.TrimSpace(stdout.String()), nil +} + +// Get gets a resource +func (c *OCClient) Get(ctx context.Context, resourceType, name, namespace string) (string, error) { + args := []string{"get", resourceType} + if name != "" { + args = append(args, name) + } + if namespace != "" { + args = append(args, "-n", namespace) + } + return c.Run(ctx, args...) +} diff --git a/test/e2e/util.go b/test/e2e/util.go new file mode 100644 index 000000000..588d34805 --- /dev/null +++ b/test/e2e/util.go @@ -0,0 +1,13 @@ +package e2e + +import ( + "os" +) + +// GetKubeconfig returns the kubeconfig path from KUBECONFIG env var or default location +func GetKubeconfig() string { + if kubeconfig := os.Getenv("KUBECONFIG"); kubeconfig != "" { + return kubeconfig + } + return os.Getenv("HOME") + "/.kube/config" +} diff --git a/test/otp/infw.go b/test/otp/infw.go new file mode 100644 index 000000000..c854386e9 --- /dev/null +++ b/test/otp/infw.go @@ -0,0 +1,71 @@ +package otp + +import ( + "context" + "fmt" + "strings" + "time" + + g "github.com/onsi/ginkgo/v2" + o "github.com/onsi/gomega" + + e2e "github.com/openshift/ingress-node-firewall/test/e2e" +) + +var _ = g.Describe("[sig-network][JIRA:Networking] ingress-node-firewall", func() { + var ( + oc *e2e.OCClient + ctx context.Context + cancel context.CancelFunc + opNamespace = "openshift-ingress-node-firewall" + ) + + g.BeforeEach(func() { + ctx, cancel = context.WithTimeout(context.Background(), 10*time.Minute) + oc = e2e.NewOCClient("") + }) + + g.AfterEach(func() { + if cancel != nil { + cancel() + } + }) + + g.It("[OTP][LEVEL0] 61481-Ingress Node Firewall Operator Installation", g.Serial, func() { + g.By("Checking Ingress Node Firewall operator installation") + + // Check that the operator namespace exists + output, err := oc.Get(ctx, "namespace", opNamespace, "") + o.Expect(err).NotTo(o.HaveOccurred(), "Operator namespace should exist") + o.Expect(output).To(o.ContainSubstring(opNamespace), "Namespace output should contain the operator namespace") + + g.By("Verifying CRDs are installed") + crdOutput, err := oc.Run(ctx, "get", "crd") + o.Expect(err).NotTo(o.HaveOccurred(), "Should be able to list CRDs") + + expectedCRDs := []string{ + "ingressnodefirewallconfigs.ingressnodefirewall.openshift.io", + "ingressnodefirewallnodestates.ingressnodefirewall.openshift.io", + "ingressnodefirewalls.ingressnodefirewall.openshift.io", + } + + for _, crd := range expectedCRDs { + o.Expect(strings.Contains(crdOutput, crd)).To(o.BeTrue(), + "CRD %s should be installed", crd) + } + + g.By("Verifying operator deployment is running") + // Check that the operator deployment exists and is ready + deploymentOutput, err := oc.Run(ctx, "get", "deployment", "-n", opNamespace, "-o=jsonpath={.items[*].metadata.name}") + o.Expect(err).NotTo(o.HaveOccurred(), "Should be able to list deployments in operator namespace") + o.Expect(deploymentOutput).NotTo(o.BeEmpty(), "There should be at least one deployment in the operator namespace") + + // Wait for the operator deployment to be ready + deploymentName := "ingress-node-firewall-controller-manager" + _, err = oc.Run(ctx, "wait", "deployment/"+deploymentName, "-n", opNamespace, "--for=condition=Available", "--timeout=5m") + o.Expect(err).NotTo(o.HaveOccurred(), "Operator deployment should be available") + + g.By("SUCCESS - Ingress Node Firewall operator and CRDs installed") + fmt.Println("Operator install and CRDs check successful!") + }) +}) From e248522a5b8e915e1c1a8f2d1e052271dc459a63 Mon Sep 17 00:00:00 2001 From: Anurag Saxena Date: Wed, 3 Jun 2026 14:33:38 -0400 Subject: [PATCH 2/2] Migrate 7 INFW e2e tests to OTE framework Add 7 new test cases migrated from openshift-tests-private into the OTE (OpenShift Tests Extension) framework, alongside the existing LEVEL0 test: - 54714: TCP NodePort Allow/Deny (Baremetal, Disruptive) - 54992: UDP NodePort Allow/Deny (Baremetal) - 55411: ICMP Allow/Deny (single-stack only) - 55410: SCTP Allow/Deny - 54973: Daemon metrics verification - 55414: Multiple CIDRs with multiple rules (single-stack only) - 73844: SSH traffic blocking (single-stack only) Updates suite definitions to use valid CEL qualifier syntax and adds an AWS suite that excludes Baremetal-labeled tests. LEVEL0 tests are LifecycleBlocking; all others are LifecycleInforming. Co-Authored-By: Claude Opus 4.6 --- test/cmd/main.go | 21 +- test/otp/helpers.go | 479 +++++++++++++++ test/otp/infw.go | 686 +++++++++++++++++++++- test/otp/testdata/infw-config.yaml | 12 + test/otp/testdata/infw-icmp.yaml | 26 + test/otp/testdata/infw-icmpv6.yaml | 26 + test/otp/testdata/infw-multiple-cidr.yaml | 27 + test/otp/testdata/infw.yaml | 26 + test/otp/testdata/pod-on-node.yaml | 20 + test/otp/testdata/sctp-client.yaml | 20 + test/otp/testdata/sctp-module.yaml | 23 + test/otp/testdata/sctp-server.yaml | 24 + test/otp/testdata/udp-listener.yaml | 28 + 13 files changed, 1386 insertions(+), 32 deletions(-) create mode 100644 test/otp/helpers.go create mode 100644 test/otp/testdata/infw-config.yaml create mode 100644 test/otp/testdata/infw-icmp.yaml create mode 100644 test/otp/testdata/infw-icmpv6.yaml create mode 100644 test/otp/testdata/infw-multiple-cidr.yaml create mode 100644 test/otp/testdata/infw.yaml create mode 100644 test/otp/testdata/pod-on-node.yaml create mode 100644 test/otp/testdata/sctp-client.yaml create mode 100644 test/otp/testdata/sctp-module.yaml create mode 100644 test/otp/testdata/sctp-server.yaml create mode 100644 test/otp/testdata/udp-listener.yaml diff --git a/test/cmd/main.go b/test/cmd/main.go index e1ed1ec52..daedc01f8 100644 --- a/test/cmd/main.go +++ b/test/cmd/main.go @@ -3,6 +3,7 @@ package main import ( "fmt" "os" + "strings" "github.com/spf13/cobra" @@ -21,25 +22,31 @@ func main() { ext.AddSuite(e.Suite{ Name: "openshift/ingress-node-firewall/conformance/parallel", Parents: []string{"openshift/conformance/parallel"}, - Qualifiers: []string{`[sig-network][JIRA:Networking] ingress-node-firewall && !labels.exists(l, l=="Serial") && !labels.exists(l, l=="Slow")`}, + Qualifiers: []string{`name.contains("[sig-network][JIRA:Networking] ingress-node-firewall") && !labels.exists(l, l=="Serial") && !labels.exists(l, l=="Slow")`}, }) ext.AddSuite(e.Suite{ Name: "openshift/ingress-node-firewall/conformance/serial", Parents: []string{"openshift/conformance/serial"}, - Qualifiers: []string{`[sig-network][JIRA:Networking] ingress-node-firewall && labels.exists(l, l=="Serial")`}, + Qualifiers: []string{`name.contains("[sig-network][JIRA:Networking] ingress-node-firewall") && labels.exists(l, l=="Serial")`}, }) ext.AddSuite(e.Suite{ Name: "openshift/ingress-node-firewall/optional/slow", Parents: []string{}, - Qualifiers: []string{`[sig-network][JIRA:Networking] ingress-node-firewall && labels.exists(l, l=="Slow")`}, + Qualifiers: []string{`name.contains("[sig-network][JIRA:Networking] ingress-node-firewall") && labels.exists(l, l=="Slow")`}, }) ext.AddSuite(e.Suite{ Name: "openshift/ingress-node-firewall/all", Parents: []string{}, - Qualifiers: []string{`[sig-network][JIRA:Networking] ingress-node-firewall`}, + Qualifiers: []string{`name.contains("[sig-network][JIRA:Networking] ingress-node-firewall")`}, + }) + + ext.AddSuite(e.Suite{ + Name: "openshift/ingress-node-firewall/aws", + Parents: []string{}, + Qualifiers: []string{`name.contains("[sig-network][JIRA:Networking] ingress-node-firewall") && !labels.exists(l, l=="Baremetal")`}, }) specs, err := g.BuildExtensionTestSpecsFromOpenShiftGinkgoSuite() @@ -49,7 +56,11 @@ func main() { } specs.Walk(func(spec *et.ExtensionTestSpec) { - spec.Lifecycle = et.LifecycleBlocking + if strings.Contains(spec.Name, "[LEVEL0]") { + spec.Lifecycle = et.LifecycleBlocking + } else { + spec.Lifecycle = et.LifecycleInforming + } }) ext.AddSpecs(specs) diff --git a/test/otp/helpers.go b/test/otp/helpers.go new file mode 100644 index 000000000..8dc9b410f --- /dev/null +++ b/test/otp/helpers.go @@ -0,0 +1,479 @@ +package otp + +import ( + "context" + "embed" + "fmt" + "os" + "strings" + "time" + + o "github.com/onsi/gomega" + + e2e "github.com/openshift/ingress-node-firewall/test/e2e" +) + +//go:embed testdata/*.yaml +var testdataFS embed.FS + +const ( + opNamespace = "openshift-ingress-node-firewall" +) + +// applyTemplateFile reads a YAML template from embedded testdata, performs parameter +// substitution, writes to a temp file, and applies via oc apply. +func applyTemplateFile(ctx context.Context, oc *e2e.OCClient, templateName string, params map[string]string) error { + content, err := testdataFS.ReadFile("testdata/" + templateName) + if err != nil { + return fmt.Errorf("reading template %s: %v", templateName, err) + } + + yaml := string(content) + for k, v := range params { + yaml = strings.ReplaceAll(yaml, "{{"+k+"}}", v) + } + + tmpFile, err := os.CreateTemp("", "infw-*.yaml") + if err != nil { + return err + } + defer os.Remove(tmpFile.Name()) + + if _, err := tmpFile.WriteString(yaml); err != nil { + tmpFile.Close() + return err + } + tmpFile.Close() + + _, err = oc.Run(ctx, "apply", "-f", tmpFile.Name()) + return err +} + +// applyRawTemplateFile applies a YAML file from embedded testdata without parameter substitution. +func applyRawTemplateFile(ctx context.Context, oc *e2e.OCClient, templateName string) error { + return applyTemplateFile(ctx, oc, templateName, nil) +} + +// getWorkerNodes returns a list of schedulable worker node names. +func getWorkerNodes(ctx context.Context, oc *e2e.OCClient) []string { + output, err := oc.Run(ctx, "get", "nodes", "-l", "node-role.kubernetes.io/worker", + "--field-selector=spec.unschedulable!=true", + "-o=jsonpath={.items[*].metadata.name}") + o.Expect(err).NotTo(o.HaveOccurred(), "failed to get worker nodes") + if output == "" { + return nil + } + return strings.Fields(output) +} + +// getMasterNode returns the name of a master/control-plane node. +func getMasterNode(ctx context.Context, oc *e2e.OCClient) string { + output, err := oc.Run(ctx, "get", "nodes", "-l", "node-role.kubernetes.io/master", + "-o=jsonpath={.items[0].metadata.name}") + o.Expect(err).NotTo(o.HaveOccurred(), "failed to get master node") + if output == "" { + output, err = oc.Run(ctx, "get", "nodes", "-l", "node-role.kubernetes.io/control-plane", + "-o=jsonpath={.items[0].metadata.name}") + o.Expect(err).NotTo(o.HaveOccurred(), "failed to get control-plane node") + } + return output +} + +// getNodeIPs returns (ipv6, ipv4) InternalIP addresses for the node. +// For single-stack clusters, the missing address family is returned as empty string. +func getNodeIPs(ctx context.Context, oc *e2e.OCClient, nodeName string) (string, string) { + output, err := oc.Run(ctx, "get", "node", nodeName, + `-o=jsonpath={.status.addresses[?(@.type=="InternalIP")].address}`) + o.Expect(err).NotTo(o.HaveOccurred(), "failed to get node IPs for %s", nodeName) + + ips := strings.Fields(output) + var ipv4, ipv6 string + for _, ip := range ips { + if strings.Contains(ip, ":") { + ipv6 = ip + } else { + ipv4 = ip + } + } + return ipv6, ipv4 +} + +// checkIPStackType returns "ipv4single", "ipv6single", or "dualstack". +func checkIPStackType(ctx context.Context, oc *e2e.OCClient) string { + output, err := oc.Run(ctx, "get", "network.config/cluster", + "-o=jsonpath={.status.clusterNetwork[*].cidr}") + o.Expect(err).NotTo(o.HaveOccurred(), "failed to get cluster network CIDRs") + + cidrs := strings.Fields(output) + hasV4 := false + hasV6 := false + for _, cidr := range cidrs { + if strings.Contains(cidr, ":") { + hasV6 = true + } else { + hasV4 = true + } + } + + switch { + case hasV4 && hasV6: + return "dualstack" + case hasV6: + return "ipv6single" + default: + return "ipv4single" + } +} + +// getPrimaryNIC returns the primary network interface name on cluster nodes. +func getPrimaryNIC(ctx context.Context, oc *e2e.OCClient) string { + workers := getWorkerNodes(ctx, oc) + o.Expect(len(workers)).NotTo(o.BeZero(), "no worker nodes found") + + output, err := oc.Run(ctx, "debug", "node/"+workers[0], "--", + "chroot", "/host", "bash", "-c", + "nmcli -t -f DEVICE,TYPE connection show --active | grep ethernet | head -1 | cut -d: -f1") + o.Expect(err).NotTo(o.HaveOccurred(), "failed to get primary NIC") + return strings.TrimSpace(output) +} + +// createPodOnNode creates a hello pod on a specific node. +func createPodOnNode(ctx context.Context, oc *e2e.OCClient, name, namespace, nodeName string) { + err := applyTemplateFile(ctx, oc, "pod-on-node.yaml", map[string]string{ + "NAME": name, + "NAMESPACE": namespace, + "NODENAME": nodeName, + }) + o.Expect(err).NotTo(o.HaveOccurred(), "failed to create pod %s", name) + waitForPodReady(ctx, oc, namespace, name) +} + +// waitForPodReady polls until the pod is in Running phase. +func waitForPodReady(ctx context.Context, oc *e2e.OCClient, namespace, name string) { + o.Eventually(func() string { + phase, _ := oc.Run(ctx, "get", "pod", name, "-n", namespace, "-o=jsonpath={.status.phase}") + return phase + }, 3*time.Minute, 5*time.Second).Should(o.Equal("Running"), "pod %s/%s not ready", namespace, name) +} + +// waitForPodWithLabelReady polls until at least one pod with the label is Running. +func waitForPodWithLabelReady(ctx context.Context, oc *e2e.OCClient, namespace, label string) { + o.Eventually(func() bool { + output, err := oc.Run(ctx, "get", "pods", "-n", namespace, "-l", label, + "-o=jsonpath={.items[*].status.phase}") + if err != nil { + return false + } + phases := strings.Fields(output) + for _, p := range phases { + if p == "Running" { + return true + } + } + return false + }, 3*time.Minute, 5*time.Second).Should(o.BeTrue(), "no pod with label %s ready in %s", label, namespace) +} + +// getPodIPs returns (primaryIP, secondaryIP) for a pod. +// For single-stack clusters, secondaryIP is empty. +func getPodIPs(ctx context.Context, oc *e2e.OCClient, namespace, name string) (string, string) { + output, err := oc.Run(ctx, "get", "pod", name, "-n", namespace, + "-o=jsonpath={.status.podIPs[*].ip}") + o.Expect(err).NotTo(o.HaveOccurred(), "failed to get pod IPs") + ips := strings.Fields(output) + if len(ips) == 0 { + return "", "" + } + if len(ips) == 1 { + return ips[0], "" + } + return ips[0], ips[1] +} + +// getPodNodeName returns the node name where the pod is running. +func getPodNodeName(ctx context.Context, oc *e2e.OCClient, namespace, name string) string { + output, err := oc.Run(ctx, "get", "pod", name, "-n", namespace, + "-o=jsonpath={.spec.nodeName}") + o.Expect(err).NotTo(o.HaveOccurred(), "failed to get pod node name") + return output +} + +// createNodePortService creates a NodePort service and returns the allocated port. +func createNodePortService(ctx context.Context, oc *e2e.OCClient, name, namespace, protocol, selector, ipFamilyPolicy string) string { + _, err := oc.Run(ctx, "create", "service", "nodeport", name, + "-n", namespace, + "--tcp=27017:8080") + o.Expect(err).NotTo(o.HaveOccurred(), "failed to create service %s", name) + + if protocol != "" && protocol != "TCP" { + _, err = oc.Run(ctx, "patch", "service", name, "-n", namespace, + "--type=json", + `-p=[{"op":"replace","path":"/spec/ports/0/protocol","value":"`+protocol+`"}]`) + o.Expect(err).NotTo(o.HaveOccurred()) + } + + if selector != "" { + _, err = oc.Run(ctx, "patch", "service", name, "-n", namespace, + "--type=json", + `-p=[{"op":"replace","path":"/spec/selector","value":{"name":"`+selector+`"}}]`) + o.Expect(err).NotTo(o.HaveOccurred()) + } + + if ipFamilyPolicy != "" { + _, err = oc.Run(ctx, "patch", "service", name, "-n", namespace, + "--type=merge", + `-p={"spec":{"ipFamilyPolicy":"`+ipFamilyPolicy+`"}}`) + o.Expect(err).NotTo(o.HaveOccurred()) + } + + nodePort, err := oc.Run(ctx, "get", "service", name, "-n", namespace, + "-o=jsonpath={.spec.ports[0].nodePort}") + o.Expect(err).NotTo(o.HaveOccurred(), "failed to get nodePort") + return nodePort +} + +// createINFWConfig creates an IngressNodeFirewallConfig CR. +func createINFWConfig(ctx context.Context, oc *e2e.OCClient) { + err := applyTemplateFile(ctx, oc, "infw-config.yaml", map[string]string{ + "NAMESPACE": opNamespace, + }) + o.Expect(err).NotTo(o.HaveOccurred(), "failed to create INFW config") + waitForINFWDaemonsReady(ctx, oc) +} + +// deleteINFWConfig deletes the IngressNodeFirewallConfig CR. +func deleteINFWConfig(ctx context.Context, oc *e2e.OCClient) { + _, err := oc.Run(ctx, "delete", "IngressNodeFirewallConfig", "ingressnodefirewallconfig", + "-n", opNamespace, "--ignore-not-found=true") + o.Expect(err).NotTo(o.HaveOccurred(), "failed to delete INFW config") +} + +// deleteINFWCR deletes an IngressNodeFirewall CR. Pass "--all" to delete all. +func deleteINFWCR(ctx context.Context, oc *e2e.OCClient, crName string) { + _, err := oc.Run(ctx, "delete", "IngressNodeFirewall", crName, "--ignore-not-found=true") + o.Expect(err).NotTo(o.HaveOccurred(), "failed to delete INFW CR %s", crName) +} + +// waitForINFWDaemonsReady waits until all INFW daemon pods are Running. +func waitForINFWDaemonsReady(ctx context.Context, oc *e2e.OCClient) { + waitForPodWithLabelReady(ctx, oc, opNamespace, "app=ingress-node-firewall-daemon") +} + +// restartINFWDaemons deletes all INFW daemon pods and waits for them to be recreated. +func restartINFWDaemons(ctx context.Context, oc *e2e.OCClient) { + _, err := oc.Run(ctx, "delete", "pod", "-l=app=ingress-node-firewall-daemon", + "-n", opNamespace) + o.Expect(err).NotTo(o.HaveOccurred(), "failed to restart INFW daemons") + waitForINFWDaemonsReady(ctx, oc) +} + +// getINFWDaemonForNode returns the INFW daemon pod name on a specific node. +func getINFWDaemonForNode(ctx context.Context, oc *e2e.OCClient, nodeName string) string { + output, err := oc.Run(ctx, "get", "pods", "-n", opNamespace, + "-l=app=ingress-node-firewall-daemon", + "--field-selector=spec.nodeName="+nodeName, + "-o=jsonpath={.items[0].metadata.name}") + o.Expect(err).NotTo(o.HaveOccurred(), "failed to get INFW daemon for node %s", nodeName) + o.Expect(output).NotTo(o.BeEmpty(), "no INFW daemon found on node %s", nodeName) + return output +} + +// checkDropEvents verifies that INFW daemon event logs contain "ruleId 1 action Drop". +func checkDropEvents(ctx context.Context, oc *e2e.OCClient, daemonPod string) { + output, err := oc.Run(ctx, "logs", "-n", opNamespace, daemonPod, "-c", "events") + o.Expect(err).NotTo(o.HaveOccurred(), "failed to get daemon event logs") + o.Expect(output).To(o.ContainSubstring("ruleId 1 action Drop"), "expected Drop events in daemon logs") +} + +// checkDropEventsWithPort verifies Drop events contain the expected destination port. +func checkDropEventsWithPort(ctx context.Context, oc *e2e.OCClient, daemonPod, dstPort string) { + output, err := oc.Run(ctx, "logs", "-n", opNamespace, daemonPod, "-c", "events") + o.Expect(err).NotTo(o.HaveOccurred(), "failed to get daemon event logs") + o.Expect(output).To(o.ContainSubstring("ruleId 1 action Drop"), "expected Drop events") + o.Expect(output).To(o.ContainSubstring("dstPort "+dstPort), "expected dstPort %s in events", dstPort) +} + +// curlNodePortPass verifies that a NodePort is reachable from one node to another. +func curlNodePortPass(ctx context.Context, oc *e2e.OCClient, fromNode, toNode, nodePort string) { + _, toNodeIPv4 := getNodeIPs(ctx, oc, toNode) + targetIP := toNodeIPv4 + if targetIP == "" { + targetIP, _ = getNodeIPs(ctx, oc, toNode) + } + o.Eventually(func() error { + _, err := debugNodeWithChroot(ctx, oc, fromNode, "curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", + "--connect-timeout", "5", + fmt.Sprintf("http://%s:%s", targetIP, nodePort)) + return err + }, 2*time.Minute, 10*time.Second).Should(o.Succeed(), "NodePort %s should be reachable from %s to %s", nodePort, fromNode, toNode) +} + +// curlNodePortFail verifies that a NodePort is NOT reachable. +func curlNodePortFail(ctx context.Context, oc *e2e.OCClient, fromNode, toNode, nodePort string) { + _, toNodeIPv4 := getNodeIPs(ctx, oc, toNode) + targetIP := toNodeIPv4 + if targetIP == "" { + targetIP, _ = getNodeIPs(ctx, oc, toNode) + } + o.Consistently(func() error { + _, err := debugNodeWithChroot(ctx, oc, fromNode, "curl", "-s", "-o", "/dev/null", + "--connect-timeout", "5", "--max-time", "5", + fmt.Sprintf("http://%s:%s", targetIP, nodePort)) + return err + }, 30*time.Second, 10*time.Second).Should(o.HaveOccurred(), "NodePort %s should NOT be reachable from %s to %s", nodePort, fromNode, toNode) +} + +// curlPod2PodPass verifies that one pod can reach another on port 8080. +func curlPod2PodPass(ctx context.Context, oc *e2e.OCClient, nsSrc, podSrc, nsDst, podDst string) { + podIP, _ := getPodIPs(ctx, oc, nsDst, podDst) + o.Eventually(func() error { + _, err := oc.Run(ctx, "exec", podSrc, "-n", nsSrc, "--", + "curl", "-s", "-o", "/dev/null", "--connect-timeout", "5", + fmt.Sprintf("http://%s:8080", podIP)) + return err + }, 2*time.Minute, 10*time.Second).Should(o.Succeed(), "pod %s/%s should reach %s/%s", nsSrc, podSrc, nsDst, podDst) +} + +// curlPod2PodFail verifies that one pod cannot reach another on port 8080. +func curlPod2PodFail(ctx context.Context, oc *e2e.OCClient, nsSrc, podSrc, nsDst, podDst string) { + podIP, _ := getPodIPs(ctx, oc, nsDst, podDst) + o.Consistently(func() error { + _, err := oc.Run(ctx, "exec", podSrc, "-n", nsSrc, "--", + "curl", "-s", "-o", "/dev/null", + "--connect-timeout", "5", "--max-time", "5", + fmt.Sprintf("http://%s:8080", podIP)) + return err + }, 30*time.Second, 10*time.Second).Should(o.HaveOccurred(), "pod %s/%s should NOT reach %s/%s", nsSrc, podSrc, nsDst, podDst) +} + +// debugNode runs a command on a node via oc debug. +func debugNode(ctx context.Context, oc *e2e.OCClient, nodeName string, cmd ...string) (string, error) { + args := append([]string{"debug", "node/" + nodeName, "--"}, cmd...) + return oc.Run(ctx, args...) +} + +// debugNodeWithChroot runs a command on a node via oc debug with chroot /host. +func debugNodeWithChroot(ctx context.Context, oc *e2e.OCClient, nodeName string, cmd ...string) (string, error) { + chrootCmd := append([]string{"chroot", "/host"}, cmd...) + args := append([]string{"debug", "node/" + nodeName, "--"}, chrootCmd...) + return oc.Run(ctx, args...) +} + +// execInPod runs a command inside a specific pod. +func execInPod(ctx context.Context, oc *e2e.OCClient, namespace, podName, command string) (string, error) { + return oc.Run(ctx, "exec", "-n", namespace, podName, "--", "bash", "-c", command) +} + +// createNamespace creates a new namespace and returns its name. +func createNamespace(ctx context.Context, oc *e2e.OCClient, name string) { + _, err := oc.Run(ctx, "create", "namespace", name) + o.Expect(err).NotTo(o.HaveOccurred(), "failed to create namespace %s", name) +} + +// deleteNamespace deletes a namespace. +func deleteNamespace(ctx context.Context, oc *e2e.OCClient, name string) { + _, _ = oc.Run(ctx, "delete", "namespace", name, "--wait=false", "--ignore-not-found=true") +} + +// setNamespacePrivileged labels a namespace with privileged PSA enforcement. +func setNamespacePrivileged(ctx context.Context, oc *e2e.OCClient, namespace string) { + _, err := oc.Run(ctx, "label", "namespace", namespace, + "pod-security.kubernetes.io/enforce=privileged", + "pod-security.kubernetes.io/audit=privileged", + "pod-security.kubernetes.io/warn=privileged", + "--overwrite") + o.Expect(err).NotTo(o.HaveOccurred(), "failed to set namespace %s as privileged", namespace) +} + +// createUDPListenerPod creates a UDP listener pod in the given namespace. +func createUDPListenerPod(ctx context.Context, oc *e2e.OCClient, namespace string) { + err := applyTemplateFile(ctx, oc, "udp-listener.yaml", map[string]string{ + "NAMESPACE": namespace, + }) + o.Expect(err).NotTo(o.HaveOccurred(), "failed to create UDP listener pod") + waitForPodWithLabelReady(ctx, oc, namespace, "name=udp-pod") +} + +// exposeUDPPod creates a NodePort service for the UDP pod and returns the NodePort. +func exposeUDPPod(ctx context.Context, oc *e2e.OCClient, podName, namespace string) string { + _, err := oc.Run(ctx, "expose", "pod", podName, "-n", namespace, + "--type=NodePort", "--port=8080", "--protocol=UDP") + o.Expect(err).NotTo(o.HaveOccurred(), "failed to expose UDP pod") + + nodePort, err := oc.Run(ctx, "get", "service", podName, "-n", namespace, + "-o=jsonpath={.spec.ports[0].nodePort}") + o.Expect(err).NotTo(o.HaveOccurred(), "failed to get UDP service nodePort") + return nodePort +} + +// prepareSCTPModule applies a MachineConfig to load the SCTP kernel module on workers. +// If the MachineConfig already exists, this is a no-op. +func prepareSCTPModule(ctx context.Context, oc *e2e.OCClient) { + _, err := oc.Run(ctx, "get", "machineconfig", "load-sctp-module") + if err == nil { + return + } + err = applyRawTemplateFile(ctx, oc, "sctp-module.yaml") + o.Expect(err).NotTo(o.HaveOccurred(), "failed to apply SCTP module MachineConfig") + + o.Eventually(func() string { + output, _ := oc.Run(ctx, "get", "mcp/worker", "-o=jsonpath={.status.conditions[?(@.type==\"Updated\")].status}") + return output + }, 30*time.Minute, 30*time.Second).Should(o.Equal("True"), "MachineConfigPool worker not updated after SCTP module install") +} + +// createSCTPClient creates an SCTP client pod on a specific node. +func createSCTPClient(ctx context.Context, oc *e2e.OCClient, namespace, nodeName string) { + err := applyTemplateFile(ctx, oc, "sctp-client.yaml", map[string]string{ + "NAMESPACE": namespace, + "NODENAME": nodeName, + }) + o.Expect(err).NotTo(o.HaveOccurred(), "failed to create SCTP client pod") + waitForPodWithLabelReady(ctx, oc, namespace, "name=sctpclient") +} + +// createSCTPServer creates an SCTP server pod on a specific node. +func createSCTPServer(ctx context.Context, oc *e2e.OCClient, namespace, nodeName string) { + err := applyTemplateFile(ctx, oc, "sctp-server.yaml", map[string]string{ + "NAMESPACE": namespace, + "NODENAME": nodeName, + }) + o.Expect(err).NotTo(o.HaveOccurred(), "failed to create SCTP server pod") + waitForPodWithLabelReady(ctx, oc, namespace, "name=sctpserver") +} + +// startSCTPServerListener starts an ncat SCTP listener in the server pod in the background. +func startSCTPServerListener(ctx context.Context, oc *e2e.OCClient, namespace, podName, port string) { + _, _ = execInPod(ctx, oc, namespace, podName, + "nohup /usr/bin/ncat -l "+port+" --sctp > /tmp/sctp.log 2>&1 &") + time.Sleep(5 * time.Second) + + output, err := execInPod(ctx, oc, namespace, podName, "ps aux | grep 'ncat.*sctp' | grep -v grep || true") + o.Expect(err).NotTo(o.HaveOccurred()) + o.Expect(output).To(o.ContainSubstring("ncat"), + "SCTP server process not started") +} + +// sendSCTPTraffic sends SCTP traffic from client to server. Returns error if traffic is blocked. +func sendSCTPTraffic(ctx context.Context, oc *e2e.OCClient, namespace, clientPod, serverIP, port string) error { + cmd := fmt.Sprintf("echo 'test traffic' | timeout 10 ncat -v %s %s --sctp", serverIP, port) + _, err := execInPod(ctx, oc, namespace, clientPod, cmd) + return err +} + +// verifySCTPServerReceived checks that the SCTP server process terminated after receiving traffic. +func verifySCTPServerReceived(ctx context.Context, oc *e2e.OCClient, namespace, podName, port string) { + time.Sleep(5 * time.Second) + output, err := execInPod(ctx, oc, namespace, podName, "ps aux | grep 'ncat.*sctp' | grep -v grep || true") + o.Expect(err).NotTo(o.HaveOccurred()) + o.Expect(output).NotTo(o.ContainSubstring("ncat"), + "SCTP server process should have terminated after receiving traffic") +} + +// portRange returns "port-(port+5)" string for INFW rule evaluation. +func portRange(port string) string { + var intVal int + _, err := fmt.Sscanf(port, "%d", &intVal) + o.Expect(err).NotTo(o.HaveOccurred(), "failed to parse port %s", port) + return fmt.Sprintf("%d-%d", intVal, intVal+5) +} diff --git a/test/otp/infw.go b/test/otp/infw.go index c854386e9..8f4ae3e6b 100644 --- a/test/otp/infw.go +++ b/test/otp/infw.go @@ -14,58 +14,690 @@ import ( var _ = g.Describe("[sig-network][JIRA:Networking] ingress-node-firewall", func() { var ( - oc *e2e.OCClient - ctx context.Context - cancel context.CancelFunc - opNamespace = "openshift-ingress-node-firewall" + oc *e2e.OCClient + ctx context.Context + cancel context.CancelFunc ) g.BeforeEach(func() { ctx, cancel = context.WithTimeout(context.Background(), 10*time.Minute) oc = e2e.NewOCClient("") - }) - - g.AfterEach(func() { - if cancel != nil { - cancel() - } + g.DeferCleanup(func() { + if cancel != nil { + cancel() + } + }) }) g.It("[OTP][LEVEL0] 61481-Ingress Node Firewall Operator Installation", g.Serial, func() { g.By("Checking Ingress Node Firewall operator installation") - // Check that the operator namespace exists output, err := oc.Get(ctx, "namespace", opNamespace, "") o.Expect(err).NotTo(o.HaveOccurred(), "Operator namespace should exist") - o.Expect(output).To(o.ContainSubstring(opNamespace), "Namespace output should contain the operator namespace") + o.Expect(output).To(o.ContainSubstring(opNamespace)) g.By("Verifying CRDs are installed") crdOutput, err := oc.Run(ctx, "get", "crd") - o.Expect(err).NotTo(o.HaveOccurred(), "Should be able to list CRDs") + o.Expect(err).NotTo(o.HaveOccurred()) expectedCRDs := []string{ "ingressnodefirewallconfigs.ingressnodefirewall.openshift.io", "ingressnodefirewallnodestates.ingressnodefirewall.openshift.io", "ingressnodefirewalls.ingressnodefirewall.openshift.io", } - for _, crd := range expectedCRDs { - o.Expect(strings.Contains(crdOutput, crd)).To(o.BeTrue(), - "CRD %s should be installed", crd) + o.Expect(crdOutput).To(o.ContainSubstring(crd), "CRD %s should be installed", crd) } g.By("Verifying operator deployment is running") - // Check that the operator deployment exists and is ready - deploymentOutput, err := oc.Run(ctx, "get", "deployment", "-n", opNamespace, "-o=jsonpath={.items[*].metadata.name}") - o.Expect(err).NotTo(o.HaveOccurred(), "Should be able to list deployments in operator namespace") - o.Expect(deploymentOutput).NotTo(o.BeEmpty(), "There should be at least one deployment in the operator namespace") - - // Wait for the operator deployment to be ready - deploymentName := "ingress-node-firewall-controller-manager" - _, err = oc.Run(ctx, "wait", "deployment/"+deploymentName, "-n", opNamespace, "--for=condition=Available", "--timeout=5m") - o.Expect(err).NotTo(o.HaveOccurred(), "Operator deployment should be available") + deploymentOutput, err := oc.Run(ctx, "get", "deployment", "-n", opNamespace, + "-o=jsonpath={.items[*].metadata.name}") + o.Expect(err).NotTo(o.HaveOccurred()) + o.Expect(deploymentOutput).NotTo(o.BeEmpty()) - g.By("SUCCESS - Ingress Node Firewall operator and CRDs installed") - fmt.Println("Operator install and CRDs check successful!") + _, err = oc.Run(ctx, "wait", "deployment/ingress-node-firewall-controller-manager", + "-n", opNamespace, "--for=condition=Available", "--timeout=5m") + o.Expect(err).NotTo(o.HaveOccurred(), "Operator deployment should be available") }) + + g.It("[OTP][WRS][V-BR.53] 54714-Check Ingress Firewall Allow/Deny functionality for TCP via Nodeport svc", + g.Serial, g.Label("Disruptive"), g.Label("Baremetal"), func() { + + workers := getWorkerNodes(ctx, oc) + o.Expect(len(workers)).To(o.BeNumerically(">=", 2), "need at least 2 worker nodes") + + ipStackType := checkIPStackType(ctx, oc) + o.Expect(ipStackType).NotTo(o.BeEmpty()) + + ns := fmt.Sprintf("e2e-infw-54714-%d", time.Now().UnixNano()%10000) + createNamespace(ctx, oc, ns) + g.DeferCleanup(func() { deleteNamespace(ctx, oc, ns) }) + + g.By("Create hello pod and TCP NodePort service") + createPodOnNode(ctx, oc, "hello-pod1", ns, workers[0]) + + var ipFamilyPolicy string + if ipStackType == "dualstack" { + ipFamilyPolicy = "RequireDualStack" + } else { + ipFamilyPolicy = "SingleStack" + } + nodePort := createNodePortService(ctx, oc, "test-service", ns, "TCP", "hello-pod", ipFamilyPolicy) + nodePortRange := portRange(nodePort) + + g.By("Verify NodePort is reachable before applying firewall rule") + curlNodePortPass(ctx, oc, workers[1], workers[0], nodePort) + + g.By("Create INFW config and daemons") + createINFWConfig(ctx, oc) + g.DeferCleanup(func() { deleteINFWConfig(ctx, oc) }) + + primaryInf := getPrimaryNIC(ctx, oc) + nodeIPv6, nodeIPv4 := getNodeIPs(ctx, oc, workers[1]) + + g.By("Create INFW CR to block TCP traffic") + if ipStackType == "dualstack" { + err := applyTemplateFile(ctx, oc, "infw-multiple-cidr.yaml", map[string]string{ + "NAME": "infw-block-nport-tcp", + "PRIMARY_INF": primaryInf, + "SRC_CIDR1": nodeIPv6 + "/128", + "SRC_CIDR2": nodeIPv4 + "/32", + "PROTOCOL_1": "TCP", + "PROTOCOLTYPE1": "tcp", + "RANGE_1": nodePortRange, + "ACTION_1": "Deny", + "PROTOCOL_2": "TCP", + "PROTOCOLTYPE2": "tcp", + "RANGE_2": nodePortRange, + "ACTION_2": "Allow", + }) + o.Expect(err).NotTo(o.HaveOccurred()) + } else { + srcCIDR := nodeIPv4 + "/32" + if ipStackType == "ipv6single" { + srcCIDR = nodeIPv6 + "/128" + } + err := applyTemplateFile(ctx, oc, "infw.yaml", map[string]string{ + "NAME": "infw-block-nport-tcp", + "PRIMARY_INF": primaryInf, + "SRC_CIDR1": srcCIDR, + "PROTOCOL_1": "TCP", + "PROTOCOLTYPE1": "tcp", + "RANGE_1": nodePortRange, + "ACTION_1": "Deny", + "PROTOCOL_2": "TCP", + "PROTOCOLTYPE2": "tcp", + "ACTION_2": "Allow", + }) + o.Expect(err).NotTo(o.HaveOccurred()) + } + g.DeferCleanup(func() { deleteINFWCR(ctx, oc, "--all") }) + + g.By("Verify NodePort is blocked") + curlNodePortFail(ctx, oc, workers[1], workers[0], nodePort) + + g.By("Verify Drop events logged") + infwDaemon := getINFWDaemonForNode(ctx, oc, workers[0]) + checkDropEvents(ctx, oc, infwDaemon) + + g.By("Remove rules, restart daemons, apply Allow rule") + deleteINFWCR(ctx, oc, "--all") + restartINFWDaemons(ctx, oc) + + if ipStackType == "dualstack" { + err := applyTemplateFile(ctx, oc, "infw-multiple-cidr.yaml", map[string]string{ + "NAME": "infw-block-nport-tcp", + "PRIMARY_INF": primaryInf, + "SRC_CIDR1": nodeIPv6 + "/128", + "SRC_CIDR2": nodeIPv4 + "/32", + "PROTOCOL_1": "TCP", + "PROTOCOLTYPE1": "tcp", + "RANGE_1": nodePortRange, + "ACTION_1": "Allow", + "PROTOCOL_2": "TCP", + "PROTOCOLTYPE2": "tcp", + "RANGE_2": nodePortRange, + "ACTION_2": "Allow", + }) + o.Expect(err).NotTo(o.HaveOccurred()) + } else { + srcCIDR := nodeIPv4 + "/32" + if ipStackType == "ipv6single" { + srcCIDR = nodeIPv6 + "/128" + } + err := applyTemplateFile(ctx, oc, "infw.yaml", map[string]string{ + "NAME": "infw-block-nport-tcp", + "PRIMARY_INF": primaryInf, + "SRC_CIDR1": srcCIDR, + "PROTOCOL_1": "TCP", + "PROTOCOLTYPE1": "tcp", + "RANGE_1": nodePortRange, + "ACTION_1": "Allow", + "PROTOCOL_2": "TCP", + "PROTOCOLTYPE2": "tcp", + "ACTION_2": "Allow", + }) + o.Expect(err).NotTo(o.HaveOccurred()) + } + + g.By("Verify NodePort is reachable again") + curlNodePortPass(ctx, oc, workers[1], workers[0], nodePort) + + g.By("Delete INFW deployment and daemonset, verify traffic still works") + _, err := oc.Run(ctx, "delete", "deployment", "ingress-node-firewall-controller-manager", + "-n", opNamespace) + o.Expect(err).NotTo(o.HaveOccurred()) + _, err = oc.Run(ctx, "delete", "ds", "ingress-node-firewall-daemon", + "-n", opNamespace) + o.Expect(err).NotTo(o.HaveOccurred()) + + curlNodePortPass(ctx, oc, workers[1], workers[0], nodePort) + }) + + g.It("[OTP][WRS][V-BR.53] 54992-Check Ingress Firewall Allow/Deny functionality for UDP via Nodeport svc", + g.Serial, g.Label("Baremetal"), func() { + + workers := getWorkerNodes(ctx, oc) + o.Expect(len(workers)).To(o.BeNumerically(">=", 2), "need at least 2 worker nodes") + + ipStackType := checkIPStackType(ctx, oc) + o.Expect(ipStackType).NotTo(o.BeEmpty()) + + ns := fmt.Sprintf("e2e-infw-54992-%d", time.Now().UnixNano()%10000) + createNamespace(ctx, oc, ns) + g.DeferCleanup(func() { deleteNamespace(ctx, oc, ns) }) + + g.By("Create UDP listener pod and expose as NodePort") + createUDPListenerPod(ctx, oc, ns) + udpPodName := "udp-pod" + nodePort := exposeUDPPod(ctx, oc, udpPodName, ns) + nodePortRange := portRange(nodePort) + + podNodeName := getPodNodeName(ctx, oc, ns, udpPodName) + masterNode := getMasterNode(ctx, oc) + _, podNodeIPv4 := getNodeIPs(ctx, oc, podNodeName) + + g.By("Create INFW config") + createINFWConfig(ctx, oc) + g.DeferCleanup(func() { deleteINFWConfig(ctx, oc) }) + + primaryInf := getPrimaryNIC(ctx, oc) + masterIPv6, masterIPv4 := getNodeIPs(ctx, oc, masterNode) + + g.By("Create INFW CR to block UDP traffic") + if ipStackType == "dualstack" { + err := applyTemplateFile(ctx, oc, "infw-multiple-cidr.yaml", map[string]string{ + "NAME": "infw-block-nport-udp", + "PRIMARY_INF": primaryInf, + "SRC_CIDR1": masterIPv6 + "/128", + "SRC_CIDR2": masterIPv4 + "/32", + "PROTOCOL_1": "UDP", + "PROTOCOLTYPE1": "udp", + "RANGE_1": nodePortRange, + "ACTION_1": "Deny", + "PROTOCOL_2": "UDP", + "PROTOCOLTYPE2": "udp", + "RANGE_2": nodePortRange, + "ACTION_2": "Allow", + }) + o.Expect(err).NotTo(o.HaveOccurred()) + } else { + srcCIDR := masterIPv4 + "/32" + if ipStackType == "ipv6single" { + srcCIDR = masterIPv6 + "/128" + } + err := applyTemplateFile(ctx, oc, "infw.yaml", map[string]string{ + "NAME": "infw-block-nport-udp", + "PRIMARY_INF": primaryInf, + "SRC_CIDR1": srcCIDR, + "PROTOCOL_1": "UDP", + "PROTOCOLTYPE1": "udp", + "RANGE_1": nodePortRange, + "ACTION_1": "Deny", + "PROTOCOL_2": "UDP", + "PROTOCOLTYPE2": "udp", + "ACTION_2": "Allow", + }) + o.Expect(err).NotTo(o.HaveOccurred()) + } + g.DeferCleanup(func() { deleteINFWCR(ctx, oc, "--all") }) + + g.By("Send UDP packet from master to pod node - expect Deny") + cmd := fmt.Sprintf("echo -n hello >/dev/udp/%s/%s", podNodeIPv4, nodePort) + debugNode(ctx, oc, masterNode, "bash", "-c", cmd) + + g.By("Verify Drop events") + infwDaemon := getINFWDaemonForNode(ctx, oc, podNodeName) + checkDropEvents(ctx, oc, infwDaemon) + + g.By("Remove rules, restart daemons, apply Allow rule") + deleteINFWCR(ctx, oc, "--all") + restartINFWDaemons(ctx, oc) + + if ipStackType == "dualstack" { + err := applyTemplateFile(ctx, oc, "infw-multiple-cidr.yaml", map[string]string{ + "NAME": "infw-block-nport-udp", + "PRIMARY_INF": primaryInf, + "SRC_CIDR1": masterIPv6 + "/128", + "SRC_CIDR2": masterIPv4 + "/32", + "PROTOCOL_1": "UDP", + "PROTOCOLTYPE1": "udp", + "RANGE_1": nodePortRange, + "ACTION_1": "Allow", + "PROTOCOL_2": "UDP", + "PROTOCOLTYPE2": "udp", + "RANGE_2": nodePortRange, + "ACTION_2": "Allow", + }) + o.Expect(err).NotTo(o.HaveOccurred()) + } else { + srcCIDR := masterIPv4 + "/32" + if ipStackType == "ipv6single" { + srcCIDR = masterIPv6 + "/128" + } + err := applyTemplateFile(ctx, oc, "infw.yaml", map[string]string{ + "NAME": "infw-block-nport-udp", + "PRIMARY_INF": primaryInf, + "SRC_CIDR1": srcCIDR, + "PROTOCOL_1": "UDP", + "PROTOCOLTYPE1": "udp", + "RANGE_1": nodePortRange, + "ACTION_1": "Allow", + "PROTOCOL_2": "UDP", + "PROTOCOLTYPE2": "udp", + "ACTION_2": "Allow", + }) + o.Expect(err).NotTo(o.HaveOccurred()) + } + + g.By("Send UDP packet again - expect Allow") + cmd = fmt.Sprintf("echo -n hello >/dev/udp/%s/%s", podNodeIPv4, nodePort) + debugNode(ctx, oc, masterNode, "bash", "-c", cmd) + + infwDaemon = getINFWDaemonForNode(ctx, oc, podNodeName) + _, err := oc.Run(ctx, "logs", "-n", opNamespace, infwDaemon, "-c", "events") + o.Expect(err).NotTo(o.HaveOccurred()) + }) + + g.It("[OTP][WRS][V-BR.53] 55411-Check Ingress Firewall Allow/Deny functionality for ICMP", + g.Serial, func() { + + ipStackType := checkIPStackType(ctx, oc) + o.Expect(ipStackType).NotTo(o.BeEmpty()) + if ipStackType == "dualstack" { + g.Skip("This case requires single stack cluster") + } + + workers := getWorkerNodes(ctx, oc) + o.Expect(len(workers)).To(o.BeNumerically(">=", 2), "need at least 2 worker nodes") + + ns1 := fmt.Sprintf("e2e-infw-55411-a-%d", time.Now().UnixNano()%10000) + createNamespace(ctx, oc, ns1) + g.DeferCleanup(func() { deleteNamespace(ctx, oc, ns1) }) + + ns2 := fmt.Sprintf("e2e-infw-55411-b-%d", time.Now().UnixNano()%10000) + createNamespace(ctx, oc, ns2) + g.DeferCleanup(func() { deleteNamespace(ctx, oc, ns2) }) + + g.By("Create hello pods on different nodes in different namespaces") + createPodOnNode(ctx, oc, "hello-pod", ns1, workers[0]) + createPodOnNode(ctx, oc, "hello-pod", ns2, workers[1]) + + hellopodIPns1, _ := getPodIPs(ctx, oc, ns1, "hello-pod") + hellopodIPns2, _ := getPodIPs(ctx, oc, ns2, "hello-pod") + + primaryInf := "genev_sys_6081" + + g.By("Create INFW config") + createINFWConfig(ctx, oc) + g.DeferCleanup(func() { deleteINFWConfig(ctx, oc) }) + + g.By("Create INFW CR to block ICMP") + var templateFile, crName string + srcCIDR := hellopodIPns1 + if ipStackType == "ipv6single" { + templateFile = "infw-icmpv6.yaml" + crName = "infw-block-icmpv6" + srcCIDR += "/128" + } else { + templateFile = "infw-icmp.yaml" + crName = "infw-block-icmp" + srcCIDR += "/32" + } + err := applyTemplateFile(ctx, oc, templateFile, map[string]string{ + "NAME": crName, + "PRIMARY_INF": primaryInf, + "SRC_CIDR": srcCIDR, + "ACTION_1": "Deny", + "ACTION_2": "Allow", + }) + o.Expect(err).NotTo(o.HaveOccurred()) + g.DeferCleanup(func() { deleteINFWCR(ctx, oc, crName) }) + + g.By("Verify ping fails with 100% packet loss") + var pingCmd string + if ipStackType == "ipv6single" { + pingCmd = "ping6 -c4 " + hellopodIPns2 + " 2>&1 || true" + } else { + pingCmd = "ping -c4 " + hellopodIPns2 + " 2>&1 || true" + } + output, _ := oc.Run(ctx, "exec", "-n", ns1, "hello-pod", "--", "/bin/sh", "-c", pingCmd) + o.Expect(output).To(o.ContainSubstring("100% packet loss")) + + g.By("Verify Drop events") + infwDaemon := getINFWDaemonForNode(ctx, oc, workers[1]) + checkDropEvents(ctx, oc, infwDaemon) + + g.By("Remove rules, create INFW CR with Allow action") + deleteINFWCR(ctx, oc, "--all") + + err = applyTemplateFile(ctx, oc, templateFile, map[string]string{ + "NAME": crName, + "PRIMARY_INF": primaryInf, + "SRC_CIDR": srcCIDR, + "ACTION_1": "Allow", + "ACTION_2": "Allow", + }) + o.Expect(err).NotTo(o.HaveOccurred()) + + g.By("Verify ping succeeds") + output, _ = oc.Run(ctx, "exec", "-n", ns1, "hello-pod", "--", "/bin/sh", "-c", pingCmd) + o.Expect(output).NotTo(o.ContainSubstring("100% packet loss")) + }) + + g.It("[OTP][WRS][V-BR.53] 55410-Check Ingress Firewall Allow/Deny functionality for SCTP", + g.Serial, func() { + + workers := getWorkerNodes(ctx, oc) + o.Expect(len(workers)).To(o.BeNumerically(">=", 2), "need at least 2 worker nodes") + + g.By("Install SCTP kernel module") + prepareSCTPModule(ctx, oc) + + ipStackType := checkIPStackType(ctx, oc) + o.Expect(ipStackType).NotTo(o.BeEmpty()) + + ns := fmt.Sprintf("e2e-infw-55410-%d", time.Now().UnixNano()%10000) + createNamespace(ctx, oc, ns) + g.DeferCleanup(func() { deleteNamespace(ctx, oc, ns) }) + setNamespacePrivileged(ctx, oc, ns) + + g.By("Create SCTP client and server pods on different nodes") + createSCTPClient(ctx, oc, ns, workers[0]) + createSCTPServer(ctx, oc, ns, workers[1]) + + nodePort := "30102" + nodePortRange := portRange(nodePort) + + g.By("Create INFW config") + createINFWConfig(ctx, oc) + g.DeferCleanup(func() { deleteINFWConfig(ctx, oc) }) + + primaryInf := "genev_sys_6081" + + sctpClientIP1, sctpClientIP2 := getPodIPs(ctx, oc, ns, "sctpclient") + sctpServerIP1, _ := getPodIPs(ctx, oc, ns, "sctpserver") + + g.By("Create INFW CR with Allow action for SCTP") + if ipStackType == "dualstack" { + err := applyTemplateFile(ctx, oc, "infw-multiple-cidr.yaml", map[string]string{ + "NAME": "infw-block-sctp", + "PRIMARY_INF": primaryInf, + "SRC_CIDR1": sctpClientIP2 + "/32", + "SRC_CIDR2": sctpClientIP1 + "/128", + "PROTOCOL_1": "SCTP", + "PROTOCOLTYPE1": "sctp", + "RANGE_1": nodePortRange, + "ACTION_1": "Allow", + "PROTOCOL_2": "SCTP", + "PROTOCOLTYPE2": "sctp", + "RANGE_2": nodePortRange, + "ACTION_2": "Allow", + }) + o.Expect(err).NotTo(o.HaveOccurred()) + } else { + srcCIDR := sctpClientIP1 + "/32" + if ipStackType == "ipv6single" { + srcCIDR = sctpClientIP1 + "/128" + } + err := applyTemplateFile(ctx, oc, "infw.yaml", map[string]string{ + "NAME": "infw-block-sctp", + "PRIMARY_INF": primaryInf, + "SRC_CIDR1": srcCIDR, + "PROTOCOL_1": "SCTP", + "PROTOCOLTYPE1": "sctp", + "RANGE_1": nodePortRange, + "ACTION_1": "Allow", + "PROTOCOL_2": "SCTP", + "PROTOCOLTYPE2": "sctp", + "ACTION_2": "Allow", + }) + o.Expect(err).NotTo(o.HaveOccurred()) + } + g.DeferCleanup(func() { deleteINFWCR(ctx, oc, "--all") }) + + g.By("Start SCTP server and send traffic - expect Allow") + startSCTPServerListener(ctx, oc, ns, "sctpserver", "30102") + err := sendSCTPTraffic(ctx, oc, ns, "sctpclient", sctpServerIP1, "30102") + o.Expect(err).NotTo(o.HaveOccurred(), "SCTP traffic should pass with Allow rule") + verifySCTPServerReceived(ctx, oc, ns, "sctpserver", "30102") + + g.By("Remove rules, restart daemons, apply Deny rule") + deleteINFWCR(ctx, oc, "--all") + restartINFWDaemons(ctx, oc) + + if ipStackType == "dualstack" { + err = applyTemplateFile(ctx, oc, "infw-multiple-cidr.yaml", map[string]string{ + "NAME": "infw-block-sctp", + "PRIMARY_INF": primaryInf, + "SRC_CIDR1": sctpClientIP2 + "/32", + "SRC_CIDR2": sctpClientIP1 + "/128", + "PROTOCOL_1": "SCTP", + "PROTOCOLTYPE1": "sctp", + "RANGE_1": nodePortRange, + "ACTION_1": "Deny", + "PROTOCOL_2": "SCTP", + "PROTOCOLTYPE2": "sctp", + "RANGE_2": nodePortRange, + "ACTION_2": "Allow", + }) + o.Expect(err).NotTo(o.HaveOccurred()) + } else { + srcCIDR := sctpClientIP1 + "/32" + if ipStackType == "ipv6single" { + srcCIDR = sctpClientIP1 + "/128" + } + err = applyTemplateFile(ctx, oc, "infw.yaml", map[string]string{ + "NAME": "infw-block-sctp", + "PRIMARY_INF": primaryInf, + "SRC_CIDR1": srcCIDR, + "PROTOCOL_1": "SCTP", + "PROTOCOLTYPE1": "sctp", + "RANGE_1": nodePortRange, + "ACTION_1": "Deny", + "PROTOCOL_2": "SCTP", + "PROTOCOLTYPE2": "sctp", + "ACTION_2": "Allow", + }) + o.Expect(err).NotTo(o.HaveOccurred()) + } + + g.By("Start SCTP server and send traffic - expect Deny") + startSCTPServerListener(ctx, oc, ns, "sctpserver", "30102") + err = sendSCTPTraffic(ctx, oc, ns, "sctpclient", sctpServerIP1, "30102") + o.Expect(err).To(o.HaveOccurred(), "SCTP traffic should be denied") + + g.By("Verify Drop events") + podNodeName := getPodNodeName(ctx, oc, ns, "sctpserver") + infwDaemon := getINFWDaemonForNode(ctx, oc, podNodeName) + checkDropEvents(ctx, oc, infwDaemon) + }) + + g.It("[OTP] 54973-Make sure events and metrics are logged for ingress-node-firewall-daemon", + g.Serial, func() { + + workers := getWorkerNodes(ctx, oc) + o.Expect(len(workers)).NotTo(o.BeZero(), "need at least 1 worker node") + + g.By("Create INFW config") + createINFWConfig(ctx, oc) + g.DeferCleanup(func() { deleteINFWConfig(ctx, oc) }) + + g.By("Check metrics endpoint on daemon pod") + infwDaemon := getINFWDaemonForNode(ctx, oc, workers[0]) + output, err := execInPod(ctx, oc, opNamespace, infwDaemon, "curl -s http://127.0.0.1:39401/metrics") + o.Expect(err).NotTo(o.HaveOccurred(), "failed to query metrics endpoint") + o.Expect(output).To(o.ContainSubstring("ingressnodefirewall"), "metrics should contain ingressnodefirewall") + }) + + g.It("[OTP] 55414-Check multiple CIDRs with multiple rules functionality with Ingress Firewall Node Operator", + g.Serial, func() { + + ipStackType := checkIPStackType(ctx, oc) + o.Expect(ipStackType).NotTo(o.BeEmpty()) + if ipStackType == "dualstack" { + g.Skip("This case requires single stack cluster") + } + + workers := getWorkerNodes(ctx, oc) + o.Expect(len(workers)).To(o.BeNumerically(">=", 2), "need at least 2 worker nodes") + + g.By("Install SCTP kernel module") + prepareSCTPModule(ctx, oc) + + ns := fmt.Sprintf("e2e-infw-55414-%d", time.Now().UnixNano()%10000) + createNamespace(ctx, oc, ns) + g.DeferCleanup(func() { deleteNamespace(ctx, oc, ns) }) + setNamespacePrivileged(ctx, oc, ns) + + g.By("Create SCTP client and server pods") + createSCTPClient(ctx, oc, ns, workers[0]) + createSCTPServer(ctx, oc, ns, workers[1]) + + g.By("Create hello pods for TCP traffic testing") + createPodOnNode(ctx, oc, "hello-pod-client", ns, workers[0]) + createPodOnNode(ctx, oc, "hello-pod-server", ns, workers[1]) + + nodePortSCTP := "30102" + portRangeSCTP := portRange(nodePortSCTP) + portRangeTCP := "8080-8081" + + g.By("Create INFW config") + createINFWConfig(ctx, oc) + g.DeferCleanup(func() { deleteINFWConfig(ctx, oc) }) + + primaryInf := "genev_sys_6081" + sctpClientIP, _ := getPodIPs(ctx, oc, ns, "sctpclient") + sctpServerIP, _ := getPodIPs(ctx, oc, ns, "sctpserver") + helloPodClientIP, _ := getPodIPs(ctx, oc, ns, "hello-pod-client") + + g.By("Create INFW CR with Allow for both SCTP and TCP") + srcCIDR1 := sctpClientIP + "/32" + srcCIDR2 := helloPodClientIP + "/32" + if ipStackType == "ipv6single" { + srcCIDR1 = sctpClientIP + "/128" + srcCIDR2 = helloPodClientIP + "/128" + } + err := applyTemplateFile(ctx, oc, "infw-multiple-cidr.yaml", map[string]string{ + "NAME": "infw-allow-sctp-tcp", + "PRIMARY_INF": primaryInf, + "SRC_CIDR1": srcCIDR1, + "SRC_CIDR2": srcCIDR2, + "PROTOCOL_1": "SCTP", + "PROTOCOLTYPE1": "sctp", + "RANGE_1": portRangeSCTP, + "ACTION_1": "Allow", + "PROTOCOL_2": "TCP", + "PROTOCOLTYPE2": "tcp", + "RANGE_2": portRangeTCP, + "ACTION_2": "Allow", + }) + o.Expect(err).NotTo(o.HaveOccurred()) + g.DeferCleanup(func() { deleteINFWCR(ctx, oc, "--all") }) + + g.By("Verify SCTP traffic passes with Allow") + startSCTPServerListener(ctx, oc, ns, "sctpserver", "30102") + err = sendSCTPTraffic(ctx, oc, ns, "sctpclient", sctpServerIP, "30102") + o.Expect(err).NotTo(o.HaveOccurred(), "SCTP traffic should pass") + + g.By("Verify TCP traffic passes with Allow") + curlPod2PodPass(ctx, oc, ns, "hello-pod-client", ns, "hello-pod-server") + + g.By("Delete Allow CR, create Deny CR") + deleteINFWCR(ctx, oc, "--all") + + err = applyTemplateFile(ctx, oc, "infw-multiple-cidr.yaml", map[string]string{ + "NAME": "infw-block-sctp-tcp", + "PRIMARY_INF": primaryInf, + "SRC_CIDR1": srcCIDR1, + "SRC_CIDR2": srcCIDR2, + "PROTOCOL_1": "SCTP", + "PROTOCOLTYPE1": "sctp", + "RANGE_1": portRangeSCTP, + "ACTION_1": "Deny", + "PROTOCOL_2": "TCP", + "PROTOCOLTYPE2": "tcp", + "RANGE_2": portRangeTCP, + "ACTION_2": "Deny", + }) + o.Expect(err).NotTo(o.HaveOccurred()) + + g.By("Verify SCTP traffic is blocked") + startSCTPServerListener(ctx, oc, ns, "sctpserver", "30102") + err = sendSCTPTraffic(ctx, oc, ns, "sctpclient", sctpServerIP, "30102") + o.Expect(err).To(o.HaveOccurred(), "SCTP traffic should be denied") + + g.By("Verify TCP traffic is blocked") + curlPod2PodFail(ctx, oc, ns, "hello-pod-client", ns, "hello-pod-server") + }) + + g.It("[OTP] 73844-Check Ingress Node Firewall functionality for blocking SSH traffic", + g.Serial, func() { + + ipStackType := checkIPStackType(ctx, oc) + o.Expect(ipStackType).NotTo(o.BeEmpty()) + if ipStackType == "dualstack" { + g.Skip("This case requires single stack cluster IPv4/IPv6") + } + + workers := getWorkerNodes(ctx, oc) + o.Expect(len(workers)).To(o.BeNumerically(">=", 2), "need at least 2 worker nodes") + + g.By("Create INFW config") + createINFWConfig(ctx, oc) + g.DeferCleanup(func() { deleteINFWConfig(ctx, oc) }) + + primaryInf := getPrimaryNIC(ctx, oc) + + g.By("Create INFW CR blocking SSH port 22") + srcCIDR := "0.0.0.0/0" + if ipStackType == "ipv6single" { + srcCIDR = "::/0" + } + err := applyTemplateFile(ctx, oc, "infw.yaml", map[string]string{ + "NAME": "infw-block-ssh", + "PRIMARY_INF": primaryInf, + "SRC_CIDR1": srcCIDR, + "PROTOCOL_1": "TCP", + "PROTOCOLTYPE1": "tcp", + "RANGE_1": "22", + "ACTION_1": "Deny", + "PROTOCOL_2": "TCP", + "PROTOCOLTYPE2": "tcp", + "ACTION_2": "Allow", + }) + o.Expect(err).NotTo(o.HaveOccurred()) + g.DeferCleanup(func() { deleteINFWCR(ctx, oc, "infw-block-ssh") }) + + g.By("Attempt SSH from master to worker - expect timeout") + masterNode := getMasterNode(ctx, oc) + sshCmd := "ssh -o ConnectTimeout=1 -o StrictHostKeyChecking=no core@" + workers[0] + " 2>&1 || true" + sshOutput, _ := debugNodeWithChroot(ctx, oc, masterNode, "/bin/bash", "-c", sshCmd) + o.Expect(strings.Contains(sshOutput, "Connection timed out")).Should(o.BeTrue(), + "SSH should timeout due to firewall rule") + + g.By("Verify Drop events with dstPort 22") + infwDaemon := getINFWDaemonForNode(ctx, oc, workers[0]) + checkDropEventsWithPort(ctx, oc, infwDaemon, "22") + }) }) diff --git a/test/otp/testdata/infw-config.yaml b/test/otp/testdata/infw-config.yaml new file mode 100644 index 000000000..096673455 --- /dev/null +++ b/test/otp/testdata/infw-config.yaml @@ -0,0 +1,12 @@ +apiVersion: ingressnodefirewall.openshift.io/v1alpha1 +kind: IngressNodeFirewallConfig +metadata: + name: ingressnodefirewallconfig + namespace: {{NAMESPACE}} +spec: + nodeSelector: + node-role.kubernetes.io/worker: "" + tolerations: + - key: "Example" + operator: "Exists" + effect: "NoExecute" diff --git a/test/otp/testdata/infw-icmp.yaml b/test/otp/testdata/infw-icmp.yaml new file mode 100644 index 000000000..b8f91d599 --- /dev/null +++ b/test/otp/testdata/infw-icmp.yaml @@ -0,0 +1,26 @@ +apiVersion: ingressnodefirewall.openshift.io/v1alpha1 +kind: IngressNodeFirewall +metadata: + name: {{NAME}} +spec: + interfaces: + - {{PRIMARY_INF}} + nodeSelector: + matchLabels: + node-role.kubernetes.io/worker: "" + ingress: + - sourceCIDRs: + - {{SRC_CIDR}} + rules: + - order: 1 + protocolConfig: + protocol: ICMP + icmp: + icmpType: 8 + action: {{ACTION_1}} + - order: 2 + protocolConfig: + protocol: ICMP + icmp: + icmpType: 8 + action: {{ACTION_2}} diff --git a/test/otp/testdata/infw-icmpv6.yaml b/test/otp/testdata/infw-icmpv6.yaml new file mode 100644 index 000000000..d59015e2a --- /dev/null +++ b/test/otp/testdata/infw-icmpv6.yaml @@ -0,0 +1,26 @@ +apiVersion: ingressnodefirewall.openshift.io/v1alpha1 +kind: IngressNodeFirewall +metadata: + name: {{NAME}} +spec: + interfaces: + - {{PRIMARY_INF}} + nodeSelector: + matchLabels: + node-role.kubernetes.io/worker: "" + ingress: + - sourceCIDRs: + - {{SRC_CIDR}} + rules: + - order: 1 + protocolConfig: + protocol: ICMPv6 + icmpv6: + icmpType: 128 + action: {{ACTION_1}} + - order: 2 + protocolConfig: + protocol: ICMPv6 + icmpv6: + icmpType: 128 + action: {{ACTION_2}} diff --git a/test/otp/testdata/infw-multiple-cidr.yaml b/test/otp/testdata/infw-multiple-cidr.yaml new file mode 100644 index 000000000..bc75daeba --- /dev/null +++ b/test/otp/testdata/infw-multiple-cidr.yaml @@ -0,0 +1,27 @@ +apiVersion: ingressnodefirewall.openshift.io/v1alpha1 +kind: IngressNodeFirewall +metadata: + name: {{NAME}} +spec: + interfaces: + - {{PRIMARY_INF}} + nodeSelector: + matchLabels: + node-role.kubernetes.io/worker: "" + ingress: + - sourceCIDRs: + - {{SRC_CIDR1}} + - {{SRC_CIDR2}} + rules: + - order: 1 + protocolConfig: + protocol: {{PROTOCOL_1}} + {{PROTOCOLTYPE1}}: + ports: "{{RANGE_1}}" + action: {{ACTION_1}} + - order: 2 + protocolConfig: + protocol: {{PROTOCOL_2}} + {{PROTOCOLTYPE2}}: + ports: "{{RANGE_2}}" + action: {{ACTION_2}} diff --git a/test/otp/testdata/infw.yaml b/test/otp/testdata/infw.yaml new file mode 100644 index 000000000..844271db5 --- /dev/null +++ b/test/otp/testdata/infw.yaml @@ -0,0 +1,26 @@ +apiVersion: ingressnodefirewall.openshift.io/v1alpha1 +kind: IngressNodeFirewall +metadata: + name: {{NAME}} +spec: + interfaces: + - {{PRIMARY_INF}} + nodeSelector: + matchLabels: + node-role.kubernetes.io/worker: "" + ingress: + - sourceCIDRs: + - {{SRC_CIDR1}} + rules: + - order: 1 + protocolConfig: + protocol: {{PROTOCOL_1}} + {{PROTOCOLTYPE1}}: + ports: "{{RANGE_1}}" + action: {{ACTION_1}} + - order: 2 + protocolConfig: + protocol: {{PROTOCOL_2}} + {{PROTOCOLTYPE2}}: + ports: "{{RANGE_1}}" + action: {{ACTION_2}} diff --git a/test/otp/testdata/pod-on-node.yaml b/test/otp/testdata/pod-on-node.yaml new file mode 100644 index 000000000..754c6e046 --- /dev/null +++ b/test/otp/testdata/pod-on-node.yaml @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{NAME}} + namespace: {{NAMESPACE}} + labels: + name: hello-pod +spec: + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + containers: + - image: "quay.io/openshifttest/hello-sdn@sha256:c89445416459e7adea9a5a416b3365ed3d74f2491beb904d61dc8d1eb89a72a4" + name: hello-pod + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + nodeName: {{NODENAME}} diff --git a/test/otp/testdata/sctp-client.yaml b/test/otp/testdata/sctp-client.yaml new file mode 100644 index 000000000..232d6be62 --- /dev/null +++ b/test/otp/testdata/sctp-client.yaml @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: Pod +metadata: + name: sctpclient + namespace: {{NAMESPACE}} + labels: + name: sctpclient +spec: + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + containers: + - name: sctpclient + image: quay.io/openshifttest/hello-sdn@sha256:c89445416459e7adea9a5a416b3365ed3d74f2491beb904d61dc8d1eb89a72a4 + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + nodeName: {{NODENAME}} diff --git a/test/otp/testdata/sctp-module.yaml b/test/otp/testdata/sctp-module.yaml new file mode 100644 index 000000000..00b10eb80 --- /dev/null +++ b/test/otp/testdata/sctp-module.yaml @@ -0,0 +1,23 @@ +apiVersion: machineconfiguration.openshift.io/v1 +kind: MachineConfig +metadata: + labels: + machineconfiguration.openshift.io/role: worker + name: load-sctp-module +spec: + config: + ignition: + version: 2.2.0 + storage: + files: + - contents: + source: data:, + verification: {} + filesystem: root + mode: 420 + path: /etc/modprobe.d/sctp-blacklist.conf + - contents: + source: data:text/plain;charset=utf-8,sctp + filesystem: root + mode: 420 + path: /etc/modules-load.d/sctp-load.conf diff --git a/test/otp/testdata/sctp-server.yaml b/test/otp/testdata/sctp-server.yaml new file mode 100644 index 000000000..e53c910f1 --- /dev/null +++ b/test/otp/testdata/sctp-server.yaml @@ -0,0 +1,24 @@ +apiVersion: v1 +kind: Pod +metadata: + name: sctpserver + namespace: {{NAMESPACE}} + labels: + name: sctpserver +spec: + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + containers: + - name: sctpserver + image: quay.io/openshifttest/hello-sdn@sha256:c89445416459e7adea9a5a416b3365ed3d74f2491beb904d61dc8d1eb89a72a4 + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + ports: + - containerPort: 30102 + name: sctpserver + protocol: SCTP + nodeName: {{NODENAME}} diff --git a/test/otp/testdata/udp-listener.yaml b/test/otp/testdata/udp-listener.yaml new file mode 100644 index 000000000..6591ef33a --- /dev/null +++ b/test/otp/testdata/udp-listener.yaml @@ -0,0 +1,28 @@ +apiVersion: v1 +kind: Pod +metadata: + name: udp-pod + namespace: {{NAMESPACE}} + labels: + name: udp-pod +spec: + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + containers: + - name: udp-pod + image: quay.io/openshifttest/hello-sdn@sha256:c89445416459e7adea9a5a416b3365ed3d74f2491beb904d61dc8d1eb89a72a4 + command: + - "/usr/bin/ncat" + - "-u" + - "-l" + - "8080" + - "--keep-open" + - "--exec" + - "/bin/cat" + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + restartPolicy: Always