diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..e3a5727b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +--- +version: 2 +updates: + - package-ecosystem: gomod + directory: / + schedule: + interval: daily + - package-ecosystem: docker + directory: / + schedule: + interval: daily + - package-ecosystem: github-actions + directory: / + schedule: + interval: daily diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..44920589 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,103 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@v6 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - uses: golangci/golangci-lint-action@v7 + with: + version: v2.0.2 + args: -v + + build: + needs: lint + strategy: + matrix: + os: [ ubuntu-latest ] + goos: [ linux ] + goarch: [amd64, arm64, ppc64le] + runs-on: ${{ matrix.os }} + env: + GO111MODULE: on + + steps: + - uses: actions/checkout@v6 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Build test for ${{ matrix.goarch }} + env: + GOARCH: ${{ matrix.goarch }} + GOOS: ${{ matrix.goos }} + run: GOARCH="${TARGET}" go build ./cmd/main.go + + test-unit: + name: Run tests on Linux amd64 + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Run tests + run: sudo make test + + test-e2e: + name: Run e2e tests + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Install bats + run: sudo apt install bats + - name: Setup registry + run: docker run -d --restart=always -p "5000:5000" --name "kind-registry" registry:2 + + - name: Get tools + working-directory: ./e2e + run: ./get_tools.sh + + - name: Setup cluster + working-directory: ./e2e + run: ./setup_cluster.sh + + - name: "Test: simple" + working-directory: ./e2e + run: | + export TERM=dumb + # enable ip6_tables + sudo modprobe ip6_tables + + ./run_all_tests.sh + + - name: Upload logs + uses: actions/upload-artifact@v6 + if: ${{ failure() }} + with: + name: kind-logs-e2e + path: ./e2e/artifacts/ diff --git a/.gitignore b/.gitignore index 5102b9a3..03d03504 100644 --- a/.gitignore +++ b/.gitignore @@ -1,18 +1,28 @@ -# Binary output dir -bin/ -e2e/bin/ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +bin/* +Dockerfile.cross -# binary at the top -/multi-networkpolicy-iptables +# Test binary, built with `go test -c` +*.test -# GOPATH created by the build script -gopath/ +# Output of the go coverage tool, specifically when used with LiteIDE +*.out +coverage.html -# Editor paths -.swp* -.swo* -.idea* +# Go workspace file +go.work -# Test outputs -*.out -*.test +# Kubernetes Generated files - skip generated files, except for vendored files +!vendor/**/zz_generated.* + +# editor and IDE paraphernalia +.idea +.vscode +*.swp +*.swo +*~ diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 00000000..5955d0b8 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,63 @@ + +version: "2" +linters: + enable: + - contextcheck + - durationcheck + - forbidigo + - ginkgolinter + - gocritic + - misspell + - nonamedreturns + - predeclared + - revive + - unconvert + - unparam + - wastedassign + disable: + - errcheck + settings: + staticcheck: + checks: + - all + - '-QF1008' # nested struct reference + - '-ST1005' # capitalized error strings + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + rules: + - linters: + - revive + - staticcheck + text: use ALL_CAPS in Go names; use CamelCase + - linters: + - revive + text: ' and that stutters;' + - path: (.+)_test\.go + text: 'dot-imports: should not use dot imports' + - path: (.+)_test\.go + text: "ginkgo-linter: wrong comparison assertion. Consider using (.+)BeZero(.+)" + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - gci + - gofumpt + settings: + gci: + sections: + - standard + - default + - prefix(github.com/k8snetworkplumbingwg/multi-network-policy-nftables) + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/.snyk b/.snyk index 997627cd..09fed827 100644 --- a/.snyk +++ b/.snyk @@ -4,10 +4,4 @@ exclude: global: - "**/*_test.go" - - vendor/github.com/google/uuid - - vendor/github.com/Microsoft/go-winio/pkg/guid - - vendor/golang.org/x/tools/cmd/stringer - - vendor/golang.org/x/tools/internal/pkgbits - - vendor/k8s.io/client-go/util/cert - - vendor/k8s.io/klog - - vendor/k8s.io/klog/v2 + - vendor/** diff --git a/Dockerfile b/Dockerfile index d484fced..f3ebccf9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,17 +1,21 @@ -# This Dockerfile is used to build the image available on DockerHub -FROM golang:1.24 as build +FROM golang:1.24 as builder +ARG TARGETOS +ARG TARGETARCH -# Add everything -ADD . /usr/src/multi-networkpolicy-iptables +WORKDIR /workspace -RUN cd /usr/src/multi-networkpolicy-iptables && \ - CGO_ENABLED=0 go build ./cmd/multi-networkpolicy-iptables/ +COPY go.mod go.mod +COPY go.sum go.sum +RUN go mod download -FROM fedora:38 -LABEL org.opencontainers.image.source https://github.com/k8snetworkplumbingwg/multi-networkpolicy-iptables -RUN dnf install -y iptables-utils iptables-legacy iptables-nft -RUN alternatives --set iptables /usr/sbin/iptables-nft -COPY --from=build /usr/src/multi-networkpolicy-iptables/multi-networkpolicy-iptables /usr/bin -WORKDIR /usr/bin +COPY cmd/main.go cmd/main.go +COPY pkg/ pkg/ -ENTRYPOINT ["multi-networkpolicy-iptables"] +RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o multi-networkpolicy-nftables cmd/main.go + +FROM fedora:42 +WORKDIR / + +RUN dnf install -y nftables +COPY --from=builder /workspace/multi-networkpolicy-nftables . +ENTRYPOINT ["/multi-networkpolicy-nftables"] diff --git a/Dockerfile.openshift b/Dockerfile.openshift index ae7e8dbb..15dd43ec 100644 --- a/Dockerfile.openshift +++ b/Dockerfile.openshift @@ -2,14 +2,14 @@ FROM registry.ci.openshift.org/ocp/builder:rhel-9-golang-1.24-openshift-4.22 AS build # Add everything -ADD . /usr/src/multi-networkpolicy-iptables -WORKDIR /usr/src/multi-networkpolicy-iptables -RUN CGO_ENABLED=0 go build ./cmd/multi-networkpolicy-iptables/ +ADD . /usr/src/multus-networkpolicy +WORKDIR /usr/src/multus-networkpolicy +RUN CGO_ENABLED=0 go build -a -o multi-networkpolicy-nftables ./cmd/ FROM registry.ci.openshift.org/ocp/4.22:base-rhel9 -LABEL org.opencontainers.image.source https://github.com/k8snetworkplumbingwg/multi-networkpolicy-iptables -RUN dnf install -y iptables -COPY --from=build /usr/src/multi-networkpolicy-iptables/multi-networkpolicy-iptables /usr/bin +LABEL org.opencontainers.image.source https://github.com/openshift/multus-networkpolicy +RUN dnf install -y nftables && dnf clean all +COPY --from=build /usr/src/multus-networkpolicy/multi-networkpolicy-nftables /usr/bin WORKDIR /usr/bin LABEL io.k8s.display-name="Multus NetworkPolicy" \ @@ -17,4 +17,7 @@ LABEL io.k8s.display-name="Multus NetworkPolicy" \ io.openshift.tags="openshift" \ maintainer="Doug Smith " -ENTRYPOINT ["multi-networkpolicy-iptables"] +# TODO: compatibility layer with the original multus-networkpolicy-iptables image. Remove this once the ClsuterNetworkOperator is updated to use the nftables implementation. +RUN ln -s /usr/bin/multi-networkpolicy-nftables /usr/bin/multi-networkpolicy-iptables + +ENTRYPOINT ["multi-networkpolicy-nftables"] diff --git a/LICENSE b/LICENSE index 261eeb9e..8170952b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,201 +1,204 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - 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. +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (which shall not include communications that are solely written + by You). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based upon (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and derivative works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control + systems, and issue tracking systems that are managed by, or on behalf + of, the Licensor for the purpose of discussing and improving the Work, + but excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to use, reproduce, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Work, and to + permit persons to whom the Work is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Work. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, trademark, patent, + attribution and other notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright notice to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Support. When redistributing the Work or + Derivative Works thereof, You may choose to offer, and charge a fee + for, acceptance of support, warranty, indemnity, or other liability + obligations and/or rights consistent with this License. However, in + accepting such obligations, You may act only on Your own behalf and on + Your sole responsibility, not on behalf of any other Contributor, and + only if You agree to indemnify, defend, and hold each Contributor + harmless for any liability incurred by, or claims asserted against, + such Contributor by reason of your accepting any such warranty or support. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same page as the copyright notice for easier identification within + third-party archives. + +Copyright [yyyy] [name of copyright owner] + +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. diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..37f9b0a7 --- /dev/null +++ b/Makefile @@ -0,0 +1,31 @@ +ENVTEST_VERSION ?= release-0.17 + +## Location to install dependencies to +LOCALBIN ?= $(shell pwd)/bin +$(LOCALBIN): + mkdir -p $(LOCALBIN) + +ENVTEST ?= $(LOCALBIN)/setup-envtest-$(ENVTEST_VERSION) + +# go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist +# $1 - target path with name of binary (ideally with version) +# $2 - package url which can be installed +# $3 - specific version of package +define go-install-tool +@[ -f $(1) ] || { \ +set -e; \ +package=$(2)@$(3) ;\ +echo "Downloading $${package}" ;\ +GOBIN=$(LOCALBIN) GOFLAGS="" go install $${package} ;\ +mv "$$(echo "$(1)" | sed "s/-$(3)$$//")" $(1) ;\ +} +endef + +.PHONY: envtest +envtest: $(ENVTEST) ## Download setup-envtest locally if necessary. +$(ENVTEST): $(LOCALBIN) + $(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest,$(ENVTEST_VERSION)) + +.PHONY: test +test: envtest ## Run tests. + KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test $$(go list ./... | grep -v /e2e) -coverprofile cover.out diff --git a/NOTICE b/NOTICE deleted file mode 100644 index dd3fc395..00000000 --- a/NOTICE +++ /dev/null @@ -1 +0,0 @@ -Copyright 2020 Kubernetes Network Plumbing Working Group diff --git a/OWNERS b/OWNERS index 9a07eedf..f5d0017e 100644 --- a/OWNERS +++ b/OWNERS @@ -13,3 +13,4 @@ approvers: - bpickard22 - pliurh - zeeke +component: "Networking" diff --git a/README.md b/README.md index 1b1b49fb..5fda5c9b 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,159 @@ -# multi-networkpolicy-iptables -[![build](https://github.com/k8snetworkplumbingwg/multi-networkpolicy-iptables/actions/workflows/build.yml/badge.svg)](https://github.com/k8snetworkplumbingwg/multi-networkpolicy-iptables/actions/workflows/build.yml)[![test](https://github.com/k8snetworkplumbingwg/multi-networkpolicy-iptables/actions/workflows/test.yml/badge.svg)](https://github.com/k8snetworkplumbingwg/multi-networkpolicy-iptables/actions/workflows/test.yml) +# Multi-Network Policy for NFTables -[multi-networkpolicy](https://github.com/k8snetworkplumbingwg/multi-networkpolicy) implementation with iptables +A Kubernetes controller that enforces MultiNetworkPolicy resources using nftables on Linux nodes. This project enables fine-grained, declarative network security for pods with multiple network interfaces. -## Current Status of the Repository +## The Problem: A Security Gap in Multi-Network Pods -It is now actively developping hence not stable yet. Bug report and feature request are welcome. +Standard Kubernetes Network Policies provide essential firewall capabilities for pod-to-pod communication on the primary cluster network. -## Description +However, when you use Network Attachment Definitions (net-attach-def) to connect pods to additional, specialized networks (e.g., for telco, storage, or high-speed data), a security gap emerges. Because these attachments are Custom Resource Definitions (CRDs), the native Kubernetes Network Policy controller is unaware of them, leaving traffic on these secondary interfaces unprotected. -Kubernetes provides [Network Policies](https://kubernetes.io/docs/concepts/services-networking/network-policies/) for network security. Currently net-attach-def does not support Network Policies because net-attach-def is CRD, user defined resources, outside of Kubernetes. -multi-network policy implements Network Policiy functionality for net-attach-def, by iptables and provies network security for net-attach-def networks. +## The Solution: MultiNetworkPolicy -## Quickstart +multi-networkpolicy-nftables bridges this gap. It introduces the MultiNetworkPolicy Custom Resource Definition and a controller that enforces these policies specifically for secondary networks. By leveraging the modern nftables framework on each node, it protects traffic that standard policies cannot see. -Install MultiNetworkPolicy CRD into Kubernetes. +## How It Works +The controller operates as a Kubernetes DaemonSet, running an agent on every node in the cluster. This agent: + +- Watches for MultiNetworkPolicy objects defined in the cluster. +- Identifies the target net-attach-def for each policy via the `k8s.v1.cni.cncf.io/policy-for` annotation. +- Generates the corresponding nftables rules based on the policy specification. +- Injects these rules directly into the target pod's network namespace, ensuring policies are isolated and do not interfere with the host or other pods. + +## Key Features + +- **Declarative, Namespace-Scoped Policies**: Manage security for secondary networks using the same familiar Kubernetes-style declarative model. +- **High-Performance Packet Filtering**: Utilizes the modern and efficient nftables kernel subsystem. +- **Seamless Integration**: Works with popular CNI plugins used for creating secondary networks. +- **CRD-Based**: Extends the Kubernetes API without modifying core components. + +## Getting Started + +### 1. Prerequisites + +This controller requires the `nf_tables` kernel module to be loaded on all container hosts (nodes). + +```bash +# Verify the module is loaded +lsmod | grep nf_tables + +# If not loaded, load it now +sudo modprobe nf_tables ``` -$ git clone https://github.com/k8snetworkplumbingwg/multi-networkpolicy -$ cd multi-networkpolicy -$ kubectl create -f scheme.yml -customresourcedefinition.apiextensions.k8s.io/multi-networkpolicies.k8s.cni.cncf.io created + +### 2. Install the MultiNetworkPolicy CRD + +First, apply the scheme to your cluster to create the MultiNetworkPolicy resource type. + +```bash +kubectl apply -f https://raw.githubusercontent.com/k8snetworkplumbingwg/multi-networkpolicy/master/scheme.yml ``` -Deploy multi-networkpolicie-iptables into Kubernetes. +Expected Output: ``` -$ git clone https://github.com/k8snetworkplumbingwg/multi-networkpolicy-iptables -$ cd multi-networkpolicy-iptables -$ kubectl create -f deploy.yml -clusterrole.rbac.authorization.k8s.io/multi-networkpolicy created -clusterrolebinding.rbac.authorization.k8s.io/multi-networkpolicy created -serviceaccount/multi-networkpolicy created -daemonset.apps/multi-networkpolicy-ds-amd64 created +customresourcedefinition.apiextensions.k8s.io/multinetworkpolicies.k8s.cni.cncf.io created ``` -## Requirements +### 3. Deploy the Controller -This project leverages `iptables` and `ip6tables` commands to do its work. Hence, `ip_tables` and `ip6_tables` kernel modules -need to be loaded on the container host: +Next, deploy the multi-networkpolicy-nftables DaemonSet, which will run the controller on each node. +```bash +kubectl apply -f https://raw.githubusercontent.com/k8snetworkplumbingwg/multi-networkpolicy-nftables/master/deploy.yaml ``` -# modprobe ip_tables ip6_tables + +Expected Output: + +``` +clusterrole.rbac.authorization.k8s.io/multi-networkpolicy-nftables created +clusterrolebinding.rbac.authorization.k8s.io/multi-networkpolicy-nftables created +serviceaccount/multi-networkpolicy-nftables created +daemonset.apps/multi-networkpolicy-nftables created ``` -## Configurations +### 4. Apply an Example Policy + +Save the following YAML to a file named `web-policy.yaml`. This policy targets pods with the label `app: web` on the `macvlan-network` secondary interface. + +```yaml +apiVersion: k8s.cni.cncf.io/v1beta1 +kind: MultiNetworkPolicy +metadata: + name: web-policy + namespace: default + annotations: + k8s.v1.cni.cncf.io/policy-for: "macvlan-network" +spec: + podSelector: + matchLabels: + app: web + policyTypes: + - Ingress + - Egress + ingress: + - from: + - podSelector: + matchLabels: + app: frontend + - ipBlock: + cidr: 10.0.0.0/8 + except: + - 10.0.1.0/24 + ports: + - protocol: TCP + port: 8080 + egress: + - to: + - namespaceSelector: + matchLabels: + name: database + ports: + - protocol: TCP + port: 5432 +``` + +Apply it to your cluster: + +```bash +kubectl apply -f web-policy.yaml +``` + +## Configuration + +### Supported CNI Plugins + +The controller validates that the target net-attach-def uses one of the following supported CNI plugins: + +- `macvlan` +- `ipvlan` +- `sriov` -See [Configurations](docs/configurations.md). +### Controller Flags -## Demo +The controller supports the following command-line flags for customization: -(TBD) +- `--hostname-override`: The hostname to use for the node. If not set, it's determined automatically. +- `--network-plugins`: Comma-separated list of CNI plugins to be considered for policies (default: "macvlan"). +- `--container-runtime-endpoint`: Path to the CRI socket (e.g., `/run/containerd/containerd.sock`). This is a required flag. +- `--host-prefix`: If non-empty, prefixes filesystem paths for chroot environments. +- `--accept-icmp`: If true, allows all ICMP traffic (default: false). +- `--accept-icmpv6`: If true, allows all ICMPv6 traffic (default: false). +- `--custom-v4-ingress-rule-file`: Path to a custom rule file for IPv4 ingress. +- `--custom-v4-egress-rule-file`: Path to a custom rule file for IPv4 egress. +- `--custom-v6-ingress-rule-file`: Path to a custom rule file for IPv6 ingress. +- `--custom-v6-egress-rule-file`: Path to a custom rule file for IPv6 egress. -### MultiNetworkPolicy DaemonSet +## Documentation -MultiNetworkPolicy creates DaemonSet and it runs `multi-networkpolicy-iptables` for each node. `multi-networkpolicy-iptables` watches MultiNetworkPolicy object and creates iptables rules into 'pod's network namespace', not container host and the iptables rules filters packets to interface, based on MultiNetworkPolicy. +For a more detailed technical design, please see the [NFTables Design Document](./docs/nftables.md). -## TODO +## License -* Bugfixing -* IPv6 support -* (TBD) +This project is licensed under the Apache License 2.0. See the LICENSE file for details. -## Contact Us +## Acknowledgments -For any questions about Multus CNI, feel free to ask a question in #general in the [NPWG Slack](https://npwg-team.slack.com/), or open up a GitHub issue. Request an invite to NPWG slack [here](https://intel-corp.herokuapp.com/). +- Built on top of the excellent [knftables](https://github.com/kubernetes-sigs/knftables) library. +- Inspired by the [multi-networkpolicy-iptables](https://github.com/k8snetworkplumbingwg/multi-networkpolicy-iptables) project. diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 00000000..10a7e518 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,214 @@ +package main + +import ( + "flag" + "fmt" + "os" + + multinetworkscheme "github.com/k8snetworkplumbingwg/multi-networkpolicy/pkg/client/clientset/versioned/scheme" + netdefscheme "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/client/clientset/versioned/scheme" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + nodeutil "k8s.io/component-helpers/node/util" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + "github.com/k8snetworkplumbingwg/multi-network-policy-nftables/pkg/controller" + "github.com/k8snetworkplumbingwg/multi-network-policy-nftables/pkg/cri" + "github.com/k8snetworkplumbingwg/multi-network-policy-nftables/pkg/datastore" + "github.com/k8snetworkplumbingwg/multi-network-policy-nftables/pkg/nftables" + "github.com/k8snetworkplumbingwg/multi-network-policy-nftables/pkg/utils" +) + +var ( + scheme = runtime.NewScheme() + setupLog = ctrl.Log.WithName("setup") +) + +func init() { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + utilruntime.Must(multinetworkscheme.AddToScheme(scheme)) + utilruntime.Must(netdefscheme.AddToScheme(scheme)) +} + +func main() { + if err := run(); err != nil { + setupLog.Error(err, "an error occurred") + os.Exit(1) + } +} + +func run() error { + var hostnameOverride string + var networkPlugins string + var criEndpoint string + var hostPrefix string + var acceptICMP bool + var acceptICMPv6 bool + var customIPv4IngressRuleFile string + var customIPv4EgressRuleFile string + var customIPv6IngressRuleFile string + var customIPv6EgressRuleFile string + + flag.StringVar(&hostnameOverride, "hostname-override", "", "The hostname to use for the node. If not set, the hostname will be determined by the node controller.") + flag.StringVar(&networkPlugins, "network-plugins", "macvlan", "Comma-separated list of network plugins to be considered for network policies.") + flag.StringVar(&criEndpoint, "container-runtime-endpoint", "", "Path to cri socket.") + flag.StringVar(&hostPrefix, "host-prefix", "", "If non-empty, will use this string as prefix for host filesystem.") + flag.BoolVar(&acceptICMP, "accept-icmp", false, "accept all ICMP traffic") + flag.BoolVar(&acceptICMPv6, "accept-icmpv6", false, "accept all ICMPv6 traffic") + flag.StringVar(&customIPv4IngressRuleFile, "custom-v4-ingress-rule-file", "", "custom rule file for IPv4 ingress") + flag.StringVar(&customIPv4EgressRuleFile, "custom-v4-egress-rule-file", "", "custom rule file for IPv4 egress") + flag.StringVar(&customIPv6IngressRuleFile, "custom-v6-ingress-rule-file", "", "custom rule file for IPv6 ingress") + flag.StringVar(&customIPv6EgressRuleFile, "custom-v6-egress-rule-file", "", "custom rule file for IPv6 egress") + + // TODO: compatibility layer with the multus-networkpolicy-iptables image. Remove this once the ClsuterNetworkOperator is updated to use the nftables implementation. + var podIptables string + flag.StringVar(&podIptables, "pod-iptables", "", "compatibility layer") + + opts := zap.Options{ + Development: true, + } + opts.BindFlags(flag.CommandLine) + flag.Parse() + + ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + + setupLog.Info("Starting multi-network-policy-nftables") + + hostname, err := nodeutil.GetHostname(hostnameOverride) + if err != nil { + return fmt.Errorf("unable to get hostname: %w", err) + } + setupLog.Info("Handling pods for", "node", hostname) + + if criEndpoint == "" { + return fmt.Errorf("container-runtime-endpoint must be set") + } + + // Process network plugins flag + plugins, err := utils.ParseCommaSeparatedList(networkPlugins) + if err != nil { + return fmt.Errorf("unable to parse network plugins: %w", err) + } + + if len(plugins) == 0 { + return fmt.Errorf("at least one network plugin must be specified") + } + + setupLog.Info("Valid network plugins", "plugins", plugins) + + // Get custom nftables rules + commonRules, err := getCustomRules(customIPv4IngressRuleFile, customIPv4EgressRuleFile, customIPv6IngressRuleFile, customIPv6EgressRuleFile) + if err != nil { + return fmt.Errorf("unable to get custom nftables rules: %w", err) + } + + // TODO: compatibility layer with the multus-networkpolicy-iptables image. Remove this once the ClsuterNetworkOperator is updated to use the nftables implementation. + commonRules = &nftables.CommonRules{} + + // Set ICMP acceptance rules + commonRules.AcceptICMP = acceptICMP + commonRules.AcceptICMPv6 = acceptICMPv6 + + // TODO: put this in ClusterNetworkOperator + commonRules.CustomIPv6EgressRules = []string{ + "icmpv6 type nd-neighbor-solicit accept", + "icmpv6 type nd-neighbor-advert accept", + "icmpv6 type nd-router-advert accept", + "icmpv6 type nd-router-solicit accept", + } + commonRules.CustomIPv6IngressRules = []string{ + "icmpv6 type nd-neighbor-solicit accept", + "icmpv6 type nd-neighbor-advert accept", + "icmpv6 type nd-router-advert accept", + "icmpv6 type nd-router-solicit accept", + } + + setupLog.Info("Common rules applied to all pods affected by MultiNetworkPolicies", "rules", commonRules) + + ctx := ctrl.SetupSignalHandler() + + criRuntime := cri.New(criEndpoint, hostPrefix) + if err := criRuntime.Connect(ctx); err != nil { + return fmt.Errorf("unable to connect to cri runtime: %w", err) + } + defer criRuntime.Close() + + // Create manager + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + Scheme: scheme, + LeaderElection: false, + }) + if err != nil { + return fmt.Errorf("unable to start manager: %w", err) + } + + ds := &datastore.Datastore{ + Policies: make(map[types.NamespacedName]*datastore.Policy), + } + + nft := &nftables.NFTables{ + Client: mgr.GetClient(), + Hostname: hostname, + CriRuntime: criRuntime, + CommonRules: commonRules, + } + + if err = (&controller.MultiNetworkReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + DS: ds, + NFT: nft, + ValidPlugins: plugins, + }).SetupWithManager(mgr); err != nil { + return fmt.Errorf("unable to create controller: %w", err) + } + + setupLog.Info("starting manager") + if err = mgr.Start(ctx); err != nil { + return fmt.Errorf("problem running manager: %w", err) + } + + return nil +} + +// getCustomRules reads custom nftables rules from the provided files and returns a CommonRules struct +func getCustomRules(customIPv4IngressRuleFile, customIPv4EgressRuleFile, customIPv6IngressRuleFile, customIPv6EgressRuleFile string) (*nftables.CommonRules, error) { + commonRules := &nftables.CommonRules{} + + if customIPv4IngressRuleFile != "" { + rules, err := utils.ReadRulesFromFile(customIPv4IngressRuleFile) + if err != nil { + return nil, fmt.Errorf("failed to read custom IPv4 ingress rules from file: %w", err) + } + commonRules.CustomIPv4IngressRules = rules + } + + if customIPv4EgressRuleFile != "" { + rules, err := utils.ReadRulesFromFile(customIPv4EgressRuleFile) + if err != nil { + return nil, fmt.Errorf("failed to read custom IPv4 egress rules from file: %w", err) + } + commonRules.CustomIPv4EgressRules = rules + } + + if customIPv6IngressRuleFile != "" { + rules, err := utils.ReadRulesFromFile(customIPv6IngressRuleFile) + if err != nil { + return nil, fmt.Errorf("failed to read custom IPv6 ingress rules from file: %w", err) + } + commonRules.CustomIPv6IngressRules = rules + } + + if customIPv6EgressRuleFile != "" { + rules, err := utils.ReadRulesFromFile(customIPv6EgressRuleFile) + if err != nil { + return nil, fmt.Errorf("failed to read custom IPv6 egress rules from file: %w", err) + } + commonRules.CustomIPv6EgressRules = rules + } + + return commonRules, nil +} diff --git a/cmd/multi-networkpolicy-iptables/main.go b/cmd/multi-networkpolicy-iptables/main.go deleted file mode 100644 index dd00cd9b..00000000 --- a/cmd/multi-networkpolicy-iptables/main.go +++ /dev/null @@ -1,91 +0,0 @@ -/* -Copyright 2020 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -// This is a Kubernetes controller to generate iptables rules for -// multi-networkpolicy. -// It reads multiNetworkpolicy object and generates iptables rules into -// container network namespaces. -package main - -import ( - //"flag" - "log" - "os" - "os/signal" - "syscall" - "time" - - "github.com/k8snetworkplumbingwg/multi-networkpolicy-iptables/pkg/server" - "github.com/spf13/cobra" - "github.com/spf13/pflag" - - "k8s.io/apimachinery/pkg/util/wait" - "k8s.io/klog" -) - -const logFlushFreqFlagName = "log-flush-frequency" - -var logFlushFreq = pflag.Duration(logFlushFreqFlagName, 5*time.Second, "Maximum number of seconds between log flushes") - -// KlogWriter serves as a bridge between the standard log package and the glog package. -type KlogWriter struct{} - -// Write implements the io.Writer interface. -func (writer KlogWriter) Write(data []byte) (n int, err error) { - klog.InfoDepth(1, string(data)) - return len(data), nil -} - -func initLogs() { - log.SetOutput(KlogWriter{}) - log.SetFlags(0) - go wait.Forever(klog.Flush, *logFlushFreq) -} - -func main() { - initLogs() - defer klog.Flush() - opts := server.NewOptions() - - cmd := &cobra.Command{ - Use: "multi-networkpolicy-node", - Long: `TBD`, - Run: func(cmd *cobra.Command, args []string) { - if err := opts.Run(); err != nil { - klog.Exit(err) - } - }, - } - opts.AddFlags(cmd.Flags()) - - signalCh := make(chan os.Signal, 16) - signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM) - go func() { - for sig := range signalCh { - klog.V(1).Infof("Caught %v, stopping...", sig) - opts.Stop() - } - }() - - klog.Infof("Executing ...") - - if err := cmd.Execute(); err != nil { - klog.Infof("Execute failed: %v", err) - os.Exit(1) - } - - klog.Infof("Exiting") -} diff --git a/demo/README.md b/demo/README.md deleted file mode 100644 index 5ab4afed..00000000 --- a/demo/README.md +++ /dev/null @@ -1,52 +0,0 @@ - -This demo assumes both working go & kind environments. For more information on -kind check: -https://kind.sigs.k8s.io/docs/user/quick-start/ - -Run a kind cluster: -``` -kind create cluster -``` - -Deploy multus: -``` -kubectl apply -f https://raw.githubusercontent.com/k8snetworkplumbingwg/multus-cni/master/deployments/multus-daemonset.yml -``` - -Deploy the MultiNetworkPolicy CRD: -``` -kubectl apply -f https://raw.githubusercontent.com/k8snetworkplumbingwg/multi-networkpolicy/master/scheme.yml -``` - -Deploy the multi-networkpolicy implementation with iptables: -``` -kubectl apply -f https://raw.githubusercontent.com/k8snetworkplumbingwg/multi-networkpolicy-iptables/master/demo/deploy.yml -``` - -Copy macvlan cni to the control plane node: -``` -curl -sSf -L --retry 5 https://github.com/containernetworking/plugins/releases/download/v1.5.0/cni-plugins-linux-amd64-v1.5.0.tgz | tar -xz -C . ./macvlan -... -docker cp macvlan kind-control-plane:/opt/cni/bin/ -``` - -Deploy a sample [network attachment definition](demo/net.yml), its -[policy](demo/policy.yml) and [pod](demo/alpine.yml) that attaches to that -network: -``` -kubectl apply -f https://raw.githubusercontent.com/k8snetworkplumbingwg/multi-networkpolicy-iptables/master/demo/net.yml -kubectl apply -f https://raw.githubusercontent.com/k8snetworkplumbingwg/multi-networkpolicy-iptables/master/demo/policy.yml -kubectl apply -f https://raw.githubusercontent.com/k8snetworkplumbingwg/multi-networkpolicy-iptables/master/demo/alpine.yml -``` - -You can the log in to the alpine pod and check the -[iptables rules](demo/iptables.log) that are enforcing the policy: -(note: this rule might be different from yours because we may change iptable generation rules...) - -``` -kubectl exec -ti alpine -- /bin/sh -... -apk update -apk add iptables -iptables -vL -``` diff --git a/demo/alpine.yml b/demo/alpine.yml deleted file mode 100644 index f8956bd0..00000000 --- a/demo/alpine.yml +++ /dev/null @@ -1,20 +0,0 @@ -apiVersion: v1 -kind: Pod -metadata: - name: alpine - namespace: default - annotations: - k8s.v1.cni.cncf.io/networks: macvlan-conf-1 -spec: - containers: - - image: alpine:3.2 - command: - - /bin/sh - - "-c" - - "sleep 60m" - imagePullPolicy: IfNotPresent - name: alpine - securityContext: - capabilities: - add: ["NET_RAW", "NET_ADMIN"] - restartPolicy: Always diff --git a/demo/deploy.yml b/demo/deploy.yml deleted file mode 100644 index e822f3da..00000000 --- a/demo/deploy.yml +++ /dev/null @@ -1,114 +0,0 @@ ---- -kind: ClusterRole -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - name: multi-networkpolicy -rules: - - apiGroups: ["k8s.cni.cncf.io"] - resources: - - '*' - verbs: - - '*' - - apiGroups: - - "" - resources: - - pods - - namespaces - verbs: - - list - - watch - - get - # Watch for changes to Kubernetes NetworkPolicies. - - apiGroups: ["networking.k8s.io"] - resources: - - networkpolicies - verbs: - - watch - - list - - apiGroups: - - "" - - events.k8s.io - resources: - - events - verbs: - - create - - patch - - update ---- -kind: ClusterRoleBinding -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - name: multi-networkpolicy -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: multi-networkpolicy -subjects: -- kind: ServiceAccount - name: multi-networkpolicy - namespace: kube-system ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - name: multi-networkpolicy - namespace: kube-system ---- -apiVersion: apps/v1 -kind: DaemonSet -metadata: - name: multi-networkpolicy-ds-amd64 - namespace: kube-system - labels: - tier: node - app: multi-networkpolicy - name: multi-networkpolicy -spec: - selector: - matchLabels: - name: multi-networkpolicy - updateStrategy: - type: RollingUpdate - template: - metadata: - labels: - tier: node - app: multi-networkpolicy - name: multi-networkpolicy - spec: - hostNetwork: true - nodeSelector: - kubernetes.io/arch: amd64 - tolerations: - - operator: Exists - effect: NoSchedule - serviceAccountName: multi-networkpolicy - containers: - - name: multi-networkpolicy - # crio support requires multus:latest for now. support 3.3 or later. - image: docker.io/nfvpe/multi-networkpolicy-iptables:snapshot-amd64 - imagePullPolicy: Always - command: ["/usr/bin/multi-networkpolicy-iptables"] - args: - - "--host-prefix=/host" - # uncomment this if runtime is docker - # - "--container-runtime=docker" - - "--container-runtime-endpoint=/run/containerd/containerd.sock" - resources: - requests: - cpu: "100m" - memory: "50Mi" - limits: - cpu: "100m" - memory: "50Mi" - securityContext: - privileged: true - capabilities: - add: ["SYS_ADMIN", "SYS_NET_ADMIN"] - volumeMounts: - - name: host - mountPath: /host - volumes: - - name: host - hostPath: - path: / diff --git a/demo/iptables.log b/demo/iptables.log deleted file mode 100644 index b00bc2cf..00000000 --- a/demo/iptables.log +++ /dev/null @@ -1,53 +0,0 @@ - # iptables -L -v -Chain INPUT (policy ACCEPT 0 packets, 0 bytes) - pkts bytes target prot opt in out source destination - 0 0 MULTI-INGRESS all -- net1 any anywhere anywhere - -Chain FORWARD (policy ACCEPT 0 packets, 0 bytes) - pkts bytes target prot opt in out source destination - -Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes) - pkts bytes target prot opt in out source destination - 0 0 MULTI-EGRESS all -- any net1 anywhere anywhere - -Chain MULTI-0-EGRESS (1 references) - pkts bytes target prot opt in out source destination - 0 0 MARK all -- any any anywhere anywhere MARK and 0xfffcffff - 0 0 MULTI-0-EGRESS-0-PORTS all -- any any anywhere anywhere - 0 0 MULTI-0-EGRESS-0-TO all -- any any anywhere anywhere - 0 0 RETURN all -- any any anywhere anywhere mark match 0x30000/0x30000 - 0 0 DROP all -- any any anywhere anywhere - -Chain MULTI-0-EGRESS-0-PORTS (1 references) - pkts bytes target prot opt in out source destination - 0 0 MARK tcp -- any net1 anywhere anywhere tcp dpt:5978 MARK or 0x10000 - -Chain MULTI-0-EGRESS-0-TO (1 references) - pkts bytes target prot opt in out source destination - 0 0 MARK all -- any net1 anywhere 10.0.0.0/24 MARK or 0x20000 - -Chain MULTI-0-INGRESS (1 references) - pkts bytes target prot opt in out source destination - 0 0 MARK all -- any any anywhere anywhere MARK and 0xfffcffff - 0 0 MULTI-0-INGRESS-0-PORTS all -- any any anywhere anywhere - 0 0 MULTI-0-INGRESS-0-FROM all -- any any anywhere anywhere - 0 0 RETURN all -- any any anywhere anywhere mark match 0x30000/0x30000 - 0 0 DROP all -- any any anywhere anywhere - -Chain MULTI-0-INGRESS-0-FROM (1 references) - pkts bytes target prot opt in out source destination - 0 0 DROP all -- net1 any 172.17.1.0/24 anywhere - 0 0 MARK all -- net1 any 172.17.0.0/16 anywhere MARK or 0x20000 - -Chain MULTI-0-INGRESS-0-PORTS (1 references) - pkts bytes target prot opt in out source destination - 0 0 MARK tcp -- net1 any anywhere anywhere tcp dpt:6379 MARK or 0x10000 - -Chain MULTI-EGRESS (1 references) - pkts bytes target prot opt in out source destination - 0 0 MULTI-0-EGRESS all -- any net1 anywhere anywhere /* policy:test-network-policy net-attach-def:default/macvlan-conf-1 */ - -Chain MULTI-INGRESS (1 references) - pkts bytes target prot opt in out source destination - 0 0 MULTI-0-INGRESS all -- net1 any anywhere anywhere /* policy:test-network-policy net-attach-def:default/macvlan-conf-1 */ - diff --git a/demo/net.yml b/demo/net.yml deleted file mode 100644 index cfad6d5c..00000000 --- a/demo/net.yml +++ /dev/null @@ -1,22 +0,0 @@ -apiVersion: "k8s.cni.cncf.io/v1" -kind: NetworkAttachmentDefinition -metadata: - name: macvlan-conf-1 -spec: - config: '{ - "cniVersion": "0.3.0", - "type": "macvlan", - "master": "eth0", - "mode": "bridge", - "ipam": { - "type": "host-local", - "ranges": [ - [ { - "subnet": "10.10.0.0/16", - "rangeStart": "10.10.1.20", - "rangeEnd": "10.10.3.50", - "gateway": "10.10.0.254" - } ] - ] - } - }' diff --git a/demo/policy.yml b/demo/policy.yml deleted file mode 100644 index ffeb4399..00000000 --- a/demo/policy.yml +++ /dev/null @@ -1,28 +0,0 @@ -apiVersion: k8s.cni.cncf.io/v1beta1 -kind: MultiNetworkPolicy -metadata: - name: test-network-policy - namespace: default - annotations: - k8s.v1.cni.cncf.io/policy-for: macvlan-conf-1 -spec: - podSelector: {} - policyTypes: - - Ingress - - Egress - ingress: - - from: - - ipBlock: - cidr: 172.17.0.0/16 - except: - - 172.17.1.0/24 - ports: - - protocol: TCP - port: 6379 - egress: - - to: - - ipBlock: - cidr: 10.0.0.0/24 - ports: - - protocol: TCP - port: 5978 diff --git a/deploy.yaml b/deploy.yaml new file mode 100644 index 00000000..5366a2ca --- /dev/null +++ b/deploy.yaml @@ -0,0 +1,149 @@ +--- +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: multi-networkpolicy-nftables +rules: + - apiGroups: ["k8s.cni.cncf.io"] + resources: + - '*' + verbs: + - '*' + - apiGroups: + - "" + resources: + - pods + - pods/status + - namespaces + verbs: + - get + - list + - watch + - apiGroups: ["networking.k8s.io"] + resources: + - networkpolicies + verbs: + - get + - list + - watch +--- +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: multi-networkpolicy-nftables +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: multi-networkpolicy-nftables +subjects: + - kind: ServiceAccount + name: multi-networkpolicy-nftables + namespace: default +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: multi-networkpolicy-nftables + namespace: default +--- +kind: ConfigMap +apiVersion: v1 +metadata: + name: multi-networkpolicy-custom-v4-rules + namespace: kube-system + labels: + tier: node + app: multi-networkpolicy +data: + custom-v4-rules.txt: | + # Custom IPv4 rules for e2e testing + # Allow traffic on port 9999 for testing custom rules + tcp dport 9999 accept + # Allow traffic from specific IP range + ip saddr 192.168.100.0/24 accept +--- +kind: ConfigMap +apiVersion: v1 +metadata: + name: multi-networkpolicy-custom-v6-rules + namespace: kube-system + labels: + tier: node + app: multi-networkpolicy +data: + custom-v6-rules.txt: | + # Custom IPv6 rules for e2e testing + # Allow traffic on port 9999 for testing custom rules + tcp dport 9999 accept + # Allow traffic from specific IPv6 range + ip6 saddr 2001:db8:100::/64 accept +--- +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: multi-networkpolicy-nftables + namespace: default + labels: + app: multi-networkpolicy-nftables +spec: + selector: + matchLabels: + name: multi-networkpolicy-nftables + updateStrategy: + type: RollingUpdate + template: + metadata: + labels: + name: multi-networkpolicy-nftables + spec: + hostNetwork: true + serviceAccountName: multi-networkpolicy-nftables + containers: + - name: multi-networkpolicy-nftables + image: localhost:5000/multus-networkpolicy-nftables:e2e + imagePullPolicy: Always + command: + - /multi-networkpolicy-nftables + args: + - "--zap-log-level=2" + - "--container-runtime-endpoint=/run/crio/crio.sock" + - "--network-plugins=macvlan,ipvlan" + - "--host-prefix=/host" + # accept all icmp/icmpv6 types for e2e testing + - "--accept-icmp" + - "--accept-icmpv6" + # custom rules for e2e testing + - "--custom-v4-ingress-rule-file=/etc/multi-networkpolicy/rules/custom-v4-rules.txt" + - "--custom-v4-egress-rule-file=/etc/multi-networkpolicy/rules/custom-v4-rules.txt" + - "--custom-v6-ingress-rule-file=/etc/multi-networkpolicy/rules/custom-v6-rules.txt" + - "--custom-v6-egress-rule-file=/etc/multi-networkpolicy/rules/custom-v6-rules.txt" + # enable debug logging for e2e + - "--zap-log-level=2" + resources: + requests: + cpu: "100m" + memory: "80Mi" + limits: + cpu: "100m" + memory: "150Mi" + securityContext: + privileged: true + capabilities: + add: ["SYS_ADMIN", "NET_ADMIN"] + volumeMounts: + - name: host + mountPath: /host + - name: multi-networkpolicy-custom-rules + mountPath: /etc/multi-networkpolicy/rules + readOnly: true + volumes: + - name: host + hostPath: + path: / + - name: multi-networkpolicy-custom-rules + projected: + sources: + - configMap: + name: multi-networkpolicy-custom-v4-rules + - configMap: + name: multi-networkpolicy-custom-v6-rules diff --git a/deploy.yml b/deploy.yml deleted file mode 100644 index 9b1d89ef..00000000 --- a/deploy.yml +++ /dev/null @@ -1,176 +0,0 @@ ---- -kind: ClusterRole -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - name: multi-networkpolicy -rules: - - apiGroups: ["k8s.cni.cncf.io"] - resources: - - '*' - verbs: - - '*' - - apiGroups: - - "" - resources: - - pods - - namespaces - verbs: - - list - - watch - - get - # Watch for changes to Kubernetes NetworkPolicies. - - apiGroups: ["networking.k8s.io"] - resources: - - networkpolicies - verbs: - - watch - - list - - apiGroups: - - "" - - events.k8s.io - resources: - - events - verbs: - - create - - patch - - update ---- -kind: ClusterRoleBinding -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - name: multi-networkpolicy -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: multi-networkpolicy -subjects: -- kind: ServiceAccount - name: multi-networkpolicy - namespace: kube-system ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - name: multi-networkpolicy - namespace: kube-system ---- -kind: ConfigMap -apiVersion: v1 -metadata: - name: multi-networkpolicy-custom-v4-rules - namespace: kube-system - labels: - tier: node - app: multi-networkpolicy -data: - custom-v4-rules.txt: | - # accept redirect - -p icmp --icmp-type redirect -j ACCEPT - # accept fragmentation-needed (for MTU discovery) - -p icmp --icmp-type fragmentation-needed -j ACCEPT ---- -kind: ConfigMap -apiVersion: v1 -metadata: - name: multi-networkpolicy-custom-v6-rules - namespace: kube-system - labels: - tier: node - app: multi-networkpolicy -data: - custom-v6-rules.txt: | - # accept NDP - -p icmpv6 --icmpv6-type neighbor-solicitation -j ACCEPT - -p icmpv6 --icmpv6-type neighbor-advertisement -j ACCEPT - # accept RA/RS - -p icmpv6 --icmpv6-type router-solicitation -j ACCEPT - -p icmpv6 --icmpv6-type router-advertisement -j ACCEPT - # accept redirect - -p icmpv6 --icmpv6-type redirect -j ACCEPT - # accept packet-too-big (for MTU discovery) - -p icmpv6 --icmpv6-type packet-too-big -j ACCEPT ---- -apiVersion: apps/v1 -kind: DaemonSet -metadata: - name: multi-networkpolicy-ds-amd64 - namespace: kube-system - labels: - tier: node - app: multi-networkpolicy - name: multi-networkpolicy -spec: - selector: - matchLabels: - name: multi-networkpolicy - updateStrategy: - type: RollingUpdate - template: - metadata: - labels: - tier: node - app: multi-networkpolicy - name: multi-networkpolicy - spec: - hostNetwork: true - nodeSelector: - kubernetes.io/arch: amd64 - tolerations: - - operator: Exists - effect: NoSchedule - serviceAccountName: multi-networkpolicy - containers: - - name: multi-networkpolicy - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:snapshot - imagePullPolicy: Always - command: ["/usr/bin/multi-networkpolicy-iptables"] - args: - - "--host-prefix=/host" - # change this if runtime is different that crio default - - "--container-runtime-endpoint=/run/crio/crio.sock" - # uncomment this if you want to store iptables rules - - "--pod-iptables=/var/lib/multi-networkpolicy/iptables" - # uncomment this if you need to accept link-local address traffic - #- "--allow-ipv6-src-prefix=fe80::/10" - #- "--allow-ipv6-dst-prefix=fe80::/10" - # uncomment this if you need to add custom iptables rules defined above configmap - #- "--custom-v4-ingress-rule-file=/etc/multi-networkpolicy/rules/custom-v4-rules.txt" - #- "--custom-v4-egress-rule-file=/etc/multi-networkpolicy/rules/custom-v4-rules.txt" - #- "--custom-v6-ingress-rule-file=/etc/multi-networkpolicy/rules/custom-v6-rules.txt" - #- "--custom-v6-egress-rule-file=/etc/multi-networkpolicy/rules/custom-v6-rules.txt" - # uncomment if you want to accept ICMP/ICMPv6 traffic - #- "--accept-icmp" - #- "--accept-icmpv6" - resources: - requests: - cpu: "100m" - memory: "80Mi" - limits: - cpu: "100m" - memory: "150Mi" - securityContext: - privileged: true - capabilities: - add: ["SYS_ADMIN", "NET_ADMIN"] - volumeMounts: - - name: host - mountPath: /host - - name: var-lib-multinetworkpolicy - mountPath: /var/lib/multi-networkpolicy - - name: multi-networkpolicy-custom-rules - mountPath: /etc/multi-networkpolicy/rules - readOnly: true - volumes: - - name: host - hostPath: - path: / - - name: var-lib-multinetworkpolicy - hostPath: - path: /var/lib/multi-networkpolicy - - name: multi-networkpolicy-custom-rules - projected: - sources: - - configMap: - name: multi-networkpolicy-custom-v4-rules - - configMap: - name: multi-networkpolicy-custom-v6-rules diff --git a/docs/configurations.md b/docs/configurations.md deleted file mode 100644 index f12170d9..00000000 --- a/docs/configurations.md +++ /dev/null @@ -1,51 +0,0 @@ -## Multi-networkpolicy-iptables Configurations - - -### Command Line Options - -Most command line options have description in help, so please execute with `--help` to see the option. - -``` -$ ./multi-networkpolicy-iptables --help -``` - -### Advanced Options - -#### Add exceptional IPv6 prefix address to accept - -Some IPv6 networks may require accepting traffic from/to specific address prefixes for the network, such as multicast address (all routers multicast address, link-local address and so on). You can configure `--allow-ipv6-src-prefix` and `--allow-ipv6-dst-prefix` to specify which prefix should be accepted (even though network policy does not have it). Both options accept comma separated IPv6 prefix list. - -``` ---allow-ipv6-src-prefix=fe80::/10 ---allow-ipv6-dst-prefix=fe80::/10,ff00::/8 -``` - -#### Add custom iptables/ip6tables rules - -Some IPv4/v6 networks may require accepting some specific traffic (e.g. DHCP). You can add custom iptable rules in ingress/egress for IPv4/v6 network to accept such traffics, by - -- `--custom-v4-ingress-rule-file` -- `--custom-v4-egress-rule-file` -- `--custom-v6-ingress-rule-file` -- `--custom-v6-egress-rule-file` - -Each option takes file path for iptable rules. This file can contain - -- iptable rules (no `-A` option) -- comment (begins with '#') - -Here is the example to accept DHCPv6 packets using the options. -``` -$ cat testv6IngressRules.txt -# comment: this accepts DHCPv6 packets from link-local address --m udp -p udp --dport 546 -d fe80::/64 -j ACCEPT - -$ cat testv6EgressRules.txt -# comment: this rules accepts DHCPv6 packet to dhcp relay agents/servers --m udp -p udp --dport 547 -d ff02::1:2 -j ACCEPT - -$ ./multi-networkpolicy-iptables \ - (snip, some options here) \ - --custom-v6-ingress-rule-file testv6IngressRules.txt \ - --custom-v6-egress-rule-file testv6EgressRules.txt -``` diff --git a/docs/nftables.md b/docs/nftables.md new file mode 100644 index 00000000..e8c3ce04 --- /dev/null +++ b/docs/nftables.md @@ -0,0 +1,570 @@ +# NFTables Implementation + +## Overview + +This document details how Kubernetes `MultiNetworkPolicy` resources are translated into NFTables rules and applied within pod network namespaces. + +## NFTables Structure + +### Table and Chain Hierarchy + +``` +Table: multi_networkpolicy (inet family) +├── Chain: input (netfilter hook, filter priority) +├── Chain: output (netfilter hook, filter priority) +├── Chain: ingress (regular chain) +│ ├── Connection tracking rule +│ ├── Jump to common-ingress +│ ├── Jump to policy chains (cnp-) +│ └── Drop rule +├── Chain: egress (regular chain) +│ ├── Connection tracking rule +│ ├── Jump to common-egress +│ ├── Jump to policy chains (cnp-) +│ └── Drop rule +├── Chain: common-ingress (shared rules) +│ ├── Optional: Accept ICMP +│ ├── Optional: Accept ICMPv6 +│ └── Custom ingress rules (IPv4/IPv6) +├── Chain: common-egress (shared rules) +│ ├── Optional: Accept ICMP +│ ├── Optional: Accept ICMPv6 +│ └── Custom egress rules (IPv4/IPv6) +└── Policy-specific chains (cnp-) + ├── Reverse rules (hairpinning support) + ├── Source/destination filtering + ├── Port/protocol rules + └── Accept rules +``` + +### Naming Conventions + +- **Table**: `multi_networkpolicy` +- **Policy chains**: `cnp-<16-char-hash>` (where hash = SHA256(policy.namespace/policy.name)[:16]) +- **Interface sets**: `smi-<16-char-hash>` (managed interfaces for policy) +- **IP sets**: `snp-<16-char-hash>____` + - Direction: `ingress` or `egress` + - Family: `ipv4` or `ipv6` + - Interface: interface name (e.g., `eth1`) + - Index: rule index number + +## Rule Generation Process + +### 1. Basic Structure Creation + +When the first policy is applied to a pod, the basic NFTables structure is created: + +```nftables +# Create table +table inet multi_networkpolicy { + # Input dispatcher chain + chain input { + type filter hook input priority filter; policy accept; + comment "Input Dispatcher" + } + + # Output dispatcher chain + chain output { + type filter hook output priority filter; policy accept; + comment "Output Dispatcher" + } + + # Ingress policy chain + chain ingress { + comment "Ingress Policies" + ct state established,related accept comment "Connection tracking" + jump common-ingress comment "Jump to common" + drop comment "Drop rule" + } + + # Egress policy chain + chain egress { + comment "Egress Policies" + ct state established,related accept comment "Connection tracking" + jump common-egress comment "Jump to common" + drop comment "Drop rule" + } + + # Common ingress chain + chain common-ingress { + comment "Common Policies" + # Optional: ICMP rules + meta l4proto icmp accept comment "Accept ICMP" + meta l4proto icmpv6 accept comment "Accept ICMPv6" + # Custom ingress rules from ConfigMaps + tcp dport 9999 accept comment "Custom Rule" + ip saddr 192.168.100.0/24 accept comment "Custom Rule" + } + + # Common egress chain + chain common-egress { + comment "Common Policies" + # Optional: ICMP rules + meta l4proto icmp accept comment "Accept ICMP" + meta l4proto icmpv6 accept comment "Accept ICMPv6" + # Custom egress rules from ConfigMaps + tcp dport 9999 accept comment "Custom Rule" + ip saddr 192.168.100.0/24 accept comment "Custom Rule" + } +} +``` + +### 2. Common Rules Configuration + +Common rules are applied to all policies through the `common-ingress` and `common-egress` chains. These are configured via the controller's command-line flags: + +- **ICMP Support**: Enable/disable ICMP and ICMPv6 traffic globally + - `--accept-icmp`: Accept ICMP (IPv4) traffic + - `--accept-icmpv6`: Accept ICMPv6 (IPv6) traffic + +- **Custom Rules**: Load custom nftables rules from files + - `--custom-v4-ingress-rule-file`: Custom IPv4 ingress rules + - `--custom-v4-egress-rule-file`: Custom IPv4 egress rules + - `--custom-v6-ingress-rule-file`: Custom IPv6 ingress rules + - `--custom-v6-egress-rule-file`: Custom IPv6 egress rules + +Custom rules are typically provided via ConfigMaps mounted into the controller pod. These rules allow cluster administrators to define global network policies without modifying individual MultiNetworkPolicy resources. + +Example custom rules: +```nftables +# Allow traffic on specific port +tcp dport 9999 accept + +# Allow traffic from specific IP range +ip saddr 192.168.100.0/24 accept +ip6 saddr 2001:db8:100::/64 accept + +# Drop traffic from specific sources +ip saddr 10.0.0.0/8 drop +``` + +### 3. Interface Set Creation + +For each policy, a set of managed interfaces is created: + +```nftables +# Managed interfaces set for policy +set smi-365f0b66bf7ef65c { + type ifname + comment "Managed interfaces set for default/web-policy" + elements = { "net1", "net2" } +} +``` + +### 4. Policy Chain Creation + +Each policy gets its own chain: + +```nftables +chain cnp-365f0b66bf7ef65c { + comment "MultiNetworkPolicy default/web-policy" + + # Reverse rules for hairpinning (added first) + iifname net1 ip saddr 10.244.1.5 accept + iifname net2 ip saddr 10.244.1.6 accept + + # Policy-specific rules follow... +} +``` + +### 5. Dispatcher Rules + +Rules in the input/output chains dispatch traffic to appropriate policy type chains: + +```nftables +# In input chain - for ingress traffic +iifname @smi-365f0b66bf7ef65c jump ingress comment "Policy default/web-policy" + +# In output chain - for egress traffic +oifname @smi-365f0b66bf7ef65c jump egress comment "Policy default/web-policy" +``` + +### 6. Ingress Chain Flow + +The ingress chain contains jumps to policy-specific chains: + +```nftables +# In ingress chain +jump cnp-365f0b66bf7ef65c comment "default/web-policy" +``` + +### 7. Reverse Rules (Hairpinning Support) + +Reverse rules are automatically generated at the beginning of each policy chain to support pod-to-pod communication within the same host (hairpinning). These rules allow traffic from the pod's own IP addresses to return: + +```nftables +# In cnp-365f0b66bf7ef65c chain - reverse rules for each interface +iifname net1 ip saddr 10.244.1.5 accept +iifname net1 ip6 saddr 2001:db8::5 accept +iifname net2 ip saddr 10.244.1.6 accept +iifname net2 ip6 saddr 2001:db8::6 accept +``` + +These rules are crucial for allowing pods to communicate with themselves or other pods on the same node through secondary network interfaces. + +### 8. Policy-Specific Rules + +Following the reverse rules, the policy chain contains the actual filtering logic: + +```nftables +# In cnp-365f0b66bf7ef65c chain - the actual policy rules +iifname "net1" ip saddr @snp-365f0b66bf7ef65c_ingress_ipv4_net1_0 tcp dport { 8080 } accept +iifname "net1" ip saddr @snp-365f0b66bf7ef65c_ingress_ipv4_cidr_0 tcp dport { 8080 } accept +iifname "net2" ip saddr @snp-365f0b66bf7ef65c_ingress_ipv4_net1_0 tcp dport { 8080 } accept +iifname "net2" ip saddr @snp-365f0b66bf7ef65c_ingress_ipv4_cidr_0 tcp dport { 8080 } accept +``` + +## Rule Types + +### 1. Allow All Rules + +When no `from`/`to` or `ports` are specified: + +```nftables +iifname "net1" accept +``` + +### 2. Port-Only Rules + +When only `ports` are specified: + +```nftables +iifname "net1" tcp dport { 8080, 8443 } accept +iifname "net1" udp dport { 53 } accept +``` + +### 3. IP Block Rules + +For CIDR-based rules with sets: + +```nftables +# Create CIDR set +set snp-365f0b66bf7ef65c_ingress_ipv4_cidr_0 { + type ipv4_addr + flags interval + comment "CIDRs for default/web-policy" + elements = { 10.0.0.0/8, 192.168.0.0/16 } +} + +# Use set in rule +iifname "net1" ip saddr @snp-365f0b66bf7ef65c_ingress_ipv4_cidr_0 tcp dport { 8080 } accept +``` + +### 4. Pod Selector Rules + +For pod-based rules: + +```nftables +# Create pod IP set +set snp-365f0b66bf7ef65c_ingress_ipv4_net1_0 { + type ipv4_addr + comment "Addresses for default/web-policy" + elements = { 10.244.1.5, 10.244.1.8 } +} + +# Use set in rule +iifname "net1" ip saddr @snp-365f0b66bf7ef65c_ingress_ipv4_net1_0 tcp dport { 8080 } accept +``` + +### 5. Namespace Selector Rules + +Similar to pod selector, but IPs are gathered from all pods in matching namespaces: + +```nftables +set snp-365f0b66bf7ef65c_ingress_ipv4_net1_0 { + type ipv4_addr + comment "Addresses for default/web-policy" + elements = { 10.244.2.10, 10.244.2.15, 10.244.2.20 } +} +``` + +## Complete Example + +Given this `MultiNetworkPolicy`: + +```yaml +apiVersion: k8s.cni.cncf.io/v1beta1 +kind: MultiNetworkPolicy +metadata: + name: web-policy + namespace: default + annotations: + k8s.v1.cni.cncf.io/policy-for: "macvlan-net1,macvlan-net2" +spec: + podSelector: + matchLabels: + app: web + policyTypes: + - Ingress + - Egress + ingress: + - from: + - podSelector: + matchLabels: + app: frontend + - ipBlock: + cidr: 10.0.0.0/8 + except: + - 10.0.1.0/24 + ports: + - protocol: TCP + port: 8080 + egress: + - to: + - namespaceSelector: + matchLabels: + name: database + ports: + - protocol: TCP + port: 5432 +``` + +And assuming the web pod has these interfaces: +- `net1` with IP `10.244.1.5` +- `net2` with IP `10.244.1.6` + +The generated NFTables rules would be: + +```nftables +table inet multi_networkpolicy { + # Managed interfaces set + set smi-365f0b66bf7ef65c { + type ifname + comment "Managed interfaces set for default/web-policy" + elements = { "net1", "net2" } + } + + # Pod selector IPs + set snp-365f0b66bf7ef65c_ingress_ipv4_net1_0 { + type ipv4_addr + comment "Addresses for default/web-policy" + elements = { 10.244.1.5, 10.244.1.8 } + } + + # IP block CIDRs (with exceptions) + set snp-365f0b66bf7ef65c_ingress_ipv4_cidr_0 { + type ipv4_addr + flags interval + comment "CIDRs for default/web-policy" + elements = { 10.0.0.0/24, 10.0.2.0/23, 10.0.4.0/22, ... } + } + + # Namespace selector IPs + set snp-365f0b66bf7ef65c_egress_ipv4_net1_0 { + type ipv4_addr + comment "Addresses for default/web-policy" + elements = { 10.244.3.10, 10.244.3.15 } + } + + # Basic chains + chain input { + type filter hook input priority filter; policy accept; + comment "Input Dispatcher" + iifname @smi-365f0b66bf7ef65c jump ingress comment "default/web-policy" + } + + chain output { + type filter hook output priority filter; policy accept; + comment "Output Dispatcher" + oifname @smi-365f0b66bf7ef65c jump egress comment "default/web-policy" + } + + chain ingress { + comment "Ingress Policies" + ct state established,related accept comment "Connection tracking" + jump common-ingress comment "Jump to common" + jump cnp-365f0b66bf7ef65c comment "default/web-policy" + drop comment "Drop rule" + } + + chain egress { + comment "Egress Policies" + ct state established,related accept comment "Connection tracking" + jump common-egress comment "Jump to common" + jump cnp-365f0b66bf7ef65c comment "default/web-policy" + drop comment "Drop rule" + } + + chain common-ingress { + comment "Common Policies" + meta l4proto icmp accept comment "Accept ICMP" + meta l4proto icmpv6 accept comment "Accept ICMPv6" + tcp dport 9999 accept comment "Custom Rule" + ip saddr 192.168.100.0/24 accept comment "Custom Rule" + } + + chain common-egress { + comment "Common Policies" + meta l4proto icmp accept comment "Accept ICMP" + meta l4proto icmpv6 accept comment "Accept ICMPv6" + tcp dport 9999 accept comment "Custom Rule" + ip saddr 192.168.100.0/24 accept comment "Custom Rule" + } + + # Policy-specific chain + chain cnp-365f0b66bf7ef65c { + comment "MultiNetworkPolicy default/web-policy" + + # Reverse rules for hairpinning (always first) + iifname net1 ip saddr 10.244.1.5 accept + iifname net2 ip saddr 10.244.1.6 accept + + # Ingress rules (for each interface) + iifname "net1" ip saddr @snp-365f0b66bf7ef65c_ingress_ipv4_net1_0 tcp dport { 8080 } accept + iifname "net1" ip saddr @snp-365f0b66bf7ef65c_ingress_ipv4_cidr_0 tcp dport { 8080 } accept + iifname "net2" ip saddr @snp-365f0b66bf7ef65c_ingress_ipv4_net1_0 tcp dport { 8080 } accept + iifname "net2" ip saddr @snp-365f0b66bf7ef65c_ingress_ipv4_cidr_0 tcp dport { 8080 } accept + + # Egress rules (for each interface) + oifname "net1" ip daddr @snp-365f0b66bf7ef65c_egress_ipv4_net1_0 tcp dport { 5432 } accept + oifname "net2" ip daddr @snp-365f0b66bf7ef65c_egress_ipv4_net1_0 tcp dport { 5432 } accept + } +} +``` + +## Advanced Features + +### 1. Hairpinning Support + +Reverse rules enable pod-to-pod communication on the same host through secondary network interfaces. Without these rules, traffic from a pod to another pod on the same host would be blocked by the policy's default drop rule. + +Example scenario: +- Pod A (IP: 10.244.1.5 on net1) sends traffic to Pod B (IP: 10.244.1.6 on net1) +- Both pods are on the same Kubernetes node +- The reverse rule `iifname net1 ip saddr 10.244.1.5 accept` allows the traffic to flow + +### 2. IPv6 Support + +The system automatically handles IPv6 addresses and creates separate sets: + +```nftables +set snp-365f0b66bf7ef65c_ingress_ipv6_net1_0 { + type ipv6_addr + flags interval + elements = { 2001:db8::/32 } +} + +# Reverse rule for IPv6 +iifname net1 ip6 saddr 2001:db8::5 accept +``` + +### 3. CIDR Exception Handling + +IP blocks with exceptions are processed to create precise interval sets: + +```yaml +ipBlock: + cidr: 10.0.0.0/8 + except: + - 10.0.1.0/24 +``` + +Becomes: +```nftables +elements = { + 10.0.0.0/24, # 10.0.0.0 - 10.0.0.255 + 10.0.2.0/23, # 10.0.2.0 - 10.0.3.255 + 10.0.4.0/22, # 10.0.4.0 - 10.0.7.255 + # ... continues to cover 10.0.0.0/8 except 10.0.1.0/24 +} +``` + +### 4. Connection Tracking + +All policies include stateful connection tracking in the policy type chains (ingress/egress): + +```nftables +ct state established,related accept +``` + +This allows return traffic for established connections without explicit rules. + +### 5. Multiple Interface Support + +Policies can apply to multiple network interfaces, with rules generated for each: + +```nftables +# Rules duplicated for each managed interface +iifname "net1" ip saddr @source_set tcp dport { 8080 } accept +iifname "net2" ip saddr @source_set tcp dport { 8080 } accept + +# Reverse rules for each interface +iifname net1 ip saddr 10.244.1.5 accept +iifname net2 ip saddr 10.244.1.6 accept +``` + +## Traffic Flow + +### Ingress Traffic Flow + +1. Packet arrives at input hook +2. Input chain checks if interface is managed (`iifname @smi-`) +3. If matched, jumps to `ingress` chain +4. Ingress chain checks connection state (established/related) +5. If new connection, jumps to `common-ingress` chain + - ICMP/ICMPv6 rules checked + - Custom ingress rules checked +6. Back to ingress chain, jumps to policy-specific chain (`cnp-`) +7. Policy chain checks reverse rules first (hairpinning) +8. If not matched, checks policy-specific rules +9. If no rule matches, falls through to ingress chain's drop rule + +### Egress Traffic Flow + +1. Packet arrives at output hook +2. Output chain checks if interface is managed (`oifname @smi-`) +3. If matched, jumps to `egress` chain +4. Egress chain checks connection state (established/related) +5. If new connection, jumps to `common-egress` chain + - ICMP/ICMPv6 rules checked + - Custom egress rules checked +6. Back to egress chain, jumps to policy-specific chain (`cnp-`) +7. Policy chain checks reverse rules first (hairpinning) +8. If not matched, checks policy-specific rules +9. If no rule matches, falls through to egress chain's drop rule + +## Performance Optimizations + +1. **Set-Based Matching**: Uses NFTables sets for O(1) IP address lookups +2. **Connection Tracking**: Reduces per-packet processing for established flows +3. **Interface Sets**: Efficient interface matching using sets +4. **Rule Consolidation**: Combines similar rules where possible +5. **Interval Sets**: Efficient CIDR range matching with interval flag +6. **Early Exit**: Reverse rules placed first for quick hairpinning decision +7. **Common Rules**: Shared rules (ICMP, custom rules) evaluated once per packet + +## Cleanup Process + +When policies are deleted or updated: + +1. **Chain Removal**: Policy-specific chains are deleted +2. **Set Cleanup**: All associated sets are removed +3. **Rule Removal**: Dispatcher rules are removed +4. **Reference Cleanup**: All references to the policy are cleaned up +5. **Common Chains**: Preserved across policy changes, rebuilt on structure updates + +The cleanup process ensures no orphaned rules or sets remain in the NFTables configuration. + +## Configuration Files + +Custom rules can be loaded from ConfigMaps: + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: multi-networkpolicy-custom-v4-rules + namespace: kube-system +data: + custom-v4-rules.txt: | + # Custom IPv4 ingress/egress rules + tcp dport 9999 accept + ip saddr 192.168.100.0/24 accept +``` + +Mount this ConfigMap to the controller pod and reference it with: +``` +--custom-v4-ingress-rule-file=/path/to/custom-v4-rules.txt +``` + +The controller reads these files on startup and applies the rules to all pods. diff --git a/e2e/Dockerfile b/e2e/Dockerfile index f9e87285..00c7c679 100644 --- a/e2e/Dockerfile +++ b/e2e/Dockerfile @@ -1,6 +1,19 @@ -FROM docker.io/fedora:38 +FROM docker.io/fedora:42 -LABEL org.opencontainers.image.source https://github.com/k8snetworkplumbingwg/multi-networkpolicy-iptables -LABEL org.opencontainers.image.base.name ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test -RUN dnf install -y nginx iptables-utils iptables-legacy iptables-nft net-tools iputils iproute tcpdump wireshark-cli nmap-ncat -RUN alternatives --set iptables /usr/sbin/iptables-nft +# Install essential tools for e2e testing +RUN dnf install -y \ + nginx \ + nftables \ + net-tools \ + iputils \ + iproute \ + tcpdump \ + wireshark-cli \ + nmap-ncat \ + jq \ + procps-ng \ + util-linux \ + && dnf clean all + +# Ensure nftables service is available +RUN systemctl enable nftables || true diff --git a/e2e/README.md b/e2e/README.md index 889f1352..3e41f80f 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -6,8 +6,8 @@ This requires [Bats](https://github.com/bats-core/bats-core) for test runner. Please install bats (e.g. dnf, apt and so on). ``` -$ git clone https://github.com/k8snetworkplumbingwg/multi-networkpolicy-iptables -$ cd multi-networkpolicy-iptables/e2e +$ git clone https://github.com/k8snetworkplumbingwg/multi-networkpolicy-nftables +$ cd multi-networkpolicy-nftables/e2e $ ./get_tools.sh $ ./setup_cluster.sh $ ./tests/simple-v4-ingress.bats diff --git a/e2e/multi-network-policy-iptables-e2e.yml b/e2e/multi-network-policy-iptables-e2e.yml deleted file mode 100644 index 5855555c..00000000 --- a/e2e/multi-network-policy-iptables-e2e.yml +++ /dev/null @@ -1,179 +0,0 @@ ---- -kind: ClusterRole -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - name: multi-networkpolicy -rules: - - apiGroups: ["k8s.cni.cncf.io"] - resources: - - '*' - verbs: - - '*' - - apiGroups: - - "" - resources: - - pods - - namespaces - verbs: - - list - - watch - - get - # Watch for changes to Kubernetes NetworkPolicies. - - apiGroups: ["networking.k8s.io"] - resources: - - networkpolicies - verbs: - - watch - - list - - apiGroups: - - "" - - events.k8s.io - resources: - - events - verbs: - - create - - patch - - update ---- -kind: ClusterRoleBinding -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - name: multi-networkpolicy -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: multi-networkpolicy -subjects: -- kind: ServiceAccount - name: multi-networkpolicy - namespace: kube-system ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - name: multi-networkpolicy - namespace: kube-system ---- -kind: ConfigMap -apiVersion: v1 -metadata: - name: multi-networkpolicy-custom-v4-rules - namespace: kube-system - labels: - tier: node - app: multi-networkpolicy -data: - custom-v4-rules.txt: | - # accept redirect - -p icmp --icmp-type redirect -j ACCEPT - # accept fragmentation-needed (for MTU discovery) - -p icmp --icmp-type fragmentation-needed -j ACCEPT ---- -kind: ConfigMap -apiVersion: v1 -metadata: - name: multi-networkpolicy-custom-v6-rules - namespace: kube-system - labels: - tier: node - app: multi-networkpolicy -data: - custom-v6-rules.txt: | - # accept NDP - -p icmpv6 --icmpv6-type neighbor-solicitation -j ACCEPT - -p icmpv6 --icmpv6-type neighbor-advertisement -j ACCEPT - # accept RA/RS - -p icmpv6 --icmpv6-type router-solicitation -j ACCEPT - -p icmpv6 --icmpv6-type router-advertisement -j ACCEPT - # accept redirect - -p icmpv6 --icmpv6-type redirect -j ACCEPT - # accept packet-too-big (for MTU discovery) - -p icmpv6 --icmpv6-type packet-too-big -j ACCEPT ---- -apiVersion: apps/v1 -kind: DaemonSet -metadata: - name: multi-networkpolicy-ds-amd64 - namespace: kube-system - labels: - tier: node - app: multi-networkpolicy - name: multi-networkpolicy -spec: - selector: - matchLabels: - name: multi-networkpolicy - updateStrategy: - type: RollingUpdate - template: - metadata: - labels: - tier: node - app: multi-networkpolicy - name: multi-networkpolicy - spec: - hostNetwork: true - nodeSelector: - kubernetes.io/arch: amd64 - tolerations: - - operator: Exists - effect: NoSchedule - serviceAccountName: multi-networkpolicy - containers: - - name: multi-networkpolicy - image: localhost:5000/multus-networkpolicy-iptables:e2e - command: ["/usr/bin/multi-networkpolicy-iptables"] - args: - - "--host-prefix=/host" - # change this if runtime is different that crio default - - "--container-runtime-endpoint=/run/containerd/containerd.sock" - # uncomment this if you want to store iptables rules - - "--pod-iptables=/var/lib/multi-networkpolicy/iptables" - # (e2e test only) enshorten sync period to fast sync - - "--sync-period=1" - # uncomment this if you need to accept link-local address traffic - #- "--allow-ipv6-src-prefix=fe80::/10" - #- "--allow-ipv6-dst-prefix=fe80::/10" - # uncomment this if you need to add custom iptables rules defined above configmap - - "--custom-v4-ingress-rule-file=/etc/multi-networkpolicy/rules/custom-v4-rules.txt" - - "--custom-v4-egress-rule-file=/etc/multi-networkpolicy/rules/custom-v4-rules.txt" - - "--custom-v6-ingress-rule-file=/etc/multi-networkpolicy/rules/custom-v6-rules.txt" - - "--custom-v6-egress-rule-file=/etc/multi-networkpolicy/rules/custom-v6-rules.txt" - # uncomment if you want to accept ICMP/ICMPv6 traffic - #- "--accept-icmp" - #- "--accept-icmpv6" - - "--network-plugins=macvlan,bond" - - -v=8 - resources: - requests: - cpu: "100m" - memory: "80Mi" - limits: - cpu: "100m" - memory: "150Mi" - securityContext: - privileged: true - capabilities: - add: ["SYS_ADMIN", "NET_ADMIN"] - volumeMounts: - - name: host - mountPath: /host - - name: var-lib-multinetworkpolicy - mountPath: /var/lib/multi-networkpolicy - - name: multi-networkpolicy-custom-rules - mountPath: /etc/multi-networkpolicy/rules - readOnly: true - volumes: - - name: host - hostPath: - path: / - - name: var-lib-multinetworkpolicy - hostPath: - path: /var/lib/multi-networkpolicy - - name: multi-networkpolicy-custom-rules - projected: - sources: - - configMap: - name: multi-networkpolicy-custom-v4-rules - - configMap: - name: multi-networkpolicy-custom-v6-rules diff --git a/e2e/multi-network-policy-nftables-e2e.yml b/e2e/multi-network-policy-nftables-e2e.yml new file mode 100644 index 00000000..0520b9b5 --- /dev/null +++ b/e2e/multi-network-policy-nftables-e2e.yml @@ -0,0 +1,167 @@ +--- +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: multi-networkpolicy +rules: + - apiGroups: ["k8s.cni.cncf.io"] + resources: + - '*' + verbs: + - '*' + - apiGroups: + - "" + resources: + - pods + - pods/status + - namespaces + verbs: + - list + - watch + - get + # Watch for changes to Kubernetes NetworkPolicies. + - apiGroups: ["networking.k8s.io"] + resources: + - networkpolicies + verbs: + - watch + - list + - apiGroups: + - "" + - events.k8s.io + resources: + - events + verbs: + - create + - patch + - update +--- +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: multi-networkpolicy +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: multi-networkpolicy +subjects: +- kind: ServiceAccount + name: multi-networkpolicy + namespace: kube-system +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: multi-networkpolicy + namespace: kube-system +--- +kind: ConfigMap +apiVersion: v1 +metadata: + name: multi-networkpolicy-custom-v4-rules + namespace: kube-system + labels: + tier: node + app: multi-networkpolicy +data: + custom-v4-rules.txt: | + # Custom IPv4 rules for e2e testing + # Allow traffic on port 9999 for testing custom rules + tcp dport 9999 accept + # Allow traffic from specific IP range + ip saddr 192.168.100.0/24 accept +--- +kind: ConfigMap +apiVersion: v1 +metadata: + name: multi-networkpolicy-custom-v6-rules + namespace: kube-system + labels: + tier: node + app: multi-networkpolicy +data: + custom-v6-rules.txt: | + # Custom IPv6 rules for e2e testing + # Allow traffic on port 9999 for testing custom rules + tcp dport 9999 accept + # Allow traffic from specific IPv6 range + ip6 saddr 2001:db8:100::/64 accept +--- +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: multi-networkpolicy-ds-amd64 + namespace: kube-system + labels: + tier: node + app: multi-networkpolicy + name: multi-networkpolicy +spec: + selector: + matchLabels: + name: multi-networkpolicy + updateStrategy: + type: RollingUpdate + template: + metadata: + labels: + tier: node + app: multi-networkpolicy + name: multi-networkpolicy + spec: + hostNetwork: true + nodeSelector: + kubernetes.io/arch: amd64 + tolerations: + - operator: Exists + effect: NoSchedule + serviceAccountName: multi-networkpolicy + containers: + - name: multi-networkpolicy + image: localhost:5000/multus-networkpolicy-nftables:e2e + command: ["/multi-networkpolicy-nftables"] + args: + # change this if runtime is different than containerd default + - "--container-runtime-endpoint=/run/containerd/containerd.sock" + # enable network plugins for e2e testing + - "--network-plugins=macvlan,bond" + # use host prefix for e2e environment + - "--host-prefix=/host" + # accept all icmp/icmpv6 types for e2e testing + - "--accept-icmp" + - "--accept-icmpv6" + # custom rules for e2e testing + - "--custom-v4-ingress-rule-file=/etc/multi-networkpolicy/rules/custom-v4-rules.txt" + - "--custom-v4-egress-rule-file=/etc/multi-networkpolicy/rules/custom-v4-rules.txt" + - "--custom-v6-ingress-rule-file=/etc/multi-networkpolicy/rules/custom-v6-rules.txt" + - "--custom-v6-egress-rule-file=/etc/multi-networkpolicy/rules/custom-v6-rules.txt" + # enable debug logging for e2e + - "--zap-log-level=2" + resources: + requests: + cpu: "100m" + memory: "80Mi" + limits: + cpu: "100m" + memory: "150Mi" + securityContext: + privileged: true + capabilities: + add: ["SYS_ADMIN", "NET_ADMIN"] + volumeMounts: + - name: host + mountPath: /host + - name: multi-networkpolicy-custom-rules + mountPath: /etc/multi-networkpolicy/rules + readOnly: true + volumes: + - name: host + hostPath: + path: / + - name: multi-networkpolicy-custom-rules + projected: + sources: + - configMap: + name: multi-networkpolicy-custom-v4-rules + - configMap: + name: multi-networkpolicy-custom-v6-rules diff --git a/e2e/setup_cluster.sh b/e2e/setup_cluster.sh index fa5101b2..a88c5f09 100755 --- a/e2e/setup_cluster.sh +++ b/e2e/setup_cluster.sh @@ -9,7 +9,11 @@ OCI_BIN="${OCI_BIN:-docker}" kind_network='kind' -$OCI_BIN build -t localhost:5000/multus-networkpolicy-iptables:e2e -f ../Dockerfile .. +# Build main image +$OCI_BIN build -t localhost:5000/multus-networkpolicy-nftables:e2e -f ../Dockerfile .. + +# Build test image +$OCI_BIN build -t localhost:5000/multus-networkpolicy-nftables-tests:e2e -f ./Dockerfile . # deploy cluster with kind cat < server on port 80 (should succeed - specific allow policy allows)" { + # Should succeed - specific allow policy allows client-a on port 80 + run kubectl -n test-accept-all exec pod-client-a -- sh -c "echo x | nc -w 1 ${server_net1} 80" + [ "$status" -eq "0" ] +} + +@test "accept-all check client-a -> server on port 8080 (should succeed - accept-all policy allows)" { + # Should succeed - accept-all policy allows all traffic + run kubectl -n test-accept-all exec pod-client-a -- sh -c "echo x | nc -w 1 ${server_net1} 8080" + [ "$status" -eq "0" ] +} + +@test "accept-all check client-b -> server on port 80 (should succeed - accept-all policy allows)" { + # Should succeed - accept-all policy allows all traffic + run kubectl -n test-accept-all exec pod-client-b -- sh -c "echo x | nc -w 1 ${server_net1} 80" + [ "$status" -eq "0" ] +} + +@test "accept-all check client-b -> server on port 8080 (should succeed - accept-all policy allows)" { + # Should succeed - accept-all policy allows all traffic + run kubectl -n test-accept-all exec pod-client-b -- sh -c "echo x | nc -w 1 ${server_net1} 8080" + [ "$status" -eq "0" ] +} + +@test "accept-all check client-c -> server on port 80 (should succeed - accept-all policy allows)" { + # Should succeed - accept-all policy allows all traffic + run kubectl -n test-accept-all exec pod-client-c -- sh -c "echo x | nc -w 1 ${server_net1} 80" + [ "$status" -eq "0" ] +} + +@test "accept-all check client-c -> server on port 8080 (should succeed - accept-all policy allows)" { + # Should succeed - accept-all policy allows all traffic + run kubectl -n test-accept-all exec pod-client-c -- sh -c "echo x | nc -w 1 ${server_net1} 8080" + [ "$status" -eq "0" ] +} + +@test "cleanup environments" { + # remove test manifests + kubectl delete -f accept-all-with-specific-allow.yml + run kubectl -n test-accept-all wait --for=delete -l app=test-accept-all pod --timeout=${kubewait_timeout} + [ "$status" -eq "0" ] +} diff --git a/e2e/tests/accept-all-with-specific-allow.yml b/e2e/tests/accept-all-with-specific-allow.yml new file mode 100644 index 00000000..bfc86c41 --- /dev/null +++ b/e2e/tests/accept-all-with-specific-allow.yml @@ -0,0 +1,145 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: test-accept-all + labels: + app: test-accept-all +--- +apiVersion: k8s.cni.cncf.io/v1 +kind: NetworkAttachmentDefinition +metadata: + name: macvlan1-accept-all + namespace: test-accept-all +spec: + config: | + { + "cniVersion": "0.3.1", + "type": "macvlan", + "master": "eth0", + "mode": "bridge", + "ipam": { + "type": "host-local", + "subnet": "192.168.220.0/24", + "rangeStart": "192.168.220.10", + "rangeEnd": "192.168.220.50", + "routes": [ + { "dst": "0.0.0.0/0" } + ], + "gateway": "192.168.220.1" + } + } +--- +apiVersion: v1 +kind: Pod +metadata: + name: pod-server + namespace: test-accept-all + labels: + app: test-accept-all + name: pod-server + annotations: + k8s.v1.cni.cncf.io/networks: macvlan1-accept-all +spec: + containers: + - name: web-server + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["sh", "-c", "nc -klp 80 & nc -klp 8080 & wait"] + securityContext: + privileged: true + ports: + - name: http + containerPort: 80 + - name: http-alt + containerPort: 8080 +--- +apiVersion: v1 +kind: Pod +metadata: + name: pod-client-a + namespace: test-accept-all + labels: + app: test-accept-all + name: pod-client-a + role: allowed-client + annotations: + k8s.v1.cni.cncf.io/networks: macvlan1-accept-all +spec: + containers: + - name: client + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["sh", "-c", "sleep 3600"] +--- +apiVersion: v1 +kind: Pod +metadata: + name: pod-client-b + namespace: test-accept-all + labels: + app: test-accept-all + name: pod-client-b + role: blocked-client + annotations: + k8s.v1.cni.cncf.io/networks: macvlan1-accept-all +spec: + containers: + - name: client + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["sh", "-c", "sleep 3600"] +--- +apiVersion: v1 +kind: Pod +metadata: + name: pod-client-c + namespace: test-accept-all + labels: + app: test-accept-all + name: pod-client-c + role: other-client + annotations: + k8s.v1.cni.cncf.io/networks: macvlan1-accept-all +spec: + containers: + - name: client + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["sh", "-c", "sleep 3600"] +--- +# Accept-all policy - allows all traffic +apiVersion: k8s.cni.cncf.io/v1beta1 +kind: MultiNetworkPolicy +metadata: + name: accept-all-policy + namespace: test-accept-all + annotations: + k8s.v1.cni.cncf.io/policy-for: macvlan1-accept-all +spec: + podSelector: + matchLabels: + name: pod-server + policyTypes: + - Ingress + ingress: + - from: [] +--- +# Specific allow policy - allows client-a on port 80 +apiVersion: k8s.cni.cncf.io/v1beta1 +kind: MultiNetworkPolicy +metadata: + name: specific-allow-policy + namespace: test-accept-all + annotations: + k8s.v1.cni.cncf.io/policy-for: macvlan1-accept-all +spec: + podSelector: + matchLabels: + name: pod-server + policyTypes: + - Ingress + ingress: + - from: + - podSelector: + matchLabels: + role: allowed-client + ports: + - port: 80 + protocol: TCP diff --git a/e2e/tests/advanced-ipblock-scenarios.bats b/e2e/tests/advanced-ipblock-scenarios.bats new file mode 100755 index 00000000..ae601036 --- /dev/null +++ b/e2e/tests/advanced-ipblock-scenarios.bats @@ -0,0 +1,89 @@ +#!/usr/bin/env bats + +# Note: +# These test cases verify advanced IP block scenarios including except blocks, +# complex CIDR combinations, and edge cases with IP ranges. + +setup() { + cd $BATS_TEST_DIRNAME + load "common" + server_net1=$(get_net1_ip "test-advanced-ipblock" "pod-server") + client_a_net1=$(get_net1_ip "test-advanced-ipblock" "pod-client-a") + client_b_net1=$(get_net1_ip "test-advanced-ipblock" "pod-client-b") + client_c_net1=$(get_net1_ip "test-advanced-ipblock" "pod-client-c") + client_d_net1=$(get_net1_ip "test-advanced-ipblock" "pod-client-d") +} + +@test "setup advanced ipblock test environments" { + # create test manifests + kubectl create -f advanced-ipblock-scenarios.yml + + # verify all pods are available + run kubectl -n test-advanced-ipblock wait --for=condition=ready -l app=test-advanced-ipblock pod --timeout=${kubewait_timeout} + [ "$status" -eq "0" ] + + sleep 5 +} + +@test "check generated nftables rules" { + # wait for sync + sleep 5 + + run has_nftables_table "test-advanced-ipblock" "pod-server" + [ "$status" -eq "0" ] + + run has_nftables_table "test-advanced-ipblock" "pod-client-a" + [ "$status" -eq "1" ] + + run has_nftables_table "test-advanced-ipblock" "pod-client-b" + [ "$status" -eq "1" ] + + run has_nftables_table "test-advanced-ipblock" "pod-client-c" + [ "$status" -eq "1" ] + + run has_nftables_table "test-advanced-ipblock" "pod-client-d" + [ "$status" -eq "1" ] +} + +@test "advanced-ipblock check client-a (2.2.8.11) -> server" { + # Should succeed - client-a is in allowed subnet + run kubectl -n test-advanced-ipblock exec pod-client-a -- sh -c "echo x | nc -w 1 ${server_net1} 5555" + [ "$status" -eq "0" ] +} + +@test "advanced-ipblock check client-b (2.2.8.12) -> server" { + # Should succeed - client-b is in allowed subnet + run kubectl -n test-advanced-ipblock exec pod-client-b -- sh -c "echo x | nc -w 1 ${server_net1} 5555" + [ "$status" -eq "0" ] +} + +@test "advanced-ipblock check client-c (2.2.8.13) -> server" { + # Should fail - client-c is in excepted range + run kubectl -n test-advanced-ipblock exec pod-client-c -- sh -c "echo x | nc -w 1 ${server_net1} 5555" + [ "$status" -eq "1" ] +} + +@test "advanced-ipblock check client-d (2.2.8.20) -> server" { + # Should succeed - client-d is in allowed subnet but not in excepted range + run kubectl -n test-advanced-ipblock exec pod-client-d -- sh -c "echo x | nc -w 1 ${server_net1} 5555" + [ "$status" -eq "0" ] +} + +@test "advanced-ipblock check server -> client-a egress" { + # Should succeed - egress to allowed subnet + run kubectl -n test-advanced-ipblock exec pod-server -- sh -c "echo x | nc -w 1 ${client_a_net1} 5555" + [ "$status" -eq "0" ] +} + +@test "advanced-ipblock check server -> client-c egress" { + # Should fail - egress to excepted range + run kubectl -n test-advanced-ipblock exec pod-server -- sh -c "echo x | nc -w 1 ${client_c_net1} 5555" + [ "$status" -eq "1" ] +} + +@test "cleanup environments" { + # remove test manifests + kubectl delete -f advanced-ipblock-scenarios.yml + run kubectl -n test-advanced-ipblock wait --for=delete -l app=test-advanced-ipblock pod --timeout=${kubewait_timeout} + [ "$status" -eq "0" ] +} diff --git a/e2e/tests/advanced-ipblock-scenarios.yml b/e2e/tests/advanced-ipblock-scenarios.yml new file mode 100644 index 00000000..ded0b2c0 --- /dev/null +++ b/e2e/tests/advanced-ipblock-scenarios.yml @@ -0,0 +1,179 @@ +--- +apiVersion: "k8s.cni.cncf.io/v1" +kind: NetworkAttachmentDefinition +metadata: + namespace: default + name: macvlan1-advanced-ipblock +spec: + config: '{ + "cniVersion": "0.3.1", + "name": "macvlan1-advanced-ipblock", + "plugins": [ + { + "type": "macvlan", + "mode": "bridge", + "capabilities": {"ips": true }, + "ipam":{ + "type":"static" + } + }] + }' +--- +apiVersion: v1 +kind: Namespace +metadata: + name: test-advanced-ipblock +--- +# Pods +apiVersion: v1 +kind: Pod +metadata: + name: pod-server + namespace: test-advanced-ipblock + annotations: + k8s.v1.cni.cncf.io/networks: '[{ + "name": "macvlan1-advanced-ipblock", + "namespace": "default", + "ips": ["2.2.8.1/24"] + }]' + labels: + app: test-advanced-ipblock + name: pod-server +spec: + containers: + - name: server + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-kl", "0.0.0.0", "5555"] + securityContext: + privileged: true +--- +apiVersion: v1 +kind: Pod +metadata: + name: pod-client-a + namespace: test-advanced-ipblock + annotations: + k8s.v1.cni.cncf.io/networks: '[{ + "name": "macvlan1-advanced-ipblock", + "namespace": "default", + "ips": ["2.2.8.11/24"] + }]' + labels: + app: test-advanced-ipblock + name: pod-client-a +spec: + containers: + - name: client + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-kl", "0.0.0.0", "5555"] + securityContext: + privileged: true +--- +apiVersion: v1 +kind: Pod +metadata: + name: pod-client-b + namespace: test-advanced-ipblock + annotations: + k8s.v1.cni.cncf.io/networks: '[{ + "name": "macvlan1-advanced-ipblock", + "namespace": "default", + "ips": ["2.2.8.12/24"] + }]' + labels: + app: test-advanced-ipblock + name: pod-client-b +spec: + containers: + - name: client + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-kl", "0.0.0.0", "5555"] + securityContext: + privileged: true +--- +apiVersion: v1 +kind: Pod +metadata: + name: pod-client-c + namespace: test-advanced-ipblock + annotations: + k8s.v1.cni.cncf.io/networks: '[{ + "name": "macvlan1-advanced-ipblock", + "namespace": "default", + "ips": ["2.2.8.13/24"] + }]' + labels: + app: test-advanced-ipblock + name: pod-client-c +spec: + containers: + - name: client + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-kl", "0.0.0.0", "5555"] + securityContext: + privileged: true +--- +apiVersion: v1 +kind: Pod +metadata: + name: pod-client-d + namespace: test-advanced-ipblock + annotations: + k8s.v1.cni.cncf.io/networks: '[{ + "name": "macvlan1-advanced-ipblock", + "namespace": "default", + "ips": ["2.2.8.20/24"] + }]' + labels: + app: test-advanced-ipblock + name: pod-client-d +spec: + containers: + - name: client + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-kl", "0.0.0.0", "5555"] + securityContext: + privileged: true +--- +# MultiNetworkPolicies +apiVersion: k8s.cni.cncf.io/v1beta1 +kind: MultiNetworkPolicy +metadata: + name: advanced-ipblock-ingress-policy + namespace: test-advanced-ipblock + annotations: + k8s.v1.cni.cncf.io/policy-for: default/macvlan1-advanced-ipblock +spec: + podSelector: + matchLabels: + name: pod-server + policyTypes: + - Ingress + ingress: + - from: + - ipBlock: + cidr: 2.2.8.0/24 + except: + - 2.2.8.13/32 + - 2.2.8.14/32 +--- +apiVersion: k8s.cni.cncf.io/v1beta1 +kind: MultiNetworkPolicy +metadata: + name: advanced-ipblock-egress-policy + namespace: test-advanced-ipblock + annotations: + k8s.v1.cni.cncf.io/policy-for: default/macvlan1-advanced-ipblock +spec: + podSelector: + matchLabels: + name: pod-server + policyTypes: + - Egress + egress: + - to: + - ipBlock: + cidr: 2.2.8.0/24 + except: + - 2.2.8.13/32 + - 2.2.8.14/32 diff --git a/e2e/tests/bond-cni.bats b/e2e/tests/bond-cni.bats index 15d679a9..ddda7d49 100755 --- a/e2e/tests/bond-cni.bats +++ b/e2e/tests/bond-cni.bats @@ -20,6 +20,20 @@ setup() { sleep 5 } +@test "check generated nftables rules" { + # wait for sync + sleep 5 + + run has_nftables_table "bond-testing" "pod-a" + [ "$status" -eq "0" ] + + run has_nftables_table "bond-testing" "pod-b" + [ "$status" -eq "1" ] + + run has_nftables_table "bond-testing" "pod-c" + [ "$status" -eq "1" ] +} + @test "bond-testing check pod-b -> pod-a" { run kubectl -n bond-testing exec pod-b -- sh -c "echo x | nc -w 1 ${pod_a_net1} 5555" [ "$status" -eq "0" ] diff --git a/e2e/tests/bond-cni.yml b/e2e/tests/bond-cni.yml index feca9960..3567f75a 100644 --- a/e2e/tests/bond-cni.yml +++ b/e2e/tests/bond-cni.yml @@ -4,7 +4,7 @@ kind: NetworkAttachmentDefinition metadata: namespace: default name: macvlan-nad -spec: +spec: config: '{ "cniVersion": "0.3.1", "name": "macvlan1", @@ -16,7 +16,7 @@ kind: NetworkAttachmentDefinition metadata: namespace: default name: bond-nad -spec: +spec: config: '{ "cniVersion": "0.3.1", "name": "test-nad", @@ -41,7 +41,7 @@ spec: apiVersion: v1 kind: Namespace metadata: - name: bond-testing + name: bond-testing --- # Pods apiVersion: v1 @@ -59,11 +59,11 @@ metadata: name: pod-a spec: containers: - - name: macvlan-worker1 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-klp", "5555"] - securityContext: - privileged: true + - name: macvlan-worker1 + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-klp", "5555"] + securityContext: + privileged: true --- apiVersion: v1 kind: Pod @@ -80,11 +80,11 @@ metadata: name: pod-b spec: containers: - - name: macvlan-worker1 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-klp", "5555"] - securityContext: - privileged: true + - name: macvlan-worker1 + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-klp", "5555"] + securityContext: + privileged: true --- apiVersion: v1 kind: Pod @@ -101,11 +101,11 @@ metadata: name: pod-c spec: containers: - - name: macvlan-worker1 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-klp", "5555"] - securityContext: - privileged: true + - name: macvlan-worker1 + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-klp", "5555"] + securityContext: + privileged: true --- # MultiNetworkPolicies apiVersion: k8s.cni.cncf.io/v1beta1 @@ -120,12 +120,12 @@ spec: matchLabels: name: pod-a ingress: - - from: - - podSelector: - matchLabels: - name: pod-b + - from: + - podSelector: + matchLabels: + name: pod-b egress: - - to: - - podSelector: - matchLabels: - name: pod-c + - to: + - podSelector: + matchLabels: + name: pod-c diff --git a/e2e/tests/common.bash b/e2e/tests/common.bash index 2a8de9c2..a90c99a0 100644 --- a/e2e/tests/common.bash +++ b/e2e/tests/common.bash @@ -19,3 +19,13 @@ get_net1_ip6() { echo "unknown ip $1" fi } + +# Check if nftables multi_networkpolicy table exists in a pod +has_nftables_table() { + if [ "$#" == "2" ]; then + kubectl exec -n $1 "$2" -- sh -c "nft list table inet multi_networkpolicy >/dev/null 2>&1" + return $? + else + return 1 + fi +} diff --git a/e2e/tests/complex-port-specifications.bats b/e2e/tests/complex-port-specifications.bats new file mode 100755 index 00000000..b230e323 --- /dev/null +++ b/e2e/tests/complex-port-specifications.bats @@ -0,0 +1,103 @@ +#!/usr/bin/env bats + +# Note: +# These test cases verify complex port specifications including named ports, +# port ranges with specific protocols, and mixed port configurations. + +setup() { + cd $BATS_TEST_DIRNAME + load "common" + server_net1=$(get_net1_ip "test-complex-ports" "pod-server") + client_a_net1=$(get_net1_ip "test-complex-ports" "pod-client-a") + client_b_net1=$(get_net1_ip "test-complex-ports" "pod-client-b") + client_c_net1=$(get_net1_ip "test-complex-ports" "pod-client-c") +} + +@test "setup complex port specifications test environments" { + # create test manifests + kubectl create -f complex-port-specifications.yml + + # verify all pods are available + run kubectl -n test-complex-ports wait --for=condition=ready -l app=test-complex-ports pod --timeout=${kubewait_timeout} + [ "$status" -eq "0" ] + + sleep 5 +} + +@test "check generated nftables rules" { + # wait for sync + sleep 5 + + run has_nftables_table "test-complex-ports" "pod-server" + [ "$status" -eq "0" ] + + run has_nftables_table "test-complex-ports" "pod-client-a" + [ "$status" -eq "1" ] + + run has_nftables_table "test-complex-ports" "pod-client-b" + [ "$status" -eq "1" ] + + run has_nftables_table "test-complex-ports" "pod-client-c" + [ "$status" -eq "1" ] +} + +@test "complex-ports check client-a -> server HTTP (80)" { + # Should succeed - HTTP port is allowed + run kubectl -n test-complex-ports exec pod-client-a -- sh -c "echo x | nc -w 1 ${server_net1} 80" + [ "$status" -eq "0" ] +} + +@test "complex-ports check client-a -> server HTTPS (443)" { + # Should succeed - HTTPS port is allowed + run kubectl -n test-complex-ports exec pod-client-a -- sh -c "echo x | nc -w 1 ${server_net1} 443" + [ "$status" -eq "0" ] +} + +@test "complex-ports check client-a -> server SSH (22)" { + # Should succeed - SSH port is allowed + run kubectl -n test-complex-ports exec pod-client-a -- sh -c "echo x | nc -w 1 ${server_net1} 22" + [ "$status" -eq "0" ] +} + +@test "complex-ports check client-a -> server DNS (53)" { + # Should succeed - DNS port is allowed + run kubectl -n test-complex-ports exec pod-client-a -- sh -c "echo x | nc -u -w 1 ${server_net1} 53" + [ "$status" -eq "0" ] +} + +@test "complex-ports check client-a -> server blocked port (8080)" { + # Should fail - port 8080 is not allowed + run kubectl -n test-complex-ports exec pod-client-a -- sh -c "echo x | nc -w 1 ${server_net1} 8080" + [ "$status" -eq "1" ] +} + +@test "complex-ports check client-b -> server HTTP (80)" { + # Should fail - client-b is not allowed + run kubectl -n test-complex-ports exec pod-client-b -- sh -c "echo x | nc -w 1 ${server_net1} 80" + [ "$status" -eq "1" ] +} + +@test "complex-ports check client-c -> server HTTP (80)" { + # Should succeed - client-c is allowed + run kubectl -n test-complex-ports exec pod-client-c -- sh -c "echo x | nc -w 1 ${server_net1} 80" + [ "$status" -eq "0" ] +} + +@test "complex-ports check server -> client-a egress" { + # Should succeed - egress to client-a is allowed + run kubectl -n test-complex-ports exec pod-server -- sh -c "echo x | nc -w 1 ${client_a_net1} 80" + [ "$status" -eq "0" ] +} + +@test "complex-ports check server -> client-b egress" { + # Should fail - egress to client-b is not allowed + run kubectl -n test-complex-ports exec pod-server -- sh -c "echo x | nc -w 1 ${client_b_net1} 80" + [ "$status" -eq "1" ] +} + +@test "cleanup environments" { + # remove test manifests + kubectl delete -f complex-port-specifications.yml + run kubectl -n test-complex-ports wait --for=delete -l app=test-complex-ports pod --timeout=${kubewait_timeout} + [ "$status" -eq "0" ] +} diff --git a/e2e/tests/complex-port-specifications.yml b/e2e/tests/complex-port-specifications.yml new file mode 100644 index 00000000..4149ddcc --- /dev/null +++ b/e2e/tests/complex-port-specifications.yml @@ -0,0 +1,171 @@ +--- +apiVersion: "k8s.cni.cncf.io/v1" +kind: NetworkAttachmentDefinition +metadata: + namespace: default + name: macvlan1-complex-ports +spec: + config: '{ + "cniVersion": "0.3.1", + "name": "macvlan1-complex-ports", + "plugins": [ + { + "type": "macvlan", + "mode": "bridge", + "ipam":{ + "type":"host-local", + "subnet":"2.2.7.0/24", + "rangeStart":"2.2.7.8", + "rangeEnd":"2.2.7.67" + } + }] + }' +--- +# namespace for MultiNetworkPolicy +apiVersion: v1 +kind: Namespace +metadata: + name: test-complex-ports +--- +# Pods +apiVersion: v1 +kind: Pod +metadata: + name: pod-server + namespace: test-complex-ports + annotations: + k8s.v1.cni.cncf.io/networks: default/macvlan1-complex-ports + labels: + app: test-complex-ports + name: pod-server +spec: + containers: + - name: web-server + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["sh", "-c", "nc -klp 80 & nc -klp 443 & nc -klp 22 & nc -lu 53 & nc -klp 8080 & wait"] + ports: + - name: http + containerPort: 80 + protocol: TCP + - name: https + containerPort: 443 + protocol: TCP + - name: ssh + containerPort: 22 + protocol: TCP + - name: dns + containerPort: 53 + protocol: UDP + - name: custom + containerPort: 8080 + protocol: TCP + securityContext: + privileged: true +--- +apiVersion: v1 +kind: Pod +metadata: + name: pod-client-a + namespace: test-complex-ports + annotations: + k8s.v1.cni.cncf.io/networks: default/macvlan1-complex-ports + labels: + app: test-complex-ports + name: pod-client-a + role: allowed-client +spec: + containers: + - name: client + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-klp", "80"] + securityContext: + privileged: true +--- +apiVersion: v1 +kind: Pod +metadata: + name: pod-client-b + namespace: test-complex-ports + annotations: + k8s.v1.cni.cncf.io/networks: default/macvlan1-complex-ports + labels: + app: test-complex-ports + name: pod-client-b + role: blocked-client +spec: + containers: + - name: client + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-klp", "80"] + securityContext: + privileged: true +--- +apiVersion: v1 +kind: Pod +metadata: + name: pod-client-c + namespace: test-complex-ports + annotations: + k8s.v1.cni.cncf.io/networks: default/macvlan1-complex-ports + labels: + app: test-complex-ports + name: pod-client-c + role: allowed-client +spec: + containers: + - name: client + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-klp", "80"] + securityContext: + privileged: true +--- +# MultiNetworkPolicies +apiVersion: k8s.cni.cncf.io/v1beta1 +kind: MultiNetworkPolicy +metadata: + name: complex-port-ingress-policy + namespace: test-complex-ports + annotations: + k8s.v1.cni.cncf.io/policy-for: default/macvlan1-complex-ports +spec: + podSelector: + matchLabels: + name: pod-server + policyTypes: + - Ingress + ingress: + - from: + - podSelector: + matchLabels: + role: allowed-client + ports: + - port: 80 + protocol: TCP + - port: 443 + protocol: TCP + - port: 22 + protocol: TCP + - port: 53 + protocol: UDP +--- +apiVersion: k8s.cni.cncf.io/v1beta1 +kind: MultiNetworkPolicy +metadata: + name: complex-port-egress-policy + namespace: test-complex-ports + annotations: + k8s.v1.cni.cncf.io/policy-for: default/macvlan1-complex-ports +spec: + podSelector: + matchLabels: + name: pod-server + policyTypes: + - Egress + egress: + - to: + - podSelector: + matchLabels: + role: allowed-client + ports: + - port: 80 + protocol: TCP diff --git a/e2e/tests/custom-rules.bats b/e2e/tests/custom-rules.bats new file mode 100755 index 00000000..59f356de --- /dev/null +++ b/e2e/tests/custom-rules.bats @@ -0,0 +1,84 @@ +#!/usr/bin/env bats + +# Note: +# This test verifies that custom rules from config maps are properly applied. +# The custom rules allow traffic on port 9999 and from specific IP ranges. +# This test ensures that custom rules work alongside regular network policies. + +setup() { + cd $BATS_TEST_DIRNAME + load "common" + server_net1=$(get_net1_ip "test-custom-rules" "pod-server") + client_a_net1=$(get_net1_ip "test-custom-rules" "pod-client-a") + client_b_net1=$(get_net1_ip "test-custom-rules" "pod-client-b") +} + +@test "setup custom rules test environment" { + # create test manifests + kubectl create -f custom-rules.yml + + # verify all pods are available + run kubectl -n test-custom-rules wait --for=condition=ready -l app=test-custom-rules pod --timeout=${kubewait_timeout} + [ "$status" -eq "0" ] + + # wait for sync + sleep 5 +} + +@test "check generated nftables rules" { + # wait for sync + sleep 5 + + run has_nftables_table "test-custom-rules" "pod-server" + [ "$status" -eq "0" ] + + run has_nftables_table "test-custom-rules" "pod-client-a" + [ "$status" -eq "1" ] + + run has_nftables_table "test-custom-rules" "pod-client-b" + [ "$status" -eq "1" ] +} + +# Test custom rules functionality +@test "custom-rules check client-a -> server on port 80 (should fail - regular policy blocks)" { + # Should fail - regular policy only allows port 8080 + run kubectl -n test-custom-rules exec pod-client-a -- sh -c "echo x | nc -w 1 ${server_net1} 80" + [ "$status" -eq "1" ] +} + +@test "custom-rules check client-a -> server on port 8080 (should succeed - regular policy allows)" { + # Should succeed - regular policy allows port 8080 + run kubectl -n test-custom-rules exec pod-client-a -- sh -c "echo x | nc -w 1 ${server_net1} 8080" + [ "$status" -eq "0" ] +} + +@test "custom-rules check client-a -> server on port 9999 (should succeed - custom rule allows)" { + # Should succeed - custom rule allows port 9999 + run kubectl -n test-custom-rules exec pod-client-a -- sh -c "echo x | nc -w 1 ${server_net1} 9999" + [ "$status" -eq "0" ] +} + +@test "custom-rules check client-b -> server on port 80 (should fail - regular policy blocks)" { + # Should fail - regular policy only allows port 8080 + run kubectl -n test-custom-rules exec pod-client-b -- sh -c "echo x | nc -w 1 ${server_net1} 80" + [ "$status" -eq "1" ] +} + +@test "custom-rules check client-b -> server on port 8080 (should fail - regular policy blocks client-b)" { + # Should fail - regular policy only allows client-a + run kubectl -n test-custom-rules exec pod-client-b -- sh -c "echo x | nc -w 1 ${server_net1} 8080" + [ "$status" -eq "1" ] +} + +@test "custom-rules check client-b -> server on port 9999 (should succeed - custom rule allows)" { + # Should succeed - custom rule allows port 9999 regardless of regular policy + run kubectl -n test-custom-rules exec pod-client-b -- sh -c "echo x | nc -w 1 ${server_net1} 9999" + [ "$status" -eq "0" ] +} + +@test "cleanup environments" { + # remove test manifests + kubectl delete -f custom-rules.yml + run kubectl -n test-custom-rules wait --for=delete -l app=test-custom-rules pod --timeout=${kubewait_timeout} + [ "$status" -eq "0" ] +} diff --git a/e2e/tests/custom-rules.yml b/e2e/tests/custom-rules.yml new file mode 100644 index 00000000..c08e5fb1 --- /dev/null +++ b/e2e/tests/custom-rules.yml @@ -0,0 +1,113 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: test-custom-rules + labels: + app: test-custom-rules +--- +apiVersion: k8s.cni.cncf.io/v1 +kind: NetworkAttachmentDefinition +metadata: + name: macvlan1-custom-rules + namespace: test-custom-rules +spec: + config: | + { + "cniVersion": "0.3.1", + "type": "macvlan", + "master": "eth0", + "mode": "bridge", + "ipam": { + "type": "host-local", + "subnet": "192.168.200.0/24", + "rangeStart": "192.168.200.10", + "rangeEnd": "192.168.200.50", + "routes": [ + { "dst": "0.0.0.0/0" } + ], + "gateway": "192.168.200.1" + } + } +--- +apiVersion: v1 +kind: Pod +metadata: + name: pod-server + namespace: test-custom-rules + labels: + app: test-custom-rules + name: pod-server + annotations: + k8s.v1.cni.cncf.io/networks: macvlan1-custom-rules +spec: + containers: + - name: web-server + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["sh", "-c", "nc -klp 80 & nc -klp 8080 & nc -klp 9999 & wait"] + securityContext: + privileged: true + ports: + - name: http + containerPort: 80 + - name: http-alt + containerPort: 8080 + - name: custom + containerPort: 9999 +--- +apiVersion: v1 +kind: Pod +metadata: + name: pod-client-a + namespace: test-custom-rules + labels: + app: test-custom-rules + name: pod-client-a + role: allowed-client + annotations: + k8s.v1.cni.cncf.io/networks: macvlan1-custom-rules +spec: + containers: + - name: client + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["sh", "-c", "sleep 3600"] +--- +apiVersion: v1 +kind: Pod +metadata: + name: pod-client-b + namespace: test-custom-rules + labels: + app: test-custom-rules + name: pod-client-b + role: blocked-client + annotations: + k8s.v1.cni.cncf.io/networks: macvlan1-custom-rules +spec: + containers: + - name: client + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["sh", "-c", "sleep 3600"] +--- +# Regular policy that only allows client-a on port 8080 +apiVersion: k8s.cni.cncf.io/v1beta1 +kind: MultiNetworkPolicy +metadata: + name: custom-rules-policy + namespace: test-custom-rules + annotations: + k8s.v1.cni.cncf.io/policy-for: macvlan1-custom-rules +spec: + podSelector: + matchLabels: + name: pod-server + policyTypes: + - Ingress + ingress: + - from: + - podSelector: + matchLabels: + role: allowed-client + ports: + - port: 8080 + protocol: TCP diff --git a/e2e/tests/deny-all-with-specific-allow.bats b/e2e/tests/deny-all-with-specific-allow.bats new file mode 100755 index 00000000..d18f72db --- /dev/null +++ b/e2e/tests/deny-all-with-specific-allow.bats @@ -0,0 +1,88 @@ +#!/usr/bin/env bats + +# Note: +# This test verifies that a deny-all policy can be overridden by a specific allow policy. +# The test shows that even though there's a deny-all policy, traffic succeeds due to +# the specific allow policy, demonstrating the additive nature of network policies. + +setup() { + cd $BATS_TEST_DIRNAME + load "common" + server_net1=$(get_net1_ip "test-deny-all" "pod-server") + client_a_net1=$(get_net1_ip "test-deny-all" "pod-client-a") + client_b_net1=$(get_net1_ip "test-deny-all" "pod-client-b") + client_c_net1=$(get_net1_ip "test-deny-all" "pod-client-c") +} + +@test "setup deny-all with specific allow test environment" { + # create test manifests + kubectl create -f deny-all-with-specific-allow.yml + + # verify all pods are available + run kubectl -n test-deny-all wait --for=condition=ready -l app=test-deny-all pod --timeout=${kubewait_timeout} + [ "$status" -eq "0" ] + + # wait for sync + sleep 5 +} + +@test "check generated nftables rules" { + # wait for sync + sleep 5 + + run has_nftables_table "test-deny-all" "pod-server" + [ "$status" -eq "0" ] + + run has_nftables_table "test-deny-all" "pod-client-a" + [ "$status" -eq "1" ] + + run has_nftables_table "test-deny-all" "pod-client-b" + [ "$status" -eq "1" ] + + run has_nftables_table "test-deny-all" "pod-client-c" + [ "$status" -eq "1" ] +} + +# Test deny-all policy behavior +@test "deny-all check client-a -> server on port 80 (should succeed - specific allow policy overrides deny-all)" { + # Should succeed - specific allow policy allows client-a on port 80 + run kubectl -n test-deny-all exec pod-client-a -- sh -c "echo x | nc -w 1 ${server_net1} 80" + [ "$status" -eq "0" ] +} + +@test "deny-all check client-a -> server on port 8080 (should fail - specific allow policy only allows port 80)" { + # Should fail - specific allow policy only allows port 80 + run kubectl -n test-deny-all exec pod-client-a -- sh -c "echo x | nc -w 1 ${server_net1} 8080" + [ "$status" -eq "1" ] +} + +@test "deny-all check client-b -> server on port 80 (should fail - specific allow policy only allows client-a)" { + # Should fail - specific allow policy only allows client-a + run kubectl -n test-deny-all exec pod-client-b -- sh -c "echo x | nc -w 1 ${server_net1} 80" + [ "$status" -eq "1" ] +} + +@test "deny-all check client-b -> server on port 8080 (should fail - specific allow policy only allows client-a)" { + # Should fail - specific allow policy only allows client-a + run kubectl -n test-deny-all exec pod-client-b -- sh -c "echo x | nc -w 1 ${server_net1} 8080" + [ "$status" -eq "1" ] +} + +@test "deny-all check client-c -> server on port 80 (should fail - specific allow policy only allows client-a)" { + # Should fail - specific allow policy only allows client-a + run kubectl -n test-deny-all exec pod-client-c -- sh -c "echo x | nc -w 1 ${server_net1} 80" + [ "$status" -eq "1" ] +} + +@test "deny-all check client-c -> server on port 8080 (should fail - specific allow policy only allows client-a)" { + # Should fail - specific allow policy only allows client-a + run kubectl -n test-deny-all exec pod-client-c -- sh -c "echo x | nc -w 1 ${server_net1} 8080" + [ "$status" -eq "1" ] +} + +@test "cleanup environments" { + # remove test manifests + kubectl delete -f deny-all-with-specific-allow.yml + run kubectl -n test-deny-all wait --for=delete -l app=test-deny-all pod --timeout=${kubewait_timeout} + [ "$status" -eq "0" ] +} diff --git a/e2e/tests/deny-all-with-specific-allow.yml b/e2e/tests/deny-all-with-specific-allow.yml new file mode 100644 index 00000000..2c54c2c7 --- /dev/null +++ b/e2e/tests/deny-all-with-specific-allow.yml @@ -0,0 +1,144 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: test-deny-all + labels: + app: test-deny-all +--- +apiVersion: k8s.cni.cncf.io/v1 +kind: NetworkAttachmentDefinition +metadata: + name: macvlan1-deny-all + namespace: default +spec: + config: | + { + "cniVersion": "0.3.1", + "type": "macvlan", + "master": "eth0", + "mode": "bridge", + "ipam": { + "type": "host-local", + "subnet": "192.168.210.0/24", + "rangeStart": "192.168.210.10", + "rangeEnd": "192.168.210.50", + "routes": [ + { "dst": "0.0.0.0/0" } + ], + "gateway": "192.168.210.1" + } + } +--- +apiVersion: v1 +kind: Pod +metadata: + name: pod-server + namespace: test-deny-all + labels: + app: test-deny-all + name: pod-server + annotations: + k8s.v1.cni.cncf.io/networks: default/macvlan1-deny-all +spec: + containers: + - name: web-server + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["sh", "-c", "nc -klp 80 & nc -klp 8080 & wait"] + securityContext: + privileged: true + ports: + - name: http + containerPort: 80 + - name: http-alt + containerPort: 8080 +--- +apiVersion: v1 +kind: Pod +metadata: + name: pod-client-a + namespace: test-deny-all + labels: + app: test-deny-all + name: pod-client-a + role: allowed-client + annotations: + k8s.v1.cni.cncf.io/networks: default/macvlan1-deny-all +spec: + containers: + - name: client + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["sh", "-c", "sleep 3600"] +--- +apiVersion: v1 +kind: Pod +metadata: + name: pod-client-b + namespace: test-deny-all + labels: + app: test-deny-all + name: pod-client-b + role: blocked-client + annotations: + k8s.v1.cni.cncf.io/networks: default/macvlan1-deny-all +spec: + containers: + - name: client + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["sh", "-c", "sleep 3600"] +--- +apiVersion: v1 +kind: Pod +metadata: + name: pod-client-c + namespace: test-deny-all + labels: + app: test-deny-all + name: pod-client-c + role: other-client + annotations: + k8s.v1.cni.cncf.io/networks: default/macvlan1-deny-all +spec: + containers: + - name: client + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["sh", "-c", "sleep 3600"] +--- +# Deny-all policy - blocks all traffic +apiVersion: k8s.cni.cncf.io/v1beta1 +kind: MultiNetworkPolicy +metadata: + name: deny-all-policy + namespace: test-deny-all + annotations: + k8s.v1.cni.cncf.io/policy-for: default/macvlan1-deny-all +spec: + podSelector: + matchLabels: + name: pod-server + policyTypes: + - Ingress + ingress: [] +--- +# Specific allow policy - allows client-a on port 80 +apiVersion: k8s.cni.cncf.io/v1beta1 +kind: MultiNetworkPolicy +metadata: + name: specific-allow-policy + namespace: test-deny-all + annotations: + k8s.v1.cni.cncf.io/policy-for: default/macvlan1-deny-all +spec: + podSelector: + matchLabels: + name: pod-server + policyTypes: + - Ingress + ingress: + - from: + - podSelector: + matchLabels: + role: allowed-client + ports: + - port: 80 + protocol: TCP diff --git a/e2e/tests/edge-cases-validation.bats b/e2e/tests/edge-cases-validation.bats new file mode 100755 index 00000000..1b84f2d3 --- /dev/null +++ b/e2e/tests/edge-cases-validation.bats @@ -0,0 +1,81 @@ +#!/usr/bin/env bats + +# Note: +# These test cases verify edge cases and validation scenarios including +# empty selectors, invalid configurations, and boundary conditions. + +setup() { + cd $BATS_TEST_DIRNAME + load "common" + server_net1=$(get_net1_ip "test-edge-cases" "pod-server") + client_a_net1=$(get_net1_ip "test-edge-cases" "pod-client-a") + client_b_net1=$(get_net1_ip "test-edge-cases" "pod-client-b") +} + +@test "setup edge cases test environments" { + # create test manifests + kubectl create -f edge-cases-validation.yml + + # verify all pods are available + run kubectl -n test-edge-cases wait --for=condition=ready -l app=test-edge-cases pod --timeout=${kubewait_timeout} + [ "$status" -eq "0" ] + + sleep 5 +} + +@test "check generated nftables rules" { + # wait for sync + sleep 5 + + run has_nftables_table "test-edge-cases" "pod-server" + [ "$status" -eq "0" ] + + run has_nftables_table "test-edge-cases" "pod-client-a" + [ "$status" -eq "1" ] + + run has_nftables_table "test-edge-cases" "pod-client-b" + [ "$status" -eq "1" ] +} + +@test "edge-cases check empty pod selector -> server" { + # Should fail - empty pod selector should block all traffic + run kubectl -n test-edge-cases exec pod-client-a -- sh -c "echo x | nc -w 1 ${server_net1} 5555" + [ "$status" -eq "1" ] +} + +@test "edge-cases check empty namespace selector -> server" { + # Should fail - empty namespace selector should block all traffic + run kubectl -n test-edge-cases exec pod-client-b -- sh -c "echo x | nc -w 1 ${server_net1} 5555" + [ "$status" -eq "1" ] +} + +@test "edge-cases check server -> client-a egress" { + # Should fail - empty egress selector should block all traffic + run kubectl -n test-edge-cases exec pod-server -- sh -c "echo x | nc -w 1 ${client_a_net1} 5555" + [ "$status" -eq "1" ] +} + +@test "edge-cases check server -> client-b egress" { + # Should fail - empty egress selector should block all traffic + run kubectl -n test-edge-cases exec pod-server -- sh -c "echo x | nc -w 1 ${client_b_net1} 5555" + [ "$status" -eq "1" ] +} + +@test "edge-cases check invalid port range -> server" { + # Should fail - invalid port range should be rejected + run kubectl -n test-edge-cases exec pod-client-a -- sh -c "echo x | nc -w 1 ${server_net1} 9999" + [ "$status" -eq "1" ] +} + +@test "edge-cases check invalid protocol -> server" { + # Should fail - invalid protocol should be rejected + run kubectl -n test-edge-cases exec pod-client-a -- sh -c "echo x | nc -w 1 ${server_net1} 5555" + [ "$status" -eq "1" ] +} + +@test "cleanup environments" { + # remove test manifests + kubectl delete -f edge-cases-validation.yml + run kubectl -n test-edge-cases wait --for=delete -l app=test-edge-cases pod --timeout=${kubewait_timeout} + [ "$status" -eq "0" ] +} diff --git a/e2e/tests/edge-cases-validation.yml b/e2e/tests/edge-cases-validation.yml new file mode 100644 index 00000000..d77b0188 --- /dev/null +++ b/e2e/tests/edge-cases-validation.yml @@ -0,0 +1,149 @@ +--- +apiVersion: "k8s.cni.cncf.io/v1" +kind: NetworkAttachmentDefinition +metadata: + namespace: default + name: macvlan1-edge-cases +spec: + config: '{ + "cniVersion": "0.3.1", + "name": "macvlan1-edge-cases", + "plugins": [ + { + "type": "macvlan", + "mode": "bridge", + "ipam":{ + "type":"host-local", + "subnet":"2.2.11.0/24", + "rangeStart":"2.2.11.8", + "rangeEnd":"2.2.11.67" + } + }] + }' +--- +apiVersion: v1 +kind: Namespace +metadata: + name: test-edge-cases +--- +# Pods +apiVersion: v1 +kind: Pod +metadata: + name: pod-server + namespace: test-edge-cases + annotations: + k8s.v1.cni.cncf.io/networks: default/macvlan1-edge-cases + labels: + app: test-edge-cases + name: pod-server +spec: + containers: + - name: server + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-kl", "0.0.0.0", "5555"] + securityContext: + privileged: true +--- +apiVersion: v1 +kind: Pod +metadata: + name: pod-client-a + namespace: test-edge-cases + annotations: + k8s.v1.cni.cncf.io/networks: default/macvlan1-edge-cases + labels: + app: test-edge-cases + name: pod-client-a +spec: + containers: + - name: client + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-kl", "0.0.0.0", "5555"] + securityContext: + privileged: true +--- +apiVersion: v1 +kind: Pod +metadata: + name: pod-client-b + namespace: test-edge-cases + annotations: + k8s.v1.cni.cncf.io/networks: default/macvlan1-edge-cases + labels: + app: test-edge-cases + name: pod-client-b +spec: + containers: + - name: client + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-kl", "0.0.0.0", "5555"] + securityContext: + privileged: true +--- +# MultiNetworkPolicies - Edge Cases +apiVersion: k8s.cni.cncf.io/v1beta1 +kind: MultiNetworkPolicy +metadata: + name: empty-pod-selector-policy + namespace: test-edge-cases + annotations: + k8s.v1.cni.cncf.io/policy-for: default/macvlan1-edge-cases +spec: + podSelector: + matchLabels: + name: pod-server + policyTypes: + - Ingress + ingress: + - from: + - podSelector: + matchLabels: + nonexistent: label + ports: + - port: 5555 + protocol: TCP +--- +apiVersion: k8s.cni.cncf.io/v1beta1 +kind: MultiNetworkPolicy +metadata: + name: empty-namespace-selector-policy + namespace: test-edge-cases + annotations: + k8s.v1.cni.cncf.io/policy-for: default/macvlan1-edge-cases +spec: + podSelector: + matchLabels: + name: pod-server + policyTypes: + - Ingress + ingress: + - from: + - namespaceSelector: + matchLabels: + nonexistent: label + ports: + - port: 5555 + protocol: TCP +--- +apiVersion: k8s.cni.cncf.io/v1beta1 +kind: MultiNetworkPolicy +metadata: + name: empty-egress-policy + namespace: test-edge-cases + annotations: + k8s.v1.cni.cncf.io/policy-for: default/macvlan1-edge-cases +spec: + podSelector: + matchLabels: + name: pod-server + policyTypes: + - Egress + egress: + - to: + - podSelector: + matchLabels: + nonexistent: label + ports: + - port: 5555 + protocol: TCP diff --git a/e2e/tests/ingress-ns-selector-no-pods.bats b/e2e/tests/ingress-ns-selector-no-pods.bats index 53d2def7..eae2d8fa 100755 --- a/e2e/tests/ingress-ns-selector-no-pods.bats +++ b/e2e/tests/ingress-ns-selector-no-pods.bats @@ -24,6 +24,20 @@ setup() { sleep 5 } +@test "check generated nftables rules" { + # wait for sync + sleep 5 + + run has_nftables_table "test-ingress-ns-selector-no-pods" "pod-server" + [ "$status" -eq "0" ] + + run has_nftables_table "test-ingress-ns-selector-no-pods" "pod-client-a" + [ "$status" -eq "1" ] + + run has_nftables_table "test-ingress-ns-selector-no-pods-blue" "pod-client-b" + [ "$status" -eq "1" ] +} + @test "test-ingress-ns-selector-no-pods check client-a -> server" { # nc should NOT succeed from client-a to server by policy run kubectl -n test-ingress-ns-selector-no-pods exec pod-client-a -- sh -c "echo x | nc -w 1 ${server_net1} 5555" diff --git a/e2e/tests/ingress-ns-selector-no-pods.yml b/e2e/tests/ingress-ns-selector-no-pods.yml index 9cd041fe..37c7d680 100644 --- a/e2e/tests/ingress-ns-selector-no-pods.yml +++ b/e2e/tests/ingress-ns-selector-no-pods.yml @@ -4,7 +4,7 @@ kind: NetworkAttachmentDefinition metadata: namespace: default name: macvlan1-simple -spec: +spec: config: '{ "cniVersion": "0.3.1", "name": "macvlan1-simple", @@ -21,16 +21,16 @@ spec: }] }' --- -# namespace for MultiNetworkPolicy +# namespace for MultiNetworkPolicy apiVersion: v1 kind: Namespace metadata: - name: test-ingress-ns-selector-no-pods ---- + name: test-ingress-ns-selector-no-pods +--- apiVersion: v1 kind: Namespace metadata: - name: test-ingress-ns-selector-no-pods-blue + name: test-ingress-ns-selector-no-pods-blue --- # Pods apiVersion: v1 @@ -45,11 +45,11 @@ metadata: name: pod-server spec: containers: - - name: macvlan-worker1 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-klp", "5555"] - securityContext: - privileged: true + - name: macvlan-worker1 + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-klp", "5555"] + securityContext: + privileged: true --- apiVersion: v1 kind: Pod @@ -63,11 +63,11 @@ metadata: name: pod-client-a spec: containers: - - name: macvlan-worker1 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-klp", "5555"] - securityContext: - privileged: true + - name: macvlan-worker1 + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-klp", "5555"] + securityContext: + privileged: true --- apiVersion: v1 kind: Pod @@ -81,11 +81,11 @@ metadata: name: pod-client-b spec: containers: - - name: macvlan-worker1 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-klp", "5555"] - securityContext: - privileged: true + - name: macvlan-worker1 + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-klp", "5555"] + securityContext: + privileged: true --- # MultiNetworkPolicies # this policy accepts ingress trafic from pod-client-a to pod-server @@ -101,9 +101,9 @@ spec: matchLabels: name: pod-server ingress: - - from: - - namespaceSelector: - matchLabels: - foo: bar # No namespace with this label exists + - from: + - namespaceSelector: + matchLabels: + foo: bar # No namespace with this label exists policyTypes: - - Ingress + - Ingress diff --git a/e2e/tests/ipblock-list.bats b/e2e/tests/ipblock-list.bats index c918c6a0..073028e5 100755 --- a/e2e/tests/ipblock-list.bats +++ b/e2e/tests/ipblock-list.bats @@ -22,6 +22,23 @@ setup() { sleep 3 } +@test "check generated nftables rules" { + # wait for sync + sleep 5 + + run has_nftables_table "test-ipblock-list" "pod-server" + [ "$status" -eq "0" ] + + run has_nftables_table "test-ipblock-list" "pod-client-a" + [ "$status" -eq "1" ] + + run has_nftables_table "test-ipblock-list" "pod-client-b" + [ "$status" -eq "1" ] + + run has_nftables_table "test-ipblock-list" "pod-client-c" + [ "$status" -eq "1" ] +} + @test "test-ipblock-list check client-a" { run kubectl -n test-ipblock-list exec pod-client-a -- sh -c "echo x | nc -w 1 ${server_net1} 5555" [ "$status" -eq "0" ] diff --git a/e2e/tests/ipblock-list.yml b/e2e/tests/ipblock-list.yml index 180afc02..a77ee176 100644 --- a/e2e/tests/ipblock-list.yml +++ b/e2e/tests/ipblock-list.yml @@ -22,7 +22,7 @@ spec: apiVersion: v1 kind: Namespace metadata: - name: test-ipblock-list + name: test-ipblock-list --- # Pods apiVersion: v1 @@ -41,11 +41,11 @@ metadata: name: pod-server spec: containers: - - name: macvlan-worker1 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-kl", "0.0.0.0", "5555"] - securityContext: - privileged: true + - name: macvlan-worker1 + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-kl", "0.0.0.0", "5555"] + securityContext: + privileged: true --- apiVersion: v1 kind: Pod @@ -63,11 +63,11 @@ metadata: name: pod-client-a spec: containers: - - name: macvlan-worker1 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-kl", "0.0.0.0", "5555"] - securityContext: - privileged: true + - name: macvlan-worker1 + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-kl", "0.0.0.0", "5555"] + securityContext: + privileged: true --- apiVersion: v1 kind: Pod @@ -85,11 +85,11 @@ metadata: name: pod-client-b spec: containers: - - name: macvlan-worker1 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-kl", "0.0.0.0", "5555"] - securityContext: - privileged: true + - name: macvlan-worker1 + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-kl", "0.0.0.0", "5555"] + securityContext: + privileged: true --- apiVersion: v1 kind: Pod @@ -107,11 +107,11 @@ metadata: name: pod-client-c spec: containers: - - name: macvlan-worker1 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-kl", "0.0.0.0", "5555"] - securityContext: - privileged: true + - name: macvlan-worker1 + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-kl", "0.0.0.0", "5555"] + securityContext: + privileged: true --- # MultiNetworkPolicies # this policy accepts ingress trafic from pod-client-a to pod-server @@ -130,11 +130,11 @@ spec: matchLabels: name: pod-server policyTypes: - - Ingress + - Ingress ingress: - - from: - - ipBlock: - cidr: 2.2.5.11/32 - - from: - - ipBlock: - cidr: 2.2.5.12/32 + - from: + - ipBlock: + cidr: 2.2.5.11/32 + - from: + - ipBlock: + cidr: 2.2.5.12/32 diff --git a/e2e/tests/ipblock-stacked.bats b/e2e/tests/ipblock-stacked.bats index 92b5ac2c..5f334b50 100755 --- a/e2e/tests/ipblock-stacked.bats +++ b/e2e/tests/ipblock-stacked.bats @@ -22,17 +22,21 @@ setup() { sleep 3 } -@test "check generated iptables rules" { +@test "check generated nftables rules" { # wait for sync sleep 5 - run kubectl -n test-ipblock-stacked exec pod-server -it -- sh -c "iptables-save | grep MULTI-0-INGRESS" - [ "$status" -eq "0" ] - run kubectl -n test-ipblock-stacked exec pod-client-a -it -- sh -c "iptables-save | grep MULTI-0-INGRESS" - [ "$status" -eq "1" ] - run kubectl -n test-ipblock-stacked exec pod-client-b -it -- sh -c "iptables-save | grep MULTI-0-INGRESS" - [ "$status" -eq "1" ] - run kubectl -n test-ipblock-stacked exec pod-client-c -it -- sh -c "iptables-save | grep MULTI-0-INGRESS" - [ "$status" -eq "1" ] + + run has_nftables_table "test-ipblock-stacked" "pod-server" + [ "$status" -eq "0" ] + + run has_nftables_table "test-ipblock-stacked" "pod-client-a" + [ "$status" -eq "1" ] + + run has_nftables_table "test-ipblock-stacked" "pod-client-b" + [ "$status" -eq "1" ] + + run has_nftables_table "test-ipblock-stacked" "pod-client-c" + [ "$status" -eq "1" ] } @test "test-ipblock-stacked check client-a" { diff --git a/e2e/tests/ipblock-stacked.yml b/e2e/tests/ipblock-stacked.yml index 6308586d..0f42d6b7 100644 --- a/e2e/tests/ipblock-stacked.yml +++ b/e2e/tests/ipblock-stacked.yml @@ -22,7 +22,7 @@ spec: apiVersion: v1 kind: Namespace metadata: - name: test-ipblock-stacked + name: test-ipblock-stacked --- # Pods apiVersion: v1 @@ -41,11 +41,11 @@ metadata: name: pod-server spec: containers: - - name: macvlan-worker1 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-kl", "0.0.0.0", "5555"] - securityContext: - privileged: true + - name: macvlan-worker1 + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-kl", "0.0.0.0", "5555"] + securityContext: + privileged: true --- apiVersion: v1 kind: Pod @@ -63,11 +63,11 @@ metadata: name: pod-client-a spec: containers: - - name: macvlan-worker1 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-kl", "0.0.0.0", "5555"] - securityContext: - privileged: true + - name: macvlan-worker1 + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-kl", "0.0.0.0", "5555"] + securityContext: + privileged: true --- apiVersion: v1 kind: Pod @@ -85,11 +85,11 @@ metadata: name: pod-client-b spec: containers: - - name: macvlan-worker1 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-kl", "0.0.0.0", "5555"] - securityContext: - privileged: true + - name: macvlan-worker1 + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-kl", "0.0.0.0", "5555"] + securityContext: + privileged: true --- apiVersion: v1 kind: Pod @@ -107,11 +107,11 @@ metadata: name: pod-client-c spec: containers: - - name: macvlan-worker1 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-kl", "0.0.0.0", "5555"] - securityContext: - privileged: true + - name: macvlan-worker1 + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-kl", "0.0.0.0", "5555"] + securityContext: + privileged: true --- # MultiNetworkPolicies # this policy accepts ingress trafic from pod-client-a to pod-server @@ -130,11 +130,11 @@ spec: matchLabels: name: pod-server policyTypes: - - Ingress + - Ingress ingress: - - from: - - ipBlock: - cidr: 2.2.5.11/32 + - from: + - ipBlock: + cidr: 2.2.5.11/32 --- # MultiNetworkPolicies # this policy accepts ingress trafic from pod-client-a to pod-server @@ -153,8 +153,8 @@ spec: matchLabels: name: pod-server policyTypes: - - Ingress + - Ingress ingress: - - from: - - ipBlock: - cidr: 2.2.5.12/32 + - from: + - ipBlock: + cidr: 2.2.5.12/32 diff --git a/e2e/tests/ipblock.bats b/e2e/tests/ipblock.bats index 0f005d92..252e4d58 100755 --- a/e2e/tests/ipblock.bats +++ b/e2e/tests/ipblock.bats @@ -22,17 +22,21 @@ setup() { sleep 3 } -@test "check generated iptables rules" { +@test "check generated nftables rules" { # wait for sync sleep 5 - run kubectl -n test-ipblock exec pod-server -it -- sh -c "iptables-save | grep MULTI-0-INGRESS" - [ "$status" -eq "0" ] - run kubectl -n test-ipblock exec pod-client-a -it -- sh -c "iptables-save | grep MULTI-0-INGRESS" - [ "$status" -eq "1" ] - run kubectl -n test-ipblock exec pod-client-b -it -- sh -c "iptables-save | grep MULTI-0-INGRESS" - [ "$status" -eq "1" ] - run kubectl -n test-ipblock exec pod-client-c -it -- sh -c "iptables-save | grep MULTI-0-INGRESS" - [ "$status" -eq "1" ] + + run has_nftables_table "test-ipblock" "pod-server" + [ "$status" -eq "0" ] + + run has_nftables_table "test-ipblock" "pod-client-a" + [ "$status" -eq "1" ] + + run has_nftables_table "test-ipblock" "pod-client-b" + [ "$status" -eq "1" ] + + run has_nftables_table "test-ipblock" "pod-client-c" + [ "$status" -eq "1" ] } @test "test-ipblock check client-a" { diff --git a/e2e/tests/ipblock.yml b/e2e/tests/ipblock.yml index 5d1cf5e7..d0390992 100644 --- a/e2e/tests/ipblock.yml +++ b/e2e/tests/ipblock.yml @@ -22,7 +22,7 @@ spec: apiVersion: v1 kind: Namespace metadata: - name: test-ipblock + name: test-ipblock --- # Pods apiVersion: v1 @@ -41,11 +41,11 @@ metadata: name: pod-server spec: containers: - - name: macvlan-worker1 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-kl", "0.0.0.0", "5555"] - securityContext: - privileged: true + - name: macvlan-worker1 + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-kl", "0.0.0.0", "5555"] + securityContext: + privileged: true --- apiVersion: v1 kind: Pod @@ -63,11 +63,11 @@ metadata: name: pod-client-a spec: containers: - - name: macvlan-worker1 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-kl", "0.0.0.0", "5555"] - securityContext: - privileged: true + - name: macvlan-worker1 + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-kl", "0.0.0.0", "5555"] + securityContext: + privileged: true --- apiVersion: v1 kind: Pod @@ -85,11 +85,11 @@ metadata: name: pod-client-b spec: containers: - - name: macvlan-worker1 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-kl", "0.0.0.0", "5555"] - securityContext: - privileged: true + - name: macvlan-worker1 + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-kl", "0.0.0.0", "5555"] + securityContext: + privileged: true --- apiVersion: v1 kind: Pod @@ -107,11 +107,11 @@ metadata: name: pod-client-c spec: containers: - - name: macvlan-worker1 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-kl", "0.0.0.0", "5555"] - securityContext: - privileged: true + - name: macvlan-worker1 + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-kl", "0.0.0.0", "5555"] + securityContext: + privileged: true --- # MultiNetworkPolicies # this policy accepts ingress trafic from pod-client-a to pod-server @@ -130,10 +130,10 @@ spec: matchLabels: name: pod-server policyTypes: - - Ingress + - Ingress ingress: - - from: - - ipBlock: - cidr: 2.2.5.11/32 - - ipBlock: - cidr: 2.2.5.12/32 + - from: + - ipBlock: + cidr: 2.2.5.11/32 + - ipBlock: + cidr: 2.2.5.12/32 diff --git a/e2e/tests/mixed-selector-policies.bats b/e2e/tests/mixed-selector-policies.bats new file mode 100755 index 00000000..c9485af3 --- /dev/null +++ b/e2e/tests/mixed-selector-policies.bats @@ -0,0 +1,137 @@ +#!/usr/bin/env bats + +# Note: +# These test cases verify mixed selector policies combining pod selectors and namespace selectors. +# +# Test Setup: +# - Policy 1 (AND): role=allowed-client AND environment=staging (ingress only) +# - Policy 2 (OR): role=allowed-client OR environment=staging (ingress only) +# - Policy 3 (AND): role=allowed-client AND environment=staging (egress only) +# +# Pods: +# - client-a: role=allowed-client, production namespace (should succeed ingress via OR policy) +# - client-b: role=blocked-client, production namespace (should fail ingress) +# - client-c: role=allowed-client, staging namespace (should succeed ingress via both policies) +# - client-d: role=blocked-client, staging namespace (should succeed ingress via OR policy) +# - client-e: role=other-client, staging namespace (should succeed ingress via OR policy) +# +# Egress only allows: role=allowed-client AND environment=staging (client-c only) + +setup() { + cd $BATS_TEST_DIRNAME + load "common" + server_net1=$(get_net1_ip "test-mixed-selectors" "pod-server") + client_a_net1=$(get_net1_ip "test-mixed-selectors" "pod-client-a") + client_b_net1=$(get_net1_ip "test-mixed-selectors" "pod-client-b") + client_c_net1=$(get_net1_ip "test-mixed-selectors-blue" "pod-client-c") + client_d_net1=$(get_net1_ip "test-mixed-selectors-blue" "pod-client-d") + client_e_net1=$(get_net1_ip "test-mixed-selectors-blue" "pod-client-e") +} + +@test "setup mixed selector test environments" { + # create test manifests + kubectl create -f mixed-selector-policies.yml + + # verify all pods are available + run kubectl -n test-mixed-selectors wait --for=condition=ready -l app=test-mixed-selectors pod --timeout=${kubewait_timeout} + [ "$status" -eq "0" ] + + run kubectl -n test-mixed-selectors-blue wait --for=condition=ready -l app=test-mixed-selectors pod --timeout=${kubewait_timeout} + [ "$status" -eq "0" ] + + sleep 5 +} + +@test "check generated nftables rules" { + # wait for sync + sleep 5 + + run has_nftables_table "test-mixed-selectors" "pod-server" + [ "$status" -eq "0" ] + + run has_nftables_table "test-mixed-selectors" "pod-client-a" + [ "$status" -eq "1" ] + + run has_nftables_table "test-mixed-selectors" "pod-client-b" + [ "$status" -eq "1" ] + + run has_nftables_table "test-mixed-selectors-blue" "pod-client-c" + [ "$status" -eq "1" ] + + run has_nftables_table "test-mixed-selectors-blue" "pod-client-d" + [ "$status" -eq "1" ] + + run has_nftables_table "test-mixed-selectors-blue" "pod-client-e" + [ "$status" -eq "1" ] +} + +# Test Policy 1 (AND condition): role=allowed-client AND environment=staging +@test "mixed-selectors check client-a (allowed role, production namespace) -> server" { + # Should succeed - client-a has allowed role (OR policy allows this) + run kubectl -n test-mixed-selectors exec pod-client-a -- sh -c "echo x | nc -w 1 ${server_net1} 5555" + [ "$status" -eq "0" ] +} + +@test "mixed-selectors check client-b (blocked role, production namespace) -> server" { + # Should fail - client-b has blocked role and is in production namespace + run kubectl -n test-mixed-selectors exec pod-client-b -- sh -c "echo x | nc -w 1 ${server_net1} 5555" + [ "$status" -eq "1" ] +} + +@test "mixed-selectors check client-c (allowed role, staging namespace) -> server" { + # Should succeed - client-c has allowed role AND is in staging namespace (both policies allow this) + run kubectl -n test-mixed-selectors-blue exec pod-client-c -- sh -c "echo x | nc -w 1 ${server_net1} 5555" + [ "$status" -eq "0" ] +} + +@test "mixed-selectors check client-d (blocked role, staging namespace) -> server" { + # Should succeed - client-d is in staging namespace (OR policy allows this) + run kubectl -n test-mixed-selectors-blue exec pod-client-d -- sh -c "echo x | nc -w 1 ${server_net1} 5555" + [ "$status" -eq "0" ] +} + +@test "mixed-selectors check client-e (other role, staging namespace) -> server" { + # Should succeed - client-e is in staging namespace (OR policy allows this) + run kubectl -n test-mixed-selectors-blue exec pod-client-e -- sh -c "echo x | nc -w 1 ${server_net1} 5555" + [ "$status" -eq "0" ] +} + +# Egress tests - only AND policy applies (role=allowed-client AND environment=staging) +@test "mixed-selectors check server -> client-a egress (allowed role, production namespace)" { + # Should fail - egress to client-a is in production namespace, not staging + run kubectl -n test-mixed-selectors exec pod-server -- sh -c "echo x | nc -w 1 ${client_a_net1} 5555" + [ "$status" -eq "1" ] +} + +@test "mixed-selectors check server -> client-b egress (blocked role, production namespace)" { + # Should fail - egress to client-b is in production namespace, not staging + run kubectl -n test-mixed-selectors exec pod-server -- sh -c "echo x | nc -w 1 ${client_b_net1} 5555" + [ "$status" -eq "1" ] +} + +@test "mixed-selectors check server -> client-c egress (allowed role, staging namespace)" { + # Should succeed - egress to client-c has correct role AND is in staging namespace + run kubectl -n test-mixed-selectors exec pod-server -- sh -c "echo x | nc -w 1 ${client_c_net1} 5555" + [ "$status" -eq "0" ] +} + +@test "mixed-selectors check server -> client-d egress (blocked role, staging namespace)" { + # Should fail - egress to client-d is in staging namespace but wrong role + run kubectl -n test-mixed-selectors exec pod-server -- sh -c "echo x | nc -w 1 ${client_d_net1} 5555" + [ "$status" -eq "1" ] +} + +@test "mixed-selectors check server -> client-e egress (other role, staging namespace)" { + # Should fail - egress to client-e is in staging namespace but wrong role + run kubectl -n test-mixed-selectors exec pod-server -- sh -c "echo x | nc -w 1 ${client_e_net1} 5555" + [ "$status" -eq "1" ] +} + +@test "cleanup environments" { + # remove test manifests + kubectl delete -f mixed-selector-policies.yml + run kubectl -n test-mixed-selectors wait --for=delete -l app=test-mixed-selectors pod --timeout=${kubewait_timeout} + [ "$status" -eq "0" ] + run kubectl -n test-mixed-selectors-blue wait --for=delete -l app=test-mixed-selectors pod --timeout=${kubewait_timeout} + [ "$status" -eq "0" ] +} diff --git a/e2e/tests/mixed-selector-policies.yml b/e2e/tests/mixed-selector-policies.yml new file mode 100644 index 00000000..a21101e9 --- /dev/null +++ b/e2e/tests/mixed-selector-policies.yml @@ -0,0 +1,234 @@ +--- +apiVersion: "k8s.cni.cncf.io/v1" +kind: NetworkAttachmentDefinition +metadata: + namespace: default + name: macvlan1-mixed-selectors +spec: + config: '{ + "cniVersion": "0.3.1", + "name": "macvlan1-mixed-selectors", + "plugins": [ + { + "type": "macvlan", + "mode": "bridge", + "ipam":{ + "type":"host-local", + "subnet":"2.2.9.0/24", + "rangeStart":"2.2.9.8", + "rangeEnd":"2.2.9.67" + } + }] + }' +--- +apiVersion: v1 +kind: Namespace +metadata: + name: test-mixed-selectors + labels: + environment: production +--- +apiVersion: v1 +kind: Namespace +metadata: + name: test-mixed-selectors-blue + labels: + environment: staging +--- +# Pods +apiVersion: v1 +kind: Pod +metadata: + name: pod-server + namespace: test-mixed-selectors + annotations: + k8s.v1.cni.cncf.io/networks: default/macvlan1-mixed-selectors + labels: + app: test-mixed-selectors + name: pod-server +spec: + containers: + - name: server + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-kl", "0.0.0.0", "5555"] + securityContext: + privileged: true +--- +# Pod in production namespace with allowed role (should be allowed by OR policy) +apiVersion: v1 +kind: Pod +metadata: + name: pod-client-a + namespace: test-mixed-selectors + annotations: + k8s.v1.cni.cncf.io/networks: default/macvlan1-mixed-selectors + labels: + app: test-mixed-selectors + name: pod-client-a + role: allowed-client +spec: + containers: + - name: client + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-kl", "0.0.0.0", "5555"] + securityContext: + privileged: true +--- +# Pod in production namespace with blocked role (should be blocked) +apiVersion: v1 +kind: Pod +metadata: + name: pod-client-b + namespace: test-mixed-selectors + annotations: + k8s.v1.cni.cncf.io/networks: default/macvlan1-mixed-selectors + labels: + app: test-mixed-selectors + name: pod-client-b + role: blocked-client +spec: + containers: + - name: client + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-kl", "0.0.0.0", "5555"] + securityContext: + privileged: true +--- +# Pod in staging namespace with allowed role (should be allowed by both AND and OR policies) +apiVersion: v1 +kind: Pod +metadata: + name: pod-client-c + namespace: test-mixed-selectors-blue + annotations: + k8s.v1.cni.cncf.io/networks: default/macvlan1-mixed-selectors + labels: + app: test-mixed-selectors + name: pod-client-c + role: allowed-client +spec: + containers: + - name: client + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-kl", "0.0.0.0", "5555"] + securityContext: + privileged: true +--- +# Pod in staging namespace with blocked role (should be allowed by OR policy due to namespace) +apiVersion: v1 +kind: Pod +metadata: + name: pod-client-d + namespace: test-mixed-selectors-blue + annotations: + k8s.v1.cni.cncf.io/networks: default/macvlan1-mixed-selectors + labels: + app: test-mixed-selectors + name: pod-client-d + role: blocked-client +spec: + containers: + - name: client + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-kl", "0.0.0.0", "5555"] + securityContext: + privileged: true +--- +# Pod in staging namespace with different role (should be allowed by OR policy due to namespace) +apiVersion: v1 +kind: Pod +metadata: + name: pod-client-e + namespace: test-mixed-selectors-blue + annotations: + k8s.v1.cni.cncf.io/networks: default/macvlan1-mixed-selectors + labels: + app: test-mixed-selectors + name: pod-client-e + role: other-client +spec: + containers: + - name: client + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-kl", "0.0.0.0", "5555"] + securityContext: + privileged: true +--- +# MultiNetworkPolicies +# Policy 1: Mixed selector (AND condition) - pods with role=allowed-client in environment=staging namespace +apiVersion: k8s.cni.cncf.io/v1beta1 +kind: MultiNetworkPolicy +metadata: + name: mixed-selector-ingress-policy + namespace: test-mixed-selectors + annotations: + k8s.v1.cni.cncf.io/policy-for: default/macvlan1-mixed-selectors +spec: + podSelector: + matchLabels: + name: pod-server + policyTypes: + - Ingress + ingress: + - from: + - namespaceSelector: + matchLabels: + environment: staging + podSelector: + matchLabels: + role: allowed-client + ports: + - port: 5555 + protocol: TCP +--- +# Policy 2: OR condition - pods with role=allowed-client OR pods in environment=staging namespace +apiVersion: k8s.cni.cncf.io/v1beta1 +kind: MultiNetworkPolicy +metadata: + name: or-selector-ingress-policy + namespace: test-mixed-selectors + annotations: + k8s.v1.cni.cncf.io/policy-for: default/macvlan1-mixed-selectors +spec: + podSelector: + matchLabels: + name: pod-server + policyTypes: + - Ingress + ingress: + - from: + - podSelector: + matchLabels: + role: allowed-client + - namespaceSelector: + matchLabels: + environment: staging + ports: + - port: 5555 + protocol: TCP +--- +# Egress Policy: Mixed selector (AND condition) +apiVersion: k8s.cni.cncf.io/v1beta1 +kind: MultiNetworkPolicy +metadata: + name: mixed-selector-egress-policy + namespace: test-mixed-selectors + annotations: + k8s.v1.cni.cncf.io/policy-for: default/macvlan1-mixed-selectors +spec: + podSelector: + matchLabels: + name: pod-server + policyTypes: + - Egress + egress: + - to: + - namespaceSelector: + matchLabels: + environment: staging + podSelector: + matchLabels: + role: allowed-client + ports: + - port: 5555 + protocol: TCP diff --git a/e2e/tests/port-range.bats b/e2e/tests/port-range.bats index dcbe1177..ec9d4f59 100755 --- a/e2e/tests/port-range.bats +++ b/e2e/tests/port-range.bats @@ -18,6 +18,18 @@ setup() { sleep 3 } +@test "check generated nftables rules" { + # wait for sync + sleep 5 + + run has_nftables_table "test-port-range" "pod-a" + [ "$status" -eq "0" ] + + run has_nftables_table "test-port-range" "pod-b" + [ "$status" -eq "1" ] +} + + @test "test-port-range check pod-a -> pod-b 5555 OK" { # nc should succeed from client-a to server by policy run kubectl -n test-port-range exec pod-a -- sh -c "echo x | nc -w 1 ${pod_b_net1} 5555" diff --git a/e2e/tests/port-range.yml b/e2e/tests/port-range.yml index 4e7f9fa2..87852051 100644 --- a/e2e/tests/port-range.yml +++ b/e2e/tests/port-range.yml @@ -41,12 +41,12 @@ metadata: spec: containers: - name: netcat-tcp-5555 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e command: ["nc", "-klp", "5555"] securityContext: privileged: true - name: netcat-tcp-6666 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e command: ["nc", "-klp", "6666"] securityContext: privileged: true @@ -64,12 +64,12 @@ metadata: spec: containers: - name: netcat-tcp-5555 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e command: ["nc", "-klp", "5555"] securityContext: privileged: true - name: netcat-tcp-6666 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e command: ["nc", "-klp", "6666"] securityContext: privileged: true diff --git a/e2e/tests/production-like-scenarios.bats b/e2e/tests/production-like-scenarios.bats new file mode 100755 index 00000000..5fd28e61 --- /dev/null +++ b/e2e/tests/production-like-scenarios.bats @@ -0,0 +1,140 @@ +#!/usr/bin/env bats + +# Note: +# These test cases verify production-like scenarios with complex real-world +# network policies that combine multiple features and edge cases. +# +# Test Setup: +# - Frontend Policy: Allows HTTP/HTTPS from production namespace, egress to backend +# - Backend Policy: Allows from frontend and monitoring, egress to database +# - Database Policy: Only allows from backend +# - Monitoring Policy: Egress to backend metrics +# +# Pods with nftables rules: frontend, backend, database, monitoring (all have policies) +# Pods without nftables rules: external (no policies affecting it) + +setup() { + cd $BATS_TEST_DIRNAME + load "common" + frontend_net1=$(get_net1_ip "test-production" "pod-frontend") + backend_net1=$(get_net1_ip "test-production" "pod-backend") + database_net1=$(get_net1_ip "test-production" "pod-database") + monitoring_net1=$(get_net1_ip "test-production" "pod-monitoring") + external_net1=$(get_net1_ip "test-external" "pod-external") +} + +@test "setup production-like test environments" { + # create test manifests + kubectl create -f production-like-scenarios.yml + + # verify all pods are available + run kubectl -n test-production wait --for=condition=ready -l app=test-production pod --timeout=${kubewait_timeout} + [ "$status" -eq "0" ] + + sleep 5 +} + +@test "check generated nftables rules" { + # wait for sync + sleep 5 + + run has_nftables_table "test-production" "pod-frontend" + [ "$status" -eq "0" ] + + run has_nftables_table "test-production" "pod-backend" + [ "$status" -eq "0" ] + + run has_nftables_table "test-production" "pod-database" + [ "$status" -eq "0" ] + + run has_nftables_table "test-production" "pod-monitoring" + [ "$status" -eq "0" ] + + run has_nftables_table "test-production" "pod-external" + [ "$status" -eq "1" ] +} + +@test "production check frontend -> backend HTTP" { + # Should succeed - frontend can access backend on HTTP + run kubectl -n test-production exec pod-frontend -- sh -c "echo x | nc -w 1 ${backend_net1} 80" + [ "$status" -eq "0" ] +} + +@test "production check frontend -> backend HTTPS" { + # Should succeed - frontend can access backend on HTTPS + run kubectl -n test-production exec pod-frontend -- sh -c "echo x | nc -w 1 ${backend_net1} 443" + [ "$status" -eq "0" ] +} + +@test "production check frontend -> backend SSH" { + # Should fail - frontend cannot access backend on SSH + run kubectl -n test-production exec pod-frontend -- sh -c "echo x | nc -w 1 ${backend_net1} 22" + [ "$status" -eq "1" ] +} + +@test "production check backend -> database" { + # Should succeed - backend can access database + run kubectl -n test-production exec pod-backend -- sh -c "echo x | nc -w 1 ${database_net1} 3306" + [ "$status" -eq "0" ] +} + +@test "production check frontend -> database" { + # Should fail - frontend cannot access database directly + run kubectl -n test-production exec pod-frontend -- sh -c "echo x | nc -w 1 ${database_net1} 3306" + [ "$status" -eq "1" ] +} + +@test "production check monitoring -> backend" { + # Should succeed - monitoring can access backend + run kubectl -n test-production exec pod-monitoring -- sh -c "echo x | nc -w 1 ${backend_net1} 8080" + [ "$status" -eq "0" ] +} + +@test "production check monitoring -> database" { + # Should fail - monitoring cannot access database + run kubectl -n test-production exec pod-monitoring -- sh -c "echo x | nc -w 1 ${database_net1} 3306" + [ "$status" -eq "1" ] +} + +@test "production check external -> frontend" { + # Should fail - external cannot access frontend + run kubectl -n test-production exec pod-external -- sh -c "echo x | nc -w 1 ${frontend_net1} 80" + [ "$status" -eq "1" ] +} + +@test "production check backend -> external" { + # Should fail - backend cannot access external + run kubectl -n test-production exec pod-backend -- sh -c "echo x | nc -w 1 ${external_net1} 80" + [ "$status" -eq "1" ] +} + +@test "production check monitoring -> frontend" { + # Should fail - monitoring cannot access frontend (no policy allows this) + run kubectl -n test-production exec pod-monitoring -- sh -c "echo x | nc -w 1 ${frontend_net1} 80" + [ "$status" -eq "1" ] +} + +@test "production check frontend -> monitoring" { + # Should fail - frontend cannot access monitoring (no policy allows this) + run kubectl -n test-production exec pod-frontend -- sh -c "echo x | nc -w 1 ${monitoring_net1} 8080" + [ "$status" -eq "1" ] +} + +@test "production check database -> backend" { + # Should fail - database cannot access backend (no egress policy for database) + run kubectl -n test-production exec pod-database -- sh -c "echo x | nc -w 1 ${backend_net1} 80" + [ "$status" -eq "1" ] +} + +@test "production check external -> backend" { + # Should fail - external cannot access backend + run kubectl -n test-production exec pod-external -- sh -c "echo x | nc -w 1 ${backend_net1} 80" + [ "$status" -eq "1" ] +} + +@test "cleanup environments" { + # remove test manifests + kubectl delete -f production-like-scenarios.yml + run kubectl -n test-production wait --for=delete -l app=test-production pod --timeout=${kubewait_timeout} + [ "$status" -eq "0" ] +} diff --git a/e2e/tests/production-like-scenarios.yml b/e2e/tests/production-like-scenarios.yml new file mode 100644 index 00000000..8642ca4f --- /dev/null +++ b/e2e/tests/production-like-scenarios.yml @@ -0,0 +1,282 @@ +--- +apiVersion: "k8s.cni.cncf.io/v1" +kind: NetworkAttachmentDefinition +metadata: + namespace: default + name: macvlan1-production +spec: + config: '{ + "cniVersion": "0.3.1", + "name": "macvlan1-production", + "plugins": [ + { + "type": "macvlan", + "mode": "bridge", + "ipam":{ + "type":"host-local", + "subnet":"2.2.12.0/24", + "rangeStart":"2.2.12.8", + "rangeEnd":"2.2.12.67" + } + }] + }' +--- +apiVersion: v1 +kind: Namespace +metadata: + name: test-production + labels: + environment: production + tier: web +--- +apiVersion: v1 +kind: Namespace +metadata: + name: test-external + labels: + environment: external + tier: external +--- +# Pods +apiVersion: v1 +kind: Pod +metadata: + name: pod-frontend + namespace: test-production + annotations: + k8s.v1.cni.cncf.io/networks: default/macvlan1-production + labels: + app: test-production + name: pod-frontend + tier: frontend +spec: + containers: + - name: frontend + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["sh", "-c", "nc -klp 80 & nc -klp 443 & wait"] + ports: + - name: http + containerPort: 80 + protocol: TCP + - name: https + containerPort: 443 + protocol: TCP + securityContext: + privileged: true +--- +apiVersion: v1 +kind: Pod +metadata: + name: pod-backend + namespace: test-production + annotations: + k8s.v1.cni.cncf.io/networks: default/macvlan1-production + labels: + app: test-production + name: pod-backend + tier: backend +spec: + containers: + - name: backend + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["sh", "-c", "nc -klp 80 & nc -klp 443 & nc -klp 22 & nc -klp 8080 & wait"] + ports: + - name: http + containerPort: 80 + protocol: TCP + - name: https + containerPort: 443 + protocol: TCP + - name: ssh + containerPort: 22 + protocol: TCP + - name: metrics + containerPort: 8080 + protocol: TCP + securityContext: + privileged: true +--- +apiVersion: v1 +kind: Pod +metadata: + name: pod-database + namespace: test-production + annotations: + k8s.v1.cni.cncf.io/networks: default/macvlan1-production + labels: + app: test-production + name: pod-database + tier: database +spec: + containers: + - name: database + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-kl", "0.0.0.0", "3306"] + ports: + - name: mysql + containerPort: 3306 + protocol: TCP + securityContext: + privileged: true +--- +apiVersion: v1 +kind: Pod +metadata: + name: pod-monitoring + namespace: test-production + annotations: + k8s.v1.cni.cncf.io/networks: default/macvlan1-production + labels: + app: test-production + name: pod-monitoring + tier: monitoring +spec: + containers: + - name: monitoring + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-kl", "0.0.0.0", "8080"] + securityContext: + privileged: true +--- +apiVersion: v1 +kind: Pod +metadata: + name: pod-external + namespace: test-external + annotations: + k8s.v1.cni.cncf.io/networks: default/macvlan1-production + labels: + app: test-external + name: pod-external + tier: external +spec: + containers: + - name: external + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-kl", "0.0.0.0", "80"] + securityContext: + privileged: true +--- +# MultiNetworkPolicies - Production-like Scenarios +# Frontend Policy - Allow HTTP/HTTPS from external, block direct database access +apiVersion: k8s.cni.cncf.io/v1beta1 +kind: MultiNetworkPolicy +metadata: + name: frontend-policy + namespace: test-production + annotations: + k8s.v1.cni.cncf.io/policy-for: default/macvlan1-production +spec: + podSelector: + matchLabels: + tier: frontend + policyTypes: + - Ingress + - Egress + ingress: + - from: + - namespaceSelector: + matchLabels: + environment: production + ports: + - port: 80 + protocol: TCP + - port: 443 + protocol: TCP + egress: + - to: + - podSelector: + matchLabels: + tier: backend + ports: + - port: 80 + protocol: TCP + - port: 443 + protocol: TCP +--- +# Backend Policy - Allow from frontend, access to database, monitoring access +apiVersion: k8s.cni.cncf.io/v1beta1 +kind: MultiNetworkPolicy +metadata: + name: backend-policy + namespace: test-production + annotations: + k8s.v1.cni.cncf.io/policy-for: default/macvlan1-production +spec: + podSelector: + matchLabels: + tier: backend + policyTypes: + - Ingress + - Egress + ingress: + - from: + - podSelector: + matchLabels: + tier: frontend + ports: + - port: 80 + protocol: TCP + - port: 443 + protocol: TCP + - from: + - podSelector: + matchLabels: + tier: monitoring + ports: + - port: 8080 + protocol: TCP + egress: + - to: + - podSelector: + matchLabels: + tier: database + ports: + - port: 3306 + protocol: TCP +--- +# Database Policy - Only allow backend access +apiVersion: k8s.cni.cncf.io/v1beta1 +kind: MultiNetworkPolicy +metadata: + name: database-policy + namespace: test-production + annotations: + k8s.v1.cni.cncf.io/policy-for: default/macvlan1-production +spec: + podSelector: + matchLabels: + tier: database + policyTypes: + - Ingress + ingress: + - from: + - podSelector: + matchLabels: + tier: backend + ports: + - port: 3306 + protocol: TCP +--- +# Monitoring Policy - Allow access to backend metrics +apiVersion: k8s.cni.cncf.io/v1beta1 +kind: MultiNetworkPolicy +metadata: + name: monitoring-policy + namespace: test-production + annotations: + k8s.v1.cni.cncf.io/policy-for: default/macvlan1-production +spec: + podSelector: + matchLabels: + tier: monitoring + policyTypes: + - Egress + egress: + - to: + - podSelector: + matchLabels: + tier: backend + ports: + - port: 8080 + protocol: TCP diff --git a/e2e/tests/protocol-only-ports.bats b/e2e/tests/protocol-only-ports.bats index 69d933c1..b7a6e8ac 100755 --- a/e2e/tests/protocol-only-ports.bats +++ b/e2e/tests/protocol-only-ports.bats @@ -2,8 +2,7 @@ # Note: # These test cases, simple, will create simple (one policy for ingress) and test the -# traffic policying by ncat (nc) command. In addition, these cases also verifies that -# simple iptables generation check by iptables-save and pod-iptable in multi-networkpolicy pod. +# traffic policying by ncat (nc) command. setup() { cd $BATS_TEST_DIRNAME @@ -23,6 +22,18 @@ setup() { sleep 3 } +@test "check generated nftables rules" { + # wait for sync + sleep 5 + + run has_nftables_table "test-protocol-only-ports" "pod-a" + [ "$status" -eq "0" ] + + run has_nftables_table "test-protocol-only-ports" "pod-b" + [ "$status" -eq "1" ] +} + + @test "test-protocol-only-ports check pod-a -> pod-b TCP" { # nc should succeed from client-a to server by policy run kubectl -n test-protocol-only-ports exec pod-a -- sh -c "echo x | nc -w 1 ${pod_b_net1} 5555" @@ -53,4 +64,3 @@ setup() { run kubectl -n test-protocol-only-ports wait --for=delete -l app=test-protocol-only-ports pod --timeout=${kubewait_timeout} [ "$status" -eq "0" ] } -#2.2.6.18 \ No newline at end of file diff --git a/e2e/tests/protocol-only-ports.yml b/e2e/tests/protocol-only-ports.yml index fc02b103..b29a1c03 100644 --- a/e2e/tests/protocol-only-ports.yml +++ b/e2e/tests/protocol-only-ports.yml @@ -4,7 +4,7 @@ kind: NetworkAttachmentDefinition metadata: namespace: default name: macvlan1-simple -spec: +spec: config: '{ "cniVersion": "0.3.1", "name": "macvlan1-simple", @@ -21,11 +21,11 @@ spec: }] }' --- -# namespace for MultiNetworkPolicy +# namespace for MultiNetworkPolicy apiVersion: v1 kind: Namespace metadata: - name: test-protocol-only-ports + name: test-protocol-only-ports --- # Pods apiVersion: v1 @@ -40,16 +40,16 @@ metadata: name: pod-a spec: containers: - - name: netcat-tcp - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-klp", "5555"] - securityContext: - privileged: true - - name: netcat-udp - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-vv", "--udp", "--keep-open", "--sh-exec", "/bin/cat >&2", "--listen", "6666"] - securityContext: - privileged: true + - name: netcat-tcp + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-klp", "5555"] + securityContext: + privileged: true + - name: netcat-udp + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-vv", "--udp", "--keep-open", "--sh-exec", "/bin/cat >&2", "--listen", "6666"] + securityContext: + privileged: true --- apiVersion: v1 kind: Pod @@ -63,16 +63,16 @@ metadata: name: pod-b spec: containers: - - name: netcat-tcp - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-klp", "5555"] - securityContext: - privileged: true - - name: netcat-udp - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-vv", "--udp", "--keep-open", "--sh-exec", "/bin/cat >&2", "--listen", "6666"] - securityContext: - privileged: true + - name: netcat-tcp + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-klp", "5555"] + securityContext: + privileged: true + - name: netcat-udp + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-vv", "--udp", "--keep-open", "--sh-exec", "/bin/cat >&2", "--listen", "6666"] + securityContext: + privileged: true --- # MultiNetworkPolicies apiVersion: k8s.cni.cncf.io/v1beta1 @@ -87,11 +87,11 @@ spec: matchLabels: name: pod-a policyTypes: - - Egress - - Ingress + - Egress + - Ingress egress: - - ports: - - protocol: TCP + - ports: + - protocol: TCP ingress: - - ports: - - protocol: UDP + - ports: + - protocol: UDP diff --git a/e2e/tests/simple-v4-egress-list.bats b/e2e/tests/simple-v4-egress-list.bats index 903a6443..20bb8a80 100755 --- a/e2e/tests/simple-v4-egress-list.bats +++ b/e2e/tests/simple-v4-egress-list.bats @@ -2,8 +2,7 @@ # Note: # These test cases, simple, will create simple (one policy for ingress) and test the -# traffic policying by ncat (nc) command. In addition, these cases also verifies that -# simple iptables generation check by iptables-save and pod-iptable in multi-networkpolicy pod. +# traffic policying by ncat (nc) command. setup() { cd $BATS_TEST_DIRNAME @@ -26,6 +25,23 @@ setup() { sleep 5 } +@test "check generated nftables rules" { + # wait for sync + sleep 5 + + run has_nftables_table "test-simple-v4-egress-list" "pod-server" + [ "$status" -eq "0" ] + + run has_nftables_table "test-simple-v4-egress-list" "pod-client-a" + [ "$status" -eq "1" ] + + run has_nftables_table "test-simple-v4-egress-list" "pod-client-b" + [ "$status" -eq "1" ] + + run has_nftables_table "test-simple-v4-egress-list" "pod-client-c" + [ "$status" -eq "1" ] +} + @test "test-simple-v4-egress-list check client-a -> server" { # nc should succeed from client-a to server by no policy definition for the direction run kubectl -n test-simple-v4-egress-list exec pod-client-a -- sh -c "echo x | nc -w 1 ${server_net1} 5555" diff --git a/e2e/tests/simple-v4-egress-list.yml b/e2e/tests/simple-v4-egress-list.yml index 2533e7f0..ca0e021b 100644 --- a/e2e/tests/simple-v4-egress-list.yml +++ b/e2e/tests/simple-v4-egress-list.yml @@ -4,7 +4,7 @@ kind: NetworkAttachmentDefinition metadata: namespace: default name: macvlan1-simple -spec: +spec: config: '{ "cniVersion": "0.3.1", "name": "macvlan1-simple", @@ -21,11 +21,11 @@ spec: }] }' --- -# namespace for MultiNetworkPolicy +# namespace for MultiNetworkPolicy apiVersion: v1 kind: Namespace metadata: - name: test-simple-v4-egress-list + name: test-simple-v4-egress-list --- # Pods apiVersion: v1 @@ -40,11 +40,11 @@ metadata: name: pod-server spec: containers: - - name: macvlan-worker1 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-klp", "5555"] - securityContext: - privileged: true + - name: macvlan-worker1 + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-klp", "5555"] + securityContext: + privileged: true --- apiVersion: v1 kind: Pod @@ -58,11 +58,11 @@ metadata: name: pod-client-a spec: containers: - - name: macvlan-worker1 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-klp", "5555"] - securityContext: - privileged: true + - name: macvlan-worker1 + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-klp", "5555"] + securityContext: + privileged: true --- apiVersion: v1 kind: Pod @@ -76,11 +76,11 @@ metadata: name: pod-client-b spec: containers: - - name: macvlan-worker1 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-klp", "5555"] - securityContext: - privileged: true + - name: macvlan-worker1 + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-klp", "5555"] + securityContext: + privileged: true --- apiVersion: v1 kind: Pod @@ -94,11 +94,11 @@ metadata: name: pod-client-c spec: containers: - - name: macvlan-worker1 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-klp", "5555"] - securityContext: - privileged: true + - name: macvlan-worker1 + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-klp", "5555"] + securityContext: + privileged: true --- # MultiNetworkPolicies # this policy accepts egress trafic from pod-client-a to pod-server @@ -114,13 +114,13 @@ spec: matchLabels: name: pod-server policyTypes: - - Egress + - Egress egress: - - to: - - podSelector: - matchLabels: - name: pod-client-a - - to: - - podSelector: - matchLabels: - name: pod-client-c + - to: + - podSelector: + matchLabels: + name: pod-client-a + - to: + - podSelector: + matchLabels: + name: pod-client-c diff --git a/e2e/tests/simple-v4-egress.bats b/e2e/tests/simple-v4-egress.bats index d299d401..ef1aa529 100755 --- a/e2e/tests/simple-v4-egress.bats +++ b/e2e/tests/simple-v4-egress.bats @@ -2,8 +2,7 @@ # Note: # These test cases, simple, will create simple (one policy for ingress) and test the -# traffic policying by ncat (nc) command. In addition, these cases also verifies that -# simple iptables generation check by iptables-save and pod-iptable in multi-networkpolicy pod. +# traffic policying by ncat (nc) command. setup() { cd $BATS_TEST_DIRNAME @@ -22,20 +21,21 @@ setup() { [ "$status" -eq "0" ] } -@test "check generated iptables rules" { +@test "check generated nftables rules" { # wait for sync sleep 5 - # check pod-server has multi-networkpolicy iptables rules for ingress - run kubectl -n test-simple-v4-egress exec pod-server -- sh -c "iptables-save | grep MULTI-0-EGRESS" - [ "$status" -eq "0" ] - # check pod-client-a has NO multi-networkpolicy iptables rules for ingress - run kubectl -n test-simple-v4-egress exec pod-client-a -- sh -c "iptables-save | grep MULTI-0-EGRESS" - [ "$status" -eq "1" ] - # check pod-client-b has NO multi-networkpolicy iptables rules for ingress - run kubectl -n test-simple-v4-egress exec pod-client-b -- sh -c "iptables-save | grep MULTI-0-EGRESS" - [ "$status" -eq "1" ] + + run has_nftables_table "test-simple-v4-egress" "pod-server" + [ "$status" -eq "0" ] + + run has_nftables_table "test-simple-v4-egress" "pod-client-a" + [ "$status" -eq "1" ] + + run has_nftables_table "test-simple-v4-egress" "pod-client-b" + [ "$status" -eq "1" ] } + @test "test-simple-v4-egress check client-a -> server" { # nc should succeed from client-a to server by no policy definition for the direction run kubectl -n test-simple-v4-egress exec pod-client-a -- sh -c "echo x | nc -w 1 ${server_net1} 5555" @@ -60,22 +60,6 @@ setup() { [ "$status" -eq "1" ] } -@test "disable multi-networkpolicy and check iptables rules" { - # disable multi-networkpolicy pods by adding invalid nodeSelector - kubectl -n kube-system patch daemonsets multi-networkpolicy-ds-amd64 -p '{"spec": {"template": {"spec": {"nodeSelector": {"non-existing": "true"}}}}}' - # check multi-networkpolicy pod is deleted - kubectl -n kube-system wait --for=delete -l app=multi-networkpolicy pod --timeout=${kubewait_timeout} - - # check iptable rules in pod-server - run kubectl -n test-simple-v4-egress exec pod-server -it -- sh -c "iptables-save | grep MULTI-0-INGRESS" - [ "$status" -eq "1" ] - - # enable multi-networkpolicy again - kubectl -n kube-system patch daemonsets multi-networkpolicy-ds-amd64 --type json -p='[{"op": "remove", "path": "/spec/template/spec/nodeSelector/non-existing"}]' - sleep 5 - kubectl -n kube-system wait --for=condition=ready -l app=multi-networkpolicy pod --timeout=${kubewait_timeout} -} - @test "cleanup environments" { # remove test manifests kubectl delete -f simple-v4-egress.yml diff --git a/e2e/tests/simple-v4-egress.yml b/e2e/tests/simple-v4-egress.yml index 442e6c5e..3b991c58 100644 --- a/e2e/tests/simple-v4-egress.yml +++ b/e2e/tests/simple-v4-egress.yml @@ -4,7 +4,7 @@ kind: NetworkAttachmentDefinition metadata: namespace: default name: macvlan1-simple -spec: +spec: config: '{ "cniVersion": "0.3.1", "name": "macvlan1-simple", @@ -21,11 +21,11 @@ spec: }] }' --- -# namespace for MultiNetworkPolicy +# namespace for MultiNetworkPolicy apiVersion: v1 kind: Namespace metadata: - name: test-simple-v4-egress + name: test-simple-v4-egress --- # Pods apiVersion: v1 @@ -40,11 +40,11 @@ metadata: name: pod-server spec: containers: - - name: macvlan-worker1 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-klp", "5555"] - securityContext: - privileged: true + - name: macvlan-worker1 + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-klp", "5555"] + securityContext: + privileged: true --- apiVersion: v1 kind: Pod @@ -58,11 +58,11 @@ metadata: name: pod-client-a spec: containers: - - name: macvlan-worker1 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-klp", "5555"] - securityContext: - privileged: true + - name: macvlan-worker1 + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-klp", "5555"] + securityContext: + privileged: true --- apiVersion: v1 kind: Pod @@ -76,11 +76,11 @@ metadata: name: pod-client-b spec: containers: - - name: macvlan-worker1 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-klp", "5555"] - securityContext: - privileged: true + - name: macvlan-worker1 + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-klp", "5555"] + securityContext: + privileged: true --- # MultiNetworkPolicies # this policy accepts egress trafic from pod-client-a to pod-server @@ -95,8 +95,10 @@ spec: podSelector: matchLabels: name: pod-server + policyTypes: + - Egress egress: - - to: - - podSelector: - matchLabels: - name: pod-client-a + - to: + - podSelector: + matchLabels: + name: pod-client-a diff --git a/e2e/tests/simple-v4-ingress-list.bats b/e2e/tests/simple-v4-ingress-list.bats index 1b5f1d92..f1910f87 100755 --- a/e2e/tests/simple-v4-ingress-list.bats +++ b/e2e/tests/simple-v4-ingress-list.bats @@ -2,8 +2,7 @@ # Note: # These test cases, simple, will create simple (one policy for ingress) and test the -# traffic policying by ncat (nc) command. In addition, these cases also verifies that -# simple iptables generation check by iptables-save and pod-iptable in multi-networkpolicy pod. +# traffic policying by ncat (nc) command. setup() { cd $BATS_TEST_DIRNAME @@ -26,6 +25,23 @@ setup() { sleep 5 } +@test "check generated nftables rules" { + # wait for sync + sleep 5 + + run has_nftables_table "test-simple-v4-ingress-list" "pod-server" + [ "$status" -eq "0" ] + + run has_nftables_table "test-simple-v4-ingress-list" "pod-client-a" + [ "$status" -eq "1" ] + + run has_nftables_table "test-simple-v4-ingress-list" "pod-client-b" + [ "$status" -eq "1" ] + + run has_nftables_table "test-simple-v4-ingress-list" "pod-client-c" + [ "$status" -eq "1" ] +} + @test "test-simple-v4-ingress-list check client-a -> server" { # nc should succeed from client-a to server by policy run kubectl -n test-simple-v4-ingress-list exec pod-client-a -- sh -c "echo x | nc -w 1 ${server_net1} 5555" diff --git a/e2e/tests/simple-v4-ingress-list.yml b/e2e/tests/simple-v4-ingress-list.yml index fa42023b..a52e4ba2 100644 --- a/e2e/tests/simple-v4-ingress-list.yml +++ b/e2e/tests/simple-v4-ingress-list.yml @@ -4,7 +4,7 @@ kind: NetworkAttachmentDefinition metadata: namespace: default name: macvlan1-simple -spec: +spec: config: '{ "cniVersion": "0.3.1", "name": "macvlan1-simple", @@ -21,11 +21,11 @@ spec: }] }' --- -# namespace for MultiNetworkPolicy +# namespace for MultiNetworkPolicy apiVersion: v1 kind: Namespace metadata: - name: test-simple-v4-ingress-list + name: test-simple-v4-ingress-list --- # Pods apiVersion: v1 @@ -40,11 +40,11 @@ metadata: name: pod-server spec: containers: - - name: macvlan-worker1 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-klp", "5555"] - securityContext: - privileged: true + - name: macvlan-worker1 + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-klp", "5555"] + securityContext: + privileged: true --- apiVersion: v1 kind: Pod @@ -58,11 +58,11 @@ metadata: name: pod-client-a spec: containers: - - name: macvlan-worker1 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-klp", "5555"] - securityContext: - privileged: true + - name: macvlan-worker1 + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-klp", "5555"] + securityContext: + privileged: true --- apiVersion: v1 kind: Pod @@ -76,11 +76,11 @@ metadata: name: pod-client-b spec: containers: - - name: macvlan-worker1 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-klp", "5555"] - securityContext: - privileged: true + - name: macvlan-worker1 + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-klp", "5555"] + securityContext: + privileged: true --- apiVersion: v1 kind: Pod @@ -94,11 +94,11 @@ metadata: name: pod-client-c spec: containers: - - name: macvlan-worker1 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-klp", "5555"] - securityContext: - privileged: true + - name: macvlan-worker1 + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-klp", "5555"] + securityContext: + privileged: true --- # MultiNetworkPolicies # this policy accepts ingress trafic from pod-client-a to pod-server @@ -114,13 +114,13 @@ spec: matchLabels: name: pod-server policyTypes: - - Ingress + - Ingress ingress: - - from: - - podSelector: - matchLabels: - name: pod-client-a - - from: - - podSelector: - matchLabels: - name: pod-client-c + - from: + - podSelector: + matchLabels: + name: pod-client-a + - from: + - podSelector: + matchLabels: + name: pod-client-c diff --git a/e2e/tests/simple-v4-ingress.bats b/e2e/tests/simple-v4-ingress.bats index 0092773c..413a6c19 100755 --- a/e2e/tests/simple-v4-ingress.bats +++ b/e2e/tests/simple-v4-ingress.bats @@ -3,7 +3,7 @@ # Note: # These test cases, simple, will create simple (one policy for ingress) and test the # traffic policying by ncat (nc) command. In addition, these cases also verifies that -# simple iptables generation check by iptables-save and pod-iptable in multi-networkpolicy pod. +# simple nftables generation check by nft list ruleset and multi-networkpolicy pod. setup() { cd $BATS_TEST_DIRNAME @@ -22,18 +22,18 @@ setup() { [ "$status" -eq "0" ] } -@test "check generated iptables rules" { +@test "check generated nftables rules" { # wait for sync sleep 5 - # check pod-server has multi-networkpolicy iptables rules for ingress - run kubectl -n test-simple-v4-ingress exec pod-server -- sh -c "iptables-save | grep MULTI-0-INGRESS" - [ "$status" -eq "0" ] - # check pod-client-a has NO multi-networkpolicy iptables rules for ingress - run kubectl -n test-simple-v4-ingress exec pod-client-a -- sh -c "iptables-save | grep MULTI-0-INGRESS" - [ "$status" -eq "1" ] - # check pod-client-b has NO multi-networkpolicy iptables rules for ingress - run kubectl -n test-simple-v4-ingress exec pod-client-b -- sh -c "iptables-save | grep MULTI-0-INGRESS" - [ "$status" -eq "1" ] + + run has_nftables_table "test-simple-v4-ingress" "pod-server" + [ "$status" -eq "0" ] + + run has_nftables_table "test-simple-v4-ingress" "pod-client-a" + [ "$status" -eq "1" ] + + run has_nftables_table "test-simple-v4-ingress" "pod-client-b" + [ "$status" -eq "1" ] } @test "test-simple-v4-ingress check client-a -> server" { @@ -60,22 +60,6 @@ setup() { [ "$status" -eq "0" ] } -@test "disable multi-networkpolicy and check iptables rules" { - # disable multi-networkpolicy pods by adding invalid nodeSelector - kubectl -n kube-system patch daemonsets multi-networkpolicy-ds-amd64 -p '{"spec": {"template": {"spec": {"nodeSelector": {"non-existing": "true"}}}}}' - # check multi-networkpolicy pod is deleted - kubectl -n kube-system wait --for=delete -l app=multi-networkpolicy pod --timeout=${kubewait_timeout} - - # check iptable rules in pod-server - run kubectl -n test-simple-v4-ingress exec pod-server -it -- sh -c "iptables-save | grep MULTI-0-INGRESS" - [ "$status" -eq "1" ] - - # enable multi-networkpolicy again - kubectl -n kube-system patch daemonsets multi-networkpolicy-ds-amd64 --type json -p='[{"op": "remove", "path": "/spec/template/spec/nodeSelector/non-existing"}]' - sleep 5 - kubectl -n kube-system wait --for=condition=ready -l app=multi-networkpolicy pod --timeout=${kubewait_timeout} -} - @test "cleanup environments" { # remove test manifests kubectl delete -f simple-v4-ingress.yml diff --git a/e2e/tests/simple-v4-ingress.yml b/e2e/tests/simple-v4-ingress.yml index f1cf6cb7..3aa2985c 100644 --- a/e2e/tests/simple-v4-ingress.yml +++ b/e2e/tests/simple-v4-ingress.yml @@ -4,7 +4,7 @@ kind: NetworkAttachmentDefinition metadata: namespace: default name: macvlan1-simple -spec: +spec: config: '{ "cniVersion": "0.3.1", "name": "macvlan1-simple", @@ -21,11 +21,11 @@ spec: }] }' --- -# namespace for MultiNetworkPolicy +# namespace for MultiNetworkPolicy apiVersion: v1 kind: Namespace metadata: - name: test-simple-v4-ingress + name: test-simple-v4-ingress --- # Pods apiVersion: v1 @@ -40,11 +40,11 @@ metadata: name: pod-server spec: containers: - - name: macvlan-worker1 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-klp", "5555"] - securityContext: - privileged: true + - name: macvlan-worker1 + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-klp", "5555"] + securityContext: + privileged: true --- apiVersion: v1 kind: Pod @@ -58,11 +58,11 @@ metadata: name: pod-client-a spec: containers: - - name: macvlan-worker1 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-klp", "5555"] - securityContext: - privileged: true + - name: macvlan-worker1 + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-klp", "5555"] + securityContext: + privileged: true --- apiVersion: v1 kind: Pod @@ -76,11 +76,11 @@ metadata: name: pod-client-b spec: containers: - - name: macvlan-worker1 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-klp", "5555"] - securityContext: - privileged: true + - name: macvlan-worker1 + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-klp", "5555"] + securityContext: + privileged: true --- # MultiNetworkPolicies # this policy accepts ingress trafic from pod-client-a to pod-server @@ -96,7 +96,7 @@ spec: matchLabels: name: pod-server ingress: - - from: - - podSelector: - matchLabels: - name: pod-client-a + - from: + - podSelector: + matchLabels: + name: pod-client-a diff --git a/e2e/tests/simple-v6-ingress-list.bats b/e2e/tests/simple-v6-ingress-list.bats index a7758e49..ce30869c 100755 --- a/e2e/tests/simple-v6-ingress-list.bats +++ b/e2e/tests/simple-v6-ingress-list.bats @@ -2,8 +2,7 @@ # Note: # These test cases, simple, will create simple (one policy for ingress) and test the -# traffic policying by ncat (nc) command. In addition, these cases also verifies that -# simple ip6tables generation check by ip6tables-save and pod-iptable in multi-networkpolicy pod. +# traffic policying by ncat (nc) command. setup() { @@ -27,6 +26,23 @@ setup() { sleep 5 } +@test "check generated nftables rules" { + # wait for sync + sleep 5 + + run has_nftables_table "test-simple-v6-ingress-list" "pod-server" + [ "$status" -eq "0" ] + + run has_nftables_table "test-simple-v6-ingress-list" "pod-client-a" + [ "$status" -eq "1" ] + + run has_nftables_table "test-simple-v6-ingress-list" "pod-client-b" + [ "$status" -eq "1" ] + + run has_nftables_table "test-simple-v6-ingress-list" "pod-client-c" + [ "$status" -eq "1" ] +} + @test "test-simple-v6-ingress-list check client-a -> server" { # nc should succeed from client-a to server by policy run kubectl -n test-simple-v6-ingress-list exec pod-client-a -- sh -c "echo x | nc -w 1 ${server_net1} 5555" diff --git a/e2e/tests/simple-v6-ingress-list.yml b/e2e/tests/simple-v6-ingress-list.yml index ab48d486..1a7640a8 100644 --- a/e2e/tests/simple-v6-ingress-list.yml +++ b/e2e/tests/simple-v6-ingress-list.yml @@ -4,7 +4,7 @@ kind: NetworkAttachmentDefinition metadata: namespace: default name: macvlan1-simple -spec: +spec: config: '{ "cniVersion": "0.3.1", "name": "macvlan1-simple", @@ -21,11 +21,11 @@ spec: }] }' --- -# namespace for MultiNetworkPolicy +# namespace for MultiNetworkPolicy apiVersion: v1 kind: Namespace metadata: - name: test-simple-v6-ingress-list + name: test-simple-v6-ingress-list --- # Pods apiVersion: v1 @@ -40,11 +40,11 @@ metadata: name: pod-server spec: containers: - - name: macvlan-worker1 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-klp", "5555"] - securityContext: - privileged: true + - name: macvlan-worker1 + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-klp", "5555"] + securityContext: + privileged: true --- apiVersion: v1 kind: Pod @@ -58,11 +58,11 @@ metadata: name: pod-client-a spec: containers: - - name: macvlan-worker1 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-klp", "5555"] - securityContext: - privileged: true + - name: macvlan-worker1 + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-klp", "5555"] + securityContext: + privileged: true --- apiVersion: v1 kind: Pod @@ -76,11 +76,11 @@ metadata: name: pod-client-b spec: containers: - - name: macvlan-worker1 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-klp", "5555"] - securityContext: - privileged: true + - name: macvlan-worker1 + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-klp", "5555"] + securityContext: + privileged: true --- apiVersion: v1 kind: Pod @@ -94,11 +94,11 @@ metadata: name: pod-client-c spec: containers: - - name: macvlan-worker1 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-klp", "5555"] - securityContext: - privileged: true + - name: macvlan-worker1 + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-klp", "5555"] + securityContext: + privileged: true --- # MultiNetworkPolicies # this policy accepts ingress trafic from pod-client-a to pod-server, or pod-client-c to pod-server @@ -114,13 +114,13 @@ spec: matchLabels: name: pod-server policyTypes: - - Ingress + - Ingress ingress: - - from: - - podSelector: - matchLabels: - name: pod-client-a - - from: - - podSelector: - matchLabels: - name: pod-client-c + - from: + - podSelector: + matchLabels: + name: pod-client-a + - from: + - podSelector: + matchLabels: + name: pod-client-c diff --git a/e2e/tests/simple-v6-ingress.bats b/e2e/tests/simple-v6-ingress.bats index c620b9a9..93fa8feb 100755 --- a/e2e/tests/simple-v6-ingress.bats +++ b/e2e/tests/simple-v6-ingress.bats @@ -2,8 +2,7 @@ # Note: # These test cases, simple, will create simple (one policy for ingress) and test the -# traffic policying by ncat (nc) command. In addition, these cases also verifies that -# simple ip6tables generation check by ip6tables-save and pod-iptable in multi-networkpolicy pod. +# traffic policying by ncat (nc) command. setup() { @@ -23,19 +22,18 @@ setup() { [ "$status" -eq "0" ] } -@test "check generated ip6tables rules" { +@test "check generated nftables rules" { # wait for sync sleep 5 - # check pod-server has multi-networkpolicy ip6tables rules for ingress - run kubectl -n test-simple-v6-ingress exec pod-server -- sh -c "ip6tables-save | grep MULTI-0-INGRESS" - [ "$status" -eq "0" ] - # check pod-client-a has NO multi-networkpolicy ip6tables rules for ingress - run kubectl -n test-simple-v6-ingress exec pod-client-a -- sh -c "ip6tables-save | grep MULTI-0-INGRESS" - [ "$status" -eq "1" ] - # check pod-client-b has NO multi-networkpolicy ip6tables rules for ingress - run kubectl -n test-simple-v6-ingress exec pod-client-b -- sh -c "ip6tables-save | grep MULTI-0-INGRESS" - [ "$status" -eq "1" ] + run has_nftables_table "test-simple-v6-ingress" "pod-server" + [ "$status" -eq "0" ] + + run has_nftables_table "test-simple-v6-ingress" "pod-client-a" + [ "$status" -eq "1" ] + + run has_nftables_table "test-simple-v6-ingress" "pod-client-b" + [ "$status" -eq "1" ] } @test "test-simple-v6-ingress check client-a -> server" { @@ -62,22 +60,6 @@ setup() { [ "$status" -eq "0" ] } -@test "disable multi-networkpolicy and check ip6tables rules" { - # disable multi-networkpolicy pods by adding invalid nodeSelector - kubectl -n kube-system patch daemonsets multi-networkpolicy-ds-amd64 -p '{"spec": {"template": {"spec": {"nodeSelector": {"non-existing": "true"}}}}}' - # check multi-networkpolicy pod is deleted - kubectl -n kube-system wait --for=delete -l app=multi-networkpolicy pod --timeout=${kubewait_timeout} - - # check ip6table rules in pod-server - run kubectl -n test-simple-v6-ingress exec pod-server -it -- sh -c "ip6tables-save | grep MULTI-0-INGRESS" - [ "$status" -eq "1" ] - - # enable multi-networkpolicy again - kubectl -n kube-system patch daemonsets multi-networkpolicy-ds-amd64 --type json -p='[{"op": "remove", "path": "/spec/template/spec/nodeSelector/non-existing"}]' - sleep 5 - kubectl -n kube-system wait --for=condition=ready -l app=multi-networkpolicy pod --timeout=${kubewait_timeout} -} - @test "cleanup environments" { # remove test manifests kubectl delete -f simple-v6-ingress.yml diff --git a/e2e/tests/simple-v6-ingress.yml b/e2e/tests/simple-v6-ingress.yml index 69bc0f48..e2ab4e80 100644 --- a/e2e/tests/simple-v6-ingress.yml +++ b/e2e/tests/simple-v6-ingress.yml @@ -4,7 +4,7 @@ kind: NetworkAttachmentDefinition metadata: namespace: default name: macvlan1-simple -spec: +spec: config: '{ "cniVersion": "0.3.1", "name": "macvlan1-simple", @@ -21,11 +21,11 @@ spec: }] }' --- -# namespace for MultiNetworkPolicy +# namespace for MultiNetworkPolicy apiVersion: v1 kind: Namespace metadata: - name: test-simple-v6-ingress + name: test-simple-v6-ingress --- # Pods apiVersion: v1 @@ -40,11 +40,11 @@ metadata: name: pod-server spec: containers: - - name: macvlan-worker1 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-klp", "5555"] - securityContext: - privileged: true + - name: macvlan-worker1 + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-klp", "5555"] + securityContext: + privileged: true --- apiVersion: v1 kind: Pod @@ -58,11 +58,11 @@ metadata: name: pod-client-a spec: containers: - - name: macvlan-worker1 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-klp", "5555"] - securityContext: - privileged: true + - name: macvlan-worker1 + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-klp", "5555"] + securityContext: + privileged: true --- apiVersion: v1 kind: Pod @@ -76,11 +76,11 @@ metadata: name: pod-client-b spec: containers: - - name: macvlan-worker1 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-klp", "5555"] - securityContext: - privileged: true + - name: macvlan-worker1 + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-klp", "5555"] + securityContext: + privileged: true --- # MultiNetworkPolicies # this policy accepts ingress trafic from pod-client-a to pod-server @@ -96,9 +96,9 @@ spec: matchLabels: name: pod-server policyTypes: - - Ingress + - Ingress ingress: - - from: - - podSelector: - matchLabels: - name: pod-client-a + - from: + - podSelector: + matchLabels: + name: pod-client-a diff --git a/e2e/tests/stacked.bats b/e2e/tests/stacked.bats index fe57b6e7..6b478d16 100755 --- a/e2e/tests/stacked.bats +++ b/e2e/tests/stacked.bats @@ -20,17 +20,21 @@ setup() { [ "$status" -eq "0" ] } -@test "check generated iptables rules" { +@test "check generated nftables rules" { # wait for sync sleep 5 - run kubectl -n test-stacked exec pod-server -it -- sh -c "iptables-save | grep MULTI-0-INGRESS" - [ "$status" -eq "0" ] - run kubectl -n test-stacked exec pod-client-a -it -- sh -c "iptables-save | grep MULTI-0-INGRESS" - [ "$status" -eq "1" ] - run kubectl -n test-stacked exec pod-client-b -it -- sh -c "iptables-save | grep MULTI-0-INGRESS" - [ "$status" -eq "1" ] - run kubectl -n test-stacked exec pod-client-c -it -- sh -c "iptables-save | grep MULTI-0-INGRESS" - [ "$status" -eq "1" ] + + run has_nftables_table "test-stacked" "pod-server" + [ "$status" -eq "0" ] + + run has_nftables_table "test-stacked" "pod-client-a" + [ "$status" -eq "1" ] + + run has_nftables_table "test-stacked" "pod-client-b" + [ "$status" -eq "1" ] + + run has_nftables_table "test-stacked" "pod-client-c" + [ "$status" -eq "1" ] } @test "test-stacked check client-a" { diff --git a/e2e/tests/stacked.yml b/e2e/tests/stacked.yml index e9766abe..da5c6191 100644 --- a/e2e/tests/stacked.yml +++ b/e2e/tests/stacked.yml @@ -4,7 +4,7 @@ kind: NetworkAttachmentDefinition metadata: namespace: default name: macvlan1-stacked -spec: +spec: config: '{ "cniVersion": "0.3.1", "name": "macvlan1-stacked", @@ -24,7 +24,7 @@ spec: apiVersion: v1 kind: Namespace metadata: - name: test-stacked + name: test-stacked --- # Pods apiVersion: v1 @@ -39,11 +39,11 @@ metadata: name: pod-server spec: containers: - - name: macvlan-worker1 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-kl", "0.0.0.0", "5555"] - securityContext: - privileged: true + - name: macvlan-worker1 + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-kl", "0.0.0.0", "5555"] + securityContext: + privileged: true --- apiVersion: v1 kind: Pod @@ -57,11 +57,11 @@ metadata: name: pod-client-a spec: containers: - - name: macvlan-worker1 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-kl", "0.0.0.0", "5555"] - securityContext: - privileged: true + - name: macvlan-worker1 + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-kl", "0.0.0.0", "5555"] + securityContext: + privileged: true --- apiVersion: v1 kind: Pod @@ -75,11 +75,11 @@ metadata: name: pod-client-b spec: containers: - - name: macvlan-worker1 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-kl", "0.0.0.0", "5555"] - securityContext: - privileged: true + - name: macvlan-worker1 + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-kl", "0.0.0.0", "5555"] + securityContext: + privileged: true --- apiVersion: v1 kind: Pod @@ -93,11 +93,11 @@ metadata: name: pod-client-c spec: containers: - - name: macvlan-worker1 - image: ghcr.io/k8snetworkplumbingwg/multi-networkpolicy-iptables:e2e-test - command: ["nc", "-kl", "0.0.0.0", "5555"] - securityContext: - privileged: true + - name: macvlan-worker1 + image: localhost:5000/multus-networkpolicy-nftables-tests:e2e + command: ["nc", "-kl", "0.0.0.0", "5555"] + securityContext: + privileged: true --- # MultiNetworkPolicies # this policy accepts ingress trafic from pod-client-a to pod-server @@ -116,12 +116,12 @@ spec: matchLabels: name: pod-server policyTypes: - - Ingress + - Ingress ingress: - - from: - - podSelector: - matchLabels: - name: pod-client-a + - from: + - podSelector: + matchLabels: + name: pod-client-a --- apiVersion: k8s.cni.cncf.io/v1beta1 kind: MultiNetworkPolicy @@ -135,9 +135,9 @@ spec: matchLabels: name: pod-server policyTypes: - - Ingress + - Ingress ingress: - - from: - - podSelector: - matchLabels: - name: pod-client-b + - from: + - podSelector: + matchLabels: + name: pod-client-b diff --git a/e2e/update_image_on_cluster.sh b/e2e/update_image_on_cluster.sh index 7b008167..61216572 100755 --- a/e2e/update_image_on_cluster.sh +++ b/e2e/update_image_on_cluster.sh @@ -4,7 +4,7 @@ set -o errexit E2E="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" export PATH=${PATH}:${E2E}/bin OCI_BIN="${OCI_BIN:-docker}" -IMAGE="localhost:5000/multus-networkpolicy-iptables:e2e" +IMAGE="localhost:5000/multus-networkpolicy-nftables:e2e" $OCI_BIN build -t ${IMAGE} ${E2E}/.. kind load docker-image ${IMAGE} diff --git a/go.mod b/go.mod index d0d88957..a8a94984 100644 --- a/go.mod +++ b/go.mod @@ -1,107 +1,121 @@ -module github.com/k8snetworkplumbingwg/multi-networkpolicy-iptables +module github.com/k8snetworkplumbingwg/multi-network-policy-nftables -go 1.24.0 +go 1.24.2 + +toolchain go1.24.7 require ( - github.com/containernetworking/cni v0.8.1 - github.com/containernetworking/plugins v0.8.6 + github.com/containernetworking/cni v1.3.0 + github.com/containernetworking/plugins v1.9.0 + github.com/go-logr/logr v1.4.3 github.com/k8snetworkplumbingwg/multi-networkpolicy v1.0.1 - github.com/k8snetworkplumbingwg/network-attachment-definition-client v0.0.0-20200528071255-22c819bc6e7e - github.com/onsi/ginkgo v1.16.4 - github.com/onsi/gomega v1.35.1 - github.com/spf13/cobra v1.9.1 - github.com/spf13/pflag v1.0.6 - google.golang.org/grpc v1.72.1 - k8s.io/api v0.34.1 - k8s.io/apimachinery v0.34.1 - k8s.io/client-go v0.34.1 - k8s.io/component-helpers v0.34.1 - k8s.io/cri-api v0.34.1 - k8s.io/cri-client v0.0.0 - k8s.io/klog v1.0.0 - k8s.io/kubernetes v1.34.1 + github.com/k8snetworkplumbingwg/network-attachment-definition-client v1.7.7 + github.com/onsi/ginkgo/v2 v2.27.3 + github.com/onsi/gomega v1.38.3 + google.golang.org/grpc v1.78.0 + k8s.io/api v0.34.2 + k8s.io/apimachinery v0.34.2 + k8s.io/client-go v0.34.2 + k8s.io/component-helpers v0.0.0-00010101000000-000000000000 + k8s.io/cri-api v0.0.0-00010101000000-000000000000 + sigs.k8s.io/controller-runtime v0.22.4 + sigs.k8s.io/knftables v0.0.18 ) require ( - github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect - github.com/go-logr/logr v1.4.2 // indirect - github.com/go-openapi/jsonpointer v0.21.0 // indirect - github.com/go-openapi/jsonreference v0.20.2 // indirect - github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-logr/zapr v1.3.0 // indirect + github.com/go-openapi/jsonpointer v0.22.0 // indirect + github.com/go-openapi/jsonreference v0.21.1 // indirect + github.com/go-openapi/swag v0.24.1 // indirect + github.com/go-openapi/swag/cmdutils v0.24.0 // indirect + github.com/go-openapi/swag/conv v0.24.0 // indirect + github.com/go-openapi/swag/fileutils v0.24.0 // indirect + github.com/go-openapi/swag/jsonname v0.24.0 // indirect + github.com/go-openapi/swag/jsonutils v0.24.0 // indirect + github.com/go-openapi/swag/loading v0.24.0 // indirect + github.com/go-openapi/swag/mangling v0.24.0 // indirect + github.com/go-openapi/swag/netutils v0.24.0 // indirect + github.com/go-openapi/swag/stringutils v0.24.0 // indirect + github.com/go-openapi/swag/typeutils v0.24.0 // indirect + github.com/go-openapi/swag/yamlutils v0.24.0 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/btree v1.1.3 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect + github.com/google/pprof v0.0.0-20250830080959-101d87ff5bc3 // indirect github.com/google/uuid v1.6.0 // 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/mailru/easyjson v0.7.7 // indirect + github.com/mailru/easyjson v0.9.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/nxadm/tail v1.4.8 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_golang v1.23.0 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.65.0 // indirect + github.com/prometheus/procfs v0.17.0 // indirect + github.com/spf13/pflag v1.0.9 // indirect + github.com/vishvananda/netns v0.0.5 // indirect github.com/x448/float16 v0.8.4 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/net v0.41.0 // indirect - golang.org/x/oauth2 v0.27.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/term v0.30.0 // indirect - golang.org/x/text v0.23.0 // indirect - golang.org/x/time v0.9.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect - google.golang.org/protobuf v1.36.5 // indirect - gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/oauth2 v0.32.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/term v0.37.0 // indirect + golang.org/x/text v0.31.0 // indirect + golang.org/x/time v0.12.0 // indirect + golang.org/x/tools v0.38.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda // indirect + google.golang.org/protobuf v1.36.10 // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/apiextensions-apiserver v0.34.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect - k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect - sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + k8s.io/kube-openapi v0.0.0-20250814151709-d7b6acb124c3 // indirect + k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) replace ( - github.com/gogo/protobuf => github.com/gogo/protobuf v1.3.2 - golang.org/x/net => golang.org/x/net v0.17.0 - golang.org/x/text => golang.org/x/text v0.3.8 - k8s.io/api => k8s.io/api v0.34.1 - k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.34.1 - k8s.io/apimachinery => k8s.io/apimachinery v0.34.1 - k8s.io/apiserver => k8s.io/apiserver v0.34.1 - k8s.io/cli-runtime => k8s.io/cli-runtime v0.34.1 - k8s.io/client-go => k8s.io/client-go v0.34.1 - k8s.io/cloud-provider => k8s.io/cloud-provider v0.34.1 - k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.34.1 - k8s.io/code-generator => k8s.io/code-generator v0.34.1 - k8s.io/component-base => k8s.io/component-base v0.34.1 - k8s.io/component-helpers => k8s.io/component-helpers v0.34.1 - k8s.io/controller-manager => k8s.io/controller-manager v0.34.1 - k8s.io/cri-api => k8s.io/cri-api v0.34.1 - k8s.io/cri-client => k8s.io/cri-client v0.34.1 - k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.34.1 - k8s.io/dynamic-resource-allocation => k8s.io/dynamic-resource-allocation v0.34.1 - k8s.io/endpointslice => k8s.io/endpointslice v0.34.1 - k8s.io/kms => k8s.io/kms v0.34.1 - k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.34.1 - k8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.34.1 - k8s.io/kube-proxy => k8s.io/kube-proxy v0.34.1 - k8s.io/kube-scheduler => k8s.io/kube-scheduler v0.34.1 - k8s.io/kubectl => k8s.io/kubectl v0.34.1 - k8s.io/kubelet => k8s.io/kubelet v0.34.1 - k8s.io/kubernetes => k8s.io/kubernetes v1.34.1 - k8s.io/legacy-cloud-providers => k8s.io/legacy-cloud-providers v0.34.1 - k8s.io/metrics => k8s.io/metrics v0.34.1 - k8s.io/mount-utils => k8s.io/mount-utils v0.34.1 - k8s.io/pod-security-admission => k8s.io/pod-security-admission v0.34.1 - k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.34.1 + k8s.io/cli-runtime => k8s.io/cli-runtime v0.34.0 + k8s.io/cloud-provider => k8s.io/cloud-provider v0.34.0 + k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.34.0 + k8s.io/component-helpers => k8s.io/component-helpers v0.34.0 + k8s.io/controller-manager => k8s.io/controller-manager v0.34.0 + k8s.io/cri-api => k8s.io/cri-api v0.34.0 + k8s.io/cri-client => k8s.io/cri-client v0.34.0 + k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.34.0 + k8s.io/dynamic-resource-allocation => k8s.io/dynamic-resource-allocation v0.34.0 + k8s.io/endpointslice => k8s.io/endpointslice v0.34.0 + k8s.io/externaljwt => k8s.io/externaljwt v0.34.0 + k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.34.0 + k8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.34.0 + k8s.io/kube-proxy => k8s.io/kube-proxy v0.34.0 + k8s.io/kube-scheduler => k8s.io/kube-scheduler v0.34.0 + k8s.io/kubectl => k8s.io/kubectl v0.34.0 + k8s.io/kubelet => k8s.io/kubelet v0.34.0 + k8s.io/metrics => k8s.io/metrics v0.34.0 + k8s.io/mount-utils => k8s.io/mount-utils v0.34.0 + k8s.io/pod-security-admission => k8s.io/pod-security-admission v0.34.0 + k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.34.0 ) diff --git a/go.sum b/go.sum index ba68a885..d63a62b6 100644 --- a/go.sum +++ b/go.sum @@ -1,177 +1,251 @@ -cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= -github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= -github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= -github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= +github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= +github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= -github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= -github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= -github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0= -github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= -github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= -github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= -github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs= -github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= -github.com/containernetworking/cni v0.7.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= -github.com/containernetworking/cni v0.8.1 h1:7zpDnQ3T3s4ucOuJ/ZCLrYBxzkg0AELFfII3Epo9TmI= -github.com/containernetworking/cni v0.8.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= -github.com/containernetworking/plugins v0.8.6 h1:npZTLiMa4CRn6m5P9+1Dz4O1j0UeFbm8VYN6dlsw568= -github.com/containernetworking/plugins v0.8.6/go.mod h1:qnw5mN19D8fIwkqW7oHHYDHVlzhJpcY6TQxn/fUyDDM= -github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= -github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/containernetworking/cni v1.3.0 h1:v6EpN8RznAZj9765HhXQrtXgX+ECGebEYEmnuFjskwo= +github.com/containernetworking/cni v1.3.0/go.mod h1:Bs8glZjjFfGPHMw6hQu82RUgEPNGEaBb9KS5KtNMnJ4= +github.com/containernetworking/plugins v1.9.0 h1:Mg3SXBdRGkdXyFC4lcwr6u2ZB2SDeL6LC3U+QrEANuQ= +github.com/containernetworking/plugins v1.9.0/go.mod h1:JG3BxoJifxxHBhG3hFyxyhid7JgRVBu/wtooGEvWf1c= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ= -github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s= -github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8= -github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4/go.mod h1:bMl4RjIciD2oAxI7DmWRx6gbeqrkoLqv3MV0vzNad+I= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/emicklei/go-restful v2.10.0+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.16.0+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= +github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= -github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= +github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= +github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= +github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= +github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE= +github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= -github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= -github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= -github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= -github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.22.0 h1:TmMhghgNef9YXxTu1tOopo+0BGEytxA+okbry0HjZsM= +github.com/go-openapi/jsonpointer v0.22.0/go.mod h1:xt3jV88UtExdIkkL7NloURjRQjbeUgcxFblMjq2iaiU= github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= -github.com/go-openapi/jsonreference v0.20.1/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= -github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= -github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= -github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= -github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= +github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= +github.com/go-openapi/jsonreference v0.21.1 h1:bSKrcl8819zKiOgxkbVNRUBIr6Wwj9KYrDbMjRs0cDA= +github.com/go-openapi/jsonreference v0.21.1/go.mod h1:PWs8rO4xxTUqKGu+lEvvCxD5k2X7QYkKAepJyCmSTT8= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= -github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= -github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.24.1 h1:DPdYTZKo6AQCRqzwr/kGkxJzHhpKxZ9i/oX0zag+MF8= +github.com/go-openapi/swag v0.24.1/go.mod h1:sm8I3lCPlspsBBwUm1t5oZeWZS0s7m/A+Psg0ooRU0A= +github.com/go-openapi/swag/cmdutils v0.24.0 h1:KlRCffHwXFI6E5MV9n8o8zBRElpY4uK4yWyAMWETo9I= +github.com/go-openapi/swag/cmdutils v0.24.0/go.mod h1:uxib2FAeQMByyHomTlsP8h1TtPd54Msu2ZDU/H5Vuf8= +github.com/go-openapi/swag/conv v0.24.0 h1:ejB9+7yogkWly6pnruRX45D1/6J+ZxRu92YFivx54ik= +github.com/go-openapi/swag/conv v0.24.0/go.mod h1:jbn140mZd7EW2g8a8Y5bwm8/Wy1slLySQQ0ND6DPc2c= +github.com/go-openapi/swag/fileutils v0.24.0 h1:U9pCpqp4RUytnD689Ek/N1d2N/a//XCeqoH508H5oak= +github.com/go-openapi/swag/fileutils v0.24.0/go.mod h1:3SCrCSBHyP1/N+3oErQ1gP+OX1GV2QYFSnrTbzwli90= +github.com/go-openapi/swag/jsonname v0.24.0 h1:2wKS9bgRV/xB8c62Qg16w4AUiIrqqiniJFtZGi3dg5k= +github.com/go-openapi/swag/jsonname v0.24.0/go.mod h1:GXqrPzGJe611P7LG4QB9JKPtUZ7flE4DOVechNaDd7Q= +github.com/go-openapi/swag/jsonutils v0.24.0 h1:F1vE1q4pg1xtO3HTyJYRmEuJ4jmIp2iZ30bzW5XgZts= +github.com/go-openapi/swag/jsonutils v0.24.0/go.mod h1:vBowZtF5Z4DDApIoxcIVfR8v0l9oq5PpYRUuteVu6f0= +github.com/go-openapi/swag/loading v0.24.0 h1:ln/fWTwJp2Zkj5DdaX4JPiddFC5CHQpvaBKycOlceYc= +github.com/go-openapi/swag/loading v0.24.0/go.mod h1:gShCN4woKZYIxPxbfbyHgjXAhO61m88tmjy0lp/LkJk= +github.com/go-openapi/swag/mangling v0.24.0 h1:PGOQpViCOUroIeak/Uj/sjGAq9LADS3mOyjznmHy2pk= +github.com/go-openapi/swag/mangling v0.24.0/go.mod h1:Jm5Go9LHkycsz0wfoaBDkdc4CkpuSnIEf62brzyCbhc= +github.com/go-openapi/swag/netutils v0.24.0 h1:Bz02HRjYv8046Ycg/w80q3g9QCWeIqTvlyOjQPDjD8w= +github.com/go-openapi/swag/netutils v0.24.0/go.mod h1:WRgiHcYTnx+IqfMCtu0hy9oOaPR0HnPbmArSRN1SkZM= +github.com/go-openapi/swag/stringutils v0.24.0 h1:i4Z/Jawf9EvXOLUbT97O0HbPUja18VdBxeadyAqS1FM= +github.com/go-openapi/swag/stringutils v0.24.0/go.mod h1:5nUXB4xA0kw2df5PRipZDslPJgJut+NjL7D25zPZ/4w= +github.com/go-openapi/swag/typeutils v0.24.0 h1:d3szEGzGDf4L2y1gYOSSLeK6h46F+zibnEas2Jm/wIw= +github.com/go-openapi/swag/typeutils v0.24.0/go.mod h1:q8C3Kmk/vh2VhpCLaoR2MVWOGP8y7Jc8l82qCTd1DYI= +github.com/go-openapi/swag/yamlutils v0.24.0 h1:bhw4894A7Iw6ne+639hsBNRHg9iZg/ISrOVr+sJGp4c= +github.com/go-openapi/swag/yamlutils v0.24.0/go.mod h1:DpKv5aYuaGm/sULePoeiG8uwMpZSfReo1HR3Ik0yaG8= 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= -github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= -github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= -github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 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 v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +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/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.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= -github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= -github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20250830080959-101d87ff5bc3 h1:c5evqpRcU++SkQZGh5cviTzmranbfRv/G2cPNDIVbCE= +github.com/google/pprof v0.0.0-20250830080959-101d87ff5bc3/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= -github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= -github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= -github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20240312041847-bd984b5ce465/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= -github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 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/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= +github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/k8snetworkplumbingwg/multi-networkpolicy v1.0.1 h1:Egj1hEVYNXWFlKpgzAXxe/2o8VNiVcAJLrKzlinILQo= github.com/k8snetworkplumbingwg/multi-networkpolicy v1.0.1/go.mod h1:kEJ4WM849yNmXekuSXLRwb+LaZ9usC06O8JgoAIq+f4= -github.com/k8snetworkplumbingwg/network-attachment-definition-client v0.0.0-20200528071255-22c819bc6e7e h1:hUUlPyMm/B8azg109Fx+mSbC3rRP7GNBTeEbU/qlRgA= -github.com/k8snetworkplumbingwg/network-attachment-definition-client v0.0.0-20200528071255-22c819bc6e7e/go.mod h1:g5bRWdyVeE9zGaKok219FZWcLbo2Lb+ANdvfVa637Mw= +github.com/k8snetworkplumbingwg/network-attachment-definition-client v1.7.7 h1:z4P744DR+PIpkjwXSEc6TvN3L6LVzmUquFgmNm8wSUc= +github.com/k8snetworkplumbingwg/network-attachment-definition-client v1.7.7/go.mod h1:CM7HAH5PNuIsqjMN0fGc1ydM74Uj+0VZFhob620nklw= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY= github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= -github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= +github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= +github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= +github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= +github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= @@ -180,278 +254,345 @@ github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8m github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= -github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= -github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU= -github.com/onsi/ginkgo/v2 v2.1.6/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= -github.com/onsi/ginkgo/v2 v2.3.0/go.mod h1:Eew0uilEqZmIEZr8JrvYlvOM7Rr6xzTmMV8AyFNU9d0= -github.com/onsi/ginkgo/v2 v2.4.0/go.mod h1:iHkDK1fKGcBoEHT5W7YBq4RFWaQulw+caOMkAt4OrFo= -github.com/onsi/ginkgo/v2 v2.5.0/go.mod h1:Luc4sArBICYCS8THh8v3i3i5CuSZO+RaQRaJoeNwomw= -github.com/onsi/ginkgo/v2 v2.7.0/go.mod h1:yjiuMwPokqY1XauOgju45q3sJt6VzQ/Fict1LFVcsAo= -github.com/onsi/ginkgo/v2 v2.8.1/go.mod h1:N1/NbDngAFcSLdyZ+/aYTYGSlq9qMCS/cNKGJjy+csc= -github.com/onsi/ginkgo/v2 v2.9.0/go.mod h1:4xkjoL/tZv4SMWeww56BU5kAt19mVB47gTWxmrTcxyk= -github.com/onsi/ginkgo/v2 v2.9.1/go.mod h1:FEcmzVcCHl+4o9bQZVab+4dC9+j+91t2FHSzmGAPfuo= -github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts= -github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= -github.com/onsi/ginkgo/v2 v2.9.7/go.mod h1:cxrmXWykAwTwhQsJOPfdIDiJ+l2RYq7U8hFU+M/1uw0= -github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM= -github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= -github.com/onsi/ginkgo/v2 v2.17.1/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs= -github.com/onsi/ginkgo/v2 v2.17.2/go.mod h1:nP2DPOQoNsQmsVyv5rDA8JkXQoCs6goXIvr/PRJ1eCc= -github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= -github.com/onsi/ginkgo/v2 v2.20.1/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI= -github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= -github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= -github.com/onsi/gomega v0.0.0-20151007035656-2152b45fa28a/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +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.27.3 h1:ICsZJ8JoYafeXFFlFAG75a7CxMsJHwgKwtO+82SE9L8= +github.com/onsi/ginkgo/v2 v2.27.3/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 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.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= -github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo= -github.com/onsi/gomega v1.21.1/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc= -github.com/onsi/gomega v1.22.1/go.mod h1:x6n7VNe4hw0vkyYUM4mjIXx3JbLiPaBPNgB7PRQ1tuM= -github.com/onsi/gomega v1.24.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg= -github.com/onsi/gomega v1.24.1/go.mod h1:3AOiACssS3/MajrniINInwbfOOtfZvplPzuRSmvt1jM= -github.com/onsi/gomega v1.26.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM= -github.com/onsi/gomega v1.27.1/go.mod h1:aHX5xOykVYzWOV4WqQy0sy8BQptgukenXpCXfadcIAw= -github.com/onsi/gomega v1.27.3/go.mod h1:5vG284IBtfDAmDyrK+eGyZmUgUlmi+Wngqo557cZ6Gw= -github.com/onsi/gomega v1.27.4/go.mod h1:riYq/GJKh8hhoM01HN6Vmuy93AarCXCBGpvFDK3q3fQ= -github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= -github.com/onsi/gomega v1.27.7/go.mod h1:1p8OOlwo2iUUDsHnOrjE5UKYJ+e3W8eQ3qSlRahPmr4= -github.com/onsi/gomega v1.27.8/go.mod h1:2J8vzI/s+2shY9XHRApDkdgPo1TKT7P2u6fXeJKFnNQ= -github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= -github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= -github.com/onsi/gomega v1.33.0/go.mod h1:+925n5YtiFsLzzafLUHzVMBpvvRAzrydIBiSIxjX3wY= -github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= -github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= -github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= -github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= -github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= -github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= +github.com/onsi/gomega v1.38.3 h1:eTX+W6dobAYfFeGC2PV6RwXRu/MyT+cQguijutvkpSM= +github.com/onsi/gomega v1.38.3/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc= +github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= +github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= +github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= +github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= -github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= -github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= -github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= 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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 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.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.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -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 v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= -github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= +github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= -go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= -go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= -go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= -go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= -go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= -go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= -go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= -go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= -go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= -go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= -golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -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/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= +golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/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= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= -golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= -golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= -golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190930201159-7c411dea38b0/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= -golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= -golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= -golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= -golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= -golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= -golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= -golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= -golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= -golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= -golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= -golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= -golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= -google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= -google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0= +gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda h1:i/Q+bfisr7gq6feoJnS/DlpdwEL4ihp41fvRiM3Ork0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= +google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= 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= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= -gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= -gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= @@ -464,54 +605,59 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/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.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= -k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= -k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= -k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= -k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= -k8s.io/code-generator v0.34.1/go.mod h1:DeWjekbDnJWRwpw3s0Jat87c+e0TgkxoR4ar608yqvg= -k8s.io/component-helpers v0.34.1 h1:gWhH3CCdwAx5P3oJqZKb4Lg5FYZTWVbdWtOI8n9U4XY= -k8s.io/component-helpers v0.34.1/go.mod h1:4VgnUH7UA/shuBur+OWoQC0xfb69sy/93ss0ybZqm3c= -k8s.io/cri-api v0.34.1 h1:n2bU++FqqJq0CNjP/5pkOs0nIx7aNpb1Xa053TecQkM= -k8s.io/cri-api v0.34.1/go.mod h1:4qVUjidMg7/Z9YGZpqIDygbkPWkg3mkS1PvOx/kpHTE= -k8s.io/cri-client v0.34.1 h1:eq6FcEPDDL379w0WhPnItj2egsMZqOtU7nv1JaJmwP0= -k8s.io/cri-client v0.34.1/go.mod h1:Dq6mKWV2ugO5tMv4xqVgcQ8vD7csP//e4KkzcFi2Pio= -k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/api v0.22.7/go.mod h1:7hejA1BgBEiSsWljUyRkIjj+AISXO16IwsaDgFjJsQE= +k8s.io/api v0.34.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY= +k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw= +k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI= +k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc= +k8s.io/apimachinery v0.22.7/go.mod h1:ZvVLP5iLhwVFg2Yx9Gh5W0um0DUauExbRhe+2Z8I1EU= +k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4= +k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/client-go v0.22.7/go.mod h1:pGU/tWSzzvsYT7M3npHhoZ3Jh9qJTTIvFvDtWuW31dw= +k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M= +k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE= +k8s.io/code-generator v0.22.7/go.mod h1:iOZwYADSgFPNGWfqHFfg1V0TNJnl1t0WyZluQp4baqU= +k8s.io/component-helpers v0.34.0 h1:5T7P9XGMoUy1JDNKzHf0p/upYbeUf8ZaSf9jbx0QlIo= +k8s.io/component-helpers v0.34.0/go.mod h1:kaOyl5tdtnymriYcVZg4uwDBe2d1wlIpXyDkt6sVnt4= +k8s.io/cri-api v0.34.0 h1:erzXelLqzDbNdryR7eVqxmR/1JfQeurE9U+HdKTgSpU= +k8s.io/cri-api v0.34.0/go.mod h1:4qVUjidMg7/Z9YGZpqIDygbkPWkg3mkS1PvOx/kpHTE= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= -k8s.io/gengo/v2 v2.0.0-20250604051438-85fd79dbfd9f/go.mod h1:EJykeLsmFC60UQbYJezXkEsG2FLrt0GPNkU5iK5GWxU= -k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= -k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= -k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= +k8s.io/gengo v0.0.0-20201214224949-b6c5ce23f027/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= -k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20200204173128-addea2498afe/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E= k8s.io/kube-openapi v0.0.0-20211109043538-20434351676c/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= -k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= -k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= -k8s.io/kubernetes v1.34.1 h1:F3p8dtpv+i8zQoebZeK5zBqM1g9x1aIdnA5vthvcuUk= -k8s.io/kubernetes v1.34.1/go.mod h1:iu+FhII+Oc/1gGWLJcer6wpyih441aNFHl7Pvm8yPto= -k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= -k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= -sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= -sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= -sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +k8s.io/kube-openapi v0.0.0-20250814151709-d7b6acb124c3 h1:liMHz39T5dJO1aOKHLvwaCjDbf07wVh6yaUlTpunnkE= +k8s.io/kube-openapi v0.0.0-20250814151709-d7b6acb124c3/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= +k8s.io/utils v0.0.0-20211116205334-6203023598ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d h1:wAhiDyZ4Tdtt7e46e9M5ZSAJ/MnPGPs+Ki1gHw4w1R0= +k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A= +sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/knftables v0.0.18 h1:6Duvmu0s/HwGifKrtl6G3AyAPYlWiZqTgS8bkVMiyaE= +sigs.k8s.io/knftables v0.0.18/go.mod h1:f/5ZLKYEUPUhVjUCg6l80ACdL7CIIyeL0DxfgojGRTk= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/structured-merge-diff/v6 v6.2.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v4 v4.2.1/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= -sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/pkg/controller/config/crd/multi-networkpolicies.yml b/pkg/controller/config/crd/multi-networkpolicies.yml new file mode 100644 index 00000000..96ebcd7f --- /dev/null +++ b/pkg/controller/config/crd/multi-networkpolicies.yml @@ -0,0 +1,927 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: multi-networkpolicies.k8s.cni.cncf.io +spec: + group: k8s.cni.cncf.io + scope: Namespaced + names: + plural: multi-networkpolicies + singular: multi-networkpolicy + kind: MultiNetworkPolicy + shortNames: + - multi-policy + versions: + - name: v1beta1 + served: true + storage: true + schema: + openAPIV3Schema: + description: "MultiNetworkPolicy is a CRD schema to provide NetworkPolicy + mechanism for net-attach-def which is specified by the Network Plumbing + Working Group. MultiNetworkPolicy is identical to Kubernetes NetworkPolicy, + See: https://kubernetes.io/docs/concepts/services-networking/network-policies/ ." + properties: + spec: + description: 'Specification of the desired behavior for this MultiNetworkPolicy.' + properties: + egress: + description: "List of egress rules to be applied to the selected pods. + Outgoing traffic is allowed if there are no NetworkPolicies selecting + the pod (and cluster policy otherwise allows the traffic), OR if the + traffic matches at least one egress rule across all of the NetworkPolicy + objects whose podSelector matches the pod. If this field is empty + then this NetworkPolicy limits all outgoing traffic (and serves solely + to ensure that the pods it selects are isolated by default). This + field is beta-level in 1.8" + items: + description: "NetworkPolicyEgressRule describes a particular set of + traffic that is allowed out of pods matched by a NetworkPolicySpec's + podSelector. The traffic must match both ports and to. This type + is beta-level in 1.8" + properties: + ports: + description: "List of destination ports for outgoing traffic. Each + item in this list is combined using a logical OR. If this field + is empty or missing, this rule matches all ports (traffic not + restricted by port). If this field is present and contains at + least one item, then this rule allows traffic only if the traffic + matches at least one port in the list." + items: + description: "NetworkPolicyPort describes a port to allow traffic on" + properties: + port: + anyOf: + - type: integer + - type: string + description: "The port on the given protocol. This can either + be a numerical or named port on a pod. If this field is + not provided, this matches all port names and numbers." + x-kubernetes-int-or-string: true + endPort: + type: integer + format: int32 + description: "If set, indicates that the range of ports from + port to endPort, inclusive, should be allowed by the policy. + This field cannot be defined if the port field is not + defined or if the port field is defined as a named (string) + port. The endPort must be equal or greater than port." + protocol: + description: "The protocol (TCP, UDP, or SCTP) which traffic + must match. If not specified, this field defaults to TCP." + type: string + type: object + type: array + to: + description: "List of destinations for outgoing traffic of pods + selected for this rule. Items in this list are combined using + a logical OR operation. If this field is empty or missing, this + rule matches all destinations (traffic not restricted by destination). + If this field is present and contains at least one item, this + rule allows traffic only if the traffic matches at least one + item in the to list." + items: + description: "NetworkPolicyPeer describes a peer to allow traffic + from. Only certain combinations of fields are allowed" + properties: + ipBlock: + description: "IPBlock defines policy on a particular IPBlock. + If this field is set then neither of the other fields + can be." + properties: + cidr: + description: "CIDR is a string representing the IP Block + Valid examples are '192.168.1.1/24'" + type: string + except: + description: "Except is a slice of CIDRs that should + not be included within an IP Block Valid examples + are '192.168.1.1/24' Except values will be rejected + if they are outside the CIDR range" + items: + type: string + type: array + required: + - cidr + type: object + namespaceSelector: + description: "Selects Namespaces using cluster-scoped labels. + This field follows standard label selector semantics; + if present but empty, it selects all namespaces. \n If + PodSelector is also set, then the NetworkPolicyPeer as + a whole selects the Pods matching PodSelector in the Namespaces + selected by NamespaceSelector. Otherwise it selects all + Pods in the Namespaces selected by NamespaceSelector." + properties: + matchExpressions: + description: "matchExpressions is a list of label selector + requirements. The requirements are ANDed." + items: + description: "A label selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values." + properties: + key: + description: "key is the label key that the selector + applies to." + type: string + operator: + description: "operator represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists and DoesNotExist." + type: string + values: + description: "values is an array of string values. + If the operator is In or NotIn, the values array + must be non-empty. If the operator is Exists + or DoesNotExist, the values array must be empty. + This array is replaced during a strategic merge + patch." + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: "matchLabels is a map of {key,value} pairs. + A single {key,value} in the matchLabels map is equivalent + to an element of matchExpressions, whose key field + is 'key', the operator is 'In', and the values array + contains only 'value'. The requirements are ANDed." + type: object + type: object + podSelector: + description: "This is a label selector which selects Pods. + This field follows standard label selector semantics; + if present but empty, it selects all pods. \n If NamespaceSelector + is also set, then the NetworkPolicyPeer as a whole selects + the Pods matching PodSelector in the Namespaces selected + by NamespaceSelector. Otherwise it selects the Pods matching + PodSelector in the policy's own Namespace." + properties: + matchExpressions: + description: "matchExpressions is a list of label selector + requirements. The requirements are ANDed." + items: + description: "A label selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values." + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: "operator represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists and DoesNotExist." + type: string + values: + description: "values is an array of string values. + If the operator is In or NotIn, the values array + must be non-empty. If the operator is Exists + or DoesNotExist, the values array must be empty. + This array is replaced during a strategic merge + patch." + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: "matchLabels is a map of {key,value} pairs. + A single {key,value} in the matchLabels map is equivalent + to an element of matchExpressions, whose key field + is 'key', the operator is 'In', and the values array + contains only 'value'. The requirements are ANDed." + type: object + type: object + type: object + type: array + type: object + type: array + ingress: + description: "List of ingress rules to be applied to the selected pods. + Traffic is allowed to a pod if there are no NetworkPolicies selecting + the pod (and cluster policy otherwise allows the traffic), OR if the + traffic source is the pod's local node, OR if the traffic matches + at least one ingress rule across all of the NetworkPolicy objects + whose podSelector matches the pod. If this field is empty then this + NetworkPolicy does not allow any traffic (and serves solely to ensure + that the pods it selects are isolated by default)" + items: + description: "NetworkPolicyIngressRule describes a particular set of + traffic that is allowed to the pods matched by a NetworkPolicySpec's + podSelector. The traffic must match both ports and from." + properties: + from: + description: "List of sources which should be able to access the + pods selected for this rule. Items in this list are combined + using a logical OR operation. If this field is empty or missing, + this rule matches all sources (traffic not restricted by source). + If this field is present and contains at least one item, this + rule allows traffic only if the traffic matches at least one + item in the from list." + items: + description: NetworkPolicyPeer describes a peer to allow traffic + from. Only certain combinations of fields are allowed + properties: + ipBlock: + description: "IPBlock defines policy on a particular IPBlock. + If this field is set then neither of the other fields + can be." + properties: + cidr: + description: "CIDR is a string representing the IP Block + Valid examples are '192.168.1.1/24'" + type: string + except: + description: "Except is a slice of CIDRs that should + not be included within an IP Block Valid examples + are '192.168.1.1/24' Except values will be rejected + if they are outside the CIDR range" + items: + type: string + type: array + required: + - cidr + type: object + namespaceSelector: + description: "Selects Namespaces using cluster-scoped labels. + This field follows standard label selector semantics; + if present but empty, it selects all namespaces. \n If + PodSelector is also set, then the NetworkPolicyPeer as + a whole selects the Pods matching PodSelector in the Namespaces + selected by NamespaceSelector. Otherwise it selects all + Pods in the Namespaces selected by NamespaceSelector." + properties: + matchExpressions: + description: "matchExpressions is a list of label selector + requirements. The requirements are ANDed." + items: + description: "A label selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values." + properties: + key: + description: "key is the label key that the selector + applies to." + type: string + operator: + description: "operator represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists and DoesNotExist." + type: string + values: + description: "values is an array of string values. + If the operator is In or NotIn, the values array + must be non-empty. If the operator is Exists + or DoesNotExist, the values array must be empty. + This array is replaced during a strategic merge + patch." + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: "matchLabels is a map of {key,value} pairs. + A single {key,value} in the matchLabels map is equivalent + to an element of matchExpressions, whose key field + is 'key', the operator is 'In', and the values array + contains only 'value'. The requirements are ANDed." + type: object + type: object + podSelector: + description: "This is a label selector which selects Pods. + This field follows standard label selector semantics; + if present but empty, it selects all pods. \n If NamespaceSelector + is also set, then the NetworkPolicyPeer as a whole selects + the Pods matching PodSelector in the Namespaces selected + by NamespaceSelector. Otherwise it selects the Pods matching + PodSelector in the policy's own Namespace." + properties: + matchExpressions: + description: "matchExpressions is a list of label selector + requirements. The requirements are ANDed." + items: + description: "A label selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values." + properties: + key: + description: "key is the label key that the selector + applies to." + type: string + operator: + description: "operator represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists and DoesNotExist." + type: string + values: + description: "values is an array of string values. + If the operator is In or NotIn, the values array + must be non-empty. If the operator is Exists + or DoesNotExist, the values array must be empty. + This array is replaced during a strategic merge + patch." + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: "matchLabels is a map of {key,value} pairs. + A single {key,value} in the matchLabels map is equivalent + to an element of matchExpressions, whose key field + is 'key', the operator is 'In', and the values array + contains only 'value'. The requirements are ANDed." + type: object + type: object + type: object + type: array + ports: + description: "List of ports which should be made accessible on + the pods selected for this rule. Each item in this list is combined + using a logical OR. If this field is empty or missing, this + rule matches all ports (traffic not restricted by port). If + this field is present and contains at least one item, then this + rule allows traffic only if the traffic matches at least one + port in the list." + items: + description: NetworkPolicyPort describes a port to allow traffic + on + properties: + port: + anyOf: + - type: integer + - type: string + description: "The port on the given protocol. This can either + be a numerical or named port on a pod. If this field is + not provided, this matches all port names and numbers." + x-kubernetes-int-or-string: true + endPort: + type: integer + format: int32 + description: "If set, indicates that the range of ports from + port to endPort, inclusive, should be allowed by the policy. + This field cannot be defined if the port field is not + defined or if the port field is defined as a named (string) + port. The endPort must be equal or greater than port." + protocol: + description: "The protocol (TCP, UDP, or SCTP) which traffic + must match. If not specified, this field defaults to TCP." + type: string + type: object + type: array + type: object + type: array + podSelector: + description: "This is a label selector which selects Pods. + This field follows standard label selector semantics; + if present but empty, it selects all pods. \n If NamespaceSelector + is also set, then the NetworkPolicyPeer as a whole selects + the Pods matching PodSelector in the Namespaces selected + by NamespaceSelector. Otherwise it selects the Pods matching + PodSelector in the policy's own Namespace." + properties: + matchExpressions: + description: "matchExpressions is a list of label selector + requirements. The requirements are ANDed." + items: + description: "A label selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values." + properties: + key: + description: "key is the label key that the selector applies to." + type: string + operator: + description: "operator represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists and DoesNotExist." + type: string + values: + description: "values is an array of string values. + If the operator is In or NotIn, the values array + must be non-empty. If the operator is Exists + or DoesNotExist, the values array must be empty. + This array is replaced during a strategic merge + patch." + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: "matchLabels is a map of {key,value} pairs. + A single {key,value} in the matchLabels map is equivalent + to an element of matchExpressions, whose key field + is 'key', the operator is 'In', and the values array + contains only 'value'. The requirements are ANDed." + type: object + type: object + policyTypes: + description: "List of rule types that the NetworkPolicy relates to. Valid + options are 'Ingress', 'Egress', or 'Ingress,Egress'. If this field + is not specified, it will default based on the existence of Ingress + or Egress rules; policies that contain an Egress section are assumed + to affect Egress, and all policies (whether or not they contain an + Ingress section) are assumed to affect Ingress. If you want to write + an egress-only policy, you must explicitly specify policyTypes [ 'Egress' + ]. Likewise, if you want to write a policy that specifies that no + egress is allowed, you must specify a policyTypes value that include + 'Egress' (since such a policy would not include an Egress section + and would otherwise default to just [ 'Ingress' ]). This field is + beta-level in 1.8" + items: + description: "Policy Type string describes the NetworkPolicy type This + type is beta-level in 1.8" + type: string + type: array + required: + - podSelector + type: object + type: object + - name: v1beta2 + served: false + storage: false + schema: + openAPIV3Schema: + description: "MultiNetworkPolicy is a CRD schema to provide NetworkPolicy + mechanism for net-attach-def which is specified by the Network Plumbing + Working Group. MultiNetworkPolicy is identical to Kubernetes NetworkPolicy, + See: https://kubernetes.io/docs/concepts/services-networking/network-policies/ ." + properties: + spec: + description: 'Specification of the desired behavior for this MultiNetworkPolicy.' + properties: + egress: + description: "List of egress rules to be applied to the selected pods. + Outgoing traffic is allowed if there are no NetworkPolicies selecting + the pod (and cluster policy otherwise allows the traffic), OR if the + traffic matches at least one egress rule across all of the NetworkPolicy + objects whose podSelector matches the pod. If this field is empty + then this NetworkPolicy limits all outgoing traffic (and serves solely + to ensure that the pods it selects are isolated by default). This + field is beta-level in 1.8" + items: + description: "NetworkPolicyEgressRule describes a particular set of + traffic that is allowed out of pods matched by a NetworkPolicySpec's + podSelector. The traffic must match both ports and to. This type + is beta-level in 1.8" + properties: + ports: + description: "List of destination ports for outgoing traffic. Each + item in this list is combined using a logical OR. If this field + is empty or missing, this rule matches all ports (traffic not + restricted by port). If this field is present and contains at + least one item, then this rule allows traffic only if the traffic + matches at least one port in the list." + items: + description: "NetworkPolicyPort describes a port to allow traffic on" + properties: + port: + anyOf: + - type: integer + - type: string + description: "The port on the given protocol. This can either + be a numerical or named port on a pod. If this field is + not provided, this matches all port names and numbers." + x-kubernetes-int-or-string: true + endPort: + type: integer + format: int32 + description: "If set, indicates that the range of ports from + port to endPort, inclusive, should be allowed by the policy. + This field cannot be defined if the port field is not + defined or if the port field is defined as a named (string) + port. The endPort must be equal or greater than port." + protocol: + description: "The protocol (TCP, UDP, or SCTP) which traffic + must match. If not specified, this field defaults to TCP." + type: string + type: object + type: array + to: + description: "List of destinations for outgoing traffic of pods + selected for this rule. Items in this list are combined using + a logical OR operation. If this field is empty or missing, this + rule matches all destinations (traffic not restricted by destination). + If this field is present and contains at least one item, this + rule allows traffic only if the traffic matches at least one + item in the to list." + items: + description: "NetworkPolicyPeer describes a peer to allow traffic + from. Only certain combinations of fields are allowed" + properties: + ipBlock: + description: "IPBlock defines policy on a particular IPBlock. + If this field is set then neither of the other fields + can be." + properties: + cidr: + description: "CIDR is a string representing the IP Block + Valid examples are '192.168.1.1/24'" + type: string + except: + description: "Except is a slice of CIDRs that should + not be included within an IP Block Valid examples + are '192.168.1.1/24' Except values will be rejected + if they are outside the CIDR range" + items: + type: string + type: array + required: + - cidr + type: object + namespaceSelector: + description: "Selects Namespaces using cluster-scoped labels. + This field follows standard label selector semantics; + if present but empty, it selects all namespaces. \n If + PodSelector is also set, then the NetworkPolicyPeer as + a whole selects the Pods matching PodSelector in the Namespaces + selected by NamespaceSelector. Otherwise it selects all + Pods in the Namespaces selected by NamespaceSelector." + properties: + matchExpressions: + description: "matchExpressions is a list of label selector + requirements. The requirements are ANDed." + items: + description: "A label selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values." + properties: + key: + description: "key is the label key that the selector + applies to." + type: string + operator: + description: "operator represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists and DoesNotExist." + type: string + values: + description: "values is an array of string values. + If the operator is In or NotIn, the values array + must be non-empty. If the operator is Exists + or DoesNotExist, the values array must be empty. + This array is replaced during a strategic merge + patch." + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: "matchLabels is a map of {key,value} pairs. + A single {key,value} in the matchLabels map is equivalent + to an element of matchExpressions, whose key field + is 'key', the operator is 'In', and the values array + contains only 'value'. The requirements are ANDed." + type: object + type: object + podSelector: + description: "This is a label selector which selects Pods. + This field follows standard label selector semantics; + if present but empty, it selects all pods. \n If NamespaceSelector + is also set, then the NetworkPolicyPeer as a whole selects + the Pods matching PodSelector in the Namespaces selected + by NamespaceSelector. Otherwise it selects the Pods matching + PodSelector in the policy's own Namespace." + properties: + matchExpressions: + description: "matchExpressions is a list of label selector + requirements. The requirements are ANDed." + items: + description: "A label selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values." + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: "operator represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists and DoesNotExist." + type: string + values: + description: "values is an array of string values. + If the operator is In or NotIn, the values array + must be non-empty. If the operator is Exists + or DoesNotExist, the values array must be empty. + This array is replaced during a strategic merge + patch." + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: "matchLabels is a map of {key,value} pairs. + A single {key,value} in the matchLabels map is equivalent + to an element of matchExpressions, whose key field + is 'key', the operator is 'In', and the values array + contains only 'value'. The requirements are ANDed." + type: object + type: object + type: object + type: array + type: object + type: array + ingress: + description: "List of ingress rules to be applied to the selected pods. + Traffic is allowed to a pod if there are no NetworkPolicies selecting + the pod (and cluster policy otherwise allows the traffic), OR if the + traffic source is the pod's local node, OR if the traffic matches + at least one ingress rule across all of the NetworkPolicy objects + whose podSelector matches the pod. If this field is empty then this + NetworkPolicy does not allow any traffic (and serves solely to ensure + that the pods it selects are isolated by default)" + items: + description: "NetworkPolicyIngressRule describes a particular set of + traffic that is allowed to the pods matched by a NetworkPolicySpec's + podSelector. The traffic must match both ports and from." + properties: + from: + description: "List of sources which should be able to access the + pods selected for this rule. Items in this list are combined + using a logical OR operation. If this field is empty or missing, + this rule matches all sources (traffic not restricted by source). + If this field is present and contains at least one item, this + rule allows traffic only if the traffic matches at least one + item in the from list." + items: + description: NetworkPolicyPeer describes a peer to allow traffic + from. Only certain combinations of fields are allowed + properties: + ipBlock: + description: "IPBlock defines policy on a particular IPBlock. + If this field is set then neither of the other fields + can be." + properties: + cidr: + description: "CIDR is a string representing the IP Block + Valid examples are '192.168.1.1/24'" + type: string + except: + description: "Except is a slice of CIDRs that should + not be included within an IP Block Valid examples + are '192.168.1.1/24' Except values will be rejected + if they are outside the CIDR range" + items: + type: string + type: array + required: + - cidr + type: object + namespaceSelector: + description: "Selects Namespaces using cluster-scoped labels. + This field follows standard label selector semantics; + if present but empty, it selects all namespaces. \n If + PodSelector is also set, then the NetworkPolicyPeer as + a whole selects the Pods matching PodSelector in the Namespaces + selected by NamespaceSelector. Otherwise it selects all + Pods in the Namespaces selected by NamespaceSelector." + properties: + matchExpressions: + description: "matchExpressions is a list of label selector + requirements. The requirements are ANDed." + items: + description: "A label selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values." + properties: + key: + description: "key is the label key that the selector + applies to." + type: string + operator: + description: "operator represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists and DoesNotExist." + type: string + values: + description: "values is an array of string values. + If the operator is In or NotIn, the values array + must be non-empty. If the operator is Exists + or DoesNotExist, the values array must be empty. + This array is replaced during a strategic merge + patch." + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: "matchLabels is a map of {key,value} pairs. + A single {key,value} in the matchLabels map is equivalent + to an element of matchExpressions, whose key field + is 'key', the operator is 'In', and the values array + contains only 'value'. The requirements are ANDed." + type: object + type: object + podSelector: + description: "This is a label selector which selects Pods. + This field follows standard label selector semantics; + if present but empty, it selects all pods. \n If NamespaceSelector + is also set, then the NetworkPolicyPeer as a whole selects + the Pods matching PodSelector in the Namespaces selected + by NamespaceSelector. Otherwise it selects the Pods matching + PodSelector in the policy's own Namespace." + properties: + matchExpressions: + description: "matchExpressions is a list of label selector + requirements. The requirements are ANDed." + items: + description: "A label selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values." + properties: + key: + description: "key is the label key that the selector + applies to." + type: string + operator: + description: "operator represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists and DoesNotExist." + type: string + values: + description: "values is an array of string values. + If the operator is In or NotIn, the values array + must be non-empty. If the operator is Exists + or DoesNotExist, the values array must be empty. + This array is replaced during a strategic merge + patch." + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: "matchLabels is a map of {key,value} pairs. + A single {key,value} in the matchLabels map is equivalent + to an element of matchExpressions, whose key field + is 'key', the operator is 'In', and the values array + contains only 'value'. The requirements are ANDed." + type: object + type: object + type: object + type: array + ports: + description: "List of ports which should be made accessible on + the pods selected for this rule. Each item in this list is combined + using a logical OR. If this field is empty or missing, this + rule matches all ports (traffic not restricted by port). If + this field is present and contains at least one item, then this + rule allows traffic only if the traffic matches at least one + port in the list." + items: + description: NetworkPolicyPort describes a port to allow traffic + on + properties: + port: + anyOf: + - type: integer + - type: string + description: "The port on the given protocol. This can either + be a numerical or named port on a pod. If this field is + not provided, this matches all port names and numbers." + x-kubernetes-int-or-string: true + endPort: + type: integer + format: int32 + description: "If set, indicates that the range of ports from + port to endPort, inclusive, should be allowed by the policy. + This field cannot be defined if the port field is not + defined or if the port field is defined as a named (string) + port. The endPort must be equal or greater than port." + protocol: + description: "The protocol (TCP, UDP, or SCTP) which traffic + must match. If not specified, this field defaults to TCP." + type: string + type: object + type: array + type: object + type: array + podSelector: + description: "This is a label selector which selects Pods. + This field follows standard label selector semantics; + if present but empty, it selects all pods. \n If NamespaceSelector + is also set, then the NetworkPolicyPeer as a whole selects + the Pods matching PodSelector in the Namespaces selected + by NamespaceSelector. Otherwise it selects the Pods matching + PodSelector in the policy's own Namespace." + properties: + matchExpressions: + description: "matchExpressions is a list of label selector + requirements. The requirements are ANDed." + items: + description: "A label selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values." + properties: + key: + description: "key is the label key that the selector applies to." + type: string + operator: + description: "operator represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists and DoesNotExist." + type: string + values: + description: "values is an array of string values. + If the operator is In or NotIn, the values array + must be non-empty. If the operator is Exists + or DoesNotExist, the values array must be empty. + This array is replaced during a strategic merge + patch." + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: "matchLabels is a map of {key,value} pairs. + A single {key,value} in the matchLabels map is equivalent + to an element of matchExpressions, whose key field + is 'key', the operator is 'In', and the values array + contains only 'value'. The requirements are ANDed." + type: object + type: object + policyTypes: + description: "List of rule types that the NetworkPolicy relates to. Valid + options are 'Ingress', 'Egress', or 'Ingress,Egress'. If this field + is not specified, it will default based on the existence of Ingress + or Egress rules; policies that contain an Egress section are assumed + to affect Egress, and all policies (whether or not they contain an + Ingress section) are assumed to affect Ingress. If you want to write + an egress-only policy, you must explicitly specify policyTypes [ 'Egress' + ]. Likewise, if you want to write a policy that specifies that no + egress is allowed, you must specify a policyTypes value that include + 'Egress' (since such a policy would not include an Egress section + and would otherwise default to just [ 'Ingress' ]). This field is + beta-level in 1.8" + items: + description: "Policy Type string describes the NetworkPolicy type This + type is beta-level in 1.8" + type: string + type: array + required: + - podSelector + type: object + type: object diff --git a/pkg/controller/config/crd/network-attachment-definition.yml b/pkg/controller/config/crd/network-attachment-definition.yml new file mode 100644 index 00000000..9aac4520 --- /dev/null +++ b/pkg/controller/config/crd/network-attachment-definition.yml @@ -0,0 +1,27 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: network-attachment-definitions.k8s.cni.cncf.io +spec: + group: k8s.cni.cncf.io + scope: Namespaced + names: + plural: network-attachment-definitions + singular: network-attachment-definition + kind: NetworkAttachmentDefinition + shortNames: + - net-attach-def + versions: + - name: v1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + config: + type: string diff --git a/pkg/controller/enqueues.go b/pkg/controller/enqueues.go new file mode 100644 index 00000000..828af050 --- /dev/null +++ b/pkg/controller/enqueues.go @@ -0,0 +1,213 @@ +package controller + +import ( + "context" + "fmt" + + "github.com/go-logr/logr" + multiv1beta1 "github.com/k8snetworkplumbingwg/multi-networkpolicy/pkg/apis/k8s.cni.cncf.io/v1beta1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/k8snetworkplumbingwg/multi-network-policy-nftables/pkg/utils" +) + +// namespaceEnqueue returns a function that enqueues policies affected by a namespace event +func namespaceEnqueue(clt client.Client) func(ctx context.Context, ns client.Object) []reconcile.Request { + return func(ctx context.Context, ns client.Object) []reconcile.Request { + logger := log.FromContext(ctx).WithValues("namespace", ns.GetName()) + + namespace, ok := ns.(*corev1.Namespace) + if !ok { + // Should not happen + return []reconcile.Request{} + } + + var mp multiv1beta1.MultiNetworkPolicyList + err := clt.List(ctx, &mp) + if err != nil { + logger.Error(err, "Failed to list policies") + return []reconcile.Request{} + } + + logger.V(1).Info("Checking policies affected by namespace") + + var requests []reconcile.Request + for _, policy := range mp.Items { + if isPolicyAffectedByNamespace(&policy, namespace, logger) { + namespaceName := types.NamespacedName{Namespace: policy.Namespace, Name: policy.Name} + logger.Info("Policy is affected by namespace", "policy", namespaceName) + requests = append(requests, reconcile.Request{NamespacedName: namespaceName}) + } + } + + return requests + } +} + +// podEnqueue returns a function that enqueues policies affected by a pod event +func podEnqueue(clt client.Client) func(ctx context.Context, ns client.Object) []reconcile.Request { + return func(ctx context.Context, ns client.Object) []reconcile.Request { + logger := log.FromContext(ctx).WithValues("pod", ns.GetName(), "namespace", ns.GetNamespace()) + pod, ok := ns.(*corev1.Pod) + if !ok { + // Should not happen + return []reconcile.Request{} + } + + var mp multiv1beta1.MultiNetworkPolicyList + err := clt.List(ctx, &mp) + if err != nil { + logger.Error(err, "Failed to list policies") + return []reconcile.Request{} + } + + logger.V(1).Info("Checking policies affected by pod") + + var requests []reconcile.Request + for _, policy := range mp.Items { + if isPolicyAffectedByPod(&policy, pod, logger) { + namespaceName := types.NamespacedName{Namespace: policy.Namespace, Name: policy.Name} + logger.Info("Policy is affected by pod", "policy", namespaceName) + requests = append(requests, reconcile.Request{NamespacedName: namespaceName}) + } + } + + return requests + } +} + +// isPolicyAffectedByNamespace checks if a policy is affected by a namespace +func isPolicyAffectedByNamespace(policy *multiv1beta1.MultiNetworkPolicy, namespace *corev1.Namespace, logger logr.Logger) bool { + // Validate input parameters + if policy == nil || namespace == nil { + return false + } + + logger = logger.WithValues("policy", fmt.Sprintf("%s/%s", policy.Namespace, policy.Name)) + + // Ingress selectors + for _, ingress := range policy.Spec.Ingress { + for _, from := range ingress.From { + // If IPBlock is set, we don't need to check the other fields + if from.IPBlock == nil && from.NamespaceSelector != nil { + if utils.MatchesSelector(*from.NamespaceSelector, namespace.Labels) { + logger.V(1).Info("Policy selected by ingress namespace selector") + return true + } + } + } + } + + // Egress selectors + for _, egress := range policy.Spec.Egress { + for _, to := range egress.To { + // If IPBlock is set, we don't need to check the other fields + if to.IPBlock == nil && to.NamespaceSelector != nil { + if utils.MatchesSelector(*to.NamespaceSelector, namespace.Labels) { + logger.V(1).Info("Policy selected by egress namespace selector") + return true + } + } + } + } + + return false +} + +// isPolicyAffectedByPod is the internal implementation without locking +func isPolicyAffectedByPod(policy *multiv1beta1.MultiNetworkPolicy, pod *corev1.Pod, logger logr.Logger) bool { + // Validate input parameters + if policy == nil || pod == nil { + return false + } + + logger = logger.WithValues("policy", fmt.Sprintf("%s/%s", policy.Namespace, policy.Name)) + + // Ingress selectors + for _, ingress := range policy.Spec.Ingress { + // If empty or missing, match all pods + if len(ingress.From) == 0 { + logger.V(1).Info("Policy selected by ingress allow all") + return true + } + + for _, from := range ingress.From { + // If IPBlock is set, we don't need to check the other fields + if from.IPBlock != nil { + continue + } + + // If only pod selector is set, then the namespace of the policy and the pod must match + if from.PodSelector != nil && from.NamespaceSelector == nil { + if pod.Namespace != policy.Namespace { + continue + } + + if utils.MatchesSelector(*from.PodSelector, pod.Labels) { + logger.V(1).Info("Policy selected by ingress pod selector") + return true + } + } else if from.PodSelector != nil { + if utils.MatchesSelector(*from.PodSelector, pod.Labels) { + logger.V(1).Info("Policy selected by ingress pod and namespace selector") + return true + } + } + } + } + + // Egress selectors + for _, egress := range policy.Spec.Egress { + // If empty or missing, match all pods + if len(egress.To) == 0 { + logger.V(1).Info("Policy selected by egress allow all") + return true + } + + for _, to := range egress.To { + // If IPBlock is set, we don't need to check the other fields + if to.IPBlock != nil { + continue + } + + // If only pod selector is set, then the namespace of the policy and the pod must match + if to.PodSelector != nil && to.NamespaceSelector == nil { + if pod.Namespace != policy.Namespace { + continue + } + + if utils.MatchesSelector(*to.PodSelector, pod.Labels) { + logger.V(1).Info("Policy selected by egress pod selector") + return true + } + } else if to.PodSelector != nil { + if utils.MatchesSelector(*to.PodSelector, pod.Labels) { + logger.V(1).Info("Policy selected by egress pod and namespace selector") + return true + } + } + } + } + + // If we are here, then the namespace of the policy and the pod must match + if pod.Namespace != policy.Namespace { + return false + } + + // Check policy pod selector + if utils.MatchesSelector(policy.Spec.PodSelector, pod.Labels) { + // We only care if the pod is running + // TODO: find a way to apply the policy only to this particular pod + // TODO: only if the pod is located in this node - Add hostname check + if pod.Status.Phase == corev1.PodRunning { + logger.V(1).Info("Policy selected by policy pod selector", "pod Status", pod.Status.Phase) + return true + } + } + + return false +} diff --git a/pkg/controller/indexes.go b/pkg/controller/indexes.go new file mode 100644 index 00000000..021c20ae --- /dev/null +++ b/pkg/controller/indexes.go @@ -0,0 +1,64 @@ +package controller + +import ( + "context" + "strconv" + + netdefutils "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/utils" + corev1 "k8s.io/api/core/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/k8snetworkplumbingwg/multi-network-policy-nftables/pkg/nftables" +) + +func setupIndexes(mgr ctrl.Manager) error { + // Pod Hostname index + if err := mgr.GetFieldIndexer().IndexField(context.Background(), &corev1.Pod{}, nftables.PodHostnameIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + + if pod.Spec.NodeName == "" { + return nil + } + + return []string{pod.Spec.NodeName} + }); err != nil { + return err + } + + // Pod Status Phase index + if err := mgr.GetFieldIndexer().IndexField(context.Background(), &corev1.Pod{}, nftables.PodStatusIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + return []string{string(pod.Status.Phase)} + }); err != nil { + return err + } + + // Pod Host Network index + if err := mgr.GetFieldIndexer().IndexField(context.Background(), &corev1.Pod{}, nftables.PodHostNetworkIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + return []string{strconv.FormatBool(pod.Spec.HostNetwork)} + }); err != nil { + return err + } + + // Pod Has Network Annotation index + return mgr.GetFieldIndexer().IndexField(context.Background(), &corev1.Pod{}, nftables.PodHasNetworkAnnotationIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + + if pod.GetAnnotations() == nil { + return []string{"false"} + } + + networks, err := netdefutils.ParsePodNetworkAnnotation(pod) + if err != nil { + return []string{"false"} + } + + if len(networks) == 0 { + return []string{"false"} + } + + return []string{"true"} + }) +} diff --git a/pkg/controller/multinetwork_controller.go b/pkg/controller/multinetwork_controller.go new file mode 100644 index 00000000..b579359f --- /dev/null +++ b/pkg/controller/multinetwork_controller.go @@ -0,0 +1,307 @@ +// Package controller provides Kubernetes controllers for managing multi-network policies +package controller + +import ( + "context" + "encoding/json" + "fmt" + "slices" + "strings" + + cnitypes "github.com/containernetworking/cni/pkg/types" + "github.com/go-logr/logr" + multiv1beta1 "github.com/k8snetworkplumbingwg/multi-networkpolicy/pkg/apis/k8s.cni.cncf.io/v1beta1" + netdefv1 "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/apis/k8s.cni.cncf.io/v1" + netdefutils "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/utils" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/k8snetworkplumbingwg/multi-network-policy-nftables/pkg/datastore" + "github.com/k8snetworkplumbingwg/multi-network-policy-nftables/pkg/nftables" +) + +// MultiNetworkReconciler reconciles a MultiNetworkPolicy object +type MultiNetworkReconciler struct { + client.Client + Scheme *runtime.Scheme + DS *datastore.Datastore + NFT nftables.SyncInterface + ValidPlugins []string +} + +// Reconcile handles the reconciliation of MultiNetworkPolicy resources +func (m *MultiNetworkReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := log.FromContext(ctx).WithValues("namespace", req.NamespacedName.Namespace, "name", req.NamespacedName.Name) + + logger.Info("Starting reconciliation of MultiNetworkPolicy") + + instance := &multiv1beta1.MultiNetworkPolicy{} + err := m.Client.Get(ctx, req.NamespacedName, instance) + if err != nil { + if !errors.IsNotFound(err) { + logger.Error(err, "Failed to get instance") + return ctrl.Result{}, err + } + + err = m.cleanUpPolicy(ctx, req.Name, req.Namespace, logger) + if err != nil { + logger.Error(err, "Failed to clean up policy") + return ctrl.Result{}, err + } + + // Ignore, not found + logger.V(1).Info("MultiNetworkPolicy not found, it might have been deleted") + return ctrl.Result{}, nil + } + + return m.processPolicy(ctx, instance, logger) +} + +// processPolicy validates and processes the MultiNetworkPolicy +func (m *MultiNetworkReconciler) processPolicy(ctx context.Context, instance *multiv1beta1.MultiNetworkPolicy, logger logr.Logger) (ctrl.Result, error) { + policyForAnnotation, err := getPolicyForAnnotation(instance) + if err != nil { + logger.Info("Failed to validate policy-for annotation", "error", err.Error()) + err = m.cleanUpPolicy(ctx, instance.Name, instance.Namespace, logger) + if err != nil { + logger.Error(err, "Failed to clean up policy") + return ctrl.Result{}, err + } + return ctrl.Result{}, nil + } + + // Get networks from policy-for annotation + networks, err := getNetworksInPolicyForAnnotation(policyForAnnotation, instance.Namespace) + if err != nil { + logger.Info("Failed to get networks from policy-for annotation", "error", err.Error()) + err = m.cleanUpPolicy(ctx, instance.Name, instance.Namespace, logger) + if err != nil { + logger.Error(err, "Failed to clean up policy") + return ctrl.Result{}, err + } + return ctrl.Result{}, nil + } + + logger.Info("Networks found in policy-for annotation", "networks", networks) + + // Verify that the networks are allowed by the valid plugins + allowedNetworks, err := m.getAllowedNetworks(ctx, networks, m.ValidPlugins, logger) + if err != nil { + logger.Info("Failed to get allowed networks", "valid plugins", m.ValidPlugins, "error", err.Error()) + err = m.cleanUpPolicy(ctx, instance.Name, instance.Namespace, logger) + if err != nil { + logger.Error(err, "Failed to clean up policy") + return ctrl.Result{}, err + } + return ctrl.Result{}, nil + } + + logger.Info("Allowed networks", "allowedNetworks", allowedNetworks) + + policy := &datastore.Policy{ + Name: instance.Name, + Namespace: instance.Namespace, + Spec: instance.Spec, + Networks: allowedNetworks, + } + + err = m.NFT.SyncPolicy(ctx, policy, nftables.SyncOperationCreate, logger) + if err != nil { + logger.Error(err, "Failed to sync policies, requeuing") + return ctrl.Result{}, err + } + + m.DS.CreatePolicy(policy) + + logger.Info("MultiNetworkPolicy reconciled successfully") + return ctrl.Result{}, nil +} + +// cleanUpPolicy cleans up a policy from the datastore +func (m *MultiNetworkReconciler) cleanUpPolicy(ctx context.Context, name string, namespace string, logger logr.Logger) error { + policy := m.DS.GetPolicy(types.NamespacedName{Namespace: namespace, Name: name}) + if policy == nil { + return nil + } + + err := m.NFT.SyncPolicy(ctx, policy, nftables.SyncOperationDelete, logger) + if err != nil { + return fmt.Errorf("failed to sync policy: %w", err) + } + + m.DS.DeletePolicy(types.NamespacedName{Namespace: namespace, Name: name}) + logger.Info("Policy cleaned up successfully") + return nil +} + +// getPolicyForAnnotation gets the policy-for annotation from the MultiNetworkPolicy +func getPolicyForAnnotation(instance *multiv1beta1.MultiNetworkPolicy) (string, error) { + annotations := instance.GetAnnotations() + if annotations == nil { + return "", fmt.Errorf("MultiNetworkPolicy has no annotations") + } + + policyForAnnotation, hasAnnotation := annotations[datastore.PolicyForAnnotation] + if !hasAnnotation { + return "", fmt.Errorf("missing required annotation %s", datastore.PolicyForAnnotation) + } + + // Validate PolicyForAnnotation is not empty + trimmedAnnotation := strings.TrimSpace(policyForAnnotation) + if trimmedAnnotation == "" { + return "", fmt.Errorf("annotation %s is empty", datastore.PolicyForAnnotation) + } + + return trimmedAnnotation, nil +} + +// getNetworksInPolicyForAnnotation gets the networks from the policy-for annotation +func getNetworksInPolicyForAnnotation(policyForAnnotation string, namespace string) ([]string, error) { + // Split by comma and check for at least one valid network name + networkNames := strings.Split(policyForAnnotation, ",") + + networks := []string{} + for _, networkName := range networkNames { + trimmedNetwork := strings.TrimSpace(networkName) + if trimmedNetwork == "" { + continue + } + + // Only allow formats: "name" or "namespace/name" + if strings.Count(trimmedNetwork, "/") > 1 { + continue + } + + var ns string + var name string + + parts := strings.Split(trimmedNetwork, "/") + if len(parts) == 2 { + ns = strings.TrimSpace(parts[0]) + name = strings.TrimSpace(parts[1]) + } else { + ns = namespace + name = strings.TrimSpace(parts[0]) + } + + if ns == "" || name == "" { + continue + } + + networks = append(networks, fmt.Sprintf("%s/%s", ns, name)) + } + + if len(networks) == 0 { + return nil, fmt.Errorf("annotation %s contains no valid network names: %s", datastore.PolicyForAnnotation, policyForAnnotation) + } + + return networks, nil +} + +// getAllowedNetworks gets the allowed networks from the networks and the valid plugins +func (m *MultiNetworkReconciler) getAllowedNetworks(ctx context.Context, networks []string, validPlugins []string, logger logr.Logger) ([]string, error) { + var allowedNetworks []string + for _, network := range networks { + parts := strings.Split(network, "/") + if len(parts) != 2 { + // Should not happen due to previous validation + logger.Info("Invalid network format, skipping", "network", network) + continue + } + + // Get Network-Attachment-Definition + var netAttachDef netdefv1.NetworkAttachmentDefinition + err := m.Client.Get(ctx, types.NamespacedName{Namespace: parts[0], Name: parts[1]}, &netAttachDef) + if err != nil { + if errors.IsNotFound(err) { + // Ignore, not found + continue + } + + return nil, fmt.Errorf("failed to get network attachment definition: %w", err) + } + + networkType, err := getNetworkType(&netAttachDef) + if err != nil { + return nil, fmt.Errorf("failed to get network type: %w", err) + } + + if slices.Contains(validPlugins, networkType) { + logger.Info("Network type is supported", "network", network, "networkType", networkType) + allowedNetworks = append(allowedNetworks, network) + } else { + logger.Info("Network type is not supported", "network", network, "networkType", networkType) + } + } + + if len(allowedNetworks) == 0 { + return nil, fmt.Errorf("no allowed networks found") + } + + return allowedNetworks, nil +} + +// getNetworkType returns the type of a network +func getNetworkType(netAttachDef *netdefv1.NetworkAttachmentDefinition) (string, error) { + if netAttachDef == nil { + return "", fmt.Errorf("network attachment definition is nil") + } + + var netType string + + confBytes, err := netdefutils.GetCNIConfigFromSpec(netAttachDef.Spec.Config, netAttachDef.Name) + if err != nil { + return "", err + } + + netconfList := &cnitypes.NetConfList{} + if err := json.Unmarshal(confBytes, netconfList); err != nil { + return "", err + } + + if len(netconfList.Plugins) == 0 { + netconf := &cnitypes.NetConf{} + if err := json.Unmarshal(confBytes, netconf); err != nil { + return "", err + } + + netType = netconf.Type + } else { + netType = netconfList.Plugins[0].Type + } + + return netType, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (m *MultiNetworkReconciler) SetupWithManager(mgr ctrl.Manager) error { + // Ensure indexes are set up + err := setupIndexes(mgr) + if err != nil { + return fmt.Errorf("failed to set up indexes: %w", err) + } + + return ctrl.NewControllerManagedBy(mgr). + For(&multiv1beta1.MultiNetworkPolicy{}). + WithEventFilter(MultiNetworkPolicyPredicate). + Watches( + &corev1.Namespace{}, + // We will enqueue policies with selectors that match the namespace + handler.EnqueueRequestsFromMapFunc(namespaceEnqueue(m.Client)), + builder.WithPredicates(NamespacePredicate), + ). + Watches( + &corev1.Pod{}, + // We will enqueue policies with selectors that match the pod + handler.EnqueueRequestsFromMapFunc(podEnqueue(m.Client)), + builder.WithPredicates(PodPredicate), + ). + Complete(m) +} diff --git a/pkg/controller/multinetwork_controller_integration_test.go b/pkg/controller/multinetwork_controller_integration_test.go new file mode 100644 index 00000000..680c4159 --- /dev/null +++ b/pkg/controller/multinetwork_controller_integration_test.go @@ -0,0 +1,1064 @@ +package controller_test + +import ( + "context" + "time" + + "github.com/go-logr/logr" + multiv1beta1 "github.com/k8snetworkplumbingwg/multi-networkpolicy/pkg/apis/k8s.cni.cncf.io/v1beta1" + netdefv1 "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/apis/k8s.cni.cncf.io/v1" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + + "github.com/k8snetworkplumbingwg/multi-network-policy-nftables/pkg/datastore" + "github.com/k8snetworkplumbingwg/multi-network-policy-nftables/pkg/nftables" +) + +type SyncPolicyCall struct { + Policy *datastore.Policy + Operation string + Trigger string // What triggered this call +} + +var _ = Describe("MultiNetworkController Integration Tests", func() { + var ( + ctx context.Context + syncPolicyCalls []SyncPolicyCall + initialSyncPolicyCalls int + ) + + BeforeEach(func() { + ctx = context.Background() + syncPolicyCalls = []SyncPolicyCall{} + initialSyncPolicyCalls = 0 + + // Reset the mock NFT to track calls + mockNFT.SyncPolicyFunc = func(_ context.Context, policy *datastore.Policy, operation nftables.SyncOperation, _ logr.Logger) error { + syncPolicyCalls = append(syncPolicyCalls, SyncPolicyCall{ + Policy: policy, + Operation: string(operation), + Trigger: "unknown", // Will be updated by specific tests + }) + return nil + } + }) + + // Helper functions for creating test resources + createTestNamespace := func(name string, labels map[string]string) *corev1.Namespace { + ns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Labels: labels, + }, + } + Expect(k8sClient.Create(ctx, ns)).To(Succeed()) + DeferCleanup(func() { + k8sClient.Delete(ctx, ns) + }) + return ns + } + + createNetworkAttachmentDefinition := func(name, namespace, config string) *netdefv1.NetworkAttachmentDefinition { + nad := &netdefv1.NetworkAttachmentDefinition{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: netdefv1.NetworkAttachmentDefinitionSpec{ + Config: config, + }, + } + Expect(k8sClient.Create(ctx, nad)).To(Succeed()) + DeferCleanup(func() { + k8sClient.Delete(ctx, nad) + }) + return nad + } + + createMultiNetworkPolicy := func(name, namespace string, annotations map[string]string, spec multiv1beta1.MultiNetworkPolicySpec) *multiv1beta1.MultiNetworkPolicy { + policy := &multiv1beta1.MultiNetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Annotations: annotations, + }, + Spec: spec, + } + Expect(k8sClient.Create(ctx, policy)).To(Succeed()) + DeferCleanup(func() { + k8sClient.Delete(ctx, policy) + }) + return policy + } + + createPod := func(name, namespace string, labels map[string]string, annotations map[string]string) *corev1.Pod { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: labels, + Annotations: annotations, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "test-container", + Image: "nginx:latest", + }, + }, + }, + } + Expect(k8sClient.Create(ctx, pod)).To(Succeed()) + DeferCleanup(func() { + k8sClient.Delete(ctx, pod) + }) + return pod + } + + waitForPolicyInDatastore := func(namespace, name string) *datastore.Policy { + var storedPolicy *datastore.Policy + Eventually(func() bool { + storedPolicy = datastoreInstance.GetPolicy(types.NamespacedName{ + Namespace: namespace, + Name: name, + }) + return storedPolicy != nil + }, 10*time.Second, 100*time.Millisecond).Should(BeTrue()) + return storedPolicy + } + + // Helper to record the initial sync calls (when policy is first created) + recordInitialSyncCalls := func() { + initialSyncPolicyCalls = len(syncPolicyCalls) + } + + // Helper to wait for any reconciliation activity (sync or cleanup) + waitForReconciliationActivity := func(trigger string) { + Eventually(func() bool { + // Update the trigger for new calls + for i := initialSyncPolicyCalls; i < len(syncPolicyCalls); i++ { + syncPolicyCalls[i].Trigger = trigger + } + // Check if there was any reconciliation activity (either sync or cleanup) + return len(syncPolicyCalls) > initialSyncPolicyCalls + }, 10*time.Second, 100*time.Millisecond).Should(BeTrue()) + } + + // Helper to verify that reconciliation was triggered (either sync or cleanup) + verifyReconciliationWasTriggered := func(trigger string, policyName string) { + // For now, we'll just verify that there was some reconciliation activity + // The actual verification of what triggered it would require more sophisticated tracking + Expect(len(syncPolicyCalls)).To(BeNumerically(">", initialSyncPolicyCalls), + "Expected reconciliation activity to be triggered by %s for policy %s", trigger, policyName) + } + + Context("MultiNetworkPolicy CRUD Operations", func() { + It("should create a policy successfully", func() { + // Create test namespace + testNs := createTestNamespace("test-ns-create", map[string]string{"environment": "test"}) + + // Create network attachment definition + createNetworkAttachmentDefinition("macvlan-net", testNs.Name, `{ + "cniVersion": "0.3.1", + "name": "macvlan-net", + "type": "macvlan", + "master": "eth0", + "mode": "bridge" + }`) + + // Create policy + policy := createMultiNetworkPolicy("test-policy", testNs.Name, map[string]string{ + datastore.PolicyForAnnotation: "test-ns-create/macvlan-net", + }, multiv1beta1.MultiNetworkPolicySpec{ + PodSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "test-app", + }, + }, + PolicyTypes: []multiv1beta1.MultiPolicyType{ + multiv1beta1.PolicyTypeIngress, + multiv1beta1.PolicyTypeEgress, + }, + Ingress: []multiv1beta1.MultiNetworkPolicyIngressRule{ + { + From: []multiv1beta1.MultiNetworkPolicyPeer{ + { + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "allowed-app", + }, + }, + }, + }, + }, + }, + }) + + // Wait for policy to be stored in datastore + storedPolicy := waitForPolicyInDatastore(policy.Namespace, policy.Name) + Expect(storedPolicy).NotTo(BeNil()) + Expect(storedPolicy.Name).To(Equal(policy.Name)) + Expect(storedPolicy.Namespace).To(Equal(policy.Namespace)) + Expect(storedPolicy.Networks).To(ContainElement("test-ns-create/macvlan-net")) + }) + + It("should update a policy when spec changes", func() { + // Create test namespace + testNs := createTestNamespace("test-ns-update", map[string]string{"environment": "test"}) + + // Create network attachment definition + createNetworkAttachmentDefinition("macvlan-net", testNs.Name, `{ + "cniVersion": "0.3.1", + "name": "macvlan-net", + "type": "macvlan", + "master": "eth0", + "mode": "bridge" + }`) + + // Create initial policy + policy := createMultiNetworkPolicy("test-policy", testNs.Name, map[string]string{ + datastore.PolicyForAnnotation: "test-ns-update/macvlan-net", + }, multiv1beta1.MultiNetworkPolicySpec{ + PodSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "test-app", + }, + }, + PolicyTypes: []multiv1beta1.MultiPolicyType{ + multiv1beta1.PolicyTypeIngress, + }, + }) + + // Wait for initial policy to be stored + waitForPolicyInDatastore(policy.Namespace, policy.Name) + + // Update policy spec + policy.Spec.PodSelector.MatchLabels["version"] = "v2" + Expect(k8sClient.Update(ctx, policy)).To(Succeed()) + + // Wait for updated policy to be stored + Eventually(func() bool { + storedPolicy := datastoreInstance.GetPolicy(types.NamespacedName{ + Namespace: policy.Namespace, + Name: policy.Name, + }) + return storedPolicy != nil && storedPolicy.Spec.PodSelector.MatchLabels["version"] == "v2" + }, 10*time.Second, 100*time.Millisecond).Should(BeTrue()) + }) + + It("should update a policy when policy-for annotation changes", func() { + // Create test namespace + testNs := createTestNamespace("test-ns-annotation", map[string]string{"environment": "test"}) + + // Create network attachment definitions + createNetworkAttachmentDefinition("macvlan-net", testNs.Name, `{ + "cniVersion": "0.3.1", + "name": "macvlan-net", + "type": "macvlan", + "master": "eth0", + "mode": "bridge" + }`) + createNetworkAttachmentDefinition("ipvlan-net", testNs.Name, `{ + "cniVersion": "0.3.1", + "name": "ipvlan-net", + "type": "ipvlan", + "master": "eth0", + "mode": "l2" + }`) + + // Create policy with single network + policy := createMultiNetworkPolicy("test-policy", testNs.Name, map[string]string{ + datastore.PolicyForAnnotation: "test-ns-annotation/macvlan-net", + }, multiv1beta1.MultiNetworkPolicySpec{ + PodSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "test-app", + }, + }, + PolicyTypes: []multiv1beta1.MultiPolicyType{ + multiv1beta1.PolicyTypeIngress, + }, + }) + + // Wait for initial policy to be stored + waitForPolicyInDatastore(policy.Namespace, policy.Name) + + // Update policy annotation to include both networks + policy.Annotations[datastore.PolicyForAnnotation] = "test-ns-annotation/macvlan-net,test-ns-annotation/ipvlan-net" + Expect(k8sClient.Update(ctx, policy)).To(Succeed()) + + // Wait for updated policy to be stored with both networks + Eventually(func() bool { + storedPolicy := datastoreInstance.GetPolicy(types.NamespacedName{ + Namespace: policy.Namespace, + Name: policy.Name, + }) + return storedPolicy != nil && len(storedPolicy.Networks) == 2 + }, 10*time.Second, 100*time.Millisecond).Should(BeTrue()) + }) + + It("should delete a policy successfully", func() { + // Create test namespace + testNs := createTestNamespace("test-ns-delete", map[string]string{"environment": "test"}) + + // Create network attachment definition + createNetworkAttachmentDefinition("macvlan-net", testNs.Name, `{ + "cniVersion": "0.3.1", + "name": "macvlan-net", + "type": "macvlan", + "master": "eth0", + "mode": "bridge" + }`) + + // Create policy + policy := createMultiNetworkPolicy("test-policy", testNs.Name, map[string]string{ + datastore.PolicyForAnnotation: "test-ns-delete/macvlan-net", + }, multiv1beta1.MultiNetworkPolicySpec{ + PodSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "test-app", + }, + }, + PolicyTypes: []multiv1beta1.MultiPolicyType{ + multiv1beta1.PolicyTypeIngress, + }, + }) + + // Wait for policy to be stored + waitForPolicyInDatastore(policy.Namespace, policy.Name) + + // Delete policy + Expect(k8sClient.Delete(ctx, policy)).To(Succeed()) + + // Wait for policy to be removed from datastore + Eventually(func() bool { + storedPolicy := datastoreInstance.GetPolicy(types.NamespacedName{ + Namespace: policy.Namespace, + Name: policy.Name, + }) + return storedPolicy == nil + }, 10*time.Second, 100*time.Millisecond).Should(BeTrue()) + }) + + It("should handle policy with missing policy-for annotation", func() { + // Create test namespace + testNs := createTestNamespace("test-ns-missing-annotation", map[string]string{"environment": "test"}) + + // Create policy without policy-for annotation + policy := createMultiNetworkPolicy("test-policy", testNs.Name, map[string]string{}, multiv1beta1.MultiNetworkPolicySpec{ + PodSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "test-app", + }, + }, + PolicyTypes: []multiv1beta1.MultiPolicyType{ + multiv1beta1.PolicyTypeIngress, + }, + }) + + // Wait and verify policy is not stored in datastore + Eventually(func() bool { + storedPolicy := datastoreInstance.GetPolicy(types.NamespacedName{ + Namespace: policy.Namespace, + Name: policy.Name, + }) + return storedPolicy == nil + }, 5*time.Second, 100*time.Millisecond).Should(BeTrue()) + }) + + It("should handle policy with invalid network references", func() { + // Create test namespace + testNs := createTestNamespace("test-ns-invalid-networks", map[string]string{"environment": "test"}) + + // Create policy with invalid network reference + policy := createMultiNetworkPolicy("test-policy", testNs.Name, map[string]string{ + datastore.PolicyForAnnotation: "nonexistent-net", + }, multiv1beta1.MultiNetworkPolicySpec{ + PodSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "test-app", + }, + }, + PolicyTypes: []multiv1beta1.MultiPolicyType{ + multiv1beta1.PolicyTypeIngress, + }, + }) + + // Wait and verify policy is not stored in datastore + Eventually(func() bool { + storedPolicy := datastoreInstance.GetPolicy(types.NamespacedName{ + Namespace: policy.Namespace, + Name: policy.Name, + }) + return storedPolicy == nil + }, 5*time.Second, 100*time.Millisecond).Should(BeTrue()) + }) + + It("should handle policy with unsupported network types", func() { + // Create test namespace + testNs := createTestNamespace("test-ns-unsupported", map[string]string{"environment": "test"}) + + // Create unsupported network attachment definition + createNetworkAttachmentDefinition("unsupported-net", testNs.Name, `{ + "cniVersion": "0.3.1", + "name": "unsupported-net", + "type": "unsupported", + "master": "eth0" + }`) + + // Create policy with unsupported network + policy := createMultiNetworkPolicy("test-policy", testNs.Name, map[string]string{ + datastore.PolicyForAnnotation: "test-ns-unsupported/unsupported-net", + }, multiv1beta1.MultiNetworkPolicySpec{ + PodSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "test-app", + }, + }, + PolicyTypes: []multiv1beta1.MultiPolicyType{ + multiv1beta1.PolicyTypeIngress, + }, + }) + + // Wait and verify policy is not stored in datastore + Eventually(func() bool { + storedPolicy := datastoreInstance.GetPolicy(types.NamespacedName{ + Namespace: policy.Namespace, + Name: policy.Name, + }) + return storedPolicy == nil + }, 5*time.Second, 100*time.Millisecond).Should(BeTrue()) + }) + }) + + Context("Namespace Change Triggers", func() { + It("should reconcile policies when namespace labels change", func() { + // Create test namespaces + testNs1 := createTestNamespace("test-ns-labels-1", map[string]string{"environment": "test"}) + _ = createTestNamespace("test-ns-labels-2", map[string]string{"environment": "production"}) + + // Create network attachment definition + createNetworkAttachmentDefinition("macvlan-net", testNs1.Name, `{ + "cniVersion": "0.3.1", + "name": "macvlan-net", + "type": "macvlan", + "master": "eth0", + "mode": "bridge" + }`) + + // Create policy manually (without DeferCleanup to avoid timing issues) + policy := &multiv1beta1.MultiNetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-policy", + Namespace: testNs1.Name, + Annotations: map[string]string{ + datastore.PolicyForAnnotation: "test-ns-labels-1/macvlan-net", + }, + }, + Spec: multiv1beta1.MultiNetworkPolicySpec{ + PodSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "test-app", + }, + }, + PolicyTypes: []multiv1beta1.MultiPolicyType{ + multiv1beta1.PolicyTypeIngress, + }, + Ingress: []multiv1beta1.MultiNetworkPolicyIngressRule{ + { + From: []multiv1beta1.MultiNetworkPolicyPeer{ + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "environment": "test", + }, + }, + }, + }, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, policy)).To(Succeed()) + + // Manually clean up the policy at the end + DeferCleanup(func() { + k8sClient.Delete(ctx, policy) + }) + + // Wait for policy to be stored and record initial sync calls + waitForPolicyInDatastore(policy.Namespace, policy.Name) + recordInitialSyncCalls() + + // Update namespace labels - this should trigger reconciliation + testNs1.Labels["environment"] = "production" + Expect(k8sClient.Update(ctx, testNs1)).To(Succeed()) + + // Wait for reconciliation activity due to namespace change + waitForReconciliationActivity("namespace-change") + + // Verify that reconciliation was triggered by namespace change + verifyReconciliationWasTriggered("namespace-change", policy.Name) + }) + + It("should reconcile policies when new namespace is created", func() { + // Create test namespace + testNs1 := createTestNamespace("test-ns-new-1", map[string]string{"environment": "test"}) + + // Create network attachment definition + createNetworkAttachmentDefinition("macvlan-net", testNs1.Name, `{ + "cniVersion": "0.3.1", + "name": "macvlan-net", + "type": "macvlan", + "master": "eth0", + "mode": "bridge" + }`) + + // Create policy manually (without DeferCleanup to avoid timing issues) + policy := &multiv1beta1.MultiNetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-policy", + Namespace: testNs1.Name, + Annotations: map[string]string{ + datastore.PolicyForAnnotation: "test-ns-new-1/macvlan-net", + }, + }, + Spec: multiv1beta1.MultiNetworkPolicySpec{ + PodSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "test-app", + }, + }, + PolicyTypes: []multiv1beta1.MultiPolicyType{ + multiv1beta1.PolicyTypeIngress, + }, + Ingress: []multiv1beta1.MultiNetworkPolicyIngressRule{ + { + From: []multiv1beta1.MultiNetworkPolicyPeer{ + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "environment": "test", + }, + }, + }, + }, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, policy)).To(Succeed()) + + // Manually clean up the policy at the end + DeferCleanup(func() { + k8sClient.Delete(ctx, policy) + }) + + // Wait for policy to be stored and record initial sync calls + waitForPolicyInDatastore(policy.Namespace, policy.Name) + recordInitialSyncCalls() + + // Create new namespace with matching labels - this should trigger reconciliation + _ = createTestNamespace("test-ns-new-2", map[string]string{"environment": "test"}) + + // Wait for reconciliation activity due to namespace creation + waitForReconciliationActivity("namespace-creation") + + // Verify that reconciliation was triggered by namespace creation + verifyReconciliationWasTriggered("namespace-creation", policy.Name) + }) + }) + + Context("Pod Change Triggers", func() { + It("should reconcile policies when pod labels change", func() { + // Create test namespace + testNs := createTestNamespace("test-ns-pod-labels", map[string]string{"environment": "test"}) + + // Create network attachment definition + createNetworkAttachmentDefinition("macvlan-net", testNs.Name, `{ + "cniVersion": "0.3.1", + "name": "macvlan-net", + "type": "macvlan", + "master": "eth0", + "mode": "bridge" + }`) + + // Create policy manually (without DeferCleanup to avoid timing issues) + policy := &multiv1beta1.MultiNetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-policy", + Namespace: testNs.Name, + Annotations: map[string]string{ + datastore.PolicyForAnnotation: "test-ns-pod-labels/macvlan-net", + }, + }, + Spec: multiv1beta1.MultiNetworkPolicySpec{ + PodSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "test-app", + }, + }, + PolicyTypes: []multiv1beta1.MultiPolicyType{ + multiv1beta1.PolicyTypeIngress, + }, + }, + } + Expect(k8sClient.Create(ctx, policy)).To(Succeed()) + + // Manually clean up the policy at the end + DeferCleanup(func() { + k8sClient.Delete(ctx, policy) + }) + + // Wait for policy to be stored and record initial sync calls + waitForPolicyInDatastore(policy.Namespace, policy.Name) + recordInitialSyncCalls() + + // Create pod with matching labels + pod := createPod("test-pod", testNs.Name, map[string]string{ + "app": "test-app", + }, map[string]string{ + "k8s.v1.cni.cncf.io/networks": "test-ns-pod-labels/macvlan-net", + }) + + // Set pod status to Running to ensure it matches the policy selector + pod.Status.Phase = corev1.PodRunning + Expect(k8sClient.Status().Update(ctx, pod)).To(Succeed()) + + // Update pod labels - this should trigger reconciliation + pod.Labels["version"] = "v2" + Expect(k8sClient.Update(ctx, pod)).To(Succeed()) + + // Wait for reconciliation activity due to pod change + waitForReconciliationActivity("pod-change") + + // Verify that reconciliation was triggered by pod change + verifyReconciliationWasTriggered("pod-change", policy.Name) + }) + + It("should reconcile policies when pod becomes ineligible", func() { + // Create test namespace + testNs := createTestNamespace("test-ns-pod-ineligible", map[string]string{"environment": "test"}) + + // Create network attachment definition + createNetworkAttachmentDefinition("macvlan-net", testNs.Name, `{ + "cniVersion": "0.3.1", + "name": "macvlan-net", + "type": "macvlan", + "master": "eth0", + "mode": "bridge" + }`) + + // Create policy + policy := createMultiNetworkPolicy("test-policy", testNs.Name, map[string]string{ + datastore.PolicyForAnnotation: "test-ns-pod-ineligible/macvlan-net", + }, multiv1beta1.MultiNetworkPolicySpec{ + PodSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "test-app", + }, + }, + PolicyTypes: []multiv1beta1.MultiPolicyType{ + multiv1beta1.PolicyTypeIngress, + }, + }) + + // Wait for policy to be stored and record initial sync calls + waitForPolicyInDatastore(policy.Namespace, policy.Name) + recordInitialSyncCalls() + + // Create pod with matching labels + pod := createPod("test-pod", testNs.Name, map[string]string{ + "app": "test-app", + }, map[string]string{ + "k8s.v1.cni.cncf.io/networks": "test-ns-pod-ineligible/macvlan-net", + }) + + // Set pod status to Running to ensure it matches the policy selector + pod.Status.Phase = corev1.PodRunning + Expect(k8sClient.Status().Update(ctx, pod)).To(Succeed()) + + // Remove network annotation to make pod ineligible - this should trigger reconciliation + delete(pod.Annotations, "k8s.v1.cni.cncf.io/networks") + Expect(k8sClient.Update(ctx, pod)).To(Succeed()) + + // Wait for reconciliation activity due to pod becoming ineligible + waitForReconciliationActivity("pod-ineligible") + + // Verify that reconciliation was triggered by pod becoming ineligible + verifyReconciliationWasTriggered("pod-ineligible", policy.Name) + }) + + It("should reconcile policies when pod is deleted", func() { + // Create test namespace + testNs := createTestNamespace("test-ns-pod-delete", map[string]string{"environment": "test"}) + + // Create network attachment definition + createNetworkAttachmentDefinition("macvlan-net", testNs.Name, `{ + "cniVersion": "0.3.1", + "name": "macvlan-net", + "type": "macvlan", + "master": "eth0", + "mode": "bridge" + }`) + + // Create policy + policy := createMultiNetworkPolicy("test-policy", testNs.Name, map[string]string{ + datastore.PolicyForAnnotation: "test-ns-pod-delete/macvlan-net", + }, multiv1beta1.MultiNetworkPolicySpec{ + PodSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "test-app", + }, + }, + PolicyTypes: []multiv1beta1.MultiPolicyType{ + multiv1beta1.PolicyTypeIngress, + }, + }) + + // Wait for policy to be stored and record initial sync calls + waitForPolicyInDatastore(policy.Namespace, policy.Name) + recordInitialSyncCalls() + + // Create pod with matching labels + pod := createPod("test-pod", testNs.Name, map[string]string{ + "app": "test-app", + }, map[string]string{ + "k8s.v1.cni.cncf.io/networks": "test-ns-pod-delete/macvlan-net", + }) + + // Set pod status to Running to ensure it matches the policy selector + pod.Status.Phase = corev1.PodRunning + Expect(k8sClient.Status().Update(ctx, pod)).To(Succeed()) + + // Delete pod - this should trigger reconciliation + Expect(k8sClient.Delete(ctx, pod)).To(Succeed()) + + // Wait for reconciliation activity due to pod deletion + waitForReconciliationActivity("pod-deletion") + + // Verify that reconciliation was triggered by pod deletion + verifyReconciliationWasTriggered("pod-deletion", policy.Name) + }) + + It("should not reconcile policies for non-eligible pods", func() { + // Create test namespace + testNs := createTestNamespace("test-ns-pod-non-eligible", map[string]string{"environment": "test"}) + + // Create network attachment definition + createNetworkAttachmentDefinition("macvlan-net", testNs.Name, `{ + "cniVersion": "0.3.1", + "name": "macvlan-net", + "type": "macvlan", + "master": "eth0", + "mode": "bridge" + }`) + + // Create policy + policy := createMultiNetworkPolicy("test-policy", testNs.Name, map[string]string{ + datastore.PolicyForAnnotation: "test-ns-pod-non-eligible/macvlan-net", + }, multiv1beta1.MultiNetworkPolicySpec{ + PodSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "test-app", + }, + }, + PolicyTypes: []multiv1beta1.MultiPolicyType{ + multiv1beta1.PolicyTypeIngress, + }, + }) + + // Wait for policy to be stored and record initial sync calls + waitForPolicyInDatastore(policy.Namespace, policy.Name) + recordInitialSyncCalls() + + // Create pod without network annotation (non-eligible) + pod := createPod("test-pod", testNs.Name, map[string]string{ + "app": "test-app", + }, map[string]string{}) + + // Update pod labels - this should NOT trigger reconciliation for non-eligible pods + pod.Labels["version"] = "v2" + Expect(k8sClient.Update(ctx, pod)).To(Succeed()) + + // Wait a bit to ensure no additional sync calls are made + time.Sleep(2 * time.Second) + + // Verify that no additional sync calls were made + Expect(syncPolicyCalls).To(HaveLen(initialSyncPolicyCalls), "Expected no additional sync calls for non-eligible pods") + }) + }) + + Context("Complex Policy Scenarios", func() { + It("should handle policy with multiple ingress rules", func() { + // Create test namespace + testNs := createTestNamespace("test-ns-complex-ingress", map[string]string{"environment": "test"}) + + // Create network attachment definition + createNetworkAttachmentDefinition("macvlan-net", testNs.Name, `{ + "cniVersion": "0.3.1", + "name": "macvlan-net", + "type": "macvlan", + "master": "eth0", + "mode": "bridge" + }`) + + // Create policy with multiple ingress rules + policy := createMultiNetworkPolicy("test-policy", testNs.Name, map[string]string{ + datastore.PolicyForAnnotation: "test-ns-complex-ingress/macvlan-net", + }, multiv1beta1.MultiNetworkPolicySpec{ + PodSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "test-app", + }, + }, + PolicyTypes: []multiv1beta1.MultiPolicyType{ + multiv1beta1.PolicyTypeIngress, + }, + Ingress: []multiv1beta1.MultiNetworkPolicyIngressRule{ + { + From: []multiv1beta1.MultiNetworkPolicyPeer{ + { + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "allowed-app", + }, + }, + }, + }, + Ports: []multiv1beta1.MultiNetworkPolicyPort{ + { + Protocol: &[]corev1.Protocol{corev1.ProtocolTCP}[0], + Port: &intstr.IntOrString{Type: intstr.Int, IntVal: 80}, + }, + }, + }, + { + From: []multiv1beta1.MultiNetworkPolicyPeer{ + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "tier": "database", + }, + }, + }, + }, + Ports: []multiv1beta1.MultiNetworkPolicyPort{ + { + Protocol: &[]corev1.Protocol{corev1.ProtocolTCP}[0], + Port: &intstr.IntOrString{Type: intstr.Int, IntVal: 5432}, + }, + }, + }, + }, + }) + + // Wait for policy to be stored + storedPolicy := waitForPolicyInDatastore(policy.Namespace, policy.Name) + Expect(storedPolicy).NotTo(BeNil()) + Expect(storedPolicy.Spec.Ingress).To(HaveLen(2)) + }) + + It("should handle policy with egress rules", func() { + // Create test namespace + testNs := createTestNamespace("test-ns-complex-egress", map[string]string{"environment": "test"}) + + // Create network attachment definition + createNetworkAttachmentDefinition("macvlan-net", testNs.Name, `{ + "cniVersion": "0.3.1", + "name": "macvlan-net", + "type": "macvlan", + "master": "eth0", + "mode": "bridge" + }`) + + // Create policy with egress rules + policy := createMultiNetworkPolicy("test-policy", testNs.Name, map[string]string{ + datastore.PolicyForAnnotation: "test-ns-complex-egress/macvlan-net", + }, multiv1beta1.MultiNetworkPolicySpec{ + PodSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "test-app", + }, + }, + PolicyTypes: []multiv1beta1.MultiPolicyType{ + multiv1beta1.PolicyTypeEgress, + }, + Egress: []multiv1beta1.MultiNetworkPolicyEgressRule{ + { + To: []multiv1beta1.MultiNetworkPolicyPeer{ + { + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "database", + }, + }, + }, + }, + Ports: []multiv1beta1.MultiNetworkPolicyPort{ + { + Protocol: &[]corev1.Protocol{corev1.ProtocolTCP}[0], + Port: &intstr.IntOrString{Type: intstr.Int, IntVal: 5432}, + }, + }, + }, + }, + }) + + // Wait for policy to be stored + storedPolicy := waitForPolicyInDatastore(policy.Namespace, policy.Name) + Expect(storedPolicy).NotTo(BeNil()) + Expect(storedPolicy.Spec.Egress).To(HaveLen(1)) + }) + + It("should handle policy with cross-namespace network reference", func() { + // Create test namespaces + testNs1 := createTestNamespace("test-ns-cross-1", map[string]string{"environment": "test"}) + testNs2 := createTestNamespace("test-ns-cross-2", map[string]string{"environment": "test"}) + + // Create network attachment definition in first namespace + createNetworkAttachmentDefinition("macvlan-net", testNs1.Name, `{ + "cniVersion": "0.3.1", + "name": "macvlan-net", + "type": "macvlan", + "master": "eth0", + "mode": "bridge" + }`) + + // Create policy in second namespace referencing first namespace's network + policy := createMultiNetworkPolicy("test-policy", testNs2.Name, map[string]string{ + datastore.PolicyForAnnotation: "test-ns-cross-1/macvlan-net", + }, multiv1beta1.MultiNetworkPolicySpec{ + PodSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "test-app-2", + }, + }, + PolicyTypes: []multiv1beta1.MultiPolicyType{ + multiv1beta1.PolicyTypeIngress, + }, + }) + + // Wait for policy to be stored + storedPolicy := waitForPolicyInDatastore(policy.Namespace, policy.Name) + Expect(storedPolicy).NotTo(BeNil()) + Expect(storedPolicy.Networks).To(ContainElement("test-ns-cross-1/macvlan-net")) + }) + + It("should handle pod with host network", func() { + // Create test namespace + testNs := createTestNamespace("test-ns-host-network", map[string]string{"environment": "test"}) + + // Create network attachment definition + createNetworkAttachmentDefinition("macvlan-net", testNs.Name, `{ + "cniVersion": "0.3.1", + "name": "macvlan-net", + "type": "macvlan", + "master": "eth0", + "mode": "bridge" + }`) + + // Create policy + policy := createMultiNetworkPolicy("test-policy", testNs.Name, map[string]string{ + datastore.PolicyForAnnotation: "test-ns-host-network/macvlan-net", + }, multiv1beta1.MultiNetworkPolicySpec{ + PodSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "test-app", + }, + }, + PolicyTypes: []multiv1beta1.MultiPolicyType{ + multiv1beta1.PolicyTypeIngress, + }, + }) + + // Wait for policy to be stored + waitForPolicyInDatastore(policy.Namespace, policy.Name) + + // Create pod with host network + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod", + Namespace: testNs.Name, + Labels: map[string]string{ + "app": "test-app", + }, + Annotations: map[string]string{ + "k8s.v1.cni.cncf.io/networks": "test-ns-host-network/macvlan-net", + }, + }, + Spec: corev1.PodSpec{ + HostNetwork: true, + Containers: []corev1.Container{ + { + Name: "test-container", + Image: "nginx:latest", + }, + }, + }, + } + Expect(k8sClient.Create(ctx, pod)).To(Succeed()) + DeferCleanup(func() { + k8sClient.Delete(ctx, pod) + }) + + // Wait for reconciliation to complete + Eventually(func() bool { + storedPolicy := datastoreInstance.GetPolicy(types.NamespacedName{ + Namespace: policy.Namespace, + Name: policy.Name, + }) + return storedPolicy != nil + }, 10*time.Second, 100*time.Millisecond).Should(BeTrue()) + }) + + It("should handle pod without network annotation", func() { + // Create test namespace + testNs := createTestNamespace("test-ns-no-annotation", map[string]string{"environment": "test"}) + + // Create network attachment definition + createNetworkAttachmentDefinition("macvlan-net", testNs.Name, `{ + "cniVersion": "0.3.1", + "name": "macvlan-net", + "type": "macvlan", + "master": "eth0", + "mode": "bridge" + }`) + + // Create policy + policy := createMultiNetworkPolicy("test-policy", testNs.Name, map[string]string{ + datastore.PolicyForAnnotation: "test-ns-no-annotation/macvlan-net", + }, multiv1beta1.MultiNetworkPolicySpec{ + PodSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "test-app", + }, + }, + PolicyTypes: []multiv1beta1.MultiPolicyType{ + multiv1beta1.PolicyTypeIngress, + }, + }) + + // Wait for policy to be stored + waitForPolicyInDatastore(policy.Namespace, policy.Name) + + // Create pod without network annotation + _ = createPod("test-pod", testNs.Name, map[string]string{ + "app": "test-app", + }, map[string]string{}) + + // Wait for reconciliation to complete + Eventually(func() bool { + storedPolicy := datastoreInstance.GetPolicy(types.NamespacedName{ + Namespace: policy.Namespace, + Name: policy.Name, + }) + return storedPolicy != nil + }, 10*time.Second, 100*time.Millisecond).Should(BeTrue()) + }) + }) +}) diff --git a/pkg/controller/multinetwork_controller_test.go b/pkg/controller/multinetwork_controller_test.go new file mode 100644 index 00000000..829044e3 --- /dev/null +++ b/pkg/controller/multinetwork_controller_test.go @@ -0,0 +1,2067 @@ +package controller + +import ( + "context" + "strings" + + "github.com/go-logr/logr" + multiv1beta1 "github.com/k8snetworkplumbingwg/multi-networkpolicy/pkg/apis/k8s.cni.cncf.io/v1beta1" + netdefv1 "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/apis/k8s.cni.cncf.io/v1" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +var _ = Describe("isPolicyAffectedByNamespace Unit Tests", func() { + logger := log.Log.WithName("test") + + Describe("Input validation", func() { + It("should return false when policy is nil", func() { + namespace := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-namespace", + Labels: map[string]string{ + "environment": "production", + }, + }, + } + + result := isPolicyAffectedByNamespace(nil, namespace, logger) + Expect(result).To(BeFalse()) + }) + + It("should return false when namespace is nil", func() { + policy := &multiv1beta1.MultiNetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-policy", + Namespace: "default", + }, + } + + result := isPolicyAffectedByNamespace(policy, nil, logger) + Expect(result).To(BeFalse()) + }) + + It("should return false when both policy and namespace are nil", func() { + result := isPolicyAffectedByNamespace(nil, nil, logger) + Expect(result).To(BeFalse()) + }) + }) + + Describe("Ingress namespace selectors", func() { + var namespace *corev1.Namespace + + BeforeEach(func() { + namespace = &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-namespace", + Labels: map[string]string{ + "environment": "production", + "tier": "frontend", + }, + }, + } + }) + + It("should return true when ingress namespace selector matches", func() { + policy := &multiv1beta1.MultiNetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-policy", + Namespace: "default", + }, + Spec: multiv1beta1.MultiNetworkPolicySpec{ + Ingress: []multiv1beta1.MultiNetworkPolicyIngressRule{ + { + From: []multiv1beta1.MultiNetworkPolicyPeer{ + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "environment": "production", + }, + }, + }, + }, + }, + }, + }, + } + + result := isPolicyAffectedByNamespace(policy, namespace, logger) + Expect(result).To(BeTrue()) + }) + + It("should return false when ingress namespace selector does not match", func() { + policy := &multiv1beta1.MultiNetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-policy", + Namespace: "default", + }, + Spec: multiv1beta1.MultiNetworkPolicySpec{ + Ingress: []multiv1beta1.MultiNetworkPolicyIngressRule{ + { + From: []multiv1beta1.MultiNetworkPolicyPeer{ + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "environment": "staging", + }, + }, + }, + }, + }, + }, + }, + } + + result := isPolicyAffectedByNamespace(policy, namespace, logger) + Expect(result).To(BeFalse()) + }) + + It("should return false when ingress peer has IPBlock (skips namespace selector)", func() { + policy := &multiv1beta1.MultiNetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-policy", + Namespace: "default", + }, + Spec: multiv1beta1.MultiNetworkPolicySpec{ + Ingress: []multiv1beta1.MultiNetworkPolicyIngressRule{ + { + From: []multiv1beta1.MultiNetworkPolicyPeer{ + { + IPBlock: &multiv1beta1.IPBlock{ + CIDR: "10.0.0.0/8", + }, + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "environment": "production", // This should be ignored + }, + }, + }, + }, + }, + }, + }, + } + + result := isPolicyAffectedByNamespace(policy, namespace, logger) + Expect(result).To(BeFalse()) + }) + + It("should return false when ingress peer has no namespace selector", func() { + policy := &multiv1beta1.MultiNetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-policy", + Namespace: "default", + }, + Spec: multiv1beta1.MultiNetworkPolicySpec{ + Ingress: []multiv1beta1.MultiNetworkPolicyIngressRule{ + { + From: []multiv1beta1.MultiNetworkPolicyPeer{ + { + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "web", + }, + }, + }, + }, + }, + }, + }, + } + + result := isPolicyAffectedByNamespace(policy, namespace, logger) + Expect(result).To(BeFalse()) + }) + + It("should return true when one of multiple ingress peers matches", func() { + policy := &multiv1beta1.MultiNetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-policy", + Namespace: "default", + }, + Spec: multiv1beta1.MultiNetworkPolicySpec{ + Ingress: []multiv1beta1.MultiNetworkPolicyIngressRule{ + { + From: []multiv1beta1.MultiNetworkPolicyPeer{ + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "environment": "staging", // Doesn't match + }, + }, + }, + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "tier": "frontend", // Matches + }, + }, + }, + }, + }, + }, + }, + } + + result := isPolicyAffectedByNamespace(policy, namespace, logger) + Expect(result).To(BeTrue()) + }) + + It("should return true when one of multiple ingress rules matches", func() { + policy := &multiv1beta1.MultiNetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-policy", + Namespace: "default", + }, + Spec: multiv1beta1.MultiNetworkPolicySpec{ + Ingress: []multiv1beta1.MultiNetworkPolicyIngressRule{ + { + From: []multiv1beta1.MultiNetworkPolicyPeer{ + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "environment": "staging", // Doesn't match + }, + }, + }, + }, + }, + { + From: []multiv1beta1.MultiNetworkPolicyPeer{ + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "tier": "frontend", // Matches + }, + }, + }, + }, + }, + }, + }, + } + + result := isPolicyAffectedByNamespace(policy, namespace, logger) + Expect(result).To(BeTrue()) + }) + }) + + Describe("Egress namespace selectors", func() { + var namespace *corev1.Namespace + + BeforeEach(func() { + namespace = &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-namespace", + Labels: map[string]string{ + "database": "mysql", + "zone": "secure", + }, + }, + } + }) + + It("should return true when egress namespace selector matches", func() { + policy := &multiv1beta1.MultiNetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-policy", + Namespace: "default", + }, + Spec: multiv1beta1.MultiNetworkPolicySpec{ + Egress: []multiv1beta1.MultiNetworkPolicyEgressRule{ + { + To: []multiv1beta1.MultiNetworkPolicyPeer{ + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "database": "mysql", + }, + }, + }, + }, + }, + }, + }, + } + + result := isPolicyAffectedByNamespace(policy, namespace, logger) + Expect(result).To(BeTrue()) + }) + + It("should return false when egress namespace selector does not match", func() { + policy := &multiv1beta1.MultiNetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-policy", + Namespace: "default", + }, + Spec: multiv1beta1.MultiNetworkPolicySpec{ + Egress: []multiv1beta1.MultiNetworkPolicyEgressRule{ + { + To: []multiv1beta1.MultiNetworkPolicyPeer{ + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "database": "postgres", + }, + }, + }, + }, + }, + }, + }, + } + + result := isPolicyAffectedByNamespace(policy, namespace, logger) + Expect(result).To(BeFalse()) + }) + + It("should return false when egress peer has IPBlock (skips namespace selector)", func() { + policy := &multiv1beta1.MultiNetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-policy", + Namespace: "default", + }, + Spec: multiv1beta1.MultiNetworkPolicySpec{ + Egress: []multiv1beta1.MultiNetworkPolicyEgressRule{ + { + To: []multiv1beta1.MultiNetworkPolicyPeer{ + { + IPBlock: &multiv1beta1.IPBlock{ + CIDR: "192.168.0.0/16", + }, + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "database": "mysql", // This should be ignored + }, + }, + }, + }, + }, + }, + }, + } + + result := isPolicyAffectedByNamespace(policy, namespace, logger) + Expect(result).To(BeFalse()) + }) + + It("should return false when egress peer has no namespace selector", func() { + policy := &multiv1beta1.MultiNetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-policy", + Namespace: "default", + }, + Spec: multiv1beta1.MultiNetworkPolicySpec{ + Egress: []multiv1beta1.MultiNetworkPolicyEgressRule{ + { + To: []multiv1beta1.MultiNetworkPolicyPeer{ + { + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "database", + }, + }, + }, + }, + }, + }, + }, + } + + result := isPolicyAffectedByNamespace(policy, namespace, logger) + Expect(result).To(BeFalse()) + }) + + It("should return true when one of multiple egress peers matches", func() { + policy := &multiv1beta1.MultiNetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-policy", + Namespace: "default", + }, + Spec: multiv1beta1.MultiNetworkPolicySpec{ + Egress: []multiv1beta1.MultiNetworkPolicyEgressRule{ + { + To: []multiv1beta1.MultiNetworkPolicyPeer{ + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "database": "postgres", // Doesn't match + }, + }, + }, + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "zone": "secure", // Matches + }, + }, + }, + }, + }, + }, + }, + } + + result := isPolicyAffectedByNamespace(policy, namespace, logger) + Expect(result).To(BeTrue()) + }) + + It("should return true when one of multiple egress rules matches", func() { + policy := &multiv1beta1.MultiNetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-policy", + Namespace: "default", + }, + Spec: multiv1beta1.MultiNetworkPolicySpec{ + Egress: []multiv1beta1.MultiNetworkPolicyEgressRule{ + { + To: []multiv1beta1.MultiNetworkPolicyPeer{ + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "database": "postgres", // Doesn't match + }, + }, + }, + }, + }, + { + To: []multiv1beta1.MultiNetworkPolicyPeer{ + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "zone": "secure", // Matches + }, + }, + }, + }, + }, + }, + }, + } + + result := isPolicyAffectedByNamespace(policy, namespace, logger) + Expect(result).To(BeTrue()) + }) + }) + + Describe("Combined ingress and egress selectors", func() { + var namespace *corev1.Namespace + + BeforeEach(func() { + namespace = &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-namespace", + Labels: map[string]string{ + "environment": "production", + "tier": "backend", + }, + }, + } + }) + + It("should return true when ingress selector matches (egress doesn't match)", func() { + policy := &multiv1beta1.MultiNetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-policy", + Namespace: "default", + }, + Spec: multiv1beta1.MultiNetworkPolicySpec{ + Ingress: []multiv1beta1.MultiNetworkPolicyIngressRule{ + { + From: []multiv1beta1.MultiNetworkPolicyPeer{ + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "environment": "production", // Matches + }, + }, + }, + }, + }, + }, + Egress: []multiv1beta1.MultiNetworkPolicyEgressRule{ + { + To: []multiv1beta1.MultiNetworkPolicyPeer{ + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "tier": "database", // Doesn't match + }, + }, + }, + }, + }, + }, + }, + } + + result := isPolicyAffectedByNamespace(policy, namespace, logger) + Expect(result).To(BeTrue()) + }) + + It("should return true when egress selector matches (ingress doesn't match)", func() { + policy := &multiv1beta1.MultiNetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-policy", + Namespace: "default", + }, + Spec: multiv1beta1.MultiNetworkPolicySpec{ + Ingress: []multiv1beta1.MultiNetworkPolicyIngressRule{ + { + From: []multiv1beta1.MultiNetworkPolicyPeer{ + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "environment": "staging", // Doesn't match + }, + }, + }, + }, + }, + }, + Egress: []multiv1beta1.MultiNetworkPolicyEgressRule{ + { + To: []multiv1beta1.MultiNetworkPolicyPeer{ + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "tier": "backend", // Matches + }, + }, + }, + }, + }, + }, + }, + } + + result := isPolicyAffectedByNamespace(policy, namespace, logger) + Expect(result).To(BeTrue()) + }) + + It("should return false when neither ingress nor egress selectors match", func() { + policy := &multiv1beta1.MultiNetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-policy", + Namespace: "default", + }, + Spec: multiv1beta1.MultiNetworkPolicySpec{ + Ingress: []multiv1beta1.MultiNetworkPolicyIngressRule{ + { + From: []multiv1beta1.MultiNetworkPolicyPeer{ + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "environment": "staging", // Doesn't match + }, + }, + }, + }, + }, + }, + Egress: []multiv1beta1.MultiNetworkPolicyEgressRule{ + { + To: []multiv1beta1.MultiNetworkPolicyPeer{ + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "tier": "database", // Doesn't match + }, + }, + }, + }, + }, + }, + }, + } + + result := isPolicyAffectedByNamespace(policy, namespace, logger) + Expect(result).To(BeFalse()) + }) + }) + + Describe("Edge cases", func() { + var namespace *corev1.Namespace + + BeforeEach(func() { + namespace = &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-namespace", + Labels: map[string]string{ + "app": "web", + }, + }, + } + }) + + It("should return false when policy has no ingress or egress rules", func() { + policy := &multiv1beta1.MultiNetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-policy", + Namespace: "default", + }, + Spec: multiv1beta1.MultiNetworkPolicySpec{ + // No Ingress or Egress rules + }, + } + + result := isPolicyAffectedByNamespace(policy, namespace, logger) + Expect(result).To(BeFalse()) + }) + + It("should return false when ingress rule has no From peers", func() { + policy := &multiv1beta1.MultiNetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-policy", + Namespace: "default", + }, + Spec: multiv1beta1.MultiNetworkPolicySpec{ + Ingress: []multiv1beta1.MultiNetworkPolicyIngressRule{ + { + // No From peers + }, + }, + }, + } + + result := isPolicyAffectedByNamespace(policy, namespace, logger) + Expect(result).To(BeFalse()) + }) + + It("should return false when egress rule has no To peers", func() { + policy := &multiv1beta1.MultiNetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-policy", + Namespace: "default", + }, + Spec: multiv1beta1.MultiNetworkPolicySpec{ + Egress: []multiv1beta1.MultiNetworkPolicyEgressRule{ + { + // No To peers + }, + }, + }, + } + + result := isPolicyAffectedByNamespace(policy, namespace, logger) + Expect(result).To(BeFalse()) + }) + + It("should return false when namespace has no labels", func() { + namespaceNoLabels := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-namespace", + // No labels + }, + } + + policy := &multiv1beta1.MultiNetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-policy", + Namespace: "default", + }, + Spec: multiv1beta1.MultiNetworkPolicySpec{ + Ingress: []multiv1beta1.MultiNetworkPolicyIngressRule{ + { + From: []multiv1beta1.MultiNetworkPolicyPeer{ + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "web", + }, + }, + }, + }, + }, + }, + }, + } + + result := isPolicyAffectedByNamespace(policy, namespaceNoLabels, logger) + Expect(result).To(BeFalse()) + }) + + It("should return true when namespace selector is empty (matches all)", func() { + testPolicy := &multiv1beta1.MultiNetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-policy", + Namespace: "default", + }, + Spec: multiv1beta1.MultiNetworkPolicySpec{ + Ingress: []multiv1beta1.MultiNetworkPolicyIngressRule{ + { + From: []multiv1beta1.MultiNetworkPolicyPeer{ + { + NamespaceSelector: &metav1.LabelSelector{ + // Empty selector matches all + }, + }, + }, + }, + }, + }, + } + + result := isPolicyAffectedByNamespace(testPolicy, namespace, logger) + Expect(result).To(BeTrue()) + }) + }) +}) + +var _ = Describe("isPolicyAffectedByPod", func() { + var ( + logger logr.Logger + pod *corev1.Pod + policy *multiv1beta1.MultiNetworkPolicy + ) + + BeforeEach(func() { + logger = logr.Discard() + + // Default pod + pod = &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod", + Namespace: "test-namespace", + Labels: map[string]string{ + "app": "test-app", + "env": "dev", + }, + }, + Status: corev1.PodStatus{ + Phase: corev1.PodRunning, + }, + } + + // Default policy + policy = &multiv1beta1.MultiNetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-policy", + Namespace: "test-namespace", + }, + Spec: multiv1beta1.MultiNetworkPolicySpec{ + PodSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "test-app", + }, + }, + }, + } + }) + + Context("input validation", func() { + It("should return false when policy is nil", func() { + result := isPolicyAffectedByPod(nil, pod, logger) + Expect(result).To(BeFalse()) + }) + + It("should return false when pod is nil", func() { + result := isPolicyAffectedByPod(policy, nil, logger) + Expect(result).To(BeFalse()) + }) + + It("should return false when both policy and pod are nil", func() { + result := isPolicyAffectedByPod(nil, nil, logger) + Expect(result).To(BeFalse()) + }) + }) + + Context("ingress rules", func() { + Context("empty ingress from rules (allow all)", func() { + BeforeEach(func() { + policy.Spec.Ingress = []multiv1beta1.MultiNetworkPolicyIngressRule{ + { + // Empty From slice means allow all + }, + } + }) + + It("should return true for empty from rules", func() { + result := isPolicyAffectedByPod(policy, pod, logger) + Expect(result).To(BeTrue()) + }) + }) + + Context("IPBlock rules", func() { + BeforeEach(func() { + policy.Spec.Ingress = []multiv1beta1.MultiNetworkPolicyIngressRule{ + { + From: []multiv1beta1.MultiNetworkPolicyPeer{ + { + IPBlock: &multiv1beta1.IPBlock{ + CIDR: "10.0.0.0/8", + }, + }, + }, + }, + } + }) + + It("should skip IPBlock rules and continue checking other conditions", func() { + // Since IPBlock is present, it should skip and continue + // With no other matching conditions, it should fall through to pod selector check + result := isPolicyAffectedByPod(policy, pod, logger) + Expect(result).To(BeTrue()) // Should match on policy pod selector + }) + }) + + Context("pod selector only (same namespace)", func() { + BeforeEach(func() { + policy.Spec.Ingress = []multiv1beta1.MultiNetworkPolicyIngressRule{ + { + From: []multiv1beta1.MultiNetworkPolicyPeer{ + { + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "test-app", + }, + }, + }, + }, + }, + } + }) + + It("should return true when pod selector matches and namespaces match", func() { + result := isPolicyAffectedByPod(policy, pod, logger) + Expect(result).To(BeTrue()) + }) + + It("should return false when pod selector matches but namespaces don't match", func() { + pod.Namespace = "different-namespace" + result := isPolicyAffectedByPod(policy, pod, logger) + Expect(result).To(BeFalse()) + }) + + It("should continue checking when pod selector doesn't match", func() { + policy.Spec.Ingress[0].From[0].PodSelector.MatchLabels = map[string]string{ + "app": "different-app", + } + // Should fall through to policy pod selector check + result := isPolicyAffectedByPod(policy, pod, logger) + Expect(result).To(BeTrue()) // Should match on policy pod selector + }) + }) + + Context("pod selector with namespace selector", func() { + BeforeEach(func() { + policy.Spec.Ingress = []multiv1beta1.MultiNetworkPolicyIngressRule{ + { + From: []multiv1beta1.MultiNetworkPolicyPeer{ + { + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "test-app", + }, + }, + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "env": "test", + }, + }, + }, + }, + }, + } + }) + + It("should return true when pod selector matches (ignores namespace matching)", func() { + result := isPolicyAffectedByPod(policy, pod, logger) + Expect(result).To(BeTrue()) + }) + + It("should continue checking when pod selector doesn't match", func() { + policy.Spec.Ingress[0].From[0].PodSelector.MatchLabels = map[string]string{ + "app": "different-app", + } + // Should fall through to policy pod selector check + result := isPolicyAffectedByPod(policy, pod, logger) + Expect(result).To(BeTrue()) // Should match on policy pod selector + }) + }) + + Context("multiple ingress rules", func() { + BeforeEach(func() { + policy.Spec.Ingress = []multiv1beta1.MultiNetworkPolicyIngressRule{ + { + From: []multiv1beta1.MultiNetworkPolicyPeer{ + { + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "different-app", // Won't match + }, + }, + }, + }, + }, + { + From: []multiv1beta1.MultiNetworkPolicyPeer{ + { + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "env": "dev", // Will match + }, + }, + }, + }, + }, + } + }) + + It("should return true if any ingress rule matches", func() { + result := isPolicyAffectedByPod(policy, pod, logger) + Expect(result).To(BeTrue()) + }) + }) + + Context("multiple peers in single ingress rule", func() { + BeforeEach(func() { + policy.Spec.Ingress = []multiv1beta1.MultiNetworkPolicyIngressRule{ + { + From: []multiv1beta1.MultiNetworkPolicyPeer{ + { + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "different-app", // Won't match + }, + }, + }, + { + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "env": "dev", // Will match + }, + }, + }, + }, + }, + } + }) + + It("should return true if any peer matches", func() { + result := isPolicyAffectedByPod(policy, pod, logger) + Expect(result).To(BeTrue()) + }) + }) + }) + + Context("egress rules", func() { + Context("empty egress to rules (allow all)", func() { + BeforeEach(func() { + policy.Spec.Egress = []multiv1beta1.MultiNetworkPolicyEgressRule{ + { + // Empty To slice means allow all + }, + } + }) + + It("should return true for empty to rules", func() { + result := isPolicyAffectedByPod(policy, pod, logger) + Expect(result).To(BeTrue()) + }) + }) + + Context("IPBlock rules", func() { + BeforeEach(func() { + policy.Spec.Egress = []multiv1beta1.MultiNetworkPolicyEgressRule{ + { + To: []multiv1beta1.MultiNetworkPolicyPeer{ + { + IPBlock: &multiv1beta1.IPBlock{ + CIDR: "10.0.0.0/8", + }, + }, + }, + }, + } + }) + + It("should skip IPBlock rules and continue checking other conditions", func() { + // Since IPBlock is present, it should skip and continue + // With no other matching conditions, it should fall through to pod selector check + result := isPolicyAffectedByPod(policy, pod, logger) + Expect(result).To(BeTrue()) // Should match on policy pod selector + }) + }) + + Context("pod selector only (same namespace)", func() { + BeforeEach(func() { + policy.Spec.Egress = []multiv1beta1.MultiNetworkPolicyEgressRule{ + { + To: []multiv1beta1.MultiNetworkPolicyPeer{ + { + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "test-app", + }, + }, + }, + }, + }, + } + }) + + It("should return true when pod selector matches and namespaces match", func() { + result := isPolicyAffectedByPod(policy, pod, logger) + Expect(result).To(BeTrue()) + }) + + It("should return false when pod selector matches but namespaces don't match", func() { + pod.Namespace = "different-namespace" + result := isPolicyAffectedByPod(policy, pod, logger) + Expect(result).To(BeFalse()) + }) + + It("should continue checking when pod selector doesn't match", func() { + policy.Spec.Egress[0].To[0].PodSelector.MatchLabels = map[string]string{ + "app": "different-app", + } + // Should fall through to policy pod selector check + result := isPolicyAffectedByPod(policy, pod, logger) + Expect(result).To(BeTrue()) // Should match on policy pod selector + }) + }) + + Context("pod selector with namespace selector", func() { + BeforeEach(func() { + policy.Spec.Egress = []multiv1beta1.MultiNetworkPolicyEgressRule{ + { + To: []multiv1beta1.MultiNetworkPolicyPeer{ + { + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "test-app", + }, + }, + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "env": "test", + }, + }, + }, + }, + }, + } + }) + + It("should return true when pod selector matches (ignores namespace matching)", func() { + result := isPolicyAffectedByPod(policy, pod, logger) + Expect(result).To(BeTrue()) + }) + + It("should continue checking when pod selector doesn't match", func() { + policy.Spec.Egress[0].To[0].PodSelector.MatchLabels = map[string]string{ + "app": "different-app", + } + // Should fall through to policy pod selector check + result := isPolicyAffectedByPod(policy, pod, logger) + Expect(result).To(BeTrue()) // Should match on policy pod selector + }) + }) + + Context("multiple egress rules", func() { + BeforeEach(func() { + policy.Spec.Egress = []multiv1beta1.MultiNetworkPolicyEgressRule{ + { + To: []multiv1beta1.MultiNetworkPolicyPeer{ + { + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "different-app", // Won't match + }, + }, + }, + }, + }, + { + To: []multiv1beta1.MultiNetworkPolicyPeer{ + { + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "env": "dev", // Will match + }, + }, + }, + }, + }, + } + }) + + It("should return true if any egress rule matches", func() { + result := isPolicyAffectedByPod(policy, pod, logger) + Expect(result).To(BeTrue()) + }) + }) + }) + + Context("policy pod selector (final check)", func() { + Context("when no ingress/egress rules match", func() { + BeforeEach(func() { + // Set up a policy with non-matching ingress/egress rules + policy.Spec.Ingress = []multiv1beta1.MultiNetworkPolicyIngressRule{ + { + From: []multiv1beta1.MultiNetworkPolicyPeer{ + { + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "different-app", + }, + }, + }, + }, + }, + } + }) + + It("should return true when policy pod selector matches and pod is running", func() { + result := isPolicyAffectedByPod(policy, pod, logger) + Expect(result).To(BeTrue()) + }) + + It("should return false when policy pod selector matches but pod is not running", func() { + pod.Status.Phase = corev1.PodPending + result := isPolicyAffectedByPod(policy, pod, logger) + Expect(result).To(BeFalse()) + }) + + It("should return false when policy pod selector doesn't match", func() { + policy.Spec.PodSelector.MatchLabels = map[string]string{ + "app": "different-app", + } + result := isPolicyAffectedByPod(policy, pod, logger) + Expect(result).To(BeFalse()) + }) + + It("should return false when namespaces don't match", func() { + pod.Namespace = "different-namespace" + result := isPolicyAffectedByPod(policy, pod, logger) + Expect(result).To(BeFalse()) + }) + }) + + Context("with different pod phases", func() { + It("should return true for running pods", func() { + pod.Status.Phase = corev1.PodRunning + result := isPolicyAffectedByPod(policy, pod, logger) + Expect(result).To(BeTrue()) + }) + + It("should return false for pending pods", func() { + pod.Status.Phase = corev1.PodPending + result := isPolicyAffectedByPod(policy, pod, logger) + Expect(result).To(BeFalse()) + }) + + It("should return false for succeeded pods", func() { + pod.Status.Phase = corev1.PodSucceeded + result := isPolicyAffectedByPod(policy, pod, logger) + Expect(result).To(BeFalse()) + }) + + It("should return false for failed pods", func() { + pod.Status.Phase = corev1.PodFailed + result := isPolicyAffectedByPod(policy, pod, logger) + Expect(result).To(BeFalse()) + }) + + It("should return false for unknown phase pods", func() { + pod.Status.Phase = corev1.PodUnknown + result := isPolicyAffectedByPod(policy, pod, logger) + Expect(result).To(BeFalse()) + }) + }) + }) + + Context("combined ingress and egress rules", func() { + BeforeEach(func() { + policy.Spec.Ingress = []multiv1beta1.MultiNetworkPolicyIngressRule{ + { + From: []multiv1beta1.MultiNetworkPolicyPeer{ + { + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "different-app", // Won't match + }, + }, + }, + }, + }, + } + policy.Spec.Egress = []multiv1beta1.MultiNetworkPolicyEgressRule{ + { + To: []multiv1beta1.MultiNetworkPolicyPeer{ + { + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "env": "dev", // Will match + }, + }, + }, + }, + }, + } + }) + + It("should return true if any rule (ingress or egress) matches", func() { + result := isPolicyAffectedByPod(policy, pod, logger) + Expect(result).To(BeTrue()) + }) + }) + + Context("edge cases", func() { + It("should handle policy with no ingress, egress, or pod selector", func() { + policy.Spec.Ingress = nil + policy.Spec.Egress = nil + policy.Spec.PodSelector = metav1.LabelSelector{} // Empty selector matches all + + result := isPolicyAffectedByPod(policy, pod, logger) + Expect(result).To(BeTrue()) // Empty selector matches all running pods + }) + + It("should handle pod with no labels", func() { + pod.Labels = nil + policy.Spec.PodSelector = metav1.LabelSelector{} // Empty selector matches all + + result := isPolicyAffectedByPod(policy, pod, logger) + Expect(result).To(BeTrue()) + }) + + It("should handle pod with empty labels map", func() { + pod.Labels = map[string]string{} + policy.Spec.PodSelector = metav1.LabelSelector{} // Empty selector matches all + + result := isPolicyAffectedByPod(policy, pod, logger) + Expect(result).To(BeTrue()) + }) + + It("should handle policy with empty pod selector (matches all)", func() { + policy.Spec.PodSelector = metav1.LabelSelector{} // Empty selector matches all + + result := isPolicyAffectedByPod(policy, pod, logger) + Expect(result).To(BeTrue()) + }) + }) +}) + +var _ = Describe("isPodTentativelyEligible", func() { + var pod *corev1.Pod + + BeforeEach(func() { + // Default eligible pod + pod = &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod", + Namespace: "test-namespace", + Annotations: map[string]string{ + "k8s.v1.cni.cncf.io/networks": "macvlan-network", + }, + }, + Spec: corev1.PodSpec{ + HostNetwork: false, + }, + } + }) + + Context("input validation", func() { + It("should return false when pod is nil", func() { + result := isPodTentativelyEligible(nil) + Expect(result).To(BeFalse()) + }) + }) + + Context("host network check", func() { + It("should return false when pod uses host network", func() { + pod.Spec.HostNetwork = true + result := isPodTentativelyEligible(pod) + Expect(result).To(BeFalse()) + }) + + It("should continue checking when pod doesn't use host network", func() { + pod.Spec.HostNetwork = false + result := isPodTentativelyEligible(pod) + Expect(result).To(BeTrue()) + }) + }) + + Context("annotations check", func() { + It("should return false when pod has no annotations", func() { + pod.Annotations = nil + result := isPodTentativelyEligible(pod) + Expect(result).To(BeFalse()) + }) + + It("should return false when pod has empty annotations map", func() { + pod.Annotations = map[string]string{} + result := isPodTentativelyEligible(pod) + Expect(result).To(BeFalse()) + }) + + It("should continue checking when pod has annotations", func() { + pod.Annotations = map[string]string{ + "k8s.v1.cni.cncf.io/networks": "macvlan-network", + } + result := isPodTentativelyEligible(pod) + Expect(result).To(BeTrue()) + }) + }) + + Context("network annotation parsing", func() { + It("should return false when network annotation is invalid", func() { + pod.Annotations = map[string]string{ + "k8s.v1.cni.cncf.io/networks": "invalid-json-[", + } + result := isPodTentativelyEligible(pod) + Expect(result).To(BeFalse()) + }) + + It("should return false when network annotation is missing", func() { + pod.Annotations = map[string]string{ + "other-annotation": "value", + } + result := isPodTentativelyEligible(pod) + Expect(result).To(BeFalse()) + }) + + It("should return false when network annotation results in empty networks", func() { + pod.Annotations = map[string]string{ + "k8s.v1.cni.cncf.io/networks": "", + } + result := isPodTentativelyEligible(pod) + Expect(result).To(BeFalse()) + }) + + It("should return true when network annotation is valid with single network", func() { + pod.Annotations = map[string]string{ + "k8s.v1.cni.cncf.io/networks": "macvlan-network", + } + result := isPodTentativelyEligible(pod) + Expect(result).To(BeTrue()) + }) + + It("should return true when network annotation is valid with multiple networks", func() { + pod.Annotations = map[string]string{ + "k8s.v1.cni.cncf.io/networks": "macvlan-network,bridge-network", + } + result := isPodTentativelyEligible(pod) + Expect(result).To(BeTrue()) + }) + + It("should return true when network annotation is valid JSON format", func() { + pod.Annotations = map[string]string{ + "k8s.v1.cni.cncf.io/networks": `[{"name": "macvlan-network"}]`, + } + result := isPodTentativelyEligible(pod) + Expect(result).To(BeTrue()) + }) + }) + + Context("edge cases", func() { + It("should handle pod with host network and no annotations", func() { + pod.Spec.HostNetwork = true + pod.Annotations = nil + result := isPodTentativelyEligible(pod) + Expect(result).To(BeFalse()) + }) + + It("should handle pod with host network and valid annotations", func() { + pod.Spec.HostNetwork = true + pod.Annotations = map[string]string{ + "k8s.v1.cni.cncf.io/networks": "macvlan-network", + } + result := isPodTentativelyEligible(pod) + Expect(result).To(BeFalse()) + }) + }) +}) + +var _ = Describe("isTentativelyEligible", func() { + Context("input validation", func() { + It("should return false when object is nil", func() { + result := isTentativelyEligible(nil) + Expect(result).To(BeFalse()) + }) + + It("should return false when object is not a pod", func() { + namespace := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-namespace", + }, + } + result := isTentativelyEligible(namespace) + Expect(result).To(BeFalse()) + }) + }) + + Context("pod eligibility", func() { + It("should return false for ineligible pod", func() { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod", + Namespace: "test-namespace", + }, + Spec: corev1.PodSpec{ + HostNetwork: true, // Makes it ineligible + }, + } + result := isTentativelyEligible(pod) + Expect(result).To(BeFalse()) + }) + + It("should return true for tentatively eligible pod", func() { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod", + Namespace: "test-namespace", + Annotations: map[string]string{ + "k8s.v1.cni.cncf.io/networks": "macvlan-network", + }, + }, + Spec: corev1.PodSpec{ + HostNetwork: false, + }, + } + result := isTentativelyEligible(pod) + Expect(result).To(BeTrue()) + }) + }) +}) + +var _ = Describe("isEligible", func() { + Context("input validation", func() { + It("should return false when object is nil", func() { + result := isEligible(nil) + Expect(result).To(BeFalse()) + }) + + It("should return false when object is not a pod", func() { + namespace := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-namespace", + }, + } + result := isEligible(namespace) + Expect(result).To(BeFalse()) + }) + }) + + Context("pod phase check", func() { + var pod *corev1.Pod + + BeforeEach(func() { + // Default tentatively eligible pod + pod = &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod", + Namespace: "test-namespace", + Annotations: map[string]string{ + "k8s.v1.cni.cncf.io/networks": "macvlan-network", + }, + }, + Spec: corev1.PodSpec{ + HostNetwork: false, + }, + } + }) + + It("should return true for running pod that is tentatively eligible", func() { + pod.Status.Phase = corev1.PodRunning + result := isEligible(pod) + Expect(result).To(BeTrue()) + }) + + It("should return false for pending pod even if tentatively eligible", func() { + pod.Status.Phase = corev1.PodPending + result := isEligible(pod) + Expect(result).To(BeFalse()) + }) + + It("should return false for succeeded pod even if tentatively eligible", func() { + pod.Status.Phase = corev1.PodSucceeded + result := isEligible(pod) + Expect(result).To(BeFalse()) + }) + + It("should return false for failed pod even if tentatively eligible", func() { + pod.Status.Phase = corev1.PodFailed + result := isEligible(pod) + Expect(result).To(BeFalse()) + }) + + It("should return false for unknown phase pod even if tentatively eligible", func() { + pod.Status.Phase = corev1.PodUnknown + result := isEligible(pod) + Expect(result).To(BeFalse()) + }) + + It("should return false for running pod that is not tentatively eligible", func() { + pod.Status.Phase = corev1.PodRunning + pod.Spec.HostNetwork = true // Makes it not tentatively eligible + result := isEligible(pod) + Expect(result).To(BeFalse()) + }) + }) + + Context("comprehensive eligibility check", func() { + It("should return false for host network pod regardless of phase", func() { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod", + Namespace: "test-namespace", + Annotations: map[string]string{ + "k8s.v1.cni.cncf.io/networks": "macvlan-network", + }, + }, + Spec: corev1.PodSpec{ + HostNetwork: true, + }, + Status: corev1.PodStatus{ + Phase: corev1.PodRunning, + }, + } + result := isEligible(pod) + Expect(result).To(BeFalse()) + }) + + It("should return false for pod without network annotations regardless of phase", func() { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod", + Namespace: "test-namespace", + }, + Spec: corev1.PodSpec{ + HostNetwork: false, + }, + Status: corev1.PodStatus{ + Phase: corev1.PodRunning, + }, + } + result := isEligible(pod) + Expect(result).To(BeFalse()) + }) + + It("should return true only for running pods with valid network annotations and no host network", func() { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod", + Namespace: "test-namespace", + Annotations: map[string]string{ + "k8s.v1.cni.cncf.io/networks": "macvlan-network", + }, + }, + Spec: corev1.PodSpec{ + HostNetwork: false, + }, + Status: corev1.PodStatus{ + Phase: corev1.PodRunning, + }, + } + result := isEligible(pod) + Expect(result).To(BeTrue()) + }) + }) +}) + +var _ = Describe("getNetworksInPolicyForAnnotation Unit Tests", func() { + Context("input validation", func() { + It("should return error for empty annotation", func() { + networks, err := getNetworksInPolicyForAnnotation("", "default") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("contains no valid network names")) + Expect(networks).To(BeNil()) + }) + + It("should return error for whitespace-only annotation", func() { + networks, err := getNetworksInPolicyForAnnotation(" ", "default") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("contains no valid network names")) + Expect(networks).To(BeNil()) + }) + + It("should return error for annotation with only commas and spaces", func() { + networks, err := getNetworksInPolicyForAnnotation(" , , , ", "default") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("contains no valid network names")) + Expect(networks).To(BeNil()) + }) + }) + + Context("single network parsing", func() { + It("should parse single network name without namespace", func() { + networks, err := getNetworksInPolicyForAnnotation("macvlan-net", "default") + Expect(err).ToNot(HaveOccurred()) + Expect(networks).To(Equal([]string{"default/macvlan-net"})) + }) + + It("should parse single network name with namespace", func() { + networks, err := getNetworksInPolicyForAnnotation("prod/macvlan-net", "default") + Expect(err).ToNot(HaveOccurred()) + Expect(networks).To(Equal([]string{"prod/macvlan-net"})) + }) + + It("should handle network name with leading/trailing spaces", func() { + networks, err := getNetworksInPolicyForAnnotation(" macvlan-net ", "default") + Expect(err).ToNot(HaveOccurred()) + Expect(networks).To(Equal([]string{"default/macvlan-net"})) + }) + + It("should handle namespace and name with spaces", func() { + networks, err := getNetworksInPolicyForAnnotation(" prod / macvlan-net ", "default") + Expect(err).ToNot(HaveOccurred()) + Expect(networks).To(Equal([]string{"prod/macvlan-net"})) + }) + }) + + Context("multiple networks parsing", func() { + It("should parse multiple networks without namespaces", func() { + networks, err := getNetworksInPolicyForAnnotation("macvlan-net,bridge-net", "default") + Expect(err).ToNot(HaveOccurred()) + Expect(networks).To(Equal([]string{"default/macvlan-net", "default/bridge-net"})) + }) + + It("should parse multiple networks with mixed namespace formats", func() { + networks, err := getNetworksInPolicyForAnnotation("macvlan-net,prod/bridge-net,test/vlan-net", "default") + Expect(err).ToNot(HaveOccurred()) + Expect(networks).To(Equal([]string{"default/macvlan-net", "prod/bridge-net", "test/vlan-net"})) + }) + + It("should handle multiple networks with spaces around commas", func() { + networks, err := getNetworksInPolicyForAnnotation(" macvlan-net , prod/bridge-net , test/vlan-net ", "default") + Expect(err).ToNot(HaveOccurred()) + Expect(networks).To(Equal([]string{"default/macvlan-net", "prod/bridge-net", "test/vlan-net"})) + }) + + It("should skip empty entries in comma-separated list", func() { + networks, err := getNetworksInPolicyForAnnotation("macvlan-net,,bridge-net,", "default") + Expect(err).ToNot(HaveOccurred()) + Expect(networks).To(Equal([]string{"default/macvlan-net", "default/bridge-net"})) + }) + }) + + Context("edge cases and potential bugs", func() { + It("should handle network names with multiple slashes (potential bug)", func() { + networks, err := getNetworksInPolicyForAnnotation("ns/name/extra", "default") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("contains no valid network names")) + Expect(networks).To(BeNil()) + }) + + It("should handle empty namespace in network name", func() { + networks, err := getNetworksInPolicyForAnnotation("/macvlan-net", "default") + Expect(err.Error()).To(ContainSubstring("contains no valid network names")) + Expect(networks).To(BeNil()) + }) + + It("should handle empty name in network name", func() { + networks, err := getNetworksInPolicyForAnnotation("prod/", "default") + Expect(err.Error()).To(ContainSubstring("contains no valid network names")) + Expect(networks).To(BeNil()) + }) + + It("should handle only slash in network name", func() { + networks, err := getNetworksInPolicyForAnnotation("/", "default") + Expect(err.Error()).To(ContainSubstring("contains no valid network names")) + Expect(networks).To(BeNil()) + }) + }) + + Context("real-world scenarios", func() { + It("should handle typical CNI network annotation format", func() { + networks, err := getNetworksInPolicyForAnnotation("macvlan-conf@eth0,bridge-conf@eth1", "kube-system") + Expect(err).ToNot(HaveOccurred()) + Expect(networks).To(Equal([]string{"kube-system/macvlan-conf@eth0", "kube-system/bridge-conf@eth1"})) + }) + + It("should handle network names with special characters", func() { + networks, err := getNetworksInPolicyForAnnotation("net-1,net_2,net.3", "default") + Expect(err).ToNot(HaveOccurred()) + Expect(networks).To(Equal([]string{"default/net-1", "default/net_2", "default/net.3"})) + }) + }) +}) + +var _ = Describe("getNetworkType Unit Tests", func() { + Context("input validation", func() { + It("should return error when netAttachDef is nil", func() { + netType, err := getNetworkType(nil) + Expect(err).To(HaveOccurred()) + Expect(netType).To(BeEmpty()) + }) + }) + + Context("CNI configuration parsing", func() { + var netAttachDef *netdefv1.NetworkAttachmentDefinition + + BeforeEach(func() { + netAttachDef = &netdefv1.NetworkAttachmentDefinition{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-network", + Namespace: "default", + }, + } + }) + + It("should parse single plugin configuration", func() { + netAttachDef.Spec.Config = `{ + "cniVersion": "0.3.1", + "type": "macvlan", + "master": "eth0" + }` + + netType, err := getNetworkType(netAttachDef) + Expect(err).ToNot(HaveOccurred()) + Expect(netType).To(Equal("macvlan")) + }) + + It("should parse plugin list configuration and return first plugin type", func() { + netAttachDef.Spec.Config = `{ + "cniVersion": "0.3.1", + "plugins": [ + { + "type": "macvlan", + "master": "eth0" + }, + { + "type": "bridge", + "bridge": "br0" + } + ] + }` + + netType, err := getNetworkType(netAttachDef) + Expect(err).ToNot(HaveOccurred()) + Expect(netType).To(Equal("macvlan")) + }) + + It("should handle empty plugin list", func() { + netAttachDef.Spec.Config = `{ + "cniVersion": "0.3.1", + "plugins": [] + }` + + netType, err := getNetworkType(netAttachDef) + Expect(err).ToNot(HaveOccurred()) + Expect(netType).To(Equal("")) + }) + + It("should return error for invalid JSON configuration", func() { + netAttachDef.Spec.Config = `{ + "cniVersion": "0.3.1", + "type": "macvlan" + "master": "eth0" + }` + + netType, err := getNetworkType(netAttachDef) + Expect(err).To(HaveOccurred()) + Expect(netType).To(BeEmpty()) + }) + + It("should return error for empty configuration", func() { + netAttachDef.Spec.Config = "" + + netType, err := getNetworkType(netAttachDef) + Expect(err).To(HaveOccurred()) + Expect(netType).To(BeEmpty()) + }) + + It("should handle configuration without type field", func() { + netAttachDef.Spec.Config = `{ + "cniVersion": "0.3.1", + "master": "eth0" + }` + + netType, err := getNetworkType(netAttachDef) + Expect(err).ToNot(HaveOccurred()) + Expect(netType).To(Equal("")) + }) + + It("should handle plugin list with empty first plugin", func() { + netAttachDef.Spec.Config = `{ + "cniVersion": "0.3.1", + "plugins": [ + {}, + { + "type": "bridge", + "bridge": "br0" + } + ] + }` + + netType, err := getNetworkType(netAttachDef) + Expect(err).ToNot(HaveOccurred()) + Expect(netType).To(Equal("")) + }) + }) + + Context("edge cases", func() { + var netAttachDef *netdefv1.NetworkAttachmentDefinition + + BeforeEach(func() { + netAttachDef = &netdefv1.NetworkAttachmentDefinition{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-network", + Namespace: "default", + }, + } + }) + + It("should handle configuration with null values", func() { + netAttachDef.Spec.Config = `{ + "cniVersion": "0.3.1", + "type": null, + "master": "eth0" + }` + + netType, err := getNetworkType(netAttachDef) + Expect(err).ToNot(HaveOccurred()) + Expect(netType).To(Equal("")) + }) + + It("should handle configuration with numeric type field", func() { + netAttachDef.Spec.Config = `{ + "cniVersion": "0.3.1", + "type": 123, + "master": "eth0" + }` + + netType, err := getNetworkType(netAttachDef) + Expect(err).To(HaveOccurred()) + Expect(netType).To(BeEmpty()) + }) + + It("should handle very large configuration", func() { + largeConfig := `{ + "cniVersion": "0.3.1", + "type": "macvlan", + "master": "eth0", + "extra": "` + strings.Repeat("x", 10000) + `" + }` + + netAttachDef.Spec.Config = largeConfig + + netType, err := getNetworkType(netAttachDef) + Expect(err).ToNot(HaveOccurred()) + Expect(netType).To(Equal("macvlan")) + }) + }) +}) + +var _ = Describe("getAllowedNetworks Unit Tests", func() { + var ( + reconciler *MultiNetworkReconciler + ctx context.Context + logger logr.Logger + fakeClient client.Client + scheme *runtime.Scheme + ) + + BeforeEach(func() { + ctx = context.Background() + logger = logr.Discard() + + // Create a fake client with the necessary schemes + scheme = runtime.NewScheme() + Expect(netdefv1.AddToScheme(scheme)).To(Succeed()) + + fakeClient = fake.NewClientBuilder().WithScheme(scheme).Build() + reconciler = &MultiNetworkReconciler{ + Client: fakeClient, + ValidPlugins: []string{"macvlan", "bridge", "ipvlan"}, + } + }) + + Context("input validation", func() { + It("should return empty slice for nil networks slice", func() { + networks, err := reconciler.getAllowedNetworks(ctx, nil, reconciler.ValidPlugins, logger) + Expect(err.Error()).To(ContainSubstring("no allowed networks found")) + Expect(networks).To(BeEmpty()) + }) + + It("should return empty slice for empty networks slice", func() { + networks, err := reconciler.getAllowedNetworks(ctx, []string{}, reconciler.ValidPlugins, logger) + Expect(err.Error()).To(ContainSubstring("no allowed networks found")) + Expect(networks).To(BeEmpty()) + }) + + It("should handle nil validPlugins slice", func() { + networks := []string{"default/macvlan-net"} + allowedNetworks, err := reconciler.getAllowedNetworks(ctx, networks, nil, logger) + Expect(err.Error()).To(ContainSubstring("no allowed networks found")) + Expect(allowedNetworks).To(BeEmpty()) + }) + + It("should handle empty validPlugins slice", func() { + networks := []string{"default/macvlan-net"} + allowedNetworks, err := reconciler.getAllowedNetworks(ctx, networks, []string{}, logger) + Expect(err.Error()).To(ContainSubstring("no allowed networks found")) + Expect(allowedNetworks).To(BeEmpty()) + }) + }) + + Context("network format validation", func() { + It("should skip networks with invalid format", func() { + networks := []string{"invalid-format"} + allowedNetworks, err := reconciler.getAllowedNetworks(ctx, networks, reconciler.ValidPlugins, logger) + Expect(err.Error()).To(ContainSubstring("no allowed networks found")) + Expect(allowedNetworks).To(BeEmpty()) // No valid networks found due to invalid format + }) + + It("should skip networks with too many parts", func() { + networks := []string{"ns/name/extra"} + allowedNetworks, err := reconciler.getAllowedNetworks(ctx, networks, reconciler.ValidPlugins, logger) + Expect(err.Error()).To(ContainSubstring("no allowed networks found")) + Expect(allowedNetworks).To(BeEmpty()) // No valid networks found due to invalid format + }) + }) + + Context("network attachment definition handling", func() { + It("should skip networks when attachment definition is not found", func() { + networks := []string{"default/nonexistent-net"} + _, err := reconciler.getAllowedNetworks(ctx, networks, reconciler.ValidPlugins, logger) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("no allowed networks found")) + }) + + It("should return error when getting attachment definition fails with non-NotFound error", func() { + // This test would require a more sophisticated fake client setup + // For now, we'll test the happy path + networks := []string{"default/macvlan-net"} + _, err := reconciler.getAllowedNetworks(ctx, networks, reconciler.ValidPlugins, logger) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("no allowed networks found")) + }) + }) + + Context("network type validation with fake client", func() { + BeforeEach(func() { + // Create a fake client with network attachment definitions + objects := []client.Object{ + &netdefv1.NetworkAttachmentDefinition{ + ObjectMeta: metav1.ObjectMeta{ + Name: "macvlan-net", + Namespace: "default", + }, + Spec: netdefv1.NetworkAttachmentDefinitionSpec{ + Config: `{ + "cniVersion": "0.3.1", + "type": "macvlan", + "master": "eth0" + }`, + }, + }, + &netdefv1.NetworkAttachmentDefinition{ + ObjectMeta: metav1.ObjectMeta{ + Name: "bridge-net", + Namespace: "default", + }, + Spec: netdefv1.NetworkAttachmentDefinitionSpec{ + Config: `{ + "cniVersion": "0.3.1", + "type": "bridge", + "bridge": "br0" + }`, + }, + }, + &netdefv1.NetworkAttachmentDefinition{ + ObjectMeta: metav1.ObjectMeta{ + Name: "unsupported-net", + Namespace: "default", + }, + Spec: netdefv1.NetworkAttachmentDefinitionSpec{ + Config: `{ + "cniVersion": "0.3.1", + "type": "unsupported", + "config": "value" + }`, + }, + }, + &netdefv1.NetworkAttachmentDefinition{ + ObjectMeta: metav1.ObjectMeta{ + Name: "invalid-config-net", + Namespace: "default", + }, + Spec: netdefv1.NetworkAttachmentDefinitionSpec{ + Config: `{ + "cniVersion": "0.3.1", + "type": "macvlan" + "master": "eth0" + }`, + }, + }, + } + + fakeClient = fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build() + reconciler.Client = fakeClient + }) + + It("should include networks with supported types", func() { + networks := []string{"default/macvlan-net"} + allowedNetworks, err := reconciler.getAllowedNetworks(ctx, networks, reconciler.ValidPlugins, logger) + Expect(err).ToNot(HaveOccurred()) + Expect(allowedNetworks).To(Equal([]string{"default/macvlan-net"})) + }) + + It("should include multiple networks with supported types", func() { + networks := []string{"default/macvlan-net", "default/bridge-net"} + allowedNetworks, err := reconciler.getAllowedNetworks(ctx, networks, reconciler.ValidPlugins, logger) + Expect(err).ToNot(HaveOccurred()) + Expect(allowedNetworks).To(Equal([]string{"default/macvlan-net", "default/bridge-net"})) + }) + + It("should exclude networks with unsupported types", func() { + networks := []string{"default/unsupported-net"} + _, err := reconciler.getAllowedNetworks(ctx, networks, reconciler.ValidPlugins, logger) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("no allowed networks found")) + }) + + It("should skip networks with invalid configuration", func() { + networks := []string{"default/invalid-config-net"} + allowedNetworks, err := reconciler.getAllowedNetworks(ctx, networks, reconciler.ValidPlugins, logger) + Expect(err).To(HaveOccurred()) + Expect(allowedNetworks).To(BeEmpty()) + }) + + It("should handle mix of valid and invalid networks", func() { + networks := []string{ + "invalid-format", + "default/macvlan-net", + "ns/name/extra", + "default/unsupported-net", + "default/bridge-net", + } + allowedNetworks, err := reconciler.getAllowedNetworks(ctx, networks, reconciler.ValidPlugins, logger) + Expect(err).ToNot(HaveOccurred()) + Expect(allowedNetworks).To(Equal([]string{"default/macvlan-net", "default/bridge-net"})) + }) + + It("should handle networks from different namespaces", func() { + // Add a network in a different namespace + otherNamespaceNet := &netdefv1.NetworkAttachmentDefinition{ + ObjectMeta: metav1.ObjectMeta{ + Name: "macvlan-net", + Namespace: "other-ns", + }, + Spec: netdefv1.NetworkAttachmentDefinitionSpec{ + Config: `{ + "cniVersion": "0.3.1", + "type": "macvlan", + "master": "eth1" + }`, + }, + } + Expect(fakeClient.Create(ctx, otherNamespaceNet)).To(Succeed()) + + networks := []string{"default/macvlan-net", "other-ns/macvlan-net"} + allowedNetworks, err := reconciler.getAllowedNetworks(ctx, networks, reconciler.ValidPlugins, logger) + Expect(err).ToNot(HaveOccurred()) + Expect(allowedNetworks).To(Equal([]string{"default/macvlan-net", "other-ns/macvlan-net"})) + }) + }) +}) diff --git a/pkg/controller/predicates.go b/pkg/controller/predicates.go new file mode 100644 index 00000000..f1ae0008 --- /dev/null +++ b/pkg/controller/predicates.go @@ -0,0 +1,213 @@ +package controller + +import ( + "reflect" + + multiv1beta1 "github.com/k8snetworkplumbingwg/multi-networkpolicy/pkg/apis/k8s.cni.cncf.io/v1beta1" + netdefutils "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/utils" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/predicate" + + "github.com/k8snetworkplumbingwg/multi-network-policy-nftables/pkg/datastore" +) + +// MultiNetworkPolicyPredicate is a predicate that checks if a policy is eligible for reconciliation +// This predicate is set with WithEventFilter which means that the predicate will be added to all watched resources. +// We will let through events for pods and namespaces which will be handled by their respective predicates. +var MultiNetworkPolicyPredicate = predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { + // Always when creating a policy, we need to reconcile it + if _, ok := e.Object.(*multiv1beta1.MultiNetworkPolicy); ok { + log.Log.V(2).Info("MultiNetworkPolicyPredicate CreateFunc", "namespace", e.Object.GetNamespace(), "name", e.Object.GetName()) + return true + } + + return true + }, + UpdateFunc: func(e event.UpdateEvent) bool { + // Pod and Namespace events will be handled by their respective predicates + if _, ok := e.ObjectOld.(*corev1.Pod); ok { + return true + } + if _, ok := e.ObjectNew.(*corev1.Namespace); ok { + return true + } + + // Mark for deletion + if e.ObjectOld.GetDeletionTimestamp() == nil && e.ObjectNew.GetDeletionTimestamp() != nil { + log.Log.V(2).Info("MultiNetworkPolicyPredicate UpdateFunc", "reason", "Marked for deletion", "namespace", e.ObjectOld.GetNamespace(), "name", e.ObjectOld.GetName()) + return true + } + + // Spec Changes + if e.ObjectOld.GetGeneration() != e.ObjectNew.GetGeneration() { + log.Log.V(2).Info("MultiNetworkPolicyPredicate UpdateFunc", "reason", "Spec changed", "namespace", e.ObjectOld.GetNamespace(), "name", e.ObjectOld.GetName()) + return true + } + + // Policy-for Annotation Changes + oldAnnotations := e.ObjectOld.GetAnnotations() + newAnnotations := e.ObjectNew.GetAnnotations() + + var oldAnnotationValue, newAnnotationValue string + if oldAnnotations != nil { + oldAnnotationValue = oldAnnotations[datastore.PolicyForAnnotation] + } + if newAnnotations != nil { + newAnnotationValue = newAnnotations[datastore.PolicyForAnnotation] + } + + if oldAnnotationValue != newAnnotationValue { + log.Log.V(2).Info("MultiNetworkPolicyPredicate UpdateFunc", "reason", "Policy-for annotation changed", "namespace", e.ObjectOld.GetNamespace(), "name", e.ObjectOld.GetName()) + return true + } + + return false + }, + DeleteFunc: func(e event.DeleteEvent) bool { + // Always when deleting a policy, we need to reconcile it + if _, ok := e.Object.(*multiv1beta1.MultiNetworkPolicy); ok { + log.Log.V(2).Info("MultiNetworkPolicyPredicate DeleteFunc", "namespace", e.Object.GetNamespace(), "name", e.Object.GetName()) + return true + } + + return true + }, + GenericFunc: func(_ event.GenericEvent) bool { + return false + }, +} + +// NamespacePredicate is a predicate that will only allow to create events, and updates when the namespace labels change. +var NamespacePredicate = predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { + // Always when creating a namespace, we need to reconcile it + log.Log.V(2).Info("NamespacePredicate CreateFunc", "name", e.Object.GetName()) + return true + }, + UpdateFunc: func(e event.UpdateEvent) bool { + // Only when the namespace labels change, we need to reconcile it + if !reflect.DeepEqual(e.ObjectOld.GetLabels(), e.ObjectNew.GetLabels()) { + log.Log.V(2).Info("NamespacePredicate UpdateFunc", "reason", "Labels changed", "name", e.ObjectNew.GetName()) + return true + } + + return false + }, + DeleteFunc: func(_ event.DeleteEvent) bool { + // We don't want to process namespace deletion events. Pods are gone for sure. + return false + }, + GenericFunc: func(_ event.GenericEvent) bool { + return false + }, +} + +// PodPredicate is a predicate that checks if a pod is eligible for reconciliation +// All events will check if the pod is eligible, except the delete event given that the pod might not be running. +// This pod might be matched by a peer selector, so we need to reconcile it. +// No need to reconcile when old and new are eligible on update events. Changes on secondary interfaces need a Pod restart. +// And containerID of first container is always parsed by demand to get the netns path. +var PodPredicate = predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { + // Always when creating a pod, we need to reconcile it + if isEligible(e.Object) { + log.Log.V(2).Info("PodPredicate CreateFunc", "namespace", e.Object.GetNamespace(), "name", e.Object.GetName()) + return true + } + + return false + }, + UpdateFunc: func(e event.UpdateEvent) bool { + oldEligible := isEligible(e.ObjectOld) + newEligible := isEligible(e.ObjectNew) + + // When pod becomes eligible + if !oldEligible && newEligible { + log.Log.V(2).Info("PodPredicate UpdateFunc", "reason", "Pod became eligible", "namespace", e.ObjectNew.GetNamespace(), "name", e.ObjectNew.GetName()) + return true + } + + // When pod becomes ineligible + if oldEligible && !newEligible { + log.Log.V(2).Info("PodPredicate UpdateFunc", "reason", "Pod became ineligible", "namespace", e.ObjectNew.GetNamespace(), "name", e.ObjectNew.GetName()) + return true + } + + // When both pods are eligible but labels changed + if oldEligible && newEligible { + if !reflect.DeepEqual(e.ObjectOld.GetLabels(), e.ObjectNew.GetLabels()) { + log.Log.V(2).Info("PodPredicate UpdateFunc", "reason", "Pod labels changed", "namespace", e.ObjectNew.GetNamespace(), "name", e.ObjectNew.GetName()) + return true + } + } + + return false + }, + DeleteFunc: func(e event.DeleteEvent) bool { + // Always when deleting a pod, we need to reconcile it + if isTentativelyEligible(e.Object) { + log.Log.V(2).Info("PodPredicate DeleteFunc", "namespace", e.Object.GetNamespace(), "name", e.Object.GetName()) + return true + } + + return false + }, + GenericFunc: func(_ event.GenericEvent) bool { + return false + }, +} + +// isEligible checks if the object is eligible for reconciliation +func isEligible(obj client.Object) bool { + pod, ok := obj.(*corev1.Pod) + if !ok { + return false + } + + if pod.Status.Phase != corev1.PodRunning { + return false + } + + return isPodTentativelyEligible(pod) +} + +// isTentativelyEligible checks if the object is tentatively eligible for reconciliation +func isTentativelyEligible(obj client.Object) bool { + pod, ok := obj.(*corev1.Pod) + if !ok { + return false + } + + return isPodTentativelyEligible(pod) +} + +// isPodTentativelyEligible checks if a pod is tentatively eligible for reconciliation +// Pod status phase check is missing to be fully eligible. +func isPodTentativelyEligible(pod *corev1.Pod) bool { + if pod == nil { + return false + } + + if pod.Spec.HostNetwork { + return false + } + + if pod.GetAnnotations() == nil { + return false + } + + networks, err := netdefutils.ParsePodNetworkAnnotation(pod) + if err != nil { + return false + } + + if len(networks) == 0 { + return false + } + + return true +} diff --git a/pkg/controller/suite_test.go b/pkg/controller/suite_test.go new file mode 100644 index 00000000..e4e189da --- /dev/null +++ b/pkg/controller/suite_test.go @@ -0,0 +1,125 @@ +package controller_test + +import ( + "context" + "fmt" + "path/filepath" + "runtime" + "testing" + + "github.com/go-logr/logr" + multinetworkscheme "github.com/k8snetworkplumbingwg/multi-networkpolicy/pkg/client/clientset/versioned/scheme" + netdefscheme "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/client/clientset/versioned/scheme" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + "github.com/k8snetworkplumbingwg/multi-network-policy-nftables/pkg/controller" + "github.com/k8snetworkplumbingwg/multi-network-policy-nftables/pkg/datastore" + "github.com/k8snetworkplumbingwg/multi-network-policy-nftables/pkg/nftables" +) + +var ( + cfg *rest.Config + k8sClient client.Client + testEnv *envtest.Environment + ctx context.Context + cancel context.CancelFunc + datastoreInstance *datastore.Datastore + mockNFT *MockNFT +) + +// MockNFT is a mock implementation of the NFT interface for testing +type MockNFT struct { + SyncPolicyFunc func(ctx context.Context, policy *datastore.Policy, operation nftables.SyncOperation, logger logr.Logger) error +} + +func (m *MockNFT) SyncPolicy(ctx context.Context, policy *datastore.Policy, operation nftables.SyncOperation, logger logr.Logger) error { + if m.SyncPolicyFunc != nil { + return m.SyncPolicyFunc(ctx, policy, operation, logger) + } + return nil +} + +func TestMultiNetworkController(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Controller Suite") +} + +var _ = BeforeSuite(func() { + ctx, cancel = context.WithCancel(context.TODO()) + + // Set up logger + ctrl.SetLogger(zap.New(zap.UseDevMode(true), zap.WriteTo(GinkgoWriter))) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("config", "crd")}, + ErrorIfCRDPathMissing: false, + + // The BinaryAssetsDirectory is only required if you want to run the tests directly + // without call the makefile target test. If not informed it will look for the + // default path defined in controller-runtime which is /usr/local/kubebuilder/. + // Note that you must have the required binaries setup under the bin directory to perform + // the tests directly. When we run make test it will be setup and used automatically. + BinaryAssetsDirectory: filepath.Join("..", "..", "bin", "k8s", + fmt.Sprintf("1.30.0-%s-%s", runtime.GOOS, runtime.GOARCH)), + } + + var err error + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + // Add custom scheme + Expect(multinetworkscheme.AddToScheme(scheme.Scheme)).To(Succeed()) + Expect(netdefscheme.AddToScheme(scheme.Scheme)).To(Succeed()) + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) + + k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme.Scheme, + }) + Expect(err).ToNot(HaveOccurred()) + + datastoreInstance = &datastore.Datastore{ + Policies: make(map[types.NamespacedName]*datastore.Policy), + } + + mockNFT = &MockNFT{ + SyncPolicyFunc: func(_ context.Context, _ *datastore.Policy, _ nftables.SyncOperation, _ logr.Logger) error { + // Mock successful sync by default + return nil + }, + } + + err = (&controller.MultiNetworkReconciler{ + Client: k8sManager.GetClient(), + Scheme: k8sManager.GetScheme(), + DS: datastoreInstance, + NFT: mockNFT, + ValidPlugins: []string{"macvlan", "ipvlan"}, + }).SetupWithManager(k8sManager) + Expect(err).ToNot(HaveOccurred()) + + go func() { + defer GinkgoRecover() + err = k8sManager.Start(ctx) + Expect(err).ToNot(HaveOccurred(), "failed to run manager") + }() +}) + +var _ = AfterSuite(func() { + cancel() + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) diff --git a/pkg/controllers/controller_suite_test.go b/pkg/controllers/controller_suite_test.go deleted file mode 100644 index 31d41a2a..00000000 --- a/pkg/controllers/controller_suite_test.go +++ /dev/null @@ -1,29 +0,0 @@ -/* -Copyright 2020 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package controllers - -import ( - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - - "testing" -) - -func TestControllers(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "controllers") -} diff --git a/pkg/controllers/doc.go b/pkg/controllers/doc.go deleted file mode 100644 index 7ce4aa01..00000000 --- a/pkg/controllers/doc.go +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) 2021 Multus Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package controllers is the package that contains controller functions. -package controllers diff --git a/pkg/controllers/namespace.go b/pkg/controllers/namespace.go deleted file mode 100644 index 605b737f..00000000 --- a/pkg/controllers/namespace.go +++ /dev/null @@ -1,263 +0,0 @@ -/* -Copyright 2020 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package controllers - -import ( - "fmt" - "reflect" - "sync" - "time" - - v1 "k8s.io/api/core/v1" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" - coreinformers "k8s.io/client-go/informers/core/v1" - "k8s.io/client-go/tools/cache" - "k8s.io/klog" -) - -// NamespaceHandler is an abstract interface of objects which receive -// notifications about pod object changes. -type NamespaceHandler interface { - // OnNamespaceAdd is called whenever creation of new ns object - // is observed. - OnNamespaceAdd(ns *v1.Namespace) - // OnNamespaceUpdate is called whenever modification of an existing - // ns object is observed. - OnNamespaceUpdate(oldNS, ns *v1.Namespace) - // OnNamespaceDelete is called whenever deletion of an existing ns - // object is observed. - OnNamespaceDelete(ns *v1.Namespace) - // OnNamespaceSynced is called once all the initial event handlers were - // called and the state is fully propagated to local cache. - OnNamespaceSynced() -} - -// NamespaceConfig ... -type NamespaceConfig struct { - listerSynced cache.InformerSynced - eventHandlers []NamespaceHandler -} - -// NewNamespaceConfig creates a new NamespaceConfig. -func NewNamespaceConfig(nsInformer coreinformers.NamespaceInformer, resyncPeriod time.Duration) *NamespaceConfig { - result := &NamespaceConfig{ - listerSynced: nsInformer.Informer().HasSynced, - } - - nsInformer.Informer().AddEventHandlerWithResyncPeriod( - cache.ResourceEventHandlerFuncs{ - AddFunc: result.handleAddNamespace, - UpdateFunc: result.handleUpdateNamespace, - DeleteFunc: result.handleDeleteNamespace, - }, - resyncPeriod, - ) - return result -} - -// RegisterEventHandler registers a handler which is called on every pod change. -func (c *NamespaceConfig) RegisterEventHandler(handler NamespaceHandler) { - c.eventHandlers = append(c.eventHandlers, handler) -} - -// Run waits for cache synced and invokes handlers after syncing. -func (c *NamespaceConfig) Run(stopCh <-chan struct{}) { - klog.Info("Starting ns config controller") - - if !cache.WaitForNamedCacheSync("ns config", stopCh, c.listerSynced) { - return - } - - for i := range c.eventHandlers { - klog.V(10).Infof("Calling handler.OnNamespaceSynced()") - c.eventHandlers[i].OnNamespaceSynced() - } -} - -func (c *NamespaceConfig) handleAddNamespace(obj interface{}) { - ns, ok := obj.(*v1.Namespace) - if !ok { - utilruntime.HandleError(fmt.Errorf("unexpected object type: %v", obj)) - return - } - - for i := range c.eventHandlers { - klog.V(10).Infof("Calling handler.OnNamespaceAdd") - c.eventHandlers[i].OnNamespaceAdd(ns) - } -} - -func (c *NamespaceConfig) handleUpdateNamespace(oldObj, newObj interface{}) { - oldNamespace, ok := oldObj.(*v1.Namespace) - if !ok { - utilruntime.HandleError(fmt.Errorf("unexpected object type: %v", oldObj)) - return - } - ns, ok := newObj.(*v1.Namespace) - if !ok { - utilruntime.HandleError(fmt.Errorf("unexpected object type: %v", newObj)) - return - } - for i := range c.eventHandlers { - klog.V(10).Infof("Calling handler.OnNamespaceUpdate") - c.eventHandlers[i].OnNamespaceUpdate(oldNamespace, ns) - } -} - -func (c *NamespaceConfig) handleDeleteNamespace(obj interface{}) { - ns, ok := obj.(*v1.Namespace) - if !ok { - tombstone, ok := obj.(cache.DeletedFinalStateUnknown) - if !ok { - utilruntime.HandleError(fmt.Errorf("unexpected object type: %v", obj)) - } - if ns, ok = tombstone.Obj.(*v1.Namespace); !ok { - utilruntime.HandleError(fmt.Errorf("unexpected object type: %v", obj)) - return - } - } - for i := range c.eventHandlers { - klog.V(10).Infof("Calling handler.OnNamespaceDelete") - c.eventHandlers[i].OnNamespaceDelete(ns) - } -} - -// NamespaceInfo contains information that defines a namespace. -type NamespaceInfo struct { - Name string - Labels map[string]string -} - -type nsChange struct { - previous NamespaceMap - current NamespaceMap -} - -// NamespaceChangeTracker carries state about uncommitted changes to an arbitrary number of -// Namespaces in the node, keyed by their namespace and name -type NamespaceChangeTracker struct { - // lock protects items. - lock sync.Mutex - // items maps a service to its podChange. - items map[string]*nsChange -} - -func (nct *NamespaceChangeTracker) newNamespaceInfo(ns *v1.Namespace) *NamespaceInfo { - return &NamespaceInfo{ - Name: ns.Name, - Labels: ns.Labels, - } -} - -// NewNamespaceChangeTracker ... -func NewNamespaceChangeTracker() *NamespaceChangeTracker { - return &NamespaceChangeTracker{ - items: make(map[string]*nsChange), - } -} - -func (nct *NamespaceChangeTracker) nsToNamespaceMap(ns *v1.Namespace) NamespaceMap { - if ns == nil { - return nil - } - - namespaceMap := make(NamespaceMap) - nsInfo := nct.newNamespaceInfo(ns) - namespaceMap[ns.Name] = *nsInfo - return namespaceMap -} - -// Update ... -func (nct *NamespaceChangeTracker) Update(previous, current *v1.Namespace) bool { - ns := current - - if nct == nil { - return false - } - - if ns == nil { - ns = previous - } - if ns == nil { - return false - } - - nct.lock.Lock() - defer nct.lock.Unlock() - - change, exists := nct.items[ns.Name] - if !exists { - change = &nsChange{} - prevNamespaceMap := nct.nsToNamespaceMap(previous) - change.previous = prevNamespaceMap - nct.items[ns.Name] = change - } - curNamespaceMap := nct.nsToNamespaceMap(current) - change.current = curNamespaceMap - if reflect.DeepEqual(change.previous, change.current) { - delete(nct.items, ns.Name) - } - return len(nct.items) >= 0 -} - -// NamespaceMap ... -type NamespaceMap map[string]NamespaceInfo - -// Update updates podMap base on the given changes -func (nm *NamespaceMap) Update(changes *NamespaceChangeTracker) { - if nm != nil { - nm.apply(changes) - } -} - -func (nm *NamespaceMap) apply(changes *NamespaceChangeTracker) { - if nm == nil || changes == nil { - return - } - - changes.lock.Lock() - defer changes.lock.Unlock() - for _, change := range changes.items { - nm.unmerge(change.previous) - nm.merge(change.current) - } - // clear changes after applying them to ServiceMap. - changes.items = make(map[string]*nsChange) - return -} - -func (nm *NamespaceMap) merge(other NamespaceMap) { - for nsName, info := range other { - (*nm)[nsName] = info - } -} - -func (nm *NamespaceMap) unmerge(other NamespaceMap) { - for nsName := range other { - delete(*nm, nsName) - } -} - -// GetNamespaceInfo ... -func (nm *NamespaceMap) GetNamespaceInfo(nsName string) (*NamespaceInfo, error) { - nsInfo, ok := (*nm)[nsName] - if ok { - return &nsInfo, nil - } - - return nil, fmt.Errorf("not found") -} diff --git a/pkg/controllers/namespace_test.go b/pkg/controllers/namespace_test.go deleted file mode 100644 index 662813ec..00000000 --- a/pkg/controllers/namespace_test.go +++ /dev/null @@ -1,170 +0,0 @@ -/* -Copyright 2020 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package controllers - -import ( - "time" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "k8s.io/api/core/v1" - "k8s.io/client-go/informers" - "k8s.io/client-go/kubernetes/fake" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -type FakeNamespaceConfigStub struct { - CounterAdd int - CounterUpdate int - CounterDelete int - CounterSynced int -} - -func (f *FakeNamespaceConfigStub) OnNamespaceAdd(_ *v1.Namespace) { - f.CounterAdd++ -} - -func (f *FakeNamespaceConfigStub) OnNamespaceUpdate(_, _ *v1.Namespace) { - f.CounterUpdate++ -} - -func (f *FakeNamespaceConfigStub) OnNamespaceDelete(_ *v1.Namespace) { - f.CounterDelete++ -} - -func (f *FakeNamespaceConfigStub) OnNamespaceSynced() { - f.CounterSynced++ -} - -func NewFakeNamespaceConfig(stub *FakeNamespaceConfigStub) *NamespaceConfig { - configSync := 15 * time.Minute - fakeClient := fake.NewSimpleClientset() - informerFactory := informers.NewSharedInformerFactoryWithOptions(fakeClient, configSync) - nsConfig := NewNamespaceConfig(informerFactory.Core().V1().Namespaces(), configSync) - nsConfig.RegisterEventHandler(stub) - return nsConfig -} - -func NewNamespace(name string, labels map[string]string) *v1.Namespace { - return &v1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Labels: labels, - }, - } -} - -var _ = Describe("namespace config", func() { - It("check add handler", func() { - stub := &FakeNamespaceConfigStub{} - nsConfig := NewFakeNamespaceConfig(stub) - nsConfig.handleAddNamespace(NewNamespace("test", nil)) - Expect(stub.CounterAdd).To(Equal(1)) - Expect(stub.CounterUpdate).To(Equal(0)) - Expect(stub.CounterDelete).To(Equal(0)) - Expect(stub.CounterSynced).To(Equal(0)) - }) - - It("check update handler", func() { - stub := &FakeNamespaceConfigStub{} - nsConfig := NewFakeNamespaceConfig(stub) - nsConfig.handleUpdateNamespace(NewNamespace("test1", nil), NewNamespace("test2", nil)) - Expect(stub.CounterAdd).To(Equal(0)) - Expect(stub.CounterUpdate).To(Equal(1)) - Expect(stub.CounterDelete).To(Equal(0)) - Expect(stub.CounterSynced).To(Equal(0)) - }) - - It("check delete handler", func() { - stub := &FakeNamespaceConfigStub{} - nsConfig := NewFakeNamespaceConfig(stub) - nsConfig.handleDeleteNamespace(NewNamespace("test1", nil)) - Expect(stub.CounterAdd).To(Equal(0)) - Expect(stub.CounterUpdate).To(Equal(0)) - Expect(stub.CounterDelete).To(Equal(1)) - Expect(stub.CounterSynced).To(Equal(0)) - }) -}) - -var _ = Describe("namespace controller", func() { - It("Initialize and verify empty", func() { - nsChanges := NewNamespaceChangeTracker() - nsMap := make(NamespaceMap) - nsMap.Update(nsChanges) - Expect(len(nsMap)).To(Equal(0)) - }) - - It("Add ns and verify", func() { - nsChanges := NewNamespaceChangeTracker() - Expect(nsChanges.Update(nil, NewNamespace("test1", map[string]string{"labelName1": "labelValue1"}))).To(BeTrue()) - - nsMap := make(NamespaceMap) - nsMap.Update(nsChanges) - Expect(len(nsMap)).To(Equal(1)) - nsTest1, ok := nsMap["test1"] - Expect(ok).To(BeTrue()) - Expect(nsTest1.Name).To(Equal("test1")) - Expect(len(nsTest1.Labels)).To(Equal(1)) - - labelTest, ok := nsTest1.Labels["labelName1"] - Expect(ok).To(BeTrue()) - Expect(labelTest).To(Equal("labelValue1")) - }) - - It("Add ns then del ns and verify", func() { - nsChanges := NewNamespaceChangeTracker() - Expect(nsChanges.Update(nil, NewNamespace("test1", map[string]string{"labelName1": "labelValue1"}))).To(BeTrue()) - Expect(nsChanges.Update(nil, NewNamespace("test2", map[string]string{"labelName2": "labelValue2"}))).To(BeTrue()) - Expect(nsChanges.Update(NewNamespace("test2", map[string]string{"labelName2": "labelValue2"}), nil)).To(BeTrue()) - - nsMap := make(NamespaceMap) - nsMap.Update(nsChanges) - Expect(len(nsMap)).To(Equal(1)) - nsTest1, ok := nsMap["test1"] - Expect(ok).To(BeTrue()) - Expect(nsTest1.Name).To(Equal("test1")) - Expect(len(nsTest1.Labels)).To(Equal(1)) - - labelTest, ok := nsTest1.Labels["labelName1"] - Expect(ok).To(BeTrue()) - Expect(labelTest).To(Equal("labelValue1")) - }) - - It("invalid Update case", func() { - nsChanges := NewNamespaceChangeTracker() - Expect(nsChanges.Update(nil, nil)).To(BeFalse()) - }) - - It("Add ns then update ns and verify", func() { - nsChanges := NewNamespaceChangeTracker() - Expect(nsChanges.Update(nil, NewNamespace("test1", map[string]string{"labelName1": "labelValue1"}))).To(BeTrue()) - Expect(nsChanges.Update(nil, NewNamespace("test1", map[string]string{"labelName2": "labelValue2"}))).To(BeTrue()) - nsMap := make(NamespaceMap) - nsMap.Update(nsChanges) - Expect(len(nsMap)).To(Equal(1)) - nsTest1, ok := nsMap["test1"] - Expect(ok).To(BeTrue()) - Expect(nsTest1.Name).To(Equal("test1")) - Expect(len(nsTest1.Labels)).To(Equal(1)) - - labelTest, ok := nsTest1.Labels["labelName2"] - Expect(ok).To(BeTrue()) - Expect(labelTest).To(Equal("labelValue2")) - }) -}) diff --git a/pkg/controllers/net-attach-def.go b/pkg/controllers/net-attach-def.go deleted file mode 100644 index 073edb28..00000000 --- a/pkg/controllers/net-attach-def.go +++ /dev/null @@ -1,309 +0,0 @@ -/* -Copyright 2020 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package controllers - -import ( - "encoding/json" - "fmt" - "reflect" - "sync" - "time" - - cnitypes "github.com/containernetworking/cni/pkg/types" - netdefv1 "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/apis/k8s.cni.cncf.io/v1" - netdefinformerv1 "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/client/informers/externalversions/k8s.cni.cncf.io/v1" - netdefutils "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/utils" - - "k8s.io/apimachinery/pkg/types" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" - "k8s.io/client-go/tools/cache" - "k8s.io/klog" -) - -// NetDefHandler is an abstract interface of objects which receive -// notifications about net-attach-def object changes. -type NetDefHandler interface { - // OnNetDefAdd is called whenever creation of new object - // is observed. - OnNetDefAdd(net *netdefv1.NetworkAttachmentDefinition) - // OnNetDefUpdate is called whenever modification of an existing - // object is observed. - OnNetDefUpdate(oldNet, net *netdefv1.NetworkAttachmentDefinition) - // OnNetDefDelete is called whenever deletion of an existing - // object is observed. - OnNetDefDelete(net *netdefv1.NetworkAttachmentDefinition) - // OnNetDefSynced is called once all the initial event handlers were - // called and the state is fully propagated to local cache. - OnNetDefSynced() -} - -// NetDefConfig ... -type NetDefConfig struct { - listerSynced cache.InformerSynced - eventHandlers []NetDefHandler -} - -// NewNetDefConfig ... -func NewNetDefConfig(netdefInformer netdefinformerv1.NetworkAttachmentDefinitionInformer, resyncPeriod time.Duration) *NetDefConfig { - result := &NetDefConfig{ - listerSynced: netdefInformer.Informer().HasSynced, - } - - netdefInformer.Informer().AddEventHandlerWithResyncPeriod( - cache.ResourceEventHandlerFuncs{ - AddFunc: result.handleAddNetDef, - UpdateFunc: result.handleUpdateNetDef, - DeleteFunc: result.handleDeleteNetDef, - }, resyncPeriod, - ) - - return result -} - -// RegisterEventHandler registers a handler which is called on every netdef change. -func (c *NetDefConfig) RegisterEventHandler(handler NetDefHandler) { - c.eventHandlers = append(c.eventHandlers, handler) -} - -// Run ... -func (c *NetDefConfig) Run(stopCh <-chan struct{}) { - klog.Info("Starting net-attach-def config controller") - - if !cache.WaitForNamedCacheSync("net-attach-def config", stopCh, c.listerSynced) { - return - } - - for i := range c.eventHandlers { - klog.V(10).Infof("Calling handler.OnNetDefSynced()") - c.eventHandlers[i].OnNetDefSynced() - } -} - -func (c *NetDefConfig) handleAddNetDef(obj interface{}) { - netdef, ok := obj.(*netdefv1.NetworkAttachmentDefinition) - if !ok { - utilruntime.HandleError(fmt.Errorf("unexpected object type: %v", obj)) - return - } - - for i := range c.eventHandlers { - klog.V(10).Infof("Calling handler.OnNetDefAdd") - c.eventHandlers[i].OnNetDefAdd(netdef) - } -} - -func (c *NetDefConfig) handleUpdateNetDef(oldObj, newObj interface{}) { - oldNetDef, ok := oldObj.(*netdefv1.NetworkAttachmentDefinition) - if !ok { - utilruntime.HandleError(fmt.Errorf("unexpected object type: %v", oldObj)) - return - } - netdef, ok := newObj.(*netdefv1.NetworkAttachmentDefinition) - if !ok { - utilruntime.HandleError(fmt.Errorf("unexpected object type: %v", newObj)) - return - } - for i := range c.eventHandlers { - klog.V(10).Infof("Calling handler.OnNetDefUpdate") - c.eventHandlers[i].OnNetDefUpdate(oldNetDef, netdef) - } -} - -func (c *NetDefConfig) handleDeleteNetDef(obj interface{}) { - netdef, ok := obj.(*netdefv1.NetworkAttachmentDefinition) - if !ok { - tombstone, ok := obj.(cache.DeletedFinalStateUnknown) - if !ok { - utilruntime.HandleError(fmt.Errorf("unexpected object type: %v", obj)) - } - if netdef, ok = tombstone.Obj.(*netdefv1.NetworkAttachmentDefinition); !ok { - utilruntime.HandleError(fmt.Errorf("unexpected object type: %v", obj)) - return - } - } - for i := range c.eventHandlers { - klog.V(10).Infof("Calling handler.OnNetDefDelete") - c.eventHandlers[i].OnNetDefDelete(netdef) - } -} - -// NetDefInfo contains information that defines a object. -type NetDefInfo struct { - Netdef *netdefv1.NetworkAttachmentDefinition - PluginType string -} - -// Name ... -func (info *NetDefInfo) Name() string { - return info.Netdef.ObjectMeta.Name -} - -// NetDefMap ... -type NetDefMap map[types.NamespacedName]NetDefInfo - -// Update ... -func (n *NetDefMap) Update(changes *NetDefChangeTracker) { - if n != nil { - n.apply(changes) - } -} - -func (n *NetDefMap) apply(changes *NetDefChangeTracker) { - if n == nil || changes == nil { - return - } - - changes.lock.Lock() - defer changes.lock.Unlock() - for _, change := range changes.items { - n.unmerge(change.previous) - n.merge(change.current) - } - // clear changes after applying them to ServiceMap. - changes.items = make(map[types.NamespacedName]*netdefChange) - return -} - -func (n *NetDefMap) merge(other NetDefMap) { - for netDefName, info := range other { - (*n)[netDefName] = info - } -} - -func (n *NetDefMap) unmerge(other NetDefMap) { - for netDefName := range other { - delete(*n, netDefName) - } -} - -type netdefChange struct { - previous NetDefMap - current NetDefMap -} - -// NetDefChangeTracker ... -type NetDefChangeTracker struct { - // lock protects items. - lock sync.Mutex - // items maps a service to its netdefChange. - items map[types.NamespacedName]*netdefChange - netdefMap NetDefMap -} - -// String ... -func (ndt *NetDefChangeTracker) String() string { - return fmt.Sprintf("netdefChange: %v", ndt.items) -} - -// GetPluginType ... -func (ndt *NetDefChangeTracker) GetPluginType(name types.NamespacedName) string { - ndt.netdefMap.Update(ndt) - if cur, ok := ndt.netdefMap[name]; ok { - return cur.PluginType - } - return "" -} - -func (ndt *NetDefChangeTracker) newNetDefInfo(netdef *netdefv1.NetworkAttachmentDefinition) (*NetDefInfo, error) { - confBytes, err := netdefutils.GetCNIConfig(netdef, "/etc/cni/multus/net.d") - if err != nil { - return nil, err - } - - netconfList := &cnitypes.NetConfList{} - if err := json.Unmarshal(confBytes, netconfList); err != nil { - return nil, err - } - - var info *NetDefInfo - if len(netconfList.Plugins) == 0 { - netconf := &cnitypes.NetConf{} - if err := json.Unmarshal(confBytes, netconf); err != nil { - return nil, err - } - - info = &NetDefInfo{ - Netdef: netdef, - PluginType: netconf.Type, - } - } else { - info = &NetDefInfo{ - Netdef: netdef, - PluginType: netconfList.Plugins[0].Type, - } - } - return info, nil -} - -func (ndt *NetDefChangeTracker) netDefToNetDefMap(netdef *netdefv1.NetworkAttachmentDefinition) NetDefMap { - if netdef == nil { - return nil - } - netdefMap := make(NetDefMap) - netdefInfo, err := ndt.newNetDefInfo(netdef) - if err != nil { - klog.Errorf("err: %v\n", err) - return nil - } - //XXX: need to revisit (why we need map?, just netdefInfo might be okey?) - netdefMap[types.NamespacedName{Namespace: netdef.Namespace, Name: netdef.Name}] = *netdefInfo - return netdefMap -} - -// Update ... -func (ndt *NetDefChangeTracker) Update(previous, current *netdefv1.NetworkAttachmentDefinition) bool { - netdef := current - - if ndt == nil { - return false - } - if netdef == nil { - netdef = previous - } - if netdef == nil { - return false - } - - namespacedName := types.NamespacedName{Namespace: netdef.Namespace, Name: netdef.Name} - - ndt.lock.Lock() - defer ndt.lock.Unlock() - - change, exists := ndt.items[namespacedName] - if !exists { - change = &netdefChange{} - prevNetDefMap := ndt.netDefToNetDefMap(previous) - change.previous = prevNetDefMap - ndt.items[namespacedName] = change - } - - curNetDefMap := ndt.netDefToNetDefMap(current) - change.current = curNetDefMap - if reflect.DeepEqual(change.previous, change.current) { - delete(ndt.items, namespacedName) - } - - return len(ndt.items) >= 0 -} - -// NewNetDefChangeTracker ... -func NewNetDefChangeTracker() *NetDefChangeTracker { - return &NetDefChangeTracker{ - items: make(map[types.NamespacedName]*netdefChange), - netdefMap: make(NetDefMap), - } -} diff --git a/pkg/controllers/net-attach-def_test.go b/pkg/controllers/net-attach-def_test.go deleted file mode 100644 index 01a3afbb..00000000 --- a/pkg/controllers/net-attach-def_test.go +++ /dev/null @@ -1,194 +0,0 @@ -/* -Copyright 2020 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package controllers - -import ( - "fmt" - "time" - - netdefv1 "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/apis/k8s.cni.cncf.io/v1" - netdeffake "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/client/clientset/versioned/fake" - netdefinformerv1 "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/client/informers/externalversions" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - types "k8s.io/apimachinery/pkg/types" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -type FakeNetDefConfigStub struct { - CounterAdd int - CounterUpdate int - CounterDelete int - CounterSynced int -} - -func (f *FakeNetDefConfigStub) OnNetDefAdd(_ *netdefv1.NetworkAttachmentDefinition) { - f.CounterAdd++ -} - -func (f *FakeNetDefConfigStub) OnNetDefUpdate(_, _ *netdefv1.NetworkAttachmentDefinition) { - f.CounterUpdate++ -} - -func (f *FakeNetDefConfigStub) OnNetDefDelete(_ *netdefv1.NetworkAttachmentDefinition) { - f.CounterDelete++ -} - -func (f *FakeNetDefConfigStub) OnNetDefSynced() { - f.CounterSynced++ -} - -func NewFakeNetDefConfig(stub *FakeNetDefConfigStub) *NetDefConfig { - configSync := 15 * time.Minute - fakeClient := netdeffake.NewSimpleClientset() - netdefInformarFactory := netdefinformerv1.NewSharedInformerFactoryWithOptions(fakeClient, configSync) - netdefConfig := NewNetDefConfig(netdefInformarFactory.K8sCniCncfIo().V1().NetworkAttachmentDefinitions(), configSync) - netdefConfig.RegisterEventHandler(stub) - return netdefConfig -} - -func NewNetDef(namespace, name, cniConfig string) *netdefv1.NetworkAttachmentDefinition { - return &netdefv1.NetworkAttachmentDefinition{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: namespace, - Name: name, - }, - Spec: netdefv1.NetworkAttachmentDefinitionSpec{ - Config: cniConfig, - }, - } -} - -func NewCNIConfig(cniName, cniType string) string { - cniConfigTemp := ` - { - "name": "%s", - "type": "%s" - }` - return fmt.Sprintf(cniConfigTemp, cniName, cniType) -} - -func NewCNIConfigList(cniName, cniType string) string { - cniConfigTemp := ` - { - "name": "%s", - "plugins": [ - { - "type": "%s" - }] - }` - return fmt.Sprintf(cniConfigTemp, cniName, cniType) -} - -var _ = Describe("net-attach-def config", func() { - It("check add handler", func() { - stub := &FakeNetDefConfigStub{} - ndConfig := NewFakeNetDefConfig(stub) - ndConfig.handleAddNetDef(NewNetDef("testns1", "test1", NewCNIConfig("cniConfig1", "testType1"))) - Expect(stub.CounterAdd).To(Equal(1)) - Expect(stub.CounterUpdate).To(Equal(0)) - Expect(stub.CounterDelete).To(Equal(0)) - Expect(stub.CounterSynced).To(Equal(0)) - }) - - It("check update handler", func() { - stub := &FakeNetDefConfigStub{} - ndConfig := NewFakeNetDefConfig(stub) - ndConfig.handleUpdateNetDef( - NewNetDef("testns1", "test1", NewCNIConfig("cniConfig1", "testType1")), - NewNetDef("testns1", "test1", NewCNIConfig("cniConfig2", "testType2"))) - Expect(stub.CounterAdd).To(Equal(0)) - Expect(stub.CounterUpdate).To(Equal(1)) - Expect(stub.CounterDelete).To(Equal(0)) - Expect(stub.CounterSynced).To(Equal(0)) - }) - - It("check delete handler", func() { - stub := &FakeNetDefConfigStub{} - ndConfig := NewFakeNetDefConfig(stub) - ndConfig.handleDeleteNetDef(NewNetDef("testns", "test", NewCNIConfig("cniConfig1", "testType1"))) - Expect(stub.CounterAdd).To(Equal(0)) - Expect(stub.CounterUpdate).To(Equal(0)) - Expect(stub.CounterDelete).To(Equal(1)) - Expect(stub.CounterSynced).To(Equal(0)) - }) -}) - -var _ = Describe("net-attach-def controller", func() { - It("Initialize and verify empty", func() { - netDefChanges := NewNetDefChangeTracker() - ndMap := make(NetDefMap) - ndMap.Update(netDefChanges) - Expect(len(ndMap)).To(Equal(0)) - }) - - It("Add netdef and verify", func() { - ndChanges := NewNetDefChangeTracker() - ndChanges.Update(nil, NewNetDef("testns1", "test1", NewCNIConfig("cniConfig1", "testType1"))) - ndChanges.Update(nil, NewNetDef("testns2", "test2", NewCNIConfigList("cniConfig2", "testType2"))) - - ndMap := make(NetDefMap) - ndMap.Update(ndChanges) - Expect(len(ndMap)).To(Equal(2)) - ndTest1, ok := ndMap[types.NamespacedName{Namespace: "testns1", Name: "test1"}] - Expect(ok).To(BeTrue()) - Expect(ndTest1.Name()).To(Equal("test1")) - Expect(ndTest1.PluginType).To(Equal("testType1")) - - ndTest2, ok := ndMap[types.NamespacedName{Namespace: "testns2", Name: "test2"}] - Expect(ok).To(BeTrue()) - Expect(ndTest2.Name()).To(Equal("test2")) - Expect(ndTest2.PluginType).To(Equal("testType2")) - }) - - It("Add netdef then del it and verify", func() { - ndChanges := NewNetDefChangeTracker() - ndChanges.Update(nil, NewNetDef("testns1", "test1", NewCNIConfigList("cniConfig1", "testType1"))) - ndChanges.Update(nil, NewNetDef("testns1", "test2", NewCNIConfig("cniConfig2", "testType2"))) - ndChanges.Update(NewNetDef("testns1", "test2", NewCNIConfig("cniConfig2", "testType2")), nil) - - ndMap := make(NetDefMap) - ndMap.Update(ndChanges) - Expect(len(ndMap)).To(Equal(1)) - ndTest1, ok := ndMap[types.NamespacedName{Namespace: "testns1", Name: "test1"}] - Expect(ok).To(BeTrue()) - Expect(ndTest1.Name()).To(Equal("test1")) - Expect(ndTest1.PluginType).To(Equal("testType1")) - }) - - It("invalid Update case", func() { - ndChanges := NewNetDefChangeTracker() - Expect(ndChanges.Update(nil, nil)).To(BeFalse()) - }) - - It("Add netdef then update it and verify", func() { - ndChanges := NewNetDefChangeTracker() - ndChanges.Update(nil, NewNetDef("testns1", "test1", NewCNIConfig("cniConfig1", "testType1"))) - ndChanges.Update(NewNetDef("testns1", "test1", NewCNIConfig("cniConfig1", "testType1")), - NewNetDef("testns1", "test1", NewCNIConfigList("cniConfig2", "testType2"))) - - ndMap := make(NetDefMap) - ndMap.Update(ndChanges) - Expect(len(ndMap)).To(Equal(1)) - ndTest1, ok := ndMap[types.NamespacedName{Namespace: "testns1", Name: "test1"}] - Expect(ok).To(BeTrue()) - Expect(ndTest1.Name()).To(Equal("test1")) - Expect(ndTest1.PluginType).To(Equal("testType2")) - }) -}) diff --git a/pkg/controllers/networkpolicy.go b/pkg/controllers/networkpolicy.go deleted file mode 100644 index 0a1d17c8..00000000 --- a/pkg/controllers/networkpolicy.go +++ /dev/null @@ -1,289 +0,0 @@ -/* -Copyright 2020 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package controllers - -import ( - "fmt" - "reflect" - "sync" - "time" - - multiv1beta1 "github.com/k8snetworkplumbingwg/multi-networkpolicy/pkg/apis/k8s.cni.cncf.io/v1beta1" - multiinformerv1beta1 "github.com/k8snetworkplumbingwg/multi-networkpolicy/pkg/client/informers/externalversions/k8s.cni.cncf.io/v1beta1" - - "k8s.io/apimachinery/pkg/types" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" - "k8s.io/client-go/tools/cache" - "k8s.io/klog" -) - -// NetworkPolicyHandler is an abstract interface of objects which receive -// notifications about policy object changes. -type NetworkPolicyHandler interface { - // OnPolicyAdd is called whenever creation of new policy object - // is observed. - OnPolicyAdd(policy *multiv1beta1.MultiNetworkPolicy) - // OnPolicyUpdate is called whenever modification of an existing - // policy object is observed. - OnPolicyUpdate(oldPolicy, policy *multiv1beta1.MultiNetworkPolicy) - // OnPolicyDelete is called whenever deletion of an existing policy - // object is observed. - OnPolicyDelete(policy *multiv1beta1.MultiNetworkPolicy) - // OnPolicySynced is called once all the initial event handlers were - // called and the state is fully propagated to local cache. - OnPolicySynced() -} - -// NetworkPolicyConfig ... -type NetworkPolicyConfig struct { - listerSynced cache.InformerSynced - eventHandlers []NetworkPolicyHandler -} - -// NewNetworkPolicyConfig creates a new NetworkPolicyConfig . -func NewNetworkPolicyConfig(policyInformer multiinformerv1beta1.MultiNetworkPolicyInformer, resyncPeriod time.Duration) *NetworkPolicyConfig { - result := &NetworkPolicyConfig{ - listerSynced: policyInformer.Informer().HasSynced, - } - - policyInformer.Informer().AddEventHandlerWithResyncPeriod( - cache.ResourceEventHandlerFuncs{ - AddFunc: result.handleAddPolicy, - UpdateFunc: result.handleUpdatePolicy, - DeleteFunc: result.handleDeletePolicy, - }, resyncPeriod, - ) - - return result -} - -// RegisterEventHandler registers a handler which is called on every policy change. -func (c *NetworkPolicyConfig) RegisterEventHandler(handler NetworkPolicyHandler) { - c.eventHandlers = append(c.eventHandlers, handler) -} - -// Run ... -func (c *NetworkPolicyConfig) Run(stopCh <-chan struct{}) { - klog.Info("Starting policy config controller") - - if !cache.WaitForNamedCacheSync("policy config", stopCh, c.listerSynced) { - return - } - - for i := range c.eventHandlers { - klog.V(10).Infof("Calling handler.OnPolicySynced()") - c.eventHandlers[i].OnPolicySynced() - } -} - -func (c *NetworkPolicyConfig) handleAddPolicy(obj interface{}) { - policy, ok := obj.(*multiv1beta1.MultiNetworkPolicy) - if !ok { - utilruntime.HandleError(fmt.Errorf("unexpected object type: %v", obj)) - return - } - - for i := range c.eventHandlers { - klog.V(10).Infof("Calling handler.OnPolicyAdd") - c.eventHandlers[i].OnPolicyAdd(policy) - } -} - -func (c *NetworkPolicyConfig) handleUpdatePolicy(oldObj, newObj interface{}) { - oldPolicy, ok := oldObj.(*multiv1beta1.MultiNetworkPolicy) - if !ok { - utilruntime.HandleError(fmt.Errorf("unexpected object type: %v", oldObj)) - return - } - policy, ok := newObj.(*multiv1beta1.MultiNetworkPolicy) - if !ok { - utilruntime.HandleError(fmt.Errorf("unexpected object type: %v", newObj)) - return - } - for i := range c.eventHandlers { - klog.V(10).Infof("Calling handler.OnPolicyUpdate") - c.eventHandlers[i].OnPolicyUpdate(oldPolicy, policy) - } -} - -func (c *NetworkPolicyConfig) handleDeletePolicy(obj interface{}) { - policy, ok := obj.(*multiv1beta1.MultiNetworkPolicy) - if !ok { - tombstone, ok := obj.(cache.DeletedFinalStateUnknown) - if !ok { - utilruntime.HandleError(fmt.Errorf("unexpected object type: %v", obj)) - } - if policy, ok = tombstone.Obj.(*multiv1beta1.MultiNetworkPolicy); !ok { - utilruntime.HandleError(fmt.Errorf("unexpected object type: %v", obj)) - return - } - } - for i := range c.eventHandlers { - klog.V(10).Infof("Calling handler.OnPolicyDelete") - c.eventHandlers[i].OnPolicyDelete(policy) - } -} - -// PolicyInfo contains information that defines a policy. -type PolicyInfo struct { - Policy *multiv1beta1.MultiNetworkPolicy -} - -// Name ... -func (info *PolicyInfo) Name() string { - return info.Policy.ObjectMeta.Name -} - -// Namespace ... -func (info *PolicyInfo) Namespace() string { - return info.Policy.ObjectMeta.Namespace -} - -// PolicyMap ... -type PolicyMap map[types.NamespacedName]PolicyInfo - -// Update ... -func (pm *PolicyMap) Update(changes *PolicyChangeTracker) { - if pm != nil { - pm.apply(changes) - } -} - -func (pm *PolicyMap) apply(changes *PolicyChangeTracker) { - if pm == nil || changes == nil { - return - } - - changes.lock.Lock() - defer changes.lock.Unlock() - for _, change := range changes.items { - pm.unmerge(change.previous) - pm.merge(change.current) - } - // clear changes after applying them to ServiceMap. - changes.items = make(map[types.NamespacedName]*policyChange) - return -} - -func (pm *PolicyMap) merge(other PolicyMap) { - for policyName, info := range other { - (*pm)[policyName] = info - } -} - -func (pm *PolicyMap) unmerge(other PolicyMap) { - for policyName := range other { - delete(*pm, policyName) - } -} - -//XXX: for debug, to be removed -/* -func (pm *PolicyMap)String() string { - if pm == nil { - return "" - } - str := "" - for _, v := range *pm { - str = fmt.Sprintf("%s\n\tpod: %s", str, v.Name()) - } - return str -} -*/ - -type policyChange struct { - previous PolicyMap - current PolicyMap -} - -// PolicyChangeTracker ... -type PolicyChangeTracker struct { - // lock protects items. - lock sync.Mutex - // items maps a service to its serviceChange. - items map[types.NamespacedName]*policyChange -} - -// String ... -func (pct *PolicyChangeTracker) String() string { - return fmt.Sprintf("policyChange: %v", pct.items) -} - -func (pct *PolicyChangeTracker) newPolicyInfo(policy *multiv1beta1.MultiNetworkPolicy) (*PolicyInfo, error) { - info := &PolicyInfo{ - Policy: policy, - } - return info, nil -} - -func (pct *PolicyChangeTracker) policyToPolicyMap(policy *multiv1beta1.MultiNetworkPolicy) PolicyMap { - if policy == nil { - return nil - } - policyMap := make(PolicyMap) - policyInfo, err := pct.newPolicyInfo(policy) - if err != nil { - return nil - } - - policyMap[types.NamespacedName{Namespace: policy.Namespace, Name: policy.Name}] = *policyInfo - return policyMap -} - -// Update ... -func (pct *PolicyChangeTracker) Update(previous, current *multiv1beta1.MultiNetworkPolicy) bool { - policy := current - - if pct == nil { - return false - } - - if policy == nil { - policy = previous - } - if policy == nil { - return false - } - - namespacedName := types.NamespacedName{Namespace: policy.Namespace, Name: policy.Name} - - pct.lock.Lock() - defer pct.lock.Unlock() - - change, exists := pct.items[namespacedName] - if !exists { - change = &policyChange{} - prevPolicyMap := pct.policyToPolicyMap(previous) - change.previous = prevPolicyMap - pct.items[namespacedName] = change - } - - curPolicyMap := pct.policyToPolicyMap(current) - change.current = curPolicyMap - if reflect.DeepEqual(change.previous, change.current) { - delete(pct.items, namespacedName) - } - - return len(pct.items) >= 0 -} - -// NewPolicyChangeTracker ... -func NewPolicyChangeTracker() *PolicyChangeTracker { - return &PolicyChangeTracker{ - items: make(map[types.NamespacedName]*policyChange), - } -} diff --git a/pkg/controllers/networkpolicy_test.go b/pkg/controllers/networkpolicy_test.go deleted file mode 100644 index 8c582531..00000000 --- a/pkg/controllers/networkpolicy_test.go +++ /dev/null @@ -1,173 +0,0 @@ -/* -Copyright 2020 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package controllers - -import ( - //"fmt" - "time" - - multiv1beta1 "github.com/k8snetworkplumbingwg/multi-networkpolicy/pkg/apis/k8s.cni.cncf.io/v1beta1" - multifake "github.com/k8snetworkplumbingwg/multi-networkpolicy/pkg/client/clientset/versioned/fake" - multiinformerv1beta1 "github.com/k8snetworkplumbingwg/multi-networkpolicy/pkg/client/informers/externalversions" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - types "k8s.io/apimachinery/pkg/types" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -type FakeNetworkPolicyConfigStub struct { - CounterAdd int - CounterUpdate int - CounterDelete int - CounterSynced int -} - -func (f *FakeNetworkPolicyConfigStub) OnPolicyAdd(_ *multiv1beta1.MultiNetworkPolicy) { - f.CounterAdd++ -} - -func (f *FakeNetworkPolicyConfigStub) OnPolicyUpdate(_, _ *multiv1beta1.MultiNetworkPolicy) { - f.CounterUpdate++ -} - -func (f *FakeNetworkPolicyConfigStub) OnPolicyDelete(_ *multiv1beta1.MultiNetworkPolicy) { - f.CounterDelete++ -} - -func (f *FakeNetworkPolicyConfigStub) OnPolicySynced() { - f.CounterSynced++ -} - -func NewFakeNetworkPolicyConfig(stub *FakeNetworkPolicyConfigStub) *NetworkPolicyConfig { - configSync := 15 * time.Minute - fakeClient := multifake.NewSimpleClientset() - informerFactory := multiinformerv1beta1.NewSharedInformerFactoryWithOptions(fakeClient, configSync) - policyConfig := NewNetworkPolicyConfig(informerFactory.K8sCniCncfIo().V1beta1().MultiNetworkPolicies(), configSync) - policyConfig.RegisterEventHandler(stub) - return policyConfig -} - -func NewNetworkPolicy(namespace, name string) *multiv1beta1.MultiNetworkPolicy { - return &multiv1beta1.MultiNetworkPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: namespace, - Name: name, - }, - } -} - -var _ = Describe("networkpolicy config", func() { - It("check add handler", func() { - stub := &FakeNetworkPolicyConfigStub{} - networkPolicyConfig := NewFakeNetworkPolicyConfig(stub) - networkPolicyConfig.handleAddPolicy(NewNetworkPolicy("testns1", "test1")) - Expect(stub.CounterAdd).To(Equal(1)) - Expect(stub.CounterUpdate).To(Equal(0)) - Expect(stub.CounterDelete).To(Equal(0)) - Expect(stub.CounterSynced).To(Equal(0)) - }) - - It("check update handler", func() { - stub := &FakeNetworkPolicyConfigStub{} - networkPolicyConfig := NewFakeNetworkPolicyConfig(stub) - networkPolicyConfig.handleUpdatePolicy( - NewNetworkPolicy("testns1", "test1"), - NewNetworkPolicy("testns2", "test1")) - Expect(stub.CounterAdd).To(Equal(0)) - Expect(stub.CounterUpdate).To(Equal(1)) - Expect(stub.CounterDelete).To(Equal(0)) - Expect(stub.CounterSynced).To(Equal(0)) - }) - - It("check delete handler", func() { - stub := &FakeNetworkPolicyConfigStub{} - networkPolicyConfig := NewFakeNetworkPolicyConfig(stub) - networkPolicyConfig.handleDeletePolicy(NewNetworkPolicy("testns1", "test1")) - Expect(stub.CounterAdd).To(Equal(0)) - Expect(stub.CounterUpdate).To(Equal(0)) - Expect(stub.CounterDelete).To(Equal(1)) - Expect(stub.CounterSynced).To(Equal(0)) - }) -}) - -var _ = Describe("networkpolicy controller", func() { - It("Initialize and verify empty", func() { - policyChanges := NewPolicyChangeTracker() - policyMap := make(PolicyMap) - policyMap.Update(policyChanges) - Expect(len(policyMap)).To(Equal(0)) - }) - - It("Add policy and verify", func() { - policyChanges := NewPolicyChangeTracker() - policyChanges.Update(nil, NewNetworkPolicy("testns1", "test1")) - policyChanges.Update(nil, NewNetworkPolicy("testns2", "test2")) - - policyMap := make(PolicyMap) - policyMap.Update(policyChanges) - Expect(len(policyMap)).To(Equal(2)) - - policyTest1, ok := policyMap[types.NamespacedName{Namespace: "testns1", Name: "test1"}] - Expect(ok).To(BeTrue()) - Expect(policyTest1.Name()).To(Equal("test1")) - Expect(policyTest1.Namespace()).To(Equal("testns1")) - policyTest2, ok := policyMap[types.NamespacedName{Namespace: "testns2", Name: "test2"}] - Expect(ok).To(BeTrue()) - Expect(policyTest2.Name()).To(Equal("test2")) - Expect(policyTest2.Namespace()).To(Equal("testns2")) - }) - - It("Add policy then delete it and verify", func() { - policyChanges := NewPolicyChangeTracker() - policyChanges.Update(nil, NewNetworkPolicy("testns1", "test1")) - policyChanges.Update(nil, NewNetworkPolicy("testns2", "test2")) - policyChanges.Update(NewNetworkPolicy("testns1", "test1"), nil) - - policyMap := make(PolicyMap) - policyMap.Update(policyChanges) - Expect(len(policyMap)).To(Equal(1)) - - policyTest2, ok := policyMap[types.NamespacedName{Namespace: "testns2", Name: "test2"}] - Expect(ok).To(BeTrue()) - Expect(policyTest2.Name()).To(Equal("test2")) - Expect(policyTest2.Namespace()).To(Equal("testns2")) - }) - - It("invalid Update case", func() { - policyChanges := NewPolicyChangeTracker() - Expect(policyChanges.Update(nil, nil)).To(BeFalse()) - }) - - It("Add policy then update it and verify", func() { - policyChanges := NewPolicyChangeTracker() - policyChanges.Update(nil, NewNetworkPolicy("testns1", "test1")) - policyChanges.Update( - NewNetworkPolicy("testns1", "test1"), - NewNetworkPolicy("testns1", "test1")) - - policyMap := make(PolicyMap) - policyMap.Update(policyChanges) - Expect(len(policyMap)).To(Equal(1)) - - policyTest1, ok := policyMap[types.NamespacedName{Namespace: "testns1", Name: "test1"}] - Expect(ok).To(BeTrue()) - Expect(policyTest1.Name()).To(Equal("test1")) - Expect(policyTest1.Namespace()).To(Equal("testns1")) - }) -}) diff --git a/pkg/controllers/pod.go b/pkg/controllers/pod.go deleted file mode 100644 index c8d80e46..00000000 --- a/pkg/controllers/pod.go +++ /dev/null @@ -1,574 +0,0 @@ -/* -Copyright 2020 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package controllers - -import ( - "context" - "encoding/json" - "fmt" - "reflect" - "strings" - "sync" - "time" - - multiutils "github.com/k8snetworkplumbingwg/multi-networkpolicy-iptables/pkg/utils" - netdefv1 "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/apis/k8s.cni.cncf.io/v1" - netdefutils "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/utils" - - "google.golang.org/grpc" - - v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/types" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" - coreinformers "k8s.io/client-go/informers/core/v1" - "k8s.io/client-go/tools/cache" - pb "k8s.io/cri-api/pkg/apis/runtime/v1" - k8sutils "k8s.io/cri-client/pkg/util" - "k8s.io/klog" -) - -// RuntimeKind is enum type variable for container runtime -type RuntimeKind string - -const ( - // Cri based runtime (e.g. cri-o) - Cri = "cri" -) - -// Set specifies container runtime kind -func (rk *RuntimeKind) Set(s string) error { - runtime := strings.ToLower(s) - switch runtime { - case Cri: - *rk = RuntimeKind(runtime) - return nil - } - return fmt.Errorf("Invalid container-runtime option %s (possible values: \"cri\")", s) -} - -// String returns current runtime kind -func (rk RuntimeKind) String() string { - return string(rk) -} - -// Type returns its type, "RuntimeKind" -func (rk RuntimeKind) Type() string { - return "RuntimeKind" -} - -// PodHandler is an abstract interface of objects which receive -// notifications about pod object changes. -type PodHandler interface { - // OnPodAdd is called whenever creation of new pod object - // is observed. - OnPodAdd(pod *v1.Pod) - // OnPodUpdate is called whenever modification of an existing - // pod object is observed. - OnPodUpdate(oldPod, pod *v1.Pod) - // OnPodDelete is called whenever deletion of an existing pod - // object is observed. - OnPodDelete(pod *v1.Pod) - // OnPodSynced is called once all the initial event handlers were - // called and the state is fully propagated to local cache. - OnPodSynced() -} - -// PodConfig ... -type PodConfig struct { - listerSynced cache.InformerSynced - eventHandlers []PodHandler -} - -// NewPodConfig creates a new PodConfig. -func NewPodConfig(podInformer coreinformers.PodInformer, resyncPeriod time.Duration) *PodConfig { - result := &PodConfig{ - listerSynced: podInformer.Informer().HasSynced, - } - - podInformer.Informer().AddEventHandlerWithResyncPeriod( - cache.ResourceEventHandlerFuncs{ - AddFunc: result.handleAddPod, - UpdateFunc: result.handleUpdatePod, - DeleteFunc: result.handleDeletePod, - }, - resyncPeriod, - ) - return result -} - -// RegisterEventHandler registers a handler which is called on every pod change. -func (c *PodConfig) RegisterEventHandler(handler PodHandler) { - c.eventHandlers = append(c.eventHandlers, handler) -} - -// Run waits for cache synced and invokes handlers after syncing. -func (c *PodConfig) Run(stopCh <-chan struct{}) { - klog.Info("Starting pod config controller") - - if !cache.WaitForNamedCacheSync("pod config", stopCh, c.listerSynced) { - return - } - - for i := range c.eventHandlers { - klog.V(10).Infof("Calling handler.OnPodSynced()") - c.eventHandlers[i].OnPodSynced() - } -} - -func (c *PodConfig) handleAddPod(obj interface{}) { - pod, ok := obj.(*v1.Pod) - if !ok { - utilruntime.HandleError(fmt.Errorf("unexpected object type: %v", obj)) - return - } - - for i := range c.eventHandlers { - klog.V(10).Infof("Calling handler.OnPodAdd") - c.eventHandlers[i].OnPodAdd(pod) - } -} - -func (c *PodConfig) handleUpdatePod(oldObj, newObj interface{}) { - oldPod, ok := oldObj.(*v1.Pod) - if !ok { - utilruntime.HandleError(fmt.Errorf("unexpected object type: %v", oldObj)) - return - } - pod, ok := newObj.(*v1.Pod) - if !ok { - utilruntime.HandleError(fmt.Errorf("unexpected object type: %v", newObj)) - return - } - for i := range c.eventHandlers { - klog.V(10).Infof("Calling handler.OnPodUpdate") - c.eventHandlers[i].OnPodUpdate(oldPod, pod) - } -} - -func (c *PodConfig) handleDeletePod(obj interface{}) { - pod, ok := obj.(*v1.Pod) - if !ok { - tombstone, ok := obj.(cache.DeletedFinalStateUnknown) - if !ok { - utilruntime.HandleError(fmt.Errorf("unexpected object type: %v", obj)) - } - if pod, ok = tombstone.Obj.(*v1.Pod); !ok { - utilruntime.HandleError(fmt.Errorf("unexpected object type: %v", obj)) - return - } - } - for i := range c.eventHandlers { - klog.V(10).Infof("Calling handler.OnPodDelete") - c.eventHandlers[i].OnPodDelete(pod) - } -} - -// InterfaceInfo ... -type InterfaceInfo struct { - NetattachName string - InterfaceName string - InterfaceType string - IPs []string -} - -// CheckPolicyNetwork checks whether given interface is target or not, -// based on policyNetworks -func (info *InterfaceInfo) CheckPolicyNetwork(policyNetworks []string) bool { - for _, policyNetworkName := range policyNetworks { - if policyNetworkName == info.NetattachName { - return true - } - } - return false -} - -// PodInfo contains information that defines a pod. -type PodInfo struct { - Name string - Namespace string - NetNSPath string - NetworkStatus []netdefv1.NetworkStatus - NodeName string - Interfaces []InterfaceInfo -} - -// CheckPolicyNetwork checks whether given pod is target or not, -// based on policyNetworks -func (info *PodInfo) CheckPolicyNetwork(policyNetworks []string) bool { - for _, intf := range info.Interfaces { - for _, policyNetworkName := range policyNetworks { - if policyNetworkName == intf.NetattachName { - return true - } - } - } - return false -} - -// GetMultusNetIFs ... -func (info *PodInfo) GetMultusNetIFs() []string { - results := []string{} - - if info != nil && len(info.NetworkStatus) > 0 { - for _, status := range info.NetworkStatus[1:] { - results = append(results, status.Interface) - } - } - return results -} - -// String ... -func (info *PodInfo) String() string { - return fmt.Sprintf("pod:%s", info.Name) -} - -type podChange struct { - previous PodMap - current PodMap -} - -// PodChangeTracker carries state about uncommitted changes to an arbitrary number of -// Pods in the node, keyed by their namespace and name -type PodChangeTracker struct { - // lock protects items. - lock sync.Mutex - hostname string - networkPlugins []string - netdefChanges *NetDefChangeTracker - // items maps a service to its podChange. - items map[types.NamespacedName]*podChange - - // for cri - criClient pb.RuntimeServiceClient - criConn *grpc.ClientConn -} - -// String -func (pct *PodChangeTracker) String() string { - return fmt.Sprintf("podChange: %v", pct.items) -} - -func (pct *PodChangeTracker) getPodNetNSPath(pod *v1.Pod) (string, error) { - netnsPath := "" - - if pod.Status.Phase != v1.PodRunning { - return "", fmt.Errorf("Pod is not running") - } - - // get Container netns - procPrefix := "" - if len(pod.Status.ContainerStatuses) == 0 { - return "", fmt.Errorf("No container status") - } - - containerURI := strings.Split(pod.Status.ContainerStatuses[0].ContainerID, "://") - if len(containerURI) < 2 { - return "", fmt.Errorf("No container ID (%s)", pod.Status.ContainerStatuses[0].ContainerID) - } - - runtimeKind := containerURI[0] - containerID := containerURI[1] - switch runtimeKind { - default: - if pct.criConn == nil { - return "", fmt.Errorf("cannot find cri client") - } - if len(containerID) > 0 { - request := &pb.ContainerStatusRequest{ - ContainerId: containerID, - Verbose: true, - } - r, err := pct.criClient.ContainerStatus(context.TODO(), request) - if err != nil { - return "", fmt.Errorf("cannot get containerStatus: %v", err) - } - - info := r.GetInfo() - var infop interface{} - json.Unmarshal([]byte(info["info"]), &infop) - pid, ok := infop.(map[string]interface{})["pid"].(float64) - if !ok { - return "", fmt.Errorf("cannot get pid from containerStatus info") - } - netnsPath = fmt.Sprintf("%s/proc/%d/ns/net", procPrefix, int(pid)) - } - } - - return netnsPath, nil -} - -// IsMultiNetworkpolicyTarget ... -func IsMultiNetworkpolicyTarget(pod *v1.Pod) bool { - if pod.Status.Phase != v1.PodRunning { - return false - } - - if pod.Spec.HostNetwork { - return false - } - return true -} - -func (pct *PodChangeTracker) newPodInfo(pod *v1.Pod) (*PodInfo, error) { - var statuses []netdefv1.NetworkStatus - var netnsPath string - var netifs []InterfaceInfo - // get network information only if the pod is ready - klog.V(8).Infof("pod:%s/%s %s/%s", pod.Namespace, pod.Name, pct.hostname, pod.Spec.NodeName) - if IsMultiNetworkpolicyTarget(pod) { - networks, err := netdefutils.ParsePodNetworkAnnotation(pod) - if err != nil { - if _, ok := err.(*netdefv1.NoK8sNetworkError); !ok { - klog.Errorf("failed to get pod network annotation: %v", err) - } - } - // parse networkStatus - statuses, _ = netdefutils.GetNetworkStatus(pod) - klog.V(1).Infof("pod:%s/%s %s/%s", pod.Namespace, pod.Name, pct.hostname, pod.Spec.NodeName) - - // get container network namespace - netnsPath = "" - if multiutils.CheckNodeNameIdentical(pct.hostname, pod.Spec.NodeName) { - netnsPath, err = pct.getPodNetNSPath(pod) - if err != nil { - klog.Errorf("failed to get pod(%s/%s) network namespace: %v", pod.Namespace, pod.Name, err) - } - klog.V(8).Infof("NetnsPath: %s", netnsPath) - } - - // netdefname -> plugin name map - networkPlugins := make(map[types.NamespacedName]string) - if networks == nil { - klog.V(8).Infof("%s/%s: NO NET", pod.Namespace, pod.Name) - } else { - klog.V(8).Infof("%s/%s: net: %v", pod.Namespace, pod.Name, networks) - } - for _, n := range networks { - namespace := pod.Namespace - if n.Namespace != "" { - namespace = n.Namespace - } - namespacedName := types.NamespacedName{Namespace: namespace, Name: n.Name} - klog.V(8).Infof("networkPlugins[%s], %v", namespacedName, pct.netdefChanges.GetPluginType(namespacedName)) - networkPlugins[namespacedName] = pct.netdefChanges.GetPluginType(namespacedName) - } - klog.V(8).Infof("netdef->pluginMap: %v", networkPlugins) - - // match it with - for _, s := range statuses { - var netNamespace, netName string - slashItems := strings.Split(s.Name, "/") - if len(slashItems) == 2 { - netNamespace = strings.TrimSpace(slashItems[0]) - netName = slashItems[1] - } else { - netNamespace = pod.ObjectMeta.Namespace - netName = s.Name - } - namespacedName := types.NamespacedName{Namespace: netNamespace, Name: netName} - - for _, pluginName := range pct.networkPlugins { - if networkPlugins[namespacedName] == pluginName { - netifs = append(netifs, InterfaceInfo{ - NetattachName: s.Name, - InterfaceName: s.Interface, - InterfaceType: networkPlugins[namespacedName], - IPs: s.IPs, - }) - } - } - } - - klog.V(6).Infof("Pod: %s/%s netns:%s netIF:%v", pod.ObjectMeta.Namespace, pod.ObjectMeta.Name, netnsPath, netifs) - } else { - klog.V(1).Infof("Pod:%s/%s %s/%s, not ready", pod.Namespace, pod.Name, pct.hostname, pod.Spec.NodeName) - } - info := &PodInfo{ - Name: pod.ObjectMeta.Name, - Namespace: pod.ObjectMeta.Namespace, - NetworkStatus: statuses, - NetNSPath: netnsPath, - NodeName: pod.Spec.NodeName, - Interfaces: netifs, - } - return info, nil -} - -// NewPodChangeTracker ... -func NewPodChangeTracker(runtime RuntimeKind, runtimeEndpoint, hostname, hostPrefix string, networkPlugins []string, ndt *NetDefChangeTracker) *PodChangeTracker { - switch runtime { - case Cri: - return NewPodChangeTrackerCri(runtimeEndpoint, hostname, hostPrefix, networkPlugins, ndt) - default: - klog.Errorf("unknown container runtime: %v", runtime) - return nil - } -} - -// NewPodChangeTrackerCri ... -func NewPodChangeTrackerCri(runtimeEndpoint, hostname, hostPrefix string, networkPlugins []string, ndt *NetDefChangeTracker) *PodChangeTracker { - criClient, criConn, err := GetCriRuntimeClient(runtimeEndpoint, hostPrefix) - if err != nil { - klog.Errorf("failed to get cri client: %v", err) - return nil - } - - return &PodChangeTracker{ - items: make(map[types.NamespacedName]*podChange), - hostname: hostname, - networkPlugins: networkPlugins, - netdefChanges: ndt, - criClient: criClient, - criConn: criConn, - } -} - -func (pct *PodChangeTracker) podToPodMap(pod *v1.Pod) PodMap { - if pod == nil { - return nil - } - - podMap := make(PodMap) - podinfo, err := pct.newPodInfo(pod) - if err != nil { - return nil - } - - podMap[types.NamespacedName{Namespace: pod.Namespace, Name: pod.Name}] = *podinfo - return podMap -} - -// Update ... -func (pct *PodChangeTracker) Update(previous, current *v1.Pod) bool { - pod := current - - if pct == nil { - return false - } - - if pod == nil { - pod = previous - } - if pod == nil { - return false - } - namespacedName := types.NamespacedName{Namespace: pod.Namespace, Name: pod.Name} - - pct.lock.Lock() - defer pct.lock.Unlock() - - change, exists := pct.items[namespacedName] - if !exists { - change = &podChange{} - prevPodMap := pct.podToPodMap(previous) - change.previous = prevPodMap - pct.items[namespacedName] = change - } - curPodMap := pct.podToPodMap(current) - change.current = curPodMap - if reflect.DeepEqual(change.previous, change.current) { - delete(pct.items, namespacedName) - } - return len(pct.items) >= 0 -} - -// PodMap ... -type PodMap map[types.NamespacedName]PodInfo - -// Update updates podMap base on the given changes -func (pm *PodMap) Update(changes *PodChangeTracker) { - if pm != nil { - pm.apply(changes) - } -} - -func (pm *PodMap) apply(changes *PodChangeTracker) { - if pm == nil || changes == nil { - return - } - - changes.lock.Lock() - defer changes.lock.Unlock() - for _, change := range changes.items { - pm.unmerge(change.previous) - pm.merge(change.current) - } - // clear changes after applying them to ServiceMap. - changes.items = make(map[types.NamespacedName]*podChange) - return -} - -func (pm *PodMap) merge(other PodMap) { - for podName, info := range other { - (*pm)[podName] = info - } -} - -func (pm *PodMap) unmerge(other PodMap) { - for podName := range other { - delete(*pm, podName) - } -} - -// GetPodInfo ... -func (pm *PodMap) GetPodInfo(pod *v1.Pod) (*PodInfo, error) { - namespacedName := types.NamespacedName{Namespace: pod.Namespace, Name: pod.Name} - - podInfo, ok := (*pm)[namespacedName] - if ok { - return &podInfo, nil - } - - return nil, fmt.Errorf("not found") -} - -// ===================================== -// misc functions... -// ===================================== -func getRuntimeClientConnection(runtimeEndpoint, hostPrefix string) (*grpc.ClientConn, error) { - HostRuntimeEndpoint := fmt.Sprintf("unix://%s%s", hostPrefix, runtimeEndpoint) - addr, dialer, err := k8sutils.GetAddressAndDialer(HostRuntimeEndpoint) - if err != nil { - return nil, err - } - - Timeout := 10 * time.Second - conn, err := grpc.Dial(addr, grpc.WithInsecure(), grpc.WithBlock(), grpc.WithTimeout(Timeout), grpc.WithContextDialer(dialer)) - if err != nil { - return nil, fmt.Errorf("failed to connect to %s, make sure you are running as root and the runtime has been started: %v", HostRuntimeEndpoint, err) - } - return conn, nil -} - -// GetCriRuntimeClient retrieves cri grpc client -func GetCriRuntimeClient(runtimeEndpoint, hostPrefix string) (pb.RuntimeServiceClient, *grpc.ClientConn, error) { - // Set up a connection to the server. - conn, err := getRuntimeClientConnection(runtimeEndpoint, hostPrefix) - if err != nil { - return nil, nil, fmt.Errorf("failed to connect: %v", err) - } - runtimeClient := pb.NewRuntimeServiceClient(conn) - return runtimeClient, conn, nil -} - -// CloseCriConnection closes grpc connection in client -func CloseCriConnection(conn *grpc.ClientConn) error { - if conn == nil { - return nil - } - return conn.Close() -} diff --git a/pkg/controllers/pod_test.go b/pkg/controllers/pod_test.go deleted file mode 100644 index 4eb0ac74..00000000 --- a/pkg/controllers/pod_test.go +++ /dev/null @@ -1,280 +0,0 @@ -/* -Copyright 2020 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package controllers - -import ( - "fmt" - "time" - - netdefv1 "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/apis/k8s.cni.cncf.io/v1" - - "k8s.io/api/core/v1" - "k8s.io/client-go/informers" - "k8s.io/client-go/kubernetes/fake" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -type FakePodConfigStub struct { - CounterAdd int - CounterUpdate int - CounterDelete int - CounterSynced int -} - -func (f *FakePodConfigStub) OnPodAdd(_ *v1.Pod) { - f.CounterAdd++ -} - -func (f *FakePodConfigStub) OnPodUpdate(_, _ *v1.Pod) { - f.CounterUpdate++ -} - -func (f *FakePodConfigStub) OnPodDelete(_ *v1.Pod) { - f.CounterDelete++ -} - -func (f *FakePodConfigStub) OnPodSynced() { - f.CounterSynced++ -} - -func NewFakePodConfig(stub *FakePodConfigStub) *PodConfig { - configSync := 15 * time.Minute - fakeClient := fake.NewSimpleClientset() - informerFactory := informers.NewSharedInformerFactoryWithOptions(fakeClient, configSync) - podConfig := NewPodConfig(informerFactory.Core().V1().Pods(), configSync) - podConfig.RegisterEventHandler(stub) - return podConfig -} - -func NewFakePodWithNetAnnotation(namespace, name, annot, status string) *v1.Pod { - return &v1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: namespace, - Name: name, - UID: "testUID", - Annotations: map[string]string{ - "k8s.v1.cni.cncf.io/networks": annot, - netdefv1.NetworkStatusAnnot: status, - }, - }, - Spec: v1.PodSpec{ - Containers: []v1.Container{ - {Name: "ctr1", Image: "image"}, - }, - }, - Status: v1.PodStatus{ - Phase: v1.PodRunning, - }, - } -} - -func NewFakeNetworkStatus(netns, netname string) string { - baseStr := ` - [{ - "name": "", - "interface": "eth0", - "ips": [ - "10.244.1.4" - ], - "mac": "aa:e1:20:71:15:01", - "default": true, - "dns": {} - },{ - "name": "%s/%s", - "interface": "net1", - "ips": [ - "10.1.1.101" - ], - "mac": "42:90:65:12:3e:bf", - "dns": {} - }] -` - return fmt.Sprintf(baseStr, netns, netname) -} - -func NewFakePod(namespace, name string) *v1.Pod { - return &v1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: namespace, - Name: name, - UID: "testUID", - }, - Spec: v1.PodSpec{ - Containers: []v1.Container{ - {Name: "ctr1", Image: "image"}, - }, - }, - Status: v1.PodStatus{ - Phase: v1.PodRunning, - }, - } -} - -func NewFakePodChangeTracker(hostname, _ string, ndt *NetDefChangeTracker) *PodChangeTracker { - return &PodChangeTracker{ - items: make(map[types.NamespacedName]*podChange), - hostname: hostname, - netdefChanges: ndt, - networkPlugins: []string{"macvlan"}, - } -} - -var _ = Describe("pod config", func() { - It("check add handler", func() { - stub := &FakePodConfigStub{} - nsConfig := NewFakePodConfig(stub) - nsConfig.handleAddPod(NewFakePod("testns1", "pod")) - Expect(stub.CounterAdd).To(Equal(1)) - Expect(stub.CounterUpdate).To(Equal(0)) - Expect(stub.CounterDelete).To(Equal(0)) - Expect(stub.CounterSynced).To(Equal(0)) - }) - - It("check update handler", func() { - stub := &FakePodConfigStub{} - nsConfig := NewFakePodConfig(stub) - nsConfig.handleUpdatePod(NewFakePod("testns1", "pod"), NewFakePod("testns2", "pod")) - Expect(stub.CounterAdd).To(Equal(0)) - Expect(stub.CounterUpdate).To(Equal(1)) - Expect(stub.CounterDelete).To(Equal(0)) - Expect(stub.CounterSynced).To(Equal(0)) - }) - - It("check update handler", func() { - stub := &FakePodConfigStub{} - nsConfig := NewFakePodConfig(stub) - nsConfig.handleDeletePod(NewFakePod("testns1", "pod")) - Expect(stub.CounterAdd).To(Equal(0)) - Expect(stub.CounterUpdate).To(Equal(0)) - Expect(stub.CounterDelete).To(Equal(1)) - Expect(stub.CounterSynced).To(Equal(0)) - }) -}) - -var _ = Describe("pod controller", func() { - It("Initialize and verify empty", func() { - ndChanges := NewNetDefChangeTracker() - podChanges := NewFakePodChangeTracker("nodeName", "hostPrefix", ndChanges) - podMap := make(PodMap) - podMap.Update(podChanges) - Expect(len(podMap)).To(Equal(0)) - }) - - It("Add pod and verify", func() { - ndChanges := NewNetDefChangeTracker() - podChanges := NewFakePodChangeTracker("nodeName", "hostPrefix", ndChanges) - - Expect(podChanges.Update(nil, NewFakePod("testns1", "testpod1"))).To(BeTrue()) - - podMap := make(PodMap) - podMap.Update(podChanges) - Expect(len(podMap)).To(Equal(1)) - - pod1, ok := podMap[types.NamespacedName{Namespace: "testns1", Name: "testpod1"}] - Expect(ok).To(BeTrue()) - Expect(pod1.Name).To(Equal("testpod1")) - Expect(pod1.Namespace).To(Equal("testns1")) - }) - - It("Add ns then del ns and verify", func() { - ndChanges := NewNetDefChangeTracker() - podChanges := NewFakePodChangeTracker("nodeName", "hostPrefix", ndChanges) - - Expect(podChanges.Update(nil, NewFakePod("testns1", "testpod1"))).To(BeTrue()) - Expect(podChanges.Update(NewFakePod("testns1", "testpod1"), nil)).To(BeTrue()) - Expect(podChanges.Update(nil, NewFakePod("testns2", "testpod2"))).To(BeTrue()) - - podMap := make(PodMap) - podMap.Update(podChanges) - Expect(len(podMap)).To(Equal(1)) - - pod1, ok := podMap[types.NamespacedName{Namespace: "testns2", Name: "testpod2"}] - Expect(ok).To(BeTrue()) - Expect(pod1.Name).To(Equal("testpod2")) - Expect(pod1.Namespace).To(Equal("testns2")) - }) - - It("invalid Update case", func() { - ndChanges := NewNetDefChangeTracker() - podChanges := NewFakePodChangeTracker("nodeName", "hostPrefix", ndChanges) - Expect(podChanges.Update(nil, nil)).To(BeFalse()) - }) - - It("Add pod with net-attach annotation and verify", func() { - ndChanges := NewNetDefChangeTracker() - podChanges := NewFakePodChangeTracker("nodeName", "hostPrefix", ndChanges) - - Expect(ndChanges.Update(nil, NewNetDef("testns1", "net-attach1", NewCNIConfig("testCNI", "macvlan")))).To(BeTrue()) - - Expect(podChanges.Update(nil, NewFakePodWithNetAnnotation("testns1", "testpod1", "net-attach1", NewFakeNetworkStatus("testns1", "net-attach1")))).To(BeTrue()) - podMap := make(PodMap) - podMap.Update(podChanges) - Expect(len(podMap)).To(Equal(1)) - - pod1, ok := podMap[types.NamespacedName{Namespace: "testns1", Name: "testpod1"}] - Expect(ok).To(BeTrue()) - Expect(pod1.Name).To(Equal("testpod1")) - Expect(pod1.Namespace).To(Equal("testns1")) - Expect(len(pod1.Interfaces)).To(Equal(1)) - }) - - It("Add pod with net-attach annotation and verify", func() { - ndChanges := NewNetDefChangeTracker() - podChanges := NewFakePodChangeTracker("nodeName", "hostPrefix", ndChanges) - - Expect(ndChanges.Update(nil, NewNetDef("testns1", "net-attach1", NewCNIConfig("testCNI", "macvlan")))).To(BeTrue()) - Expect(podChanges.Update(nil, NewFakePod("testns1", "testpod1"))).To(BeTrue()) - podMap := make(PodMap) - podMap.Update(podChanges) - Expect(len(podMap)).To(Equal(1)) - - pod1, ok := podMap[types.NamespacedName{Namespace: "testns1", Name: "testpod1"}] - Expect(ok).To(BeTrue()) - Expect(pod1.Name).To(Equal("testpod1")) - Expect(pod1.Namespace).To(Equal("testns1")) - Expect(len(pod1.Interfaces)).To(Equal(0)) - - Expect(podChanges.Update(NewFakePod("testns1", "testpod1"), NewFakePodWithNetAnnotation("testns1", "testpod1", "net-attach1", NewFakeNetworkStatus("testns1", "net-attach1")))).To(BeTrue()) - - podMap.Update(podChanges) - Expect(len(podMap)).To(Equal(1)) - - pod2, ok := podMap[types.NamespacedName{Namespace: "testns1", Name: "testpod1"}] - Expect(ok).To(BeTrue()) - Expect(pod2.Name).To(Equal("testpod1")) - Expect(pod2.Namespace).To(Equal("testns1")) - Expect(len(pod2.Interfaces)).To(Equal(1)) - }) - -}) - -var _ = Describe("runtime kind", func() { - It("Check container runtime valid case", func() { - var runtime RuntimeKind - Expect(runtime.Set("cri")).To(BeNil()) - Expect(runtime.Set("CRI")).To(BeNil()) - }) - It("Check container runtime option invalid case", func() { - var runtime RuntimeKind - Expect(runtime.Set("Foobar")).To(MatchError("Invalid container-runtime option Foobar (possible values: \"cri\")")) - }) -}) diff --git a/pkg/cri/cri.go b/pkg/cri/cri.go new file mode 100644 index 00000000..01d9dc13 --- /dev/null +++ b/pkg/cri/cri.go @@ -0,0 +1,197 @@ +package cri + +import ( + "context" + "encoding/json" + "fmt" + "net" + "strings" + "sync" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/status" + corev1 "k8s.io/api/core/v1" + pb "k8s.io/cri-api/pkg/apis/runtime/v1" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +const ( + runtimeDialTimeout = 10 * time.Second +) + +type Runtime struct { + CriEndpoint string + HostPrefix string + + sync.RWMutex + RuntimeClient pb.RuntimeServiceClient + Conn *grpc.ClientConn +} + +// New creates a new CriRuntime instance. +func New(criEndpoint, hostPrefix string) *Runtime { + return &Runtime{ + CriEndpoint: criEndpoint, + HostPrefix: hostPrefix, + } +} + +// Connect connects to the CRI runtime. +func (c *Runtime) Connect(ctx context.Context) error { + c.Lock() + defer c.Unlock() + + return c.connect(ctx) +} + +func (c *Runtime) connect(ctx context.Context) error { + logger := log.FromContext(ctx) + + // Close the connection if it exists + if c.Conn != nil { + c.Conn.Close() + } + + conn, err := c.getRuntimeClientConnection(ctx) + if err != nil { + return fmt.Errorf("failed to get runtime client connection: %w", err) + } + + c.Conn = conn + c.RuntimeClient = pb.NewRuntimeServiceClient(conn) + + logger.Info("Successfully connected to CRI runtime") + return nil +} + +// Close closes the connection to the CRI runtime. +func (c *Runtime) Close() error { + c.Lock() + defer c.Unlock() + + if c.Conn != nil { + return c.Conn.Close() + } + return nil +} + +// getRuntimeClientConnection establishes a gRPC connection to the CRI Unix socket +func (c *Runtime) getRuntimeClientConnection(ctx context.Context) (*grpc.ClientConn, error) { + target := fmt.Sprintf("unix://%s%s", c.HostPrefix, c.CriEndpoint) + + // Custom dialer for Unix sockets + dialer := func(ctx context.Context, addr string) (net.Conn, error) { + return (&net.Dialer{}).DialContext(ctx, "unix", addr) + } + + // Set timeout for the dial + timeout, cancel := context.WithTimeout(ctx, runtimeDialTimeout) + defer cancel() + + trimmedTarget := strings.TrimPrefix(target, "unix://") + if trimmedTarget == "" { + return nil, fmt.Errorf("invalid target: %s", target) + } + + // Dial the CRI runtime + //nolint:staticcheck + conn, err := grpc.DialContext(timeout, trimmedTarget, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithContextDialer(dialer)) + if err != nil { + return nil, fmt.Errorf("failed to connect to CRI runtime %s: %w", target, err) + } + + return conn, nil +} + +// containerInfoJSON is the JSON structure for the container info +type containerInfoJSON struct { + PID int `json:"pid"` +} + +// GetPodNetNSPath gets the network namespace path for a pod +func (c *Runtime) GetPodNetNSPath(ctx context.Context, pod *corev1.Pod) (string, error) { + logger := log.FromContext(ctx).WithValues("pod", pod.Name, "namespace", pod.Namespace) + + c.Lock() + defer c.Unlock() + + // Connect if we are not connected + if c.Conn == nil { + err := c.connect(ctx) + if err != nil { + return "", fmt.Errorf("failed to connect to CRI runtime: %w", err) + } + } + + // Get the container ID from the pod status + if len(pod.Status.ContainerStatuses) == 0 { + return "", fmt.Errorf("no container statuses found for pod %s", pod.Name) + } + + splitContainerID := strings.Split(pod.Status.ContainerStatuses[0].ContainerID, "://") + if len(splitContainerID) != 2 { + return "", fmt.Errorf("invalid container ID for pod %s", pod.Name) + } + + containerID := splitContainerID[1] + if containerID == "" { + return "", fmt.Errorf("empty container ID for pod %s", pod.Name) + } + + // Send request to get container status + req := &pb.ContainerStatusRequest{ + ContainerId: containerID, + Verbose: true, + } + + resp, err := c.RuntimeClient.ContainerStatus(ctx, req) + if err != nil { + // Check if the error is a gRPC status error. + st, ok := status.FromError(err) + if ok && st.Code() == codes.Unavailable { + // The connection is dead. Log it and try to reconnect. + logger.Info("CRI connection is unavailable, attempting to reconnect...") + + if reconnErr := c.connect(ctx); reconnErr != nil { + return "", fmt.Errorf("failed to reconnect to CRI: %w", reconnErr) + } + + // After reconnecting, retry the RPC call one more time. + logger.Info("Reconnected. Retrying ContainerStatus call...") + resp, err = c.RuntimeClient.ContainerStatus(ctx, req) + if err != nil { + return "", fmt.Errorf("failed to get container status on retry for pod %s: %w", pod.Name, err) + } + } else { + // It was a different kind of error + return "", fmt.Errorf("failed to get container status for pod %s: %w", pod.Name, err) + } + } + + // Get the PID from the info map + info := resp.GetInfo() + if info == nil { + return "", fmt.Errorf("no info map found for container %s", containerID) + } + + infoJSONString, ok := info["info"] + if !ok { + return "", fmt.Errorf("key 'info' not found in container status info map for %s", containerID) + } + + var parsedInfo containerInfoJSON + if err := json.Unmarshal([]byte(infoJSONString), &parsedInfo); err != nil { + return "", fmt.Errorf("failed to unmarshal container info JSON for %s: %w", containerID, err) + } + + if parsedInfo.PID <= 0 { + return "", fmt.Errorf("invalid PID '%d' found for container %s", parsedInfo.PID, containerID) + } + + netnsPath := fmt.Sprintf("%s/proc/%d/ns/net", c.HostPrefix, parsedInfo.PID) + logger.Info("Found netns path", "netnsPath", netnsPath) + return netnsPath, nil +} diff --git a/pkg/datastore/datastore.go b/pkg/datastore/datastore.go new file mode 100644 index 00000000..9f37022c --- /dev/null +++ b/pkg/datastore/datastore.go @@ -0,0 +1,51 @@ +package datastore + +import ( + "sync" + + multiv1beta1 "github.com/k8snetworkplumbingwg/multi-networkpolicy/pkg/apis/k8s.cni.cncf.io/v1beta1" + "k8s.io/apimachinery/pkg/types" +) + +// PolicyForAnnotation is the policy-for annotation key that indicates which network this policy applies to +const PolicyForAnnotation = "k8s.v1.cni.cncf.io/policy-for" + +// Datastore is a datastore for multi-network policies +type Datastore struct { + sync.RWMutex + Policies map[types.NamespacedName]*Policy +} + +// Policy represents a multi-network policy stored in the datastore +type Policy struct { + Name string + Namespace string + Networks []string + + Spec multiv1beta1.MultiNetworkPolicySpec +} + +// GetPolicy gets a policy from the datastore +func (d *Datastore) GetPolicy(namespaceName types.NamespacedName) *Policy { + d.RLock() + defer d.RUnlock() + + return d.Policies[namespaceName] +} + +// DeletePolicy deletes a policy from the datastore +func (d *Datastore) DeletePolicy(key types.NamespacedName) { + d.Lock() + defer d.Unlock() + + delete(d.Policies, key) +} + +// CreatePolicy creates a policy in the datastore +func (d *Datastore) CreatePolicy(policy *Policy) { + d.Lock() + defer d.Unlock() + + key := types.NamespacedName{Namespace: policy.Namespace, Name: policy.Name} + d.Policies[key] = policy +} diff --git a/pkg/datastore/datastore_test.go b/pkg/datastore/datastore_test.go new file mode 100644 index 00000000..634dd906 --- /dev/null +++ b/pkg/datastore/datastore_test.go @@ -0,0 +1,174 @@ +package datastore + +import ( + "testing" + + multiv1beta1 "github.com/k8snetworkplumbingwg/multi-networkpolicy/pkg/apis/k8s.cni.cncf.io/v1beta1" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +func TestDatastore(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Datastore Suite") +} + +var _ = Describe("Datastore", func() { + var ds *Datastore + + BeforeEach(func() { + ds = &Datastore{ + Policies: make(map[types.NamespacedName]*Policy), + } + }) + + Describe("Policy Management", func() { + Context("when creating a policy", func() { + It("should store the policy correctly", func() { + policy := &Policy{ + Name: "test-policy", + Namespace: "test-ns", + Networks: []string{"network1"}, + Spec: multiv1beta1.MultiNetworkPolicySpec{ + PodSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "test"}, + }, + }, + } + + ds.CreatePolicy(policy) + + key := types.NamespacedName{Namespace: "test-ns", Name: "test-policy"} + retrievedPolicy := ds.GetPolicy(key) + Expect(retrievedPolicy).NotTo(BeNil()) + Expect(retrievedPolicy.Name).To(Equal("test-policy")) + Expect(retrievedPolicy.Namespace).To(Equal("test-ns")) + Expect(retrievedPolicy.Networks).To(Equal([]string{"network1"})) + }) + + It("should overwrite existing policy with same key", func() { + key := types.NamespacedName{Namespace: "test-ns", Name: "test-policy"} + + policy1 := &Policy{ + Name: "test-policy", + Namespace: "test-ns", + Networks: []string{"network1"}, + } + + policy2 := &Policy{ + Name: "test-policy", + Namespace: "test-ns", + Networks: []string{"network2"}, + } + + ds.CreatePolicy(policy1) + ds.CreatePolicy(policy2) + + retrievedPolicy := ds.GetPolicy(key) + Expect(retrievedPolicy).NotTo(BeNil()) + Expect(retrievedPolicy.Networks).To(Equal([]string{"network2"})) + }) + }) + + Context("when getting a policy", func() { + It("should return the policy if it exists", func() { + policy := &Policy{ + Name: "existing-policy", + Namespace: "test-ns", + Networks: []string{"network1"}, + } + + ds.CreatePolicy(policy) + + key := types.NamespacedName{Namespace: "test-ns", Name: "existing-policy"} + retrievedPolicy := ds.GetPolicy(key) + Expect(retrievedPolicy).NotTo(BeNil()) + Expect(retrievedPolicy.Name).To(Equal("existing-policy")) + }) + + It("should return nil if policy doesn't exist", func() { + key := types.NamespacedName{Namespace: "test-ns", Name: "non-existent"} + retrievedPolicy := ds.GetPolicy(key) + Expect(retrievedPolicy).To(BeNil()) + }) + }) + + Context("when deleting a policy", func() { + It("should remove the policy from the datastore", func() { + policy := &Policy{ + Name: "to-delete", + Namespace: "test-ns", + Networks: []string{"network1"}, + } + + ds.CreatePolicy(policy) + + key := types.NamespacedName{Namespace: "test-ns", Name: "to-delete"} + // Verify policy exists + Expect(ds.GetPolicy(key)).NotTo(BeNil()) + + // Delete policy + ds.DeletePolicy(key) + + // Verify policy is gone + Expect(ds.GetPolicy(key)).To(BeNil()) + }) + + It("should handle deletion of non-existent policy gracefully", func() { + key := types.NamespacedName{Namespace: "test-ns", Name: "non-existent"} + + // Should not panic + Expect(func() { + ds.DeletePolicy(key) + }).NotTo(Panic()) + }) + }) + }) + + Describe("Concurrent Access", func() { + It("should handle concurrent reads and writes safely", func() { + done := make(chan bool, 3) + + // Concurrent writer + go func() { + defer GinkgoRecover() + for i := 0; i < 100; i++ { + policy := &Policy{ + Name: "concurrent-policy", + Namespace: "test-ns", + Networks: []string{"network1"}, + } + ds.CreatePolicy(policy) + } + done <- true + }() + + // Concurrent reader + go func() { + defer GinkgoRecover() + for i := 0; i < 100; i++ { + key := types.NamespacedName{Namespace: "test-ns", Name: "concurrent-policy"} + ds.GetPolicy(key) // May return nil or the policy + } + done <- true + }() + + // Concurrent deleter + go func() { + defer GinkgoRecover() + for i := 0; i < 100; i++ { + key := types.NamespacedName{Namespace: "test-ns", Name: "concurrent-policy"} + ds.DeletePolicy(key) + } + done <- true + }() + + // Wait for all goroutines to complete + for i := 0; i < 3; i++ { + Eventually(done).Should(Receive()) + } + }) + }) +}) diff --git a/pkg/nftables/cleanup.go b/pkg/nftables/cleanup.go new file mode 100644 index 00000000..c1e5665d --- /dev/null +++ b/pkg/nftables/cleanup.go @@ -0,0 +1,146 @@ +package nftables + +import ( + "context" + "fmt" + "strings" + + "github.com/go-logr/logr" + "sigs.k8s.io/knftables" + + "github.com/k8snetworkplumbingwg/multi-network-policy-nftables/pkg/utils" +) + +// cleanUpPolicy cleans up the policy +func cleanUpPolicy(ctx context.Context, policyName string, policyNamespace string, logger logr.Logger) error { + nft, err := knftables.New(knftables.InetFamily, tableName) + if err != nil { + return fmt.Errorf("failed to create nftables client: %w", err) + } + + return cleanUp(ctx, nft, policyName, policyNamespace, logger) +} + +// cleanUp cleans up the policy chains, rules and sets +func cleanUp(ctx context.Context, nft knftables.Interface, policyName string, policyNamespace string, logger logr.Logger) error { + logger.Info("Cleaning up policy") + + tx := nft.NewTransaction() + + policyRuleComment := fmt.Sprintf("%s/%s", policyNamespace, policyName) + + // Delete rule in input chain + rules, err := nft.ListRules(ctx, inputChain) + if err != nil { + if !knftables.IsNotFound(err) { + return fmt.Errorf("failed to list rules in input chain: %w", err) + } + } + + for _, rule := range rules { + if rule.Comment != nil && *rule.Comment == policyRuleComment { + logger.V(1).Info("Deleting rule in input chain", "rule", rule.Comment) + tx.Delete(rule) + } + } + + // Delete rule in output chain + rules, err = nft.ListRules(ctx, outputChain) + if err != nil { + if !knftables.IsNotFound(err) { + return fmt.Errorf("failed to list rules in output chain: %w", err) + } + } + + for _, rule := range rules { + if rule.Comment != nil && *rule.Comment == policyRuleComment { + logger.V(1).Info("Deleting rule in output chain", "rule", rule.Comment) + tx.Delete(rule) + } + } + + // Delete rule in ingress chain + rules, err = nft.ListRules(ctx, ingressChain) + if err != nil { + if !knftables.IsNotFound(err) { + return fmt.Errorf("failed to list rules in ingress chain: %w", err) + } + } + + for _, rule := range rules { + if rule.Comment != nil && *rule.Comment == policyRuleComment { + logger.V(1).Info("Deleting rule in ingress chain", "rule", rule.Comment) + tx.Delete(rule) + } + } + + // Delete rule in egress chain + rules, err = nft.ListRules(ctx, egressChain) + if err != nil { + if !knftables.IsNotFound(err) { + return fmt.Errorf("failed to list rules in egress chain: %w", err) + } + } + + for _, rule := range rules { + if rule.Comment != nil && *rule.Comment == policyRuleComment { + logger.V(1).Info("Deleting rule in egress chain", "rule", rule.Comment) + tx.Delete(rule) + } + } + + hashName := utils.GetHashName(policyName, policyNamespace) + + // Delete policy chains + chains, err := nft.List(ctx, "chains") + if err != nil { + if !knftables.IsNotFound(err) { + return fmt.Errorf("failed to list chains: %w", err) + } + } + + for _, chain := range chains { + if chain == fmt.Sprintf("%s%s", prefixNetworkPolicyChain, hashName) { + logger.V(1).Info("Deleting policy chain", "chain", chain) + tx.Flush(&knftables.Chain{ + Name: chain, + }) + + tx.Delete(&knftables.Chain{ + Name: chain, + }) + } + } + + // Delete policy sets + policySetPrefix := fmt.Sprintf("%s%s", prefixNetworkPolicySet, hashName) + managedInterfacesSetPrefix := fmt.Sprintf("%s%s", prefixManagedInterfacesSet, hashName) + + // Delete policy sets + sets, err := nft.List(ctx, "sets") + if err != nil { + if !knftables.IsNotFound(err) { + return fmt.Errorf("failed to list sets: %w", err) + } + } + + for _, set := range sets { + if strings.HasPrefix(set, policySetPrefix) || strings.HasPrefix(set, managedInterfacesSetPrefix) { + logger.V(1).Info("Deleting policy set", "set", set) + tx.Flush(&knftables.Set{ + Name: set, + }) + + tx.Delete(&knftables.Set{ + Name: set, + }) + } + } + + err = nft.Run(ctx, tx) + if err != nil { + return fmt.Errorf("failed to run transaction: %w", err) + } + + return nil +} diff --git a/pkg/nftables/enforce.go b/pkg/nftables/enforce.go new file mode 100644 index 00000000..d21e4824 --- /dev/null +++ b/pkg/nftables/enforce.go @@ -0,0 +1,970 @@ +package nftables + +import ( + "context" + "fmt" + "net" + "strings" + + "github.com/go-logr/logr" + multiv1beta1 "github.com/k8snetworkplumbingwg/multi-networkpolicy/pkg/apis/k8s.cni.cncf.io/v1beta1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/knftables" + + "github.com/k8snetworkplumbingwg/multi-network-policy-nftables/pkg/datastore" + "github.com/k8snetworkplumbingwg/multi-network-policy-nftables/pkg/utils" +) + +// enforcePolicy applies the NFTables policy for a pod +func (n *NFTables) enforcePolicy(ctx context.Context, pod *corev1.Pod, interfaces []Interface, policy *datastore.Policy, logger logr.Logger) error { + logger.Info("Applying policy") + + nft, err := knftables.New(knftables.InetFamily, tableName) + if err != nil { + return fmt.Errorf("failed to create nftables client: %w", err) + } + + // Clean up the policy even if the pod is not matched by the policy + err = cleanUp(ctx, nft, policy.Name, policy.Namespace, logger) + if err != nil { + return fmt.Errorf("failed to clean up policy: %w", err) + } + + if !utils.MatchesSelector(policy.Spec.PodSelector, pod.Labels) { + logger.Info("Pod not matched by policy pod selector, skipping") + return nil + } + + // Find the interfaces on the pod that belong to the networks of the policy (Policy-for annotation) + matchedInterfaces := getMatchedInterfaces(interfaces, policy.Networks) + if len(matchedInterfaces) == 0 { + logger.Info("No matched interfaces found, skipping") + return nil + } + + logger.Info("Found interfaces matched by policy", "matchedInterfaces", matchedInterfaces) + + // It creates the input, output chains and the common-ingress and common-egress chains + // It also ensures the policy type structure for ingress and egress which is a connection tracking rule + // and a jump rule to the common-ingress and common-egress chains, and a drop rule at the end of the chain + err = ensureBasicStructure(ctx, nft, n.CommonRules, logger) + if err != nil { + return fmt.Errorf("failed to ensure basic structure: %w", err) + } + + // Get the first 16 characters of the SHA256 hash of the namespace name of the policy to be used as nft object identifier + hashName := utils.GetHashName(policy.Name, policy.Namespace) + + // We will apply all generated rules in a single transaction + tx := nft.NewTransaction() + + // Create a set with the interfaces that are managed by the policy in the input and output chains + createManagedInterfacesSet(tx, matchedInterfaces, hashName, policy.Namespace, policy.Name, logger) + + // Check if the policy has ingress or egress enabled + ingressEnabled, egressEnabled := checkPolicyTypes(policy) + + logger.Info("Policy types", "ingressEnabled", ingressEnabled, "egressEnabled", egressEnabled) + + mnpChainName := fmt.Sprintf("%s%s", prefixNetworkPolicyChain, hashName) + + if ingressEnabled { + logger.V(1).Info("Enforcing ingress rules") + + dispatcherRuleComment := fmt.Sprintf("%s/%s", policy.Namespace, policy.Name) + createDispatcherRule(tx, hashName, inputChain, dispatcherRuleComment, logger) + + err = createPolicyChain(ctx, nft, tx, mnpChainName, ingressChain, policy.Namespace, policy.Name, logger) + if err != nil { + return fmt.Errorf("failed to create policy chain: %w", err) + } + + err = n.createIngressRules(ctx, tx, matchedInterfaces, policy, hashName, logger) + if err != nil { + return fmt.Errorf("failed to apply ingress rules: %w", err) + } + + logger.Info("Ingress rules applied") + } + + if egressEnabled { + logger.V(1).Info("Enforcing egress rules") + + dispatcherRuleComment := fmt.Sprintf("%s/%s", policy.Namespace, policy.Name) + createDispatcherRule(tx, hashName, outputChain, dispatcherRuleComment, logger) + + err = createPolicyChain(ctx, nft, tx, mnpChainName, egressChain, policy.Namespace, policy.Name, logger) + if err != nil { + return fmt.Errorf("failed to create policy chain: %w", err) + } + + err = n.createEgressRules(ctx, tx, matchedInterfaces, policy, hashName, logger) + if err != nil { + return fmt.Errorf("failed to apply egress rules: %w", err) + } + + logger.Info("Egress rules applied") + } + + err = nft.Run(ctx, tx) + if err != nil { + return fmt.Errorf("failed to run transaction: %w", err) + } + + return nil +} + +// ensureBasicStructure ensures the basic NFTables structure +func ensureBasicStructure(ctx context.Context, nft knftables.Interface, commonRules *CommonRules, logger logr.Logger) error { + logger.Info("Ensuring basic NFTables structure") + + tx := nft.NewTransaction() + + tx.Add(&knftables.Table{ + Comment: knftables.PtrTo("MultiNetworkPolicy"), + }) + + // Add the input chain + tx.Add(&knftables.Chain{ + Name: inputChain, + Type: knftables.PtrTo(knftables.FilterType), + Hook: knftables.PtrTo(knftables.InputHook), + Priority: knftables.PtrTo(knftables.FilterPriority), + Comment: knftables.PtrTo("Input Dispatcher"), + }) + + // Add the output chain + tx.Add(&knftables.Chain{ + Name: outputChain, + Type: knftables.PtrTo(knftables.FilterType), + Hook: knftables.PtrTo(knftables.OutputHook), + Priority: knftables.PtrTo(knftables.FilterPriority), + Comment: knftables.PtrTo("Output Dispatcher"), + }) + + // Ensure policy type structure for ingress + err := policyTypeStructure(ctx, nft, tx, ingressChain, "Ingress Policies", commonIngressChain, logger) + if err != nil { + return fmt.Errorf("failed to ensure policy type structure for ingress: %w", err) + } + + // Ensure policy type structure for egress + err = policyTypeStructure(ctx, nft, tx, egressChain, "Egress Policies", commonEgressChain, logger) + if err != nil { + return fmt.Errorf("failed to ensure policy type structure for egress: %w", err) + } + + // Create common rules + createCommonRules(tx, commonRules, logger) + + err = nft.Run(ctx, tx) + if err != nil { + return fmt.Errorf("failed to run transaction: %w", err) + } + + return nil +} + +// policyTypeStructure ensures the basic NFTables structure for a policy type +func policyTypeStructure(ctx context.Context, nft knftables.Interface, tx *knftables.Transaction, chainName string, chainComment string, commonChainName string, logger logr.Logger) error { + // Add ingress objects + tx.Add(&knftables.Chain{ + Name: chainName, + Comment: knftables.PtrTo(chainComment), + }) + + tx.Add(&knftables.Chain{ + Name: commonChainName, + Comment: knftables.PtrTo("Common Policies"), + }) + + // Ensure connection tracking rule in chain + connectionTrackingRule, err := findRuleInChain(ctx, nft, chainName, connectionTrackingRuleComment) + if err != nil { + return fmt.Errorf("failed to find connection tracking rule in %s chain: %w", chainName, err) + } + + if connectionTrackingRule == nil { + // First time we run, we need to add the connection tracking rule + logger.V(1).Info("Adding connection tracking rule to chain", "chain", chainName) + tx.Add(&knftables.Rule{ + Chain: chainName, + Rule: knftables.Concat("ct state established,related accept"), + Comment: knftables.PtrTo(connectionTrackingRuleComment), + }) + } + + // Ensure jump rule to common chain + jumpCommonRule, err := findRuleInChain(ctx, nft, chainName, jumpCommonRuleComment) + if err != nil { + return fmt.Errorf("failed to find jump rule to common in %s chain: %w", chainName, err) + } + + if jumpCommonRule == nil { + // First time we run, we need to add the jump rule + logger.V(1).Info("Adding jump rule to common chain", "chain", chainName) + tx.Add(&knftables.Rule{ + Chain: chainName, + Rule: knftables.Concat("jump", commonChainName), + Comment: knftables.PtrTo(jumpCommonRuleComment), + }) + } + + // Ensure drop rule in chain + dropRule, err := findRuleInChain(ctx, nft, chainName, dropRuleComment) + if err != nil { + return fmt.Errorf("failed to find drop rule in %s chain: %w", chainName, err) + } + + if dropRule == nil { + // First time we run, we need to add the drop rule + logger.V(1).Info("Adding drop rule to chain", "chain", chainName) + tx.Add(&knftables.Rule{ + Chain: chainName, + Rule: knftables.Concat("drop"), + Comment: knftables.PtrTo(dropRuleComment), + }) + } + + return nil +} + +// createCommonRules creates the common rules in the common chains +func createCommonRules(tx *knftables.Transaction, commonRules *CommonRules, logger logr.Logger) { + logger.Info("Creating common rules") + + if commonRules == nil { + logger.Info("No common rules specified, skipping") + return + } + + // Flush common chains to ensure no stale rules + tx.Flush(&knftables.Chain{ + Name: commonIngressChain, + }) + + tx.Flush(&knftables.Chain{ + Name: commonEgressChain, + }) + + if commonRules.AcceptICMP { + logger.Info("Adding rule to accept ICMP traffic in common ingress and egress chains") + // Accept ICMP traffic in common ingress chain + tx.Add(&knftables.Rule{ + Chain: commonIngressChain, + Rule: knftables.Concat("meta l4proto icmp accept"), + Comment: knftables.PtrTo("Accept ICMP"), + }) + + // Accept ICMP traffic in common egress chain + tx.Add(&knftables.Rule{ + Chain: commonEgressChain, + Rule: knftables.Concat("meta l4proto icmp accept"), + Comment: knftables.PtrTo("Accept ICMP"), + }) + } + + if commonRules.AcceptICMPv6 { + logger.Info("Adding rule to accept ICMPv6 traffic in common ingress and egress chains") + // Accept ICMPv6 traffic in common ingress chain + tx.Add(&knftables.Rule{ + Chain: commonIngressChain, + Rule: knftables.Concat("meta l4proto icmpv6 accept"), + Comment: knftables.PtrTo("Accept ICMPv6"), + }) + + // Accept ICMPv6 traffic in common egress chain + tx.Add(&knftables.Rule{ + Chain: commonEgressChain, + Rule: knftables.Concat("meta l4proto icmpv6 accept"), + Comment: knftables.PtrTo("Accept ICMPv6"), + }) + } + + // Add custom rules to common ingress chain + combined := commonRules.CustomIPv4IngressRules + combined = append(combined, commonRules.CustomIPv6IngressRules...) + for _, rule := range combined { + logger.V(1).Info("Adding custom rule to common ingress chain", "rule", rule) + tx.Add(&knftables.Rule{ + Chain: commonIngressChain, + Rule: rule, + Comment: knftables.PtrTo("Custom Rule"), + }) + } + + // Add custom rules to common egress chain + combined = commonRules.CustomIPv4EgressRules + combined = append(combined, commonRules.CustomIPv6EgressRules...) + for _, rule := range combined { + logger.V(1).Info("Adding custom rule to common egress chain", "rule", rule) + tx.Add(&knftables.Rule{ + Chain: commonEgressChain, + Rule: rule, + Comment: knftables.PtrTo("Custom Rule"), + }) + } +} + +// createManagedInterfacesSet creates the managed interfaces set +func createManagedInterfacesSet(tx *knftables.Transaction, matchedInterfaces []Interface, hashName string, policyNamespace string, policyName string, logger logr.Logger) { + logger.Info("Creating managed interfaces set") + + name := fmt.Sprintf("%s%s", prefixManagedInterfacesSet, hashName) + + tx.Add(&knftables.Set{ + Name: name, + Type: "ifname", + Comment: knftables.PtrTo(fmt.Sprintf("Managed interfaces set for %s/%s", policyNamespace, policyName)), + }) + + // Add interfaces to the managed interfaces set + for _, intf := range matchedInterfaces { + logger.V(1).Info("Adding interface to managed interfaces set", "interface", intf.Name) + tx.Add(&knftables.Element{ + Set: name, + Key: []string{intf.Name}, + }) + } +} + +// createDispatcherRule creates the dispatcher rule in the dispatcher chain +func createDispatcherRule(tx *knftables.Transaction, hashName string, dispatcherChainName string, comment string, logger logr.Logger) { + logger.Info("Creating dispatcher rule in dispatcher chain", "dispatcherChainName", dispatcherChainName) + + managedInterfacesSetName := fmt.Sprintf("%s%s", prefixManagedInterfacesSet, hashName) + + trafficDirection := "iifname" + policyTypeChainName := "ingress" + if dispatcherChainName == outputChain { + trafficDirection = "oifname" + policyTypeChainName = "egress" + } + + tx.Add(&knftables.Rule{ + Chain: dispatcherChainName, + Rule: knftables.Concat(trafficDirection, fmt.Sprintf("@%s", managedInterfacesSetName), "jump", policyTypeChainName), + Comment: knftables.PtrTo(comment), + }) +} + +// createPolicyChain creates the policy chain and jump rule from policy type chain +func createPolicyChain(ctx context.Context, nft knftables.Interface, tx *knftables.Transaction, npChainName string, policyTypeChainName string, namespace string, name string, logger logr.Logger) error { + logger.Info("Creating policy chain", "npChainName", npChainName) + + tx.Add(&knftables.Chain{ + Name: npChainName, + Comment: knftables.PtrTo(fmt.Sprintf("MultiNetworkPolicy %s/%s", namespace, name)), + }) + + // Find drop rule in policy chain + dropRule, err := findRuleInChain(ctx, nft, policyTypeChainName, dropRuleComment) + if err != nil || dropRule == nil { + // Should never happen + return fmt.Errorf("failed to find drop rule in %s chain: %w", policyTypeChainName, err) + } + + // Insert jump rule before the drop rule + tx.Insert(&knftables.Rule{ + Chain: policyTypeChainName, + Rule: knftables.Concat("jump", npChainName), + Comment: knftables.PtrTo(fmt.Sprintf("%s/%s", namespace, name)), + Handle: dropRule.Handle, + }) + + return nil +} + +// createIngressRules creates the ingress rules for a policy +func (n *NFTables) createIngressRules(ctx context.Context, tx *knftables.Transaction, matchedInterfaces []Interface, policy *datastore.Policy, hashName string, logger logr.Logger) error { + logger.Info("Creating ingress rules") + + npChainName := fmt.Sprintf("%s%s", prefixNetworkPolicyChain, hashName) + + // Reverse rules for IPv4 and IPv6 - hairpinning + createReverseRules(tx, matchedInterfaces, npChainName, logger) + + if len(policy.Spec.Ingress) == 0 { + logger.Info("No ingress rules specified, no rules will be created") + return nil + } + + for i, peer := range policy.Spec.Ingress { + logger.V(1).Info("Processing ingress peer", "index", i) + + var portRuleSections []string + if len(peer.Ports) > 0 { + portRuleSections = getPortRuleSections(peer.Ports) + } + + // Allow all traffic + if len(peer.From) == 0 { + logger.Info("No sources specified, accepting traffic from all sources") + + var ipRuleSections []string + for _, intf := range matchedInterfaces { + ipRuleSections = append(ipRuleSections, knftables.Concat("iifname", intf.Name)) + } + + createRules(tx, npChainName, ipRuleSections, portRuleSections, logger) + continue + } + + logger.V(1).Info("Processing ingress peer with sources specified") + + // Get the peer info which contains the pods, cidrs and excepts + peerInfo, err := n.parsePeers(ctx, peer.From, policy.Namespace, logger) + if err != nil { + return fmt.Errorf("failed to parse peers: %w", err) + } + + var ipRuleSections []string + + if len(peerInfo.pods) != 0 { + logger.V(1).Info("Found pods selected by peer's selectors", "count", len(peerInfo.pods)) + + podInterfacesMap := getPodInterfacesMap(peerInfo.pods, policy.Networks) + + // We need to process each interface individually + for _, intf := range matchedInterfaces { + // Create the IP addresses set for the interface. + // Sets cannot be inet family, so we need to create separate sets for IPv4 and IPv6 + // Each ingress entry will have its own set for the interface + ipv4SetName := fmt.Sprintf("%s%s_ingress_ipv4_%s_%d", prefixNetworkPolicySet, hashName, intf.Name, i) + ipv6SetName := fmt.Sprintf("%s%s_ingress_ipv6_%s_%d", prefixNetworkPolicySet, hashName, intf.Name, i) + setComment := fmt.Sprintf("Addresses for %s/%s", policy.Namespace, policy.Name) + + ipv4Addresses, ipv6Addresses := classifyAddresses(podInterfacesMap, intf.Network) + + if len(ipv4Addresses) > 0 { + createAndPopulateIPSet(tx, ipv4SetName, "ipv4_addr", setComment, ipv4Addresses, false) + ipRuleSections = append(ipRuleSections, knftables.Concat("iifname", intf.Name, "ip", "saddr", fmt.Sprintf("@%s", ipv4SetName))) + } + + if len(ipv6Addresses) > 0 { + createAndPopulateIPSet(tx, ipv6SetName, "ipv6_addr", setComment, ipv6Addresses, false) + ipRuleSections = append(ipRuleSections, knftables.Concat("iifname", intf.Name, "ip6", "saddr", fmt.Sprintf("@%s", ipv6SetName))) + } + } + } + + if len(peerInfo.cidrs) > 0 { + logger.V(1).Info("Found IP blocks", "cidrs", len(peerInfo.cidrs), "excepts", len(peerInfo.excepts)) + + ipv4CidrsSetName := fmt.Sprintf("%s%s_ingress_ipv4_cidr_%d", prefixNetworkPolicySet, hashName, i) + ipv6CidrsSetName := fmt.Sprintf("%s%s_ingress_ipv6_cidr_%d", prefixNetworkPolicySet, hashName, i) + cidrsSetComment := fmt.Sprintf("CIDRs for %s/%s", policy.Namespace, policy.Name) + + ipv4ExceptsSetName := fmt.Sprintf("%s%s_ingress_ipv4_except_%d", prefixNetworkPolicySet, hashName, i) + ipv6ExceptsSetName := fmt.Sprintf("%s%s_ingress_ipv6_except_%d", prefixNetworkPolicySet, hashName, i) + exceptsSetComment := fmt.Sprintf("Excepts for %s/%s", policy.Namespace, policy.Name) + + ipv4CIDRs, ipv6CIDRs := utils.SplitCIDRs(peerInfo.cidrs) + ipv4Excepts, ipv6Excepts := utils.SplitCIDRs(peerInfo.excepts) + + if len(ipv4CIDRs) > 0 { + createAndPopulateIPSet(tx, ipv4CidrsSetName, "ipv4_addr", cidrsSetComment, ipv4CIDRs, true) + if len(ipv4Excepts) > 0 { + createAndPopulateIPSet(tx, ipv4ExceptsSetName, "ipv4_addr", exceptsSetComment, ipv4Excepts, true) + } + } + + if len(ipv6CIDRs) > 0 { + createAndPopulateIPSet(tx, ipv6CidrsSetName, "ipv6_addr", cidrsSetComment, ipv6CIDRs, true) + if len(ipv6Excepts) > 0 { + createAndPopulateIPSet(tx, ipv6ExceptsSetName, "ipv6_addr", exceptsSetComment, ipv6Excepts, true) + } + } + + managedInterfacesSetName := fmt.Sprintf("%s%s", prefixManagedInterfacesSet, hashName) + if len(ipv4CIDRs) > 0 { + rule := knftables.Concat("iifname", fmt.Sprintf("@%s", managedInterfacesSetName), "ip", "saddr", fmt.Sprintf("@%s", ipv4CidrsSetName)) + if len(ipv4Excepts) > 0 { + rule = knftables.Concat(rule, "ip", "saddr", "!=", fmt.Sprintf("@%s", ipv4ExceptsSetName)) + } + + ipRuleSections = append(ipRuleSections, rule) + } + + if len(ipv6CIDRs) > 0 { + rule := knftables.Concat("iifname", fmt.Sprintf("@%s", managedInterfacesSetName), "ip6", "saddr", fmt.Sprintf("@%s", ipv6CidrsSetName)) + if len(ipv6Excepts) > 0 { + rule = knftables.Concat(rule, "ip6", "saddr", "!=", fmt.Sprintf("@%s", ipv6ExceptsSetName)) + } + + ipRuleSections = append(ipRuleSections, rule) + } + } + + createRules(tx, npChainName, ipRuleSections, portRuleSections, logger) + } + + return nil +} + +// createEgressRules creates the egress rules for a policy +func (n *NFTables) createEgressRules(ctx context.Context, tx *knftables.Transaction, matchedInterfaces []Interface, policy *datastore.Policy, hashName string, logger logr.Logger) error { + logger.Info("Creating egress rules") + + npChainName := fmt.Sprintf("%s%s", prefixNetworkPolicyChain, hashName) + + if len(policy.Spec.Egress) == 0 { + logger.Info("No egress rules specified, no rules will be created") + return nil + } + + for i, peer := range policy.Spec.Egress { + logger.V(1).Info("Processing egress peer", "index", i) + + var portRuleSections []string + if len(peer.Ports) > 0 { + portRuleSections = getPortRuleSections(peer.Ports) + } + + // Allow all traffic + if len(peer.To) == 0 { + logger.Info("No destinations specified, accepting traffic to all destinations") + + var ipRuleSections []string + for _, intf := range matchedInterfaces { + ipRuleSections = append(ipRuleSections, knftables.Concat("oifname", intf.Name)) + } + + createRules(tx, npChainName, ipRuleSections, portRuleSections, logger) + continue + } + + logger.V(1).Info("Processing egress peer with destinations specified") + + // Get the peer info which contains the pods, cidrs and excepts + peerInfo, err := n.parsePeers(ctx, peer.To, policy.Namespace, logger) + if err != nil { + return fmt.Errorf("failed to parse peers: %w", err) + } + + var ipRuleSections []string + + if len(peerInfo.pods) != 0 { + logger.V(1).Info("Found pods selected by peer's selectors", "count", len(peerInfo.pods)) + + podInterfacesMap := getPodInterfacesMap(peerInfo.pods, policy.Networks) + + // We need to process each interface individually + for _, intf := range matchedInterfaces { + // Create the IP addresses set for the interface. + // Sets cannot be inet family, so we need to create separate sets for IPv4 and IPv6 + // Each egress entry will have its own set for the interface + ipv4SetName := fmt.Sprintf("%s%s_egress_ipv4_%s_%d", prefixNetworkPolicySet, hashName, intf.Name, i) + ipv6SetName := fmt.Sprintf("%s%s_egress_ipv6_%s_%d", prefixNetworkPolicySet, hashName, intf.Name, i) + setComment := fmt.Sprintf("Addresses for %s/%s", policy.Namespace, policy.Name) + + ipv4Addresses, ipv6Addresses := classifyAddresses(podInterfacesMap, intf.Network) + + if len(ipv4Addresses) > 0 { + createAndPopulateIPSet(tx, ipv4SetName, "ipv4_addr", setComment, ipv4Addresses, false) + ipRuleSections = append(ipRuleSections, knftables.Concat("oifname", intf.Name, "ip", "daddr", fmt.Sprintf("@%s", ipv4SetName))) + } + + if len(ipv6Addresses) > 0 { + createAndPopulateIPSet(tx, ipv6SetName, "ipv6_addr", setComment, ipv6Addresses, false) + ipRuleSections = append(ipRuleSections, knftables.Concat("oifname", intf.Name, "ip6", "daddr", fmt.Sprintf("@%s", ipv6SetName))) + } + } + } + + if len(peerInfo.cidrs) > 0 { + logger.V(1).Info("Found IP blocks", "cidrs", len(peerInfo.cidrs), "excepts", len(peerInfo.excepts)) + + ipv4CidrsSetName := fmt.Sprintf("%s%s_egress_ipv4_cidr_%d", prefixNetworkPolicySet, hashName, i) + ipv6CidrsSetName := fmt.Sprintf("%s%s_egress_ipv6_cidr_%d", prefixNetworkPolicySet, hashName, i) + cidrsSetComment := fmt.Sprintf("CIDRs for %s/%s", policy.Namespace, policy.Name) + + ipv4ExceptsSetName := fmt.Sprintf("%s%s_egress_ipv4_except_%d", prefixNetworkPolicySet, hashName, i) + ipv6ExceptsSetName := fmt.Sprintf("%s%s_egress_ipv6_except_%d", prefixNetworkPolicySet, hashName, i) + exceptsSetComment := fmt.Sprintf("Excepts for %s/%s", policy.Namespace, policy.Name) + + ipv4CIDRs, ipv6CIDRs := utils.SplitCIDRs(peerInfo.cidrs) + ipv4Excepts, ipv6Excepts := utils.SplitCIDRs(peerInfo.excepts) + + if len(ipv4CIDRs) > 0 { + createAndPopulateIPSet(tx, ipv4CidrsSetName, "ipv4_addr", cidrsSetComment, ipv4CIDRs, true) + if len(ipv4Excepts) > 0 { + createAndPopulateIPSet(tx, ipv4ExceptsSetName, "ipv4_addr", exceptsSetComment, ipv4Excepts, true) + } + } + + if len(ipv6CIDRs) > 0 { + createAndPopulateIPSet(tx, ipv6CidrsSetName, "ipv6_addr", cidrsSetComment, ipv6CIDRs, true) + if len(ipv6Excepts) > 0 { + createAndPopulateIPSet(tx, ipv6ExceptsSetName, "ipv6_addr", exceptsSetComment, ipv6Excepts, true) + } + } + + managedInterfacesSetName := fmt.Sprintf("%s%s", prefixManagedInterfacesSet, hashName) + if len(ipv4CIDRs) > 0 { + rule := knftables.Concat("oifname", fmt.Sprintf("@%s", managedInterfacesSetName), "ip", "daddr", fmt.Sprintf("@%s", ipv4CidrsSetName)) + if len(ipv4Excepts) > 0 { + rule = knftables.Concat(rule, "ip", "daddr", "!=", fmt.Sprintf("@%s", ipv4ExceptsSetName)) + } + + ipRuleSections = append(ipRuleSections, rule) + } + + if len(ipv6CIDRs) > 0 { + rule := knftables.Concat("oifname", fmt.Sprintf("@%s", managedInterfacesSetName), "ip6", "daddr", fmt.Sprintf("@%s", ipv6CidrsSetName)) + if len(ipv6Excepts) > 0 { + rule = knftables.Concat(rule, "ip6", "daddr", "!=", fmt.Sprintf("@%s", ipv6ExceptsSetName)) + } + + ipRuleSections = append(ipRuleSections, rule) + } + } + + createRules(tx, npChainName, ipRuleSections, portRuleSections, logger) + } + + return nil +} + +// createReverseRules creates the reverse rules for the policy chain +func createReverseRules(tx *knftables.Transaction, matchedInterfaces []Interface, npChainName string, logger logr.Logger) { + logger.Info("Creating reverse routes") + + for _, intf := range matchedInterfaces { + for _, ip := range intf.IPs { + // Validate IP address + parsedIP := net.ParseIP(ip) + if parsedIP == nil { + logger.V(1).Info("Skipping invalid IP address", "ip", ip, "interface", intf.Name) + continue + } + + // Find ip version + ipVersion := "ip" + if parsedIP.To4() == nil { + ipVersion = "ip6" + } + + // Create the reverse route + tx.Add(&knftables.Rule{ + Chain: npChainName, + Rule: knftables.Concat("iifname", intf.Name, ipVersion, "saddr", ip, "accept"), + }) + } + } +} + +// findRuleInChain finds a rule in a chain by comment +func findRuleInChain(ctx context.Context, nft knftables.Interface, chain string, comment string) (*knftables.Rule, error) { + rules, err := nft.ListRules(ctx, chain) + if err != nil { + // Ignore not found error + if !knftables.IsNotFound(err) { + return nil, fmt.Errorf("failed to list rules in chain %s: %w", chain, err) + } + } + + for _, rule := range rules { + if rule.Comment != nil && *rule.Comment == comment { + return rule, nil + } + } + + return nil, nil +} + +// getPortRuleSections gets the port rule sections for a policy +func getPortRuleSections(ports []multiv1beta1.MultiNetworkPolicyPort) []string { + protocolToPorts := make(map[string][]string) + for _, port := range ports { + p := corev1.ProtocolTCP + if port.Protocol != nil { + p = *port.Protocol + } + + protocol := strings.ToLower(string(p)) + if port.Port != nil { + if port.EndPort != nil { + protocolToPorts[protocol] = append(protocolToPorts[protocol], fmt.Sprintf("%s-%d", port.Port.String(), *port.EndPort)) + } else { + // Handle both integer and string ports + if port.Port.Type == intstr.String { + // For named ports (strings), convert to lowercase + portStr := strings.ToLower(port.Port.StrVal) + protocolToPorts[protocol] = append(protocolToPorts[protocol], portStr) + } else { + protocolToPorts[protocol] = append(protocolToPorts[protocol], port.Port.String()) + } + } + } else { + if _, exists := protocolToPorts[protocol]; !exists { + protocolToPorts[protocol] = nil + } + } + } + + // Generate the rule sections for the ports + var portRuleSections []string + for protocol, ports := range protocolToPorts { + if ports == nil { + portRuleSections = append(portRuleSections, knftables.Concat("meta", "l4proto", protocol, "accept")) + } else { + // Anonymous set for the ports + joinedPorts := strings.Join(ports, ",") + portRuleSections = append(portRuleSections, knftables.Concat("meta", "l4proto", protocol, "th", "dport", "{", joinedPorts, "}", "accept")) + } + } + + return portRuleSections +} + +// createRules creates the rules for the policy chain +func createRules(tx *knftables.Transaction, npChainName string, ipRuleSections []string, portRuleSections []string, logger logr.Logger) { + if len(portRuleSections) == 0 { + logger.V(1).Info("No port restrictions specified, creating rules with just IP restrictions", "ipRuleSections", ipRuleSections) + for _, ipRuleSection := range ipRuleSections { + tx.Add(&knftables.Rule{ + Chain: npChainName, + Rule: knftables.Concat(ipRuleSection, "accept"), + }) + } + } else { + logger.V(1).Info("Port restrictions specified, creating rules with both IP and port restrictions", "ipRuleSections", ipRuleSections, "portRuleSections", portRuleSections) + for _, ipRuleSection := range ipRuleSections { + for _, portRuleSection := range portRuleSections { + tx.Add(&knftables.Rule{ + Chain: npChainName, + Rule: knftables.Concat(ipRuleSection, portRuleSection), + }) + } + } + } +} + +// peerInfo contains the information for a peer +type peerInfo struct { + pods []corev1.Pod + cidrs []string + excepts []string +} + +// parsePeers parses the peers and returns the peer info +func (n *NFTables) parsePeers(ctx context.Context, peers []multiv1beta1.MultiNetworkPolicyPeer, policyNamespace string, logger logr.Logger) (*peerInfo, error) { + logger.V(1).Info("Parsing peers", "peers", peers) + + var pods []corev1.Pod + var cidrs []string + var excepts []string + + // To avoid duplicates + podMap := make(map[string]corev1.Pod) + + for _, peer := range peers { + if peer.IPBlock != nil { + cidrs = append(cidrs, peer.IPBlock.CIDR) + excepts = append(excepts, peer.IPBlock.Except...) + + // When IPBlock is set, we don't need to check the other fields + continue + } + + switch { + case peer.NamespaceSelector != nil && peer.PodSelector != nil: + // When both namespace selector and pod selector are set, we first need to get the namespaces by namespace selector + // and then get the pods from the namespaces by pod selector + namespaces, err := n.getNamespacesByNamespaceSelector(ctx, peer.NamespaceSelector) + if err != nil { + return nil, fmt.Errorf("failed to get namespaces by namespace selector: %w", err) + } + + for _, ns := range namespaces { + namespacePods, err := n.getPodsByPodSelector(ctx, peer.PodSelector, ns.Name) + if err != nil { + return nil, fmt.Errorf("failed to get pods by pod selector: %w", err) + } + + for _, pod := range namespacePods { + podMap[pod.Namespace+"/"+pod.Name] = pod + } + } + case peer.NamespaceSelector != nil: + // When only namespace selector is set, we need to get the pods from the namespaces by namespace selector + // and then get all pods from the namespaces + namespaces, err := n.getNamespacesByNamespaceSelector(ctx, peer.NamespaceSelector) + if err != nil { + return nil, fmt.Errorf("failed to get namespaces by namespace selector: %w", err) + } + + for _, ns := range namespaces { + namespacePods, err := n.getPodsByNamespace(ctx, ns.Name) + if err != nil { + return nil, fmt.Errorf("failed to get pods by namespace: %w", err) + } + + for _, pod := range namespacePods { + podMap[pod.Namespace+"/"+pod.Name] = pod + } + } + case peer.PodSelector != nil: + // When only pod selector is set, we need to get the pods from the policy namespaces by pod selector + filteredPods, err := n.getPodsByPodSelector(ctx, peer.PodSelector, policyNamespace) + if err != nil { + return nil, fmt.Errorf("failed to get pods by pod selector: %w", err) + } + + for _, pod := range filteredPods { + podMap[pod.Namespace+"/"+pod.Name] = pod + } + } + } + + // Convert the map to a slice + for _, pod := range podMap { + pods = append(pods, pod) + } + + return &peerInfo{ + pods: pods, + cidrs: cidrs, + excepts: excepts, + }, nil +} + +// getPodsByPodSelector gets the pods by pod selector +func (n *NFTables) getPodsByPodSelector(ctx context.Context, selector *metav1.LabelSelector, namespace string) ([]corev1.Pod, error) { + pods := &corev1.PodList{} + + podSelector, err := metav1.LabelSelectorAsSelector(selector) + if err != nil { + // Invalid selector, skip + return nil, nil + } + + listOptions := []client.ListOption{ + client.MatchingFields{ + PodStatusIndex: string(corev1.PodRunning), + PodHostNetworkIndex: "false", + PodHasNetworkAnnotationIndex: "true", + }, + client.InNamespace(namespace), + } + + if !podSelector.Empty() { + listOptions = append(listOptions, client.MatchingLabelsSelector{Selector: podSelector}) + } + + err = n.Client.List(ctx, pods, listOptions...) + if err != nil { + return nil, fmt.Errorf("failed to list pods: %w", err) + } + + return pods.Items, nil +} + +// getPodsByNamespace gets the pods by namespace +func (n *NFTables) getPodsByNamespace(ctx context.Context, namespace string) ([]corev1.Pod, error) { + pods := &corev1.PodList{} + + err := n.Client.List(ctx, pods, client.InNamespace(namespace), client.MatchingFields{ + PodStatusIndex: string(corev1.PodRunning), + PodHostNetworkIndex: "false", + PodHasNetworkAnnotationIndex: "true", + }) + if err != nil { + return nil, fmt.Errorf("failed to list pods: %w", err) + } + + return pods.Items, nil +} + +// getNamespacesByNamespaceSelector gets the namespaces by namespace selector +func (n *NFTables) getNamespacesByNamespaceSelector(ctx context.Context, selector *metav1.LabelSelector) ([]corev1.Namespace, error) { + namespaces := &corev1.NamespaceList{} + + namespaceSelector, err := metav1.LabelSelectorAsSelector(selector) + if err != nil { + // Invalid selector, skip + return nil, nil + } + + listOptions := []client.ListOption{} + + if !namespaceSelector.Empty() { + listOptions = append(listOptions, client.MatchingLabelsSelector{Selector: namespaceSelector}) + } + + err = n.Client.List(ctx, namespaces, listOptions...) + if err != nil { + return nil, fmt.Errorf("failed to list namespaces: %w", err) + } + + return namespaces.Items, nil +} + +// getPodInterfacesMap returns a map of valid interfaces per pod +func getPodInterfacesMap(pods []corev1.Pod, networks []string) map[string][]Interface { + // Create a map of valid interfaces per pod + podInterfacesMap := make(map[string][]Interface) + for _, pod := range pods { + podInterfacesMap[pod.Name+"/"+pod.Namespace] = getMatchedInterfaces(getInterfaces(&pod), networks) + } + + return podInterfacesMap +} + +// classifyAddresses classifies the IP addresses into IPv4 and IPv6 +func classifyAddresses(interfacesPerPod map[string][]Interface, network string) ([]string, []string) { + var ipv4Addresses []string + var ipv6Addresses []string + + for _, interfaces := range interfacesPerPod { + for _, intf := range interfaces { + if intf.Network == network { + for _, ip := range intf.IPs { + // Parse the IP address to validate and classify it + parsedIP := net.ParseIP(ip) + if parsedIP == nil { + // Invalid IP address, skip it + continue + } + + // Check if it's IPv4 (including IPv4-mapped IPv6) + if parsedIP.To4() != nil { + ipv4Addresses = append(ipv4Addresses, ip) + } else { + ipv6Addresses = append(ipv6Addresses, ip) + } + } + } + } + } + + return ipv4Addresses, ipv6Addresses +} + +// createAndPopulateIPSet creates and populates an IP set +func createAndPopulateIPSet(tx *knftables.Transaction, name string, setType string, setComment string, addresses []string, needsIntervalFlag bool) { + // Create and populate the set + + set := &knftables.Set{ + Name: name, + Type: setType, + Comment: knftables.PtrTo(setComment), + } + + if needsIntervalFlag { + set.Flags = []knftables.SetFlag{knftables.IntervalFlag} + } + + tx.Add(set) + + for _, address := range addresses { + tx.Add(&knftables.Element{ + Set: name, + Key: []string{address}, + }) + } +} diff --git a/pkg/nftables/nftables.go b/pkg/nftables/nftables.go new file mode 100644 index 00000000..874461d3 --- /dev/null +++ b/pkg/nftables/nftables.go @@ -0,0 +1,240 @@ +// Package nftables provides the tools to apply nftables rules based on the MultiNetworkPolicy resource +package nftables + +import ( + "context" + "errors" + "fmt" + "slices" + "strings" + + "github.com/containernetworking/plugins/pkg/ns" + "github.com/go-logr/logr" + multiv1beta1 "github.com/k8snetworkplumbingwg/multi-networkpolicy/pkg/apis/k8s.cni.cncf.io/v1beta1" + netdefutils "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/utils" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/k8snetworkplumbingwg/multi-network-policy-nftables/pkg/cri" + "github.com/k8snetworkplumbingwg/multi-network-policy-nftables/pkg/datastore" +) + +const ( + tableName = "multi_networkpolicy" + + inputChain = "input" + outputChain = "output" + ingressChain = "ingress" + egressChain = "egress" + commonIngressChain = "common-ingress" + commonEgressChain = "common-egress" + + dropRuleComment = "Drop rule" + connectionTrackingRuleComment = "Connection tracking" + jumpCommonRuleComment = "Jump to common" + + prefixManagedInterfacesSet = "smi-" + prefixNetworkPolicyChain = "cnp-" + prefixNetworkPolicySet = "snp-" + + PodHostnameIndex = "pod.spec.nodeName" + PodStatusIndex = "pod.status.phase" + PodHostNetworkIndex = "pod.spec.hostNetwork" + PodHasNetworkAnnotationIndex = "k8s.v1.cni.cncf.io/networks" +) + +type SyncInterface interface { + SyncPolicy(ctx context.Context, policy *datastore.Policy, operation SyncOperation, logger logr.Logger) error +} + +var _ SyncInterface = &NFTables{} + +// NFTables is the struct that contains the nftables client and the datastore +type NFTables struct { + client.Client + Hostname string + CriRuntime *cri.Runtime + CommonRules *CommonRules +} + +type SyncError struct { + message string +} + +func (e *SyncError) Error() string { + return e.message +} + +func NewSyncError(format string, args ...interface{}) *SyncError { + return &SyncError{message: fmt.Sprintf(format, args...)} +} + +// CommonRules represents the common rules to be applied to all policies +type CommonRules struct { + AcceptICMP bool + AcceptICMPv6 bool + + CustomIPv4IngressRules []string + CustomIPv6IngressRules []string + CustomIPv4EgressRules []string + CustomIPv6EgressRules []string +} + +// Interface represents a network interface +type Interface struct { + Name string + Network string + IPs []string +} + +type SyncOperation string + +const ( + SyncOperationCreate SyncOperation = "create" + SyncOperationDelete SyncOperation = "delete" +) + +// SyncPolicy syncs the policy to the nftables +func (n *NFTables) SyncPolicy(ctx context.Context, policy *datastore.Policy, operation SyncOperation, logger logr.Logger) error { + logger.Info("Syncing policy") + + pods := &corev1.PodList{} + err := n.Client.List(ctx, pods, + client.InNamespace(policy.Namespace), + client.MatchingFields{ + PodHostnameIndex: n.Hostname, + PodStatusIndex: string(corev1.PodRunning), + PodHostNetworkIndex: "false", + PodHasNetworkAnnotationIndex: "true", + }) + if err != nil { + return fmt.Errorf("failed to list pods for hostname %s: %w", n.Hostname, err) + } + + if len(pods.Items) == 0 { + logger.Info("No pods found to enforce policy, skipping") + return nil + } + + logger.Info("Found pods to enforce policy", "hostname", n.Hostname, "count", len(pods.Items)) + + // Generate nftables rules + for _, pod := range pods.Items { + logger := logger.WithValues("pod", pod.Name, "namespace", pod.Namespace) + + interfaces := getInterfaces(&pod) + + if len(interfaces) == 0 { + logger.V(1).Info("No interfaces found, skipping") + continue + } + + netnsPath, err := n.CriRuntime.GetPodNetNSPath(ctx, &pod) + if err != nil { + return fmt.Errorf("failed to get network namespace path: %w", err) + } + + netns, err := ns.GetNS(netnsPath) + if err != nil { + logger.V(1).Info("Failed to open network namespace, skipping") + continue + } + + // Use anonymous function to ensure netns is always closed for this iteration + err = func() error { + defer netns.Close() + return netns.Do(func(_ ns.NetNS) error { + var err error + if operation == SyncOperationDelete { + err = cleanUpPolicy(ctx, policy.Name, policy.Namespace, logger) + } + + if operation == SyncOperationCreate { + err = n.enforcePolicy(ctx, &pod, interfaces, policy, logger) + } + + if err != nil { + return NewSyncError("failed to enforce NFTables policies: %v", err) + } + + return nil + }) + }() + if err != nil { + // Check if this is an actual nftables error vs pod lifecycle error + var syncError *SyncError + if errors.As(err, &syncError) { + return err + } + + logger.Info("Pod lifecycle error, ignoring", "error", err) + } + } + + return nil +} + +// getInterfaces gets the interfaces for a pod +func getInterfaces(pod *corev1.Pod) []Interface { + networks, _ := netdefutils.ParsePodNetworkAnnotation(pod) + + networkNames := make([]string, 0, len(networks)) + for _, network := range networks { + networkNames = append(networkNames, network.Name) + } + + networkStatus, _ := netdefutils.GetNetworkStatus(pod) + + var interfaces []Interface + for _, status := range networkStatus { + var name string + var namespace string + + // Parse name + slashItems := strings.Split(status.Name, "/") + if len(slashItems) == 2 { + namespace = strings.TrimSpace(slashItems[0]) + name = strings.TrimSpace(slashItems[1]) + } else { + namespace = pod.Namespace + name = strings.TrimSpace(status.Name) + } + + // Check if network is in the list of networks + if !slices.Contains(networkNames, name) { + continue + } + + intf := Interface{ + Name: status.Interface, + Network: fmt.Sprintf("%s/%s", namespace, name), + IPs: status.IPs, + } + + interfaces = append(interfaces, intf) + } + + return interfaces +} + +// getMatchedInterfaces returns the interfaces that match the given networks +func getMatchedInterfaces(interfaces []Interface, networks []string) []Interface { + var matchedInterfaces []Interface + for _, intf := range interfaces { + if slices.Contains(networks, intf.Network) { + matchedInterfaces = append(matchedInterfaces, intf) + } + } + + return matchedInterfaces +} + +// checkPolicyTypes checks if the policy has ingress or egress enabled +func checkPolicyTypes(policy *datastore.Policy) (bool, bool) { + // if no policy types are specified, ingress is always set + if len(policy.Spec.PolicyTypes) == 0 { + return true, len(policy.Spec.Egress) > 0 + } + + return slices.Contains(policy.Spec.PolicyTypes, multiv1beta1.PolicyTypeIngress), slices.Contains(policy.Spec.PolicyTypes, multiv1beta1.PolicyTypeEgress) +} diff --git a/pkg/nftables/nftables_integration_test.go b/pkg/nftables/nftables_integration_test.go new file mode 100644 index 00000000..29ff571b --- /dev/null +++ b/pkg/nftables/nftables_integration_test.go @@ -0,0 +1,725 @@ +package nftables + +import ( + "context" + "fmt" + "runtime" + + "github.com/containernetworking/plugins/pkg/ns" + "github.com/containernetworking/plugins/pkg/testutils" + "github.com/go-logr/logr" + multiv1beta1 "github.com/k8snetworkplumbingwg/multi-networkpolicy/pkg/apis/k8s.cni.cncf.io/v1beta1" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + k8sruntime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/intstr" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/knftables" + + "github.com/k8snetworkplumbingwg/multi-network-policy-nftables/pkg/datastore" + "github.com/k8snetworkplumbingwg/multi-network-policy-nftables/pkg/utils" +) + +var _ = Describe("NFTables Simple Integration Tests", func() { + var ( + ctx context.Context + logger logr.Logger + targetPod *corev1.Pod + matchedInterfaces []Interface + actualHashName string + + // Test pods for comprehensive test + backendPod *corev1.Pod + frontendPod1 *corev1.Pod + frontendPod2 *corev1.Pod + databasePod *corev1.Pod + + // Test namespaces + prodNamespace *corev1.Namespace + devNamespace *corev1.Namespace + ) + + BeforeEach(func() { + ctx = context.Background() + logger = logr.Discard() + + // Create target pod (the one policies apply to) + targetPod = &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "target-pod", + Namespace: "test-ns", + Labels: map[string]string{"app": "web"}, + Annotations: map[string]string{ + "k8s.v1.cni.cncf.io/networks": "net1,net2", + "k8s.v1.cni.cncf.io/network-status": `[{"name":"test-ns/net1","interface":"eth1","ips":["10.0.1.1","2001:db8:1::1"],"dns":{}},{"name":"test-ns/net2","interface":"eth2","ips":["10.0.2.1","2001:db8:2::1"],"dns":{}}]`, + }, + }, + Spec: corev1.PodSpec{HostNetwork: false}, + Status: corev1.PodStatus{Phase: corev1.PodRunning}, + } + + matchedInterfaces = []Interface{ + {Name: "eth1", Network: "test-ns/net1", IPs: []string{"10.0.1.1", "2001:db8:1::1"}}, + {Name: "eth2", Network: "test-ns/net2", IPs: []string{"10.0.2.1", "2001:db8:2::1"}}, + } + + // Create test pods for comprehensive test + backendPod = createDualStackPod("backend-pod", "test-ns", + map[string]string{"app": "backend", "tier": "api"}, + "10.0.1.10", "10.0.2.10", "2001:db8:1::10", "2001:db8:2::10") + + frontendPod1 = createDualStackPod("frontend-pod1", "production", + map[string]string{"app": "frontend", "role": "web"}, + "10.0.1.20", "10.0.2.20", "2001:db8:1::20", "2001:db8:2::20") + + frontendPod2 = createDualStackPod("frontend-pod2", "production", + map[string]string{"app": "frontend", "role": "logs"}, + "10.0.1.21", "10.0.2.21", "2001:db8:1::21", "2001:db8:2::21") + + databasePod = createDualStackPod("database-pod", "development", + map[string]string{"app": "database", "tier": "data"}, + "10.0.1.30", "10.0.2.30", "2001:db8:1::30", "2001:db8:2::30") + + // Create test namespaces + prodNamespace = &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "production", + Labels: map[string]string{"env": "prod"}, + }, + } + devNamespace = &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "development", + Labels: map[string]string{"env": "dev"}, + }, + } + }) + + It("should handle deny-all policy", func() { + defer GinkgoRecover() + + netNS, err := testutils.NewNS() + Expect(err).NotTo(HaveOccurred()) + defer netNS.Close() + + err = netNS.Do(func(_ ns.NetNS) error { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + nft, err := knftables.New(knftables.InetFamily, "multi_networkpolicy") + if err != nil { + return err + } + + nftablesWithPods := &NFTables{ + Client: createFakeClient([]*corev1.Pod{targetPod}), + } + + policy := createDenyAllPolicy("deny-all", "test-ns") + actualHashName = utils.GetHashName(policy.Name, policy.Namespace) + + err = nftablesWithPods.enforcePolicy(ctx, targetPod, matchedInterfaces, policy, logger) + if err != nil { + return err + } + + // Verify basic structures exist + expectedSetElements := map[string]int{"smi-" + actualHashName: 2} + expectedPolicyRules := 4 // Deny-all has 4 reverse rules in ingress + + err = verifyChainAndRules(ctx, nft, actualHashName, expectedPolicyRules) + if err != nil { + return err + } + return verifySetAndElements(ctx, nft, expectedSetElements) + }) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should handle accept-all policy", func() { + defer GinkgoRecover() + + netNS, err := testutils.NewNS() + Expect(err).NotTo(HaveOccurred()) + defer netNS.Close() + + err = netNS.Do(func(_ ns.NetNS) error { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + nft, err := knftables.New(knftables.InetFamily, "multi_networkpolicy") + if err != nil { + return err + } + + nftablesWithPods := &NFTables{ + Client: createFakeClient([]*corev1.Pod{targetPod}), + } + + policy := createAcceptAllPolicy("accept-all", "test-ns") + actualHashName = utils.GetHashName(policy.Name, policy.Namespace) + + err = nftablesWithPods.enforcePolicy(ctx, targetPod, matchedInterfaces, policy, logger) + if err != nil { + return err + } + + // Verify basic structures exist + expectedSetElements := map[string]int{"smi-" + actualHashName: 2} + expectedPolicyRules := 8 // 2 ingress + 2 egress (1 per interface each) + 4 reverse rules in ingress + + err = verifyChainAndRules(ctx, nft, actualHashName, expectedPolicyRules) + if err != nil { + return err + } + return verifySetAndElements(ctx, nft, expectedSetElements) + }) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should handle accept-all with port restrictions", func() { + defer GinkgoRecover() + + netNS, err := testutils.NewNS() + Expect(err).NotTo(HaveOccurred()) + defer netNS.Close() + + err = netNS.Do(func(_ ns.NetNS) error { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + nft, err := knftables.New(knftables.InetFamily, "multi_networkpolicy") + if err != nil { + return err + } + + nftablesWithPods := &NFTables{ + Client: createFakeClient([]*corev1.Pod{targetPod}), + } + + policy := createAcceptAllWithPortsPolicy("accept-ports", "test-ns") + actualHashName = utils.GetHashName(policy.Name, policy.Namespace) + + err = nftablesWithPods.enforcePolicy(ctx, targetPod, matchedInterfaces, policy, logger) + if err != nil { + return err + } + + // Verify basic structures exist - ports create anonymous sets, so just count rules + expectedSetElements := map[string]int{"smi-" + actualHashName: 2} + expectedPolicyRules := 8 // Port restrictions create grouped rules + 4 reverse rules in ingress + + err = verifyChainAndRules(ctx, nft, actualHashName, expectedPolicyRules) + if err != nil { + return err + } + return verifySetAndElements(ctx, nft, expectedSetElements) + }) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should handle comprehensive stacked policy", func() { + defer GinkgoRecover() + + netNS, err := testutils.NewNS() + Expect(err).NotTo(HaveOccurred()) + defer netNS.Close() + + err = netNS.Do(func(_ ns.NetNS) error { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + nft, err := knftables.New(knftables.InetFamily, "multi_networkpolicy") + if err != nil { + return err + } + + nftablesWithPods := &NFTables{ + Client: createFakeClientWithNamespaces([]*corev1.Pod{targetPod, backendPod, frontendPod1, frontendPod2, databasePod}, + []*corev1.Namespace{prodNamespace, devNamespace}), + } + + policy := createComprehensivePolicy("comprehensive", "test-ns") + actualHashName = utils.GetHashName(policy.Name, policy.Namespace) + + err = nftablesWithPods.enforcePolicy(ctx, targetPod, matchedInterfaces, policy, logger) + if err != nil { + return err + } + + // Verify set and its elements exist + expectedSetElements := map[string]int{ + "smi-" + actualHashName: 2, // eth1, eth2 + // Just verify some key sets have elements + "snp-" + actualHashName + "_ingress_ipv4_eth1_0": 1, // backendPod ipv4 + "snp-" + actualHashName + "_ingress_ipv6_eth1_0": 1, // backendpod ipv6 + "snp-" + actualHashName + "_ingress_ipv4_eth2_0": 1, // backendPod ipv4 + "snp-" + actualHashName + "_ingress_ipv6_eth2_0": 1, // backendpod ipv6 + + "snp-" + actualHashName + "_ingress_ipv4_eth1_1": 2, // frontend1, frontend2 ipv4 + "snp-" + actualHashName + "_ingress_ipv6_eth1_1": 2, // frontend1, frontend2 ipv6 + "snp-" + actualHashName + "_ingress_ipv4_eth2_1": 2, // frontend1, frontend2 ipv4 + "snp-" + actualHashName + "_ingress_ipv6_eth2_1": 2, // frontend1, frontend2 ipv6 + + "snp-" + actualHashName + "_ingress_ipv4_cidr_2": 1, // cidr ipv4 + "snp-" + actualHashName + "_ingress_ipv4_except_2": 1, // except ipv4 + "snp-" + actualHashName + "_ingress_ipv6_cidr_2": 1, // cidr ipv4 + "snp-" + actualHashName + "_ingress_ipv6_except_2": 1, // except ipv4 + + "snp-" + actualHashName + "_egress_ipv4_eth1_0": 1, // frontend2 ipv4 + "snp-" + actualHashName + "_egress_ipv6_eth1_0": 1, // frontend2 ipv6 + "snp-" + actualHashName + "_egress_ipv4_eth2_0": 1, // frontend2 ipv4 + "snp-" + actualHashName + "_egress_ipv6_eth2_0": 1, // frontend2 ipv6 + } + + // 10 ingres + 4 egress + 4 reverse rules in ingress + expectedRules := 18 + + err = verifyChainAndRules(ctx, nft, actualHashName, expectedRules) + if err != nil { + return err + } + return verifySetAndElements(ctx, nft, expectedSetElements) + }) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should handle full livecycle", func() { + defer GinkgoRecover() + + netNS, err := testutils.NewNS() + Expect(err).NotTo(HaveOccurred()) + defer netNS.Close() + + err = netNS.Do(func(_ ns.NetNS) error { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + nft, err := knftables.New(knftables.InetFamily, "multi_networkpolicy") + if err != nil { + return err + } + + nftablesWithPods := &NFTables{ + Client: createFakeClientWithNamespaces([]*corev1.Pod{targetPod, backendPod, frontendPod1, frontendPod2, databasePod}, + []*corev1.Namespace{prodNamespace, devNamespace}), + } + + // Add deny all policy + policy := createDenyAllPolicy("deny-all", "test-ns") + actualHashNameDeny := utils.GetHashName(policy.Name, policy.Namespace) + + err = nftablesWithPods.enforcePolicy(ctx, targetPod, matchedInterfaces, policy, logger) + if err != nil { + return err + } + + // Verify basic structures exist + expectedSetElements := map[string]int{"smi-" + actualHashNameDeny: 2} + expectedPolicyRules := 4 // Deny-all has 4 reverse rules in ingress + + err = verifyChainAndRules(ctx, nft, actualHashNameDeny, expectedPolicyRules) + if err != nil { + return err + } + err = verifySetAndElements(ctx, nft, expectedSetElements) + if err != nil { + return err + } + + // Add comprehensive policy + policy = createComprehensivePolicy("comprehensive", "test-ns") + actualHashName = utils.GetHashName(policy.Name, policy.Namespace) + + err = nftablesWithPods.enforcePolicy(ctx, targetPod, matchedInterfaces, policy, logger) + if err != nil { + return err + } + + // Verify set and its elements exist + expectedSetElements = map[string]int{ + "smi-" + actualHashNameDeny: 2, + "smi-" + actualHashName: 2, // eth1, eth2 + // Just verify some key sets have elements + "snp-" + actualHashName + "_ingress_ipv4_eth1_0": 1, // backendPod ipv4 + "snp-" + actualHashName + "_ingress_ipv6_eth1_0": 1, // backendpod ipv6 + "snp-" + actualHashName + "_ingress_ipv4_eth2_0": 1, // backendPod ipv4 + "snp-" + actualHashName + "_ingress_ipv6_eth2_0": 1, // backendpod ipv6 + + "snp-" + actualHashName + "_ingress_ipv4_eth1_1": 2, // frontend1, frontend2 ipv4 + "snp-" + actualHashName + "_ingress_ipv6_eth1_1": 2, // frontend1, frontend2 ipv6 + "snp-" + actualHashName + "_ingress_ipv4_eth2_1": 2, // frontend1, frontend2 ipv4 + "snp-" + actualHashName + "_ingress_ipv6_eth2_1": 2, // frontend1, frontend2 ipv6 + + "snp-" + actualHashName + "_ingress_ipv4_cidr_2": 1, // cidr ipv4 + "snp-" + actualHashName + "_ingress_ipv4_except_2": 1, // except ipv4 + "snp-" + actualHashName + "_ingress_ipv6_cidr_2": 1, // cidr ipv4 + "snp-" + actualHashName + "_ingress_ipv6_except_2": 1, // except ipv4 + + "snp-" + actualHashName + "_egress_ipv4_eth1_0": 1, // frontend2 ipv4 + "snp-" + actualHashName + "_egress_ipv6_eth1_0": 1, // frontend2 ipv6 + "snp-" + actualHashName + "_egress_ipv4_eth2_0": 1, // frontend2 ipv4 + "snp-" + actualHashName + "_egress_ipv6_eth2_0": 1, // frontend2 ipv6 + } + + // 10 ingres + 4 egress + 4 reverse rules in ingress + expectedRules := 18 + + err = verifySetAndElements(ctx, nft, expectedSetElements) + if err != nil { + return err + } + + // Comprehensive policy should be in effect + err = verifyChainAndRules(ctx, nft, actualHashName, expectedRules) + if err != nil { + return err + } + + // Clean up comprehensive policy + err = cleanUpPolicy(ctx, policy.Name, policy.Namespace, logger) + if err != nil { + return err + } + + // Verify basic structures exist + expectedSetElements = map[string]int{"smi-" + actualHashNameDeny: 2} + expectedPolicyRules = 4 // Deny-all has 4 reverse rules in ingress + + err = verifySetAndElements(ctx, nft, expectedSetElements) + if err != nil { + return err + } + + err = verifyChainAndRules(ctx, nft, actualHashName, 14) + if err == nil { + return err + } + + return verifyChainAndRules(ctx, nft, actualHashNameDeny, expectedPolicyRules) + }) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should be able to clean up on a pod without nft objects", func() { + defer GinkgoRecover() + + netNS, err := testutils.NewNS() + Expect(err).NotTo(HaveOccurred()) + defer netNS.Close() + + err = netNS.Do(func(_ ns.NetNS) error { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + // Clean up comprehensive policy + return cleanUpPolicy(ctx, "policy-test", "namespace", logger) + }) + Expect(err).NotTo(HaveOccurred()) + }) +}) + +// Helper function to create a dual-stack pod +func createDualStackPod(name, namespace string, labels map[string]string, ipv4Net1, ipv4Net2, ipv6Net1, ipv6Net2 string) *corev1.Pod { + // We assume that the network attachment definition is common. There is no restriction per namespace + networkStatus := `[{"name":"test-ns/net1","interface":"eth1","ips":["` + ipv4Net1 + `","` + ipv6Net1 + `"],"dns":{}},{"name":"test-ns/net2","interface":"eth2","ips":["` + ipv4Net2 + `","` + ipv6Net2 + `"],"dns":{}}]` + + return &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: labels, + Annotations: map[string]string{ + "k8s.v1.cni.cncf.io/networks": "net1,net2", + "k8s.v1.cni.cncf.io/network-status": networkStatus, + }, + }, + Spec: corev1.PodSpec{HostNetwork: false}, + Status: corev1.PodStatus{Phase: corev1.PodRunning}, + } +} + +// Helper function to create a fake client with pods +func createFakeClient(pods []*corev1.Pod) client.Client { + scheme := k8sruntime.NewScheme() + _ = corev1.AddToScheme(scheme) + + objects := make([]client.Object, len(pods)) + for i, pod := range pods { + objects[i] = pod + } + + return fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(objects...). + WithIndex(&corev1.Pod{}, PodHostnameIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + return []string{pod.Spec.NodeName} + }). + WithIndex(&corev1.Pod{}, PodStatusIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + return []string{string(pod.Status.Phase)} + }). + WithIndex(&corev1.Pod{}, PodHostNetworkIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + return []string{fmt.Sprintf("%t", pod.Spec.HostNetwork)} + }). + WithIndex(&corev1.Pod{}, PodHasNetworkAnnotationIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + _, hasAnnotation := pod.Annotations["k8s.v1.cni.cncf.io/networks"] + return []string{fmt.Sprintf("%t", hasAnnotation)} + }). + Build() +} + +// Helper function to create a fake client with pods and namespaces +func createFakeClientWithNamespaces(pods []*corev1.Pod, namespaces []*corev1.Namespace) client.Client { + scheme := k8sruntime.NewScheme() + _ = corev1.AddToScheme(scheme) + + objects := make([]client.Object, len(pods)+len(namespaces)) + for i, pod := range pods { + objects[i] = pod + } + for i, ns := range namespaces { + objects[len(pods)+i] = ns + } + + return fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(objects...). + WithIndex(&corev1.Pod{}, PodHostnameIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + return []string{pod.Spec.NodeName} + }). + WithIndex(&corev1.Pod{}, PodStatusIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + return []string{string(pod.Status.Phase)} + }). + WithIndex(&corev1.Pod{}, PodHostNetworkIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + return []string{fmt.Sprintf("%t", pod.Spec.HostNetwork)} + }). + WithIndex(&corev1.Pod{}, PodHasNetworkAnnotationIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + _, hasAnnotation := pod.Annotations["k8s.v1.cni.cncf.io/networks"] + return []string{fmt.Sprintf("%t", hasAnnotation)} + }). + Build() +} + +// Policy creation helpers +func createDenyAllPolicy(name, namespace string) *datastore.Policy { + return &datastore.Policy{ + Name: name, + Namespace: namespace, + Networks: []string{"test-ns/net1", "test-ns/net2"}, + Spec: multiv1beta1.MultiNetworkPolicySpec{ + PodSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "web"}, + }, + PolicyTypes: []multiv1beta1.MultiPolicyType{ + multiv1beta1.PolicyTypeIngress, + multiv1beta1.PolicyTypeEgress, + }, + // Empty Ingress and Egress = deny all + }, + } +} + +func createAcceptAllPolicy(name, namespace string) *datastore.Policy { + return &datastore.Policy{ + Name: name, + Namespace: namespace, + Networks: []string{"test-ns/net1", "test-ns/net2"}, + Spec: multiv1beta1.MultiNetworkPolicySpec{ + PodSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "web"}, + }, + PolicyTypes: []multiv1beta1.MultiPolicyType{ + multiv1beta1.PolicyTypeIngress, + multiv1beta1.PolicyTypeEgress, + }, + Ingress: []multiv1beta1.MultiNetworkPolicyIngressRule{ + {}, // Empty From = accept all + }, + Egress: []multiv1beta1.MultiNetworkPolicyEgressRule{ + {}, // Empty To = accept all + }, + }, + } +} + +func createAcceptAllWithPortsPolicy(name, namespace string) *datastore.Policy { + endPort := int32(8010) + return &datastore.Policy{ + Name: name, + Namespace: namespace, + Networks: []string{"test-ns/net1", "test-ns/net2"}, + Spec: multiv1beta1.MultiNetworkPolicySpec{ + PodSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "web"}, + }, + PolicyTypes: []multiv1beta1.MultiPolicyType{ + multiv1beta1.PolicyTypeIngress, + multiv1beta1.PolicyTypeEgress, + }, + Ingress: []multiv1beta1.MultiNetworkPolicyIngressRule{ + { + Ports: []multiv1beta1.MultiNetworkPolicyPort{ + {Port: &intstr.IntOrString{Type: intstr.Int, IntVal: 80}}, // Specific port + {Port: &intstr.IntOrString{Type: intstr.String, StrVal: "https"}}, // Named port + {Port: &intstr.IntOrString{Type: intstr.Int, IntVal: 8000}, EndPort: &endPort}, // Port range + }, + }, + }, + Egress: []multiv1beta1.MultiNetworkPolicyEgressRule{ + { + Ports: []multiv1beta1.MultiNetworkPolicyPort{ + {Port: &intstr.IntOrString{Type: intstr.Int, IntVal: 443}}, // HTTPS egress + }, + }, + }, + }, + } +} + +func createComprehensivePolicy(name, namespace string) *datastore.Policy { + endPort := int32(8010) + return &datastore.Policy{ + Name: name, + Namespace: namespace, + Networks: []string{"test-ns/net1", "test-ns/net2"}, + Spec: multiv1beta1.MultiNetworkPolicySpec{ + PodSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "web"}, + }, + PolicyTypes: []multiv1beta1.MultiPolicyType{ + multiv1beta1.PolicyTypeIngress, + multiv1beta1.PolicyTypeEgress, + }, + Ingress: []multiv1beta1.MultiNetworkPolicyIngressRule{ + { + // Rule 0: Pod selector + From: []multiv1beta1.MultiNetworkPolicyPeer{ + { + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "backend"}, + }, + }, + }, + Ports: []multiv1beta1.MultiNetworkPolicyPort{ + {Port: &intstr.IntOrString{Type: intstr.Int, IntVal: 80}}, // Specific port + {Port: &intstr.IntOrString{Type: intstr.String, StrVal: "https"}}, // Named port + {Port: &intstr.IntOrString{Type: intstr.Int, IntVal: 8000}, EndPort: &endPort}, // Port range + }, + }, + { + // Rule 1: Namespace selector + From: []multiv1beta1.MultiNetworkPolicyPeer{ + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"env": "prod"}, + }, + }, + }, + }, + { + // Rule 2: IPBlock with exceptions + From: []multiv1beta1.MultiNetworkPolicyPeer{ + { + IPBlock: &multiv1beta1.IPBlock{ + CIDR: "10.0.0.0/8", + Except: []string{"10.1.0.0/16"}, + }, + }, + { + IPBlock: &multiv1beta1.IPBlock{ + CIDR: "2001:db8::/32", + Except: []string{"2001:db8:1::/48"}, + }, + }, + }, + Ports: []multiv1beta1.MultiNetworkPolicyPort{ + {Port: &intstr.IntOrString{Type: intstr.Int, IntVal: 80}}, // Specific port + {Port: &intstr.IntOrString{Type: intstr.String, StrVal: "https"}}, // Named port + {Port: &intstr.IntOrString{Type: intstr.Int, IntVal: 8000}, EndPort: &endPort}, // Port range + }, + }, + }, + Egress: []multiv1beta1.MultiNetworkPolicyEgressRule{ + { + // Egress to database pods + To: []multiv1beta1.MultiNetworkPolicyPeer{ + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"env": "prod"}, + }, + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "frontend", "role": "logs"}, + }, + }, + }, + }, + }, + }, + } +} + +// verifyChainAndRules verifies the chain and rule count for a given policy +func verifyChainAndRules(ctx context.Context, nft knftables.Interface, hashName string, expectedRules int) error { + // Get policy chain + chainName := fmt.Sprintf("cnp-%s", hashName) + rules, err := nft.ListRules(ctx, chainName) + if err != nil { + return err + } + + // Verify rule count + if len(rules) != expectedRules { + return fmt.Errorf("expected %d rules in policy chain, got %d", expectedRules, len(rules)) + } + + return nil +} + +// verifySetAndElements verifies the sets and their element counts +func verifySetAndElements(ctx context.Context, nft knftables.Interface, expectedSetElements map[string]int) error { + // Get all sets + sets, err := nft.List(ctx, "sets") + if err != nil { + return err + } + + // Verify set count + if len(sets) != len(expectedSetElements) { + return fmt.Errorf("expected %d sets, got %d", len(expectedSetElements), len(sets)) + } + + // Verify expected sets exist + for expectedSet, expectedElements := range expectedSetElements { + found := false + for _, set := range sets { + if set == expectedSet { + found = true + // Verify element count + elements, err := nft.ListElements(ctx, "set", expectedSet) + if err != nil { + return err + } + if len(elements) != expectedElements { + return fmt.Errorf("expected %d elements in set %s, got %d", expectedElements, expectedSet, len(elements)) + } + break + } + } + if !found { + return fmt.Errorf("expected set %s not found", expectedSet) + } + } + + return nil +} diff --git a/pkg/nftables/nftables_test.go b/pkg/nftables/nftables_test.go new file mode 100644 index 00000000..b16e1c4c --- /dev/null +++ b/pkg/nftables/nftables_test.go @@ -0,0 +1,6076 @@ +package nftables + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/go-logr/logr" + multiv1beta1 "github.com/k8snetworkplumbingwg/multi-networkpolicy/pkg/apis/k8s.cni.cncf.io/v1beta1" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/intstr" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/knftables" + + "github.com/k8snetworkplumbingwg/multi-network-policy-nftables/pkg/datastore" +) + +func TestNFTablesUnit(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "NFTables Suite") +} + +var _ = Describe("NFTables Functions", func() { + Context("getInterfaces", func() { + var pod *corev1.Pod + + BeforeEach(func() { + pod = &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod", + Namespace: "default", + }, + } + }) + + Context("when pod has no network annotations", func() { + It("should return empty interfaces slice", func() { + interfaces := getInterfaces(pod) + Expect(interfaces).To(BeEmpty()) + }) + }) + + Context("when pod has network annotation but no status", func() { + BeforeEach(func() { + pod.Annotations = map[string]string{ + "k8s.v1.cni.cncf.io/networks": "net1", + } + }) + + It("should return empty interfaces slice", func() { + interfaces := getInterfaces(pod) + Expect(interfaces).To(BeEmpty()) + }) + }) + + Context("when pod has network status but no network annotation", func() { + BeforeEach(func() { + pod.Annotations = map[string]string{ + "k8s.v1.cni.cncf.io/network-status": `[ + { + "name": "default/net1", + "interface": "eth1", + "ips": ["10.0.0.1"] + } + ]`, + } + }) + + It("should return empty interfaces slice", func() { + interfaces := getInterfaces(pod) + Expect(interfaces).To(BeEmpty()) + }) + }) + + Context("when pod has both network annotation and status", func() { + Context("with single network", func() { + BeforeEach(func() { + pod.Annotations = map[string]string{ + "k8s.v1.cni.cncf.io/networks": "net1", + "k8s.v1.cni.cncf.io/network-status": `[ + { + "name": "default/net1", + "interface": "eth1", + "ips": ["10.0.0.1"] + } + ]`, + } + }) + + It("should return single interface", func() { + interfaces := getInterfaces(pod) + Expect(interfaces).To(HaveLen(1)) + Expect(interfaces[0]).To(Equal(Interface{ + Name: "eth1", + Network: "default/net1", + IPs: []string{"10.0.0.1"}, + })) + }) + }) + + Context("with multiple networks", func() { + BeforeEach(func() { + pod.Annotations = map[string]string{ + "k8s.v1.cni.cncf.io/networks": "net1,net2", + "k8s.v1.cni.cncf.io/network-status": `[ + { + "name": "default/net1", + "interface": "eth1", + "ips": ["10.0.0.1"] + }, + { + "name": "default/net2", + "interface": "eth2", + "ips": ["10.0.0.2", "2001:db8::1"] + } + ]`, + } + }) + + It("should return multiple interfaces", func() { + interfaces := getInterfaces(pod) + Expect(interfaces).To(HaveLen(2)) + + // Sort interfaces by network name for consistent testing + if len(interfaces) == 2 && interfaces[0].Network == "net2" { + interfaces[0], interfaces[1] = interfaces[1], interfaces[0] + } + + Expect(interfaces[0]).To(Equal(Interface{ + Name: "eth1", + Network: "default/net1", + IPs: []string{"10.0.0.1"}, + })) + Expect(interfaces[1]).To(Equal(Interface{ + Name: "eth2", + Network: "default/net2", + IPs: []string{"10.0.0.2", "2001:db8::1"}, + })) + }) + }) + + Context("with namespaced network annotation", func() { + BeforeEach(func() { + pod.Annotations = map[string]string{ + "k8s.v1.cni.cncf.io/networks": "test-namespace/net1", + "k8s.v1.cni.cncf.io/network-status": `[ + { + "name": "test-namespace/net1", + "interface": "eth1", + "ips": ["10.0.0.1"] + } + ]`, + } + }) + + It("should return interface with correct network name", func() { + interfaces := getInterfaces(pod) + Expect(interfaces).To(HaveLen(1)) + Expect(interfaces[0]).To(Equal(Interface{ + Name: "eth1", + Network: "test-namespace/net1", + IPs: []string{"10.0.0.1"}, + })) + }) + }) + + Context("with mixed namespaced and non-namespaced networks", func() { + BeforeEach(func() { + pod.Annotations = map[string]string{ + "k8s.v1.cni.cncf.io/networks": "test-namespace/net1,net2", + "k8s.v1.cni.cncf.io/network-status": `[ + { + "name": "test-namespace/net1", + "interface": "eth1", + "ips": ["10.0.0.1"] + }, + { + "name": "net2", + "interface": "eth2", + "ips": ["10.0.0.2"] + } + ]`, + } + }) + + It("should return both interfaces with correct network names", func() { + interfaces := getInterfaces(pod) + Expect(interfaces).To(HaveLen(2)) + + // Sort interfaces by network name for consistent testing + if len(interfaces) == 2 && interfaces[0].Network == "net2" { + interfaces[0], interfaces[1] = interfaces[1], interfaces[0] + } + + Expect(interfaces[0]).To(Equal(Interface{ + Name: "eth1", + Network: "test-namespace/net1", + IPs: []string{"10.0.0.1"}, + })) + Expect(interfaces[1]).To(Equal(Interface{ + Name: "eth2", + Network: "default/net2", + IPs: []string{"10.0.0.2"}, + })) + }) + }) + + Context("when network status has networks not in annotation", func() { + BeforeEach(func() { + pod.Annotations = map[string]string{ + "k8s.v1.cni.cncf.io/networks": "net1", + "k8s.v1.cni.cncf.io/network-status": `[ + { + "name": "default/net1", + "interface": "eth1", + "ips": ["10.0.0.1"] + }, + { + "name": "default/net2", + "interface": "eth2", + "ips": ["10.0.0.2"] + } + ]`, + } + }) + + It("should only return interfaces for networks in annotation", func() { + interfaces := getInterfaces(pod) + Expect(interfaces).To(HaveLen(1)) + Expect(interfaces[0]).To(Equal(Interface{ + Name: "eth1", + Network: "default/net1", + IPs: []string{"10.0.0.1"}, + })) + }) + }) + + Context("when network annotation has networks not in status", func() { + BeforeEach(func() { + pod.Annotations = map[string]string{ + "k8s.v1.cni.cncf.io/networks": "net1,net2", + "k8s.v1.cni.cncf.io/network-status": `[ + { + "name": "default/net1", + "interface": "eth1", + "ips": ["10.0.0.1"] + } + ]`, + } + }) + + It("should only return interfaces for networks with status", func() { + interfaces := getInterfaces(pod) + Expect(interfaces).To(HaveLen(1)) + Expect(interfaces[0]).To(Equal(Interface{ + Name: "eth1", + Network: "default/net1", + IPs: []string{"10.0.0.1"}, + })) + }) + }) + + Context("with complex network names and spacing", func() { + BeforeEach(func() { + pod.Annotations = map[string]string{ + "k8s.v1.cni.cncf.io/networks": `[ + {"name": "net-1"}, + {"name": "net_2"} + ]`, + "k8s.v1.cni.cncf.io/network-status": `[ + { + "name": "default/net-1", + "interface": "eth1", + "ips": ["10.0.0.1"] + }, + { + "name": "default/net_2", + "interface": "eth2", + "ips": ["10.0.0.2"] + } + ]`, + } + }) + + It("should handle network names with dashes and underscores", func() { + interfaces := getInterfaces(pod) + Expect(interfaces).To(HaveLen(2)) + + // Sort interfaces by network name for consistent testing + if len(interfaces) == 2 && interfaces[0].Network == "net_2" { + interfaces[0], interfaces[1] = interfaces[1], interfaces[0] + } + + Expect(interfaces[0]).To(Equal(Interface{ + Name: "eth1", + Network: "default/net-1", + IPs: []string{"10.0.0.1"}, + })) + Expect(interfaces[1]).To(Equal(Interface{ + Name: "eth2", + Network: "default/net_2", + IPs: []string{"10.0.0.2"}, + })) + }) + }) + + Context("with empty IPs array", func() { + BeforeEach(func() { + pod.Annotations = map[string]string{ + "k8s.v1.cni.cncf.io/networks": "net1", + "k8s.v1.cni.cncf.io/network-status": `[ + { + "name": "default/net1", + "interface": "eth1", + "ips": [] + } + ]`, + } + }) + + It("should return interface with empty IPs", func() { + interfaces := getInterfaces(pod) + Expect(interfaces).To(HaveLen(1)) + Expect(interfaces[0]).To(Equal(Interface{ + Name: "eth1", + Network: "default/net1", + IPs: []string{}, + })) + }) + }) + + Context("with nil IPs field", func() { + BeforeEach(func() { + pod.Annotations = map[string]string{ + "k8s.v1.cni.cncf.io/networks": "net1", + "k8s.v1.cni.cncf.io/network-status": `[ + { + "name": "default/net1", + "interface": "eth1" + } + ]`, + } + }) + + It("should return interface with nil IPs", func() { + interfaces := getInterfaces(pod) + Expect(interfaces).To(HaveLen(1)) + Expect(interfaces[0].Name).To(Equal("eth1")) + Expect(interfaces[0].Network).To(Equal("default/net1")) + Expect(interfaces[0].IPs).To(BeNil()) + }) + }) + + Context("with invalid JSON in network status", func() { + BeforeEach(func() { + pod.Annotations = map[string]string{ + "k8s.v1.cni.cncf.io/networks": "net1", + "k8s.v1.cni.cncf.io/network-status": `invalid json`, + } + }) + + It("should return empty interfaces slice", func() { + interfaces := getInterfaces(pod) + Expect(interfaces).To(BeEmpty()) + }) + }) + + Context("with malformed network annotation", func() { + BeforeEach(func() { + pod.Annotations = map[string]string{ + "k8s.v1.cni.cncf.io/networks": `[{"name": "net1"}]`, // JSON instead of string + "k8s.v1.cni.cncf.io/network-status": `[ + { + "name": "default/net1", + "interface": "eth1", + "ips": ["10.0.0.1"] + } + ]`, + } + }) + + It("should handle parsing error gracefully", func() { + interfaces := getInterfaces(pod) + // Should not panic and return empty or handle gracefully + // The exact behavior depends on netdefutils.ParsePodNetworkAnnotation implementation + Expect(interfaces).NotTo(BeNil()) + }) + }) + }) + + Context("edge cases", func() { + Context("when pod is nil", func() { + It("should panic (expected behavior)", func() { + Expect(func() { + getInterfaces(nil) + }).To(Panic()) + }) + }) + + Context("when pod has no annotations", func() { + BeforeEach(func() { + pod.Annotations = nil + }) + + It("should return empty interfaces slice", func() { + interfaces := getInterfaces(pod) + Expect(interfaces).To(BeEmpty()) + }) + }) + + Context("when pod has empty annotations map", func() { + BeforeEach(func() { + pod.Annotations = map[string]string{} + }) + + It("should return empty interfaces slice", func() { + interfaces := getInterfaces(pod) + Expect(interfaces).To(BeEmpty()) + }) + }) + }) + + Context("real-world scenarios", func() { + Context("with Multus CNI typical annotation format", func() { + BeforeEach(func() { + pod.Annotations = map[string]string{ + "k8s.v1.cni.cncf.io/networks": `[ + {"name": "macvlan-net", "namespace": "default"}, + {"name": "sriov-net", "namespace": "kube-system"} + ]`, + "k8s.v1.cni.cncf.io/network-status": `[ + { + "name": "default/macvlan-net", + "interface": "net1", + "ips": ["192.168.1.100"], + "mac": "02:42:c0:a8:01:64" + }, + { + "name": "kube-system/sriov-net", + "interface": "net2", + "ips": ["10.56.217.100", "2001:db8::100"], + "mac": "02:42:0a:38:d9:64" + } + ]`, + } + }) + + It("should return interfaces with correct network names", func() { + interfaces := getInterfaces(pod) + Expect(interfaces).To(HaveLen(2)) + + // Sort interfaces by network name for consistent testing + if len(interfaces) == 2 && interfaces[0].Network == "sriov-net" { + interfaces[0], interfaces[1] = interfaces[1], interfaces[0] + } + + Expect(interfaces[0]).To(Equal(Interface{ + Name: "net1", + Network: "default/macvlan-net", + IPs: []string{"192.168.1.100"}, + })) + Expect(interfaces[1]).To(Equal(Interface{ + Name: "net2", + Network: "kube-system/sriov-net", + IPs: []string{"10.56.217.100", "2001:db8::100"}, + })) + }) + }) + + Context("with simple string format for networks", func() { + BeforeEach(func() { + pod.Annotations = map[string]string{ + "k8s.v1.cni.cncf.io/networks": "macvlan-net@eth1,sriov-net@eth2", + "k8s.v1.cni.cncf.io/network-status": `[ + { + "name": "default/macvlan-net", + "interface": "eth1", + "ips": ["192.168.1.100"] + }, + { + "name": "default/sriov-net", + "interface": "eth2", + "ips": ["10.56.217.100"] + } + ]`, + } + }) + + It("should return interfaces correctly", func() { + interfaces := getInterfaces(pod) + Expect(interfaces).To(HaveLen(2)) + + // Sort interfaces by network name for consistent testing + if len(interfaces) == 2 && interfaces[0].Network == "sriov-net" { + interfaces[0], interfaces[1] = interfaces[1], interfaces[0] + } + + Expect(interfaces[0]).To(Equal(Interface{ + Name: "eth1", + Network: "default/macvlan-net", + IPs: []string{"192.168.1.100"}, + })) + Expect(interfaces[1]).To(Equal(Interface{ + Name: "eth2", + Network: "default/sriov-net", + IPs: []string{"10.56.217.100"}, + })) + }) + }) + }) + }) + + Context("ensureBasicStructure", func() { + var ( + ctx context.Context + nft knftables.Interface + logger logr.Logger + ) + + BeforeEach(func() { + ctx = context.Background() + nft = knftables.NewFake(knftables.InetFamily, tableName) + logger = logr.Discard() + }) + + It("should create all required chains and rules", func() { + err := ensureBasicStructure(ctx, nft, nil, logger) + Expect(err).NotTo(HaveOccurred()) + + // Get all generated rules using Dump() + dump := nft.(*knftables.Fake).Dump() + + // Split the dump into lines for easier verification + dumpLines := strings.Split(dump, "\n") + + // Expected rules that should be generated (based on actual output) + expectedRules := []string{ + "add table inet multi_networkpolicy { comment \"MultiNetworkPolicy\" ; }", + "add chain inet multi_networkpolicy common-egress { comment \"Common Policies\" ; }", + "add chain inet multi_networkpolicy common-ingress { comment \"Common Policies\" ; }", + "add chain inet multi_networkpolicy egress { comment \"Egress Policies\" ; }", + "add chain inet multi_networkpolicy ingress { comment \"Ingress Policies\" ; }", + "add chain inet multi_networkpolicy input { type filter hook input priority 0 ; comment \"Input Dispatcher\" ; }", + "add chain inet multi_networkpolicy output { type filter hook output priority 0 ; comment \"Output Dispatcher\" ; }", + "add rule inet multi_networkpolicy egress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy egress jump common-egress comment \"Jump to common\"", + "add rule inet multi_networkpolicy egress drop comment \"Drop rule\"", + "add rule inet multi_networkpolicy ingress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy ingress jump common-ingress comment \"Jump to common\"", + "add rule inet multi_networkpolicy ingress drop comment \"Drop rule\"", + "", // Empty line at the end + } + + // Verify exact number of expected rules + Expect(dumpLines).To(HaveLen(len(expectedRules)), "Expected exactly %d rules, but got %d. Rules: %v", len(expectedRules), len(dumpLines), dumpLines) + + // Verify each expected rule exists completely + for _, expectedRule := range expectedRules { + found := false + for _, actualRule := range dumpLines { + if strings.Contains(actualRule, expectedRule) { + found = true + break + } + } + Expect(found).To(BeTrue(), "Expected rule not found: %s\nActual rules: %v", expectedRule, dumpLines) + } + }) + }) + + Context("createManagedInterfacesSet", func() { + var ( + ctx context.Context + nft knftables.Interface + logger logr.Logger + ) + + BeforeEach(func() { + ctx = context.Background() + nft = knftables.NewFake(knftables.InetFamily, tableName) + logger = logr.Discard() + }) + + It("should create set with no elements for empty interfaces", func() { + // First create the table + tx := nft.NewTransaction() + tx.Add(&knftables.Table{ + Comment: knftables.PtrTo("MultiNetworkPolicy"), + }) + err := nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + // Now create the managed interfaces set + tx = nft.NewTransaction() + interfaces := []Interface{} // Empty interfaces + hashName := "test123" + policyNamespace := "test-ns" + policyName := "test-policy" + + createManagedInterfacesSet(tx, interfaces, hashName, policyNamespace, policyName, logger) + + // Run transaction to generate rules + err = nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + // Get all generated rules using Dump() + dump := nft.(*knftables.Fake).Dump() + dumpLines := strings.Split(dump, "\n") + + // Expected rules: table + set creation, no elements + expectedRules := []string{ + "add table inet multi_networkpolicy", + "add set inet multi_networkpolicy smi-test123 { type ifname ; comment \"Managed interfaces set for test-ns/test-policy\" ; }", + "", // Empty line at the end + } + + // Verify exact number of expected rules + Expect(dumpLines).To(HaveLen(len(expectedRules)), "Expected exactly %d rules, but got %d. Rules: %v", len(expectedRules), len(dumpLines), dumpLines) + + // Verify each expected rule exists completely + for _, expectedRule := range expectedRules { + found := false + for _, actualRule := range dumpLines { + if strings.Contains(actualRule, expectedRule) { + found = true + break + } + } + Expect(found).To(BeTrue(), "Expected rule not found: %s\nActual rules: %v", expectedRule, dumpLines) + } + }) + + It("should create set with elements for multiple interfaces", func() { + // First create the table + tx := nft.NewTransaction() + tx.Add(&knftables.Table{ + Comment: knftables.PtrTo("MultiNetworkPolicy"), + }) + err := nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + // Now create the managed interfaces set + tx = nft.NewTransaction() + interfaces := []Interface{ + {Name: "eth1", Network: "default/net1", IPs: []string{"10.0.0.1"}}, + {Name: "eth2", Network: "default/net2", IPs: []string{"10.0.0.2"}}, + {Name: "eth3", Network: "default/net3", IPs: []string{"10.0.0.3"}}, + } + hashName := "abc456" + policyNamespace := "prod-ns" + policyName := "prod-policy" + + createManagedInterfacesSet(tx, interfaces, hashName, policyNamespace, policyName, logger) + + // Run transaction to generate rules + err = nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + // Get all generated rules using Dump() + dump := nft.(*knftables.Fake).Dump() + dumpLines := strings.Split(dump, "\n") + + // Expected rules: table + set creation + 3 elements + expectedRules := []string{ + "add table inet multi_networkpolicy", + "add set inet multi_networkpolicy smi-abc456 { type ifname ; comment \"Managed interfaces set for prod-ns/prod-policy\" ; }", + "add element inet multi_networkpolicy smi-abc456 { eth1 }", + "add element inet multi_networkpolicy smi-abc456 { eth2 }", + "add element inet multi_networkpolicy smi-abc456 { eth3 }", + "", // Empty line at the end + } + + // Verify exact number of expected rules + Expect(dumpLines).To(HaveLen(len(expectedRules)), "Expected exactly %d rules, but got %d. Rules: %v", len(expectedRules), len(dumpLines), dumpLines) + + // Verify each expected rule exists completely + for _, expectedRule := range expectedRules { + found := false + for _, actualRule := range dumpLines { + if strings.Contains(actualRule, expectedRule) { + found = true + break + } + } + Expect(found).To(BeTrue(), "Expected rule not found: %s\nActual rules: %v", expectedRule, dumpLines) + } + }) + }) + + Context("createPolicyChain", func() { + var ( + ctx context.Context + nft knftables.Interface + logger logr.Logger + ) + + BeforeEach(func() { + ctx = context.Background() + nft = knftables.NewFake(knftables.InetFamily, tableName) + logger = logr.Discard() + }) + + It("should create policy chain with rules", func() { + // First ensure basic structure exists + err := ensureBasicStructure(ctx, nft, nil, logger) + Expect(err).NotTo(HaveOccurred()) + + // Create policy chain + tx := nft.NewTransaction() + npChainName := "pi-abc123" + policyTypeChainName := "ingress" + policyNamespace := "test-ns" + policyName := "test-policy" + + err = createPolicyChain(ctx, nft, tx, npChainName, policyTypeChainName, policyNamespace, policyName, logger) + Expect(err).NotTo(HaveOccurred()) + + // Run transaction to generate rules + err = nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + // Get all generated rules using Dump() + dump := nft.(*knftables.Fake).Dump() + dumpLines := strings.Split(dump, "\n") + + // Expected rules: basic structure + new policy chain + jump rule + expectedRules := []string{ + "add table inet multi_networkpolicy { comment \"MultiNetworkPolicy\" ; }", + "add chain inet multi_networkpolicy common-egress { comment \"Common Policies\" ; }", + "add chain inet multi_networkpolicy common-ingress { comment \"Common Policies\" ; }", + "add chain inet multi_networkpolicy egress { comment \"Egress Policies\" ; }", + "add chain inet multi_networkpolicy ingress { comment \"Ingress Policies\" ; }", + "add chain inet multi_networkpolicy input { type filter hook input priority 0 ; comment \"Input Dispatcher\" ; }", + "add chain inet multi_networkpolicy output { type filter hook output priority 0 ; comment \"Output Dispatcher\" ; }", + "add chain inet multi_networkpolicy pi-abc123 { comment \"MultiNetworkPolicy test-ns/test-policy\" ; }", + "add rule inet multi_networkpolicy egress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy egress jump common-egress comment \"Jump to common\"", + "add rule inet multi_networkpolicy egress drop comment \"Drop rule\"", + "add rule inet multi_networkpolicy ingress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy ingress jump common-ingress comment \"Jump to common\"", + "add rule inet multi_networkpolicy ingress jump pi-abc123 comment \"test-ns/test-policy\"", + "add rule inet multi_networkpolicy ingress drop comment \"Drop rule\"", + "", // Empty line at the end + } + + // Verify exact number of expected rules + Expect(dumpLines).To(HaveLen(len(expectedRules)), "Expected exactly %d rules, but got %d. Rules: %v", len(expectedRules), len(dumpLines), dumpLines) + + // Verify each expected rule exists completely + for _, expectedRule := range expectedRules { + found := false + for _, actualRule := range dumpLines { + if strings.Contains(actualRule, expectedRule) { + found = true + break + } + } + Expect(found).To(BeTrue(), "Expected rule not found: %s\nActual rules: %v", expectedRule, dumpLines) + } + }) + }) + + Context("createDispatcherRule", func() { + var ( + ctx context.Context + nft knftables.Interface + logger logr.Logger + ) + + BeforeEach(func() { + ctx = context.Background() + nft = knftables.NewFake(knftables.InetFamily, tableName) + logger = logr.Discard() + }) + + It("should create input dispatcher rule", func() { + // First ensure basic structure exists + err := ensureBasicStructure(ctx, nft, nil, logger) + Expect(err).NotTo(HaveOccurred()) + + // Create managed interfaces set + tx := nft.NewTransaction() + interfaces := []Interface{ + {Name: "eth1", Network: "default/net1", IPs: []string{"10.0.0.1"}}, + {Name: "eth2", Network: "default/net2", IPs: []string{"10.0.0.2"}}, + } + hashName := "abc123" + policyNamespace := "test-ns" + policyName := "test-policy" + + createManagedInterfacesSet(tx, interfaces, hashName, policyNamespace, policyName, logger) + + // Create dispatcher rule for input + dispatcherChainName := "input" + comment := "test-ns/test-policy" + createDispatcherRule(tx, hashName, dispatcherChainName, comment, logger) + + // Run transaction to generate rules + err = nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + // Get all generated rules using Dump() + dump := nft.(*knftables.Fake).Dump() + dumpLines := strings.Split(dump, "\n") + + // Expected rules: basic structure + managed interfaces set + input dispatcher rule + expectedRules := []string{ + "add table inet multi_networkpolicy { comment \"MultiNetworkPolicy\" ; }", + "add chain inet multi_networkpolicy common-egress { comment \"Common Policies\" ; }", + "add chain inet multi_networkpolicy common-ingress { comment \"Common Policies\" ; }", + "add chain inet multi_networkpolicy egress { comment \"Egress Policies\" ; }", + "add chain inet multi_networkpolicy ingress { comment \"Ingress Policies\" ; }", + "add chain inet multi_networkpolicy input { type filter hook input priority 0 ; comment \"Input Dispatcher\" ; }", + "add chain inet multi_networkpolicy output { type filter hook output priority 0 ; comment \"Output Dispatcher\" ; }", + "add set inet multi_networkpolicy smi-abc123 { type ifname ; comment \"Managed interfaces set for test-ns/test-policy\" ; }", + "add element inet multi_networkpolicy smi-abc123 { eth1 }", + "add element inet multi_networkpolicy smi-abc123 { eth2 }", + "add rule inet multi_networkpolicy egress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy egress jump common-egress comment \"Jump to common\"", + "add rule inet multi_networkpolicy egress drop comment \"Drop rule\"", + "add rule inet multi_networkpolicy ingress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy ingress jump common-ingress comment \"Jump to common\"", + "add rule inet multi_networkpolicy ingress drop comment \"Drop rule\"", + "add rule inet multi_networkpolicy input iifname @smi-abc123 jump ingress comment \"test-ns/test-policy\"", + "", // Empty line at the end + } + + // Verify exact number of expected rules + Expect(dumpLines).To(HaveLen(len(expectedRules)), "Expected exactly %d rules, but got %d. Rules: %v", len(expectedRules), len(dumpLines), dumpLines) + + // Verify each expected rule exists completely + for _, expectedRule := range expectedRules { + found := false + for _, actualRule := range dumpLines { + if strings.Contains(actualRule, expectedRule) { + found = true + break + } + } + Expect(found).To(BeTrue(), "Expected rule not found: %s\nActual rules: %v", expectedRule, dumpLines) + } + }) + + It("should create output dispatcher rule", func() { + // First ensure basic structure exists + err := ensureBasicStructure(ctx, nft, nil, logger) + Expect(err).NotTo(HaveOccurred()) + + // Create managed interfaces set + tx := nft.NewTransaction() + interfaces := []Interface{ + {Name: "eth1", Network: "default/net1", IPs: []string{"10.0.0.1"}}, + {Name: "eth2", Network: "default/net2", IPs: []string{"10.0.0.2"}}, + } + hashName := "def456" + policyNamespace := "prod-ns" + policyName := "prod-policy" + + createManagedInterfacesSet(tx, interfaces, hashName, policyNamespace, policyName, logger) + + // Create dispatcher rule for output + dispatcherChainName := "output" + comment := "prod-ns/prod-policy" + createDispatcherRule(tx, hashName, dispatcherChainName, comment, logger) + + // Run transaction to generate rules + err = nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + // Get all generated rules using Dump() + dump := nft.(*knftables.Fake).Dump() + dumpLines := strings.Split(dump, "\n") + + // Expected rules: basic structure + managed interfaces set + output dispatcher rule + expectedRules := []string{ + "add table inet multi_networkpolicy { comment \"MultiNetworkPolicy\" ; }", + "add chain inet multi_networkpolicy common-egress { comment \"Common Policies\" ; }", + "add chain inet multi_networkpolicy common-ingress { comment \"Common Policies\" ; }", + "add chain inet multi_networkpolicy egress { comment \"Egress Policies\" ; }", + "add chain inet multi_networkpolicy ingress { comment \"Ingress Policies\" ; }", + "add chain inet multi_networkpolicy input { type filter hook input priority 0 ; comment \"Input Dispatcher\" ; }", + "add chain inet multi_networkpolicy output { type filter hook output priority 0 ; comment \"Output Dispatcher\" ; }", + "add set inet multi_networkpolicy smi-def456 { type ifname ; comment \"Managed interfaces set for prod-ns/prod-policy\" ; }", + "add element inet multi_networkpolicy smi-def456 { eth1 }", + "add element inet multi_networkpolicy smi-def456 { eth2 }", + "add rule inet multi_networkpolicy egress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy egress jump common-egress comment \"Jump to common\"", + "add rule inet multi_networkpolicy egress drop comment \"Drop rule\"", + "add rule inet multi_networkpolicy ingress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy ingress jump common-ingress comment \"Jump to common\"", + "add rule inet multi_networkpolicy ingress drop comment \"Drop rule\"", + "add rule inet multi_networkpolicy output oifname @smi-def456 jump egress comment \"prod-ns/prod-policy\"", + "", // Empty line at the end + } + + // Verify exact number of expected rules + Expect(dumpLines).To(HaveLen(len(expectedRules)), "Expected exactly %d rules, but got %d. Rules: %v", len(expectedRules), len(dumpLines), dumpLines) + + // Verify each expected rule exists completely + for _, expectedRule := range expectedRules { + found := false + for _, actualRule := range dumpLines { + if strings.Contains(actualRule, expectedRule) { + found = true + break + } + } + Expect(found).To(BeTrue(), "Expected rule not found: %s\nActual rules: %v", expectedRule, dumpLines) + } + }) + }) + + Context("getPortRuleSections", func() { + It("should return empty slice for empty ports", func() { + ports := []multiv1beta1.MultiNetworkPolicyPort{} + result := getPortRuleSections(ports) + Expect(result).To(BeEmpty()) + }) + + It("should skip ports with nil protocol", func() { + ports := []multiv1beta1.MultiNetworkPolicyPort{ + { + Protocol: nil, // nil protocol should be skipped + Port: &intstr.IntOrString{Type: intstr.Int, IntVal: 80}, + }, + } + result := getPortRuleSections(ports) + Expect(result).To(HaveLen(1)) + Expect(result[0]).To(Equal("meta l4proto tcp th dport { 80 } accept")) + }) + + It("should handle protocol without port (allow all ports)", func() { + tcp := corev1.ProtocolTCP + ports := []multiv1beta1.MultiNetworkPolicyPort{ + { + Protocol: &tcp, + Port: nil, // nil port means allow all ports for this protocol + }, + } + result := getPortRuleSections(ports) + Expect(result).To(HaveLen(1)) + Expect(result[0]).To(Equal("meta l4proto tcp accept")) + }) + + It("should handle single integer port", func() { + tcp := corev1.ProtocolTCP + ports := []multiv1beta1.MultiNetworkPolicyPort{ + { + Protocol: &tcp, + Port: &intstr.IntOrString{Type: intstr.Int, IntVal: 80}, + }, + } + result := getPortRuleSections(ports) + Expect(result).To(HaveLen(1)) + Expect(result[0]).To(Equal("meta l4proto tcp th dport { 80 } accept")) + }) + + It("should handle single string port (named port)", func() { + tcp := corev1.ProtocolTCP + ports := []multiv1beta1.MultiNetworkPolicyPort{ + { + Protocol: &tcp, + Port: &intstr.IntOrString{Type: intstr.String, StrVal: "HTTP"}, + }, + } + result := getPortRuleSections(ports) + Expect(result).To(HaveLen(1)) + Expect(result[0]).To(Equal("meta l4proto tcp th dport { http } accept")) + }) + + It("should handle port range with EndPort", func() { + tcp := corev1.ProtocolTCP + endPort := int32(8080) + ports := []multiv1beta1.MultiNetworkPolicyPort{ + { + Protocol: &tcp, + Port: &intstr.IntOrString{Type: intstr.Int, IntVal: 8000}, + EndPort: &endPort, + }, + } + result := getPortRuleSections(ports) + Expect(result).To(HaveLen(1)) + Expect(result[0]).To(Equal("meta l4proto tcp th dport { 8000-8080 } accept")) + }) + + It("should handle multiple ports for same protocol", func() { + tcp := corev1.ProtocolTCP + ports := []multiv1beta1.MultiNetworkPolicyPort{ + { + Protocol: &tcp, + Port: &intstr.IntOrString{Type: intstr.Int, IntVal: 80}, + }, + { + Protocol: &tcp, + Port: &intstr.IntOrString{Type: intstr.Int, IntVal: 443}, + }, + { + Protocol: &tcp, + Port: &intstr.IntOrString{Type: intstr.String, StrVal: "SSH"}, + }, + } + result := getPortRuleSections(ports) + Expect(result).To(HaveLen(1)) + // Note: order might vary due to map iteration, so check for both possible orders + expectedPorts := []string{"80", "443", "ssh"} + Expect(result[0]).To(ContainSubstring("meta l4proto tcp th dport {")) + Expect(result[0]).To(ContainSubstring("} accept")) + for _, port := range expectedPorts { + Expect(result[0]).To(ContainSubstring(port)) + } + }) + + It("should handle multiple protocols", func() { + tcp := corev1.ProtocolTCP + udp := corev1.ProtocolUDP + ports := []multiv1beta1.MultiNetworkPolicyPort{ + { + Protocol: &tcp, + Port: &intstr.IntOrString{Type: intstr.Int, IntVal: 80}, + }, + { + Protocol: &udp, + Port: &intstr.IntOrString{Type: intstr.Int, IntVal: 53}, + }, + } + result := getPortRuleSections(ports) + Expect(result).To(HaveLen(2)) + + // Check that both protocols are present (order may vary) + rules := strings.Join(result, " ") + Expect(rules).To(ContainSubstring("meta l4proto tcp th dport { 80 } accept")) + Expect(rules).To(ContainSubstring("meta l4proto udp th dport { 53 } accept")) + }) + + It("should handle mixed protocol with and without ports", func() { + tcp := corev1.ProtocolTCP + udp := corev1.ProtocolUDP + ports := []multiv1beta1.MultiNetworkPolicyPort{ + { + Protocol: &tcp, + Port: &intstr.IntOrString{Type: intstr.Int, IntVal: 80}, + }, + { + Protocol: &udp, + Port: nil, // Allow all UDP ports + }, + } + result := getPortRuleSections(ports) + Expect(result).To(HaveLen(2)) + + rules := strings.Join(result, " ") + Expect(rules).To(ContainSubstring("meta l4proto tcp th dport { 80 } accept")) + Expect(rules).To(ContainSubstring("meta l4proto udp accept")) + }) + + It("should handle complex combination with ranges and named ports", func() { + tcp := corev1.ProtocolTCP + udp := corev1.ProtocolUDP + sctp := corev1.ProtocolSCTP + endPort := int32(9000) + ports := []multiv1beta1.MultiNetworkPolicyPort{ + { + Protocol: &tcp, + Port: &intstr.IntOrString{Type: intstr.Int, IntVal: 80}, + }, + { + Protocol: &tcp, + Port: &intstr.IntOrString{Type: intstr.String, StrVal: "HTTPS"}, + }, + { + Protocol: &tcp, + Port: &intstr.IntOrString{Type: intstr.Int, IntVal: 8000}, + EndPort: &endPort, + }, + { + Protocol: &udp, + Port: &intstr.IntOrString{Type: intstr.Int, IntVal: 53}, + }, + { + Protocol: &sctp, + Port: nil, // Allow all SCTP ports + }, + } + result := getPortRuleSections(ports) + Expect(result).To(HaveLen(3)) // TCP, UDP, SCTP + + rules := strings.Join(result, " ") + Expect(rules).To(ContainSubstring("meta l4proto sctp accept")) + Expect(rules).To(ContainSubstring("meta l4proto udp th dport { 53 } accept")) + // TCP should have multiple ports: 80, https (lowercase), and 8000-9000 range + Expect(rules).To(ContainSubstring("meta l4proto tcp th dport {")) + Expect(rules).To(ContainSubstring("80")) + Expect(rules).To(ContainSubstring("https")) + Expect(rules).To(ContainSubstring("8000-9000")) + }) + + It("should convert protocol names to lowercase", func() { + tcp := corev1.Protocol("TCP") // Uppercase + ports := []multiv1beta1.MultiNetworkPolicyPort{ + { + Protocol: &tcp, + Port: &intstr.IntOrString{Type: intstr.Int, IntVal: 80}, + }, + } + result := getPortRuleSections(ports) + Expect(result).To(HaveLen(1)) + Expect(result[0]).To(Equal("meta l4proto tcp th dport { 80 } accept")) + }) + + It("should convert named ports to lowercase", func() { + tcp := corev1.ProtocolTCP + ports := []multiv1beta1.MultiNetworkPolicyPort{ + { + Protocol: &tcp, + Port: &intstr.IntOrString{Type: intstr.String, StrVal: "HTTP"}, + }, + } + result := getPortRuleSections(ports) + Expect(result).To(HaveLen(1)) + Expect(result[0]).To(Equal("meta l4proto tcp th dport { http } accept")) + }) + }) + + Context("createIngressRules", func() { + var ( + ctx context.Context + nft knftables.Interface + logger logr.Logger + ) + + BeforeEach(func() { + ctx = context.Background() + nft = knftables.NewFake(knftables.InetFamily, tableName) + logger = logr.Discard() + }) + + It("should create no rules for deny-all policy (empty ingress)", func() { + // First ensure basic structure exists + err := ensureBasicStructure(ctx, nft, nil, logger) + Expect(err).NotTo(HaveOccurred()) + + // Create a policy with no ingress rules (deny all) + policy := &datastore.Policy{ + Name: "deny-all-policy", + Namespace: "test-ns", + Spec: multiv1beta1.MultiNetworkPolicySpec{ + PolicyTypes: []multiv1beta1.MultiPolicyType{multiv1beta1.PolicyTypeIngress}, + Ingress: []multiv1beta1.MultiNetworkPolicyIngressRule{}, // Empty = deny all + }, + } + + // Setup interfaces and transaction + matchedInterfaces := []Interface{ + {Name: "eth1", Network: "default/net1", IPs: []string{"10.0.0.1"}}, + {Name: "eth2", Network: "default/net2", IPs: []string{"10.0.0.2"}}, + } + tx := nft.NewTransaction() + hashName := "abc123" + + // Create policy chain first (as done in enforcePolicy) + npChainName := fmt.Sprintf("cnp-%s", hashName) + err = createPolicyChain(ctx, nft, tx, npChainName, "ingress", policy.Namespace, policy.Name, logger) + Expect(err).NotTo(HaveOccurred()) + + // Create a minimal NFTables instance for testing + nftables := &NFTables{ + Client: nil, // We don't need the client for this test since no API calls are made + } + + // Call createIngressRules + err = nftables.createIngressRules(ctx, tx, matchedInterfaces, policy, hashName, logger) + Expect(err).NotTo(HaveOccurred()) + + // Run transaction to generate rules + err = nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + // Get all generated rules using Dump() + dump := nft.(*knftables.Fake).Dump() + + // Split the dump into lines for easier verification + dumpLines := strings.Split(dump, "\n") + + // Expected rules that should be generated (based on actual output) + expectedRules := []string{ + "add table inet multi_networkpolicy { comment \"MultiNetworkPolicy\" ; }", + "add chain inet multi_networkpolicy input { type filter hook input priority 0 ; comment \"Input Dispatcher\" ; }", + "add chain inet multi_networkpolicy output { type filter hook output priority 0 ; comment \"Output Dispatcher\" ; }", + "add chain inet multi_networkpolicy ingress { comment \"Ingress Policies\" ; }", + "add chain inet multi_networkpolicy egress { comment \"Egress Policies\" ; }", + "add chain inet multi_networkpolicy common-ingress { comment \"Common Policies\" ; }", + "add chain inet multi_networkpolicy common-egress { comment \"Common Policies\" ; }", + "add rule inet multi_networkpolicy egress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy egress jump common-egress comment \"Jump to common\"", + "add rule inet multi_networkpolicy egress drop comment \"Drop rule\"", + "add rule inet multi_networkpolicy ingress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy ingress jump common-ingress comment \"Jump to common\"", + "add rule inet multi_networkpolicy ingress drop comment \"Drop rule\"", + + // New commands to verify + "add chain inet multi_networkpolicy cnp-abc123 { comment \"MultiNetworkPolicy test-ns/deny-all-policy\" ; }", + "add rule inet multi_networkpolicy ingress jump cnp-abc123 comment \"test-ns/deny-all-policy\"", + "add rule inet multi_networkpolicy cnp-abc123 iifname eth1 ip saddr 10.0.0.1 accept", + "add rule inet multi_networkpolicy cnp-abc123 iifname eth2 ip saddr 10.0.0.2 accept", + "", // Empty line at the end + } + + // Verify exact number of expected rules + Expect(dumpLines).To(HaveLen(len(expectedRules)), "Expected exactly %d rules, but got %d. Rules: %v", len(expectedRules), len(dumpLines), dumpLines) + + // Verify each expected rule exists completely + for _, expectedRule := range expectedRules { + found := false + for _, actualRule := range dumpLines { + if strings.Contains(actualRule, expectedRule) { + found = true + break + } + } + Expect(found).To(BeTrue(), "Expected rule not found: %s\nActual rules: %v", expectedRule, dumpLines) + } + }) + + It("should create accept-all rules for policy with empty ingress entry", func() { + // First ensure basic structure exists + err := ensureBasicStructure(ctx, nft, nil, logger) + Expect(err).NotTo(HaveOccurred()) + + // Create a policy with single empty ingress entry (accept all) + policy := &datastore.Policy{ + Name: "accept-all-policy", + Namespace: "test-ns", + Spec: multiv1beta1.MultiNetworkPolicySpec{ + PolicyTypes: []multiv1beta1.MultiPolicyType{multiv1beta1.PolicyTypeIngress}, + Ingress: []multiv1beta1.MultiNetworkPolicyIngressRule{ + { + From: []multiv1beta1.MultiNetworkPolicyPeer{}, // Empty From = accept from all sources + Ports: []multiv1beta1.MultiNetworkPolicyPort{}, // Empty Ports = accept all ports + }, + }, + }, + } + + // Setup interfaces and transaction + matchedInterfaces := []Interface{ + {Name: "eth1", Network: "default/net1", IPs: []string{"10.0.0.1"}}, + {Name: "eth2", Network: "default/net2", IPs: []string{"10.0.0.2"}}, + } + tx := nft.NewTransaction() + hashName := "def456" + + // Create policy chain first (as done in enforcePolicy) + npChainName := fmt.Sprintf("cnp-%s", hashName) + err = createPolicyChain(ctx, nft, tx, npChainName, "ingress", policy.Namespace, policy.Name, logger) + Expect(err).NotTo(HaveOccurred()) + + // Create NFTables instance + nftables := &NFTables{ + Client: nil, + } + + // Call createIngressRules + err = nftables.createIngressRules(ctx, tx, matchedInterfaces, policy, hashName, logger) + Expect(err).NotTo(HaveOccurred()) + + // Run transaction to generate rules + err = nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + // Get all generated rules using Dump() + dump := nft.(*knftables.Fake).Dump() + + // Split the dump into lines for easier verification + dumpLines := strings.Split(dump, "\n") + + // Expected rules that should be generated (based on actual output) + expectedRules := []string{ + "add table inet multi_networkpolicy { comment \"MultiNetworkPolicy\" ; }", + "add chain inet multi_networkpolicy input { type filter hook input priority 0 ; comment \"Input Dispatcher\" ; }", + "add chain inet multi_networkpolicy output { type filter hook output priority 0 ; comment \"Output Dispatcher\" ; }", + "add chain inet multi_networkpolicy ingress { comment \"Ingress Policies\" ; }", + "add chain inet multi_networkpolicy egress { comment \"Egress Policies\" ; }", + "add chain inet multi_networkpolicy common-ingress { comment \"Common Policies\" ; }", + "add chain inet multi_networkpolicy common-egress { comment \"Common Policies\" ; }", + "add rule inet multi_networkpolicy egress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy egress jump common-egress comment \"Jump to common\"", + "add rule inet multi_networkpolicy egress drop comment \"Drop rule\"", + "add rule inet multi_networkpolicy ingress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy ingress jump common-ingress comment \"Jump to common\"", + "add rule inet multi_networkpolicy ingress drop comment \"Drop rule\"", + // New commands to verify + "add chain inet multi_networkpolicy cnp-def456 { comment \"MultiNetworkPolicy test-ns/accept-all-policy\" ; }", + "add rule inet multi_networkpolicy ingress jump cnp-def456 comment \"test-ns/accept-all-policy\"", + "add rule inet multi_networkpolicy cnp-def456 iifname eth1 ip saddr 10.0.0.1 accept", + "add rule inet multi_networkpolicy cnp-def456 iifname eth2 ip saddr 10.0.0.2 accept", + "add rule inet multi_networkpolicy cnp-def456 iifname eth1 accept", + "add rule inet multi_networkpolicy cnp-def456 iifname eth2 accept", + "", // Empty line at the end + } + + // Verify exact number of expected rules + Expect(dumpLines).To(HaveLen(len(expectedRules)), "Expected exactly %d rules, but got %d. Rules: %v", len(expectedRules), len(dumpLines), dumpLines) + + // Verify each expected rule exists completely + for _, expectedRule := range expectedRules { + found := false + for _, actualRule := range dumpLines { + if strings.Contains(actualRule, expectedRule) { + found = true + break + } + } + Expect(found).To(BeTrue(), "Expected rule not found: %s\nActual rules: %v", expectedRule, dumpLines) + } + }) + + It("should create port-restricted rules for multiple ingress entries with nil From", func() { + // First ensure basic structure exists + err := ensureBasicStructure(ctx, nft, nil, logger) + Expect(err).NotTo(HaveOccurred()) + + // Create a policy with multiple ingress entries, nil From, specific ports + tcp := corev1.ProtocolTCP + udp := corev1.ProtocolUDP + policy := &datastore.Policy{ + Name: "port-restricted-policy", + Namespace: "prod-ns", + Spec: multiv1beta1.MultiNetworkPolicySpec{ + PolicyTypes: []multiv1beta1.MultiPolicyType{multiv1beta1.PolicyTypeIngress}, + Ingress: []multiv1beta1.MultiNetworkPolicyIngressRule{ + { + From: []multiv1beta1.MultiNetworkPolicyPeer{}, // Empty From = accept from all sources + Ports: []multiv1beta1.MultiNetworkPolicyPort{ + { + Protocol: &tcp, + Port: &intstr.IntOrString{Type: intstr.Int, IntVal: 80}, + }, + { + Protocol: &tcp, + Port: &intstr.IntOrString{Type: intstr.Int, IntVal: 443}, + }, + }, + }, + { + From: []multiv1beta1.MultiNetworkPolicyPeer{}, // Empty From = accept from all sources + Ports: []multiv1beta1.MultiNetworkPolicyPort{ + { + Protocol: &udp, + Port: &intstr.IntOrString{Type: intstr.Int, IntVal: 53}, + }, + }, + }, + { + From: []multiv1beta1.MultiNetworkPolicyPeer{}, // Empty From = accept from all sources + Ports: []multiv1beta1.MultiNetworkPolicyPort{ + { + Protocol: &tcp, + Port: &intstr.IntOrString{Type: intstr.String, StrVal: "SSH"}, + }, + }, + }, + }, + }, + } + + // Setup interfaces and transaction + matchedInterfaces := []Interface{ + {Name: "eth1", Network: "default/net1", IPs: []string{"10.0.0.1"}}, + } + tx := nft.NewTransaction() + hashName := "ghi789" + + // Create policy chain first (as done in enforcePolicy) + npChainName := fmt.Sprintf("cnp-%s", hashName) + err = createPolicyChain(ctx, nft, tx, npChainName, "ingress", policy.Namespace, policy.Name, logger) + Expect(err).NotTo(HaveOccurred()) + + // Create NFTables instance + nftables := &NFTables{ + Client: nil, + } + + // Call createIngressRules + err = nftables.createIngressRules(ctx, tx, matchedInterfaces, policy, hashName, logger) + Expect(err).NotTo(HaveOccurred()) + + // Run transaction to generate rules + err = nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + // Get all generated rules using Dump() + dump := nft.(*knftables.Fake).Dump() + + // Split the dump into lines for easier verification + dumpLines := strings.Split(dump, "\n") + + // Expected rules that should be generated (based on actual output) + expectedRules := []string{ + "add table inet multi_networkpolicy { comment \"MultiNetworkPolicy\" ; }", + "add chain inet multi_networkpolicy input { type filter hook input priority 0 ; comment \"Input Dispatcher\" ; }", + "add chain inet multi_networkpolicy output { type filter hook output priority 0 ; comment \"Output Dispatcher\" ; }", + "add chain inet multi_networkpolicy ingress { comment \"Ingress Policies\" ; }", + "add chain inet multi_networkpolicy egress { comment \"Egress Policies\" ; }", + "add chain inet multi_networkpolicy common-ingress { comment \"Common Policies\" ; }", + "add chain inet multi_networkpolicy common-egress { comment \"Common Policies\" ; }", + "add rule inet multi_networkpolicy egress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy egress jump common-egress comment \"Jump to common\"", + "add rule inet multi_networkpolicy egress drop comment \"Drop rule\"", + "add rule inet multi_networkpolicy ingress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy ingress jump common-ingress comment \"Jump to common\"", + "add rule inet multi_networkpolicy ingress drop comment \"Drop rule\"", + + // New commands to verify + "add chain inet multi_networkpolicy cnp-ghi789 { comment \"MultiNetworkPolicy prod-ns/port-restricted-policy\" ; }", + "add rule inet multi_networkpolicy ingress jump cnp-ghi789 comment \"prod-ns/port-restricted-policy\"", + "add rule inet multi_networkpolicy cnp-ghi789 iifname eth1 meta l4proto tcp th dport { 80,443 } accept", + "add rule inet multi_networkpolicy cnp-ghi789 iifname eth1 meta l4proto udp th dport { 53 } accept", + "add rule inet multi_networkpolicy cnp-ghi789 iifname eth1 meta l4proto tcp th dport { ssh } accept", + "add rule inet multi_networkpolicy cnp-ghi789 iifname eth1 ip saddr 10.0.0.1 accept", + "", // Empty line at the end + } + + // Verify exact number of expected rules + Expect(dumpLines).To(HaveLen(len(expectedRules)), "Expected exactly %d rules, but got %d. Rules: %v", len(expectedRules), len(dumpLines), dumpLines) + + // Verify each expected rule exists completely + for _, expectedRule := range expectedRules { + found := false + for _, actualRule := range dumpLines { + if strings.Contains(actualRule, expectedRule) { + found = true + break + } + } + Expect(found).To(BeTrue(), "Expected rule not found: %s\nActual rules: %v", expectedRule, dumpLines) + } + }) + + // Comprehensive tests for full coverage + It("should create rules for ingress with IPv4-only pod selector", func() { + err := ensureBasicStructure(ctx, nft, nil, logger) + Expect(err).NotTo(HaveOccurred()) + + // Create fake client with IPv4-only pod + scheme := runtime.NewScheme() + _ = corev1.AddToScheme(scheme) + + pod1 := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod1", + Namespace: "default", + Labels: map[string]string{"app": "web"}, + Annotations: map[string]string{ + "k8s.v1.cni.cncf.io/networks": `[{"name": "net1"}]`, + "k8s.v1.cni.cncf.io/network-status": `[{ + "name": "default/net1", + "interface": "eth1", + "ips": ["10.0.1.1"] + }]`, + }, + }, + Status: corev1.PodStatus{Phase: corev1.PodRunning}, + Spec: corev1.PodSpec{HostNetwork: false}, + } + + fakeClient := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(pod1). + WithIndex(&corev1.Pod{}, PodStatusIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + return []string{string(pod.Status.Phase)} + }). + WithIndex(&corev1.Pod{}, PodHostNetworkIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + return []string{fmt.Sprintf("%t", pod.Spec.HostNetwork)} + }). + WithIndex(&corev1.Pod{}, PodHasNetworkAnnotationIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + _, hasAnnotation := pod.Annotations["k8s.v1.cni.cncf.io/networks"] + return []string{fmt.Sprintf("%t", hasAnnotation)} + }). + Build() + + policy := &datastore.Policy{ + Name: "ipv4-pod-policy", + Namespace: "default", + Networks: []string{"default/net1"}, + Spec: multiv1beta1.MultiNetworkPolicySpec{ + Ingress: []multiv1beta1.MultiNetworkPolicyIngressRule{ + { + From: []multiv1beta1.MultiNetworkPolicyPeer{ + { + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "web"}, + }, + }, + }, + }, + }, + }, + } + + matchedInterfaces := []Interface{ + {Name: "eth1", Network: "default/net1", IPs: []string{"10.0.1.1"}}, + } + + tx := nft.NewTransaction() + hashName := "ipv4test" + err = createPolicyChain(ctx, nft, tx, fmt.Sprintf("cnp-%s", hashName), "ingress", policy.Namespace, policy.Name, logger) + Expect(err).NotTo(HaveOccurred()) + + nftablesInstance := &NFTables{Client: fakeClient} + err = nftablesInstance.createIngressRules(ctx, tx, matchedInterfaces, policy, hashName, logger) + Expect(err).NotTo(HaveOccurred()) + + err = nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + dump := nft.(*knftables.Fake).Dump() + + // Split the dump into lines for easier verification + dumpLines := strings.Split(dump, "\n") + + // Expected rules that should be generated (based on actual output) + expectedRules := []string{ + "add table inet multi_networkpolicy { comment \"MultiNetworkPolicy\" ; }", + "add chain inet multi_networkpolicy input { type filter hook input priority 0 ; comment \"Input Dispatcher\" ; }", + "add chain inet multi_networkpolicy output { type filter hook output priority 0 ; comment \"Output Dispatcher\" ; }", + "add chain inet multi_networkpolicy ingress { comment \"Ingress Policies\" ; }", + "add chain inet multi_networkpolicy egress { comment \"Egress Policies\" ; }", + "add chain inet multi_networkpolicy common-ingress { comment \"Common Policies\" ; }", + "add chain inet multi_networkpolicy common-egress { comment \"Common Policies\" ; }", + "add rule inet multi_networkpolicy egress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy egress jump common-egress comment \"Jump to common\"", + "add rule inet multi_networkpolicy egress drop comment \"Drop rule\"", + "add rule inet multi_networkpolicy ingress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy ingress jump common-ingress comment \"Jump to common\"", + "add rule inet multi_networkpolicy ingress drop comment \"Drop rule\"", + + // New commands to verify + "add chain inet multi_networkpolicy cnp-ipv4test { comment \"MultiNetworkPolicy default/ipv4-pod-policy\" ; }", + "add rule inet multi_networkpolicy ingress jump cnp-ipv4test comment \"default/ipv4-pod-policy\"", + "add set inet multi_networkpolicy snp-ipv4test_ingress_ipv4_eth1_0 { type ipv4_addr ; comment \"Addresses for default/ipv4-pod-policy\" ; }", + "add element inet multi_networkpolicy snp-ipv4test_ingress_ipv4_eth1_0 { 10.0.1.1 }", + "add rule inet multi_networkpolicy cnp-ipv4test iifname eth1 ip saddr @snp-ipv4test_ingress_ipv4_eth1_0 accept", + "add rule inet multi_networkpolicy cnp-ipv4test iifname eth1 ip saddr 10.0.1.1 accept", + "", // Empty line at the end + } + + // Verify exact number of expected rules + Expect(dumpLines).To(HaveLen(len(expectedRules)), "Expected exactly %d rules, but got %d. Rules: %v", len(expectedRules), len(dumpLines), dumpLines) + + // Verify each expected rule exists completely + for _, expectedRule := range expectedRules { + found := false + for _, actualRule := range dumpLines { + if strings.Contains(actualRule, expectedRule) { + found = true + break + } + } + Expect(found).To(BeTrue(), "Expected rule not found: %s\nActual rules: %v", expectedRule, dumpLines) + } + }) + + It("should create rules for ingress with IPv6-only pod selector", func() { + err := ensureBasicStructure(ctx, nft, nil, logger) + Expect(err).NotTo(HaveOccurred()) + + scheme := runtime.NewScheme() + _ = corev1.AddToScheme(scheme) + + pod2 := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod2", + Namespace: "default", + Labels: map[string]string{"app": "db"}, + Annotations: map[string]string{ + "k8s.v1.cni.cncf.io/networks": `[{"name": "net1"}]`, + "k8s.v1.cni.cncf.io/network-status": `[{ + "name": "default/net1", + "interface": "eth1", + "ips": ["2001:db8::1"] + }]`, + }, + }, + Status: corev1.PodStatus{Phase: corev1.PodRunning}, + Spec: corev1.PodSpec{HostNetwork: false}, + } + + fakeClient := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(pod2). + WithIndex(&corev1.Pod{}, PodStatusIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + return []string{string(pod.Status.Phase)} + }). + WithIndex(&corev1.Pod{}, PodHostNetworkIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + return []string{fmt.Sprintf("%t", pod.Spec.HostNetwork)} + }). + WithIndex(&corev1.Pod{}, PodHasNetworkAnnotationIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + _, hasAnnotation := pod.Annotations["k8s.v1.cni.cncf.io/networks"] + return []string{fmt.Sprintf("%t", hasAnnotation)} + }). + Build() + + policy := &datastore.Policy{ + Name: "ipv6-pod-policy", + Namespace: "default", + Networks: []string{"default/net1"}, + Spec: multiv1beta1.MultiNetworkPolicySpec{ + Ingress: []multiv1beta1.MultiNetworkPolicyIngressRule{ + { + From: []multiv1beta1.MultiNetworkPolicyPeer{ + { + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "db"}, + }, + }, + }, + }, + }, + }, + } + + matchedInterfaces := []Interface{ + {Name: "eth1", Network: "default/net1", IPs: []string{"10.0.1.1"}}, + } + + tx := nft.NewTransaction() + hashName := "ipv6test" + err = createPolicyChain(ctx, nft, tx, fmt.Sprintf("cnp-%s", hashName), "ingress", policy.Namespace, policy.Name, logger) + Expect(err).NotTo(HaveOccurred()) + + nftablesInstance := &NFTables{Client: fakeClient} + err = nftablesInstance.createIngressRules(ctx, tx, matchedInterfaces, policy, hashName, logger) + Expect(err).NotTo(HaveOccurred()) + + err = nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + dump := nft.(*knftables.Fake).Dump() + + // Split the dump into lines for easier verification + dumpLines := strings.Split(dump, "\n") + + // Expected rules that should be generated (based on actual output) + expectedRules := []string{ + "add table inet multi_networkpolicy { comment \"MultiNetworkPolicy\" ; }", + "add chain inet multi_networkpolicy input { type filter hook input priority 0 ; comment \"Input Dispatcher\" ; }", + "add chain inet multi_networkpolicy output { type filter hook output priority 0 ; comment \"Output Dispatcher\" ; }", + "add chain inet multi_networkpolicy ingress { comment \"Ingress Policies\" ; }", + "add chain inet multi_networkpolicy egress { comment \"Egress Policies\" ; }", + "add chain inet multi_networkpolicy common-ingress { comment \"Common Policies\" ; }", + "add chain inet multi_networkpolicy common-egress { comment \"Common Policies\" ; }", + "add rule inet multi_networkpolicy egress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy egress jump common-egress comment \"Jump to common\"", + "add rule inet multi_networkpolicy egress drop comment \"Drop rule\"", + "add rule inet multi_networkpolicy ingress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy ingress jump common-ingress comment \"Jump to common\"", + "add rule inet multi_networkpolicy ingress drop comment \"Drop rule\"", + + // New commands to verify + "add chain inet multi_networkpolicy cnp-ipv6test { comment \"MultiNetworkPolicy default/ipv6-pod-policy\" ; }", + "add rule inet multi_networkpolicy ingress jump cnp-ipv6test comment \"default/ipv6-pod-policy\"", + "add set inet multi_networkpolicy snp-ipv6test_ingress_ipv6_eth1_0 { type ipv6_addr ; comment \"Addresses for default/ipv6-pod-policy\" ; }", + "add element inet multi_networkpolicy snp-ipv6test_ingress_ipv6_eth1_0 { 2001:db8::1 }", + "add rule inet multi_networkpolicy cnp-ipv6test iifname eth1 ip6 saddr @snp-ipv6test_ingress_ipv6_eth1_0 accept", + "add rule inet multi_networkpolicy cnp-ipv6test iifname eth1 ip saddr 10.0.1.1 accept", + "", // Empty line at the end + } + + // Verify exact number of expected rules + Expect(dumpLines).To(HaveLen(len(expectedRules)), "Expected exactly %d rules, but got %d. Rules: %v", len(expectedRules), len(dumpLines), dumpLines) + + // Verify each expected rule exists completely + for _, expectedRule := range expectedRules { + found := false + for _, actualRule := range dumpLines { + if strings.Contains(actualRule, expectedRule) { + found = true + break + } + } + Expect(found).To(BeTrue(), "Expected rule not found: %s\nActual rules: %v", expectedRule, dumpLines) + } + }) + + It("should create rules for ingress with dual-stack pod selector", func() { + err := ensureBasicStructure(ctx, nft, nil, logger) + Expect(err).NotTo(HaveOccurred()) + + scheme := runtime.NewScheme() + _ = corev1.AddToScheme(scheme) + + pod3 := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod3", + Namespace: "default", + Labels: map[string]string{"app": "api"}, + Annotations: map[string]string{ + "k8s.v1.cni.cncf.io/networks": `[{"name": "net1"}]`, + "k8s.v1.cni.cncf.io/network-status": `[{ + "name": "default/net1", + "interface": "eth1", + "ips": ["10.0.1.2", "2001:db8::2"] + }]`, + }, + }, + Status: corev1.PodStatus{Phase: corev1.PodRunning}, + Spec: corev1.PodSpec{HostNetwork: false}, + } + + fakeClient := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(pod3). + WithIndex(&corev1.Pod{}, PodStatusIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + return []string{string(pod.Status.Phase)} + }). + WithIndex(&corev1.Pod{}, PodHostNetworkIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + return []string{fmt.Sprintf("%t", pod.Spec.HostNetwork)} + }). + WithIndex(&corev1.Pod{}, PodHasNetworkAnnotationIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + _, hasAnnotation := pod.Annotations["k8s.v1.cni.cncf.io/networks"] + return []string{fmt.Sprintf("%t", hasAnnotation)} + }). + Build() + + policy := &datastore.Policy{ + Name: "dual-stack-policy", + Namespace: "default", + Networks: []string{"default/net1"}, + Spec: multiv1beta1.MultiNetworkPolicySpec{ + Ingress: []multiv1beta1.MultiNetworkPolicyIngressRule{ + { + From: []multiv1beta1.MultiNetworkPolicyPeer{ + { + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "api"}, + }, + }, + }, + }, + }, + }, + } + + matchedInterfaces := []Interface{ + {Name: "eth1", Network: "default/net1", IPs: []string{"10.0.1.1"}}, + } + + tx := nft.NewTransaction() + hashName := "dualtest" + err = createPolicyChain(ctx, nft, tx, fmt.Sprintf("cnp-%s", hashName), "ingress", policy.Namespace, policy.Name, logger) + Expect(err).NotTo(HaveOccurred()) + + nftablesInstance := &NFTables{Client: fakeClient} + err = nftablesInstance.createIngressRules(ctx, tx, matchedInterfaces, policy, hashName, logger) + Expect(err).NotTo(HaveOccurred()) + + err = nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + dump := nft.(*knftables.Fake).Dump() + + // Split the dump into lines for easier verification + dumpLines := strings.Split(dump, "\n") + + // Expected rules that should be generated (based on actual output) + expectedRules := []string{ + "add table inet multi_networkpolicy { comment \"MultiNetworkPolicy\" ; }", + "add chain inet multi_networkpolicy input { type filter hook input priority 0 ; comment \"Input Dispatcher\" ; }", + "add chain inet multi_networkpolicy output { type filter hook output priority 0 ; comment \"Output Dispatcher\" ; }", + "add chain inet multi_networkpolicy ingress { comment \"Ingress Policies\" ; }", + "add chain inet multi_networkpolicy egress { comment \"Egress Policies\" ; }", + "add chain inet multi_networkpolicy common-ingress { comment \"Common Policies\" ; }", + "add chain inet multi_networkpolicy common-egress { comment \"Common Policies\" ; }", + "add rule inet multi_networkpolicy egress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy egress jump common-egress comment \"Jump to common\"", + "add rule inet multi_networkpolicy egress drop comment \"Drop rule\"", + "add rule inet multi_networkpolicy ingress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy ingress jump common-ingress comment \"Jump to common\"", + "add rule inet multi_networkpolicy ingress drop comment \"Drop rule\"", + + // New commands to verify + "add chain inet multi_networkpolicy cnp-dualtest { comment \"MultiNetworkPolicy default/dual-stack-policy\" ; }", + "add rule inet multi_networkpolicy ingress jump cnp-dualtest comment \"default/dual-stack-policy\"", + "add set inet multi_networkpolicy snp-dualtest_ingress_ipv4_eth1_0 { type ipv4_addr ; comment \"Addresses for default/dual-stack-policy\" ; }", + "add set inet multi_networkpolicy snp-dualtest_ingress_ipv6_eth1_0 { type ipv6_addr ; comment \"Addresses for default/dual-stack-policy\" ; }", + "add element inet multi_networkpolicy snp-dualtest_ingress_ipv4_eth1_0 { 10.0.1.2 }", + "add element inet multi_networkpolicy snp-dualtest_ingress_ipv6_eth1_0 { 2001:db8::2 }", + "add rule inet multi_networkpolicy cnp-dualtest iifname eth1 ip saddr @snp-dualtest_ingress_ipv4_eth1_0 accept", + "add rule inet multi_networkpolicy cnp-dualtest iifname eth1 ip6 saddr @snp-dualtest_ingress_ipv6_eth1_0 accept", + "add rule inet multi_networkpolicy cnp-dualtest iifname eth1 ip saddr 10.0.1.1 accept", + "", // Empty line at the end + } + + // Verify exact number of expected rules + Expect(dumpLines).To(HaveLen(len(expectedRules)), "Expected exactly %d rules, but got %d. Rules: %v", len(expectedRules), len(dumpLines), dumpLines) + + // Verify each expected rule exists completely + for _, expectedRule := range expectedRules { + found := false + for _, actualRule := range dumpLines { + if strings.Contains(actualRule, expectedRule) { + found = true + break + } + } + Expect(found).To(BeTrue(), "Expected rule not found: %s\nActual rules: %v", expectedRule, dumpLines) + } + }) + + It("should create rules for ingress with IPv4 IPBlock", func() { + err := ensureBasicStructure(ctx, nft, nil, logger) + Expect(err).NotTo(HaveOccurred()) + + policy := &datastore.Policy{ + Name: "ipv4-ipblock-policy", + Namespace: "default", + Networks: []string{"default/net1"}, + Spec: multiv1beta1.MultiNetworkPolicySpec{ + Ingress: []multiv1beta1.MultiNetworkPolicyIngressRule{ + { + From: []multiv1beta1.MultiNetworkPolicyPeer{ + { + IPBlock: &multiv1beta1.IPBlock{ + CIDR: "10.0.0.0/24", + Except: []string{"10.0.0.1/32", "10.0.0.2/32"}, + }, + }, + }, + }, + }, + }, + } + + matchedInterfaces := []Interface{ + {Name: "eth1", Network: "default/net1", IPs: []string{"10.0.1.1"}}, + } + + tx := nft.NewTransaction() + hashName := "ipv4block" + + createManagedInterfacesSet(tx, matchedInterfaces, hashName, policy.Namespace, policy.Name, logger) + + err = createPolicyChain(ctx, nft, tx, fmt.Sprintf("cnp-%s", hashName), "ingress", policy.Namespace, policy.Name, logger) + Expect(err).NotTo(HaveOccurred()) + + nftablesInstance := &NFTables{Client: nil} + err = nftablesInstance.createIngressRules(ctx, tx, matchedInterfaces, policy, hashName, logger) + Expect(err).NotTo(HaveOccurred()) + + err = nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + dump := nft.(*knftables.Fake).Dump() + + // Split the dump into lines for easier verification + dumpLines := strings.Split(dump, "\n") + + // Expected rules that should be generated (based on actual output) + expectedRules := []string{ + "add table inet multi_networkpolicy { comment \"MultiNetworkPolicy\" ; }", + "add chain inet multi_networkpolicy input { type filter hook input priority 0 ; comment \"Input Dispatcher\" ; }", + "add chain inet multi_networkpolicy output { type filter hook output priority 0 ; comment \"Output Dispatcher\" ; }", + "add chain inet multi_networkpolicy ingress { comment \"Ingress Policies\" ; }", + "add chain inet multi_networkpolicy egress { comment \"Egress Policies\" ; }", + "add chain inet multi_networkpolicy common-ingress { comment \"Common Policies\" ; }", + "add chain inet multi_networkpolicy common-egress { comment \"Common Policies\" ; }", + "add rule inet multi_networkpolicy egress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy egress jump common-egress comment \"Jump to common\"", + "add rule inet multi_networkpolicy egress drop comment \"Drop rule\"", + "add rule inet multi_networkpolicy ingress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy ingress jump common-ingress comment \"Jump to common\"", + "add rule inet multi_networkpolicy ingress drop comment \"Drop rule\"", + + "add set inet multi_networkpolicy smi-ipv4block { type ifname ; comment \"Managed interfaces set for default/ipv4-ipblock-policy\" ; }", + "add element inet multi_networkpolicy smi-ipv4block { eth1 }", + + // New commands to verify + "add chain inet multi_networkpolicy cnp-ipv4block { comment \"MultiNetworkPolicy default/ipv4-ipblock-policy\" ; }", + "add rule inet multi_networkpolicy ingress jump cnp-ipv4block comment \"default/ipv4-ipblock-policy\"", + "add set inet multi_networkpolicy snp-ipv4block_ingress_ipv4_cidr_0 { type ipv4_addr ; flags interval ; comment \"CIDRs for default/ipv4-ipblock-policy\" ; }", + "add set inet multi_networkpolicy snp-ipv4block_ingress_ipv4_except_0 { type ipv4_addr ; flags interval ; comment \"Excepts for default/ipv4-ipblock-policy\" ; }", + "add element inet multi_networkpolicy snp-ipv4block_ingress_ipv4_cidr_0 { 10.0.0.0/24 }", + "add element inet multi_networkpolicy snp-ipv4block_ingress_ipv4_except_0 { 10.0.0.1/32 }", + "add element inet multi_networkpolicy snp-ipv4block_ingress_ipv4_except_0 { 10.0.0.2/32 }", + "add rule inet multi_networkpolicy cnp-ipv4block iifname @smi-ipv4block ip saddr @snp-ipv4block_ingress_ipv4_cidr_0 ip saddr != @snp-ipv4block_ingress_ipv4_except_0 accept", + "add rule inet multi_networkpolicy cnp-ipv4block iifname eth1 ip saddr 10.0.1.1 accept", + "", // Empty line at the end + } + + // Verify exact number of expected rules + Expect(dumpLines).To(HaveLen(len(expectedRules)), "Expected exactly %d rules, but got %d. Rules: %v", len(expectedRules), len(dumpLines), dumpLines) + + // Verify each expected rule exists completely + for _, expectedRule := range expectedRules { + found := false + for _, actualRule := range dumpLines { + if strings.Contains(actualRule, expectedRule) { + found = true + break + } + } + Expect(found).To(BeTrue(), "Expected rule not found: %s\nActual rules: %v", expectedRule, dumpLines) + } + }) + + It("should create rules for ingress with IPv6 IPBlock", func() { + err := ensureBasicStructure(ctx, nft, nil, logger) + Expect(err).NotTo(HaveOccurred()) + + policy := &datastore.Policy{ + Name: "ipv6-ipblock-policy", + Namespace: "default", + Networks: []string{"default/net1"}, + Spec: multiv1beta1.MultiNetworkPolicySpec{ + Ingress: []multiv1beta1.MultiNetworkPolicyIngressRule{ + { + From: []multiv1beta1.MultiNetworkPolicyPeer{ + { + IPBlock: &multiv1beta1.IPBlock{ + CIDR: "2001:db8::/32", + Except: []string{"2001:db8::1/128"}, + }, + }, + }, + }, + }, + }, + } + + matchedInterfaces := []Interface{ + {Name: "eth1", Network: "default/net1", IPs: []string{"10.0.1.1"}}, + } + + tx := nft.NewTransaction() + hashName := "ipv6block" + + createManagedInterfacesSet(tx, matchedInterfaces, hashName, policy.Namespace, policy.Name, logger) + + err = createPolicyChain(ctx, nft, tx, fmt.Sprintf("cnp-%s", hashName), "ingress", policy.Namespace, policy.Name, logger) + Expect(err).NotTo(HaveOccurred()) + + nftablesInstance := &NFTables{Client: nil} + err = nftablesInstance.createIngressRules(ctx, tx, matchedInterfaces, policy, hashName, logger) + Expect(err).NotTo(HaveOccurred()) + + err = nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + dump := nft.(*knftables.Fake).Dump() + + // Split the dump into lines for easier verification + dumpLines := strings.Split(dump, "\n") + + // Expected rules that should be generated (based on actual output) + expectedRules := []string{ + "add table inet multi_networkpolicy { comment \"MultiNetworkPolicy\" ; }", + "add chain inet multi_networkpolicy input { type filter hook input priority 0 ; comment \"Input Dispatcher\" ; }", + "add chain inet multi_networkpolicy output { type filter hook output priority 0 ; comment \"Output Dispatcher\" ; }", + "add chain inet multi_networkpolicy ingress { comment \"Ingress Policies\" ; }", + "add chain inet multi_networkpolicy egress { comment \"Egress Policies\" ; }", + "add chain inet multi_networkpolicy common-ingress { comment \"Common Policies\" ; }", + "add chain inet multi_networkpolicy common-egress { comment \"Common Policies\" ; }", + "add rule inet multi_networkpolicy egress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy egress jump common-egress comment \"Jump to common\"", + "add rule inet multi_networkpolicy egress drop comment \"Drop rule\"", + "add rule inet multi_networkpolicy ingress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy ingress jump common-ingress comment \"Jump to common\"", + "add rule inet multi_networkpolicy ingress drop comment \"Drop rule\"", + "add set inet multi_networkpolicy smi-ipv6block { type ifname ; comment \"Managed interfaces set for default/ipv6-ipblock-policy\" ; }", + "add element inet multi_networkpolicy smi-ipv6block { eth1 }", + + // New commands to verify + "add chain inet multi_networkpolicy cnp-ipv6block { comment \"MultiNetworkPolicy default/ipv6-ipblock-policy\" ; }", + "add rule inet multi_networkpolicy ingress jump cnp-ipv6block comment \"default/ipv6-ipblock-policy\"", + "add set inet multi_networkpolicy snp-ipv6block_ingress_ipv6_cidr_0 { type ipv6_addr ; flags interval ; comment \"CIDRs for default/ipv6-ipblock-policy\" ; }", + "add set inet multi_networkpolicy snp-ipv6block_ingress_ipv6_except_0 { type ipv6_addr ; flags interval ; comment \"Excepts for default/ipv6-ipblock-policy\" ; }", + "add element inet multi_networkpolicy snp-ipv6block_ingress_ipv6_cidr_0 { 2001:db8::/32 }", + "add element inet multi_networkpolicy snp-ipv6block_ingress_ipv6_except_0 { 2001:db8::1/128 }", + "add rule inet multi_networkpolicy cnp-ipv6block iifname @smi-ipv6block ip6 saddr @snp-ipv6block_ingress_ipv6_cidr_0 ip6 saddr != @snp-ipv6block_ingress_ipv6_except_0 accept", + "add rule inet multi_networkpolicy cnp-ipv6block iifname eth1 ip saddr 10.0.1.1 accept", + "", // Empty line at the end + } + + // Verify exact number of expected rules + Expect(dumpLines).To(HaveLen(len(expectedRules)), "Expected exactly %d rules, but got %d. Rules: %v", len(expectedRules), len(dumpLines), dumpLines) + + // Verify each expected rule exists completely + for _, expectedRule := range expectedRules { + found := false + for _, actualRule := range dumpLines { + if strings.Contains(actualRule, expectedRule) { + found = true + break + } + } + Expect(found).To(BeTrue(), "Expected rule not found: %s\nActual rules: %v", expectedRule, dumpLines) + } + }) + + It("should create rules for ingress with dual-stack IPBlock", func() { + err := ensureBasicStructure(ctx, nft, nil, logger) + Expect(err).NotTo(HaveOccurred()) + + policy := &datastore.Policy{ + Name: "dual-ipblock-policy", + Namespace: "default", + Networks: []string{"default/net1"}, + Spec: multiv1beta1.MultiNetworkPolicySpec{ + Ingress: []multiv1beta1.MultiNetworkPolicyIngressRule{ + { + From: []multiv1beta1.MultiNetworkPolicyPeer{ + { + IPBlock: &multiv1beta1.IPBlock{ + CIDR: "10.0.0.0/24", + }, + }, + { + IPBlock: &multiv1beta1.IPBlock{ + CIDR: "2001:db8::/32", + }, + }, + }, + }, + }, + }, + } + + matchedInterfaces := []Interface{ + {Name: "eth1", Network: "default/net1", IPs: []string{"10.0.1.1"}}, + } + + tx := nft.NewTransaction() + hashName := "dualblock" + + createManagedInterfacesSet(tx, matchedInterfaces, hashName, policy.Namespace, policy.Name, logger) + + err = createPolicyChain(ctx, nft, tx, fmt.Sprintf("cnp-%s", hashName), "ingress", policy.Namespace, policy.Name, logger) + Expect(err).NotTo(HaveOccurred()) + + nftablesInstance := &NFTables{Client: nil} + err = nftablesInstance.createIngressRules(ctx, tx, matchedInterfaces, policy, hashName, logger) + Expect(err).NotTo(HaveOccurred()) + + err = nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + dump := nft.(*knftables.Fake).Dump() + + // Split the dump into lines for easier verification + dumpLines := strings.Split(dump, "\n") + + // Expected rules that should be generated (based on actual output) + expectedRules := []string{ + "add table inet multi_networkpolicy { comment \"MultiNetworkPolicy\" ; }", + "add chain inet multi_networkpolicy input { type filter hook input priority 0 ; comment \"Input Dispatcher\" ; }", + "add chain inet multi_networkpolicy output { type filter hook output priority 0 ; comment \"Output Dispatcher\" ; }", + "add chain inet multi_networkpolicy ingress { comment \"Ingress Policies\" ; }", + "add chain inet multi_networkpolicy egress { comment \"Egress Policies\" ; }", + "add chain inet multi_networkpolicy common-ingress { comment \"Common Policies\" ; }", + "add chain inet multi_networkpolicy common-egress { comment \"Common Policies\" ; }", + "add rule inet multi_networkpolicy egress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy egress jump common-egress comment \"Jump to common\"", + "add rule inet multi_networkpolicy egress drop comment \"Drop rule\"", + "add rule inet multi_networkpolicy ingress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy ingress jump common-ingress comment \"Jump to common\"", + "add rule inet multi_networkpolicy ingress drop comment \"Drop rule\"", + "add set inet multi_networkpolicy smi-dualblock { type ifname ; comment \"Managed interfaces set for default/dual-ipblock-policy\" ; }", + "add element inet multi_networkpolicy smi-dualblock { eth1 }", + + // New commands to verify + "add chain inet multi_networkpolicy cnp-dualblock { comment \"MultiNetworkPolicy default/dual-ipblock-policy\" ; }", + "add rule inet multi_networkpolicy ingress jump cnp-dualblock comment \"default/dual-ipblock-policy\"", + "add set inet multi_networkpolicy snp-dualblock_ingress_ipv4_cidr_0 { type ipv4_addr ; flags interval ; comment \"CIDRs for default/dual-ipblock-policy\" ; }", + "add set inet multi_networkpolicy snp-dualblock_ingress_ipv6_cidr_0 { type ipv6_addr ; flags interval ; comment \"CIDRs for default/dual-ipblock-policy\" ; }", + "add element inet multi_networkpolicy snp-dualblock_ingress_ipv4_cidr_0 { 10.0.0.0/24 }", + "add element inet multi_networkpolicy snp-dualblock_ingress_ipv6_cidr_0 { 2001:db8::/32 }", + "add rule inet multi_networkpolicy cnp-dualblock iifname @smi-dualblock ip saddr @snp-dualblock_ingress_ipv4_cidr_0 accept", + "add rule inet multi_networkpolicy cnp-dualblock iifname @smi-dualblock ip6 saddr @snp-dualblock_ingress_ipv6_cidr_0 accept", + "add rule inet multi_networkpolicy cnp-dualblock iifname eth1 ip saddr 10.0.1.1 accept", + "", // Empty line at the end + } + + // Verify exact number of expected rules + Expect(dumpLines).To(HaveLen(len(expectedRules)), "Expected exactly %d rules, but got %d. Rules: %v", len(expectedRules), len(dumpLines), dumpLines) + + // Verify each expected rule exists completely + for _, expectedRule := range expectedRules { + found := false + for _, actualRule := range dumpLines { + if strings.Contains(actualRule, expectedRule) { + found = true + break + } + } + Expect(found).To(BeTrue(), "Expected rule not found: %s\nActual rules: %v", expectedRule, dumpLines) + } + }) + + It("should create rules for ingress with mixed pod selector and IPBlock with ports", func() { + err := ensureBasicStructure(ctx, nft, nil, logger) + Expect(err).NotTo(HaveOccurred()) + + scheme := runtime.NewScheme() + _ = corev1.AddToScheme(scheme) + + pod1 := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod1", + Namespace: "default", + Labels: map[string]string{"app": "web"}, + Annotations: map[string]string{ + "k8s.v1.cni.cncf.io/networks": `[{"name": "net1"}]`, + "k8s.v1.cni.cncf.io/network-status": `[{ + "name": "default/net1", + "interface": "eth1", + "ips": ["10.0.1.1"] + }]`, + }, + }, + Status: corev1.PodStatus{Phase: corev1.PodRunning}, + Spec: corev1.PodSpec{HostNetwork: false}, + } + + fakeClient := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(pod1). + WithIndex(&corev1.Pod{}, PodStatusIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + return []string{string(pod.Status.Phase)} + }). + WithIndex(&corev1.Pod{}, PodHostNetworkIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + return []string{fmt.Sprintf("%t", pod.Spec.HostNetwork)} + }). + WithIndex(&corev1.Pod{}, PodHasNetworkAnnotationIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + _, hasAnnotation := pod.Annotations["k8s.v1.cni.cncf.io/networks"] + return []string{fmt.Sprintf("%t", hasAnnotation)} + }). + Build() + + tcpProtocol := corev1.ProtocolTCP + port80 := intstr.FromInt(80) + + policy := &datastore.Policy{ + Name: "mixed-policy", + Namespace: "default", + Networks: []string{"default/net1"}, + Spec: multiv1beta1.MultiNetworkPolicySpec{ + Ingress: []multiv1beta1.MultiNetworkPolicyIngressRule{ + { + From: []multiv1beta1.MultiNetworkPolicyPeer{ + { + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "web"}, + }, + }, + { + IPBlock: &multiv1beta1.IPBlock{ + CIDR: "192.168.1.0/24", + }, + }, + }, + Ports: []multiv1beta1.MultiNetworkPolicyPort{ + {Protocol: &tcpProtocol, Port: &port80}, + }, + }, + }, + }, + } + + matchedInterfaces := []Interface{ + {Name: "eth1", Network: "default/net1", IPs: []string{"10.0.1.1"}}, + } + + tx := nft.NewTransaction() + hashName := "mixed" + + createManagedInterfacesSet(tx, matchedInterfaces, hashName, policy.Namespace, policy.Name, logger) + + err = createPolicyChain(ctx, nft, tx, fmt.Sprintf("cnp-%s", hashName), "ingress", policy.Namespace, policy.Name, logger) + Expect(err).NotTo(HaveOccurred()) + + nftablesInstance := &NFTables{Client: fakeClient} + err = nftablesInstance.createIngressRules(ctx, tx, matchedInterfaces, policy, hashName, logger) + Expect(err).NotTo(HaveOccurred()) + + err = nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + dump := nft.(*knftables.Fake).Dump() + + // Split the dump into lines for easier verification + dumpLines := strings.Split(dump, "\n") + + // Expected rules that should be generated (based on actual output) + expectedRules := []string{ + "add table inet multi_networkpolicy { comment \"MultiNetworkPolicy\" ; }", + "add chain inet multi_networkpolicy input { type filter hook input priority 0 ; comment \"Input Dispatcher\" ; }", + "add chain inet multi_networkpolicy output { type filter hook output priority 0 ; comment \"Output Dispatcher\" ; }", + "add chain inet multi_networkpolicy ingress { comment \"Ingress Policies\" ; }", + "add chain inet multi_networkpolicy egress { comment \"Egress Policies\" ; }", + "add chain inet multi_networkpolicy common-ingress { comment \"Common Policies\" ; }", + "add chain inet multi_networkpolicy common-egress { comment \"Common Policies\" ; }", + "add rule inet multi_networkpolicy egress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy egress jump common-egress comment \"Jump to common\"", + "add rule inet multi_networkpolicy egress drop comment \"Drop rule\"", + "add rule inet multi_networkpolicy ingress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy ingress jump common-ingress comment \"Jump to common\"", + "add rule inet multi_networkpolicy ingress drop comment \"Drop rule\"", + "add set inet multi_networkpolicy smi-mixed { type ifname ; comment \"Managed interfaces set for default/mixed-policy\" ; }", + "add element inet multi_networkpolicy smi-mixed { eth1 }", + + // New commands to verify + "add chain inet multi_networkpolicy cnp-mixed { comment \"MultiNetworkPolicy default/mixed-policy\" ; }", + "add rule inet multi_networkpolicy ingress jump cnp-mixed comment \"default/mixed-policy\"", + "add set inet multi_networkpolicy snp-mixed_ingress_ipv4_eth1_0 { type ipv4_addr ; comment \"Addresses for default/mixed-policy\" ; }", + "add set inet multi_networkpolicy snp-mixed_ingress_ipv4_cidr_0 { type ipv4_addr ; flags interval ; comment \"CIDRs for default/mixed-policy\" ; }", + "add element inet multi_networkpolicy snp-mixed_ingress_ipv4_eth1_0 { 10.0.1.1 }", + "add element inet multi_networkpolicy snp-mixed_ingress_ipv4_cidr_0 { 192.168.1.0/24 }", + "add rule inet multi_networkpolicy cnp-mixed iifname eth1 ip saddr @snp-mixed_ingress_ipv4_eth1_0 meta l4proto tcp th dport { 80 } accept", + "add set inet multi_networkpolicy snp-mixed_ingress_ipv4_cidr_0 { type ipv4_addr ; flags interval ; comment \"CIDRs for default/mixed-policy\" ; }", + "add rule inet multi_networkpolicy cnp-mixed iifname eth1 ip saddr 10.0.1.1 accept", + "", // Empty line at the end + } + + // Verify exact number of expected rules + Expect(dumpLines).To(HaveLen(len(expectedRules)), "Expected exactly %d rules, but got %d. Rules: %v", len(expectedRules), len(dumpLines), dumpLines) + + // Verify each expected rule exists completely + for _, expectedRule := range expectedRules { + found := false + for _, actualRule := range dumpLines { + if strings.Contains(actualRule, expectedRule) { + found = true + break + } + } + Expect(found).To(BeTrue(), "Expected rule not found: %s\nActual rules: %v", expectedRule, dumpLines) + } + }) + + It("should handle multiple interfaces for the same network", func() { + err := ensureBasicStructure(ctx, nft, nil, logger) + Expect(err).NotTo(HaveOccurred()) + + scheme := runtime.NewScheme() + _ = corev1.AddToScheme(scheme) + + pod1 := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod1", + Namespace: "default", + Labels: map[string]string{"app": "web"}, + Annotations: map[string]string{ + "k8s.v1.cni.cncf.io/networks": `[{"name": "net1"}]`, + "k8s.v1.cni.cncf.io/network-status": `[{ + "name": "default/net1", + "interface": "eth1", + "ips": ["10.0.1.1"] + }]`, + }, + }, + Status: corev1.PodStatus{Phase: corev1.PodRunning}, + Spec: corev1.PodSpec{HostNetwork: false}, + } + + fakeClient := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(pod1). + WithIndex(&corev1.Pod{}, PodStatusIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + return []string{string(pod.Status.Phase)} + }). + WithIndex(&corev1.Pod{}, PodHostNetworkIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + return []string{fmt.Sprintf("%t", pod.Spec.HostNetwork)} + }). + WithIndex(&corev1.Pod{}, PodHasNetworkAnnotationIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + _, hasAnnotation := pod.Annotations["k8s.v1.cni.cncf.io/networks"] + return []string{fmt.Sprintf("%t", hasAnnotation)} + }). + Build() + + policy := &datastore.Policy{ + Name: "multi-interface-policy", + Namespace: "default", + Networks: []string{"default/net1"}, + Spec: multiv1beta1.MultiNetworkPolicySpec{ + Ingress: []multiv1beta1.MultiNetworkPolicyIngressRule{ + { + From: []multiv1beta1.MultiNetworkPolicyPeer{ + { + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "web"}, + }, + }, + }, + }, + }, + }, + } + + matchedInterfaces := []Interface{ + {Name: "eth1", Network: "default/net1", IPs: []string{"10.0.1.1"}}, + {Name: "eth2", Network: "default/net1", IPs: []string{"10.0.1.2"}}, + {Name: "eth3", Network: "default/net1", IPs: []string{"10.0.1.3"}}, + } + + tx := nft.NewTransaction() + hashName := "multiintf" + err = createPolicyChain(ctx, nft, tx, fmt.Sprintf("cnp-%s", hashName), "ingress", policy.Namespace, policy.Name, logger) + Expect(err).NotTo(HaveOccurred()) + + nftablesInstance := &NFTables{Client: fakeClient} + err = nftablesInstance.createIngressRules(ctx, tx, matchedInterfaces, policy, hashName, logger) + Expect(err).NotTo(HaveOccurred()) + + err = nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + dump := nft.(*knftables.Fake).Dump() + + // Split the dump into lines for easier verification + dumpLines := strings.Split(dump, "\n") + + // Expected rules that should be generated (based on actual output) + expectedRules := []string{ + "add table inet multi_networkpolicy { comment \"MultiNetworkPolicy\" ; }", + "add chain inet multi_networkpolicy input { type filter hook input priority 0 ; comment \"Input Dispatcher\" ; }", + "add chain inet multi_networkpolicy output { type filter hook output priority 0 ; comment \"Output Dispatcher\" ; }", + "add chain inet multi_networkpolicy ingress { comment \"Ingress Policies\" ; }", + "add chain inet multi_networkpolicy egress { comment \"Egress Policies\" ; }", + "add chain inet multi_networkpolicy common-ingress { comment \"Common Policies\" ; }", + "add chain inet multi_networkpolicy common-egress { comment \"Common Policies\" ; }", + "add rule inet multi_networkpolicy egress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy egress jump common-egress comment \"Jump to common\"", + "add rule inet multi_networkpolicy egress drop comment \"Drop rule\"", + "add rule inet multi_networkpolicy ingress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy ingress jump common-ingress comment \"Jump to common\"", + "add rule inet multi_networkpolicy ingress drop comment \"Drop rule\"", + + // New commands to verify + "add chain inet multi_networkpolicy cnp-multiintf { comment \"MultiNetworkPolicy default/multi-interface-policy\" ; }", + "add rule inet multi_networkpolicy ingress jump cnp-multiintf comment \"default/multi-interface-policy\"", + "add set inet multi_networkpolicy snp-multiintf_ingress_ipv4_eth1_0 { type ipv4_addr ; comment \"Addresses for default/multi-interface-policy\" ; }", + "add set inet multi_networkpolicy snp-multiintf_ingress_ipv4_eth2_0 { type ipv4_addr ; comment \"Addresses for default/multi-interface-policy\" ; }", + "add set inet multi_networkpolicy snp-multiintf_ingress_ipv4_eth3_0 { type ipv4_addr ; comment \"Addresses for default/multi-interface-policy\" ; }", + "add element inet multi_networkpolicy snp-multiintf_ingress_ipv4_eth1_0 { 10.0.1.1 }", + "add element inet multi_networkpolicy snp-multiintf_ingress_ipv4_eth2_0 { 10.0.1.1 }", + "add element inet multi_networkpolicy snp-multiintf_ingress_ipv4_eth3_0 { 10.0.1.1 }", + "add rule inet multi_networkpolicy cnp-multiintf iifname eth1 ip saddr @snp-multiintf_ingress_ipv4_eth1_0 accept", + "add rule inet multi_networkpolicy cnp-multiintf iifname eth2 ip saddr @snp-multiintf_ingress_ipv4_eth2_0 accept", + "add rule inet multi_networkpolicy cnp-multiintf iifname eth3 ip saddr @snp-multiintf_ingress_ipv4_eth3_0 accept", + "add rule inet multi_networkpolicy cnp-multiintf iifname eth1 ip saddr 10.0.1.1 accept", + "add rule inet multi_networkpolicy cnp-multiintf iifname eth2 ip saddr 10.0.1.2 accept", + "add rule inet multi_networkpolicy cnp-multiintf iifname eth3 ip saddr 10.0.1.3 accept", + "", // Empty line at the end + } + + // Verify exact number of expected rules + Expect(dumpLines).To(HaveLen(len(expectedRules)), "Expected exactly %d rules, but got %d. Rules: %v", len(expectedRules), len(dumpLines), dumpLines) + + // Verify each expected rule exists completely + for _, expectedRule := range expectedRules { + found := false + for _, actualRule := range dumpLines { + if strings.Contains(actualRule, expectedRule) { + found = true + break + } + } + Expect(found).To(BeTrue(), "Expected rule not found: %s\nActual rules: %v", expectedRule, dumpLines) + } + }) + }) + + Context("parsePeers", func() { + var ( + ctx context.Context + fakeClient client.Client + nftables *NFTables + logger logr.Logger + policyNamespace string + ) + + BeforeEach(func() { + ctx = context.Background() + logger = logr.Discard() + policyNamespace = "default" + + // Create fake client with test data + scheme := runtime.NewScheme() + _ = corev1.AddToScheme(scheme) + + // Create test pods + pod1 := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod1", + Namespace: "default", + Labels: map[string]string{"app": "web", "tier": "frontend"}, + Annotations: map[string]string{ + "k8s.v1.cni.cncf.io/networks": "net1", + }, + }, + Status: corev1.PodStatus{ + Phase: corev1.PodRunning, + }, + Spec: corev1.PodSpec{ + HostNetwork: false, + }, + } + + pod2 := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod2", + Namespace: "kube-system", + Labels: map[string]string{"app": "db", "tier": "backend"}, + Annotations: map[string]string{ + "k8s.v1.cni.cncf.io/networks": "net2", + }, + }, + Status: corev1.PodStatus{ + Phase: corev1.PodRunning, + }, + Spec: corev1.PodSpec{ + HostNetwork: false, + }, + } + + pod3 := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod3", + Namespace: "production", + Labels: map[string]string{"app": "api", "env": "prod"}, + Annotations: map[string]string{ + "k8s.v1.cni.cncf.io/networks": "net3", + }, + }, + Status: corev1.PodStatus{ + Phase: corev1.PodRunning, + }, + Spec: corev1.PodSpec{ + HostNetwork: false, + }, + } + + // Create test namespaces + ns1 := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default", + Labels: map[string]string{"env": "test"}, + }, + } + + ns2 := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "kube-system", + Labels: map[string]string{"env": "system"}, + }, + } + + ns3 := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "production", + Labels: map[string]string{"env": "prod"}, + }, + } + + fakeClient = fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(pod1, pod2, pod3, ns1, ns2, ns3). + WithIndex(&corev1.Pod{}, PodStatusIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + return []string{string(pod.Status.Phase)} + }). + WithIndex(&corev1.Pod{}, PodHostNetworkIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + return []string{fmt.Sprintf("%t", pod.Spec.HostNetwork)} + }). + WithIndex(&corev1.Pod{}, PodHasNetworkAnnotationIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + _, hasAnnotation := pod.Annotations["k8s.v1.cni.cncf.io/networks"] + return []string{fmt.Sprintf("%t", hasAnnotation)} + }). + Build() + + nftables = &NFTables{ + Client: fakeClient, + } + }) + + It("should handle empty peers list", func() { + peers := []multiv1beta1.MultiNetworkPolicyPeer{} + + result, err := nftables.parsePeers(ctx, peers, policyNamespace, logger) + Expect(err).NotTo(HaveOccurred()) + Expect(result).NotTo(BeNil()) + Expect(result.pods).To(BeEmpty()) + Expect(result.cidrs).To(BeEmpty()) + Expect(result.excepts).To(BeEmpty()) + }) + + It("should handle IPBlock peer", func() { + peers := []multiv1beta1.MultiNetworkPolicyPeer{ + { + IPBlock: &multiv1beta1.IPBlock{ + CIDR: "10.0.0.0/24", + Except: []string{"10.0.0.1", "10.0.0.2"}, + }, + }, + } + + result, err := nftables.parsePeers(ctx, peers, policyNamespace, logger) + Expect(err).NotTo(HaveOccurred()) + Expect(result).NotTo(BeNil()) + Expect(result.pods).To(BeEmpty()) + Expect(result.cidrs).To(HaveLen(1)) + Expect(result.cidrs[0]).To(Equal("10.0.0.0/24")) + Expect(result.excepts).To(HaveLen(2)) + Expect(result.excepts).To(ContainElements("10.0.0.1", "10.0.0.2")) + }) + + It("should handle multiple IPBlock peers", func() { + peers := []multiv1beta1.MultiNetworkPolicyPeer{ + { + IPBlock: &multiv1beta1.IPBlock{ + CIDR: "10.0.0.0/24", + Except: []string{"10.0.0.1"}, + }, + }, + { + IPBlock: &multiv1beta1.IPBlock{ + CIDR: "192.168.1.0/24", + Except: []string{"192.168.1.1", "192.168.1.2"}, + }, + }, + } + + result, err := nftables.parsePeers(ctx, peers, policyNamespace, logger) + Expect(err).NotTo(HaveOccurred()) + Expect(result).NotTo(BeNil()) + Expect(result.pods).To(BeEmpty()) + Expect(result.cidrs).To(HaveLen(2)) + Expect(result.cidrs).To(ContainElements("10.0.0.0/24", "192.168.1.0/24")) + Expect(result.excepts).To(HaveLen(3)) + Expect(result.excepts).To(ContainElements("10.0.0.1", "192.168.1.1", "192.168.1.2")) + }) + + It("should handle both NamespaceSelector and PodSelector", func() { + peers := []multiv1beta1.MultiNetworkPolicyPeer{ + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"env": "prod"}, + }, + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "api"}, + }, + }, + } + + result, err := nftables.parsePeers(ctx, peers, policyNamespace, logger) + Expect(err).NotTo(HaveOccurred()) + Expect(result).NotTo(BeNil()) + Expect(result.cidrs).To(BeEmpty()) + Expect(result.excepts).To(BeEmpty()) + Expect(result.pods).To(HaveLen(1)) + Expect(result.pods[0].Name).To(Equal("pod3")) + Expect(result.pods[0].Namespace).To(Equal("production")) + }) + + It("should handle only NamespaceSelector", func() { + peers := []multiv1beta1.MultiNetworkPolicyPeer{ + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"env": "system"}, + }, + }, + } + + result, err := nftables.parsePeers(ctx, peers, policyNamespace, logger) + Expect(err).NotTo(HaveOccurred()) + Expect(result).NotTo(BeNil()) + Expect(result.cidrs).To(BeEmpty()) + Expect(result.excepts).To(BeEmpty()) + Expect(result.pods).To(HaveLen(1)) + Expect(result.pods[0].Name).To(Equal("pod2")) + Expect(result.pods[0].Namespace).To(Equal("kube-system")) + }) + + It("should handle only PodSelector", func() { + peers := []multiv1beta1.MultiNetworkPolicyPeer{ + { + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "web"}, + }, + }, + } + + result, err := nftables.parsePeers(ctx, peers, policyNamespace, logger) + Expect(err).NotTo(HaveOccurred()) + Expect(result).NotTo(BeNil()) + Expect(result.cidrs).To(BeEmpty()) + Expect(result.excepts).To(BeEmpty()) + Expect(result.pods).To(HaveLen(1)) + Expect(result.pods[0].Name).To(Equal("pod1")) + Expect(result.pods[0].Namespace).To(Equal("default")) + }) + + It("should handle mixed peer types", func() { + peers := []multiv1beta1.MultiNetworkPolicyPeer{ + { + IPBlock: &multiv1beta1.IPBlock{ + CIDR: "10.0.0.0/24", + }, + }, + { + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "web"}, + }, + }, + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"env": "system"}, + }, + }, + } + + result, err := nftables.parsePeers(ctx, peers, policyNamespace, logger) + Expect(err).NotTo(HaveOccurred()) + Expect(result).NotTo(BeNil()) + Expect(result.cidrs).To(HaveLen(1)) + Expect(result.cidrs[0]).To(Equal("10.0.0.0/24")) + Expect(result.excepts).To(BeEmpty()) + Expect(result.pods).To(HaveLen(2)) + + podNames := []string{} + for _, pod := range result.pods { + podNames = append(podNames, pod.Name) + } + Expect(podNames).To(ContainElements("pod1", "pod2")) + }) + + It("should deduplicate pods", func() { + peers := []multiv1beta1.MultiNetworkPolicyPeer{ + { + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "web"}, + }, + }, + { + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"tier": "frontend"}, + }, + }, + } + + result, err := nftables.parsePeers(ctx, peers, policyNamespace, logger) + Expect(err).NotTo(HaveOccurred()) + Expect(result).NotTo(BeNil()) + Expect(result.pods).To(HaveLen(1)) // Should be deduplicated + Expect(result.pods[0].Name).To(Equal("pod1")) + }) + + It("should handle empty namespace selector results", func() { + peers := []multiv1beta1.MultiNetworkPolicyPeer{ + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"env": "nonexistent"}, + }, + }, + } + + result, err := nftables.parsePeers(ctx, peers, policyNamespace, logger) + Expect(err).NotTo(HaveOccurred()) + Expect(result).NotTo(BeNil()) + Expect(result.pods).To(BeEmpty()) + }) + + It("should handle empty pod selector results", func() { + peers := []multiv1beta1.MultiNetworkPolicyPeer{ + { + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "nonexistent"}, + }, + }, + } + + result, err := nftables.parsePeers(ctx, peers, policyNamespace, logger) + Expect(err).NotTo(HaveOccurred()) + Expect(result).NotTo(BeNil()) + Expect(result.pods).To(BeEmpty()) + }) + + It("should handle invalid namespace selector", func() { + peers := []multiv1beta1.MultiNetworkPolicyPeer{ + { + NamespaceSelector: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "invalid", + Operator: "InvalidOperator", // Invalid operator + Values: []string{"value"}, + }, + }, + }, + }, + } + + result, err := nftables.parsePeers(ctx, peers, policyNamespace, logger) + Expect(err).NotTo(HaveOccurred()) + Expect(result).NotTo(BeNil()) + Expect(result.pods).To(BeEmpty()) // Invalid selector should return empty results + }) + + It("should handle invalid pod selector", func() { + peers := []multiv1beta1.MultiNetworkPolicyPeer{ + { + PodSelector: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "invalid", + Operator: "InvalidOperator", // Invalid operator + Values: []string{"value"}, + }, + }, + }, + }, + } + + result, err := nftables.parsePeers(ctx, peers, policyNamespace, logger) + Expect(err).NotTo(HaveOccurred()) + Expect(result).NotTo(BeNil()) + Expect(result.pods).To(BeEmpty()) // Invalid selector should return empty results + }) + }) + + Context("getPodsByPodSelector", func() { + var ( + ctx context.Context + fakeClient client.Client + nftables *NFTables + ) + + BeforeEach(func() { + ctx = context.Background() + + scheme := runtime.NewScheme() + _ = corev1.AddToScheme(scheme) + + // Create test pods + pod1 := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "web-pod", + Namespace: "default", + Labels: map[string]string{"app": "web", "tier": "frontend"}, + Annotations: map[string]string{ + "k8s.v1.cni.cncf.io/networks": "net1", + }, + }, + Status: corev1.PodStatus{Phase: corev1.PodRunning}, + Spec: corev1.PodSpec{HostNetwork: false}, + } + + pod2 := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "db-pod", + Namespace: "default", + Labels: map[string]string{"app": "db", "tier": "backend"}, + Annotations: map[string]string{ + "k8s.v1.cni.cncf.io/networks": "net2", + }, + }, + Status: corev1.PodStatus{Phase: corev1.PodRunning}, + Spec: corev1.PodSpec{HostNetwork: false}, + } + + // Pod without network annotation (should be filtered out) + pod3 := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "no-network-pod", + Namespace: "default", + Labels: map[string]string{"app": "system"}, + }, + Status: corev1.PodStatus{Phase: corev1.PodRunning}, + Spec: corev1.PodSpec{HostNetwork: false}, + } + + // Host network pod (should be filtered out) + pod4 := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "host-network-pod", + Namespace: "default", + Labels: map[string]string{"app": "host"}, + Annotations: map[string]string{ + "k8s.v1.cni.cncf.io/networks": "net3", + }, + }, + Status: corev1.PodStatus{Phase: corev1.PodRunning}, + Spec: corev1.PodSpec{HostNetwork: true}, + } + + // Non-running pod (should be filtered out) + pod5 := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pending-pod", + Namespace: "default", + Labels: map[string]string{"app": "pending"}, + Annotations: map[string]string{ + "k8s.v1.cni.cncf.io/networks": "net4", + }, + }, + Status: corev1.PodStatus{Phase: corev1.PodPending}, + Spec: corev1.PodSpec{HostNetwork: false}, + } + + fakeClient = fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(pod1, pod2, pod3, pod4, pod5). + WithIndex(&corev1.Pod{}, PodStatusIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + return []string{string(pod.Status.Phase)} + }). + WithIndex(&corev1.Pod{}, PodHostNetworkIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + return []string{fmt.Sprintf("%t", pod.Spec.HostNetwork)} + }). + WithIndex(&corev1.Pod{}, PodHasNetworkAnnotationIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + _, hasAnnotation := pod.Annotations["k8s.v1.cni.cncf.io/networks"] + return []string{fmt.Sprintf("%t", hasAnnotation)} + }). + Build() + + nftables = &NFTables{Client: fakeClient} + }) + + It("should get pods by label selector", func() { + selector := &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "web"}, + } + + pods, err := nftables.getPodsByPodSelector(ctx, selector, "default") + Expect(err).NotTo(HaveOccurred()) + Expect(pods).To(HaveLen(1)) + Expect(pods[0].Name).To(Equal("web-pod")) + }) + + It("should filter pods by running status, non-host network, and network annotation", func() { + selector := &metav1.LabelSelector{ + MatchLabels: map[string]string{}, // Match all pods + } + + pods, err := nftables.getPodsByPodSelector(ctx, selector, "default") + Expect(err).NotTo(HaveOccurred()) + Expect(pods).To(HaveLen(2)) // Only web-pod and db-pod should match + + podNames := []string{} + for _, pod := range pods { + podNames = append(podNames, pod.Name) + } + Expect(podNames).To(ContainElements("web-pod", "db-pod")) + }) + + It("should handle invalid selector", func() { + selector := &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "invalid", + Operator: "InvalidOperator", + Values: []string{"value"}, + }, + }, + } + + pods, err := nftables.getPodsByPodSelector(ctx, selector, "default") + Expect(err).NotTo(HaveOccurred()) + Expect(pods).To(BeNil()) // Invalid selector should return nil + }) + + It("should return empty for no matching pods", func() { + selector := &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "nonexistent"}, + } + + pods, err := nftables.getPodsByPodSelector(ctx, selector, "default") + Expect(err).NotTo(HaveOccurred()) + Expect(pods).To(BeEmpty()) + }) + }) + + Context("getPodsByNamespace", func() { + var ( + ctx context.Context + fakeClient client.Client + nftables *NFTables + ) + + BeforeEach(func() { + ctx = context.Background() + + scheme := runtime.NewScheme() + _ = corev1.AddToScheme(scheme) + + // Create test pods in different namespaces + pod1 := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod1", + Namespace: "target-ns", + Labels: map[string]string{"app": "web"}, + Annotations: map[string]string{ + "k8s.v1.cni.cncf.io/networks": "net1", + }, + }, + Status: corev1.PodStatus{Phase: corev1.PodRunning}, + Spec: corev1.PodSpec{HostNetwork: false}, + } + + pod2 := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod2", + Namespace: "target-ns", + Labels: map[string]string{"app": "db"}, + Annotations: map[string]string{ + "k8s.v1.cni.cncf.io/networks": "net2", + }, + }, + Status: corev1.PodStatus{Phase: corev1.PodRunning}, + Spec: corev1.PodSpec{HostNetwork: false}, + } + + // Pod in different namespace + pod3 := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod3", + Namespace: "other-ns", + Labels: map[string]string{"app": "api"}, + Annotations: map[string]string{ + "k8s.v1.cni.cncf.io/networks": "net3", + }, + }, + Status: corev1.PodStatus{Phase: corev1.PodRunning}, + Spec: corev1.PodSpec{HostNetwork: false}, + } + + // Pod that should be filtered out (no network annotation) + pod4 := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "filtered-pod", + Namespace: "target-ns", + Labels: map[string]string{"app": "filtered"}, + }, + Status: corev1.PodStatus{Phase: corev1.PodRunning}, + Spec: corev1.PodSpec{HostNetwork: false}, + } + + fakeClient = fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(pod1, pod2, pod3, pod4). + WithIndex(&corev1.Pod{}, PodStatusIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + return []string{string(pod.Status.Phase)} + }). + WithIndex(&corev1.Pod{}, PodHostNetworkIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + return []string{fmt.Sprintf("%t", pod.Spec.HostNetwork)} + }). + WithIndex(&corev1.Pod{}, PodHasNetworkAnnotationIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + _, hasAnnotation := pod.Annotations["k8s.v1.cni.cncf.io/networks"] + return []string{fmt.Sprintf("%t", hasAnnotation)} + }). + Build() + + nftables = &NFTables{Client: fakeClient} + }) + + It("should get all eligible pods from namespace", func() { + pods, err := nftables.getPodsByNamespace(ctx, "target-ns") + Expect(err).NotTo(HaveOccurred()) + Expect(pods).To(HaveLen(2)) + + podNames := []string{} + for _, pod := range pods { + podNames = append(podNames, pod.Name) + } + Expect(podNames).To(ContainElements("pod1", "pod2")) + }) + + It("should return empty for nonexistent namespace", func() { + pods, err := nftables.getPodsByNamespace(ctx, "nonexistent-ns") + Expect(err).NotTo(HaveOccurred()) + Expect(pods).To(BeEmpty()) + }) + + It("should filter pods correctly", func() { + pods, err := nftables.getPodsByNamespace(ctx, "target-ns") + Expect(err).NotTo(HaveOccurred()) + + // Should not include filtered-pod (no network annotation) + for _, pod := range pods { + Expect(pod.Name).NotTo(Equal("filtered-pod")) + } + }) + }) + + Context("getNamespacesByNamespaceSelector", func() { + var ( + ctx context.Context + fakeClient client.Client + nftables *NFTables + ) + + BeforeEach(func() { + ctx = context.Background() + + scheme := runtime.NewScheme() + _ = corev1.AddToScheme(scheme) + + // Create test namespaces + ns1 := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "production", + Labels: map[string]string{"env": "prod", "tier": "production"}, + }, + } + + ns2 := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "staging", + Labels: map[string]string{"env": "staging", "tier": "staging"}, + }, + } + + ns3 := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "development", + Labels: map[string]string{"env": "dev", "tier": "development"}, + }, + } + + ns4 := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "no-labels", + }, + } + + fakeClient = fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(ns1, ns2, ns3, ns4). + Build() + + nftables = &NFTables{Client: fakeClient} + }) + + It("should get namespaces by label selector", func() { + selector := &metav1.LabelSelector{ + MatchLabels: map[string]string{"env": "prod"}, + } + + namespaces, err := nftables.getNamespacesByNamespaceSelector(ctx, selector) + Expect(err).NotTo(HaveOccurred()) + Expect(namespaces).To(HaveLen(1)) + Expect(namespaces[0].Name).To(Equal("production")) + }) + + It("should handle match expressions", func() { + selector := &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "tier", + Operator: metav1.LabelSelectorOpIn, + Values: []string{"production", "staging"}, + }, + }, + } + + namespaces, err := nftables.getNamespacesByNamespaceSelector(ctx, selector) + Expect(err).NotTo(HaveOccurred()) + Expect(namespaces).To(HaveLen(2)) + + nsNames := []string{} + for _, ns := range namespaces { + nsNames = append(nsNames, ns.Name) + } + Expect(nsNames).To(ContainElements("production", "staging")) + }) + + It("should return empty for no matching namespaces", func() { + selector := &metav1.LabelSelector{ + MatchLabels: map[string]string{"env": "nonexistent"}, + } + + namespaces, err := nftables.getNamespacesByNamespaceSelector(ctx, selector) + Expect(err).NotTo(HaveOccurred()) + Expect(namespaces).To(BeEmpty()) + }) + + It("should handle invalid selector", func() { + selector := &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "invalid", + Operator: "InvalidOperator", + Values: []string{"value"}, + }, + }, + } + + namespaces, err := nftables.getNamespacesByNamespaceSelector(ctx, selector) + Expect(err).NotTo(HaveOccurred()) + Expect(namespaces).To(BeNil()) // Invalid selector should return nil + }) + + It("should handle empty selector (match all)", func() { + selector := &metav1.LabelSelector{} + + namespaces, err := nftables.getNamespacesByNamespaceSelector(ctx, selector) + Expect(err).NotTo(HaveOccurred()) + Expect(namespaces).To(HaveLen(4)) // Should match all namespaces + }) + }) + + Context("getPodInterfacesMap", func() { + It("should create empty map for empty pods list", func() { + pods := []corev1.Pod{} + networks := []string{"default/net1", "default/net2"} + + result := getPodInterfacesMap(pods, networks) + Expect(result).NotTo(BeNil()) + Expect(result).To(BeEmpty()) + }) + + It("should create map with pod interfaces", func() { + pods := []corev1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pod1", + Namespace: "default", + Annotations: map[string]string{ + "k8s.v1.cni.cncf.io/networks": `[ + {"name": "net1"}, + {"name": "net2"} + ]`, + "k8s.v1.cni.cncf.io/network-status": `[ + { + "name": "default/net1", + "interface": "eth1", + "ips": ["10.0.1.1", "2001:db8::1"] + }, + { + "name": "default/net2", + "interface": "eth2", + "ips": ["10.0.2.1"] + } + ]`, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pod2", + Namespace: "kube-system", + Annotations: map[string]string{ + "k8s.v1.cni.cncf.io/networks": `[ + {"name": "net1"}, + {"name": "net3"} + ]`, + "k8s.v1.cni.cncf.io/network-status": `[ + { + "name": "kube-system/net1", + "interface": "eth1", + "ips": ["10.0.1.2"] + }, + { + "name": "kube-system/net3", + "interface": "eth3", + "ips": ["10.0.3.1"] + } + ]`, + }, + }, + }, + } + networks := []string{"default/net1", "default/net2", "kube-system/net1"} + + result := getPodInterfacesMap(pods, networks) + Expect(result).To(HaveLen(2)) + + // Check pod1 interfaces + pod1Key := "pod1/default" + Expect(result).To(HaveKey(pod1Key)) + pod1Interfaces := result[pod1Key] + Expect(pod1Interfaces).To(HaveLen(2)) // net1 and net2 + + // Verify net1 interface for pod1 + net1Found := false + net2Found := false + for _, intf := range pod1Interfaces { + if intf.Network == "default/net1" { + net1Found = true + Expect(intf.Name).To(Equal("eth1")) + Expect(intf.IPs).To(ContainElements("10.0.1.1", "2001:db8::1")) + } + if intf.Network == "default/net2" { + net2Found = true + Expect(intf.Name).To(Equal("eth2")) + Expect(intf.IPs).To(ContainElement("10.0.2.1")) + } + } + Expect(net1Found).To(BeTrue()) + Expect(net2Found).To(BeTrue()) + + // Check pod2 interfaces + pod2Key := "pod2/kube-system" + Expect(result).To(HaveKey(pod2Key)) + pod2Interfaces := result[pod2Key] + Expect(pod2Interfaces).To(HaveLen(1)) // Only net1 matches networks filter + Expect(pod2Interfaces[0].Network).To(Equal("kube-system/net1")) + Expect(pod2Interfaces[0].Name).To(Equal("eth1")) + Expect(pod2Interfaces[0].IPs).To(ContainElement("10.0.1.2")) + }) + + It("should filter interfaces by networks", func() { + pods := []corev1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pod1", + Namespace: "default", + Annotations: map[string]string{ + "k8s.v1.cni.cncf.io/networks": `[ + {"name": "net1"}, + {"name": "net2"}, + {"name": "net3"} + ]`, + "k8s.v1.cni.cncf.io/network-status": `[ + { + "name": "default/net1", + "interface": "eth1", + "ips": ["10.0.1.1"] + }, + { + "name": "net2", + "interface": "eth2", + "ips": ["10.0.2.1"] + }, + { + "name": "net3", + "interface": "eth3", + "ips": ["10.0.3.1"] + } + ]`, + }, + }, + }, + } + networks := []string{"default/net1", "default/net3"} // Only net1 and net3 + + result := getPodInterfacesMap(pods, networks) + Expect(result).To(HaveLen(1)) + + pod1Key := "pod1/default" + pod1Interfaces := result[pod1Key] + Expect(pod1Interfaces).To(HaveLen(2)) // Only net1 and net3 + + networkNames := []string{} + for _, intf := range pod1Interfaces { + networkNames = append(networkNames, intf.Network) + } + Expect(networkNames).To(ContainElements("default/net1", "default/net3")) + Expect(networkNames).NotTo(ContainElement("default/net2")) + }) + + It("should handle pods with no network annotations", func() { + pods := []corev1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pod1", + Namespace: "default", + // No network annotations + }, + }, + } + networks := []string{"default/net1"} + + result := getPodInterfacesMap(pods, networks) + Expect(result).To(HaveLen(1)) + + pod1Key := "pod1/default" + Expect(result).To(HaveKey(pod1Key)) + Expect(result[pod1Key]).To(BeEmpty()) // No interfaces + }) + + It("should handle pods with invalid network annotations", func() { + pods := []corev1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pod1", + Namespace: "default", + Annotations: map[string]string{ + "k8s.v1.cni.cncf.io/networks": "invalid-json", + }, + }, + }, + } + networks := []string{"default/net1"} + + result := getPodInterfacesMap(pods, networks) + Expect(result).To(HaveLen(1)) + + pod1Key := "pod1/default" + Expect(result).To(HaveKey(pod1Key)) + Expect(result[pod1Key]).To(BeEmpty()) // No valid interfaces + }) + + It("should handle empty networks list", func() { + pods := []corev1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pod1", + Namespace: "default", + Annotations: map[string]string{ + "k8s.v1.cni.cncf.io/networks": `[ + {"name": "net1"} + ]`, + "k8s.v1.cni.cncf.io/network-status": `[ + { + "name": "default/net1", + "interface": "eth1", + "ips": ["10.0.1.1"] + } + ]`, + }, + }, + }, + } + networks := []string{} // Empty networks + + result := getPodInterfacesMap(pods, networks) + Expect(result).To(HaveLen(1)) + + pod1Key := "pod1/default" + Expect(result).To(HaveKey(pod1Key)) + Expect(result[pod1Key]).To(BeEmpty()) // No matching networks + }) + }) + + Context("classifyAddresses", func() { + It("should return empty slices for empty input", func() { + interfacesPerPod := map[string][]Interface{} + network := "net1" + + ipv4, ipv6 := classifyAddresses(interfacesPerPod, network) + Expect(ipv4).To(BeEmpty()) + Expect(ipv6).To(BeEmpty()) + }) + + It("should classify IPv4 and IPv6 addresses correctly", func() { + interfacesPerPod := map[string][]Interface{ + "pod1/default": { + { + Name: "eth1", + Network: "default/net1", + IPs: []string{"10.0.1.1", "192.168.1.1", "2001:db8::1", "::1"}, + }, + }, + "pod2/default": { + { + Name: "eth1", + Network: "default/net1", + IPs: []string{"172.16.1.1", "2001:db8::2"}, + }, + }, + } + network := "default/net1" + + ipv4, ipv6 := classifyAddresses(interfacesPerPod, network) + + Expect(ipv4).To(HaveLen(3)) + Expect(ipv4).To(ContainElements("10.0.1.1", "192.168.1.1", "172.16.1.1")) + + Expect(ipv6).To(HaveLen(3)) // ::1, 2001:db8::1, 2001:db8::2 + Expect(ipv6).To(ContainElements("2001:db8::1", "::1", "2001:db8::2")) + }) + + It("should handle IPv4-mapped IPv6 addresses as IPv4", func() { + interfacesPerPod := map[string][]Interface{ + "pod1/default": { + { + Name: "eth1", + Network: "default/net1", + IPs: []string{"::ffff:192.168.1.1", "::ffff:10.0.0.1"}, + }, + }, + } + network := "default/net1" + + ipv4, ipv6 := classifyAddresses(interfacesPerPod, network) + + // IPv4-mapped IPv6 addresses should be classified as IPv4 + Expect(ipv4).To(HaveLen(2)) + Expect(ipv4).To(ContainElements("::ffff:192.168.1.1", "::ffff:10.0.0.1")) + Expect(ipv6).To(BeEmpty()) + }) + + It("should filter by network name", func() { + interfacesPerPod := map[string][]Interface{ + "pod1/default": { + { + Name: "eth1", + Network: "default/net1", + IPs: []string{"10.0.1.1", "2001:db8::1"}, + }, + { + Name: "eth2", + Network: "default/net2", + IPs: []string{"10.0.2.1", "2001:db8::2"}, + }, + }, + } + network := "default/net1" // Only net1 + + ipv4, ipv6 := classifyAddresses(interfacesPerPod, network) + + Expect(ipv4).To(HaveLen(1)) + Expect(ipv4).To(ContainElement("10.0.1.1")) + Expect(ipv4).NotTo(ContainElement("10.0.2.1")) // From net2, should be filtered out + + Expect(ipv6).To(HaveLen(1)) + Expect(ipv6).To(ContainElement("2001:db8::1")) + Expect(ipv6).NotTo(ContainElement("2001:db8::2")) // From net2, should be filtered out + }) + + It("should skip invalid IP addresses", func() { + interfacesPerPod := map[string][]Interface{ + "pod1/default": { + { + Name: "eth1", + Network: "default/net1", + IPs: []string{"10.0.1.1", "invalid-ip", "2001:db8::1", "not.an.ip", "192.168.1.1"}, + }, + }, + } + network := "default/net1" + + ipv4, ipv6 := classifyAddresses(interfacesPerPod, network) + + // Should only include valid IPs + Expect(ipv4).To(HaveLen(2)) + Expect(ipv4).To(ContainElements("10.0.1.1", "192.168.1.1")) + Expect(ipv4).NotTo(ContainElements("invalid-ip", "not.an.ip")) + + Expect(ipv6).To(HaveLen(1)) + Expect(ipv6).To(ContainElement("2001:db8::1")) + }) + + It("should handle mixed valid and invalid addresses across multiple pods", func() { + interfacesPerPod := map[string][]Interface{ + "pod1/default": { + { + Name: "eth1", + Network: "default/net1", + IPs: []string{"10.0.1.1", "invalid", "2001:db8::1"}, + }, + }, + "pod2/kube-system": { + { + Name: "eth1", + Network: "default/net1", + IPs: []string{"bad-ip", "192.168.1.1", "2001:db8::2", ""}, + }, + }, + } + network := "default/net1" + + ipv4, ipv6 := classifyAddresses(interfacesPerPod, network) + + Expect(ipv4).To(HaveLen(2)) + Expect(ipv4).To(ContainElements("10.0.1.1", "192.168.1.1")) + + Expect(ipv6).To(HaveLen(2)) + Expect(ipv6).To(ContainElements("2001:db8::1", "2001:db8::2")) + }) + + It("should handle empty IPs slice", func() { + interfacesPerPod := map[string][]Interface{ + "pod1/default": { + { + Name: "eth1", + Network: "default/net1", + IPs: []string{}, // Empty IPs + }, + }, + } + network := "default/net1" + + ipv4, ipv6 := classifyAddresses(interfacesPerPod, network) + Expect(ipv4).To(BeEmpty()) + Expect(ipv6).To(BeEmpty()) + }) + + It("should handle no matching network", func() { + interfacesPerPod := map[string][]Interface{ + "pod1/default": { + { + Name: "eth1", + Network: "default/net1", + IPs: []string{"10.0.1.1", "2001:db8::1"}, + }, + }, + } + network := "default/net2" // Different network + + ipv4, ipv6 := classifyAddresses(interfacesPerPod, network) + Expect(ipv4).To(BeEmpty()) + Expect(ipv6).To(BeEmpty()) + }) + + It("should handle various IPv6 formats", func() { + interfacesPerPod := map[string][]Interface{ + "pod1/default": { + { + Name: "eth1", + Network: "default/net1", + IPs: []string{ + "2001:db8::1", // Standard IPv6 + "2001:0db8:0000:0000:0000:0000:0000:0001", // Full IPv6 + "::1", // Loopback IPv6 + "fe80::1", // Link-local IPv6 + "::ffff:192.168.1.1", // IPv4-mapped IPv6 (should be IPv4) + "2001:db8:85a3::8a2e:370:7334", // Compressed IPv6 + }, + }, + }, + } + network := "default/net1" + + ipv4, ipv6 := classifyAddresses(interfacesPerPod, network) + + // IPv4-mapped IPv6 should be classified as IPv4 + Expect(ipv4).To(HaveLen(1)) + Expect(ipv4).To(ContainElement("::ffff:192.168.1.1")) + + // All other IPv6 addresses + Expect(ipv6).To(HaveLen(5)) + Expect(ipv6).To(ContainElements( + "2001:db8::1", + "2001:0db8:0000:0000:0000:0000:0000:0001", + "::1", + "fe80::1", + "2001:db8:85a3::8a2e:370:7334", + )) + }) + + It("should handle multiple interfaces per pod with same network", func() { + interfacesPerPod := map[string][]Interface{ + "pod1/default": { + { + Name: "eth1", + Network: "default/net1", + IPs: []string{"10.0.1.1", "2001:db8::1"}, + }, + { + Name: "eth2", + Network: "default/net1", // Same network, different interface + IPs: []string{"10.0.1.2", "2001:db8::2"}, + }, + }, + } + network := "default/net1" + + ipv4, ipv6 := classifyAddresses(interfacesPerPod, network) + + Expect(ipv4).To(HaveLen(2)) + Expect(ipv4).To(ContainElements("10.0.1.1", "10.0.1.2")) + + Expect(ipv6).To(HaveLen(2)) + Expect(ipv6).To(ContainElements("2001:db8::1", "2001:db8::2")) + }) + }) + + Context("createAndPopulateIPSet", func() { + var ( + nft knftables.Interface + ctx context.Context + logger logr.Logger + ) + + BeforeEach(func() { + nft = knftables.NewFake(knftables.InetFamily, tableName) + ctx = context.Background() + logger = logr.Discard() + + // Create the basic structure first + err := ensureBasicStructure(ctx, nft, nil, logger) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should create and populate IP set with addresses", func() { + tx := nft.NewTransaction() + setName := "test-set" + setType := "ipv4_addr" + setComment := "Test IP Set" + addresses := []string{"10.0.1.1", "10.0.1.2", "192.168.1.1"} + + createAndPopulateIPSet(tx, setName, setType, setComment, addresses, false) + + // Run transaction to generate rules + err := nft.Run(context.Background(), tx) + Expect(err).NotTo(HaveOccurred()) + + // Get all generated rules using Dump() + dump := nft.(*knftables.Fake).Dump() + dumpLines := strings.Split(dump, "\n") + + // Expected rules: basic structure + set creation + elements + expectedNewRules := []string{ + fmt.Sprintf("add set inet %s %s { type %s ; comment \"%s\" ; }", tableName, setName, setType, setComment), + fmt.Sprintf("add element inet %s %s { 10.0.1.1 }", tableName, setName), + fmt.Sprintf("add element inet %s %s { 10.0.1.2 }", tableName, setName), + fmt.Sprintf("add element inet %s %s { 192.168.1.1 }", tableName, setName), + } + + // Verify that our new rules exist in the dump + for _, expectedRule := range expectedNewRules { + found := false + for _, actualRule := range dumpLines { + if actualRule == expectedRule { + found = true + break + } + } + Expect(found).To(BeTrue(), "Expected rule not found: %s\nActual rules: %v", expectedRule, dumpLines) + } + + // Verify the set was created with the correct type and comment + setFound := false + for _, line := range dumpLines { + if strings.Contains(line, fmt.Sprintf("add set inet %s %s", tableName, setName)) { + Expect(line).To(ContainSubstring(setType)) + Expect(line).To(ContainSubstring(setComment)) + setFound = true + break + } + } + Expect(setFound).To(BeTrue(), "Set creation rule not found") + + // Verify all elements were added + elementCount := 0 + for _, line := range dumpLines { + if strings.Contains(line, fmt.Sprintf("add element inet %s %s", tableName, setName)) { + elementCount++ + } + } + Expect(elementCount).To(Equal(len(addresses)), "Expected %d elements, found %d", len(addresses), elementCount) + }) + + It("should create empty IP set when no addresses provided", func() { + tx := nft.NewTransaction() + setName := "empty-set" + setType := "ipv6_addr" + setComment := "Empty IP Set" + addresses := []string{} // Empty addresses + + createAndPopulateIPSet(tx, setName, setType, setComment, addresses, false) + + // Run transaction to generate rules + err := nft.Run(context.Background(), tx) + Expect(err).NotTo(HaveOccurred()) + + // Get all generated rules using Dump() + dump := nft.(*knftables.Fake).Dump() + dumpLines := strings.Split(dump, "\n") + + // Verify the set was created + setFound := false + for _, line := range dumpLines { + if strings.Contains(line, fmt.Sprintf("add set inet %s %s", tableName, setName)) { + Expect(line).To(ContainSubstring(setType)) + Expect(line).To(ContainSubstring(setComment)) + setFound = true + break + } + } + Expect(setFound).To(BeTrue(), "Set creation rule not found") + + // Verify no elements were added (should be 0) + elementCount := 0 + for _, line := range dumpLines { + if strings.Contains(line, fmt.Sprintf("add element inet %s %s", tableName, setName)) { + elementCount++ + } + } + Expect(elementCount).To(Equal(0), "Expected 0 elements for empty set, found %d", elementCount) + }) + + It("should handle different set types and single address", func() { + tx := nft.NewTransaction() + setName := "single-addr-set" + setType := "inet_service" + setComment := "Single Address Set" + addresses := []string{"80"} + + createAndPopulateIPSet(tx, setName, setType, setComment, addresses, false) + + // Run transaction to generate rules + err := nft.Run(context.Background(), tx) + Expect(err).NotTo(HaveOccurred()) + + // Get all generated rules using Dump() + dump := nft.(*knftables.Fake).Dump() + dumpLines := strings.Split(dump, "\n") + + // Verify the set was created + setFound := false + for _, line := range dumpLines { + if strings.Contains(line, fmt.Sprintf("add set inet %s %s", tableName, setName)) { + Expect(line).To(ContainSubstring(setType)) + Expect(line).To(ContainSubstring(setComment)) + setFound = true + break + } + } + Expect(setFound).To(BeTrue(), "Set creation rule not found") + + // Verify the element was added + elementFound := false + for _, line := range dumpLines { + if strings.Contains(line, fmt.Sprintf("add element inet %s %s { 80 }", tableName, setName)) { + elementFound = true + break + } + } + Expect(elementFound).To(BeTrue(), "Expected element '80' not found") + }) + + It("should handle IPv6 addresses", func() { + tx := nft.NewTransaction() + setName := "ipv6-set" + setType := "ipv6_addr" + setComment := "IPv6 Address Set" + addresses := []string{"2001:db8::1", "::1", "fe80::1"} + + createAndPopulateIPSet(tx, setName, setType, setComment, addresses, false) + + // Run transaction to generate rules + err := nft.Run(context.Background(), tx) + Expect(err).NotTo(HaveOccurred()) + + // Get all generated rules using Dump() + dump := nft.(*knftables.Fake).Dump() + dumpLines := strings.Split(dump, "\n") + + // Verify the set was created + setFound := false + for _, line := range dumpLines { + if strings.Contains(line, fmt.Sprintf("add set inet %s %s", tableName, setName)) { + Expect(line).To(ContainSubstring(setType)) + Expect(line).To(ContainSubstring(setComment)) + setFound = true + break + } + } + Expect(setFound).To(BeTrue(), "Set creation rule not found") + + // Verify all IPv6 elements were added + expectedElements := []string{"2001:db8::1", "::1", "fe80::1"} + for _, expectedElement := range expectedElements { + elementFound := false + for _, line := range dumpLines { + if strings.Contains(line, fmt.Sprintf("add element inet %s %s { %s }", tableName, setName, expectedElement)) { + elementFound = true + break + } + } + Expect(elementFound).To(BeTrue(), "Expected IPv6 element '%s' not found", expectedElement) + } + }) + + It("should handle special characters in set name and comment", func() { + tx := nft.NewTransaction() + setName := "special-name_123" + setType := "ipv4_addr" + setComment := "Set with special chars & symbols" + addresses := []string{"172.16.0.1"} + + createAndPopulateIPSet(tx, setName, setType, setComment, addresses, false) + + // Run transaction to generate rules + err := nft.Run(context.Background(), tx) + Expect(err).NotTo(HaveOccurred()) + + // Get all generated rules using Dump() + dump := nft.(*knftables.Fake).Dump() + dumpLines := strings.Split(dump, "\n") + + // Verify the set was created with special characters + setFound := false + for _, line := range dumpLines { + if strings.Contains(line, fmt.Sprintf("add set inet %s %s", tableName, setName)) { + Expect(line).To(ContainSubstring(setType)) + Expect(line).To(ContainSubstring(setComment)) + setFound = true + break + } + } + Expect(setFound).To(BeTrue(), "Set creation rule not found") + + // Verify the element was added + elementFound := false + for _, line := range dumpLines { + if strings.Contains(line, fmt.Sprintf("add element inet %s %s { 172.16.0.1 }", tableName, setName)) { + elementFound = true + break + } + } + Expect(elementFound).To(BeTrue(), "Expected element '172.16.0.1' not found") + }) + + It("should create set with interval flag when needsIntervalFlag is true", func() { + tx := nft.NewTransaction() + setName := "interval-set" + setType := "ipv4_addr" + setComment := "Set with Interval Flag" + addresses := []string{"10.0.0.0/24", "192.168.1.0/24"} + + createAndPopulateIPSet(tx, setName, setType, setComment, addresses, true) + + // Run transaction to generate rules + err := nft.Run(context.Background(), tx) + Expect(err).NotTo(HaveOccurred()) + + // Get all generated rules using Dump() + dump := nft.(*knftables.Fake).Dump() + dumpLines := strings.Split(dump, "\n") + + // Verify the set was created with interval flag + setFound := false + for _, line := range dumpLines { + if strings.Contains(line, fmt.Sprintf("add set inet %s %s", tableName, setName)) { + Expect(line).To(ContainSubstring(setType)) + Expect(line).To(ContainSubstring(setComment)) + Expect(line).To(ContainSubstring("flags interval")) + setFound = true + break + } + } + Expect(setFound).To(BeTrue(), "Set creation rule with interval flag not found") + + // Verify all CIDR elements were added + expectedElements := []string{"10.0.0.0/24", "192.168.1.0/24"} + for _, expectedElement := range expectedElements { + elementFound := false + for _, line := range dumpLines { + if strings.Contains(line, fmt.Sprintf("add element inet %s %s { %s }", tableName, setName, expectedElement)) { + elementFound = true + break + } + } + Expect(elementFound).To(BeTrue(), "Expected CIDR element '%s' not found", expectedElement) + } + }) + + It("should create set without interval flag when needsIntervalFlag is false", func() { + tx := nft.NewTransaction() + setName := "no-interval-set" + setType := "ipv4_addr" + setComment := "Set without Interval Flag" + addresses := []string{"10.0.1.1", "192.168.1.1"} + + createAndPopulateIPSet(tx, setName, setType, setComment, addresses, false) + + // Run transaction to generate rules + err := nft.Run(context.Background(), tx) + Expect(err).NotTo(HaveOccurred()) + + // Get all generated rules using Dump() + dump := nft.(*knftables.Fake).Dump() + dumpLines := strings.Split(dump, "\n") + + // Verify the set was created without interval flag + setFound := false + for _, line := range dumpLines { + if strings.Contains(line, fmt.Sprintf("add set inet %s %s", tableName, setName)) { + Expect(line).To(ContainSubstring(setType)) + Expect(line).To(ContainSubstring(setComment)) + Expect(line).NotTo(ContainSubstring("flags interval")) + setFound = true + break + } + } + Expect(setFound).To(BeTrue(), "Set creation rule not found") + + // Verify all IP elements were added + expectedElements := []string{"10.0.1.1", "192.168.1.1"} + for _, expectedElement := range expectedElements { + elementFound := false + for _, line := range dumpLines { + if strings.Contains(line, fmt.Sprintf("add element inet %s %s { %s }", tableName, setName, expectedElement)) { + elementFound = true + break + } + } + Expect(elementFound).To(BeTrue(), "Expected IP element '%s' not found", expectedElement) + } + }) + }) + + Context("checkPolicyTypes", func() { + It("should return (true, false) when no policy types are specified and no egress rules", func() { + policy := &datastore.Policy{ + Spec: multiv1beta1.MultiNetworkPolicySpec{ + PolicyTypes: []multiv1beta1.MultiPolicyType{}, // Empty policy types + Egress: []multiv1beta1.MultiNetworkPolicyEgressRule{}, // No egress rules + }, + } + + hasIngress, hasEgress := checkPolicyTypes(policy) + Expect(hasIngress).To(BeTrue(), "Ingress should be enabled by default when no policy types specified") + Expect(hasEgress).To(BeFalse(), "Egress should be disabled when no policy types specified and no egress rules") + }) + + It("should return (true, true) when no policy types are specified but egress rules exist", func() { + policy := &datastore.Policy{ + Spec: multiv1beta1.MultiNetworkPolicySpec{ + PolicyTypes: []multiv1beta1.MultiPolicyType{}, // Empty policy types + Egress: []multiv1beta1.MultiNetworkPolicyEgressRule{ + {}, // At least one egress rule + }, + }, + } + + hasIngress, hasEgress := checkPolicyTypes(policy) + Expect(hasIngress).To(BeTrue(), "Ingress should be enabled by default when no policy types specified") + Expect(hasEgress).To(BeTrue(), "Egress should be enabled when no policy types specified but egress rules exist") + }) + + It("should return (true, false) when only ingress policy type is specified", func() { + policy := &datastore.Policy{ + Spec: multiv1beta1.MultiNetworkPolicySpec{ + PolicyTypes: []multiv1beta1.MultiPolicyType{multiv1beta1.PolicyTypeIngress}, + }, + } + + hasIngress, hasEgress := checkPolicyTypes(policy) + Expect(hasIngress).To(BeTrue(), "Ingress should be enabled when PolicyTypeIngress is specified") + Expect(hasEgress).To(BeFalse(), "Egress should be disabled when only PolicyTypeIngress is specified") + }) + + It("should return (false, true) when only egress policy type is specified", func() { + policy := &datastore.Policy{ + Spec: multiv1beta1.MultiNetworkPolicySpec{ + PolicyTypes: []multiv1beta1.MultiPolicyType{multiv1beta1.PolicyTypeEgress}, + }, + } + + hasIngress, hasEgress := checkPolicyTypes(policy) + Expect(hasIngress).To(BeFalse(), "Ingress should be disabled when only PolicyTypeEgress is specified") + Expect(hasEgress).To(BeTrue(), "Egress should be enabled when PolicyTypeEgress is specified") + }) + + It("should return (true, true) when both ingress and egress policy types are specified", func() { + policy := &datastore.Policy{ + Spec: multiv1beta1.MultiNetworkPolicySpec{ + PolicyTypes: []multiv1beta1.MultiPolicyType{ + multiv1beta1.PolicyTypeIngress, + multiv1beta1.PolicyTypeEgress, + }, + }, + } + + hasIngress, hasEgress := checkPolicyTypes(policy) + Expect(hasIngress).To(BeTrue(), "Ingress should be enabled when PolicyTypeIngress is specified") + Expect(hasEgress).To(BeTrue(), "Egress should be enabled when PolicyTypeEgress is specified") + }) + + It("should handle multiple egress rules correctly when no policy types specified", func() { + policy := &datastore.Policy{ + Spec: multiv1beta1.MultiNetworkPolicySpec{ + PolicyTypes: []multiv1beta1.MultiPolicyType{}, // Empty policy types + Egress: []multiv1beta1.MultiNetworkPolicyEgressRule{ + {}, // First egress rule + {}, // Second egress rule + }, + }, + } + + hasIngress, hasEgress := checkPolicyTypes(policy) + Expect(hasIngress).To(BeTrue(), "Ingress should be enabled by default when no policy types specified") + Expect(hasEgress).To(BeTrue(), "Egress should be enabled when multiple egress rules exist") + }) + }) + + Context("createEgressRules", func() { + var ( + ctx context.Context + nft knftables.Interface + logger logr.Logger + ) + + BeforeEach(func() { + ctx = context.Background() + nft = knftables.NewFake(knftables.InetFamily, tableName) + logger = logr.Discard() + }) + + It("should create no rules for deny-all policy (empty egress)", func() { + // First ensure basic structure exists + err := ensureBasicStructure(ctx, nft, nil, logger) + Expect(err).NotTo(HaveOccurred()) + + // Create a policy with no egress rules (deny all) + policy := &datastore.Policy{ + Name: "deny-all-policy", + Namespace: "test-ns", + Spec: multiv1beta1.MultiNetworkPolicySpec{ + PolicyTypes: []multiv1beta1.MultiPolicyType{multiv1beta1.PolicyTypeEgress}, + Egress: []multiv1beta1.MultiNetworkPolicyEgressRule{}, // Empty = deny all + }, + } + + // Setup interfaces and transaction + matchedInterfaces := []Interface{ + {Name: "eth1", Network: "default/net1", IPs: []string{"10.0.0.1"}}, + {Name: "eth2", Network: "default/net2", IPs: []string{"10.0.0.2"}}, + } + tx := nft.NewTransaction() + hashName := "abc123" + + // Create a minimal NFTables instance for testing + nftables := &NFTables{ + Client: nil, // We don't need the client for this test since no API calls are made + } + + // Call createEgressRules + err = nftables.createEgressRules(ctx, tx, matchedInterfaces, policy, hashName, logger) + Expect(err).NotTo(HaveOccurred()) + + // Run transaction to generate rules + err = nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + // Get all generated rules using Dump() + dump := nft.(*knftables.Fake).Dump() + + // Split the dump into lines for easier verification + dumpLines := strings.Split(dump, "\n") + + // Expected rules that should be generated (based on actual output) + expectedRules := []string{ + "add table inet multi_networkpolicy { comment \"MultiNetworkPolicy\" ; }", + "add chain inet multi_networkpolicy input { type filter hook input priority 0 ; comment \"Input Dispatcher\" ; }", + "add chain inet multi_networkpolicy output { type filter hook output priority 0 ; comment \"Output Dispatcher\" ; }", + "add chain inet multi_networkpolicy ingress { comment \"Ingress Policies\" ; }", + "add chain inet multi_networkpolicy egress { comment \"Egress Policies\" ; }", + "add chain inet multi_networkpolicy common-ingress { comment \"Common Policies\" ; }", + "add chain inet multi_networkpolicy common-egress { comment \"Common Policies\" ; }", + "add rule inet multi_networkpolicy egress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy egress jump common-egress comment \"Jump to common\"", + "add rule inet multi_networkpolicy egress drop comment \"Drop rule\"", + "add rule inet multi_networkpolicy ingress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy ingress jump common-ingress comment \"Jump to common\"", + "add rule inet multi_networkpolicy ingress drop comment \"Drop rule\"", + "", // Empty line at the end + } + + // Verify exact number of expected rules + Expect(dumpLines).To(HaveLen(len(expectedRules)), "Expected exactly %d rules, but got %d. Rules: %v", len(expectedRules), len(dumpLines), dumpLines) + + // Verify each expected rule exists completely + for _, expectedRule := range expectedRules { + found := false + for _, actualRule := range dumpLines { + if strings.Contains(actualRule, expectedRule) { + found = true + break + } + } + Expect(found).To(BeTrue(), "Expected rule not found: %s\nActual rules: %v", expectedRule, dumpLines) + } + }) + + It("should create accept-all rules for policy with empty egress entry", func() { + // First ensure basic structure exists + err := ensureBasicStructure(ctx, nft, nil, logger) + Expect(err).NotTo(HaveOccurred()) + + // Create a policy with an empty egress entry (accept to all destinations) + policy := &datastore.Policy{ + Name: "accept-all-policy", + Namespace: "test-ns", + Spec: multiv1beta1.MultiNetworkPolicySpec{ + PolicyTypes: []multiv1beta1.MultiPolicyType{multiv1beta1.PolicyTypeEgress}, + Egress: []multiv1beta1.MultiNetworkPolicyEgressRule{ + {}, // Empty entry = accept to all destinations + }, + }, + } + + // Setup interfaces and transaction + matchedInterfaces := []Interface{ + {Name: "eth1", Network: "default/net1", IPs: []string{"10.0.0.1"}}, + {Name: "eth2", Network: "default/net2", IPs: []string{"10.0.0.2"}}, + } + tx := nft.NewTransaction() + hashName := "def456" + + // Create policy chain first (as done in enforcePolicy) + npChainName := fmt.Sprintf("cnp-%s", hashName) + err = createPolicyChain(ctx, nft, tx, npChainName, "egress", policy.Namespace, policy.Name, logger) + Expect(err).NotTo(HaveOccurred()) + + // Create NFTables instance + nftables := &NFTables{ + Client: nil, + } + + // Call createEgressRules + err = nftables.createEgressRules(ctx, tx, matchedInterfaces, policy, hashName, logger) + Expect(err).NotTo(HaveOccurred()) + + // Run transaction to generate rules + err = nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + // Get all generated rules using Dump() + dump := nft.(*knftables.Fake).Dump() + + // Split the dump into lines for easier verification + dumpLines := strings.Split(dump, "\n") + + // Expected rules that should be generated (based on actual output) + expectedRules := []string{ + "add table inet multi_networkpolicy { comment \"MultiNetworkPolicy\" ; }", + "add chain inet multi_networkpolicy input { type filter hook input priority 0 ; comment \"Input Dispatcher\" ; }", + "add chain inet multi_networkpolicy output { type filter hook output priority 0 ; comment \"Output Dispatcher\" ; }", + "add chain inet multi_networkpolicy ingress { comment \"Ingress Policies\" ; }", + "add chain inet multi_networkpolicy egress { comment \"Egress Policies\" ; }", + "add chain inet multi_networkpolicy common-ingress { comment \"Common Policies\" ; }", + "add chain inet multi_networkpolicy common-egress { comment \"Common Policies\" ; }", + "add rule inet multi_networkpolicy egress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy egress jump common-egress comment \"Jump to common\"", + "add rule inet multi_networkpolicy egress drop comment \"Drop rule\"", + "add rule inet multi_networkpolicy ingress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy ingress jump common-ingress comment \"Jump to common\"", + "add rule inet multi_networkpolicy ingress drop comment \"Drop rule\"", + + // New commands to check + "add chain inet multi_networkpolicy cnp-def456 { comment \"MultiNetworkPolicy test-ns/accept-all-policy\" ; }", + "add rule inet multi_networkpolicy ingress jump common-ingress comment \"Jump to common\"", + "add rule inet multi_networkpolicy cnp-def456 oifname eth1 accept", + "add rule inet multi_networkpolicy cnp-def456 oifname eth2 accept", + "", // Empty line at the end + } + + // Verify exact number of expected rules + Expect(dumpLines).To(HaveLen(len(expectedRules)), "Expected exactly %d rules, but got %d. Rules: %v", len(expectedRules), len(dumpLines), dumpLines) + + // Verify each expected rule exists completely + for _, expectedRule := range expectedRules { + found := false + for _, actualRule := range dumpLines { + if strings.Contains(actualRule, expectedRule) { + found = true + break + } + } + Expect(found).To(BeTrue(), "Expected rule not found: %s\nActual rules: %v", expectedRule, dumpLines) + } + }) + + It("should create port-restricted rules for multiple egress entries with nil To", func() { + // First ensure basic structure exists + err := ensureBasicStructure(ctx, nft, nil, logger) + Expect(err).NotTo(HaveOccurred()) + + // Create a policy with multiple egress entries with nil To (accept to all destinations) but specific ports + tcp := corev1.ProtocolTCP + udp := corev1.ProtocolUDP + policy := &datastore.Policy{ + Name: "port-restricted-policy", + Namespace: "prod-ns", + Spec: multiv1beta1.MultiNetworkPolicySpec{ + PolicyTypes: []multiv1beta1.MultiPolicyType{multiv1beta1.PolicyTypeEgress}, + Egress: []multiv1beta1.MultiNetworkPolicyEgressRule{ + { + To: []multiv1beta1.MultiNetworkPolicyPeer{}, // Empty To = accept to all destinations + Ports: []multiv1beta1.MultiNetworkPolicyPort{ + { + Protocol: &tcp, + Port: &intstr.IntOrString{Type: intstr.Int, IntVal: 80}, + }, + { + Protocol: &tcp, + Port: &intstr.IntOrString{Type: intstr.Int, IntVal: 443}, + }, + }, + }, + { + To: []multiv1beta1.MultiNetworkPolicyPeer{}, // Empty To = accept to all destinations + Ports: []multiv1beta1.MultiNetworkPolicyPort{ + { + Protocol: &udp, + Port: &intstr.IntOrString{Type: intstr.Int, IntVal: 53}, + }, + }, + }, + { + To: []multiv1beta1.MultiNetworkPolicyPeer{}, // Empty To = accept to all destinations + Ports: []multiv1beta1.MultiNetworkPolicyPort{ + { + Protocol: &tcp, + Port: &intstr.IntOrString{Type: intstr.String, StrVal: "SSH"}, + }, + }, + }, + }, + }, + } + + // Setup interfaces and transaction + matchedInterfaces := []Interface{ + {Name: "eth1", Network: "default/net1", IPs: []string{"10.0.0.1"}}, + } + tx := nft.NewTransaction() + hashName := "ghi789" + + // Create policy chain first (as done in enforcePolicy) + npChainName := fmt.Sprintf("cnp-%s", hashName) + err = createPolicyChain(ctx, nft, tx, npChainName, "egress", policy.Namespace, policy.Name, logger) + Expect(err).NotTo(HaveOccurred()) + + // Create NFTables instance + nftables := &NFTables{ + Client: nil, + } + + // Call createEgressRules + err = nftables.createEgressRules(ctx, tx, matchedInterfaces, policy, hashName, logger) + Expect(err).NotTo(HaveOccurred()) + + // Run transaction to generate rules + err = nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + // Get all generated rules using Dump() + dump := nft.(*knftables.Fake).Dump() + + // Split the dump into lines for easier verification + dumpLines := strings.Split(dump, "\n") + + // Expected rules that should be generated (based on actual output) + expectedRules := []string{ + "add table inet multi_networkpolicy { comment \"MultiNetworkPolicy\" ; }", + "add chain inet multi_networkpolicy input { type filter hook input priority 0 ; comment \"Input Dispatcher\" ; }", + "add chain inet multi_networkpolicy output { type filter hook output priority 0 ; comment \"Output Dispatcher\" ; }", + "add chain inet multi_networkpolicy ingress { comment \"Ingress Policies\" ; }", + "add chain inet multi_networkpolicy egress { comment \"Egress Policies\" ; }", + "add chain inet multi_networkpolicy common-ingress { comment \"Common Policies\" ; }", + "add chain inet multi_networkpolicy common-egress { comment \"Common Policies\" ; }", + "add rule inet multi_networkpolicy egress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy egress jump common-egress comment \"Jump to common\"", + "add rule inet multi_networkpolicy egress drop comment \"Drop rule\"", + "add rule inet multi_networkpolicy ingress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy ingress jump common-ingress comment \"Jump to common\"", + "add rule inet multi_networkpolicy ingress drop comment \"Drop rule\"", + + // New commands to check + "add chain inet multi_networkpolicy cnp-ghi789 { comment \"MultiNetworkPolicy prod-ns/port-restricted-policy\" ; }", + "add rule inet multi_networkpolicy ingress jump common-ingress comment \"Jump to common\"", + "add rule inet multi_networkpolicy cnp-ghi789 oifname eth1 meta l4proto tcp th dport { 80,443 } accept", + "add rule inet multi_networkpolicy cnp-ghi789 oifname eth1 meta l4proto udp th dport { 53 } accept", + "add rule inet multi_networkpolicy cnp-ghi789 oifname eth1 meta l4proto tcp th dport { ssh } accept", + "", // Empty line at the end + } + + // Verify exact number of expected rules + Expect(dumpLines).To(HaveLen(len(expectedRules)), "Expected exactly %d rules, but got %d. Rules: %v", len(expectedRules), len(dumpLines), dumpLines) + + // Verify each expected rule exists completely + for _, expectedRule := range expectedRules { + found := false + for _, actualRule := range dumpLines { + if strings.Contains(actualRule, expectedRule) { + found = true + break + } + } + Expect(found).To(BeTrue(), "Expected rule not found: %s\nActual rules: %v", expectedRule, dumpLines) + } + }) + + // Comprehensive tests for full coverage + It("should create rules for egress with IPv4-only pod selector", func() { + err := ensureBasicStructure(ctx, nft, nil, logger) + Expect(err).NotTo(HaveOccurred()) + + // Create fake client with IPv4-only pod + scheme := runtime.NewScheme() + _ = corev1.AddToScheme(scheme) + + pod1 := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod1", + Namespace: "default", + Labels: map[string]string{"app": "web"}, + Annotations: map[string]string{ + "k8s.v1.cni.cncf.io/networks": `[{"name": "net1"}]`, + "k8s.v1.cni.cncf.io/network-status": `[{ + "name": "default/net1", + "interface": "eth1", + "ips": ["10.0.1.1"] + }]`, + }, + }, + Status: corev1.PodStatus{Phase: corev1.PodRunning}, + Spec: corev1.PodSpec{HostNetwork: false}, + } + + fakeClient := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(pod1). + WithIndex(&corev1.Pod{}, PodStatusIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + return []string{string(pod.Status.Phase)} + }). + WithIndex(&corev1.Pod{}, PodHostNetworkIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + return []string{fmt.Sprintf("%t", pod.Spec.HostNetwork)} + }). + WithIndex(&corev1.Pod{}, PodHasNetworkAnnotationIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + _, hasAnnotation := pod.Annotations["k8s.v1.cni.cncf.io/networks"] + return []string{fmt.Sprintf("%t", hasAnnotation)} + }). + Build() + + policy := &datastore.Policy{ + Name: "ipv4-pod-policy", + Namespace: "default", + Networks: []string{"default/net1"}, + Spec: multiv1beta1.MultiNetworkPolicySpec{ + Egress: []multiv1beta1.MultiNetworkPolicyEgressRule{ + { + To: []multiv1beta1.MultiNetworkPolicyPeer{ + { + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "web"}, + }, + }, + }, + }, + }, + }, + } + + matchedInterfaces := []Interface{ + {Name: "eth1", Network: "default/net1", IPs: []string{"10.0.1.1"}}, + } + + tx := nft.NewTransaction() + hashName := "ipv4test" + err = createPolicyChain(ctx, nft, tx, fmt.Sprintf("cnp-%s", hashName), "egress", policy.Namespace, policy.Name, logger) + Expect(err).NotTo(HaveOccurred()) + + nftablesInstance := &NFTables{Client: fakeClient} + err = nftablesInstance.createEgressRules(ctx, tx, matchedInterfaces, policy, hashName, logger) + Expect(err).NotTo(HaveOccurred()) + + err = nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + dump := nft.(*knftables.Fake).Dump() + + // Split the dump into lines for easier verification + dumpLines := strings.Split(dump, "\n") + + // Expected rules that should be generated (based on actual output) + expectedRules := []string{ + "add table inet multi_networkpolicy { comment \"MultiNetworkPolicy\" ; }", + "add chain inet multi_networkpolicy input { type filter hook input priority 0 ; comment \"Input Dispatcher\" ; }", + "add chain inet multi_networkpolicy output { type filter hook output priority 0 ; comment \"Output Dispatcher\" ; }", + "add chain inet multi_networkpolicy ingress { comment \"Ingress Policies\" ; }", + "add chain inet multi_networkpolicy egress { comment \"Egress Policies\" ; }", + "add chain inet multi_networkpolicy common-ingress { comment \"Common Policies\" ; }", + "add chain inet multi_networkpolicy common-egress { comment \"Common Policies\" ; }", + "add rule inet multi_networkpolicy egress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy egress jump common-egress comment \"Jump to common\"", + "add rule inet multi_networkpolicy egress drop comment \"Drop rule\"", + "add rule inet multi_networkpolicy ingress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy ingress jump common-ingress comment \"Jump to common\"", + "add rule inet multi_networkpolicy ingress drop comment \"Drop rule\"", + + // New commands to check + "add chain inet multi_networkpolicy cnp-ipv4test { comment \"MultiNetworkPolicy default/ipv4-pod-policy\" ; }", + "add rule inet multi_networkpolicy egress jump cnp-ipv4test comment \"default/ipv4-pod-policy\"", + "add set inet multi_networkpolicy snp-ipv4test_egress_ipv4_eth1_0 { type ipv4_addr ; comment \"Addresses for default/ipv4-pod-policy\" ; }", + "add element inet multi_networkpolicy snp-ipv4test_egress_ipv4_eth1_0 { 10.0.1.1 }", + "add rule inet multi_networkpolicy cnp-ipv4test oifname eth1 ip daddr @snp-ipv4test_egress_ipv4_eth1_0 accept", + "", // Empty line at the end + } + + // Verify exact number of expected rules + Expect(dumpLines).To(HaveLen(len(expectedRules)), "Expected exactly %d rules, but got %d. Rules: %v", len(expectedRules), len(dumpLines), dumpLines) + + // Verify each expected rule exists completely + for _, expectedRule := range expectedRules { + found := false + for _, actualRule := range dumpLines { + if strings.Contains(actualRule, expectedRule) { + found = true + break + } + } + Expect(found).To(BeTrue(), "Expected rule not found: %s\nActual rules: %v", expectedRule, dumpLines) + } + }) + + It("should create rules for egress with IPv6-only pod selector", func() { + err := ensureBasicStructure(ctx, nft, nil, logger) + Expect(err).NotTo(HaveOccurred()) + + // Create fake client with IPv6-only pod + scheme := runtime.NewScheme() + _ = corev1.AddToScheme(scheme) + + pod1 := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod1", + Namespace: "default", + Labels: map[string]string{"app": "web"}, + Annotations: map[string]string{ + "k8s.v1.cni.cncf.io/networks": `[{"name": "net1"}]`, + "k8s.v1.cni.cncf.io/network-status": `[{ + "name": "default/net1", + "interface": "eth1", + "ips": ["2001:db8::1"] + }]`, + }, + }, + Status: corev1.PodStatus{Phase: corev1.PodRunning}, + Spec: corev1.PodSpec{HostNetwork: false}, + } + + fakeClient := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(pod1). + WithIndex(&corev1.Pod{}, PodStatusIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + return []string{string(pod.Status.Phase)} + }). + WithIndex(&corev1.Pod{}, PodHostNetworkIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + return []string{fmt.Sprintf("%t", pod.Spec.HostNetwork)} + }). + WithIndex(&corev1.Pod{}, PodHasNetworkAnnotationIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + _, hasAnnotation := pod.Annotations["k8s.v1.cni.cncf.io/networks"] + return []string{fmt.Sprintf("%t", hasAnnotation)} + }). + Build() + + policy := &datastore.Policy{ + Name: "ipv6-pod-policy", + Namespace: "default", + Networks: []string{"default/net1"}, + Spec: multiv1beta1.MultiNetworkPolicySpec{ + Egress: []multiv1beta1.MultiNetworkPolicyEgressRule{ + { + To: []multiv1beta1.MultiNetworkPolicyPeer{ + { + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "web"}, + }, + }, + }, + }, + }, + }, + } + + matchedInterfaces := []Interface{ + {Name: "eth1", Network: "default/net1", IPs: []string{"2001:db8::1"}}, + } + + tx := nft.NewTransaction() + hashName := "ipv6test" + err = createPolicyChain(ctx, nft, tx, fmt.Sprintf("cnp-%s", hashName), "egress", policy.Namespace, policy.Name, logger) + Expect(err).NotTo(HaveOccurred()) + + nftablesInstance := &NFTables{Client: fakeClient} + err = nftablesInstance.createEgressRules(ctx, tx, matchedInterfaces, policy, hashName, logger) + Expect(err).NotTo(HaveOccurred()) + + err = nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + dump := nft.(*knftables.Fake).Dump() + + // Split the dump into lines for easier verification + dumpLines := strings.Split(dump, "\n") + + // Expected rules that should be generated (based on actual output) + expectedRules := []string{ + "add table inet multi_networkpolicy { comment \"MultiNetworkPolicy\" ; }", + "add chain inet multi_networkpolicy input { type filter hook input priority 0 ; comment \"Input Dispatcher\" ; }", + "add chain inet multi_networkpolicy output { type filter hook output priority 0 ; comment \"Output Dispatcher\" ; }", + "add chain inet multi_networkpolicy ingress { comment \"Ingress Policies\" ; }", + "add chain inet multi_networkpolicy egress { comment \"Egress Policies\" ; }", + "add chain inet multi_networkpolicy common-ingress { comment \"Common Policies\" ; }", + "add chain inet multi_networkpolicy common-egress { comment \"Common Policies\" ; }", + "add rule inet multi_networkpolicy egress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy egress jump common-egress comment \"Jump to common\"", + "add rule inet multi_networkpolicy egress drop comment \"Drop rule\"", + "add rule inet multi_networkpolicy ingress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy ingress jump common-ingress comment \"Jump to common\"", + "add rule inet multi_networkpolicy ingress drop comment \"Drop rule\"", + + // New commands to check + "add chain inet multi_networkpolicy cnp-ipv6test { comment \"MultiNetworkPolicy default/ipv6-pod-policy\" ; }", + "add rule inet multi_networkpolicy egress jump cnp-ipv6test comment \"default/ipv6-pod-policy\"", + "add set inet multi_networkpolicy snp-ipv6test_egress_ipv6_eth1_0 { type ipv6_addr ; comment \"Addresses for default/ipv6-pod-policy\" ; }", + "add element inet multi_networkpolicy snp-ipv6test_egress_ipv6_eth1_0 { 2001:db8::1 }", + "add rule inet multi_networkpolicy cnp-ipv6test oifname eth1 ip6 daddr @snp-ipv6test_egress_ipv6_eth1_0 accept", + "", // Empty line at the end + } + + // Verify exact number of expected rules + Expect(dumpLines).To(HaveLen(len(expectedRules)), "Expected exactly %d rules, but got %d. Rules: %v", len(expectedRules), len(dumpLines), dumpLines) + + // Verify each expected rule exists completely + for _, expectedRule := range expectedRules { + found := false + for _, actualRule := range dumpLines { + if strings.Contains(actualRule, expectedRule) { + found = true + break + } + } + Expect(found).To(BeTrue(), "Expected rule not found: %s\nActual rules: %v", expectedRule, dumpLines) + } + }) + + It("should create rules for egress with dual-stack pod selector", func() { + err := ensureBasicStructure(ctx, nft, nil, logger) + Expect(err).NotTo(HaveOccurred()) + + // Create fake client with dual-stack pod + scheme := runtime.NewScheme() + _ = corev1.AddToScheme(scheme) + + pod1 := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod1", + Namespace: "default", + Labels: map[string]string{"app": "web"}, + Annotations: map[string]string{ + "k8s.v1.cni.cncf.io/networks": `[{"name": "net1"}]`, + "k8s.v1.cni.cncf.io/network-status": `[{ + "name": "default/net1", + "interface": "eth1", + "ips": ["10.0.1.2", "2001:db8::2"] + }]`, + }, + }, + Status: corev1.PodStatus{Phase: corev1.PodRunning}, + Spec: corev1.PodSpec{HostNetwork: false}, + } + + fakeClient := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(pod1). + WithIndex(&corev1.Pod{}, PodStatusIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + return []string{string(pod.Status.Phase)} + }). + WithIndex(&corev1.Pod{}, PodHostNetworkIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + return []string{fmt.Sprintf("%t", pod.Spec.HostNetwork)} + }). + WithIndex(&corev1.Pod{}, PodHasNetworkAnnotationIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + _, hasAnnotation := pod.Annotations["k8s.v1.cni.cncf.io/networks"] + return []string{fmt.Sprintf("%t", hasAnnotation)} + }). + Build() + + policy := &datastore.Policy{ + Name: "dual-stack-policy", + Namespace: "default", + Networks: []string{"default/net1"}, + Spec: multiv1beta1.MultiNetworkPolicySpec{ + Egress: []multiv1beta1.MultiNetworkPolicyEgressRule{ + { + To: []multiv1beta1.MultiNetworkPolicyPeer{ + { + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "web"}, + }, + }, + }, + }, + }, + }, + } + + matchedInterfaces := []Interface{ + {Name: "eth1", Network: "default/net1", IPs: []string{"10.0.1.2", "2001:db8::2"}}, + } + + tx := nft.NewTransaction() + hashName := "dualtest" + err = createPolicyChain(ctx, nft, tx, fmt.Sprintf("cnp-%s", hashName), "egress", policy.Namespace, policy.Name, logger) + Expect(err).NotTo(HaveOccurred()) + + nftablesInstance := &NFTables{Client: fakeClient} + err = nftablesInstance.createEgressRules(ctx, tx, matchedInterfaces, policy, hashName, logger) + Expect(err).NotTo(HaveOccurred()) + + err = nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + dump := nft.(*knftables.Fake).Dump() + + // Split the dump into lines for easier verification + dumpLines := strings.Split(dump, "\n") + + // Expected rules that should be generated (based on actual output) + expectedRules := []string{ + "add table inet multi_networkpolicy { comment \"MultiNetworkPolicy\" ; }", + "add chain inet multi_networkpolicy input { type filter hook input priority 0 ; comment \"Input Dispatcher\" ; }", + "add chain inet multi_networkpolicy output { type filter hook output priority 0 ; comment \"Output Dispatcher\" ; }", + "add chain inet multi_networkpolicy ingress { comment \"Ingress Policies\" ; }", + "add chain inet multi_networkpolicy egress { comment \"Egress Policies\" ; }", + "add chain inet multi_networkpolicy common-ingress { comment \"Common Policies\" ; }", + "add chain inet multi_networkpolicy common-egress { comment \"Common Policies\" ; }", + "add rule inet multi_networkpolicy egress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy egress jump common-egress comment \"Jump to common\"", + "add rule inet multi_networkpolicy egress drop comment \"Drop rule\"", + "add rule inet multi_networkpolicy ingress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy ingress jump common-ingress comment \"Jump to common\"", + "add rule inet multi_networkpolicy ingress drop comment \"Drop rule\"", + + // New commands to check + "add chain inet multi_networkpolicy cnp-dualtest { comment \"MultiNetworkPolicy default/dual-stack-policy\" ; }", + "add rule inet multi_networkpolicy egress jump cnp-dualtest comment \"default/dual-stack-policy\"", + "add set inet multi_networkpolicy snp-dualtest_egress_ipv4_eth1_0 { type ipv4_addr ; comment \"Addresses for default/dual-stack-policy\" ; }", + "add set inet multi_networkpolicy snp-dualtest_egress_ipv6_eth1_0 { type ipv6_addr ; comment \"Addresses for default/dual-stack-policy\" ; }", + "add element inet multi_networkpolicy snp-dualtest_egress_ipv4_eth1_0 { 10.0.1.2 }", + "add element inet multi_networkpolicy snp-dualtest_egress_ipv6_eth1_0 { 2001:db8::2 }", + "add rule inet multi_networkpolicy cnp-dualtest oifname eth1 ip daddr @snp-dualtest_egress_ipv4_eth1_0 accept", + "add rule inet multi_networkpolicy cnp-dualtest oifname eth1 ip6 daddr @snp-dualtest_egress_ipv6_eth1_0 accept", + "", // Empty line at the end + } + + // Verify exact number of expected rules + Expect(dumpLines).To(HaveLen(len(expectedRules)), "Expected exactly %d rules, but got %d. Rules: %v", len(expectedRules), len(dumpLines), dumpLines) + + // Verify each expected rule exists completely + for _, expectedRule := range expectedRules { + found := false + for _, actualRule := range dumpLines { + if strings.Contains(actualRule, expectedRule) { + found = true + break + } + } + Expect(found).To(BeTrue(), "Expected rule not found: %s\nActual rules: %v", expectedRule, dumpLines) + } + }) + + It("should create rules for egress with IPv4 IPBlock", func() { + err := ensureBasicStructure(ctx, nft, nil, logger) + Expect(err).NotTo(HaveOccurred()) + + policy := &datastore.Policy{ + Name: "ipv4-ipblock-policy", + Namespace: "default", + Networks: []string{"default/net1"}, + Spec: multiv1beta1.MultiNetworkPolicySpec{ + Egress: []multiv1beta1.MultiNetworkPolicyEgressRule{ + { + To: []multiv1beta1.MultiNetworkPolicyPeer{ + { + IPBlock: &multiv1beta1.IPBlock{ + CIDR: "10.0.0.0/24", + Except: []string{"10.0.0.1/32", "10.0.0.2/32"}, + }, + }, + }, + }, + }, + }, + } + + matchedInterfaces := []Interface{ + {Name: "eth1", Network: "default/net1", IPs: []string{"10.0.1.1"}}, + } + + tx := nft.NewTransaction() + hashName := "ipv4block" + + createManagedInterfacesSet(tx, matchedInterfaces, hashName, policy.Namespace, policy.Name, logger) + + err = createPolicyChain(ctx, nft, tx, fmt.Sprintf("cnp-%s", hashName), "egress", policy.Namespace, policy.Name, logger) + Expect(err).NotTo(HaveOccurred()) + + nftablesInstance := &NFTables{Client: nil} + err = nftablesInstance.createEgressRules(ctx, tx, matchedInterfaces, policy, hashName, logger) + Expect(err).NotTo(HaveOccurred()) + + err = nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + dump := nft.(*knftables.Fake).Dump() + + // Split the dump into lines for easier verification + dumpLines := strings.Split(dump, "\n") + + // Expected rules that should be generated (based on actual output) + expectedRules := []string{ + "add table inet multi_networkpolicy { comment \"MultiNetworkPolicy\" ; }", + "add chain inet multi_networkpolicy input { type filter hook input priority 0 ; comment \"Input Dispatcher\" ; }", + "add chain inet multi_networkpolicy output { type filter hook output priority 0 ; comment \"Output Dispatcher\" ; }", + "add chain inet multi_networkpolicy ingress { comment \"Ingress Policies\" ; }", + "add chain inet multi_networkpolicy egress { comment \"Egress Policies\" ; }", + "add chain inet multi_networkpolicy common-ingress { comment \"Common Policies\" ; }", + "add chain inet multi_networkpolicy common-egress { comment \"Common Policies\" ; }", + "add rule inet multi_networkpolicy egress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy egress jump common-egress comment \"Jump to common\"", + "add rule inet multi_networkpolicy egress drop comment \"Drop rule\"", + "add rule inet multi_networkpolicy ingress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy ingress jump common-ingress comment \"Jump to common\"", + "add rule inet multi_networkpolicy ingress drop comment \"Drop rule\"", + "add set inet multi_networkpolicy smi-ipv4block { type ifname ; comment \"Managed interfaces set for default/ipv4-ipblock-policy\" ; }", + "add element inet multi_networkpolicy smi-ipv4block { eth1 }", + + // New commands to check + "add chain inet multi_networkpolicy cnp-ipv4block { comment \"MultiNetworkPolicy default/ipv4-ipblock-policy\" ; }", + "add rule inet multi_networkpolicy egress jump cnp-ipv4block comment \"default/ipv4-ipblock-policy\"", + "add set inet multi_networkpolicy snp-ipv4block_egress_ipv4_cidr_0 { type ipv4_addr ; flags interval ; comment \"CIDRs for default/ipv4-ipblock-policy\" ; }", + "add set inet multi_networkpolicy snp-ipv4block_egress_ipv4_except_0 { type ipv4_addr ; flags interval ; comment \"Excepts for default/ipv4-ipblock-policy\" ; }", + "add element inet multi_networkpolicy snp-ipv4block_egress_ipv4_cidr_0 { 10.0.0.0/24 }", + "add element inet multi_networkpolicy snp-ipv4block_egress_ipv4_except_0 { 10.0.0.1/32 }", + "add element inet multi_networkpolicy snp-ipv4block_egress_ipv4_except_0 { 10.0.0.2/32 }", + "add rule inet multi_networkpolicy cnp-ipv4block oifname @smi-ipv4block ip daddr @snp-ipv4block_egress_ipv4_cidr_0 ip daddr != @snp-ipv4block_egress_ipv4_except_0 accept", + "", // Empty line at the end + } + + // Verify exact number of expected rules + Expect(dumpLines).To(HaveLen(len(expectedRules)), "Expected exactly %d rules, but got %d. Rules: %v", len(expectedRules), len(dumpLines), dumpLines) + + // Verify each expected rule exists completely + for _, expectedRule := range expectedRules { + found := false + for _, actualRule := range dumpLines { + if strings.Contains(actualRule, expectedRule) { + found = true + break + } + } + Expect(found).To(BeTrue(), "Expected rule not found: %s\nActual rules: %v", expectedRule, dumpLines) + } + }) + + It("should create rules for egress with IPv6 IPBlock", func() { + err := ensureBasicStructure(ctx, nft, nil, logger) + Expect(err).NotTo(HaveOccurred()) + + policy := &datastore.Policy{ + Name: "ipv6-ipblock-policy", + Namespace: "default", + Networks: []string{"default/net1"}, + Spec: multiv1beta1.MultiNetworkPolicySpec{ + Egress: []multiv1beta1.MultiNetworkPolicyEgressRule{ + { + To: []multiv1beta1.MultiNetworkPolicyPeer{ + { + IPBlock: &multiv1beta1.IPBlock{ + CIDR: "2001:db8::/32", + Except: []string{"2001:db8::1/128"}, + }, + }, + }, + }, + }, + }, + } + + matchedInterfaces := []Interface{ + {Name: "eth1", Network: "default/net1", IPs: []string{"2001:db8::2"}}, + } + + tx := nft.NewTransaction() + hashName := "ipv6block" + + createManagedInterfacesSet(tx, matchedInterfaces, hashName, policy.Namespace, policy.Name, logger) + + err = createPolicyChain(ctx, nft, tx, fmt.Sprintf("cnp-%s", hashName), "egress", policy.Namespace, policy.Name, logger) + Expect(err).NotTo(HaveOccurred()) + + nftablesInstance := &NFTables{Client: nil} + err = nftablesInstance.createEgressRules(ctx, tx, matchedInterfaces, policy, hashName, logger) + Expect(err).NotTo(HaveOccurred()) + + err = nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + dump := nft.(*knftables.Fake).Dump() + + // Split the dump into lines for easier verification + dumpLines := strings.Split(dump, "\n") + + // Expected rules that should be generated (based on actual output) + expectedRules := []string{ + "add table inet multi_networkpolicy { comment \"MultiNetworkPolicy\" ; }", + "add chain inet multi_networkpolicy input { type filter hook input priority 0 ; comment \"Input Dispatcher\" ; }", + "add chain inet multi_networkpolicy output { type filter hook output priority 0 ; comment \"Output Dispatcher\" ; }", + "add chain inet multi_networkpolicy ingress { comment \"Ingress Policies\" ; }", + "add chain inet multi_networkpolicy egress { comment \"Egress Policies\" ; }", + "add chain inet multi_networkpolicy common-ingress { comment \"Common Policies\" ; }", + "add chain inet multi_networkpolicy common-egress { comment \"Common Policies\" ; }", + "add rule inet multi_networkpolicy egress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy egress jump common-egress comment \"Jump to common\"", + "add rule inet multi_networkpolicy egress drop comment \"Drop rule\"", + "add rule inet multi_networkpolicy ingress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy ingress jump common-ingress comment \"Jump to common\"", + "add rule inet multi_networkpolicy ingress drop comment \"Drop rule\"", + "add set inet multi_networkpolicy smi-ipv6block { type ifname ; comment \"Managed interfaces set for default/ipv6-ipblock-policy\" ; }", + "add element inet multi_networkpolicy smi-ipv6block { eth1 }", + + // New commands to check + "add chain inet multi_networkpolicy cnp-ipv6block { comment \"MultiNetworkPolicy default/ipv6-ipblock-policy\" ; }", + "add rule inet multi_networkpolicy egress jump cnp-ipv6block comment \"default/ipv6-ipblock-policy\"", + "add set inet multi_networkpolicy snp-ipv6block_egress_ipv6_cidr_0 { type ipv6_addr ; flags interval ; comment \"CIDRs for default/ipv6-ipblock-policy\" ; }", + "add set inet multi_networkpolicy snp-ipv6block_egress_ipv6_except_0 { type ipv6_addr ; flags interval ; comment \"Excepts for default/ipv6-ipblock-policy\" ; }", + "add element inet multi_networkpolicy snp-ipv6block_egress_ipv6_cidr_0 { 2001:db8::/32 }", + "add element inet multi_networkpolicy snp-ipv6block_egress_ipv6_except_0 { 2001:db8::1/128 }", + "add rule inet multi_networkpolicy cnp-ipv6block oifname @smi-ipv6block ip6 daddr @snp-ipv6block_egress_ipv6_cidr_0 ip6 daddr != @snp-ipv6block_egress_ipv6_except_0 accept", + "", // Empty line at the end + } + + // Verify exact number of expected rules + Expect(dumpLines).To(HaveLen(len(expectedRules)), "Expected exactly %d rules, but got %d. Rules: %v", len(expectedRules), len(dumpLines), dumpLines) + + // Verify each expected rule exists completely + for _, expectedRule := range expectedRules { + found := false + for _, actualRule := range dumpLines { + if strings.Contains(actualRule, expectedRule) { + found = true + break + } + } + Expect(found).To(BeTrue(), "Expected rule not found: %s\nActual rules: %v", expectedRule, dumpLines) + } + }) + + It("should create rules for egress with dual-stack IPBlock", func() { + err := ensureBasicStructure(ctx, nft, nil, logger) + Expect(err).NotTo(HaveOccurred()) + + policy := &datastore.Policy{ + Name: "dual-ipblock-policy", + Namespace: "default", + Networks: []string{"default/net1"}, + Spec: multiv1beta1.MultiNetworkPolicySpec{ + Egress: []multiv1beta1.MultiNetworkPolicyEgressRule{ + { + To: []multiv1beta1.MultiNetworkPolicyPeer{ + { + IPBlock: &multiv1beta1.IPBlock{ + CIDR: "10.0.0.0/24", + }, + }, + { + IPBlock: &multiv1beta1.IPBlock{ + CIDR: "2001:db8::/32", + }, + }, + }, + }, + }, + }, + } + + matchedInterfaces := []Interface{ + {Name: "eth1", Network: "default/net1", IPs: []string{"10.0.1.1", "2001:db8::1"}}, + } + + tx := nft.NewTransaction() + hashName := "dualblock" + + createManagedInterfacesSet(tx, matchedInterfaces, hashName, policy.Namespace, policy.Name, logger) + + err = createPolicyChain(ctx, nft, tx, fmt.Sprintf("cnp-%s", hashName), "egress", policy.Namespace, policy.Name, logger) + Expect(err).NotTo(HaveOccurred()) + + nftablesInstance := &NFTables{Client: nil} + err = nftablesInstance.createEgressRules(ctx, tx, matchedInterfaces, policy, hashName, logger) + Expect(err).NotTo(HaveOccurred()) + + err = nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + dump := nft.(*knftables.Fake).Dump() + + // Split the dump into lines for easier verification + dumpLines := strings.Split(dump, "\n") + + // Expected rules that should be generated (based on actual output) + expectedRules := []string{ + "add table inet multi_networkpolicy { comment \"MultiNetworkPolicy\" ; }", + "add chain inet multi_networkpolicy input { type filter hook input priority 0 ; comment \"Input Dispatcher\" ; }", + "add chain inet multi_networkpolicy output { type filter hook output priority 0 ; comment \"Output Dispatcher\" ; }", + "add chain inet multi_networkpolicy ingress { comment \"Ingress Policies\" ; }", + "add chain inet multi_networkpolicy egress { comment \"Egress Policies\" ; }", + "add chain inet multi_networkpolicy common-ingress { comment \"Common Policies\" ; }", + "add chain inet multi_networkpolicy common-egress { comment \"Common Policies\" ; }", + "add rule inet multi_networkpolicy egress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy egress jump common-egress comment \"Jump to common\"", + "add rule inet multi_networkpolicy egress drop comment \"Drop rule\"", + "add rule inet multi_networkpolicy ingress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy ingress jump common-ingress comment \"Jump to common\"", + "add rule inet multi_networkpolicy ingress drop comment \"Drop rule\"", + "add set inet multi_networkpolicy smi-dualblock { type ifname ; comment \"Managed interfaces set for default/dual-ipblock-policy\" ; }", + "add element inet multi_networkpolicy smi-dualblock { eth1 }", + + // New commands to check + "add chain inet multi_networkpolicy cnp-dualblock { comment \"MultiNetworkPolicy default/dual-ipblock-policy\" ; }", + "add rule inet multi_networkpolicy egress jump cnp-dualblock comment \"default/dual-ipblock-policy\"", + "add set inet multi_networkpolicy snp-dualblock_egress_ipv4_cidr_0 { type ipv4_addr ; flags interval ; comment \"CIDRs for default/dual-ipblock-policy\" ; }", + "add set inet multi_networkpolicy snp-dualblock_egress_ipv6_cidr_0 { type ipv6_addr ; flags interval ; comment \"CIDRs for default/dual-ipblock-policy\" ; }", + "add element inet multi_networkpolicy snp-dualblock_egress_ipv4_cidr_0 { 10.0.0.0/24 }", + "add element inet multi_networkpolicy snp-dualblock_egress_ipv6_cidr_0 { 2001:db8::/32 }", + "add rule inet multi_networkpolicy cnp-dualblock oifname @smi-dualblock ip daddr @snp-dualblock_egress_ipv4_cidr_0 accept", + "add rule inet multi_networkpolicy cnp-dualblock oifname @smi-dualblock ip6 daddr @snp-dualblock_egress_ipv6_cidr_0 accept", + "", // Empty line at the end + } + + // Verify exact number of expected rules + Expect(dumpLines).To(HaveLen(len(expectedRules)), "Expected exactly %d rules, but got %d. Rules: %v", len(expectedRules), len(dumpLines), dumpLines) + + // Verify each expected rule exists completely + for _, expectedRule := range expectedRules { + found := false + for _, actualRule := range dumpLines { + if strings.Contains(actualRule, expectedRule) { + found = true + break + } + } + Expect(found).To(BeTrue(), "Expected rule not found: %s\nActual rules: %v", expectedRule, dumpLines) + } + }) + + It("should create rules for egress with mixed pod selector and IPBlock with ports", func() { + err := ensureBasicStructure(ctx, nft, nil, logger) + Expect(err).NotTo(HaveOccurred()) + + // Create fake client with pod + scheme := runtime.NewScheme() + _ = corev1.AddToScheme(scheme) + + pod1 := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod1", + Namespace: "default", + Labels: map[string]string{"app": "web"}, + Annotations: map[string]string{ + "k8s.v1.cni.cncf.io/networks": `[{"name": "net1"}]`, + "k8s.v1.cni.cncf.io/network-status": `[{ + "name": "default/net1", + "interface": "eth1", + "ips": ["10.0.1.1"] + }]`, + }, + }, + Status: corev1.PodStatus{Phase: corev1.PodRunning}, + Spec: corev1.PodSpec{HostNetwork: false}, + } + + fakeClient := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(pod1). + WithIndex(&corev1.Pod{}, PodStatusIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + return []string{string(pod.Status.Phase)} + }). + WithIndex(&corev1.Pod{}, PodHostNetworkIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + return []string{fmt.Sprintf("%t", pod.Spec.HostNetwork)} + }). + WithIndex(&corev1.Pod{}, PodHasNetworkAnnotationIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + _, hasAnnotation := pod.Annotations["k8s.v1.cni.cncf.io/networks"] + return []string{fmt.Sprintf("%t", hasAnnotation)} + }). + Build() + + tcp := corev1.ProtocolTCP + policy := &datastore.Policy{ + Name: "mixed-policy", + Namespace: "default", + Networks: []string{"default/net1"}, + Spec: multiv1beta1.MultiNetworkPolicySpec{ + Egress: []multiv1beta1.MultiNetworkPolicyEgressRule{ + { + To: []multiv1beta1.MultiNetworkPolicyPeer{ + { + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "web"}, + }, + }, + { + IPBlock: &multiv1beta1.IPBlock{ + CIDR: "192.168.1.0/24", + }, + }, + }, + Ports: []multiv1beta1.MultiNetworkPolicyPort{ + { + Protocol: &tcp, + Port: &intstr.IntOrString{Type: intstr.Int, IntVal: 80}, + }, + }, + }, + }, + }, + } + + matchedInterfaces := []Interface{ + {Name: "eth1", Network: "default/net1", IPs: []string{"10.0.1.1"}}, + } + + tx := nft.NewTransaction() + hashName := "mixed" + + createManagedInterfacesSet(tx, matchedInterfaces, hashName, policy.Namespace, policy.Name, logger) + + err = createPolicyChain(ctx, nft, tx, fmt.Sprintf("cnp-%s", hashName), "egress", policy.Namespace, policy.Name, logger) + Expect(err).NotTo(HaveOccurred()) + + nftablesInstance := &NFTables{Client: fakeClient} + err = nftablesInstance.createEgressRules(ctx, tx, matchedInterfaces, policy, hashName, logger) + Expect(err).NotTo(HaveOccurred()) + + err = nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + dump := nft.(*knftables.Fake).Dump() + + // Split the dump into lines for easier verification + dumpLines := strings.Split(dump, "\n") + + // Expected rules that should be generated (based on actual output) + expectedRules := []string{ + "add table inet multi_networkpolicy { comment \"MultiNetworkPolicy\" ; }", + "add chain inet multi_networkpolicy input { type filter hook input priority 0 ; comment \"Input Dispatcher\" ; }", + "add chain inet multi_networkpolicy output { type filter hook output priority 0 ; comment \"Output Dispatcher\" ; }", + "add chain inet multi_networkpolicy ingress { comment \"Ingress Policies\" ; }", + "add chain inet multi_networkpolicy egress { comment \"Egress Policies\" ; }", + "add chain inet multi_networkpolicy common-ingress { comment \"Common Policies\" ; }", + "add chain inet multi_networkpolicy common-egress { comment \"Common Policies\" ; }", + "add rule inet multi_networkpolicy egress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy egress jump common-egress comment \"Jump to common\"", + "add rule inet multi_networkpolicy egress drop comment \"Drop rule\"", + "add rule inet multi_networkpolicy ingress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy ingress jump common-ingress comment \"Jump to common\"", + "add rule inet multi_networkpolicy ingress drop comment \"Drop rule\"", + "add set inet multi_networkpolicy smi-mixed { type ifname ; comment \"Managed interfaces set for default/mixed-policy\" ; }", + "add element inet multi_networkpolicy smi-mixed { eth1 }", + + // New commands to check + "add chain inet multi_networkpolicy cnp-mixed { comment \"MultiNetworkPolicy default/mixed-policy\" ; }", + "add rule inet multi_networkpolicy egress jump cnp-mixed comment \"default/mixed-policy\"", + "add set inet multi_networkpolicy snp-mixed_egress_ipv4_cidr_0 { type ipv4_addr ; flags interval ; comment \"CIDRs for default/mixed-policy\" ; }", + "add set inet multi_networkpolicy snp-mixed_egress_ipv4_eth1_0 { type ipv4_addr ; comment \"Addresses for default/mixed-policy\" ; }", + "add element inet multi_networkpolicy snp-mixed_egress_ipv4_cidr_0 { 192.168.1.0/24 }", + "add element inet multi_networkpolicy snp-mixed_egress_ipv4_eth1_0 { 10.0.1.1 }", + "add rule inet multi_networkpolicy cnp-mixed oifname eth1 ip daddr @snp-mixed_egress_ipv4_eth1_0 meta l4proto tcp th dport { 80 } accept", + "add rule inet multi_networkpolicy cnp-mixed oifname @smi-mixed ip daddr @snp-mixed_egress_ipv4_cidr_0 meta l4proto tcp th dport { 80 } accept", + "", // Empty line at the end + } + + // Verify exact number of expected rules + Expect(dumpLines).To(HaveLen(len(expectedRules)), "Expected exactly %d rules, but got %d. Rules: %v", len(expectedRules), len(dumpLines), dumpLines) + + // Verify each expected rule exists completely + for _, expectedRule := range expectedRules { + found := false + for _, actualRule := range dumpLines { + if strings.Contains(actualRule, expectedRule) { + found = true + break + } + } + Expect(found).To(BeTrue(), "Expected rule not found: %s\nActual rules: %v", expectedRule, dumpLines) + } + }) + + It("should create rules for egress with multiple interfaces for same network", func() { + err := ensureBasicStructure(ctx, nft, nil, logger) + Expect(err).NotTo(HaveOccurred()) + + // Create fake client with pods having multiple interfaces + scheme := runtime.NewScheme() + _ = corev1.AddToScheme(scheme) + + pod1 := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod1", + Namespace: "default", + Labels: map[string]string{"app": "web"}, + Annotations: map[string]string{ + "k8s.v1.cni.cncf.io/networks": `[{"name": "net1"}, {"name": "net1"}, {"name": "net1"}]`, + "k8s.v1.cni.cncf.io/network-status": `[{ + "name": "net1", + "interface": "eth1", + "ips": ["10.0.1.1"] + }, { + "name": "net1", + "interface": "eth2", + "ips": ["10.0.1.2"] + }, { + "name": "net1", + "interface": "eth3", + "ips": ["10.0.1.3"] + }]`, + }, + }, + Status: corev1.PodStatus{Phase: corev1.PodRunning}, + Spec: corev1.PodSpec{HostNetwork: false}, + } + + fakeClient := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(pod1). + WithIndex(&corev1.Pod{}, PodStatusIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + return []string{string(pod.Status.Phase)} + }). + WithIndex(&corev1.Pod{}, PodHostNetworkIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + return []string{fmt.Sprintf("%t", pod.Spec.HostNetwork)} + }). + WithIndex(&corev1.Pod{}, PodHasNetworkAnnotationIndex, func(obj client.Object) []string { + pod := obj.(*corev1.Pod) + _, hasAnnotation := pod.Annotations["k8s.v1.cni.cncf.io/networks"] + return []string{fmt.Sprintf("%t", hasAnnotation)} + }). + Build() + + policy := &datastore.Policy{ + Name: "multi-interface-policy", + Namespace: "default", + Networks: []string{"default/net1"}, + Spec: multiv1beta1.MultiNetworkPolicySpec{ + Egress: []multiv1beta1.MultiNetworkPolicyEgressRule{ + { + To: []multiv1beta1.MultiNetworkPolicyPeer{ + { + PodSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "web"}, + }, + }, + }, + }, + }, + }, + } + + matchedInterfaces := []Interface{ + {Name: "eth1", Network: "default/net1", IPs: []string{"10.0.1.1"}}, + {Name: "eth2", Network: "default/net1", IPs: []string{"10.0.1.2"}}, + {Name: "eth3", Network: "default/net1", IPs: []string{"10.0.1.3"}}, + } + + tx := nft.NewTransaction() + hashName := "multiintf" + err = createPolicyChain(ctx, nft, tx, fmt.Sprintf("cnp-%s", hashName), "egress", policy.Namespace, policy.Name, logger) + Expect(err).NotTo(HaveOccurred()) + + nftablesInstance := &NFTables{Client: fakeClient} + err = nftablesInstance.createEgressRules(ctx, tx, matchedInterfaces, policy, hashName, logger) + Expect(err).NotTo(HaveOccurred()) + + err = nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + dump := nft.(*knftables.Fake).Dump() + + // Split the dump into lines for easier verification + dumpLines := strings.Split(dump, "\n") + + // Expected rules that should be generated (based on actual output) + expectedRules := []string{ + "add table inet multi_networkpolicy { comment \"MultiNetworkPolicy\" ; }", + "add chain inet multi_networkpolicy input { type filter hook input priority 0 ; comment \"Input Dispatcher\" ; }", + "add chain inet multi_networkpolicy output { type filter hook output priority 0 ; comment \"Output Dispatcher\" ; }", + "add chain inet multi_networkpolicy ingress { comment \"Ingress Policies\" ; }", + "add chain inet multi_networkpolicy egress { comment \"Egress Policies\" ; }", + "add chain inet multi_networkpolicy common-ingress { comment \"Common Policies\" ; }", + "add chain inet multi_networkpolicy common-egress { comment \"Common Policies\" ; }", + "add rule inet multi_networkpolicy egress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy egress jump common-egress comment \"Jump to common\"", + "add rule inet multi_networkpolicy egress drop comment \"Drop rule\"", + "add rule inet multi_networkpolicy ingress ct state established,related accept comment \"Connection tracking\"", + "add rule inet multi_networkpolicy ingress jump common-ingress comment \"Jump to common\"", + "add rule inet multi_networkpolicy ingress drop comment \"Drop rule\"", + + // New commands to check + "add chain inet multi_networkpolicy cnp-multiintf { comment \"MultiNetworkPolicy default/multi-interface-policy\" ; }", + "add rule inet multi_networkpolicy egress jump cnp-multiintf comment \"default/multi-interface-policy\"", + "add set inet multi_networkpolicy snp-multiintf_egress_ipv4_eth1_0 { type ipv4_addr ; comment \"Addresses for default/multi-interface-policy\" ; }", + "add set inet multi_networkpolicy snp-multiintf_egress_ipv4_eth2_0 { type ipv4_addr ; comment \"Addresses for default/multi-interface-policy\" ; }", + "add set inet multi_networkpolicy snp-multiintf_egress_ipv4_eth3_0 { type ipv4_addr ; comment \"Addresses for default/multi-interface-policy\" ; }", + "add element inet multi_networkpolicy snp-multiintf_egress_ipv4_eth1_0 { 10.0.1.1 }", + "add element inet multi_networkpolicy snp-multiintf_egress_ipv4_eth1_0 { 10.0.1.2 }", + "add element inet multi_networkpolicy snp-multiintf_egress_ipv4_eth1_0 { 10.0.1.3 }", + "add element inet multi_networkpolicy snp-multiintf_egress_ipv4_eth2_0 { 10.0.1.1 }", + "add element inet multi_networkpolicy snp-multiintf_egress_ipv4_eth2_0 { 10.0.1.2 }", + "add element inet multi_networkpolicy snp-multiintf_egress_ipv4_eth2_0 { 10.0.1.3 }", + "add element inet multi_networkpolicy snp-multiintf_egress_ipv4_eth3_0 { 10.0.1.1 }", + "add element inet multi_networkpolicy snp-multiintf_egress_ipv4_eth3_0 { 10.0.1.2 }", + "add element inet multi_networkpolicy snp-multiintf_egress_ipv4_eth3_0 { 10.0.1.3 }", + "add rule inet multi_networkpolicy cnp-multiintf oifname eth1 ip daddr @snp-multiintf_egress_ipv4_eth1_0 accept", + "add rule inet multi_networkpolicy cnp-multiintf oifname eth2 ip daddr @snp-multiintf_egress_ipv4_eth2_0 accept", + "add rule inet multi_networkpolicy cnp-multiintf oifname eth3 ip daddr @snp-multiintf_egress_ipv4_eth3_0 accept", + "", // Empty line at the end + } + + // Verify exact number of expected rules + Expect(dumpLines).To(HaveLen(len(expectedRules)), "Expected exactly %d rules, but got %d. Rules: %v", len(expectedRules), len(dumpLines), dumpLines) + + // Verify each expected rule exists completely + for _, expectedRule := range expectedRules { + found := false + for _, actualRule := range dumpLines { + if strings.Contains(actualRule, expectedRule) { + found = true + break + } + } + Expect(found).To(BeTrue(), "Expected rule not found: %s\nActual rules: %v", expectedRule, dumpLines) + } + }) + }) + + Context("createCommonRules", func() { + var ( + nft knftables.Interface + ctx context.Context + logger logr.Logger + tableName string + ) + + BeforeEach(func() { + ctx = context.Background() + tableName = "test-table" + nft = knftables.NewFake(knftables.InetFamily, tableName) + logger = logr.Discard() + }) + + // Helper function to create table and common chains + createTableAndChains := func() { + tx := nft.NewTransaction() + tx.Add(&knftables.Table{ + Comment: knftables.PtrTo("MultiNetworkPolicy"), + }) + // Create common chains + tx.Add(&knftables.Chain{ + Name: commonIngressChain, + Comment: knftables.PtrTo("Common Ingress Policies"), + }) + tx.Add(&knftables.Chain{ + Name: commonEgressChain, + Comment: knftables.PtrTo("Common Egress Policies"), + }) + err := nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + } + + Context("when commonRules is nil", func() { + It("should not add any rules", func() { + createTableAndChains() + + // Now test createCommonRules + tx := nft.NewTransaction() + createCommonRules(tx, nil, logger) + + // Verify no operations were performed (only the common chains exist) + chains, err := nft.List(ctx, "chain") + Expect(err).NotTo(HaveOccurred()) + Expect(chains).To(ContainElements(commonIngressChain, commonEgressChain)) + }) + }) + + Context("when commonRules has both ICMP and ICMPv6 disabled", func() { + It("should only flush common chains", func() { + createTableAndChains() + + commonRules := &CommonRules{ + AcceptICMP: false, + AcceptICMPv6: false, + } + + tx := nft.NewTransaction() + createCommonRules(tx, commonRules, logger) + + // Run the transaction + err := nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + // Verify only flush operations were performed (no rules added) + chains, err := nft.List(ctx, "chain") + Expect(err).NotTo(HaveOccurred()) + Expect(chains).To(ContainElements(commonIngressChain, commonEgressChain)) + }) + }) + + Context("when commonRules has only ICMP enabled", func() { + It("should add ICMP rules to both common chains", func() { + createTableAndChains() + + commonRules := &CommonRules{ + AcceptICMP: true, + AcceptICMPv6: false, + } + + tx := nft.NewTransaction() + createCommonRules(tx, commonRules, logger) + + // Run the transaction + err := nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + // Verify the common chains were created + chains, err := nft.List(ctx, "chain") + Expect(err).NotTo(HaveOccurred()) + Expect(chains).To(ContainElements(commonIngressChain, commonEgressChain)) + + // Verify ICMP rules were added to both chains + ingressRules, err := nft.ListRules(ctx, commonIngressChain) + Expect(err).NotTo(HaveOccurred()) + Expect(ingressRules).To(HaveLen(1)) + Expect(*ingressRules[0].Comment).To(Equal("Accept ICMP")) + + egressRules, err := nft.ListRules(ctx, commonEgressChain) + Expect(err).NotTo(HaveOccurred()) + Expect(egressRules).To(HaveLen(1)) + Expect(*egressRules[0].Comment).To(Equal("Accept ICMP")) + }) + }) + + Context("when commonRules has only ICMPv6 enabled", func() { + It("should add ICMPv6 rules to both common chains", func() { + createTableAndChains() + + commonRules := &CommonRules{ + AcceptICMP: false, + AcceptICMPv6: true, + } + + tx := nft.NewTransaction() + createCommonRules(tx, commonRules, logger) + + // Run the transaction + err := nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + // Verify the common chains were created + chains, err := nft.List(ctx, "chain") + Expect(err).NotTo(HaveOccurred()) + Expect(chains).To(ContainElements(commonIngressChain, commonEgressChain)) + + // Verify ICMPv6 rules were added to both chains + ingressRules, err := nft.ListRules(ctx, commonIngressChain) + Expect(err).NotTo(HaveOccurred()) + Expect(ingressRules).To(HaveLen(1)) + Expect(*ingressRules[0].Comment).To(Equal("Accept ICMPv6")) + + egressRules, err := nft.ListRules(ctx, commonEgressChain) + Expect(err).NotTo(HaveOccurred()) + Expect(egressRules).To(HaveLen(1)) + Expect(*egressRules[0].Comment).To(Equal("Accept ICMPv6")) + }) + }) + + Context("when commonRules has both ICMP and ICMPv6 enabled", func() { + It("should add both ICMP and ICMPv6 rules to both common chains", func() { + createTableAndChains() + + commonRules := &CommonRules{ + AcceptICMP: true, + AcceptICMPv6: true, + } + + tx := nft.NewTransaction() + createCommonRules(tx, commonRules, logger) + + // Run the transaction + err := nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + // Verify the common chains were created + chains, err := nft.List(ctx, "chain") + Expect(err).NotTo(HaveOccurred()) + Expect(chains).To(ContainElements(commonIngressChain, commonEgressChain)) + + // Verify both ICMP and ICMPv6 rules were added to ingress chain + ingressRules, err := nft.ListRules(ctx, commonIngressChain) + Expect(err).NotTo(HaveOccurred()) + Expect(ingressRules).To(HaveLen(2)) + + comments := []string{*ingressRules[0].Comment, *ingressRules[1].Comment} + Expect(comments).To(ContainElements("Accept ICMP", "Accept ICMPv6")) + + // Verify both ICMP and ICMPv6 rules were added to egress chain + egressRules, err := nft.ListRules(ctx, commonEgressChain) + Expect(err).NotTo(HaveOccurred()) + Expect(egressRules).To(HaveLen(2)) + + comments = []string{*egressRules[0].Comment, *egressRules[1].Comment} + Expect(comments).To(ContainElements("Accept ICMP", "Accept ICMPv6")) + }) + }) + + Context("when called multiple times with different rules", func() { + It("should flush and recreate rules each time", func() { + createTableAndChains() + + // First call with ICMP only + commonRules1 := &CommonRules{ + AcceptICMP: true, + AcceptICMPv6: false, + } + + tx1 := nft.NewTransaction() + createCommonRules(tx1, commonRules1, logger) + err := nft.Run(ctx, tx1) + Expect(err).NotTo(HaveOccurred()) + + // Verify only ICMP rules exist + ingressRules1, err := nft.ListRules(ctx, commonIngressChain) + Expect(err).NotTo(HaveOccurred()) + Expect(ingressRules1).To(HaveLen(1)) + Expect(*ingressRules1[0].Comment).To(Equal("Accept ICMP")) + + // Second call with both ICMP and ICMPv6 + commonRules2 := &CommonRules{ + AcceptICMP: true, + AcceptICMPv6: true, + } + + tx2 := nft.NewTransaction() + createCommonRules(tx2, commonRules2, logger) + err = nft.Run(ctx, tx2) + Expect(err).NotTo(HaveOccurred()) + + // Verify both ICMP and ICMPv6 rules exist (previous rules were flushed) + ingressRules2, err := nft.ListRules(ctx, commonIngressChain) + Expect(err).NotTo(HaveOccurred()) + Expect(ingressRules2).To(HaveLen(2)) + + comments := []string{*ingressRules2[0].Comment, *ingressRules2[1].Comment} + Expect(comments).To(ContainElements("Accept ICMP", "Accept ICMPv6")) + }) + }) + + Context("custom rules handling", func() { + It("should add custom IPv4 ingress rules", func() { + createTableAndChains() + + commonRules := &CommonRules{ + AcceptICMP: false, + AcceptICMPv6: false, + CustomIPv4IngressRules: []string{ + "tcp dport 8080 accept", + "udp dport 9090 accept", + }, + } + + tx := nft.NewTransaction() + createCommonRules(tx, commonRules, logger) + + // Run the transaction + err := nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + // Verify custom rules were added to ingress chain + ingressRules, err := nft.ListRules(ctx, commonIngressChain) + Expect(err).NotTo(HaveOccurred()) + Expect(ingressRules).To(HaveLen(2)) + Expect(*ingressRules[0].Comment).To(Equal("Custom Rule")) + Expect(*ingressRules[1].Comment).To(Equal("Custom Rule")) + + // Verify no custom rules were added to egress chain (only ingress rules) + egressRules, err := nft.ListRules(ctx, commonEgressChain) + Expect(err).NotTo(HaveOccurred()) + Expect(egressRules).To(BeEmpty()) + }) + + It("should add custom IPv6 ingress rules", func() { + createTableAndChains() + + commonRules := &CommonRules{ + AcceptICMP: false, + AcceptICMPv6: false, + CustomIPv6IngressRules: []string{ + "ip6 saddr 2001:db8::/32 accept", + "ip6 daddr fe80::/10 drop", + }, + } + + tx := nft.NewTransaction() + createCommonRules(tx, commonRules, logger) + + // Run the transaction + err := nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + // Verify custom rules were added to ingress chain + ingressRules, err := nft.ListRules(ctx, commonIngressChain) + Expect(err).NotTo(HaveOccurred()) + Expect(ingressRules).To(HaveLen(2)) + Expect(*ingressRules[0].Comment).To(Equal("Custom Rule")) + Expect(*ingressRules[1].Comment).To(Equal("Custom Rule")) + + // Verify no custom rules were added to egress chain (only ingress rules) + egressRules, err := nft.ListRules(ctx, commonEgressChain) + Expect(err).NotTo(HaveOccurred()) + Expect(egressRules).To(BeEmpty()) + }) + + It("should add custom IPv4 egress rules", func() { + createTableAndChains() + + commonRules := &CommonRules{ + AcceptICMP: false, + AcceptICMPv6: false, + CustomIPv4EgressRules: []string{ + "ip saddr 192.168.1.0/24 accept", + "ip daddr 10.0.0.0/8 drop", + }, + } + + tx := nft.NewTransaction() + createCommonRules(tx, commonRules, logger) + + // Run the transaction + err := nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + // Verify no custom rules were added to ingress chain (only egress rules) + ingressRules, err := nft.ListRules(ctx, commonIngressChain) + Expect(err).NotTo(HaveOccurred()) + Expect(ingressRules).To(BeEmpty()) + + // Verify custom rules were added to egress chain + egressRules, err := nft.ListRules(ctx, commonEgressChain) + Expect(err).NotTo(HaveOccurred()) + Expect(egressRules).To(HaveLen(2)) + Expect(*egressRules[0].Comment).To(Equal("Custom Rule")) + Expect(*egressRules[1].Comment).To(Equal("Custom Rule")) + }) + + It("should add custom IPv6 egress rules", func() { + createTableAndChains() + + commonRules := &CommonRules{ + AcceptICMP: false, + AcceptICMPv6: false, + CustomIPv6EgressRules: []string{ + "ip6 saddr 2001:db8::/32 accept", + "ip6 daddr fe80::/10 drop", + }, + } + + tx := nft.NewTransaction() + createCommonRules(tx, commonRules, logger) + + // Run the transaction + err := nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + // Verify no custom rules were added to ingress chain (only egress rules) + ingressRules, err := nft.ListRules(ctx, commonIngressChain) + Expect(err).NotTo(HaveOccurred()) + Expect(ingressRules).To(BeEmpty()) + + // Verify custom rules were added to egress chain + egressRules, err := nft.ListRules(ctx, commonEgressChain) + Expect(err).NotTo(HaveOccurred()) + Expect(egressRules).To(HaveLen(2)) + Expect(*egressRules[0].Comment).To(Equal("Custom Rule")) + Expect(*egressRules[1].Comment).To(Equal("Custom Rule")) + }) + + It("should add all types of custom rules together", func() { + createTableAndChains() + + commonRules := &CommonRules{ + AcceptICMP: true, + AcceptICMPv6: true, + CustomIPv4IngressRules: []string{ + "tcp dport 8080 accept", + }, + CustomIPv6IngressRules: []string{ + "ip6 saddr 2001:db8::/32 accept", + }, + CustomIPv4EgressRules: []string{ + "ip saddr 192.168.1.0/24 accept", + }, + CustomIPv6EgressRules: []string{ + "ip6 daddr fe80::/10 drop", + }, + } + + tx := nft.NewTransaction() + createCommonRules(tx, commonRules, logger) + + // Run the transaction + err := nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + // Verify all rules were added to ingress chain + // Should have: ICMP, ICMPv6, IPv4 ingress custom, IPv6 ingress custom = 4 rules + ingressRules, err := nft.ListRules(ctx, commonIngressChain) + Expect(err).NotTo(HaveOccurred()) + Expect(ingressRules).To(HaveLen(4)) + + // Verify all rules were added to egress chain + // Should have: ICMP, ICMPv6, IPv4 egress custom, IPv6 egress custom = 4 rules + egressRules, err := nft.ListRules(ctx, commonEgressChain) + Expect(err).NotTo(HaveOccurred()) + Expect(egressRules).To(HaveLen(4)) + }) + + It("should handle empty custom rules", func() { + createTableAndChains() + + commonRules := &CommonRules{ + AcceptICMP: false, + AcceptICMPv6: false, + CustomIPv4IngressRules: []string{}, + CustomIPv6IngressRules: []string{}, + CustomIPv4EgressRules: []string{}, + CustomIPv6EgressRules: []string{}, + } + + tx := nft.NewTransaction() + createCommonRules(tx, commonRules, logger) + + // Run the transaction + err := nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + // Verify no rules were added + ingressRules, err := nft.ListRules(ctx, commonIngressChain) + Expect(err).NotTo(HaveOccurred()) + Expect(ingressRules).To(BeEmpty()) + + egressRules, err := nft.ListRules(ctx, commonEgressChain) + Expect(err).NotTo(HaveOccurred()) + Expect(egressRules).To(BeEmpty()) + }) + + It("should flush and recreate custom rules on multiple calls", func() { + createTableAndChains() + + // First call with some custom rules + commonRules1 := &CommonRules{ + AcceptICMP: false, + AcceptICMPv6: false, + CustomIPv4IngressRules: []string{ + "tcp dport 8080 accept", + }, + } + + tx1 := nft.NewTransaction() + createCommonRules(tx1, commonRules1, logger) + err := nft.Run(ctx, tx1) + Expect(err).NotTo(HaveOccurred()) + + // Verify first set of rules + ingressRules1, err := nft.ListRules(ctx, commonIngressChain) + Expect(err).NotTo(HaveOccurred()) + Expect(ingressRules1).To(HaveLen(1)) + + // Second call with different custom rules + commonRules2 := &CommonRules{ + AcceptICMP: false, + AcceptICMPv6: false, + CustomIPv4IngressRules: []string{ + "tcp dport 9090 accept", + "udp dport 8080 accept", + }, + } + + tx2 := nft.NewTransaction() + createCommonRules(tx2, commonRules2, logger) + err = nft.Run(ctx, tx2) + Expect(err).NotTo(HaveOccurred()) + + // Verify second set of rules (previous rules should be flushed) + ingressRules2, err := nft.ListRules(ctx, commonIngressChain) + Expect(err).NotTo(HaveOccurred()) + Expect(ingressRules2).To(HaveLen(2)) + }) + }) + + Context("rule content verification", func() { + It("should create correct ICMP rule content", func() { + createTableAndChains() + + commonRules := &CommonRules{ + AcceptICMP: true, + AcceptICMPv6: false, + } + + tx := nft.NewTransaction() + createCommonRules(tx, commonRules, logger) + + // Run the transaction + err := nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + // Get the actual nftables output to verify rule content + cmd := nft.(*knftables.Fake).Dump() + Expect(cmd).To(ContainSubstring("meta l4proto icmp accept")) + }) + + It("should create correct ICMPv6 rule content", func() { + createTableAndChains() + + commonRules := &CommonRules{ + AcceptICMP: false, + AcceptICMPv6: true, + } + + tx := nft.NewTransaction() + createCommonRules(tx, commonRules, logger) + + // Run the transaction + err := nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + // Get the actual nftables output to verify rule content + cmd := nft.(*knftables.Fake).Dump() + Expect(cmd).To(ContainSubstring("meta l4proto icmpv6 accept")) + }) + }) + }) + + Context("createReverseRules", func() { + var ( + nft knftables.Interface + ctx context.Context + logger logr.Logger + tableName string + ) + + BeforeEach(func() { + ctx = context.Background() + tableName = "test-table" + nft = knftables.NewFake(knftables.InetFamily, tableName) + logger = logr.Discard() + }) + + // Helper function to create table and policy chain + createTableAndPolicyChain := func(chainName string) { + tx := nft.NewTransaction() + tx.Add(&knftables.Table{ + Comment: knftables.PtrTo("MultiNetworkPolicy"), + }) + tx.Add(&knftables.Chain{ + Name: chainName, + Comment: knftables.PtrTo("Test Policy Chain"), + }) + err := nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + } + + Context("when interfaces have valid IPv4 addresses", func() { + It("should create reverse routes for IPv4 addresses", func() { + chainName := "test-policy-chain" + createTableAndPolicyChain(chainName) + + matchedInterfaces := []Interface{ + { + Name: "eth0", + Network: "default/macvlan1", + IPs: []string{"192.168.1.10", "192.168.1.11"}, + }, + { + Name: "eth1", + Network: "default/macvlan2", + IPs: []string{"192.168.2.10"}, + }, + } + + tx := nft.NewTransaction() + createReverseRules(tx, matchedInterfaces, chainName, logger) + + // Run the transaction + err := nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + // Verify rules were created + rules, err := nft.ListRules(ctx, chainName) + Expect(err).NotTo(HaveOccurred()) + Expect(rules).To(HaveLen(3)) // 2 IPs from eth0 + 1 IP from eth1 + + // Verify rule content + cmd := nft.(*knftables.Fake).Dump() + Expect(cmd).To(ContainSubstring("iifname eth0 ip saddr 192.168.1.10 accept")) + Expect(cmd).To(ContainSubstring("iifname eth0 ip saddr 192.168.1.11 accept")) + Expect(cmd).To(ContainSubstring("iifname eth1 ip saddr 192.168.2.10 accept")) + }) + }) + + Context("when interfaces have valid IPv6 addresses", func() { + It("should create reverse routes for IPv6 addresses", func() { + chainName := "test-policy-chain" + createTableAndPolicyChain(chainName) + + matchedInterfaces := []Interface{ + { + Name: "eth0", + Network: "default/macvlan1", + IPs: []string{"2001:db8::1", "2001:db8::2"}, + }, + { + Name: "eth1", + Network: "default/macvlan2", + IPs: []string{"2001:db8:1::1"}, + }, + } + + tx := nft.NewTransaction() + createReverseRules(tx, matchedInterfaces, chainName, logger) + + // Run the transaction + err := nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + // Verify rules were created + rules, err := nft.ListRules(ctx, chainName) + Expect(err).NotTo(HaveOccurred()) + Expect(rules).To(HaveLen(3)) // 2 IPs from eth0 + 1 IP from eth1 + + // Verify rule content + cmd := nft.(*knftables.Fake).Dump() + Expect(cmd).To(ContainSubstring("iifname eth0 ip6 saddr 2001:db8::1 accept")) + Expect(cmd).To(ContainSubstring("iifname eth0 ip6 saddr 2001:db8::2 accept")) + Expect(cmd).To(ContainSubstring("iifname eth1 ip6 saddr 2001:db8:1::1 accept")) + }) + }) + + Context("when interfaces have mixed IPv4 and IPv6 addresses", func() { + It("should create reverse routes for both IPv4 and IPv6 addresses", func() { + chainName := "test-policy-chain" + createTableAndPolicyChain(chainName) + + matchedInterfaces := []Interface{ + { + Name: "eth0", + Network: "default/macvlan1", + IPs: []string{"192.168.1.10", "2001:db8::1"}, + }, + { + Name: "eth1", + Network: "default/macvlan2", + IPs: []string{"10.0.0.1", "2001:db8:1::1"}, + }, + } + + tx := nft.NewTransaction() + createReverseRules(tx, matchedInterfaces, chainName, logger) + + // Run the transaction + err := nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + // Verify rules were created + rules, err := nft.ListRules(ctx, chainName) + Expect(err).NotTo(HaveOccurred()) + Expect(rules).To(HaveLen(4)) // 2 IPs from eth0 + 2 IPs from eth1 + + // Verify rule content + cmd := nft.(*knftables.Fake).Dump() + Expect(cmd).To(ContainSubstring("iifname eth0 ip saddr 192.168.1.10 accept")) + Expect(cmd).To(ContainSubstring("iifname eth0 ip6 saddr 2001:db8::1 accept")) + Expect(cmd).To(ContainSubstring("iifname eth1 ip saddr 10.0.0.1 accept")) + Expect(cmd).To(ContainSubstring("iifname eth1 ip6 saddr 2001:db8:1::1 accept")) + }) + }) + + Context("when interfaces have empty IP lists", func() { + It("should not create any rules for interfaces with empty IP lists", func() { + chainName := "test-policy-chain" + createTableAndPolicyChain(chainName) + + matchedInterfaces := []Interface{ + { + Name: "eth0", + Network: "default/macvlan1", + IPs: []string{}, // Empty IP list + }, + { + Name: "eth1", + Network: "default/macvlan2", + IPs: nil, // Nil IP list + }, + } + + tx := nft.NewTransaction() + createReverseRules(tx, matchedInterfaces, chainName, logger) + + // Run the transaction + err := nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + // Verify no rules were created + rules, err := nft.ListRules(ctx, chainName) + Expect(err).NotTo(HaveOccurred()) + Expect(rules).To(BeEmpty()) + }) + }) + + Context("when interfaces have invalid IP addresses", func() { + It("should handle invalid IP addresses gracefully", func() { + chainName := "test-policy-chain" + createTableAndPolicyChain(chainName) + + matchedInterfaces := []Interface{ + { + Name: "eth0", + Network: "default/macvlan1", + IPs: []string{"192.168.1.10", "invalid-ip", "2001:db8::1"}, + }, + } + + tx := nft.NewTransaction() + createReverseRules(tx, matchedInterfaces, chainName, logger) + + // Run the transaction + err := nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + // Verify only valid IPs created rules + rules, err := nft.ListRules(ctx, chainName) + Expect(err).NotTo(HaveOccurred()) + Expect(rules).To(HaveLen(2)) // Only valid IPs + + // Verify rule content + cmd := nft.(*knftables.Fake).Dump() + Expect(cmd).To(ContainSubstring("iifname eth0 ip saddr 192.168.1.10 accept")) + Expect(cmd).To(ContainSubstring("iifname eth0 ip6 saddr 2001:db8::1 accept")) + Expect(cmd).NotTo(ContainSubstring("invalid-ip")) + }) + }) + + Context("when no interfaces are provided", func() { + It("should not create any rules", func() { + chainName := "test-policy-chain" + createTableAndPolicyChain(chainName) + + matchedInterfaces := []Interface{} + + tx := nft.NewTransaction() + createReverseRules(tx, matchedInterfaces, chainName, logger) + + // Run the transaction + err := nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + // Verify no rules were created + rules, err := nft.ListRules(ctx, chainName) + Expect(err).NotTo(HaveOccurred()) + Expect(rules).To(BeEmpty()) + }) + }) + + Context("when interfaces have special characters in names", func() { + It("should handle special characters in interface names", func() { + chainName := "test-policy-chain" + createTableAndPolicyChain(chainName) + + matchedInterfaces := []Interface{ + { + Name: "eth0.100", // VLAN interface + Network: "default/macvlan1", + IPs: []string{"192.168.1.10"}, + }, + { + Name: "bond0", // Bond interface + Network: "default/macvlan2", + IPs: []string{"192.168.2.10"}, + }, + } + + tx := nft.NewTransaction() + createReverseRules(tx, matchedInterfaces, chainName, logger) + + // Run the transaction + err := nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + // Verify rules were created + rules, err := nft.ListRules(ctx, chainName) + Expect(err).NotTo(HaveOccurred()) + Expect(rules).To(HaveLen(2)) + + // Verify rule content + cmd := nft.(*knftables.Fake).Dump() + Expect(cmd).To(ContainSubstring("iifname eth0.100 ip saddr 192.168.1.10 accept")) + Expect(cmd).To(ContainSubstring("iifname bond0 ip saddr 192.168.2.10 accept")) + }) + }) + + Context("when interfaces have duplicate IPs", func() { + It("should create rules for duplicate IPs on different interfaces", func() { + chainName := "test-policy-chain" + createTableAndPolicyChain(chainName) + + matchedInterfaces := []Interface{ + { + Name: "eth0", + Network: "default/macvlan1", + IPs: []string{"192.168.1.10"}, + }, + { + Name: "eth1", + Network: "default/macvlan2", + IPs: []string{"192.168.1.10"}, // Same IP on different interface + }, + } + + tx := nft.NewTransaction() + createReverseRules(tx, matchedInterfaces, chainName, logger) + + // Run the transaction + err := nft.Run(ctx, tx) + Expect(err).NotTo(HaveOccurred()) + + // Verify rules were created for both interfaces + rules, err := nft.ListRules(ctx, chainName) + Expect(err).NotTo(HaveOccurred()) + Expect(rules).To(HaveLen(2)) + + // Verify rule content + cmd := nft.(*knftables.Fake).Dump() + Expect(cmd).To(ContainSubstring("iifname eth0 ip saddr 192.168.1.10 accept")) + Expect(cmd).To(ContainSubstring("iifname eth1 ip saddr 192.168.1.10 accept")) + }) + }) + }) +}) diff --git a/pkg/server/doc.go b/pkg/server/doc.go deleted file mode 100644 index 56364f28..00000000 --- a/pkg/server/doc.go +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) 2021 Multus Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package server is the package that contains server functions. -package server diff --git a/pkg/server/options.go b/pkg/server/options.go deleted file mode 100644 index 84222f9d..00000000 --- a/pkg/server/options.go +++ /dev/null @@ -1,199 +0,0 @@ -/* -Copyright 2020 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package server - -import ( - "bufio" - "flag" - "net" - "os" - "strings" - - "github.com/k8snetworkplumbingwg/multi-networkpolicy-iptables/pkg/controllers" - "github.com/spf13/pflag" - - nodeutil "k8s.io/component-helpers/node/util" - "k8s.io/klog" -) - -// Options stores option for the command -type Options struct { - // kubeconfig is the path to a KubeConfig file. - Kubeconfig string - // master is used to override the kubeconfig's URL to the apiserver - master string - hostnameOverride string - hostPrefix string - containerRuntime controllers.RuntimeKind - containerRuntimeEndpoint string - networkPlugins []string - podIptables string - syncPeriod int - acceptICMPv6 bool - acceptICMP bool - allowIPv6SrcPrefixText string - allowIPv6DstPrefixText string - customIPv4IngressRuleFile string - customIPv4EgressRuleFile string - customIPv6IngressRuleFile string - customIPv6EgressRuleFile string - - // updated by command line parsing - allowIPv6SrcPrefix []string - allowIPv6DstPrefix []string - customIPv4IngressRule []string - customIPv4EgressRule []string - customIPv6IngressRule []string - customIPv6EgressRule []string - // stopCh is used to stop the command - stopCh chan struct{} -} - -// AddFlags adds command line flags into command -func (o *Options) AddFlags(fs *pflag.FlagSet) { - klog.InitFlags(nil) - fs.SortFlags = false - fs.Var(&o.containerRuntime, "container-runtime", "Container runtime using for the cluster. Possible values: 'cri'. ") - fs.StringVar(&o.containerRuntimeEndpoint, "container-runtime-endpoint", o.containerRuntimeEndpoint, "Path to cri socket.") - fs.StringVar(&o.Kubeconfig, "kubeconfig", o.Kubeconfig, "Path to kubeconfig file with authorization information (the master location is set by the master flag).") - fs.StringVar(&o.master, "master", o.master, "The address of the Kubernetes API server (overrides any value in kubeconfig)") - fs.StringVar(&o.hostnameOverride, "hostname-override", o.hostnameOverride, "If non-empty, will use this string as identification instead of the actual hostname.") - fs.StringVar(&o.hostPrefix, "host-prefix", o.hostPrefix, "If non-empty, will use this string as prefix for host filesystem.") - fs.StringSliceVar(&o.networkPlugins, "network-plugins", []string{"macvlan"}, "List of network plugins to be be considered for network policies.") - fs.StringVar(&o.podIptables, "pod-iptables", o.podIptables, "If non-empty, will use this path to store pod's iptables for troubleshooting helper.") - fs.IntVar(&o.syncPeriod, "sync-period", defaultSyncPeriod, "sync period for multi-networkpolicy syncRunner") - fs.BoolVar(&o.acceptICMP, "accept-icmp", false, "accept all ICMP traffic") - fs.BoolVar(&o.acceptICMPv6, "accept-icmpv6", false, "accept all ICMPv6 traffic") - fs.StringVar(&o.allowIPv6SrcPrefixText, "allow-ipv6-src-prefix", "", "Accept source IPv6 prefix list, comma separated (e.g. \"fe80::/10\")") - fs.StringVar(&o.allowIPv6DstPrefixText, "allow-ipv6-dst-prefix", "", "Accept destination IPv6 prefix list, comma separated (e.g. \"fe80:/10,ff00::/8\")") - fs.StringVar(&o.customIPv4IngressRuleFile, "custom-v4-ingress-rule-file", "", "custom rule file for IPv4 ingress") - fs.StringVar(&o.customIPv4EgressRuleFile, "custom-v4-egress-rule-file", "", "custom rule file for IPv4 egress") - fs.StringVar(&o.customIPv6IngressRuleFile, "custom-v6-ingress-rule-file", "", "custom rule file for IPv6 ingress") - fs.StringVar(&o.customIPv6EgressRuleFile, "custom-v6-egress-rule-file", "", "custom rule file for IPv6 egress") - fs.AddGoFlagSet(flag.CommandLine) -} - -func parseCustomRuleFile(filename string, rules *[]string) error { - if filename != "" { - *rules = []string{} - fp, err := os.Open(filename) - if err != nil { - return err - } - defer fp.Close() - - scanner := bufio.NewScanner(fp) - for scanner.Scan() { - rule := scanner.Text() - if strings.HasPrefix(rule, "#") { // skip rule begin with '#' - continue - } - *rules = append(*rules, rule) - } - - if err := scanner.Err(); err != nil { - return err - } - } - return nil -} - -func parseIPPrefixText(prefixText string, prefixList *[]string) error { - if prefixText != "" { - *prefixList = []string{} - for _, addrRaw := range strings.Split(prefixText, ",") { - addr := strings.TrimSpace(addrRaw) - _, _, err := net.ParseCIDR(addr) - if err != nil { - return err - } - *prefixList = append(*prefixList, addr) - } - } - return nil -} - -// Validate checks several options and fill processed value -func (o *Options) Validate() error { - - // Validate IPv6 source prefix list - if err := parseIPPrefixText(o.allowIPv6SrcPrefixText, &o.allowIPv6SrcPrefix); err != nil { - return err - } - - // Validate IPv6 destination prefix list - if err := parseIPPrefixText(o.allowIPv6DstPrefixText, &o.allowIPv6DstPrefix); err != nil { - return err - } - - // Validate v4 ingress rules - if err := parseCustomRuleFile(o.customIPv4IngressRuleFile, &o.customIPv4IngressRule); err != nil { - return err - } - - // Validate v4 engress rules - if err := parseCustomRuleFile(o.customIPv4EgressRuleFile, &o.customIPv4EgressRule); err != nil { - return err - } - - // Validate v6 ingress rules - if err := parseCustomRuleFile(o.customIPv6IngressRuleFile, &o.customIPv6IngressRule); err != nil { - return err - } - - // Validate v6 engress rules - err := parseCustomRuleFile(o.customIPv6EgressRuleFile, &o.customIPv6EgressRule) - return err -} - -// Run invokes server -func (o *Options) Run() error { - server, err := NewServer(o) - if err != nil { - return err - } - - hostname, err := nodeutil.GetHostname(o.hostnameOverride) - if err != nil { - return err - } - klog.Infof("hostname: %v", hostname) - klog.Infof("container-runtime: %v", o.containerRuntime) - - // validate option and update it (check v6prefix list) - err = o.Validate() - if err != nil { - return err - } - - server.Run(hostname, o.stopCh) - - return nil -} - -// Stop halts the command -func (o *Options) Stop() { - o.stopCh <- struct{}{} -} - -// NewOptions initializes Options -func NewOptions() *Options { - return &Options{ - containerRuntime: controllers.Cri, - stopCh: make(chan struct{}), - } -} diff --git a/pkg/server/policyrules.go b/pkg/server/policyrules.go deleted file mode 100644 index 6829a026..00000000 --- a/pkg/server/policyrules.go +++ /dev/null @@ -1,715 +0,0 @@ -/* -Copyright 2020 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package server - -import ( - "bytes" - "fmt" - "os" - "strings" - - "github.com/k8snetworkplumbingwg/multi-networkpolicy-iptables/pkg/controllers" - multiv1beta1 "github.com/k8snetworkplumbingwg/multi-networkpolicy/pkg/apis/k8s.cni.cncf.io/v1beta1" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/klog" - utiliptables "k8s.io/kubernetes/pkg/util/iptables" -) - -// PolicyNetworkAnnotation is annotation for multiNetworkPolicy, -// to specify which networks(i.e. net-attach-def) are the targets -// of the policy -const PolicyNetworkAnnotation = "k8s.v1.cni.cncf.io/policy-for" - -/* -// GetChainLines parses a table's iptables-save data to find chains in the table. -// It returns a map of iptables.Chain to []byte where the []byte is the chain line -// from save (with counters etc.). -// Note that to avoid allocations memory is SHARED with save. -func GetChainLines(table Table, save []byte) map[Chain][]byte { -*/ -type iptableBuffer struct { - currentFilter map[utiliptables.Chain]struct{} - currentChain map[utiliptables.Chain]bool - activeChain map[utiliptables.Chain]bool - policyCommon *bytes.Buffer - policyIndex *bytes.Buffer - ingressPorts *bytes.Buffer - ingressFrom *bytes.Buffer - egressPorts *bytes.Buffer - egressTo *bytes.Buffer - filterChains *bytes.Buffer - filterRules *bytes.Buffer - isIPv6 bool -} - -func newIptableBuffer() *iptableBuffer { - buf := &iptableBuffer{ - currentFilter: make(map[utiliptables.Chain]struct{}), - policyCommon: bytes.NewBuffer(nil), - policyIndex: bytes.NewBuffer(nil), - ingressPorts: bytes.NewBuffer(nil), - ingressFrom: bytes.NewBuffer(nil), - egressPorts: bytes.NewBuffer(nil), - egressTo: bytes.NewBuffer(nil), - filterChains: bytes.NewBuffer(nil), - filterRules: bytes.NewBuffer(nil), - currentChain: map[utiliptables.Chain]bool{}, - activeChain: map[utiliptables.Chain]bool{}, - } - return buf -} - -func (ipt *iptableBuffer) Init(iptables utiliptables.Interface) { - ipt.isIPv6 = iptables.IsIPv6() - - tmpbuf := bytes.NewBuffer(nil) - tmpbuf.Reset() - err := iptables.SaveInto(utiliptables.TableFilter, tmpbuf) - if err != nil { - klog.Errorf("failed to get iptable filter: %v", err) - return - } - chainsFromTable := utiliptables.GetChainsFromTable(tmpbuf.Bytes()) - ipt.currentFilter = make(map[utiliptables.Chain]struct{}) - for k := range chainsFromTable { - ipt.currentFilter[k] = struct{}{} - } - - for k := range ipt.currentFilter { - if strings.HasPrefix(string(k), "MULTI-") { - ipt.currentChain[k] = true - } - } - - ipt.filterRules.Reset() - ipt.filterChains.Reset() - writeLine(ipt.filterChains, "*filter") - - // Make sure we keep stats for the top-level chains, if they existed - // (which most should have because we created them above). - for _, chainName := range []utiliptables.Chain{ingressChain, ingressCommonChain, egressChain, egressCommonChain} { - ipt.activeChain[chainName] = true - if _, ok := ipt.currentFilter[chainName]; ok { - writeBytesLine(ipt.filterChains, fmt.Sprintf(":%s - [0:0]", chainName)) - } else { - writeLine(ipt.filterChains, utiliptables.MakeChainLine(chainName)) - } - } -} - -// Reset clears iptableBuffer -func (ipt *iptableBuffer) Reset() { - ipt.policyCommon.Reset() - ipt.policyIndex.Reset() - ipt.ingressPorts.Reset() - ipt.ingressFrom.Reset() - ipt.egressPorts.Reset() - ipt.egressTo.Reset() -} - -func (ipt *iptableBuffer) FinalizeRules() { - for k := range ipt.activeChain { - delete(ipt.currentChain, k) - } - for chainName := range ipt.currentChain { - if _, ok := ipt.currentFilter[chainName]; ok { - writeBytesLine(ipt.filterChains, fmt.Sprintf(":%s - [0:0]", chainName)) - } - writeLine(ipt.policyIndex, "-X", string(chainName)) - } - ipt.filterRules.Write(ipt.filterChains.Bytes()) - ipt.filterRules.Write(ipt.policyCommon.Bytes()) - ipt.filterRules.Write(ipt.policyIndex.Bytes()) - ipt.filterRules.Write(ipt.ingressPorts.Bytes()) - ipt.filterRules.Write(ipt.ingressFrom.Bytes()) - ipt.filterRules.Write(ipt.egressPorts.Bytes()) - ipt.filterRules.Write(ipt.egressTo.Bytes()) - writeLine(ipt.filterRules, "COMMIT") -} - -func (ipt *iptableBuffer) SaveRules(path string) error { - file, err := os.Create(path) - defer file.Close() - if err != nil { - return err - } - //_, err = ipt.filterRules.WriteTo(file) - fmt.Fprintf(file, "%s", ipt.filterRules.String()) - return err -} - -func (ipt *iptableBuffer) SyncRules(iptables utiliptables.Interface) error { - if klog.V(4) { - klog.Infof("SyncRules\n%s\n", ipt.filterRules.String()) - } - return iptables.RestoreAll(ipt.filterRules.Bytes(), utiliptables.NoFlushTables, utiliptables.RestoreCounters) -} - -func (ipt *iptableBuffer) IsUsed() bool { - return (len(ipt.activeChain) != 0) -} - -func (ipt *iptableBuffer) CreateFilterChain(chainName string) { - ipt.activeChain[utiliptables.Chain(chainName)] = true - // Create chain if not exists - if _, ok := ipt.currentFilter[utiliptables.Chain(chainName)]; ok { - writeBytesLine(ipt.filterChains, fmt.Sprintf(":%s - [0:0]", chainName)) - } else { - writeLine(ipt.filterChains, utiliptables.MakeChainLine(utiliptables.Chain(chainName))) - } -} - -func (ipt *iptableBuffer) renderIngressCommon(s *Server) { - // Add jump from MULTI-INGRESS - writeLine(ipt.policyIndex, "-A", ingressChain, "-j", ingressCommonChain) - - if ipt.isIPv6 { - if s.Options.acceptICMPv6 { - // Allow incoming ICMPv6 traffic - writeLine(ipt.policyCommon, "-A", ingressCommonChain, "-p icmpv6 -j ACCEPT") - } - - // add source prefix whitelist - if len(s.Options.allowIPv6SrcPrefix) != 0 { - for _, addr := range s.Options.allowIPv6SrcPrefix { - writeLine(ipt.policyCommon, "-A", ingressCommonChain, - "-s", strings.TrimSpace(addr), "-j ACCEPT") - } - } - - // add destination prefix whitelist - if len(s.Options.allowIPv6DstPrefix) != 0 { - for _, addr := range s.Options.allowIPv6DstPrefix { - writeLine(ipt.policyCommon, "-A", ingressCommonChain, - "-d", strings.TrimSpace(addr), "-j ACCEPT") - } - } - - // add custom rules - if s.Options.customIPv6IngressRule != nil { - for _, rule := range s.Options.customIPv6IngressRule { - writeLine(ipt.policyCommon, "-A", ingressCommonChain, rule) - } - } - } else { // IPv4 - if s.Options.acceptICMP { - // Allow incoming ICMPv6 traffic to let Neighbor Discovery Protocol work (RFC4861) - writeLine(ipt.policyCommon, "-A", ingressCommonChain, "-p icmp -j ACCEPT") - } - - // add custom rules - if s.Options.customIPv4IngressRule != nil { - for _, rule := range s.Options.customIPv4IngressRule { - writeLine(ipt.policyCommon, "-A", ingressCommonChain, rule) - } - } - } - writeLine(ipt.policyCommon, "-A", ingressCommonChain, "-m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT") -} - -func (ipt *iptableBuffer) renderIngress(s *Server, podInfo *controllers.PodInfo, idx int, policy *multiv1beta1.MultiNetworkPolicy, policyNetworks []string) { - chainName := fmt.Sprintf("MULTI-%d-INGRESS", idx) - ipt.CreateFilterChain(chainName) - - for _, podIntf := range podInfo.Interfaces { - if podIntf.CheckPolicyNetwork(policyNetworks) { - comment := fmt.Sprintf("\"policy:%s net-attach-def:%s\"", policy.Name, podIntf.NetattachName) - writeLine(ipt.policyIndex, "-A", ingressChain, - "-m", "comment", "--comment", comment, "-i", podIntf.InterfaceName, - "-j", chainName) - writeLine(ipt.policyIndex, "-A", ingressChain, - "-m", "mark", "--mark", "0x30000/0x30000", "-j", "RETURN") - } - } - - for n, ingress := range policy.Spec.Ingress { - writeLine(ipt.policyIndex, "-A", chainName, - "-j", "MARK", "--set-xmark 0x0/0x30000") - ipt.renderIngressPorts(s, podInfo, idx, n, ingress.Ports, policyNetworks) - ipt.renderIngressFrom(s, podInfo, idx, n, ingress.From, policyNetworks) - writeLine(ipt.policyIndex, "-A", chainName, "-m", "mark", "--mark", "0x30000/0x30000", "-j", "RETURN") - } -} - -func (ipt *iptableBuffer) renderIngressPorts(_ *Server, podInfo *controllers.PodInfo, pIndex, iIndex int, ports []multiv1beta1.MultiNetworkPolicyPort, policyNetworks []string) { - chainName := fmt.Sprintf("MULTI-%d-INGRESS-%d-PORTS", pIndex, iIndex) - ipt.CreateFilterChain(chainName) - - // Add jump from MULTI-INGRESS - writeLine(ipt.policyIndex, "-A", fmt.Sprintf("MULTI-%d-INGRESS", pIndex), "-j", chainName) - - validPorts := 0 - for _, port := range ports { - proto := renderProtocol(port.Protocol) - - for _, podIntf := range podInfo.Interfaces { - if !podIntf.CheckPolicyNetwork(policyNetworks) { - continue - } - - dport := "" - if port.Port != nil { - dport = "--dport " + port.Port.String() - if port.EndPort != nil { - dport = fmt.Sprintf("--dport %s:%d", port.Port.String(), *port.EndPort) - } - } - - writeLine(ipt.ingressPorts, "-A", chainName, - "-i", podIntf.InterfaceName, - "-m", proto, "-p", proto, dport, - "-j", "MARK", "--set-xmark", "0x10000/0x10000") - - validPorts++ - } - } - - // Add skip rule if no ports - if len(ports) == 0 || validPorts == 0 { - writeLine(ipt.ingressPorts, "-A", chainName, - "-m", "comment", "--comment", "\"no ingress ports, skipped\"", - "-j", "MARK", "--set-xmark", "0x10000/0x10000") - } -} - -func (ipt *iptableBuffer) renderIngressFrom(s *Server, podInfo *controllers.PodInfo, pIndex, iIndex int, from []multiv1beta1.MultiNetworkPolicyPeer, policyNetworks []string) { - chainName := fmt.Sprintf("MULTI-%d-INGRESS-%d-FROM", pIndex, iIndex) - ipt.CreateFilterChain(chainName) - - // Add jump from MULTI-INGRESS - writeLine(ipt.policyIndex, "-A", fmt.Sprintf("MULTI-%d-INGRESS", pIndex), "-j", chainName) - - s.podMap.Update(s.podChanges) - for _, peer := range from { - if peer.IPBlock != nil { - ipt.renderIngressFromIPBlock(podInfo, chainName, peer, policyNetworks) - continue - } - - if peer.PodSelector != nil || peer.NamespaceSelector != nil { - ipt.renderIngressFromSelector(s, podInfo, chainName, peer, policyNetworks) - continue - } - - klog.Errorf("unknown rule: %+v", peer) - } - - // Add skip rule if no froms - if len(from) == 0 { - writeLine(ipt.ingressFrom, "-A", chainName, - "-m", "comment", "--comment", "\"no ingress from, skipped\"", - "-j", "MARK", "--set-xmark", "0x20000/0x20000") - } -} - -func (ipt *iptableBuffer) renderIngressFromSelector(s *Server, podInfo *controllers.PodInfo, chainName string, peer multiv1beta1.MultiNetworkPolicyPeer, policyNetworks []string) { - podSelectorMap, err := metav1.LabelSelectorAsMap(peer.PodSelector) - if err != nil { - klog.Errorf("pod selector: %v", err) - return - } - podLabelSelector := labels.Set(podSelectorMap).AsSelectorPreValidated() - pods, err := s.podLister.Pods(metav1.NamespaceAll).List(podLabelSelector) - if err != nil { - klog.Errorf("pod list failed:%v", err) - return - } - - var nsSelector labels.Selector - if peer.NamespaceSelector != nil { - nsSelectorMap, err := metav1.LabelSelectorAsMap(peer.NamespaceSelector) - if err != nil { - klog.Errorf("namespace selector: %v", err) - return - } - nsSelector = labels.Set(nsSelectorMap).AsSelectorPreValidated() - } - s.namespaceMap.Update(s.nsChanges) - - for _, sPod := range pods { - nsLabels, err := s.namespaceMap.GetNamespaceInfo(sPod.Namespace) - if err != nil { - klog.Errorf("cannot get namespace info: %v %v", sPod.ObjectMeta.Name, err) - continue - } - if nsSelector != nil && !nsSelector.Matches(labels.Set(nsLabels.Labels)) { - continue - } - s.podMap.Update(s.podChanges) - sPodinfo, err := s.podMap.GetPodInfo(sPod) - if err != nil { - klog.Errorf("cannot get %s/%s podInfo: %v", sPod.Namespace, sPod.Name, err) - continue - } - for _, podIntf := range podInfo.Interfaces { - if !podIntf.CheckPolicyNetwork(policyNetworks) { - continue - } - for _, sPodIntf := range sPodinfo.Interfaces { - if !sPodIntf.CheckPolicyNetwork(policyNetworks) { - continue - } - for _, ip := range sPodIntf.IPs { - if ipt.isIPFamilyCompatible(ip) { - writeLine(ipt.ingressFrom, "-A", chainName, - "-i", podIntf.InterfaceName, "-s", ip, - "-j", "MARK", "--set-xmark", "0x20000/0x20000") - } - } - // ingress should accept reverse path - for _, ip := range podIntf.IPs { - if ipt.isIPFamilyCompatible(ip) { - writeLine(ipt.ingressFrom, "-A", chainName, - "-i", podIntf.InterfaceName, "-s", ip, - "-j", "MARK", "--set-xmark", "0x20000/0x20000") - } - } - } - } - } -} - -func (ipt *iptableBuffer) renderIngressFromIPBlock(podInfo *controllers.PodInfo, chainName string, peer multiv1beta1.MultiNetworkPolicyPeer, policyNetworks []string) { - for _, except := range peer.IPBlock.Except { - for _, podIntf := range podInfo.Interfaces { - if !podIntf.CheckPolicyNetwork(policyNetworks) { - continue - } - if ipt.isIPFamilyCompatible(except) { - writeLine(ipt.ingressFrom, "-A", chainName, - "-i", podIntf.InterfaceName, "-s", except, "-j", "DROP") - } - } - } - for _, podIntf := range podInfo.Interfaces { - if !podIntf.CheckPolicyNetwork(policyNetworks) { - continue - } - if ipt.isIPFamilyCompatible(peer.IPBlock.CIDR) { - writeLine(ipt.ingressFrom, "-A", chainName, - "-i", podIntf.InterfaceName, "-s", peer.IPBlock.CIDR, - "-j", "MARK", "--set-xmark", "0x20000/0x20000") - } - } - for _, podIntf := range podInfo.Interfaces { - if !podIntf.CheckPolicyNetwork(policyNetworks) { - continue - } - for _, ip := range podIntf.IPs { - if ipt.isIPFamilyCompatible(ip) { - writeLine(ipt.ingressFrom, "-A", chainName, - "-i", podIntf.InterfaceName, "-s", ip, - "-j", "MARK", "--set-xmark", "0x20000/0x20000") - } - } - } -} - -func (ipt *iptableBuffer) renderEgressCommon(s *Server) { - // Add jump from MULTI-EGRESS - writeLine(ipt.policyIndex, "-A", egressChain, "-j", egressCommonChain) - if ipt.isIPv6 { - if s.Options.acceptICMPv6 { - // Allow outgoing ICMPv6 traffic - writeLine(ipt.policyCommon, "-A", egressCommonChain, "-p icmpv6 -j ACCEPT") - } - - // add source prefix whitelist - if s.Options.allowIPv6SrcPrefix != nil { - for _, addr := range s.Options.allowIPv6SrcPrefix { - writeLine(ipt.policyCommon, "-A", egressCommonChain, - "-s", strings.TrimSpace(addr), "-j ACCEPT") - } - } - - // add destination prefix whitelist - if s.Options.allowIPv6DstPrefix != nil { - for _, addr := range s.Options.allowIPv6DstPrefix { - writeLine(ipt.policyCommon, "-A", egressCommonChain, - "-d", strings.TrimSpace(addr), "-j ACCEPT") - } - } - - // add custom rules - if s.Options.customIPv6EgressRule != nil { - for _, rule := range s.Options.customIPv6EgressRule { - writeLine(ipt.policyCommon, "-A", egressCommonChain, rule) - } - } - } else { // IPv4 - if s.Options.acceptICMP { - // Allow outgoing ICMP traffic - writeLine(ipt.policyCommon, "-A", egressCommonChain, "-p icmp -j ACCEPT") - } - - // add custom rules - if s.Options.customIPv4EgressRule != nil { - for _, rule := range s.Options.customIPv4EgressRule { - writeLine(ipt.policyCommon, "-A", egressCommonChain, rule) - } - } - } - - writeLine(ipt.policyCommon, "-A", egressCommonChain, "-m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT") -} - -func (ipt *iptableBuffer) renderEgress(s *Server, podInfo *controllers.PodInfo, idx int, policy *multiv1beta1.MultiNetworkPolicy, policyNetworks []string) { - chainName := fmt.Sprintf("MULTI-%d-EGRESS", idx) - ipt.CreateFilterChain(chainName) - - for _, podIntf := range podInfo.Interfaces { - if podIntf.CheckPolicyNetwork(policyNetworks) { - comment := fmt.Sprintf("\"policy:%s net-attach-def:%s\"", policy.Name, podIntf.NetattachName) - writeLine(ipt.policyIndex, "-A", egressChain, - "-m", "comment", "--comment", comment, "-o", podIntf.InterfaceName, - "-j", chainName) - writeLine(ipt.policyIndex, "-A", egressChain, - "-m", "mark", "--mark", "0x30000/0x30000", "-j", "RETURN") - } - } - - for n, egress := range policy.Spec.Egress { - writeLine(ipt.policyIndex, "-A", chainName, "-j", "MARK", "--set-xmark 0x0/0x30000") - ipt.renderEgressPorts(s, podInfo, idx, n, egress.Ports, policyNetworks) - ipt.renderEgressTo(s, podInfo, idx, n, egress.To, policyNetworks) - writeLine(ipt.policyIndex, "-A", chainName, "-m", "mark", "--mark", "0x30000/0x30000", "-j", "RETURN") - } -} - -func (ipt *iptableBuffer) renderEgressPorts(_ *Server, podInfo *controllers.PodInfo, pIndex, iIndex int, ports []multiv1beta1.MultiNetworkPolicyPort, policyNetworks []string) { - chainName := fmt.Sprintf("MULTI-%d-EGRESS-%d-PORTS", pIndex, iIndex) - ipt.CreateFilterChain(chainName) - - // Add jump from MULTI-EGRESS - writeLine(ipt.policyIndex, "-A", fmt.Sprintf("MULTI-%d-EGRESS", pIndex), "-j", chainName) - - validPorts := 0 - for _, port := range ports { - proto := renderProtocol(port.Protocol) - - for _, podIntf := range podInfo.Interfaces { - if !podIntf.CheckPolicyNetwork(policyNetworks) { - continue - } - - dport := "" - if port.Port != nil { - dport = "--dport " + port.Port.String() - if port.EndPort != nil { - dport = fmt.Sprintf("--dport %s:%d", port.Port.String(), *port.EndPort) - } - } - - writeLine(ipt.egressPorts, "-A", chainName, - "-o", podIntf.InterfaceName, - "-m", proto, "-p", proto, dport, - "-j", "MARK", "--set-xmark", "0x10000/0x10000") - validPorts++ - } - } - - // Add skip rules if no ports - if len(ports) == 0 || validPorts == 0 { - writeLine(ipt.egressPorts, "-A", chainName, - "-m", "comment", "--comment", "\"no egress ports, skipped\"", - "-j", "MARK", "--set-xmark", "0x10000/0x10000") - } -} - -func (ipt *iptableBuffer) renderEgressTo(s *Server, podInfo *controllers.PodInfo, pIndex, iIndex int, to []multiv1beta1.MultiNetworkPolicyPeer, policyNetworks []string) { - chainName := fmt.Sprintf("MULTI-%d-EGRESS-%d-TO", pIndex, iIndex) - ipt.CreateFilterChain(chainName) - - // Add jump from MULTI-EGRESS - writeLine(ipt.policyIndex, "-A", fmt.Sprintf("MULTI-%d-EGRESS", pIndex), "-j", chainName) - - s.podMap.Update(s.podChanges) - for _, peer := range to { - if peer.IPBlock != nil { - ipt.renderEgressToIPBlock(podInfo, chainName, peer, policyNetworks) - continue - } - - if peer.PodSelector != nil || peer.NamespaceSelector != nil { - ipt.renderEgressToSelector(s, podInfo, chainName, peer, policyNetworks) - continue - } - - klog.Errorf("unknown rule: %+v", peer) - } - - // Add skip rules if no to - if len(to) == 0 { - writeLine(ipt.egressTo, "-A", chainName, - "-m", "comment", "--comment", "\"no egress to, skipped\"", - "-j", "MARK", "--set-xmark", "0x20000/0x20000") - } -} - -func (ipt *iptableBuffer) renderEgressToSelector(s *Server, podInfo *controllers.PodInfo, chainName string, peer multiv1beta1.MultiNetworkPolicyPeer, policyNetworks []string) { - podSelectorMap, err := metav1.LabelSelectorAsMap(peer.PodSelector) - if err != nil { - klog.Errorf("pod selector: %v", err) - return - } - podLabelSelector := labels.Set(podSelectorMap).AsSelectorPreValidated() - pods, err := s.podLister.Pods(metav1.NamespaceAll).List(podLabelSelector) - if err != nil { - klog.Errorf("pod list failed:%v", err) - return - } - - var nsSelector labels.Selector - if peer.NamespaceSelector != nil { - nsSelectorMap, err := metav1.LabelSelectorAsMap(peer.NamespaceSelector) - if err != nil { - klog.Errorf("namespace selector: %v", err) - return - } - nsSelector = labels.Set(nsSelectorMap).AsSelectorPreValidated() - } - s.namespaceMap.Update(s.nsChanges) - s.podMap.Update(s.podChanges) - - for _, sPod := range pods { - nsLabels, err := s.namespaceMap.GetNamespaceInfo(sPod.Namespace) - if err != nil { - klog.Errorf("cannot get namespace info: %v", err) - continue - } - if nsSelector != nil && !nsSelector.Matches(labels.Set(nsLabels.Labels)) { - continue - } - s.podMap.Update(s.podChanges) - sPodinfo, err := s.podMap.GetPodInfo(sPod) - if err != nil { - klog.Errorf("cannot get %s/%s podInfo: %v", sPod.Namespace, sPod.Name, err) - continue - } - for _, podIntf := range podInfo.Interfaces { - if !podIntf.CheckPolicyNetwork(policyNetworks) { - continue - } - for _, sPodIntf := range sPodinfo.Interfaces { - if !sPodIntf.CheckPolicyNetwork(policyNetworks) { - continue - } - for _, ip := range sPodIntf.IPs { - if ipt.isIPFamilyCompatible(ip) { - writeLine(ipt.egressTo, "-A", chainName, - "-o", podIntf.InterfaceName, "-d", ip, - "-j", "MARK", "--set-xmark", "0x20000/0x20000") - } - } - // egress should accept reverse path - for _, ip := range podIntf.IPs { - if ipt.isIPFamilyCompatible(ip) { - writeLine(ipt.egressTo, "-A", chainName, - "-o", podIntf.InterfaceName, "-d", ip, - "-j", "MARK", "--set-xmark", "0x20000/0x20000") - } - } - } - } - } -} - -func (ipt *iptableBuffer) renderEgressToIPBlock(podInfo *controllers.PodInfo, chainName string, peer multiv1beta1.MultiNetworkPolicyPeer, policyNetworks []string) { - for _, except := range peer.IPBlock.Except { - for _, multi := range podInfo.Interfaces { - if !multi.CheckPolicyNetwork(policyNetworks) { - continue - } - if ipt.isIPFamilyCompatible(except) { - writeLine(ipt.egressTo, "-A", chainName, - "-o", multi.InterfaceName, "-d", except, "-j", "DROP") - } - } - } - for _, podIntf := range podInfo.Interfaces { - if !podIntf.CheckPolicyNetwork(policyNetworks) { - continue - } - if ipt.isIPFamilyCompatible(peer.IPBlock.CIDR) { - writeLine(ipt.egressTo, "-A", chainName, - "-o", podIntf.InterfaceName, "-d", peer.IPBlock.CIDR, - "-j", "MARK", "--set-xmark", "0x20000/0x20000") - } - } - // egress should accept reverse path - for _, podIntf := range podInfo.Interfaces { - if !podIntf.CheckPolicyNetwork(policyNetworks) { - continue - } - for _, ip := range podIntf.IPs { - if ipt.isIPFamilyCompatible(ip) { - writeLine(ipt.egressTo, "-A", chainName, - "-o", podIntf.InterfaceName, "-d", ip, - "-j", "MARK", "--set-xmark", "0x20000/0x20000") - } - } - } -} - -func (ipt *iptableBuffer) isIPFamilyCompatible(ip string) bool { - if ipt.isIPv6 && isAddressIPv6(ip) { - return true - } - - if !ipt.isIPv6 && isAddressIPv4(ip) { - return true - } - - return false -} - -// Join all words with spaces, terminate with newline and write to buf. -func writeLine(buf *bytes.Buffer, words ...string) { - // We avoid strings.Join for performance reasons. - for i := range words { - buf.WriteString(words[i]) - if i < len(words)-1 { - buf.WriteByte(' ') - } else { - buf.WriteByte('\n') - } - } -} - -func writeBytesLine(buf *bytes.Buffer, str string) { - buf.Write([]byte(str)) - buf.WriteByte('\n') -} - -func renderProtocol(proto *v1.Protocol) string { - p := v1.ProtocolTCP - if proto != nil { - p = *proto - } - - return strings.ToLower(string(p)) -} - -func isAddressIPv6(ip string) bool { - return strings.Contains(ip, ":") -} - -func isAddressIPv4(ip string) bool { - return strings.Contains(ip, ".") -} diff --git a/pkg/server/policyrules_test.go b/pkg/server/policyrules_test.go deleted file mode 100644 index dbdd024e..00000000 --- a/pkg/server/policyrules_test.go +++ /dev/null @@ -1,2831 +0,0 @@ -/* -Copyright 2020 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package server - -import ( - "bytes" - "fmt" - "io/ioutil" - "os" - "path/filepath" - "time" - - "github.com/k8snetworkplumbingwg/multi-networkpolicy-iptables/pkg/controllers" - multiv1beta1 "github.com/k8snetworkplumbingwg/multi-networkpolicy/pkg/apis/k8s.cni.cncf.io/v1beta1" - multifake "github.com/k8snetworkplumbingwg/multi-networkpolicy/pkg/client/clientset/versioned/fake" - netdefv1 "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/apis/k8s.cni.cncf.io/v1" - netfake "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/client/clientset/versioned/fake" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/intstr" - "k8s.io/apimachinery/pkg/util/wait" - "k8s.io/client-go/informers" - k8sfake "k8s.io/client-go/kubernetes/fake" - utiliptables "k8s.io/kubernetes/pkg/util/iptables" - fakeiptables "k8s.io/kubernetes/pkg/util/iptables/testing" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -var informerFactory informers.SharedInformerFactory - -// NewFakeServer creates fake server object for unit-test -func NewFakeServer(hostname string) *Server { - fakeClient := k8sfake.NewSimpleClientset() - netClient := netfake.NewSimpleClientset() - policyClient := multifake.NewSimpleClientset() - - policyChanges := controllers.NewPolicyChangeTracker() - if policyChanges == nil { - return nil - } - netdefChanges := controllers.NewNetDefChangeTracker() - if netdefChanges == nil { - return nil - } - nsChanges := controllers.NewNamespaceChangeTracker() - if nsChanges == nil { - return nil - } - // expects that /var/run/containerd/containerd.sock, for docker/containerd - hostPrefix := "/" - networkPlugins := []string{"multi"} - containerRuntime := controllers.RuntimeKind(controllers.Cri) - podChanges := controllers.NewPodChangeTracker(containerRuntime, "/var/run/containerd/containerd.sock", hostname, hostPrefix, networkPlugins, netdefChanges) - if podChanges == nil { - return nil - } - informerFactory = informers.NewSharedInformerFactoryWithOptions(fakeClient, 15*time.Minute) - podConfig := controllers.NewPodConfig(informerFactory.Core().V1().Pods(), 15*time.Minute) - - nodeRef := &v1.ObjectReference{ - Kind: "Node", - Name: hostname, - UID: types.UID(hostname), - Namespace: "", - } - - server := &Server{ - Client: fakeClient, - Hostname: hostname, - NetworkPolicyClient: policyClient, - NetDefClient: netClient, - ConfigSyncPeriod: 15 * time.Minute, - NodeRef: nodeRef, - ip4Tables: fakeiptables.NewFake(), - ip6Tables: fakeiptables.NewIPv6Fake(), - Options: &Options{}, - - hostPrefix: hostPrefix, - policyChanges: policyChanges, - podChanges: podChanges, - netdefChanges: netdefChanges, - nsChanges: nsChanges, - podMap: make(controllers.PodMap), - policyMap: make(controllers.PolicyMap), - namespaceMap: make(controllers.NamespaceMap), - podLister: informerFactory.Core().V1().Pods().Lister(), - } - podConfig.RegisterEventHandler(server) - informerFactory.Start(wait.NeverStop) - go podConfig.Run(wait.NeverStop) - return server -} - -func NewFakePodWithNetAnnotation(namespace, name, annot, status string, labels map[string]string) *v1.Pod { - return &v1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: namespace, - Name: name, - UID: "testUID", - Annotations: map[string]string{ - "k8s.v1.cni.cncf.io/networks": annot, - netdefv1.NetworkStatusAnnot: status, - }, - Labels: labels, - }, - Spec: v1.PodSpec{ - Containers: []v1.Container{ - {Name: "ctr1", Image: "image"}, - }, - }, - Status: v1.PodStatus{ - Phase: v1.PodRunning, - }, - } -} - -func AddNamespace(s *Server, name string) { - namespace := &v1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Labels: map[string]string{ - "nsname": name, - }, - }, - } - Expect(s.nsChanges.Update(nil, namespace)).To(BeTrue()) - s.namespaceMap.Update(s.nsChanges) -} - -func AddPod(s *Server, pod *v1.Pod) { - Expect(s.podChanges.Update(nil, pod)).To(BeTrue()) - s.podMap.Update(s.podChanges) - informerFactory.Core().V1().Pods().Informer().GetIndexer().Add(pod) -} - -func NewFakeNetworkStatus(netns, netname, eth0, net1 string) string { - // dummy interface is for testing not to include dummy ip in iptable rules - baseStr := ` - [{ - "name": "", - "interface": "eth0", - "ips": [ - "%s" - ], - "mac": "aa:e1:20:71:15:01", - "default": true, - "dns": {} - },{ - "name": "%s/%s", - "interface": "net1", - "ips": [ - "%s" - ], - "mac": "42:90:65:12:3e:bf", - "dns": {} - },{ - "name": "dummy-interface", - "interface": "net2", - "ips": [ - "244.244.244.244" - ], - "mac": "42:90:65:12:3e:bf", - "dns": {} - }] -` - return fmt.Sprintf(baseStr, eth0, netns, netname, net1) -} - -func NewNetDef(namespace, name, cniConfig string) *netdefv1.NetworkAttachmentDefinition { - return &netdefv1.NetworkAttachmentDefinition{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: namespace, - Name: name, - }, - Spec: netdefv1.NetworkAttachmentDefinitionSpec{ - Config: cniConfig, - }, - } -} - -func NewCNIConfig(cniName, cniType string) string { - cniConfigTemp := ` - { - "name": "%s", - "type": "%s" - }` - return fmt.Sprintf(cniConfigTemp, cniName, cniType) -} - -func NewCNIConfigList(cniName, cniType string) string { - cniConfigTemp := ` - { - "name": "%s", - "plugins": [ - { - "type": "%s" - }] - }` - return fmt.Sprintf(cniConfigTemp, cniName, cniType) -} - -var _ = Describe("policyrules testing", func() { - var tmpDir string - - BeforeEach(func() { - var err error - tmpDir, err = ioutil.TempDir("", "multi-networkpolicy-iptables") - Expect(err).NotTo(HaveOccurred()) - }) - - AfterEach(func() { - err := os.RemoveAll(tmpDir) - Expect(err).NotTo(HaveOccurred()) - }) - - It("Initialization", func() { - ipt := fakeiptables.NewFake() - Expect(ipt).NotTo(BeNil()) - buf := newIptableBuffer() - Expect(buf).NotTo(BeNil()) - - // verify buf initialized at init - buf.Init(ipt) - filterChains := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] -` - Expect(buf.filterChains.String()).To(Equal(filterChains)) - Expect(buf.policyIndex.String()).To(Equal("")) - Expect(buf.ingressPorts.String()).To(Equal("")) - Expect(buf.ingressFrom.String()).To(Equal("")) - Expect(buf.egressPorts.String()).To(Equal("")) - Expect(buf.egressTo.String()).To(Equal("")) - - // finalize buf and verify rules buffer - buf.FinalizeRules() - filterRules := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] -COMMIT -` - Expect(buf.filterRules.String()).To(Equal(filterRules)) - - // sync and verify iptable - Expect(buf.SyncRules(ipt)).To(BeNil()) - iptableRules := bytes.NewBuffer(nil) - ipt.SaveInto(utiliptables.TableFilter, iptableRules) - tableRules := - `*filter -:INPUT - [0:0] -:FORWARD - [0:0] -:OUTPUT - [0:0] -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] -COMMIT -` - Expect(iptableRules.String()).To(Equal(tableRules)) - - // reset and verify empty - buf.Reset() - Expect(buf.policyIndex.String()).To(Equal("")) - Expect(buf.ingressPorts.String()).To(Equal("")) - Expect(buf.ingressFrom.String()).To(Equal("")) - Expect(buf.egressPorts.String()).To(Equal("")) - Expect(buf.egressTo.String()).To(Equal("")) - }) - - It("ingress common - default", func() { - buf4 := newIptableBuffer() - buf6 := newIptableBuffer() - Expect(buf4).NotTo(BeNil()) - Expect(buf6).NotTo(BeNil()) - - // verify buf initialized at init - s := NewFakeServer("samplehost") - Expect(s).NotTo(BeNil()) - - buf4.Init(s.ip4Tables) - buf6.Init(s.ip6Tables) - - // check IPv4 case - buf4.renderIngressCommon(s) - buf4.FinalizeRules() - finalizedRules4 := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] --A MULTI-INGRESS-COMMON -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT --A MULTI-INGRESS -j MULTI-INGRESS-COMMON -COMMIT -` - Expect(buf4.filterRules.String()).To(Equal(finalizedRules4)) - - // check IPv6 case - buf6.renderIngressCommon(s) - buf6.FinalizeRules() - finalizedRules6 := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] --A MULTI-INGRESS-COMMON -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT --A MULTI-INGRESS -j MULTI-INGRESS-COMMON -COMMIT -` - Expect(buf6.filterRules.String()).To(Equal(finalizedRules6)) - }) - - It("ingress common - icmp", func() { - buf4 := newIptableBuffer() - buf6 := newIptableBuffer() - Expect(buf4).NotTo(BeNil()) - Expect(buf6).NotTo(BeNil()) - - // verify buf initialized at init - s := NewFakeServer("samplehost") - Expect(s).NotTo(BeNil()) - s.Options.acceptICMP = true - - buf4.Init(s.ip4Tables) - buf6.Init(s.ip6Tables) - - // check IPv4 case - buf4.renderIngressCommon(s) - buf4.FinalizeRules() - finalizedRules4 := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] --A MULTI-INGRESS-COMMON -p icmp -j ACCEPT --A MULTI-INGRESS-COMMON -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT --A MULTI-INGRESS -j MULTI-INGRESS-COMMON -COMMIT -` - Expect(buf4.filterRules.String()).To(Equal(finalizedRules4)) - - // check IPv6 case - buf6.renderIngressCommon(s) - buf6.FinalizeRules() - finalizedRules6 := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] --A MULTI-INGRESS-COMMON -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT --A MULTI-INGRESS -j MULTI-INGRESS-COMMON -COMMIT -` - Expect(buf6.filterRules.String()).To(Equal(finalizedRules6)) - }) - - It("ingress common - icmpv6", func() { - buf4 := newIptableBuffer() - buf6 := newIptableBuffer() - Expect(buf4).NotTo(BeNil()) - Expect(buf6).NotTo(BeNil()) - - // verify buf initialized at init - s := NewFakeServer("samplehost") - Expect(s).NotTo(BeNil()) - s.Options.acceptICMPv6 = true - - buf4.Init(s.ip4Tables) - buf6.Init(s.ip6Tables) - - // check IPv4 case - buf4.renderIngressCommon(s) - buf4.FinalizeRules() - finalizedRules4 := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] --A MULTI-INGRESS-COMMON -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT --A MULTI-INGRESS -j MULTI-INGRESS-COMMON -COMMIT -` - Expect(buf4.filterRules.String()).To(Equal(finalizedRules4)) - - // check IPv6 case - buf6.renderIngressCommon(s) - buf6.FinalizeRules() - finalizedRules6 := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] --A MULTI-INGRESS-COMMON -p icmpv6 -j ACCEPT --A MULTI-INGRESS-COMMON -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT --A MULTI-INGRESS -j MULTI-INGRESS-COMMON -COMMIT -` - Expect(buf6.filterRules.String()).To(Equal(finalizedRules6)) - }) - - It("ingress common - allow src v6 prefix", func() { - buf4 := newIptableBuffer() - buf6 := newIptableBuffer() - Expect(buf4).NotTo(BeNil()) - Expect(buf6).NotTo(BeNil()) - - // verify buf initialized at init - s := NewFakeServer("samplehost") - Expect(s).NotTo(BeNil()) - s.Options.allowIPv6SrcPrefixText = "11::/8 , 22::/64" - err := s.Options.Validate() - Expect(err).NotTo(HaveOccurred()) - - buf4.Init(s.ip4Tables) - buf6.Init(s.ip6Tables) - - // check IPv4 case - buf4.renderIngressCommon(s) - buf4.FinalizeRules() - finalizedRules4 := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] --A MULTI-INGRESS-COMMON -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT --A MULTI-INGRESS -j MULTI-INGRESS-COMMON -COMMIT -` - Expect(buf4.filterRules.String()).To(Equal(finalizedRules4)) - - // check IPv6 case - buf6.renderIngressCommon(s) - buf6.FinalizeRules() - finalizedRules6 := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] --A MULTI-INGRESS-COMMON -s 11::/8 -j ACCEPT --A MULTI-INGRESS-COMMON -s 22::/64 -j ACCEPT --A MULTI-INGRESS-COMMON -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT --A MULTI-INGRESS -j MULTI-INGRESS-COMMON -COMMIT -` - Expect(buf6.filterRules.String()).To(Equal(finalizedRules6)) - }) - - It("ingress common - allow dst v6 prefix", func() { - buf4 := newIptableBuffer() - buf6 := newIptableBuffer() - Expect(buf4).NotTo(BeNil()) - Expect(buf6).NotTo(BeNil()) - - // verify buf initialized at init - s := NewFakeServer("samplehost") - Expect(s).NotTo(BeNil()) - s.Options.allowIPv6DstPrefixText = "11::/8 , 22::/64" - Expect(s.Options.Validate()).To(BeNil()) - - buf4.Init(s.ip4Tables) - buf6.Init(s.ip6Tables) - - // check IPv4 case - buf4.renderIngressCommon(s) - buf4.FinalizeRules() - finalizedRules4 := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] --A MULTI-INGRESS-COMMON -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT --A MULTI-INGRESS -j MULTI-INGRESS-COMMON -COMMIT -` - Expect(buf4.filterRules.String()).To(Equal(finalizedRules4)) - - // check IPv6 case - buf6.renderIngressCommon(s) - buf6.FinalizeRules() - finalizedRules6 := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] --A MULTI-INGRESS-COMMON -d 11::/8 -j ACCEPT --A MULTI-INGRESS-COMMON -d 22::/64 -j ACCEPT --A MULTI-INGRESS-COMMON -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT --A MULTI-INGRESS -j MULTI-INGRESS-COMMON -COMMIT -` - Expect(buf6.filterRules.String()).To(Equal(finalizedRules6)) - }) - - It("ingress common - custom v4 rules", func() { - tmpRuleFile := filepath.Join(tmpDir, "testInputRules.txt") - ioutil.WriteFile(tmpRuleFile, []byte( - `# comment: this accepts DHCP packet --m udp -p udp --sport bootps --dport bootpc -j ACCEPT -`), 0600) - buf4 := newIptableBuffer() - buf6 := newIptableBuffer() - Expect(buf4).NotTo(BeNil()) - Expect(buf6).NotTo(BeNil()) - - // verify buf initialized at init - s := NewFakeServer("samplehost") - Expect(s).NotTo(BeNil()) - - // configure rule file and parse it - s.Options.customIPv4IngressRuleFile = tmpRuleFile - Expect(s.Options.Validate()).To(BeNil()) - - buf4.Init(s.ip4Tables) - buf6.Init(s.ip6Tables) - - // check IPv4 case - buf4.renderIngressCommon(s) - buf4.FinalizeRules() - finalizedRules4 := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] --A MULTI-INGRESS-COMMON -m udp -p udp --sport bootps --dport bootpc -j ACCEPT --A MULTI-INGRESS-COMMON -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT --A MULTI-INGRESS -j MULTI-INGRESS-COMMON -COMMIT -` - Expect(buf4.filterRules.String()).To(Equal(finalizedRules4)) - - // check IPv6 case - buf6.renderIngressCommon(s) - buf6.FinalizeRules() - finalizedRules6 := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] --A MULTI-INGRESS-COMMON -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT --A MULTI-INGRESS -j MULTI-INGRESS-COMMON -COMMIT -` - Expect(buf6.filterRules.String()).To(Equal(finalizedRules6)) - }) - - It("ingress common - custom v6 rules", func() { - tmpRuleFile := filepath.Join(tmpDir, "testInputRules.txt") - ioutil.WriteFile(tmpRuleFile, []byte( - `# comment: this accepts DHCPv6 packets from link-local address --m udp -p udp --dport 546 -d fe80::/64 -j ACCEPT -`), 0600) - buf4 := newIptableBuffer() - buf6 := newIptableBuffer() - Expect(buf4).NotTo(BeNil()) - Expect(buf6).NotTo(BeNil()) - - // verify buf initialized at init - s := NewFakeServer("samplehost") - Expect(s).NotTo(BeNil()) - - // configure rule file and parse it - s.Options.customIPv6IngressRuleFile = tmpRuleFile - Expect(s.Options.Validate()).To(BeNil()) - - buf4.Init(s.ip4Tables) - buf6.Init(s.ip6Tables) - - // check IPv4 case - buf4.renderIngressCommon(s) - buf4.FinalizeRules() - finalizedRules4 := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] --A MULTI-INGRESS-COMMON -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT --A MULTI-INGRESS -j MULTI-INGRESS-COMMON -COMMIT -` - Expect(buf4.filterRules.String()).To(Equal(finalizedRules4)) - - // check IPv6 case - buf6.renderIngressCommon(s) - buf6.FinalizeRules() - finalizedRules6 := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] --A MULTI-INGRESS-COMMON -m udp -p udp --dport 546 -d fe80::/64 -j ACCEPT --A MULTI-INGRESS-COMMON -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT --A MULTI-INGRESS -j MULTI-INGRESS-COMMON -COMMIT -` - Expect(buf6.filterRules.String()).To(Equal(finalizedRules6)) - }) - - It("egress common - default", func() { - buf4 := newIptableBuffer() - buf6 := newIptableBuffer() - Expect(buf4).NotTo(BeNil()) - Expect(buf6).NotTo(BeNil()) - - // verify buf initialized at init - s := NewFakeServer("samplehost") - Expect(s).NotTo(BeNil()) - - buf4.Init(s.ip4Tables) - buf6.Init(s.ip6Tables) - - // check IPv4 case - buf4.renderEgressCommon(s) - buf4.FinalizeRules() - finalizedRules4 := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] --A MULTI-EGRESS-COMMON -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT --A MULTI-EGRESS -j MULTI-EGRESS-COMMON -COMMIT -` - Expect(buf4.filterRules.String()).To(Equal(finalizedRules4)) - - // check IPv6 case - buf6.renderEgressCommon(s) - buf6.FinalizeRules() - finalizedRules6 := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] --A MULTI-EGRESS-COMMON -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT --A MULTI-EGRESS -j MULTI-EGRESS-COMMON -COMMIT -` - Expect(buf6.filterRules.String()).To(Equal(finalizedRules6)) - }) - - It("egress common - icmp", func() { - buf4 := newIptableBuffer() - buf6 := newIptableBuffer() - Expect(buf4).NotTo(BeNil()) - Expect(buf6).NotTo(BeNil()) - - // verify buf initialized at init - s := NewFakeServer("samplehost") - Expect(s).NotTo(BeNil()) - s.Options.acceptICMP = true - - buf4.Init(s.ip4Tables) - buf6.Init(s.ip6Tables) - - // check IPv4 case - buf4.renderEgressCommon(s) - buf4.FinalizeRules() - finalizedRules4 := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] --A MULTI-EGRESS-COMMON -p icmp -j ACCEPT --A MULTI-EGRESS-COMMON -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT --A MULTI-EGRESS -j MULTI-EGRESS-COMMON -COMMIT -` - Expect(buf4.filterRules.String()).To(Equal(finalizedRules4)) - - // check IPv6 case - buf6.renderEgressCommon(s) - buf6.FinalizeRules() - finalizedRules6 := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] --A MULTI-EGRESS-COMMON -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT --A MULTI-EGRESS -j MULTI-EGRESS-COMMON -COMMIT -` - Expect(buf6.filterRules.String()).To(Equal(finalizedRules6)) - }) - - It("egress common - icmpv6", func() { - buf4 := newIptableBuffer() - buf6 := newIptableBuffer() - Expect(buf4).NotTo(BeNil()) - Expect(buf6).NotTo(BeNil()) - - // verify buf initialized at init - s := NewFakeServer("samplehost") - Expect(s).NotTo(BeNil()) - s.Options.acceptICMPv6 = true - - buf4.Init(s.ip4Tables) - buf6.Init(s.ip6Tables) - - // check IPv4 case - buf4.renderEgressCommon(s) - buf4.FinalizeRules() - finalizedRules4 := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] --A MULTI-EGRESS-COMMON -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT --A MULTI-EGRESS -j MULTI-EGRESS-COMMON -COMMIT -` - Expect(buf4.filterRules.String()).To(Equal(finalizedRules4)) - - // check IPv6 case - buf6.renderEgressCommon(s) - buf6.FinalizeRules() - finalizedRules6 := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] --A MULTI-EGRESS-COMMON -p icmpv6 -j ACCEPT --A MULTI-EGRESS-COMMON -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT --A MULTI-EGRESS -j MULTI-EGRESS-COMMON -COMMIT -` - Expect(buf6.filterRules.String()).To(Equal(finalizedRules6)) - }) - - It("egress common - allow src v6 prefix", func() { - buf4 := newIptableBuffer() - buf6 := newIptableBuffer() - Expect(buf4).NotTo(BeNil()) - Expect(buf6).NotTo(BeNil()) - - // verify buf initialized at init - s := NewFakeServer("samplehost") - Expect(s).NotTo(BeNil()) - s.Options.allowIPv6SrcPrefixText = "11::/8 , 22::/64" - Expect(s.Options.Validate()).To(BeNil()) - - buf4.Init(s.ip4Tables) - buf6.Init(s.ip6Tables) - - // check IPv4 case - buf4.renderEgressCommon(s) - buf4.FinalizeRules() - finalizedRules4 := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] --A MULTI-EGRESS-COMMON -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT --A MULTI-EGRESS -j MULTI-EGRESS-COMMON -COMMIT -` - Expect(buf4.filterRules.String()).To(Equal(finalizedRules4)) - - // check IPv6 case - buf6.renderEgressCommon(s) - buf6.FinalizeRules() - finalizedRules6 := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] --A MULTI-EGRESS-COMMON -s 11::/8 -j ACCEPT --A MULTI-EGRESS-COMMON -s 22::/64 -j ACCEPT --A MULTI-EGRESS-COMMON -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT --A MULTI-EGRESS -j MULTI-EGRESS-COMMON -COMMIT -` - Expect(buf6.filterRules.String()).To(Equal(finalizedRules6)) - }) - - It("egress common - allow dest v6 prefix", func() { - buf4 := newIptableBuffer() - buf6 := newIptableBuffer() - Expect(buf4).NotTo(BeNil()) - Expect(buf6).NotTo(BeNil()) - - // verify buf initialized at init - s := NewFakeServer("samplehost") - Expect(s).NotTo(BeNil()) - s.Options.allowIPv6DstPrefixText = "11::/8 , 22::/64" - Expect(s.Options.Validate()).To(BeNil()) - - buf4.Init(s.ip4Tables) - buf6.Init(s.ip6Tables) - - // check IPv4 case - buf4.renderEgressCommon(s) - buf4.FinalizeRules() - finalizedRules4 := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] --A MULTI-EGRESS-COMMON -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT --A MULTI-EGRESS -j MULTI-EGRESS-COMMON -COMMIT -` - Expect(buf4.filterRules.String()).To(Equal(finalizedRules4)) - - // check IPv6 case - buf6.renderEgressCommon(s) - buf6.FinalizeRules() - finalizedRules6 := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] --A MULTI-EGRESS-COMMON -d 11::/8 -j ACCEPT --A MULTI-EGRESS-COMMON -d 22::/64 -j ACCEPT --A MULTI-EGRESS-COMMON -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT --A MULTI-EGRESS -j MULTI-EGRESS-COMMON -COMMIT -` - Expect(buf6.filterRules.String()).To(Equal(finalizedRules6)) - }) - - It("egress common - custom v4 rules", func() { - tmpRuleFile := filepath.Join(tmpDir, "testInputRules.txt") - ioutil.WriteFile(tmpRuleFile, []byte( - `# comment: this rules accepts DHCP packets --m udp -p udp --sport bootc --dport bootps -j ACCEPT -`), 0600) - buf4 := newIptableBuffer() - buf6 := newIptableBuffer() - Expect(buf4).NotTo(BeNil()) - Expect(buf6).NotTo(BeNil()) - - // verify buf initialized at init - s := NewFakeServer("samplehost") - Expect(s).NotTo(BeNil()) - - // configure rule file and parse it - s.Options.customIPv4EgressRuleFile = tmpRuleFile - Expect(s.Options.Validate()).To(BeNil()) - - buf4.Init(s.ip4Tables) - buf6.Init(s.ip6Tables) - - // check IPv4 case - buf4.renderEgressCommon(s) - buf4.FinalizeRules() - finalizedRules4 := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] --A MULTI-EGRESS-COMMON -m udp -p udp --sport bootc --dport bootps -j ACCEPT --A MULTI-EGRESS-COMMON -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT --A MULTI-EGRESS -j MULTI-EGRESS-COMMON -COMMIT -` - Expect(buf4.filterRules.String()).To(Equal(finalizedRules4)) - - // check IPv6 case - buf6.renderEgressCommon(s) - buf6.FinalizeRules() - finalizedRules6 := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] --A MULTI-EGRESS-COMMON -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT --A MULTI-EGRESS -j MULTI-EGRESS-COMMON -COMMIT -` - Expect(buf6.filterRules.String()).To(Equal(finalizedRules6)) - }) - - It("egress common - custom v6 rules", func() { - tmpRuleFile := filepath.Join(tmpDir, "testInputRules.txt") - ioutil.WriteFile(tmpRuleFile, []byte( - `# comment: this rules accepts DHCPv6 packet to dhcp relay agents/servers --m udp -p udp --dport 547 -d ff02::1:2 -j ACCEPT -`), 0600) - buf4 := newIptableBuffer() - buf6 := newIptableBuffer() - Expect(buf4).NotTo(BeNil()) - Expect(buf6).NotTo(BeNil()) - - // verify buf initialized at init - s := NewFakeServer("samplehost") - Expect(s).NotTo(BeNil()) - - // configure rule file and parse it - s.Options.customIPv6EgressRuleFile = tmpRuleFile - Expect(s.Options.Validate()).To(BeNil()) - - buf4.Init(s.ip4Tables) - buf6.Init(s.ip6Tables) - - // check IPv4 case - buf4.renderEgressCommon(s) - buf4.FinalizeRules() - finalizedRules4 := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] --A MULTI-EGRESS-COMMON -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT --A MULTI-EGRESS -j MULTI-EGRESS-COMMON -COMMIT -` - Expect(buf4.filterRules.String()).To(Equal(finalizedRules4)) - - // check IPv6 case - buf6.renderEgressCommon(s) - buf6.FinalizeRules() - finalizedRules6 := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] --A MULTI-EGRESS-COMMON -m udp -p udp --dport 547 -d ff02::1:2 -j ACCEPT --A MULTI-EGRESS-COMMON -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT --A MULTI-EGRESS -j MULTI-EGRESS-COMMON -COMMIT -` - Expect(buf6.filterRules.String()).To(Equal(finalizedRules6)) - }) - - It("ingress rules ipblock", func() { - port := intstr.FromInt(8888) - protoTCP := v1.ProtocolTCP - ingressPolicies1 := &multiv1beta1.MultiNetworkPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "ingressPolicies1", - Namespace: "testns1", - }, - Spec: multiv1beta1.MultiNetworkPolicySpec{ - Ingress: []multiv1beta1.MultiNetworkPolicyIngressRule{ - { - Ports: []multiv1beta1.MultiNetworkPolicyPort{ - { - Protocol: &protoTCP, - Port: &port, - }, - }, - From: []multiv1beta1.MultiNetworkPolicyPeer{ - { - IPBlock: &multiv1beta1.IPBlock{ - CIDR: "10.1.1.1/24", - Except: []string{"10.1.1.254"}, - }, - }, - }, - }, - }, - }, - } - - ipt := fakeiptables.NewFake() - Expect(ipt).NotTo(BeNil()) - buf := newIptableBuffer() - Expect(buf).NotTo(BeNil()) - - // verify buf initialized at init - buf.Init(ipt) - s := NewFakeServer("samplehost") - Expect(s).NotTo(BeNil()) - - Expect(s.netdefChanges.Update( - nil, - NewNetDef("testns1", "net-attach1", NewCNIConfig("testCNI", "multi")))).To(BeTrue()) - Expect(s.netdefChanges.GetPluginType(types.NamespacedName{Namespace: "testns1", Name: "net-attach1"})).To(Equal("multi")) - - pod1 := NewFakePodWithNetAnnotation( - "testns1", - "testpod1", - "net-attach1", - NewFakeNetworkStatus("testns1", "net-attach1", "192.168.1.1", "10.1.1.1"), - nil) - AddPod(s, pod1) - podInfo1, err := s.podMap.GetPodInfo(pod1) - Expect(err).NotTo(HaveOccurred()) - - buf.renderIngress(s, podInfo1, 0, ingressPolicies1, []string{"testns1/net-attach1"}) - - portRules := `-A MULTI-0-INGRESS-0-PORTS -i net1 -m tcp -p tcp --dport 8888 -j MARK --set-xmark 0x10000/0x10000 -` - Expect(buf.ingressPorts.String()).To(Equal(portRules)) - - fromRules := - `-A MULTI-0-INGRESS-0-FROM -i net1 -s 10.1.1.254 -j DROP --A MULTI-0-INGRESS-0-FROM -i net1 -s 10.1.1.1/24 -j MARK --set-xmark 0x20000/0x20000 --A MULTI-0-INGRESS-0-FROM -i net1 -s 10.1.1.1 -j MARK --set-xmark 0x20000/0x20000 -` - Expect(buf.ingressFrom.String()).To(Equal(fromRules)) - - buf.FinalizeRules() - finalizedRules := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] -:MULTI-0-INGRESS - [0:0] -:MULTI-0-INGRESS-0-PORTS - [0:0] -:MULTI-0-INGRESS-0-FROM - [0:0] --A MULTI-INGRESS -m comment --comment "policy:ingressPolicies1 net-attach-def:testns1/net-attach1" -i net1 -j MULTI-0-INGRESS --A MULTI-INGRESS -m mark --mark 0x30000/0x30000 -j RETURN --A MULTI-0-INGRESS -j MARK --set-xmark 0x0/0x30000 --A MULTI-0-INGRESS -j MULTI-0-INGRESS-0-PORTS --A MULTI-0-INGRESS -j MULTI-0-INGRESS-0-FROM --A MULTI-0-INGRESS -m mark --mark 0x30000/0x30000 -j RETURN --A MULTI-0-INGRESS-0-PORTS -i net1 -m tcp -p tcp --dport 8888 -j MARK --set-xmark 0x10000/0x10000 --A MULTI-0-INGRESS-0-FROM -i net1 -s 10.1.1.254 -j DROP --A MULTI-0-INGRESS-0-FROM -i net1 -s 10.1.1.1/24 -j MARK --set-xmark 0x20000/0x20000 --A MULTI-0-INGRESS-0-FROM -i net1 -s 10.1.1.1 -j MARK --set-xmark 0x20000/0x20000 -COMMIT -` - Expect(buf.filterRules.String()).To(Equal(finalizedRules)) - }) - - It("ingress rules endport", func() { - port0 := intstr.FromInt(8888) - port1 := intstr.FromInt(9999) - endport := int32(11111) - protoTCP := v1.ProtocolTCP - ingressPolicies1 := &multiv1beta1.MultiNetworkPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "ingressPolicies1", - Namespace: "testns1", - }, - Spec: multiv1beta1.MultiNetworkPolicySpec{ - Ingress: []multiv1beta1.MultiNetworkPolicyIngressRule{ - { - Ports: []multiv1beta1.MultiNetworkPolicyPort{ - { - Protocol: &protoTCP, - Port: &port0, - }, - { - Protocol: &protoTCP, - Port: &port1, - EndPort: &endport, - }, - }, - }, - }, - }, - } - - ipt := fakeiptables.NewFake() - Expect(ipt).NotTo(BeNil()) - buf := newIptableBuffer() - Expect(buf).NotTo(BeNil()) - - // verify buf initialized at init - buf.Init(ipt) - s := NewFakeServer("samplehost") - Expect(s).NotTo(BeNil()) - - Expect(s.netdefChanges.Update( - nil, - NewNetDef("testns1", "net-attach1", NewCNIConfig("testCNI", "multi")))).To(BeTrue()) - Expect(s.netdefChanges.GetPluginType(types.NamespacedName{Namespace: "testns1", Name: "net-attach1"})).To(Equal("multi")) - - pod1 := NewFakePodWithNetAnnotation( - "testns1", - "testpod1", - "net-attach1", - NewFakeNetworkStatus("testns1", "net-attach1", "192.168.1.1", "10.1.1.1"), - nil) - AddPod(s, pod1) - podInfo1, err := s.podMap.GetPodInfo(pod1) - Expect(err).NotTo(HaveOccurred()) - - buf.renderIngress(s, podInfo1, 0, ingressPolicies1, []string{"testns1/net-attach1"}) - - portRules := - `-A MULTI-0-INGRESS-0-PORTS -i net1 -m tcp -p tcp --dport 8888 -j MARK --set-xmark 0x10000/0x10000 --A MULTI-0-INGRESS-0-PORTS -i net1 -m tcp -p tcp --dport 9999:11111 -j MARK --set-xmark 0x10000/0x10000 -` - - Expect(buf.ingressPorts.String()).To(Equal(portRules)) - - buf.FinalizeRules() - finalizedRules := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] -:MULTI-0-INGRESS - [0:0] -:MULTI-0-INGRESS-0-PORTS - [0:0] -:MULTI-0-INGRESS-0-FROM - [0:0] --A MULTI-INGRESS -m comment --comment "policy:ingressPolicies1 net-attach-def:testns1/net-attach1" -i net1 -j MULTI-0-INGRESS --A MULTI-INGRESS -m mark --mark 0x30000/0x30000 -j RETURN --A MULTI-0-INGRESS -j MARK --set-xmark 0x0/0x30000 --A MULTI-0-INGRESS -j MULTI-0-INGRESS-0-PORTS --A MULTI-0-INGRESS -j MULTI-0-INGRESS-0-FROM --A MULTI-0-INGRESS -m mark --mark 0x30000/0x30000 -j RETURN --A MULTI-0-INGRESS-0-PORTS -i net1 -m tcp -p tcp --dport 8888 -j MARK --set-xmark 0x10000/0x10000 --A MULTI-0-INGRESS-0-PORTS -i net1 -m tcp -p tcp --dport 9999:11111 -j MARK --set-xmark 0x10000/0x10000 --A MULTI-0-INGRESS-0-FROM -m comment --comment "no ingress from, skipped" -j MARK --set-xmark 0x20000/0x20000 -COMMIT -` - Expect(buf.filterRules.String()).To(Equal(finalizedRules)) - }) - - It("ingress rules podselector/matchlabels", func() { - port := intstr.FromInt(8888) - protoTCP := v1.ProtocolTCP - ingressPolicies1 := &multiv1beta1.MultiNetworkPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "ingressPolicies1", - Namespace: "testns1", - }, - Spec: multiv1beta1.MultiNetworkPolicySpec{ - Ingress: []multiv1beta1.MultiNetworkPolicyIngressRule{ - { - Ports: []multiv1beta1.MultiNetworkPolicyPort{ - { - Protocol: &protoTCP, - Port: &port, - }, - }, - From: []multiv1beta1.MultiNetworkPolicyPeer{ - { - PodSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "foobar": "enabled", - }, - }, - }, - }, - }, - }, - }, - } - - ipt := fakeiptables.NewFake() - Expect(ipt).NotTo(BeNil()) - buf := newIptableBuffer() - Expect(buf).NotTo(BeNil()) - - // verify buf initialized at init - buf.Init(ipt) - s := NewFakeServer("samplehost") - Expect(s).NotTo(BeNil()) - - AddNamespace(s, "testns1") - - Expect(s.netdefChanges.Update( - nil, - NewNetDef("testns1", "net-attach1", NewCNIConfig("testCNI", "multi")))).To(BeTrue()) - Expect(s.netdefChanges.GetPluginType(types.NamespacedName{Namespace: "testns1", Name: "net-attach1"})).To(Equal("multi")) - - pod1 := NewFakePodWithNetAnnotation( - "testns1", - "testpod1", - "net-attach1", - NewFakeNetworkStatus("testns1", "net-attach1", "192.168.1.1", "10.1.1.1"), - nil) - AddPod(s, pod1) - podInfo1, err := s.podMap.GetPodInfo(pod1) - Expect(err).NotTo(HaveOccurred()) - - pod2 := NewFakePodWithNetAnnotation( - "testns1", - "testpod2", - "net-attach1", - NewFakeNetworkStatus("testns1", "net-attach1", "192.168.1.2", "10.1.1.2"), - map[string]string{ - "foobar": "enabled", - }) - AddPod(s, pod2) - - buf.renderIngress(s, podInfo1, 0, ingressPolicies1, []string{"testns1/net-attach1"}) - - portRules := `-A MULTI-0-INGRESS-0-PORTS -i net1 -m tcp -p tcp --dport 8888 -j MARK --set-xmark 0x10000/0x10000 -` - Expect(buf.ingressPorts.String()).To(Equal(portRules)) - - fromRules := - `-A MULTI-0-INGRESS-0-FROM -i net1 -s 10.1.1.2 -j MARK --set-xmark 0x20000/0x20000 --A MULTI-0-INGRESS-0-FROM -i net1 -s 10.1.1.1 -j MARK --set-xmark 0x20000/0x20000 -` - Expect(buf.ingressFrom.String()).To(Equal(fromRules)) - - buf.FinalizeRules() - finalizedRules := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] -:MULTI-0-INGRESS - [0:0] -:MULTI-0-INGRESS-0-PORTS - [0:0] -:MULTI-0-INGRESS-0-FROM - [0:0] --A MULTI-INGRESS -m comment --comment "policy:ingressPolicies1 net-attach-def:testns1/net-attach1" -i net1 -j MULTI-0-INGRESS --A MULTI-INGRESS -m mark --mark 0x30000/0x30000 -j RETURN --A MULTI-0-INGRESS -j MARK --set-xmark 0x0/0x30000 --A MULTI-0-INGRESS -j MULTI-0-INGRESS-0-PORTS --A MULTI-0-INGRESS -j MULTI-0-INGRESS-0-FROM --A MULTI-0-INGRESS -m mark --mark 0x30000/0x30000 -j RETURN --A MULTI-0-INGRESS-0-PORTS -i net1 -m tcp -p tcp --dport 8888 -j MARK --set-xmark 0x10000/0x10000 --A MULTI-0-INGRESS-0-FROM -i net1 -s 10.1.1.2 -j MARK --set-xmark 0x20000/0x20000 --A MULTI-0-INGRESS-0-FROM -i net1 -s 10.1.1.1 -j MARK --set-xmark 0x20000/0x20000 -COMMIT -` - Expect(buf.filterRules.String()).To(Equal(finalizedRules)) - }) - - It("ingress rules namespace selector", func() { - ingressPolicies1 := &multiv1beta1.MultiNetworkPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "ingressPolicies1", - Namespace: "testns1", - }, - Spec: multiv1beta1.MultiNetworkPolicySpec{ - Ingress: []multiv1beta1.MultiNetworkPolicyIngressRule{ - { - From: []multiv1beta1.MultiNetworkPolicyPeer{ - { - NamespaceSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "nsname": "testns2", - }, - }, - }, - }, - }, - }, - }, - } - - ipt := fakeiptables.NewFake() - Expect(ipt).NotTo(BeNil()) - buf := newIptableBuffer() - Expect(buf).NotTo(BeNil()) - - // verify buf initialized at init - buf.Init(ipt) - s := NewFakeServer("samplehost") - Expect(s).NotTo(BeNil()) - - AddNamespace(s, "testns1") - AddNamespace(s, "testns2") - - Expect(s.netdefChanges.Update( - nil, - NewNetDef("testns1", "net-attach1", NewCNIConfig("testCNI", "multi")))).To(BeTrue()) - Expect(s.netdefChanges.GetPluginType(types.NamespacedName{Namespace: "testns1", Name: "net-attach1"})).To(Equal("multi")) - Expect(s.netdefChanges.Update( - nil, - NewNetDef("testns2", "net-attach1", NewCNIConfig("testCNI", "multi")))).To(BeTrue()) - Expect(s.netdefChanges.GetPluginType(types.NamespacedName{Namespace: "testns2", Name: "net-attach1"})).To(Equal("multi")) - - pod1 := NewFakePodWithNetAnnotation( - "testns1", - "testpod1", - "net-attach1", - NewFakeNetworkStatus("testns1", "net-attach1", "192.168.1.1", "10.1.1.1"), - nil) - AddPod(s, pod1) - podInfo1, err := s.podMap.GetPodInfo(pod1) - Expect(err).NotTo(HaveOccurred()) - - pod2 := NewFakePodWithNetAnnotation( - "testns2", - "testpod2", - "net-attach1", - NewFakeNetworkStatus("testns2", "net-attach1", "192.168.1.2", "10.1.1.2"), - nil) - AddPod(s, pod2) - buf.renderIngress(s, podInfo1, 0, ingressPolicies1, []string{"testns1/net-attach1", "testns2/net-attach1"}) - - buf.FinalizeRules() - finalizedRules := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] -:MULTI-0-INGRESS - [0:0] -:MULTI-0-INGRESS-0-PORTS - [0:0] -:MULTI-0-INGRESS-0-FROM - [0:0] --A MULTI-INGRESS -m comment --comment "policy:ingressPolicies1 net-attach-def:testns1/net-attach1" -i net1 -j MULTI-0-INGRESS --A MULTI-INGRESS -m mark --mark 0x30000/0x30000 -j RETURN --A MULTI-0-INGRESS -j MARK --set-xmark 0x0/0x30000 --A MULTI-0-INGRESS -j MULTI-0-INGRESS-0-PORTS --A MULTI-0-INGRESS -j MULTI-0-INGRESS-0-FROM --A MULTI-0-INGRESS -m mark --mark 0x30000/0x30000 -j RETURN --A MULTI-0-INGRESS-0-PORTS -m comment --comment "no ingress ports, skipped" -j MARK --set-xmark 0x10000/0x10000 --A MULTI-0-INGRESS-0-FROM -i net1 -s 10.1.1.2 -j MARK --set-xmark 0x20000/0x20000 --A MULTI-0-INGRESS-0-FROM -i net1 -s 10.1.1.1 -j MARK --set-xmark 0x20000/0x20000 -COMMIT -` - - Expect(buf.filterRules.String()).To(Equal(string(finalizedRules))) - }) - - It("ingress rules namespaceSeelctor with non existent labels", func() { - ingressPolicies1 := &multiv1beta1.MultiNetworkPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "ingressPolicies1", - Namespace: "testns1", - }, - Spec: multiv1beta1.MultiNetworkPolicySpec{ - Ingress: []multiv1beta1.MultiNetworkPolicyIngressRule{ - { - From: []multiv1beta1.MultiNetworkPolicyPeer{ - { - NamespaceSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "foo": "bar", // No namespace exists with this label - }, - }, - }, - }, - }, - }, - }, - } - - ipt := fakeiptables.NewFake() - Expect(ipt).NotTo(BeNil()) - buf := newIptableBuffer() - Expect(buf).NotTo(BeNil()) - - // verify buf initialized at init - buf.Init(ipt) - s := NewFakeServer("samplehost") - Expect(s).NotTo(BeNil()) - - AddNamespace(s, "testns1") - AddNamespace(s, "testns2") - - Expect(s.netdefChanges.Update( - nil, - NewNetDef("testns1", "net-attach1", NewCNIConfig("testCNI", "multi")))).To(BeTrue()) - Expect(s.netdefChanges.GetPluginType(types.NamespacedName{Namespace: "testns1", Name: "net-attach1"})).To(Equal("multi")) - Expect(s.netdefChanges.Update( - nil, - NewNetDef("testns2", "net-attach1", NewCNIConfig("testCNI", "multi")))).To(BeTrue()) - Expect(s.netdefChanges.GetPluginType(types.NamespacedName{Namespace: "testns2", Name: "net-attach1"})).To(Equal("multi")) - - pod1 := NewFakePodWithNetAnnotation( - "testns1", - "testpod1", - "net-attach1", - NewFakeNetworkStatus("testns1", "net-attach1", "192.168.1.1", "10.1.1.1"), - nil) - AddPod(s, pod1) - podInfo1, err := s.podMap.GetPodInfo(pod1) - Expect(err).NotTo(HaveOccurred()) - - pod2 := NewFakePodWithNetAnnotation( - "testns2", - "testpod2", - "net-attach1", - NewFakeNetworkStatus("testns2", "net-attach1", "192.168.1.2", "10.1.1.2"), - nil) - AddPod(s, pod2) - buf.renderIngress(s, podInfo1, 0, ingressPolicies1, []string{"testns1/net-attach1", "testns2/net-attach1"}) - - buf.FinalizeRules() - finalizedRules := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] -:MULTI-0-INGRESS - [0:0] -:MULTI-0-INGRESS-0-PORTS - [0:0] -:MULTI-0-INGRESS-0-FROM - [0:0] --A MULTI-INGRESS -m comment --comment "policy:ingressPolicies1 net-attach-def:testns1/net-attach1" -i net1 -j MULTI-0-INGRESS --A MULTI-INGRESS -m mark --mark 0x30000/0x30000 -j RETURN --A MULTI-0-INGRESS -j MARK --set-xmark 0x0/0x30000 --A MULTI-0-INGRESS -j MULTI-0-INGRESS-0-PORTS --A MULTI-0-INGRESS -j MULTI-0-INGRESS-0-FROM --A MULTI-0-INGRESS -m mark --mark 0x30000/0x30000 -j RETURN --A MULTI-0-INGRESS-0-PORTS -m comment --comment "no ingress ports, skipped" -j MARK --set-xmark 0x10000/0x10000 -COMMIT -` - Expect(buf.filterRules.String()).To(Equal(string(finalizedRules))) - }) - - It("enforce policy with net-attach-def in a different namespace than pods", func() { - ingressPolicies1 := &multiv1beta1.MultiNetworkPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "ingressPolicies1", - Namespace: "testns1", - }, - Spec: multiv1beta1.MultiNetworkPolicySpec{ - Ingress: []multiv1beta1.MultiNetworkPolicyIngressRule{ - { - From: []multiv1beta1.MultiNetworkPolicyPeer{ - { - NamespaceSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "nsname": "testns2", - }, - }, - }, - }, - }, - }, - }, - } - - ipt := fakeiptables.NewFake() - Expect(ipt).NotTo(BeNil()) - buf := newIptableBuffer() - Expect(buf).NotTo(BeNil()) - - // verify buf initialized at init - buf.Init(ipt) - s := NewFakeServer("samplehost") - Expect(s).NotTo(BeNil()) - - AddNamespace(s, "default") - AddNamespace(s, "testns1") - AddNamespace(s, "testns2") - - Expect(s.netdefChanges.Update( - nil, - NewNetDef("default", "net-attach1", NewCNIConfig("testCNI", "multi")))).To(BeTrue()) - Expect(s.netdefChanges.GetPluginType(types.NamespacedName{Namespace: "default", Name: "net-attach1"})).To(Equal("multi")) - - pod1 := NewFakePodWithNetAnnotation( - "testns1", - "testpod1", - "default/net-attach1", - NewFakeNetworkStatus("default", "net-attach1", "192.168.1.1", "10.1.1.1"), - nil) - AddPod(s, pod1) - podInfo1, err := s.podMap.GetPodInfo(pod1) - Expect(err).NotTo(HaveOccurred()) - - pod2 := NewFakePodWithNetAnnotation( - "testns2", - "testpod2", - "default/net-attach1", - NewFakeNetworkStatus("default", "net-attach1", "192.168.1.2", "10.1.1.2"), - nil) - AddPod(s, pod2) - buf.renderIngress(s, podInfo1, 0, ingressPolicies1, []string{"default/net-attach1"}) - - buf.FinalizeRules() - finalizedRules := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] -:MULTI-0-INGRESS - [0:0] -:MULTI-0-INGRESS-0-PORTS - [0:0] -:MULTI-0-INGRESS-0-FROM - [0:0] --A MULTI-INGRESS -m comment --comment "policy:ingressPolicies1 net-attach-def:default/net-attach1" -i net1 -j MULTI-0-INGRESS --A MULTI-INGRESS -m mark --mark 0x30000/0x30000 -j RETURN --A MULTI-0-INGRESS -j MARK --set-xmark 0x0/0x30000 --A MULTI-0-INGRESS -j MULTI-0-INGRESS-0-PORTS --A MULTI-0-INGRESS -j MULTI-0-INGRESS-0-FROM --A MULTI-0-INGRESS -m mark --mark 0x30000/0x30000 -j RETURN --A MULTI-0-INGRESS-0-PORTS -m comment --comment "no ingress ports, skipped" -j MARK --set-xmark 0x10000/0x10000 --A MULTI-0-INGRESS-0-FROM -i net1 -s 10.1.1.2 -j MARK --set-xmark 0x20000/0x20000 --A MULTI-0-INGRESS-0-FROM -i net1 -s 10.1.1.1 -j MARK --set-xmark 0x20000/0x20000 -COMMIT -` - Expect(buf.filterRules.String()).To(Equal(string(finalizedRules))) - }) - - It("egress rules ipblock", func() { - port := intstr.FromInt(8888) - protoTCP := v1.ProtocolTCP - egressPolicies1 := &multiv1beta1.MultiNetworkPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "EgressPolicies1", - Namespace: "testns1", - }, - Spec: multiv1beta1.MultiNetworkPolicySpec{ - Egress: []multiv1beta1.MultiNetworkPolicyEgressRule{ - { - Ports: []multiv1beta1.MultiNetworkPolicyPort{ - { - Protocol: &protoTCP, - Port: &port, - }, - }, - To: []multiv1beta1.MultiNetworkPolicyPeer{ - { - IPBlock: &multiv1beta1.IPBlock{ - CIDR: "10.1.1.1/24", - Except: []string{"10.1.1.254"}, - }, - }, - }, - }, - }, - }, - } - - ipt := fakeiptables.NewFake() - Expect(ipt).NotTo(BeNil()) - buf := newIptableBuffer() - Expect(buf).NotTo(BeNil()) - - // verify buf initialized at init - buf.Init(ipt) - s := NewFakeServer("samplehost") - Expect(s).NotTo(BeNil()) - - Expect(s.netdefChanges.Update( - nil, - NewNetDef("testns1", "net-attach1", NewCNIConfig("testCNI", "multi")))).To(BeTrue()) - Expect(s.netdefChanges.GetPluginType(types.NamespacedName{Namespace: "testns1", Name: "net-attach1"})).To(Equal("multi")) - - pod1 := NewFakePodWithNetAnnotation( - "testns1", - "testpod1", - "net-attach1", - NewFakeNetworkStatus("testns1", "net-attach1", "192.168.1.1", "10.1.1.1"), - nil) - AddPod(s, pod1) - podInfo1, err := s.podMap.GetPodInfo(pod1) - Expect(err).NotTo(HaveOccurred()) - - buf.renderEgress(s, podInfo1, 0, egressPolicies1, []string{"testns1/net-attach1"}) - - portRules := `-A MULTI-0-EGRESS-0-PORTS -o net1 -m tcp -p tcp --dport 8888 -j MARK --set-xmark 0x10000/0x10000 -` - Expect(buf.egressPorts.String()).To(Equal(portRules)) - - toRules := - `-A MULTI-0-EGRESS-0-TO -o net1 -d 10.1.1.254 -j DROP --A MULTI-0-EGRESS-0-TO -o net1 -d 10.1.1.1/24 -j MARK --set-xmark 0x20000/0x20000 --A MULTI-0-EGRESS-0-TO -o net1 -d 10.1.1.1 -j MARK --set-xmark 0x20000/0x20000 -` - Expect(buf.egressTo.String()).To(Equal(toRules)) - - buf.FinalizeRules() - finalizedRules := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] -:MULTI-0-EGRESS - [0:0] -:MULTI-0-EGRESS-0-PORTS - [0:0] -:MULTI-0-EGRESS-0-TO - [0:0] --A MULTI-EGRESS -m comment --comment "policy:EgressPolicies1 net-attach-def:testns1/net-attach1" -o net1 -j MULTI-0-EGRESS --A MULTI-EGRESS -m mark --mark 0x30000/0x30000 -j RETURN --A MULTI-0-EGRESS -j MARK --set-xmark 0x0/0x30000 --A MULTI-0-EGRESS -j MULTI-0-EGRESS-0-PORTS --A MULTI-0-EGRESS -j MULTI-0-EGRESS-0-TO --A MULTI-0-EGRESS -m mark --mark 0x30000/0x30000 -j RETURN --A MULTI-0-EGRESS-0-PORTS -o net1 -m tcp -p tcp --dport 8888 -j MARK --set-xmark 0x10000/0x10000 --A MULTI-0-EGRESS-0-TO -o net1 -d 10.1.1.254 -j DROP --A MULTI-0-EGRESS-0-TO -o net1 -d 10.1.1.1/24 -j MARK --set-xmark 0x20000/0x20000 --A MULTI-0-EGRESS-0-TO -o net1 -d 10.1.1.1 -j MARK --set-xmark 0x20000/0x20000 -COMMIT -` - Expect(buf.filterRules.String()).To(Equal(finalizedRules)) - }) - - It("egress rules endport", func() { - port0 := intstr.FromInt(8888) - port1 := intstr.FromInt(9999) - endport := int32(11111) - protoTCP := v1.ProtocolTCP - egressPolicies1 := &multiv1beta1.MultiNetworkPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "EgressPolicies1", - Namespace: "testns1", - }, - Spec: multiv1beta1.MultiNetworkPolicySpec{ - Egress: []multiv1beta1.MultiNetworkPolicyEgressRule{ - { - Ports: []multiv1beta1.MultiNetworkPolicyPort{ - { - Protocol: &protoTCP, - Port: &port0, - }, - { - Protocol: &protoTCP, - Port: &port1, - EndPort: &endport, - }, - }, - }, - }, - }, - } - - ipt := fakeiptables.NewFake() - Expect(ipt).NotTo(BeNil()) - buf := newIptableBuffer() - Expect(buf).NotTo(BeNil()) - - // verify buf initialized at init - buf.Init(ipt) - s := NewFakeServer("samplehost") - Expect(s).NotTo(BeNil()) - - Expect(s.netdefChanges.Update( - nil, - NewNetDef("testns1", "net-attach1", NewCNIConfig("testCNI", "multi")))).To(BeTrue()) - Expect(s.netdefChanges.GetPluginType(types.NamespacedName{Namespace: "testns1", Name: "net-attach1"})).To(Equal("multi")) - - pod1 := NewFakePodWithNetAnnotation( - "testns1", - "testpod1", - "net-attach1", - NewFakeNetworkStatus("testns1", "net-attach1", "192.168.1.1", "10.1.1.1"), - nil) - AddPod(s, pod1) - podInfo1, err := s.podMap.GetPodInfo(pod1) - Expect(err).NotTo(HaveOccurred()) - - buf.renderEgress(s, podInfo1, 0, egressPolicies1, []string{"testns1/net-attach1"}) - - portRules := - `-A MULTI-0-EGRESS-0-PORTS -o net1 -m tcp -p tcp --dport 8888 -j MARK --set-xmark 0x10000/0x10000 --A MULTI-0-EGRESS-0-PORTS -o net1 -m tcp -p tcp --dport 9999:11111 -j MARK --set-xmark 0x10000/0x10000 -` - Expect(buf.egressPorts.String()).To(Equal(portRules)) - - buf.FinalizeRules() - finalizedRules := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] -:MULTI-0-EGRESS - [0:0] -:MULTI-0-EGRESS-0-PORTS - [0:0] -:MULTI-0-EGRESS-0-TO - [0:0] --A MULTI-EGRESS -m comment --comment "policy:EgressPolicies1 net-attach-def:testns1/net-attach1" -o net1 -j MULTI-0-EGRESS --A MULTI-EGRESS -m mark --mark 0x30000/0x30000 -j RETURN --A MULTI-0-EGRESS -j MARK --set-xmark 0x0/0x30000 --A MULTI-0-EGRESS -j MULTI-0-EGRESS-0-PORTS --A MULTI-0-EGRESS -j MULTI-0-EGRESS-0-TO --A MULTI-0-EGRESS -m mark --mark 0x30000/0x30000 -j RETURN --A MULTI-0-EGRESS-0-PORTS -o net1 -m tcp -p tcp --dport 8888 -j MARK --set-xmark 0x10000/0x10000 --A MULTI-0-EGRESS-0-PORTS -o net1 -m tcp -p tcp --dport 9999:11111 -j MARK --set-xmark 0x10000/0x10000 --A MULTI-0-EGRESS-0-TO -m comment --comment "no egress to, skipped" -j MARK --set-xmark 0x20000/0x20000 -COMMIT -` - Expect(buf.filterRules.String()).To(Equal(finalizedRules)) - }) - - It("egress rules podselector/matchlabels", func() { - port := intstr.FromInt(8888) - protoTCP := v1.ProtocolTCP - egressPolicies1 := &multiv1beta1.MultiNetworkPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "EgressPolicies1", - Namespace: "testns1", - }, - Spec: multiv1beta1.MultiNetworkPolicySpec{ - Egress: []multiv1beta1.MultiNetworkPolicyEgressRule{ - { - Ports: []multiv1beta1.MultiNetworkPolicyPort{ - { - Protocol: &protoTCP, - Port: &port, - }, - }, - To: []multiv1beta1.MultiNetworkPolicyPeer{ - { - PodSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "foobar": "enabled", - }, - }, - }, - }, - }, - }, - }, - } - - ipt := fakeiptables.NewFake() - Expect(ipt).NotTo(BeNil()) - buf := newIptableBuffer() - Expect(buf).NotTo(BeNil()) - - // verify buf initialized at init - buf.Init(ipt) - s := NewFakeServer("samplehost") - Expect(s).NotTo(BeNil()) - - AddNamespace(s, "testns1") - - Expect(s.netdefChanges.Update( - nil, - NewNetDef("testns1", "net-attach1", NewCNIConfig("testCNI", "multi")))).To(BeTrue()) - Expect(s.netdefChanges.GetPluginType(types.NamespacedName{Namespace: "testns1", Name: "net-attach1"})).To(Equal("multi")) - - pod1 := NewFakePodWithNetAnnotation( - "testns1", - "testpod1", - "net-attach1", - NewFakeNetworkStatus("testns1", "net-attach1", "192.168.1.1", "10.1.1.1"), - nil) - AddPod(s, pod1) - podInfo1, err := s.podMap.GetPodInfo(pod1) - Expect(err).NotTo(HaveOccurred()) - - pod2 := NewFakePodWithNetAnnotation( - "testns1", - "testpod2", - "net-attach1", - NewFakeNetworkStatus("testns1", "net-attach1", "192.168.1.2", "10.1.1.2"), - map[string]string{ - "foobar": "enabled", - }) - AddPod(s, pod2) - - buf.renderEgress(s, podInfo1, 0, egressPolicies1, []string{"testns1/net-attach1"}) - - portRules := `-A MULTI-0-EGRESS-0-PORTS -o net1 -m tcp -p tcp --dport 8888 -j MARK --set-xmark 0x10000/0x10000 -` - Expect(buf.egressPorts.String()).To(Equal(portRules)) - - toRules := - `-A MULTI-0-EGRESS-0-TO -o net1 -d 10.1.1.2 -j MARK --set-xmark 0x20000/0x20000 --A MULTI-0-EGRESS-0-TO -o net1 -d 10.1.1.1 -j MARK --set-xmark 0x20000/0x20000 -` - Expect(buf.egressTo.String()).To(Equal(toRules)) - - buf.FinalizeRules() - finalizedRules := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] -:MULTI-0-EGRESS - [0:0] -:MULTI-0-EGRESS-0-PORTS - [0:0] -:MULTI-0-EGRESS-0-TO - [0:0] --A MULTI-EGRESS -m comment --comment "policy:EgressPolicies1 net-attach-def:testns1/net-attach1" -o net1 -j MULTI-0-EGRESS --A MULTI-EGRESS -m mark --mark 0x30000/0x30000 -j RETURN --A MULTI-0-EGRESS -j MARK --set-xmark 0x0/0x30000 --A MULTI-0-EGRESS -j MULTI-0-EGRESS-0-PORTS --A MULTI-0-EGRESS -j MULTI-0-EGRESS-0-TO --A MULTI-0-EGRESS -m mark --mark 0x30000/0x30000 -j RETURN --A MULTI-0-EGRESS-0-PORTS -o net1 -m tcp -p tcp --dport 8888 -j MARK --set-xmark 0x10000/0x10000 --A MULTI-0-EGRESS-0-TO -o net1 -d 10.1.1.2 -j MARK --set-xmark 0x20000/0x20000 --A MULTI-0-EGRESS-0-TO -o net1 -d 10.1.1.1 -j MARK --set-xmark 0x20000/0x20000 -COMMIT -` - Expect(buf.filterRules.String()).To(Equal(finalizedRules)) - }) - - It("default values", func() { - port := intstr.FromInt(8888) - policies1 := &multiv1beta1.MultiNetworkPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "policies1", - Namespace: "testns1", - }, - Spec: multiv1beta1.MultiNetworkPolicySpec{ - Ingress: []multiv1beta1.MultiNetworkPolicyIngressRule{ - { - Ports: []multiv1beta1.MultiNetworkPolicyPort{ - { - Port: &port, - }, - }, - }, - }, - Egress: []multiv1beta1.MultiNetworkPolicyEgressRule{ - { - Ports: []multiv1beta1.MultiNetworkPolicyPort{ - { - Port: &port, - }, - }, - }, - }, - }, - } - - ipt := fakeiptables.NewFake() - Expect(ipt).NotTo(BeNil()) - buf := newIptableBuffer() - Expect(buf).NotTo(BeNil()) - buf.Init(ipt) - - s := NewFakeServer("samplehost") - Expect(s).NotTo(BeNil()) - - AddNamespace(s, "testns1") - - Expect(s.netdefChanges.Update( - nil, - NewNetDef("testns1", "net-attach1", NewCNIConfig("testCNI", "multi"))), - ).To(BeTrue()) - Expect(s.netdefChanges.GetPluginType(types.NamespacedName{Namespace: "testns1", Name: "net-attach1"})). - To(Equal("multi")) - - pod1 := NewFakePodWithNetAnnotation( - "testns1", - "testpod1", - "net-attach1", - NewFakeNetworkStatus("testns1", "net-attach1", "192.168.1.1", "10.1.1.1"), - nil) - AddPod(s, pod1) - podInfo1, err := s.podMap.GetPodInfo(pod1) - Expect(err).NotTo(HaveOccurred()) - - buf.renderIngress(s, podInfo1, 0, policies1, []string{"testns1/net-attach1"}) - buf.renderEgress(s, podInfo1, 0, policies1, []string{"testns1/net-attach1"}) - - portRules := `-A MULTI-0-INGRESS-0-PORTS -i net1 -m tcp -p tcp --dport 8888 -j MARK --set-xmark 0x10000/0x10000 -` - Expect(buf.ingressPorts.String()).To(Equal(portRules)) - - portRules = `-A MULTI-0-EGRESS-0-PORTS -o net1 -m tcp -p tcp --dport 8888 -j MARK --set-xmark 0x10000/0x10000 -` - Expect(buf.egressPorts.String()).To(Equal(portRules)) - }) - - It("policyType should be implicitly inferred", func() { - - policy1 := &multiv1beta1.MultiNetworkPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "ingressPolicies1", - Namespace: "testns1", - Annotations: map[string]string{ - PolicyNetworkAnnotation: "net-attach1", - }, - }, - Spec: multiv1beta1.MultiNetworkPolicySpec{ - PodSelector: metav1.LabelSelector{ - MatchLabels: map[string]string{ - "role": "targetpod", - }, - }, - Ingress: []multiv1beta1.MultiNetworkPolicyIngressRule{{ - From: []multiv1beta1.MultiNetworkPolicyPeer{{ - PodSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "foobar": "enabled", - }, - }, - }}, - }}, - }, - } - - s := NewFakeServer("samplehost") - Expect(s).NotTo(BeNil()) - - AddNamespace(s, "testns1") - - Expect( - s.netdefChanges.Update(nil, NewNetDef("testns1", "net-attach1", NewCNIConfig("testCNI", "multi"))), - ).To(BeTrue()) - - pod1 := NewFakePodWithNetAnnotation( - "testns1", - "testpod1", - "net-attach1", - NewFakeNetworkStatus("testns1", "net-attach1", "192.168.1.1", "10.1.1.1"), - map[string]string{ - "role": "targetpod", - }) - pod1.Spec.NodeName = "samplehost" - - AddPod(s, pod1) - podInfo1, err := s.podMap.GetPodInfo(pod1) - Expect(err).NotTo(HaveOccurred()) - - pod2 := NewFakePodWithNetAnnotation( - "testns1", - "testpod2", - "net-attach1", - NewFakeNetworkStatus("testns1", "net-attach1", "192.168.1.2", "10.1.1.2"), - map[string]string{ - "foobar": "enabled", - }) - AddPod(s, pod2) - - Expect( - s.policyChanges.Update(nil, policy1), - ).To(BeTrue()) - s.policyMap.Update(s.policyChanges) - - result := fakeiptables.NewFake() - s.ip4Tables = result - - s.generatePolicyRulesForPod(pod1, podInfo1) - - Expect(string(result.Dump.String())).To(Equal(`*nat -:PREROUTING - [0:0] -:INPUT - [0:0] -:OUTPUT - [0:0] -:POSTROUTING - [0:0] --A PREROUTING -i net1 -j RETURN -COMMIT -*filter -:INPUT - [0:0] -:FORWARD - [0:0] -:OUTPUT - [0:0] -:MULTI-INGRESS - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS-COMMON - [0:0] -:MULTI-0-INGRESS - [0:0] -:MULTI-0-INGRESS-0-PORTS - [0:0] -:MULTI-0-INGRESS-0-FROM - [0:0] --A INPUT -i net1 -j MULTI-INGRESS --A OUTPUT -o net1 -j MULTI-EGRESS --A MULTI-INGRESS -j MULTI-INGRESS-COMMON --A MULTI-INGRESS -m comment --comment "policy:ingressPolicies1 net-attach-def:testns1/net-attach1" -i net1 -j MULTI-0-INGRESS --A MULTI-INGRESS -m mark --mark 0x30000/0x30000 -j RETURN --A MULTI-INGRESS -j DROP --A MULTI-INGRESS-COMMON -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT --A MULTI-0-INGRESS -j MARK --set-xmark 0x0/0x30000 --A MULTI-0-INGRESS -j MULTI-0-INGRESS-0-PORTS --A MULTI-0-INGRESS -j MULTI-0-INGRESS-0-FROM --A MULTI-0-INGRESS -m mark --mark 0x30000/0x30000 -j RETURN --A MULTI-0-INGRESS-0-PORTS -m comment --comment "no ingress ports, skipped" -j MARK --set-xmark 0x10000/0x10000 --A MULTI-0-INGRESS-0-FROM -i net1 -s 10.1.1.2 -j MARK --set-xmark 0x20000/0x20000 --A MULTI-0-INGRESS-0-FROM -i net1 -s 10.1.1.1 -j MARK --set-xmark 0x20000/0x20000 -COMMIT -*mangle -COMMIT -`)) - - }) - - It("match all ports when only the protocol is specified", func() { - // https://github.com/zeeke/multi-networkpolicy/blob/f76867e779b86b5ca6ba0002bfe716876e66e959/scheme.yml#L59 - - protoTCP := v1.ProtocolTCP - protoUDP := v1.ProtocolTCP - policy1 := &multiv1beta1.MultiNetworkPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "all-ports-policy", - Namespace: "testns1", - Annotations: map[string]string{ - PolicyNetworkAnnotation: "net-attach1", - }, - }, - Spec: multiv1beta1.MultiNetworkPolicySpec{ - PodSelector: metav1.LabelSelector{ - MatchLabels: map[string]string{ - "role": "targetpod", - }, - }, - Ingress: []multiv1beta1.MultiNetworkPolicyIngressRule{{ - Ports: []multiv1beta1.MultiNetworkPolicyPort{{ - Protocol: &protoTCP, - }}, - }}, - Egress: []multiv1beta1.MultiNetworkPolicyEgressRule{{ - Ports: []multiv1beta1.MultiNetworkPolicyPort{{ - Protocol: &protoUDP, - }}, - }}, - }, - } - - s := NewFakeServer("samplehost") - Expect(s).NotTo(BeNil()) - - AddNamespace(s, "testns1") - - Expect( - s.netdefChanges.Update(nil, NewNetDef("testns1", "net-attach1", NewCNIConfig("testCNI", "multi"))), - ).To(BeTrue()) - - pod1 := NewFakePodWithNetAnnotation( - "testns1", - "testpod1", - "net-attach1", - NewFakeNetworkStatus("testns1", "net-attach1", "192.168.1.1", "10.1.1.1"), - map[string]string{ - "role": "targetpod", - }) - pod1.Spec.NodeName = "samplehost" - - AddPod(s, pod1) - podInfo1, err := s.podMap.GetPodInfo(pod1) - Expect(err).NotTo(HaveOccurred()) - - Expect( - s.policyChanges.Update(nil, policy1), - ).To(BeTrue()) - s.policyMap.Update(s.policyChanges) - - result := fakeiptables.NewFake() - s.ip4Tables = result - - s.generatePolicyRulesForPod(pod1, podInfo1) - Expect(result.Dump.String()).To(Equal(`*nat -:PREROUTING - [0:0] -:INPUT - [0:0] -:OUTPUT - [0:0] -:POSTROUTING - [0:0] --A PREROUTING -i net1 -j RETURN -COMMIT -*filter -:INPUT - [0:0] -:FORWARD - [0:0] -:OUTPUT - [0:0] -:MULTI-INGRESS - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS-COMMON - [0:0] -:MULTI-0-INGRESS - [0:0] -:MULTI-0-INGRESS-0-PORTS - [0:0] -:MULTI-0-INGRESS-0-FROM - [0:0] -:MULTI-0-EGRESS - [0:0] -:MULTI-0-EGRESS-0-PORTS - [0:0] -:MULTI-0-EGRESS-0-TO - [0:0] --A INPUT -i net1 -j MULTI-INGRESS --A OUTPUT -o net1 -j MULTI-EGRESS --A MULTI-INGRESS -j MULTI-INGRESS-COMMON --A MULTI-INGRESS -m comment --comment "policy:all-ports-policy net-attach-def:testns1/net-attach1" -i net1 -j MULTI-0-INGRESS --A MULTI-INGRESS -m mark --mark 0x30000/0x30000 -j RETURN --A MULTI-INGRESS -j DROP --A MULTI-EGRESS -j MULTI-EGRESS-COMMON --A MULTI-EGRESS -m comment --comment "policy:all-ports-policy net-attach-def:testns1/net-attach1" -o net1 -j MULTI-0-EGRESS --A MULTI-EGRESS -m mark --mark 0x30000/0x30000 -j RETURN --A MULTI-EGRESS -j DROP --A MULTI-INGRESS-COMMON -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT --A MULTI-EGRESS-COMMON -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT --A MULTI-0-INGRESS -j MARK --set-xmark 0x0/0x30000 --A MULTI-0-INGRESS -j MULTI-0-INGRESS-0-PORTS --A MULTI-0-INGRESS -j MULTI-0-INGRESS-0-FROM --A MULTI-0-INGRESS -m mark --mark 0x30000/0x30000 -j RETURN --A MULTI-0-INGRESS-0-PORTS -i net1 -m tcp -p tcp -j MARK --set-xmark 0x10000/0x10000 --A MULTI-0-INGRESS-0-FROM -m comment --comment "no ingress from, skipped" -j MARK --set-xmark 0x20000/0x20000 --A MULTI-0-EGRESS -j MARK --set-xmark 0x0/0x30000 --A MULTI-0-EGRESS -j MULTI-0-EGRESS-0-PORTS --A MULTI-0-EGRESS -j MULTI-0-EGRESS-0-TO --A MULTI-0-EGRESS -m mark --mark 0x30000/0x30000 -j RETURN --A MULTI-0-EGRESS-0-PORTS -o net1 -m tcp -p tcp -j MARK --set-xmark 0x10000/0x10000 --A MULTI-0-EGRESS-0-TO -m comment --comment "no egress to, skipped" -j MARK --set-xmark 0x20000/0x20000 -COMMIT -*mangle -COMMIT -`)) - - }) - - It("ignore `podSelector` and `namespaceSelector` when IPBlock field is set", func() { - policy1 := &multiv1beta1.MultiNetworkPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "ipblock-override-policy", - Namespace: "testns1", - Annotations: map[string]string{ - PolicyNetworkAnnotation: "net-attach1", - }, - }, - Spec: multiv1beta1.MultiNetworkPolicySpec{ - PodSelector: metav1.LabelSelector{ - MatchLabels: map[string]string{ - "role": "targetpod", - }, - }, - Ingress: []multiv1beta1.MultiNetworkPolicyIngressRule{{ - From: []multiv1beta1.MultiNetworkPolicyPeer{{ - IPBlock: &multiv1beta1.IPBlock{ - CIDR: "1.1.1.0/16", - Except: []string{"1.1.1.1"}, - }, - PodSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"tobe": "ignored"}, - }, - NamespaceSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"tobe": "ignored"}, - }, - }}, - }}, - Egress: []multiv1beta1.MultiNetworkPolicyEgressRule{{ - To: []multiv1beta1.MultiNetworkPolicyPeer{{ - IPBlock: &multiv1beta1.IPBlock{ - CIDR: "2.2.2.0/16", - Except: []string{"2.2.2.2"}, - }, - PodSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"tobe": "ignored"}, - }, - NamespaceSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"tobe": "ignored"}, - }, - }}, - }}, - }, - } - - s := NewFakeServer("samplehost") - Expect(s).NotTo(BeNil()) - - AddNamespace(s, "testns1") - - Expect( - s.netdefChanges.Update(nil, NewNetDef("testns1", "net-attach1", NewCNIConfig("testCNI", "multi"))), - ).To(BeTrue()) - - pod1 := NewFakePodWithNetAnnotation( - "testns1", - "testpod1", - "net-attach1", - NewFakeNetworkStatus("testns1", "net-attach1", "192.168.1.1", "10.1.1.1"), - map[string]string{ - "role": "targetpod", - }) - pod1.Spec.NodeName = "samplehost" - - AddPod(s, pod1) - podInfo1, err := s.podMap.GetPodInfo(pod1) - Expect(err).NotTo(HaveOccurred()) - - Expect( - s.policyChanges.Update(nil, policy1), - ).To(BeTrue()) - s.policyMap.Update(s.policyChanges) - - result := fakeiptables.NewFake() - s.ip4Tables = result - - s.generatePolicyRulesForPod(pod1, podInfo1) - Expect(result.Dump.String()).To(Equal(`*nat -:PREROUTING - [0:0] -:INPUT - [0:0] -:OUTPUT - [0:0] -:POSTROUTING - [0:0] --A PREROUTING -i net1 -j RETURN -COMMIT -*filter -:INPUT - [0:0] -:FORWARD - [0:0] -:OUTPUT - [0:0] -:MULTI-INGRESS - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS-COMMON - [0:0] -:MULTI-0-INGRESS - [0:0] -:MULTI-0-INGRESS-0-PORTS - [0:0] -:MULTI-0-INGRESS-0-FROM - [0:0] -:MULTI-0-EGRESS - [0:0] -:MULTI-0-EGRESS-0-PORTS - [0:0] -:MULTI-0-EGRESS-0-TO - [0:0] --A INPUT -i net1 -j MULTI-INGRESS --A OUTPUT -o net1 -j MULTI-EGRESS --A MULTI-INGRESS -j MULTI-INGRESS-COMMON --A MULTI-INGRESS -m comment --comment "policy:ipblock-override-policy net-attach-def:testns1/net-attach1" -i net1 -j MULTI-0-INGRESS --A MULTI-INGRESS -m mark --mark 0x30000/0x30000 -j RETURN --A MULTI-INGRESS -j DROP --A MULTI-EGRESS -j MULTI-EGRESS-COMMON --A MULTI-EGRESS -m comment --comment "policy:ipblock-override-policy net-attach-def:testns1/net-attach1" -o net1 -j MULTI-0-EGRESS --A MULTI-EGRESS -m mark --mark 0x30000/0x30000 -j RETURN --A MULTI-EGRESS -j DROP --A MULTI-INGRESS-COMMON -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT --A MULTI-EGRESS-COMMON -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT --A MULTI-0-INGRESS -j MARK --set-xmark 0x0/0x30000 --A MULTI-0-INGRESS -j MULTI-0-INGRESS-0-PORTS --A MULTI-0-INGRESS -j MULTI-0-INGRESS-0-FROM --A MULTI-0-INGRESS -m mark --mark 0x30000/0x30000 -j RETURN --A MULTI-0-INGRESS-0-PORTS -m comment --comment "no ingress ports, skipped" -j MARK --set-xmark 0x10000/0x10000 --A MULTI-0-INGRESS-0-FROM -i net1 -s 1.1.1.1 -j DROP --A MULTI-0-INGRESS-0-FROM -i net1 -s 1.1.1.0/16 -j MARK --set-xmark 0x20000/0x20000 --A MULTI-0-INGRESS-0-FROM -i net1 -s 10.1.1.1 -j MARK --set-xmark 0x20000/0x20000 --A MULTI-0-EGRESS -j MARK --set-xmark 0x0/0x30000 --A MULTI-0-EGRESS -j MULTI-0-EGRESS-0-PORTS --A MULTI-0-EGRESS -j MULTI-0-EGRESS-0-TO --A MULTI-0-EGRESS -m mark --mark 0x30000/0x30000 -j RETURN --A MULTI-0-EGRESS-0-PORTS -m comment --comment "no egress ports, skipped" -j MARK --set-xmark 0x10000/0x10000 --A MULTI-0-EGRESS-0-TO -o net1 -d 2.2.2.2 -j DROP --A MULTI-0-EGRESS-0-TO -o net1 -d 2.2.2.0/16 -j MARK --set-xmark 0x20000/0x20000 --A MULTI-0-EGRESS-0-TO -o net1 -d 10.1.1.1 -j MARK --set-xmark 0x20000/0x20000 -COMMIT -*mangle -COMMIT -`)) - - }) - - Context("IPv6", func() { - It("shoud avoid using IPv4 addresses on ip6tables", func() { - - policy1 := &multiv1beta1.MultiNetworkPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "ingressPolicies1", - Namespace: "testns1", - }, - Spec: multiv1beta1.MultiNetworkPolicySpec{ - Ingress: []multiv1beta1.MultiNetworkPolicyIngressRule{{ - From: []multiv1beta1.MultiNetworkPolicyPeer{{ - PodSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "foobar": "enabled", - }, - }, - }}, - }}, - Egress: []multiv1beta1.MultiNetworkPolicyEgressRule{{ - To: []multiv1beta1.MultiNetworkPolicyPeer{{ - PodSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "foobar": "enabled", - }, - }, - }}, - }}, - }, - } - - s := NewFakeServer("samplehost") - Expect(s).NotTo(BeNil()) - - AddNamespace(s, "testns1") - - Expect( - s.netdefChanges.Update(nil, NewNetDef("testns1", "net-attach1", NewCNIConfig("testCNI", "multi"))), - ).To(BeTrue()) - - pod1 := NewFakePodWithNetAnnotation( - "testns1", - "testpod1", - "net-attach1", - NewFakeNetworkStatus("testns1", "net-attach1", "192.168.1.1", "10.1.1.1"), - nil) - AddPod(s, pod1) - podInfo1, err := s.podMap.GetPodInfo(pod1) - Expect(err).NotTo(HaveOccurred()) - - pod2 := NewFakePodWithNetAnnotation( - "testns1", - "testpod2", - "net-attach1", - NewFakeNetworkStatus("testns1", "net-attach1", "192.168.1.2", "10.1.1.2"), - map[string]string{ - "foobar": "enabled", - }) - AddPod(s, pod2) - - ipt := fakeiptables.NewIPv6Fake() - buf := newIptableBuffer() - buf.Init(ipt) - - buf.renderIngress(s, podInfo1, 0, policy1, []string{"testns1/net-attach1"}) - buf.renderEgress(s, podInfo1, 0, policy1, []string{"testns1/net-attach1"}) - - buf.FinalizeRules() - - expectedRules := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] -:MULTI-0-INGRESS - [0:0] -:MULTI-0-INGRESS-0-PORTS - [0:0] -:MULTI-0-INGRESS-0-FROM - [0:0] -:MULTI-0-EGRESS - [0:0] -:MULTI-0-EGRESS-0-PORTS - [0:0] -:MULTI-0-EGRESS-0-TO - [0:0] --A MULTI-INGRESS -m comment --comment "policy:ingressPolicies1 net-attach-def:testns1/net-attach1" -i net1 -j MULTI-0-INGRESS --A MULTI-INGRESS -m mark --mark 0x30000/0x30000 -j RETURN --A MULTI-0-INGRESS -j MARK --set-xmark 0x0/0x30000 --A MULTI-0-INGRESS -j MULTI-0-INGRESS-0-PORTS --A MULTI-0-INGRESS -j MULTI-0-INGRESS-0-FROM --A MULTI-0-INGRESS -m mark --mark 0x30000/0x30000 -j RETURN --A MULTI-EGRESS -m comment --comment "policy:ingressPolicies1 net-attach-def:testns1/net-attach1" -o net1 -j MULTI-0-EGRESS --A MULTI-EGRESS -m mark --mark 0x30000/0x30000 -j RETURN --A MULTI-0-EGRESS -j MARK --set-xmark 0x0/0x30000 --A MULTI-0-EGRESS -j MULTI-0-EGRESS-0-PORTS --A MULTI-0-EGRESS -j MULTI-0-EGRESS-0-TO --A MULTI-0-EGRESS -m mark --mark 0x30000/0x30000 -j RETURN --A MULTI-0-INGRESS-0-PORTS -m comment --comment "no ingress ports, skipped" -j MARK --set-xmark 0x10000/0x10000 --A MULTI-0-EGRESS-0-PORTS -m comment --comment "no egress ports, skipped" -j MARK --set-xmark 0x10000/0x10000 -COMMIT -` - - Expect(buf.filterRules.String()).To(Equal(expectedRules), buf.filterRules.String) - }) - - It("shoud manage dual stack networks", func() { - - policy1 := &multiv1beta1.MultiNetworkPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "ingressPolicies1", - Namespace: "testns1", - }, - Spec: multiv1beta1.MultiNetworkPolicySpec{ - Ingress: []multiv1beta1.MultiNetworkPolicyIngressRule{{ - From: []multiv1beta1.MultiNetworkPolicyPeer{{ - PodSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "foobar": "enabled", - }, - }, - }}, - }}, - Egress: []multiv1beta1.MultiNetworkPolicyEgressRule{{ - To: []multiv1beta1.MultiNetworkPolicyPeer{{ - PodSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "foobar": "enabled", - }, - }, - }}, - }}, - }, - } - - s := NewFakeServer("samplehost") - Expect(s).NotTo(BeNil()) - - AddNamespace(s, "testns1") - - Expect( - s.netdefChanges.Update(nil, NewNetDef("testns1", "net-attach1", NewCNIConfig("testCNI", "multi"))), - ).To(BeTrue()) - - pod1 := NewFakePodWithNetAnnotation( - "testns1", - "testpod1", - "net-attach1", - NewFakeNetworkStatus("testns1", "net-attach1", "192.168.1.1", "10.1.1.1\",\"2001:db8:a::11"), - nil) - AddPod(s, pod1) - podInfo1, err := s.podMap.GetPodInfo(pod1) - Expect(err).NotTo(HaveOccurred()) - - pod2 := NewFakePodWithNetAnnotation( - "testns1", - "testpod2", - "net-attach1", - NewFakeNetworkStatus("testns1", "net-attach1", "192.168.1.2", "10.1.1.2\",\"2001:db8:a::12"), - map[string]string{ - "foobar": "enabled", - }) - AddPod(s, pod2) - _, err = s.podMap.GetPodInfo(pod2) - Expect(err).NotTo(HaveOccurred()) - - ipt := fakeiptables.NewIPv6Fake() - buf := newIptableBuffer() - buf.Init(ipt) - - buf.renderIngress(s, podInfo1, 0, policy1, []string{"testns1/net-attach1"}) - buf.renderEgress(s, podInfo1, 0, policy1, []string{"testns1/net-attach1"}) - - buf.FinalizeRules() - - expectedRules := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] -:MULTI-0-INGRESS - [0:0] -:MULTI-0-INGRESS-0-PORTS - [0:0] -:MULTI-0-INGRESS-0-FROM - [0:0] -:MULTI-0-EGRESS - [0:0] -:MULTI-0-EGRESS-0-PORTS - [0:0] -:MULTI-0-EGRESS-0-TO - [0:0] --A MULTI-INGRESS -m comment --comment "policy:ingressPolicies1 net-attach-def:testns1/net-attach1" -i net1 -j MULTI-0-INGRESS --A MULTI-INGRESS -m mark --mark 0x30000/0x30000 -j RETURN --A MULTI-0-INGRESS -j MARK --set-xmark 0x0/0x30000 --A MULTI-0-INGRESS -j MULTI-0-INGRESS-0-PORTS --A MULTI-0-INGRESS -j MULTI-0-INGRESS-0-FROM --A MULTI-0-INGRESS -m mark --mark 0x30000/0x30000 -j RETURN --A MULTI-EGRESS -m comment --comment "policy:ingressPolicies1 net-attach-def:testns1/net-attach1" -o net1 -j MULTI-0-EGRESS --A MULTI-EGRESS -m mark --mark 0x30000/0x30000 -j RETURN --A MULTI-0-EGRESS -j MARK --set-xmark 0x0/0x30000 --A MULTI-0-EGRESS -j MULTI-0-EGRESS-0-PORTS --A MULTI-0-EGRESS -j MULTI-0-EGRESS-0-TO --A MULTI-0-EGRESS -m mark --mark 0x30000/0x30000 -j RETURN --A MULTI-0-INGRESS-0-PORTS -m comment --comment "no ingress ports, skipped" -j MARK --set-xmark 0x10000/0x10000 --A MULTI-0-INGRESS-0-FROM -i net1 -s 2001:db8:a::12 -j MARK --set-xmark 0x20000/0x20000 --A MULTI-0-INGRESS-0-FROM -i net1 -s 2001:db8:a::11 -j MARK --set-xmark 0x20000/0x20000 --A MULTI-0-EGRESS-0-PORTS -m comment --comment "no egress ports, skipped" -j MARK --set-xmark 0x10000/0x10000 --A MULTI-0-EGRESS-0-TO -o net1 -d 2001:db8:a::12 -j MARK --set-xmark 0x20000/0x20000 --A MULTI-0-EGRESS-0-TO -o net1 -d 2001:db8:a::11 -j MARK --set-xmark 0x20000/0x20000 -COMMIT -` - Expect(buf.filterRules.String()).To(Equal(expectedRules)) - }) - }) -}) - -var _ = Describe("policyrules testing - invalid case", func() { - It("Initialization", func() { - ipt := fakeiptables.NewFake() - Expect(ipt).NotTo(BeNil()) - buf := newIptableBuffer() - Expect(buf).NotTo(BeNil()) - - // verify buf initialized at init - buf.Init(ipt) - filterChains := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] -` - Expect(buf.filterChains.String()).To(Equal(filterChains)) - Expect(buf.policyIndex.String()).To(Equal("")) - Expect(buf.ingressPorts.String()).To(Equal("")) - Expect(buf.ingressFrom.String()).To(Equal("")) - Expect(buf.egressPorts.String()).To(Equal("")) - Expect(buf.egressTo.String()).To(Equal("")) - - // finalize buf and verify rules buffer - buf.FinalizeRules() - filterRules := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] -COMMIT -` - Expect(buf.filterRules.String()).To(Equal(filterRules)) - - // sync and verify iptable - Expect(buf.SyncRules(ipt)).To(BeNil()) - iptableRules := bytes.NewBuffer(nil) - ipt.SaveInto(utiliptables.TableFilter, iptableRules) - tableRules := - `*filter -:INPUT - [0:0] -:FORWARD - [0:0] -:OUTPUT - [0:0] -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] -COMMIT -` - Expect(iptableRules.String()).To(Equal(tableRules)) - - // reset and verify empty - buf.Reset() - Expect(buf.policyIndex.String()).To(Equal("")) - Expect(buf.ingressPorts.String()).To(Equal("")) - Expect(buf.ingressFrom.String()).To(Equal("")) - Expect(buf.egressPorts.String()).To(Equal("")) - Expect(buf.egressTo.String()).To(Equal("")) - }) - - It("ingress rules ipblock", func() { - port := intstr.FromInt(8888) - protoTCP := v1.ProtocolTCP - ingressPolicies1 := &multiv1beta1.MultiNetworkPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "ingressPolicies1", - Namespace: "testns1", - }, - Spec: multiv1beta1.MultiNetworkPolicySpec{ - Ingress: []multiv1beta1.MultiNetworkPolicyIngressRule{ - { - Ports: []multiv1beta1.MultiNetworkPolicyPort{ - { - Protocol: &protoTCP, - Port: &port, - }, - }, - From: []multiv1beta1.MultiNetworkPolicyPeer{ - { - IPBlock: &multiv1beta1.IPBlock{ - CIDR: "10.1.1.1/24", - Except: []string{"10.1.1.1"}, - }, - }, - }, - }, - }, - }, - } - - ipt := fakeiptables.NewFake() - Expect(ipt).NotTo(BeNil()) - buf := newIptableBuffer() - Expect(buf).NotTo(BeNil()) - - // verify buf initialized at init - buf.Init(ipt) - s := NewFakeServer("samplehost") - Expect(s).NotTo(BeNil()) - - Expect(s.netdefChanges.Update( - nil, - NewNetDef("testns1", "net-attach1", NewCNIConfig("testCNI", "multi")))).To(BeTrue()) - Expect(s.netdefChanges.GetPluginType(types.NamespacedName{Namespace: "testns1", Name: "net-attach1"})).To(Equal("multi")) - - pod1 := NewFakePodWithNetAnnotation( - "testns1", - "testpod1", - "net-attach1", - NewFakeNetworkStatus("testns1", "net-attach1", "192.168.1.1", "10.1.1.1"), - nil) - AddPod(s, pod1) - podInfo1, err := s.podMap.GetPodInfo(pod1) - Expect(err).NotTo(HaveOccurred()) - - buf.renderIngress(s, podInfo1, 0, ingressPolicies1, []string{}) - - buf.FinalizeRules() - finalizedRules := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] -:MULTI-0-INGRESS - [0:0] -:MULTI-0-INGRESS-0-PORTS - [0:0] -:MULTI-0-INGRESS-0-FROM - [0:0] --A MULTI-0-INGRESS -j MARK --set-xmark 0x0/0x30000 --A MULTI-0-INGRESS -j MULTI-0-INGRESS-0-PORTS --A MULTI-0-INGRESS -j MULTI-0-INGRESS-0-FROM --A MULTI-0-INGRESS -m mark --mark 0x30000/0x30000 -j RETURN --A MULTI-0-INGRESS-0-PORTS -m comment --comment "no ingress ports, skipped" -j MARK --set-xmark 0x10000/0x10000 -COMMIT -` - Expect(buf.filterRules.String()).To(Equal(finalizedRules), buf.filterRules.String()) - }) - - It("ingress rules podselector/matchlabels", func() { - port := intstr.FromInt(8888) - protoTCP := v1.ProtocolTCP - ingressPolicies1 := &multiv1beta1.MultiNetworkPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "ingressPolicies1", - Namespace: "testns1", - }, - Spec: multiv1beta1.MultiNetworkPolicySpec{ - Ingress: []multiv1beta1.MultiNetworkPolicyIngressRule{ - { - Ports: []multiv1beta1.MultiNetworkPolicyPort{ - { - Protocol: &protoTCP, - Port: &port, - }, - }, - From: []multiv1beta1.MultiNetworkPolicyPeer{ - { - PodSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "foobar": "enabled", - }, - }, - }, - }, - }, - }, - }, - } - - ipt := fakeiptables.NewFake() - Expect(ipt).NotTo(BeNil()) - buf := newIptableBuffer() - Expect(buf).NotTo(BeNil()) - - // verify buf initialized at init - buf.Init(ipt) - s := NewFakeServer("samplehost") - Expect(s).NotTo(BeNil()) - - AddNamespace(s, "testns1") - - Expect(s.netdefChanges.Update( - nil, - NewNetDef("testns1", "net-attach1", NewCNIConfig("testCNI", "multi")))).To(BeTrue()) - Expect(s.netdefChanges.GetPluginType(types.NamespacedName{Namespace: "testns1", Name: "net-attach1"})).To(Equal("multi")) - - pod1 := NewFakePodWithNetAnnotation( - "testns1", - "testpod1", - "net-attach1", - NewFakeNetworkStatus("testns1", "net-attach1", "192.168.1.1", "10.1.1.1"), - nil) - AddPod(s, pod1) - podInfo1, err := s.podMap.GetPodInfo(pod1) - Expect(err).NotTo(HaveOccurred()) - - pod2 := NewFakePodWithNetAnnotation( - "testns1", - "testpod2", - "net-attach1", - NewFakeNetworkStatus("testns1", "net-attach1", "192.168.1.2", "10.1.1.2"), - map[string]string{ - "foobar": "enabled", - }) - AddPod(s, pod2) - - buf.renderIngress(s, podInfo1, 0, ingressPolicies1, []string{}) - - buf.FinalizeRules() - finalizedRules := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] -:MULTI-0-INGRESS - [0:0] -:MULTI-0-INGRESS-0-PORTS - [0:0] -:MULTI-0-INGRESS-0-FROM - [0:0] --A MULTI-0-INGRESS -j MARK --set-xmark 0x0/0x30000 --A MULTI-0-INGRESS -j MULTI-0-INGRESS-0-PORTS --A MULTI-0-INGRESS -j MULTI-0-INGRESS-0-FROM --A MULTI-0-INGRESS -m mark --mark 0x30000/0x30000 -j RETURN --A MULTI-0-INGRESS-0-PORTS -m comment --comment "no ingress ports, skipped" -j MARK --set-xmark 0x10000/0x10000 -COMMIT -` - Expect(buf.filterRules.String()).To(Equal(finalizedRules), buf.filterRules.String()) - }) - - It("egress rules ipblock", func() { - port := intstr.FromInt(8888) - protoTCP := v1.ProtocolTCP - egressPolicies1 := &multiv1beta1.MultiNetworkPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "EgressPolicies1", - Namespace: "testns1", - }, - Spec: multiv1beta1.MultiNetworkPolicySpec{ - Egress: []multiv1beta1.MultiNetworkPolicyEgressRule{ - { - Ports: []multiv1beta1.MultiNetworkPolicyPort{ - { - Protocol: &protoTCP, - Port: &port, - }, - }, - To: []multiv1beta1.MultiNetworkPolicyPeer{ - { - IPBlock: &multiv1beta1.IPBlock{ - CIDR: "10.1.1.1/24", - Except: []string{"10.1.1.1"}, - }, - }, - }, - }, - }, - }, - } - - ipt := fakeiptables.NewFake() - Expect(ipt).NotTo(BeNil()) - buf := newIptableBuffer() - Expect(buf).NotTo(BeNil()) - - // verify buf initialized at init - buf.Init(ipt) - s := NewFakeServer("samplehost") - Expect(s).NotTo(BeNil()) - - Expect(s.netdefChanges.Update( - nil, - NewNetDef("testns1", "net-attach1", NewCNIConfig("testCNI", "multi")))).To(BeTrue()) - Expect(s.netdefChanges.GetPluginType(types.NamespacedName{Namespace: "testns1", Name: "net-attach1"})).To(Equal("multi")) - - pod1 := NewFakePodWithNetAnnotation( - "testns1", - "testpod1", - "net-attach1", - NewFakeNetworkStatus("testns1", "net-attach1", "192.168.1.1", "10.1.1.1"), - nil) - AddPod(s, pod1) - podInfo1, err := s.podMap.GetPodInfo(pod1) - Expect(err).NotTo(HaveOccurred()) - - buf.renderEgress(s, podInfo1, 0, egressPolicies1, []string{}) - - buf.FinalizeRules() - finalizedRules := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] -:MULTI-0-EGRESS - [0:0] -:MULTI-0-EGRESS-0-PORTS - [0:0] -:MULTI-0-EGRESS-0-TO - [0:0] --A MULTI-0-EGRESS -j MARK --set-xmark 0x0/0x30000 --A MULTI-0-EGRESS -j MULTI-0-EGRESS-0-PORTS --A MULTI-0-EGRESS -j MULTI-0-EGRESS-0-TO --A MULTI-0-EGRESS -m mark --mark 0x30000/0x30000 -j RETURN --A MULTI-0-EGRESS-0-PORTS -m comment --comment "no egress ports, skipped" -j MARK --set-xmark 0x10000/0x10000 -COMMIT -` - Expect(buf.filterRules.String()).To(Equal(finalizedRules)) - }) - - It("egress rules podselector/matchlabels", func() { - port := intstr.FromInt(8888) - protoTCP := v1.ProtocolTCP - egressPolicies1 := &multiv1beta1.MultiNetworkPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "EgressPolicies1", - Namespace: "testns1", - }, - Spec: multiv1beta1.MultiNetworkPolicySpec{ - Egress: []multiv1beta1.MultiNetworkPolicyEgressRule{ - { - Ports: []multiv1beta1.MultiNetworkPolicyPort{ - { - Protocol: &protoTCP, - Port: &port, - }, - }, - To: []multiv1beta1.MultiNetworkPolicyPeer{ - { - PodSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "foobar": "enabled", - }, - }, - }, - }, - }, - }, - }, - } - - ipt := fakeiptables.NewFake() - Expect(ipt).NotTo(BeNil()) - buf := newIptableBuffer() - Expect(buf).NotTo(BeNil()) - - // verify buf initialized at init - buf.Init(ipt) - s := NewFakeServer("samplehost") - Expect(s).NotTo(BeNil()) - - AddNamespace(s, "testns1") - - Expect(s.netdefChanges.Update( - nil, - NewNetDef("testns1", "net-attach1", NewCNIConfig("testCNI", "multi")))).To(BeTrue()) - Expect(s.netdefChanges.GetPluginType(types.NamespacedName{Namespace: "testns1", Name: "net-attach1"})).To(Equal("multi")) - - pod1 := NewFakePodWithNetAnnotation( - "testns1", - "testpod1", - "net-attach1", - NewFakeNetworkStatus("testns1", "net-attach1", "192.168.1.1", "10.1.1.1"), - nil) - AddPod(s, pod1) - podInfo1, err := s.podMap.GetPodInfo(pod1) - Expect(err).NotTo(HaveOccurred()) - - pod2 := NewFakePodWithNetAnnotation( - "testns1", - "testpod2", - "net-attach1", - NewFakeNetworkStatus("testns1", "net-attach1", "192.168.1.2", "10.1.1.2"), - map[string]string{ - "foobar": "enabled", - }) - AddPod(s, pod2) - - buf.renderEgress(s, podInfo1, 0, egressPolicies1, []string{"testns2/net-attach1"}) - - buf.FinalizeRules() - finalizedRules := - `*filter -:MULTI-INGRESS - [0:0] -:MULTI-INGRESS-COMMON - [0:0] -:MULTI-EGRESS - [0:0] -:MULTI-EGRESS-COMMON - [0:0] -:MULTI-0-EGRESS - [0:0] -:MULTI-0-EGRESS-0-PORTS - [0:0] -:MULTI-0-EGRESS-0-TO - [0:0] --A MULTI-0-EGRESS -j MARK --set-xmark 0x0/0x30000 --A MULTI-0-EGRESS -j MULTI-0-EGRESS-0-PORTS --A MULTI-0-EGRESS -j MULTI-0-EGRESS-0-TO --A MULTI-0-EGRESS -m mark --mark 0x30000/0x30000 -j RETURN --A MULTI-0-EGRESS-0-PORTS -m comment --comment "no egress ports, skipped" -j MARK --set-xmark 0x10000/0x10000 -COMMIT -` - Expect(buf.filterRules.String()).To(Equal(finalizedRules)) - }) - -}) diff --git a/pkg/server/server.go b/pkg/server/server.go deleted file mode 100644 index d267e0cf..00000000 --- a/pkg/server/server.go +++ /dev/null @@ -1,725 +0,0 @@ -/* -Copyright 2020 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package server - -import ( - "bytes" - "fmt" - "os" - "strings" - "sync" - "sync/atomic" - "time" - - "github.com/containernetworking/plugins/pkg/ns" - - "github.com/k8snetworkplumbingwg/multi-networkpolicy-iptables/pkg/controllers" - multiutils "github.com/k8snetworkplumbingwg/multi-networkpolicy-iptables/pkg/utils" - multiv1beta1 "github.com/k8snetworkplumbingwg/multi-networkpolicy/pkg/apis/k8s.cni.cncf.io/v1beta1" - multiclient "github.com/k8snetworkplumbingwg/multi-networkpolicy/pkg/client/clientset/versioned" - multiinformer "github.com/k8snetworkplumbingwg/multi-networkpolicy/pkg/client/informers/externalversions" - multilisterv1beta1 "github.com/k8snetworkplumbingwg/multi-networkpolicy/pkg/client/listers/k8s.cni.cncf.io/v1beta1" - netdefv1 "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/apis/k8s.cni.cncf.io/v1" - netdefclient "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/client/clientset/versioned" - netdefinformerv1 "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/client/informers/externalversions" - - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/wait" - "k8s.io/client-go/informers" - clientset "k8s.io/client-go/kubernetes" - "k8s.io/client-go/kubernetes/scheme" - v1core "k8s.io/client-go/kubernetes/typed/core/v1" - corelisters "k8s.io/client-go/listers/core/v1" - "k8s.io/client-go/rest" - "k8s.io/client-go/tools/clientcmd" - clientcmdapi "k8s.io/client-go/tools/clientcmd/api" - "k8s.io/client-go/tools/record" - nodeutil "k8s.io/component-helpers/node/util" - "k8s.io/klog" - api "k8s.io/kubernetes/pkg/apis/core" - "k8s.io/kubernetes/pkg/proxy/runner" - utiliptables "k8s.io/kubernetes/pkg/util/iptables" -) - -const defaultSyncPeriod = 30 - -// Server structure defines data for server -type Server struct { - podChanges *controllers.PodChangeTracker - policyChanges *controllers.PolicyChangeTracker - netdefChanges *controllers.NetDefChangeTracker - nsChanges *controllers.NamespaceChangeTracker - mu sync.Mutex // protects the following fields - podMap controllers.PodMap - policyMap controllers.PolicyMap - namespaceMap controllers.NamespaceMap - Client clientset.Interface - Hostname string - hostPrefix string - NetworkPolicyClient multiclient.Interface - NetDefClient netdefclient.Interface - Broadcaster record.EventBroadcaster - Recorder record.EventRecorder - Options *Options - ConfigSyncPeriod time.Duration - NodeRef *v1.ObjectReference - ip4Tables utiliptables.Interface - ip6Tables utiliptables.Interface - - initialized int32 - - podSynced bool - policySynced bool - netdefSynced bool - nsSynced bool - - podLister corelisters.PodLister - policyLister multilisterv1beta1.MultiNetworkPolicyLister - - syncRunner *runner.BoundedFrequencyRunner - syncRunnerStopCh chan struct{} -} - -// RunPodConfig ... -func (s *Server) RunPodConfig() { - klog.Infof("Starting pod config") - informerFactory := informers.NewSharedInformerFactoryWithOptions(s.Client, s.ConfigSyncPeriod) - s.podLister = informerFactory.Core().V1().Pods().Lister() - - podConfig := controllers.NewPodConfig(informerFactory.Core().V1().Pods(), s.ConfigSyncPeriod) - podConfig.RegisterEventHandler(s) - go podConfig.Run(wait.NeverStop) - informerFactory.Start(wait.NeverStop) - s.SyncLoop() -} - -// Run ... -func (s *Server) Run(_ string, stopCh chan struct{}) { - if s.Broadcaster != nil { - s.Broadcaster.StartRecordingToSink( - &v1core.EventSinkImpl{Interface: s.Client.CoreV1().Events("")}) - } - - informerFactory := informers.NewSharedInformerFactoryWithOptions(s.Client, s.ConfigSyncPeriod) - nsConfig := controllers.NewNamespaceConfig(informerFactory.Core().V1().Namespaces(), s.ConfigSyncPeriod) - nsConfig.RegisterEventHandler(s) - go nsConfig.Run(wait.NeverStop) - informerFactory.Start(wait.NeverStop) - - policyInformerFactory := multiinformer.NewSharedInformerFactoryWithOptions( - s.NetworkPolicyClient, s.ConfigSyncPeriod) - s.policyLister = policyInformerFactory.K8sCniCncfIo().V1beta1().MultiNetworkPolicies().Lister() - - policyConfig := controllers.NewNetworkPolicyConfig( - policyInformerFactory.K8sCniCncfIo().V1beta1().MultiNetworkPolicies(), s.ConfigSyncPeriod) - policyConfig.RegisterEventHandler(s) - go policyConfig.Run(wait.NeverStop) - policyInformerFactory.Start(wait.NeverStop) - - netdefInformarFactory := netdefinformerv1.NewSharedInformerFactoryWithOptions( - s.NetDefClient, s.ConfigSyncPeriod) - netdefConfig := controllers.NewNetDefConfig( - netdefInformarFactory.K8sCniCncfIo().V1().NetworkAttachmentDefinitions(), s.ConfigSyncPeriod) - netdefConfig.RegisterEventHandler(s) - go netdefConfig.Run(wait.NeverStop) - netdefInformarFactory.Start(wait.NeverStop) - - s.birthCry() - - // Wait for stop signal - <-stopCh - - // Stop the sync runner loop - s.syncRunnerStopCh <- struct{}{} - - // Delete all iptables by running the `syncMultiPolicy` with no MultiNetworkPolicies - s.policyMap = nil - s.syncMultiPolicy() -} - -func (s *Server) setInitialized(value bool) { - var initialized int32 - if value { - initialized = 1 - } - atomic.StoreInt32(&s.initialized, initialized) -} - -func (s *Server) isInitialized() bool { - return atomic.LoadInt32(&s.initialized) > 0 -} - -func (s *Server) birthCry() { - klog.Infof("Starting network-policy-node") - s.Recorder.Eventf(s.NodeRef, api.EventTypeNormal, "Starting", "Starting network-policy-node.") -} - -// SyncLoop ... -func (s *Server) SyncLoop() { - s.syncRunner.Loop(s.syncRunnerStopCh) -} - -// NewServer ... -func NewServer(o *Options) (*Server, error) { - var kubeConfig *rest.Config - var err error - if len(o.Kubeconfig) == 0 { - klog.Info("Neither kubeconfig file nor master URL was specified. Falling back to in-cluster config.") - kubeConfig, err = rest.InClusterConfig() - } else { - kubeConfig, err = clientcmd.NewNonInteractiveDeferredLoadingClientConfig( - &clientcmd.ClientConfigLoadingRules{ExplicitPath: o.Kubeconfig}, - &clientcmd.ConfigOverrides{ClusterInfo: clientcmdapi.Cluster{Server: o.master}}, - ).ClientConfig() - } - if err != nil { - return nil, fmt.Errorf("server creation failed for kubeconfig [%s] master URL [%s]: %w", o.Kubeconfig, o.master, err) - } - - if o.podIptables != "" { - // cleanup current pod iptables directory if it exists - if _, err := os.Stat(o.podIptables); err == nil || !os.IsNotExist(err) { - err = os.RemoveAll(o.podIptables) - if err != nil { - return nil, fmt.Errorf("server creation failed while deleting pod iptables directory [%s]: %w", o.podIptables, err) - } - } - // create pod iptables directory - err = os.Mkdir(o.podIptables, 0700) - if err != nil { - return nil, fmt.Errorf("server creation failed while creating pod iptables directory [%s]: %w", o.podIptables, err) - } - } - - client, err := clientset.NewForConfig(kubeConfig) - if err != nil { - return nil, fmt.Errorf("server creation failed while creating clientset for kubeconfig [%s]: %w", kubeConfig, err) - } - - networkPolicyClient, err := multiclient.NewForConfig(kubeConfig) - if err != nil { - return nil, fmt.Errorf("server creation failed while multi network policy creating clientset for kubeconfig [%s]: %w", kubeConfig, err) - } - - netdefClient, err := netdefclient.NewForConfig(kubeConfig) - if err != nil { - return nil, fmt.Errorf("server creation failed while creating net-attach-def clientset for kubeconfig [%s]: %w", kubeConfig, err) - } - - hostname, err := nodeutil.GetHostname(o.hostnameOverride) - if err != nil { - return nil, fmt.Errorf("server creation failed while getting hostname with override [%s]: %w", o.hostnameOverride, err) - } - - eventBroadcaster := record.NewBroadcaster() - recorder := eventBroadcaster.NewRecorder( - scheme.Scheme, - v1.EventSource{Component: "multi-networkpolicy-node", Host: hostname}) - - nodeRef := &v1.ObjectReference{ - Kind: "Node", - Name: hostname, - UID: types.UID(hostname), - Namespace: "", - } - - syncPeriod := time.Duration(o.syncPeriod) * time.Second - minSyncPeriod := 0 * time.Second - retryInterval := 5 * time.Second - - policyChanges := controllers.NewPolicyChangeTracker() - if policyChanges == nil { - return nil, fmt.Errorf("cannot create policy change tracker") - } - netdefChanges := controllers.NewNetDefChangeTracker() - if netdefChanges == nil { - return nil, fmt.Errorf("cannot create net-attach-def change tracker") - } - nsChanges := controllers.NewNamespaceChangeTracker() - if nsChanges == nil { - return nil, fmt.Errorf("cannot create namespace change tracker") - } - podChanges := controllers.NewPodChangeTracker(o.containerRuntime, o.containerRuntimeEndpoint, hostname, o.hostPrefix, o.networkPlugins, netdefChanges) - if podChanges == nil { - return nil, fmt.Errorf("cannot create pod change tracker") - } - - server := &Server{ - Options: o, - Client: client, - Hostname: hostname, - hostPrefix: o.hostPrefix, - NetworkPolicyClient: networkPolicyClient, - NetDefClient: netdefClient, - Broadcaster: eventBroadcaster, - Recorder: recorder, - ConfigSyncPeriod: 15 * time.Minute, - NodeRef: nodeRef, - ip4Tables: utiliptables.New(utiliptables.ProtocolIPv4), - ip6Tables: utiliptables.New(utiliptables.ProtocolIPv6), - - policyChanges: policyChanges, - podChanges: podChanges, - netdefChanges: netdefChanges, - nsChanges: nsChanges, - podMap: make(controllers.PodMap), - policyMap: make(controllers.PolicyMap), - namespaceMap: make(controllers.NamespaceMap), - } - server.syncRunner = runner.NewBoundedFrequencyRunner( - "sync-runner", server.syncMultiPolicy, minSyncPeriod, retryInterval, syncPeriod) - server.syncRunnerStopCh = make(chan struct{}) - return server, nil -} - -// Sync ... -func (s *Server) Sync() { - klog.V(4).Infof("Sync Done!") - if s.syncRunner != nil { - s.syncRunner.Run() - } -} - -// AllSynced ... -func (s *Server) AllSynced() bool { - return (s.policySynced == true && s.netdefSynced == true && s.nsSynced == true) -} - -// OnPodAdd ... -func (s *Server) OnPodAdd(pod *v1.Pod) { - klog.V(4).Infof("OnPodUpdate") - s.OnPodUpdate(nil, pod) -} - -// OnPodUpdate ... -func (s *Server) OnPodUpdate(oldPod, pod *v1.Pod) { - klog.V(4).Infof("OnPodUpdate %s -> %s", podNamespacedName(oldPod), podNamespacedName(pod)) - if s.podChanges.Update(oldPod, pod) && s.podSynced { - s.Sync() - } -} - -// OnPodDelete ... -func (s *Server) OnPodDelete(pod *v1.Pod) { - klog.V(4).Infof("OnPodDelete") - s.OnPodUpdate(pod, nil) - if multiutils.CheckNodeNameIdentical(s.Hostname, pod.Spec.NodeName) { - podIptables := fmt.Sprintf("%s/%s", s.Options.podIptables, pod.UID) - if _, err := os.Stat(podIptables); err == nil { - err := os.RemoveAll(podIptables) - if err != nil { - klog.Errorf("cannot remove pod dir(%s): %v", podIptables, err) - } - } - } -} - -// OnPodSynced ... -func (s *Server) OnPodSynced() { - klog.Infof("OnPodSynced") - s.mu.Lock() - s.podSynced = true - s.setInitialized(s.podSynced) - s.mu.Unlock() - - s.Sync() -} - -// OnPolicyAdd ... -func (s *Server) OnPolicyAdd(policy *multiv1beta1.MultiNetworkPolicy) { - klog.V(4).Infof("OnPolicyAdd") - s.OnPolicyUpdate(nil, policy) -} - -// OnPolicyUpdate ... -func (s *Server) OnPolicyUpdate(oldPolicy, policy *multiv1beta1.MultiNetworkPolicy) { - klog.V(4).Infof("OnPolicyUpdate %s -> %s", policyNamespacedName(oldPolicy), policyNamespacedName(policy)) - if s.policyChanges.Update(oldPolicy, policy) && s.isInitialized() { - s.Sync() - } -} - -// OnPolicyDelete ... -func (s *Server) OnPolicyDelete(policy *multiv1beta1.MultiNetworkPolicy) { - klog.V(4).Infof("OnPolicyDelete") - s.OnPolicyUpdate(policy, nil) -} - -// OnPolicySynced ... -func (s *Server) OnPolicySynced() { - klog.Infof("OnPolicySynced") - s.mu.Lock() - s.policySynced = true - s.setInitialized(s.policySynced) - s.mu.Unlock() - - if s.AllSynced() { - s.RunPodConfig() - } -} - -// OnNetDefAdd ... -func (s *Server) OnNetDefAdd(net *netdefv1.NetworkAttachmentDefinition) { - klog.V(4).Infof("OnNetDefAdd") - s.OnNetDefUpdate(nil, net) -} - -// OnNetDefUpdate ... -func (s *Server) OnNetDefUpdate(oldNet, net *netdefv1.NetworkAttachmentDefinition) { - klog.V(4).Infof("OnNetDefUpdate %s -> %s", nadNamespacedName(oldNet), nadNamespacedName(net)) - if s.netdefChanges.Update(oldNet, net) && s.isInitialized() { - s.Sync() - } -} - -// OnNetDefDelete ... -func (s *Server) OnNetDefDelete(net *netdefv1.NetworkAttachmentDefinition) { - klog.V(4).Infof("OnNetDefDelete") - s.OnNetDefUpdate(net, nil) -} - -// OnNetDefSynced ... -func (s *Server) OnNetDefSynced() { - klog.Infof("OnNetDefSynced") - s.mu.Lock() - s.netdefSynced = true - s.setInitialized(s.netdefSynced) - s.mu.Unlock() - - if s.AllSynced() { - s.RunPodConfig() - } -} - -// OnNamespaceAdd ... -func (s *Server) OnNamespaceAdd(ns *v1.Namespace) { - klog.V(4).Infof("OnNamespaceAdd") - s.OnNamespaceUpdate(nil, ns) -} - -// OnNamespaceUpdate ... -func (s *Server) OnNamespaceUpdate(oldNamespace, ns *v1.Namespace) { - klog.V(4).Infof("OnNamespaceUpdate: %s -> %s", namespaceName(oldNamespace), namespaceName(ns)) - if s.nsChanges.Update(oldNamespace, ns) && s.isInitialized() { - s.Sync() - } -} - -// OnNamespaceDelete ... -func (s *Server) OnNamespaceDelete(ns *v1.Namespace) { - klog.V(4).Infof("OnNamespaceDelete") - s.OnNamespaceUpdate(ns, nil) -} - -// OnNamespaceSynced ... -func (s *Server) OnNamespaceSynced() { - klog.Infof("OnNamespaceSynced") - s.mu.Lock() - s.nsSynced = true - s.setInitialized(s.nsSynced) - s.mu.Unlock() - - if s.AllSynced() { - s.RunPodConfig() - } -} - -func (s *Server) syncMultiPolicy() error { - klog.V(4).Infof("syncMultiPolicy") - s.namespaceMap.Update(s.nsChanges) - s.podMap.Update(s.podChanges) - s.policyMap.Update(s.policyChanges) - - pods, err := s.podLister.Pods(metav1.NamespaceAll).List(labels.Everything()) - if err != nil { - klog.Errorf("failed to get pods: %v", err) - return fmt.Errorf("failed to list pods for sync: %w", err) - } - var syncError error - for _, p := range pods { - s.podMap.Update(s.podChanges) - if !controllers.IsMultiNetworkpolicyTarget(p) { - klog.V(8).Infof("SKIP SYNC %s/%s", p.Namespace, p.Name) - continue - } - klog.V(8).Infof("SYNC %s/%s", p.Namespace, p.Name) - if multiutils.CheckNodeNameIdentical(s.Hostname, p.Spec.NodeName) { - s.podMap.Update(s.podChanges) - podInfo, err := s.podMap.GetPodInfo(p) - if err != nil { - klog.Errorf("cannot get %s/%s podInfo: %v", p.Namespace, p.Name, err) - if syncError == nil { - syncError = fmt.Errorf("failed to get pod info for %s/%s: %w", p.Namespace, p.Name, err) - } - continue - } - if len(podInfo.Interfaces) == 0 { - klog.V(8).Infof("skipped due to no interfaces") - continue - } - netnsPath := podInfo.NetNSPath - if s.hostPrefix != "" { - netnsPath = fmt.Sprintf("%s/%s", s.hostPrefix, netnsPath) - } - - netns, err := ns.GetNS(netnsPath) - if err != nil { - klog.Errorf("cannot get pod (%s/%s:%s) netns (%s): %v", p.Namespace, p.Name, p.Status.Phase, netnsPath, err) - if syncError == nil { - syncError = fmt.Errorf("failed to get netns for %s/%s: %w", p.Namespace, p.Name, err) - } - continue - } - - klog.V(8).Infof("pod: %s/%s %s", p.Namespace, p.Name, netnsPath) - err = netns.Do(func(_ ns.NetNS) error { - return s.generatePolicyRulesForPod(p, podInfo) - }) - if err != nil { - klog.Errorf("failed to apply policy rules for pod [%s/%s]: %v", p.Namespace, p.Name, err) - if syncError == nil { - syncError = fmt.Errorf("failed to apply policy rules for %s/%s: %w", p.Namespace, p.Name, err) - } - } - } else { - klog.V(8).Infof("SYNC %s/%s: skipped", p.Namespace, p.Name) - } - } - return syncError -} - -func (s *Server) backupIptablesRules(pod *v1.Pod, suffix string, iptables utiliptables.Interface) error { - // skip it if no podiptables option - if s.Options.podIptables == "" { - return nil - } - - podIptables := fmt.Sprintf("%s/%s", s.Options.podIptables, pod.UID) - // create directory for pod if not exist - if _, err := os.Stat(podIptables); os.IsNotExist(err) { - err := os.Mkdir(podIptables, 0700) - if err != nil { - klog.Errorf("cannot create pod dir (%s): %v", podIptables, err) - return err - } - } - fileExt := "iptables" - if iptables.IsIPv6() { - fileExt = "ip6tables" - } - file, err := os.Create(fmt.Sprintf("%s/%s.%s", podIptables, suffix, fileExt)) - if err != nil { - klog.Errorf("cannot create pod file %s/%s.%s: %v", podIptables, suffix, fileExt, err) - return err - } - defer file.Close() - var buffer bytes.Buffer - - // store iptable result to file - //XXX: need error handling? (see kube-proxy) - _ = iptables.SaveInto(utiliptables.TableMangle, &buffer) - _ = iptables.SaveInto(utiliptables.TableFilter, &buffer) - _ = iptables.SaveInto(utiliptables.TableNAT, &buffer) - _, err = buffer.WriteTo(file) - - return err -} - -const ( - ingressChain = "MULTI-INGRESS" - egressChain = "MULTI-EGRESS" - ingressCommonChain = "MULTI-INGRESS-COMMON" - egressCommonChain = "MULTI-EGRESS-COMMON" -) - -func (s *Server) generatePolicyRulesForPod(pod *v1.Pod, podInfo *controllers.PodInfo) error { - err := s.generatePolicyRulesForPodAndFamily(pod, podInfo, s.ip4Tables) - if err != nil { - return fmt.Errorf("can't generate iptables for pod [%s]: %w", podNamespacedName(pod), err) - } - - err = s.generatePolicyRulesForPodAndFamily(pod, podInfo, s.ip6Tables) - if err != nil { - return fmt.Errorf("can't generate ip6tables for pod [%s]: %w", podNamespacedName(pod), err) - } - - return nil -} - -func (s *Server) generatePolicyRulesForPodAndFamily(pod *v1.Pod, podInfo *controllers.PodInfo, iptables utiliptables.Interface) error { - klog.V(4).Infof("Generate rules for Pod: %v/%v\n", podInfo.Namespace, podInfo.Name) - // -t filter -N MULTI-INGRESS # ensure chain - iptables.EnsureChain(utiliptables.TableFilter, ingressChain) - // -t filter -N MULTI-EGRESS # ensure chain - iptables.EnsureChain(utiliptables.TableFilter, egressChain) - // -t filter -N MULTI-INGRESS-COMMON # ensure chain - iptables.EnsureChain(utiliptables.TableFilter, ingressCommonChain) - // -t filter -N MULTI-EGRESS-COMMON # ensure chain - iptables.EnsureChain(utiliptables.TableFilter, egressCommonChain) - - for _, multiIF := range podInfo.Interfaces { - // -A INPUT -j MULTI-INGRESS # ensure rules - iptables.EnsureRule( - utiliptables.Prepend, utiliptables.TableFilter, "INPUT", "-i", multiIF.InterfaceName, "-j", ingressChain) - // -A OUTPUT -j MULTI-EGRESS # ensure rules - iptables.EnsureRule( - utiliptables.Prepend, utiliptables.TableFilter, "OUTPUT", "-o", multiIF.InterfaceName, "-j", egressChain) - // -A PREROUTING -i net1 -j RETURN # ensure rules - iptables.EnsureRule( - utiliptables.Prepend, utiliptables.TableNAT, "PREROUTING", "-i", multiIF.InterfaceName, "-j", "RETURN") - } - // -A MULTI-INGRESS -j MULTI-INGRESS-COMMON # ensure rules - iptables.EnsureRule( - utiliptables.Prepend, utiliptables.TableFilter, ingressChain, "-j", ingressCommonChain) - // -A MULTI-EGRESS -j MULTI-EGRESS-COMMON # ensure rules - iptables.EnsureRule( - utiliptables.Prepend, utiliptables.TableFilter, egressChain, "-j", egressCommonChain) - - iptableBuffer := newIptableBuffer() - iptableBuffer.Init(iptables) - iptableBuffer.Reset() - - idx := 0 - ingressRendered := 0 - egressRendered := 0 - for _, p := range s.policyMap { - policy := p.Policy - if policy.GetNamespace() != pod.Namespace { - continue - } - if policy.Spec.PodSelector.Size() != 0 { - policyMap, err := metav1.LabelSelectorAsMap(&policy.Spec.PodSelector) - if err != nil { - klog.Errorf("bad label selector for policy [%s]: %v", policyNamespacedName(policy), err) - continue - } - policyPodSelector := labels.Set(policyMap).AsSelectorPreValidated() - if !policyPodSelector.Matches(labels.Set(pod.Labels)) { - continue - } - } - - ingressEnable, egressEnable := getEnabledPolicyTypes(policy) - klog.V(8).Infof("ingress/egress = %v/%v\n", ingressEnable, egressEnable) - - policyNetworksAnnot, ok := policy.GetAnnotations()[PolicyNetworkAnnotation] - if !ok { - continue - } - policyNetworksAnnot = strings.ReplaceAll(policyNetworksAnnot, " ", "") - policyNetworks := strings.Split(policyNetworksAnnot, ",") - for pidx, networkName := range policyNetworks { - // fill namespace - if strings.IndexAny(networkName, "/") == -1 { - policyNetworks[pidx] = fmt.Sprintf("%s/%s", policy.GetNamespace(), networkName) - } - } - - if podInfo.CheckPolicyNetwork(policyNetworks) { - if ingressEnable { - iptableBuffer.renderIngressCommon(s) - iptableBuffer.renderIngress(s, podInfo, idx, policy, policyNetworks) - ingressRendered++ - } - if egressEnable { - iptableBuffer.renderEgressCommon(s) - iptableBuffer.renderEgress(s, podInfo, idx, policy, policyNetworks) - egressRendered++ - } - idx++ - } - } - if ingressRendered != 0 { - writeLine(iptableBuffer.policyIndex, "-A", "MULTI-INGRESS", "-j", "DROP") - } - if egressRendered != 0 { - writeLine(iptableBuffer.policyIndex, "-A", "MULTI-EGRESS", "-j", "DROP") - } - - if !iptableBuffer.IsUsed() { - iptableBuffer.Init(iptables) - } - - iptableBuffer.FinalizeRules() - - /* store generated iptables rules if podIptables is enabled */ - if s.Options.podIptables != "" { - if iptables.IsIPv6() { - filePath := fmt.Sprintf("%s/%s/networkpolicy.ip6tables", s.Options.podIptables, pod.UID) - iptableBuffer.SaveRules(filePath) - } else { - filePath := fmt.Sprintf("%s/%s/networkpolicy.iptables", s.Options.podIptables, pod.UID) - iptableBuffer.SaveRules(filePath) - } - } - - if err := iptableBuffer.SyncRules(iptables); err != nil { - klog.Errorf("sync rules failed for pod [%s]: %v", podNamespacedName(pod), err) - return err - } - - s.backupIptablesRules(pod, "current", iptables) - - return nil -} - -func podNamespacedName(o *v1.Pod) string { - if o == nil { - return "" - } - return o.GetNamespace() + "/" + o.GetName() -} - -func namespaceName(o *v1.Namespace) string { - if o == nil { - return "" - } - return o.GetName() -} - -func policyNamespacedName(o *multiv1beta1.MultiNetworkPolicy) string { - if o == nil { - return "" - } - return o.GetNamespace() + "/" + o.GetName() -} - -func nadNamespacedName(o *netdefv1.NetworkAttachmentDefinition) string { - if o == nil { - return "" - } - return o.GetNamespace() + "/" + o.GetName() -} - -func getEnabledPolicyTypes(policy *multiv1beta1.MultiNetworkPolicy) (bool, bool) { - var ingressEnable, egressEnable bool - if len(policy.Spec.PolicyTypes) > 0 { - for _, v := range policy.Spec.PolicyTypes { - if strings.EqualFold(string(v), string(multiv1beta1.PolicyTypeIngress)) { - ingressEnable = true - } else if strings.EqualFold(string(v), string(multiv1beta1.PolicyTypeEgress)) { - egressEnable = true - } - } - return ingressEnable, egressEnable - } - - return len(policy.Spec.Ingress) > 0, len(policy.Spec.Egress) > 0 -} diff --git a/pkg/server/server_suite_test.go b/pkg/server/server_suite_test.go deleted file mode 100644 index 2d73861a..00000000 --- a/pkg/server/server_suite_test.go +++ /dev/null @@ -1,29 +0,0 @@ -/* -Copyright 2020 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package server - -import ( - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - - "testing" -) - -func TestServer(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "server") -} diff --git a/pkg/utils/doc.go b/pkg/utils/doc.go deleted file mode 100644 index fbf689e2..00000000 --- a/pkg/utils/doc.go +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) 2021 Multus Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package utils is the package that contains utility functions. -package utils diff --git a/pkg/utils/node.go b/pkg/utils/node.go deleted file mode 100644 index 5c09e2dd..00000000 --- a/pkg/utils/node.go +++ /dev/null @@ -1,27 +0,0 @@ -/* -Copyright 2020 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package utils - -import ( - "strings" -) - -// CheckNodeNameIdentical checks both strings point a same node -// it just checks hostname without domain -func CheckNodeNameIdentical(s1, s2 string) bool { - return strings.Split(s1, ".")[0] == strings.Split(s2, ".")[0] -} diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go new file mode 100644 index 00000000..e1bfff4c --- /dev/null +++ b/pkg/utils/utils.go @@ -0,0 +1,116 @@ +package utils + +import ( + "bufio" + "crypto/sha256" + "fmt" + "net" + "os" + "strings" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" +) + +// ParseCommaSeparatedList parses a comma-separated string into a slice of non-empty strings. +func ParseCommaSeparatedList(input string) ([]string, error) { + if input == "" { + return nil, fmt.Errorf("input string cannot be empty") + } + + elements := strings.Split(input, ",") + result := make([]string, 0, len(elements)) + + for _, element := range elements { + trimmed := strings.TrimSpace(element) + if trimmed != "" { + result = append(result, trimmed) + } + } + + if len(result) == 0 { + return nil, fmt.Errorf("no valid elements found in comma-separated list: %q", input) + } + + return result, nil +} + +// GetHashName returns the first 16 characters of the SHA256 hash of the namespace name +func GetHashName(name, namespace string) string { + namespaceName := fmt.Sprintf("%s-%s", name, namespace) + + hash := sha256.Sum256([]byte(namespaceName)) + return fmt.Sprintf("%x", hash[:16]) +} + +// MatchesSelector checks if the pod labels match the given label selector +func MatchesSelector(selector metav1.LabelSelector, podLabels map[string]string) bool { + // Convert the metav1.LabelSelector to a labels.Selector + labelSelector, err := metav1.LabelSelectorAsSelector(&selector) + if err != nil { + // If the selector is invalid, we don't match + return false + } + + if labelSelector.Empty() { + return true + } + + // Convert pod labels to a labels.Set and check if it matches + podLabelSet := labels.Set(podLabels) + return labelSelector.Matches(podLabelSet) +} + +// SplitCIDRs splits the CIDRs into IPv4 and IPv6 CIDRs +func SplitCIDRs(cidrs []string) ([]string, []string) { + var ipv4CIDRs []string + var ipv6CIDRs []string + + for _, cidr := range cidrs { + // Parse the CIDR to validate and classify it + parsedIP, _, err := net.ParseCIDR(cidr) + if err != nil { + continue + } + + // Check if it's IPv4 (including IPv4-mapped IPv6) + if parsedIP.To4() != nil { + ipv4CIDRs = append(ipv4CIDRs, cidr) + } else { + ipv6CIDRs = append(ipv6CIDRs, cidr) + } + } + + return ipv4CIDRs, ipv6CIDRs +} + +// ReadRulesFromFile reads rules from a file +func ReadRulesFromFile(filePath string) ([]string, error) { + var rules []string + + if filePath == "" { + return nil, fmt.Errorf("file path cannot be empty") + } + + f, err := os.Open(filePath) + if err != nil { + return nil, err + } + defer f.Close() + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + rule := scanner.Text() + if rule == "" || strings.HasPrefix(rule, "#") { + continue + } + + rules = append(rules, rule) + } + + if err = scanner.Err(); err != nil { + return nil, err + } + + return rules, nil +} diff --git a/pkg/utils/utils_test.go b/pkg/utils/utils_test.go new file mode 100644 index 00000000..ca226cce --- /dev/null +++ b/pkg/utils/utils_test.go @@ -0,0 +1,317 @@ +package utils + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestUtils(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Utils Suite") +} + +var _ = Describe("ParseCommaSeparatedList", func() { + Context("with valid input", func() { + It("should parse simple comma-separated list", func() { + result, err := ParseCommaSeparatedList("a,b,c") + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal([]string{"a", "b", "c"})) + }) + + It("should trim whitespace from elements", func() { + result, err := ParseCommaSeparatedList(" a , b , c ") + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal([]string{"a", "b", "c"})) + }) + + It("should filter out empty elements", func() { + result, err := ParseCommaSeparatedList("a,,b,,,c") + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal([]string{"a", "b", "c"})) + }) + + It("should handle single element", func() { + result, err := ParseCommaSeparatedList("single") + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal([]string{"single"})) + }) + + It("should handle single element with whitespace", func() { + result, err := ParseCommaSeparatedList(" single ") + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal([]string{"single"})) + }) + }) + + Context("with invalid input", func() { + It("should return error for empty string", func() { + result, err := ParseCommaSeparatedList("") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("input string cannot be empty")) + Expect(result).To(BeNil()) + }) + + It("should return error for string with only commas", func() { + result, err := ParseCommaSeparatedList(",,,") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("no valid elements found")) + Expect(result).To(BeNil()) + }) + + It("should return error for string with only whitespace and commas", func() { + result, err := ParseCommaSeparatedList(" , , ") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("no valid elements found")) + Expect(result).To(BeNil()) + }) + }) + + Describe("GetHashName", func() { + It("should return consistent hash for same input", func() { + hash1 := GetHashName("test-policy", "test-namespace") + hash2 := GetHashName("test-policy", "test-namespace") + Expect(hash1).To(Equal(hash2)) + }) + + It("should return different hashes for different inputs", func() { + hash1 := GetHashName("policy1", "namespace1") + hash2 := GetHashName("policy2", "namespace2") + Expect(hash1).NotTo(Equal(hash2)) + }) + + It("should return different hashes for same name but different namespace", func() { + hash1 := GetHashName("test-policy", "namespace1") + hash2 := GetHashName("test-policy", "namespace2") + Expect(hash1).NotTo(Equal(hash2)) + }) + + It("should return different hashes for different name but same namespace", func() { + hash1 := GetHashName("policy1", "test-namespace") + hash2 := GetHashName("policy2", "test-namespace") + Expect(hash1).NotTo(Equal(hash2)) + }) + + It("should return 32 character hex string", func() { + hash := GetHashName("test-policy", "test-namespace") + Expect(hash).To(HaveLen(32)) + Expect(hash).To(MatchRegexp("^[a-f0-9]{32}$")) + }) + + It("should handle empty strings", func() { + hash1 := GetHashName("", "") + hash2 := GetHashName("", "test") + hash3 := GetHashName("test", "") + + Expect(hash1).To(HaveLen(32)) + Expect(hash2).To(HaveLen(32)) + Expect(hash3).To(HaveLen(32)) + + // All should be different + Expect(hash1).NotTo(Equal(hash2)) + Expect(hash1).NotTo(Equal(hash3)) + Expect(hash2).NotTo(Equal(hash3)) + }) + }) + + Describe("MatchesPodSelector", func() { + Context("when selector is empty", func() { + It("should match any pod", func() { + selector := metav1.LabelSelector{} + podLabels := map[string]string{"app": "test", "version": "v1"} + + result := MatchesSelector(selector, podLabels) + Expect(result).To(BeTrue()) + }) + }) + + Context("when selector matches pod labels", func() { + It("should return true", func() { + selector := metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "test"}, + } + podLabels := map[string]string{"app": "test", "version": "v1"} + + result := MatchesSelector(selector, podLabels) + Expect(result).To(BeTrue()) + }) + }) + + Context("when selector does not match pod labels", func() { + It("should return false", func() { + selector := metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "different"}, + } + podLabels := map[string]string{"app": "test", "version": "v1"} + + result := MatchesSelector(selector, podLabels) + Expect(result).To(BeFalse()) + }) + }) + }) + + Context("splitCIDRs", func() { + It("should return empty slices for empty input", func() { + cidrs := []string{} + + ipv4, ipv6 := SplitCIDRs(cidrs) + Expect(ipv4).To(BeEmpty()) + Expect(ipv6).To(BeEmpty()) + }) + + It("should split IPv4 and IPv6 CIDRs correctly", func() { + cidrs := []string{ + "10.0.0.0/24", // IPv4 + "192.168.1.0/24", // IPv4 + "2001:db8::/32", // IPv6 + "fe80::/64", // IPv6 + "172.16.0.0/16", // IPv4 + "::1/128", // IPv6 loopback + } + + ipv4, ipv6 := SplitCIDRs(cidrs) + + Expect(ipv4).To(HaveLen(3)) + Expect(ipv4).To(ContainElements("10.0.0.0/24", "192.168.1.0/24", "172.16.0.0/16")) + + Expect(ipv6).To(HaveLen(3)) + Expect(ipv6).To(ContainElements("2001:db8::/32", "fe80::/64", "::1/128")) + }) + + It("should handle only IPv4 CIDRs", func() { + cidrs := []string{ + "10.0.0.0/8", + "192.168.0.0/16", + "172.16.0.0/12", + "127.0.0.0/8", + } + + ipv4, ipv6 := SplitCIDRs(cidrs) + + Expect(ipv4).To(HaveLen(4)) + Expect(ipv4).To(ContainElements("10.0.0.0/8", "192.168.0.0/16", "172.16.0.0/12", "127.0.0.0/8")) + Expect(ipv6).To(BeEmpty()) + }) + + It("should handle only IPv6 CIDRs", func() { + cidrs := []string{ + "2001:db8::/32", + "fe80::/10", + "::1/128", + "2001:4860:4860::8888/128", + } + + ipv4, ipv6 := SplitCIDRs(cidrs) + + Expect(ipv4).To(BeEmpty()) + Expect(ipv6).To(HaveLen(4)) + Expect(ipv6).To(ContainElements("2001:db8::/32", "fe80::/10", "::1/128", "2001:4860:4860::8888/128")) + }) + + It("should skip invalid CIDRs", func() { + cidrs := []string{ + "10.0.0.0/24", // Valid IPv4 + "invalid-cidr", // Invalid + "2001:db8::/32", // Valid IPv6 + "300.400.500.600/24", // Invalid IPv4 + "192.168.1.0/33", // Invalid prefix length for IPv4 + "fe80::/64", // Valid IPv6 + "not-a-cidr", // Invalid + "2001:db8::/129", // Invalid prefix length for IPv6 + } + + ipv4, ipv6 := SplitCIDRs(cidrs) + + // Only valid CIDRs should be included + Expect(ipv4).To(HaveLen(1)) + Expect(ipv4).To(ContainElement("10.0.0.0/24")) + + Expect(ipv6).To(HaveLen(2)) + Expect(ipv6).To(ContainElements("2001:db8::/32", "fe80::/64")) + }) + + It("should handle IPv4-mapped IPv6 addresses as IPv4", func() { + cidrs := []string{ + "::ffff:192.168.1.0/120", // IPv4-mapped IPv6 + "::ffff:10.0.0.0/120", // IPv4-mapped IPv6 + "2001:db8::/32", // Pure IPv6 + "192.168.1.0/24", // Pure IPv4 + } + + ipv4, ipv6 := SplitCIDRs(cidrs) + + // IPv4-mapped IPv6 should be classified as IPv4 + Expect(ipv4).To(HaveLen(3)) + Expect(ipv4).To(ContainElements("::ffff:192.168.1.0/120", "::ffff:10.0.0.0/120", "192.168.1.0/24")) + + Expect(ipv6).To(HaveLen(1)) + Expect(ipv6).To(ContainElement("2001:db8::/32")) + }) + + It("should handle single host addresses with /32 and /128", func() { + cidrs := []string{ + "192.168.1.1/32", // Single IPv4 host + "10.0.0.1/32", // Single IPv4 host + "2001:db8::1/128", // Single IPv6 host + "fe80::1/128", // Single IPv6 host + } + + ipv4, ipv6 := SplitCIDRs(cidrs) + + Expect(ipv4).To(HaveLen(2)) + Expect(ipv4).To(ContainElements("192.168.1.1/32", "10.0.0.1/32")) + + Expect(ipv6).To(HaveLen(2)) + Expect(ipv6).To(ContainElements("2001:db8::1/128", "fe80::1/128")) + }) + + It("should handle edge case prefix lengths", func() { + cidrs := []string{ + "0.0.0.0/0", // IPv4 default route + "192.168.1.0/32", // IPv4 /32 + "::/0", // IPv6 default route + "2001:db8::1/128", // IPv6 /128 + } + + ipv4, ipv6 := SplitCIDRs(cidrs) + + Expect(ipv4).To(HaveLen(2)) + Expect(ipv4).To(ContainElements("0.0.0.0/0", "192.168.1.0/32")) + + Expect(ipv6).To(HaveLen(2)) + Expect(ipv6).To(ContainElements("::/0", "2001:db8::1/128")) + }) + + It("should handle mixed valid and invalid CIDRs preserving order", func() { + cidrs := []string{ + "10.0.0.0/24", // Valid IPv4 - should be first + "invalid", // Invalid - should be skipped + "172.16.0.0/16", // Valid IPv4 - should be second + "2001:db8::/32", // Valid IPv6 - should be first + "bad-cidr", // Invalid - should be skipped + "fe80::/64", // Valid IPv6 - should be second + } + + ipv4, ipv6 := SplitCIDRs(cidrs) + + // Verify correct classification and order preservation + Expect(ipv4).To(HaveLen(2)) + Expect(ipv4[0]).To(Equal("10.0.0.0/24")) + Expect(ipv4[1]).To(Equal("172.16.0.0/16")) + + Expect(ipv6).To(HaveLen(2)) + Expect(ipv6[0]).To(Equal("2001:db8::/32")) + Expect(ipv6[1]).To(Equal("fe80::/64")) + }) + + It("should handle nil input gracefully", func() { + var cidrs []string // nil slice + + ipv4, ipv6 := SplitCIDRs(cidrs) + Expect(ipv4).To(BeEmpty()) + Expect(ipv6).To(BeEmpty()) + }) + }) +}) diff --git a/vendor/github.com/Masterminds/semver/v3/.gitignore b/vendor/github.com/Masterminds/semver/v3/.gitignore new file mode 100644 index 00000000..6b061e61 --- /dev/null +++ b/vendor/github.com/Masterminds/semver/v3/.gitignore @@ -0,0 +1 @@ +_fuzz/ \ No newline at end of file diff --git a/vendor/github.com/Masterminds/semver/v3/.golangci.yml b/vendor/github.com/Masterminds/semver/v3/.golangci.yml new file mode 100644 index 00000000..fbc63325 --- /dev/null +++ b/vendor/github.com/Masterminds/semver/v3/.golangci.yml @@ -0,0 +1,27 @@ +run: + deadline: 2m + +linters: + disable-all: true + enable: + - misspell + - govet + - staticcheck + - errcheck + - unparam + - ineffassign + - nakedret + - gocyclo + - dupl + - goimports + - revive + - gosec + - gosimple + - typecheck + - unused + +linters-settings: + gofmt: + simplify: true + dupl: + threshold: 600 diff --git a/vendor/github.com/Masterminds/semver/v3/CHANGELOG.md b/vendor/github.com/Masterminds/semver/v3/CHANGELOG.md new file mode 100644 index 00000000..fabe5e43 --- /dev/null +++ b/vendor/github.com/Masterminds/semver/v3/CHANGELOG.md @@ -0,0 +1,268 @@ +# Changelog + +## 3.4.0 (2025-06-27) + +### Added + +- #268: Added property to Constraints to include prereleases for Check and Validate + +### Changed + +- #263: Updated Go testing for 1.24, 1.23, and 1.22 +- #269: Updated the error message handling for message case and wrapping errors +- #266: Restore the ability to have leading 0's when parsing with NewVersion. + Opt-out of this by setting CoerceNewVersion to false. + +### Fixed + +- #257: Fixed the CodeQL link (thanks @dmitris) +- #262: Restored detailed errors when failed to parse with NewVersion. Opt-out + of this by setting DetailedNewVersionErrors to false for faster performance. +- #267: Handle pre-releases for an "and" group if one constraint includes them + +## 3.3.1 (2024-11-19) + +### Fixed + +- #253: Fix for allowing some version that were invalid + +## 3.3.0 (2024-08-27) + +### Added + +- #238: Add LessThanEqual and GreaterThanEqual functions (thanks @grosser) +- #213: nil version equality checking (thanks @KnutZuidema) + +### Changed + +- #241: Simplify StrictNewVersion parsing (thanks @grosser) +- Testing support up through Go 1.23 +- Minimum version set to 1.21 as this is what's tested now +- Fuzz testing now supports caching + +## 3.2.1 (2023-04-10) + +### Changed + +- #198: Improved testing around pre-release names +- #200: Improved code scanning with addition of CodeQL +- #201: Testing now includes Go 1.20. Go 1.17 has been dropped +- #202: Migrated Fuzz testing to Go built-in Fuzzing. CI runs daily +- #203: Docs updated for security details + +### Fixed + +- #199: Fixed issue with range transformations + +## 3.2.0 (2022-11-28) + +### Added + +- #190: Added text marshaling and unmarshaling +- #167: Added JSON marshalling for constraints (thanks @SimonTheLeg) +- #173: Implement encoding.TextMarshaler and encoding.TextUnmarshaler on Version (thanks @MarkRosemaker) +- #179: Added New() version constructor (thanks @kazhuravlev) + +### Changed + +- #182/#183: Updated CI testing setup + +### Fixed + +- #186: Fixing issue where validation of constraint section gave false positives +- #176: Fix constraints check with *-0 (thanks @mtt0) +- #181: Fixed Caret operator (^) gives unexpected results when the minor version in constraint is 0 (thanks @arshchimni) +- #161: Fixed godoc (thanks @afirth) + +## 3.1.1 (2020-11-23) + +### Fixed + +- #158: Fixed issue with generated regex operation order that could cause problem + +## 3.1.0 (2020-04-15) + +### Added + +- #131: Add support for serializing/deserializing SQL (thanks @ryancurrah) + +### Changed + +- #148: More accurate validation messages on constraints + +## 3.0.3 (2019-12-13) + +### Fixed + +- #141: Fixed issue with <= comparison + +## 3.0.2 (2019-11-14) + +### Fixed + +- #134: Fixed broken constraint checking with ^0.0 (thanks @krmichelos) + +## 3.0.1 (2019-09-13) + +### Fixed + +- #125: Fixes issue with module path for v3 + +## 3.0.0 (2019-09-12) + +This is a major release of the semver package which includes API changes. The Go +API is compatible with ^1. The Go API was not changed because many people are using +`go get` without Go modules for their applications and API breaking changes cause +errors which we have or would need to support. + +The changes in this release are the handling based on the data passed into the +functions. These are described in the added and changed sections below. + +### Added + +- StrictNewVersion function. This is similar to NewVersion but will return an + error if the version passed in is not a strict semantic version. For example, + 1.2.3 would pass but v1.2.3 or 1.2 would fail because they are not strictly + speaking semantic versions. This function is faster, performs fewer operations, + and uses fewer allocations than NewVersion. +- Fuzzing has been performed on NewVersion, StrictNewVersion, and NewConstraint. + The Makefile contains the operations used. For more information on you can start + on Wikipedia at https://en.wikipedia.org/wiki/Fuzzing +- Now using Go modules + +### Changed + +- NewVersion has proper prerelease and metadata validation with error messages + to signal an issue with either of them +- ^ now operates using a similar set of rules to npm/js and Rust/Cargo. If the + version is >=1 the ^ ranges works the same as v1. For major versions of 0 the + rules have changed. The minor version is treated as the stable version unless + a patch is specified and then it is equivalent to =. One difference from npm/js + is that prereleases there are only to a specific version (e.g. 1.2.3). + Prereleases here look over multiple versions and follow semantic version + ordering rules. This pattern now follows along with the expected and requested + handling of this packaged by numerous users. + +## 1.5.0 (2019-09-11) + +### Added + +- #103: Add basic fuzzing for `NewVersion()` (thanks @jesse-c) + +### Changed + +- #82: Clarify wildcard meaning in range constraints and update tests for it (thanks @greysteil) +- #83: Clarify caret operator range for pre-1.0.0 dependencies (thanks @greysteil) +- #72: Adding docs comment pointing to vert for a cli +- #71: Update the docs on pre-release comparator handling +- #89: Test with new go versions (thanks @thedevsaddam) +- #87: Added $ to ValidPrerelease for better validation (thanks @jeremycarroll) + +### Fixed + +- #78: Fix unchecked error in example code (thanks @ravron) +- #70: Fix the handling of pre-releases and the 0.0.0 release edge case +- #97: Fixed copyright file for proper display on GitHub +- #107: Fix handling prerelease when sorting alphanum and num +- #109: Fixed where Validate sometimes returns wrong message on error + +## 1.4.2 (2018-04-10) + +### Changed + +- #72: Updated the docs to point to vert for a console appliaction +- #71: Update the docs on pre-release comparator handling + +### Fixed + +- #70: Fix the handling of pre-releases and the 0.0.0 release edge case + +## 1.4.1 (2018-04-02) + +### Fixed + +- Fixed #64: Fix pre-release precedence issue (thanks @uudashr) + +## 1.4.0 (2017-10-04) + +### Changed + +- #61: Update NewVersion to parse ints with a 64bit int size (thanks @zknill) + +## 1.3.1 (2017-07-10) + +### Fixed + +- Fixed #57: number comparisons in prerelease sometimes inaccurate + +## 1.3.0 (2017-05-02) + +### Added + +- #45: Added json (un)marshaling support (thanks @mh-cbon) +- Stability marker. See https://masterminds.github.io/stability/ + +### Fixed + +- #51: Fix handling of single digit tilde constraint (thanks @dgodd) + +### Changed + +- #55: The godoc icon moved from png to svg + +## 1.2.3 (2017-04-03) + +### Fixed + +- #46: Fixed 0.x.x and 0.0.x in constraints being treated as * + +## Release 1.2.2 (2016-12-13) + +### Fixed + +- #34: Fixed issue where hyphen range was not working with pre-release parsing. + +## Release 1.2.1 (2016-11-28) + +### Fixed + +- #24: Fixed edge case issue where constraint "> 0" does not handle "0.0.1-alpha" + properly. + +## Release 1.2.0 (2016-11-04) + +### Added + +- #20: Added MustParse function for versions (thanks @adamreese) +- #15: Added increment methods on versions (thanks @mh-cbon) + +### Fixed + +- Issue #21: Per the SemVer spec (section 9) a pre-release is unstable and + might not satisfy the intended compatibility. The change here ignores pre-releases + on constraint checks (e.g., ~ or ^) when a pre-release is not part of the + constraint. For example, `^1.2.3` will ignore pre-releases while + `^1.2.3-alpha` will include them. + +## Release 1.1.1 (2016-06-30) + +### Changed + +- Issue #9: Speed up version comparison performance (thanks @sdboyer) +- Issue #8: Added benchmarks (thanks @sdboyer) +- Updated Go Report Card URL to new location +- Updated Readme to add code snippet formatting (thanks @mh-cbon) +- Updating tagging to v[SemVer] structure for compatibility with other tools. + +## Release 1.1.0 (2016-03-11) + +- Issue #2: Implemented validation to provide reasons a versions failed a + constraint. + +## Release 1.0.1 (2015-12-31) + +- Fixed #1: * constraint failing on valid versions. + +## Release 1.0.0 (2015-10-20) + +- Initial release diff --git a/vendor/github.com/Masterminds/semver/v3/LICENSE.txt b/vendor/github.com/Masterminds/semver/v3/LICENSE.txt new file mode 100644 index 00000000..9ff7da9c --- /dev/null +++ b/vendor/github.com/Masterminds/semver/v3/LICENSE.txt @@ -0,0 +1,19 @@ +Copyright (C) 2014-2019, Matt Butcher and Matt Farina + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/github.com/Masterminds/semver/v3/Makefile b/vendor/github.com/Masterminds/semver/v3/Makefile new file mode 100644 index 00000000..9ca87a2c --- /dev/null +++ b/vendor/github.com/Masterminds/semver/v3/Makefile @@ -0,0 +1,31 @@ +GOPATH=$(shell go env GOPATH) +GOLANGCI_LINT=$(GOPATH)/bin/golangci-lint + +.PHONY: lint +lint: $(GOLANGCI_LINT) + @echo "==> Linting codebase" + @$(GOLANGCI_LINT) run + +.PHONY: test +test: + @echo "==> Running tests" + GO111MODULE=on go test -v + +.PHONY: test-cover +test-cover: + @echo "==> Running Tests with coverage" + GO111MODULE=on go test -cover . + +.PHONY: fuzz +fuzz: + @echo "==> Running Fuzz Tests" + go env GOCACHE + go test -fuzz=FuzzNewVersion -fuzztime=15s . + go test -fuzz=FuzzStrictNewVersion -fuzztime=15s . + go test -fuzz=FuzzNewConstraint -fuzztime=15s . + +$(GOLANGCI_LINT): + # Install golangci-lint. The configuration for it is in the .golangci.yml + # file in the root of the repository + echo ${GOPATH} + curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(GOPATH)/bin v1.56.2 diff --git a/vendor/github.com/Masterminds/semver/v3/README.md b/vendor/github.com/Masterminds/semver/v3/README.md new file mode 100644 index 00000000..2f56c676 --- /dev/null +++ b/vendor/github.com/Masterminds/semver/v3/README.md @@ -0,0 +1,274 @@ +# SemVer + +The `semver` package provides the ability to work with [Semantic Versions](http://semver.org) in Go. Specifically it provides the ability to: + +* Parse semantic versions +* Sort semantic versions +* Check if a semantic version fits within a set of constraints +* Optionally work with a `v` prefix + +[![Stability: +Active](https://masterminds.github.io/stability/active.svg)](https://masterminds.github.io/stability/active.html) +[![](https://github.com/Masterminds/semver/workflows/Tests/badge.svg)](https://github.com/Masterminds/semver/actions) +[![GoDoc](https://img.shields.io/static/v1?label=godoc&message=reference&color=blue)](https://pkg.go.dev/github.com/Masterminds/semver/v3) +[![Go Report Card](https://goreportcard.com/badge/github.com/Masterminds/semver)](https://goreportcard.com/report/github.com/Masterminds/semver) + +## Package Versions + +Note, import `github.com/Masterminds/semver/v3` to use the latest version. + +There are three major versions fo the `semver` package. + +* 3.x.x is the stable and active version. This version is focused on constraint + compatibility for range handling in other tools from other languages. It has + a similar API to the v1 releases. The development of this version is on the master + branch. The documentation for this version is below. +* 2.x was developed primarily for [dep](https://github.com/golang/dep). There are + no tagged releases and the development was performed by [@sdboyer](https://github.com/sdboyer). + There are API breaking changes from v1. This version lives on the [2.x branch](https://github.com/Masterminds/semver/tree/2.x). +* 1.x.x is the original release. It is no longer maintained. You should use the + v3 release instead. You can read the documentation for the 1.x.x release + [here](https://github.com/Masterminds/semver/blob/release-1/README.md). + +## Parsing Semantic Versions + +There are two functions that can parse semantic versions. The `StrictNewVersion` +function only parses valid version 2 semantic versions as outlined in the +specification. The `NewVersion` function attempts to coerce a version into a +semantic version and parse it. For example, if there is a leading v or a version +listed without all 3 parts (e.g. `v1.2`) it will attempt to coerce it into a valid +semantic version (e.g., 1.2.0). In both cases a `Version` object is returned +that can be sorted, compared, and used in constraints. + +When parsing a version an error is returned if there is an issue parsing the +version. For example, + + v, err := semver.NewVersion("1.2.3-beta.1+build345") + +The version object has methods to get the parts of the version, compare it to +other versions, convert the version back into a string, and get the original +string. Getting the original string is useful if the semantic version was coerced +into a valid form. + +There are package level variables that affect how `NewVersion` handles parsing. + +- `CoerceNewVersion` is `true` by default. When set to `true` it coerces non-compliant + versions into SemVer. For example, allowing a leading 0 in a major, minor, or patch + part. This enables the use of CalVer in versions even when not compliant with SemVer. + When set to `false` less coercion work is done. +- `DetailedNewVersionErrors` provides more detailed errors. It only has an affect when + `CoerceNewVersion` is set to `false`. When `DetailedNewVersionErrors` is set to `true` + it can provide some more insight into why a version is invalid. Setting + `DetailedNewVersionErrors` to `false` is faster on performance but provides less + detailed error messages if a version fails to parse. + +## Sorting Semantic Versions + +A set of versions can be sorted using the `sort` package from the standard library. +For example, + +```go +raw := []string{"1.2.3", "1.0", "1.3", "2", "0.4.2",} +vs := make([]*semver.Version, len(raw)) +for i, r := range raw { + v, err := semver.NewVersion(r) + if err != nil { + t.Errorf("Error parsing version: %s", err) + } + + vs[i] = v +} + +sort.Sort(semver.Collection(vs)) +``` + +## Checking Version Constraints + +There are two methods for comparing versions. One uses comparison methods on +`Version` instances and the other uses `Constraints`. There are some important +differences to notes between these two methods of comparison. + +1. When two versions are compared using functions such as `Compare`, `LessThan`, + and others it will follow the specification and always include pre-releases + within the comparison. It will provide an answer that is valid with the + comparison section of the spec at https://semver.org/#spec-item-11 +2. When constraint checking is used for checks or validation it will follow a + different set of rules that are common for ranges with tools like npm/js + and Rust/Cargo. This includes considering pre-releases to be invalid if the + ranges does not include one. If you want to have it include pre-releases a + simple solution is to include `-0` in your range. +3. Constraint ranges can have some complex rules including the shorthand use of + ~ and ^. For more details on those see the options below. + +There are differences between the two methods or checking versions because the +comparison methods on `Version` follow the specification while comparison ranges +are not part of the specification. Different packages and tools have taken it +upon themselves to come up with range rules. This has resulted in differences. +For example, npm/js and Cargo/Rust follow similar patterns while PHP has a +different pattern for ^. The comparison features in this package follow the +npm/js and Cargo/Rust lead because applications using it have followed similar +patters with their versions. + +Checking a version against version constraints is one of the most featureful +parts of the package. + +```go +c, err := semver.NewConstraint(">= 1.2.3") +if err != nil { + // Handle constraint not being parsable. +} + +v, err := semver.NewVersion("1.3") +if err != nil { + // Handle version not being parsable. +} +// Check if the version meets the constraints. The variable a will be true. +a := c.Check(v) +``` + +### Basic Comparisons + +There are two elements to the comparisons. First, a comparison string is a list +of space or comma separated AND comparisons. These are then separated by || (OR) +comparisons. For example, `">= 1.2 < 3.0.0 || >= 4.2.3"` is looking for a +comparison that's greater than or equal to 1.2 and less than 3.0.0 or is +greater than or equal to 4.2.3. + +The basic comparisons are: + +* `=`: equal (aliased to no operator) +* `!=`: not equal +* `>`: greater than +* `<`: less than +* `>=`: greater than or equal to +* `<=`: less than or equal to + +### Working With Prerelease Versions + +Pre-releases, for those not familiar with them, are used for software releases +prior to stable or generally available releases. Examples of pre-releases include +development, alpha, beta, and release candidate releases. A pre-release may be +a version such as `1.2.3-beta.1` while the stable release would be `1.2.3`. In the +order of precedence, pre-releases come before their associated releases. In this +example `1.2.3-beta.1 < 1.2.3`. + +According to the Semantic Version specification, pre-releases may not be +API compliant with their release counterpart. It says, + +> A pre-release version indicates that the version is unstable and might not satisfy the intended compatibility requirements as denoted by its associated normal version. + +SemVer's comparisons using constraints without a pre-release comparator will skip +pre-release versions. For example, `>=1.2.3` will skip pre-releases when looking +at a list of releases while `>=1.2.3-0` will evaluate and find pre-releases. + +The reason for the `0` as a pre-release version in the example comparison is +because pre-releases can only contain ASCII alphanumerics and hyphens (along with +`.` separators), per the spec. Sorting happens in ASCII sort order, again per the +spec. The lowest character is a `0` in ASCII sort order +(see an [ASCII Table](http://www.asciitable.com/)) + +Understanding ASCII sort ordering is important because A-Z comes before a-z. That +means `>=1.2.3-BETA` will return `1.2.3-alpha`. What you might expect from case +sensitivity doesn't apply here. This is due to ASCII sort ordering which is what +the spec specifies. + +The `Constraints` instance returned from `semver.NewConstraint()` has a property +`IncludePrerelease` that, when set to true, will return prerelease versions when calls +to `Check()` and `Validate()` are made. + +### Hyphen Range Comparisons + +There are multiple methods to handle ranges and the first is hyphens ranges. +These look like: + +* `1.2 - 1.4.5` which is equivalent to `>= 1.2 <= 1.4.5` +* `2.3.4 - 4.5` which is equivalent to `>= 2.3.4 <= 4.5` + +Note that `1.2-1.4.5` without whitespace is parsed completely differently; it's +parsed as a single constraint `1.2.0` with _prerelease_ `1.4.5`. + +### Wildcards In Comparisons + +The `x`, `X`, and `*` characters can be used as a wildcard character. This works +for all comparison operators. When used on the `=` operator it falls +back to the patch level comparison (see tilde below). For example, + +* `1.2.x` is equivalent to `>= 1.2.0, < 1.3.0` +* `>= 1.2.x` is equivalent to `>= 1.2.0` +* `<= 2.x` is equivalent to `< 3` +* `*` is equivalent to `>= 0.0.0` + +### Tilde Range Comparisons (Patch) + +The tilde (`~`) comparison operator is for patch level ranges when a minor +version is specified and major level changes when the minor number is missing. +For example, + +* `~1.2.3` is equivalent to `>= 1.2.3, < 1.3.0` +* `~1` is equivalent to `>= 1, < 2` +* `~2.3` is equivalent to `>= 2.3, < 2.4` +* `~1.2.x` is equivalent to `>= 1.2.0, < 1.3.0` +* `~1.x` is equivalent to `>= 1, < 2` + +### Caret Range Comparisons (Major) + +The caret (`^`) comparison operator is for major level changes once a stable +(1.0.0) release has occurred. Prior to a 1.0.0 release the minor versions acts +as the API stability level. This is useful when comparisons of API versions as a +major change is API breaking. For example, + +* `^1.2.3` is equivalent to `>= 1.2.3, < 2.0.0` +* `^1.2.x` is equivalent to `>= 1.2.0, < 2.0.0` +* `^2.3` is equivalent to `>= 2.3, < 3` +* `^2.x` is equivalent to `>= 2.0.0, < 3` +* `^0.2.3` is equivalent to `>=0.2.3 <0.3.0` +* `^0.2` is equivalent to `>=0.2.0 <0.3.0` +* `^0.0.3` is equivalent to `>=0.0.3 <0.0.4` +* `^0.0` is equivalent to `>=0.0.0 <0.1.0` +* `^0` is equivalent to `>=0.0.0 <1.0.0` + +## Validation + +In addition to testing a version against a constraint, a version can be validated +against a constraint. When validation fails a slice of errors containing why a +version didn't meet the constraint is returned. For example, + +```go +c, err := semver.NewConstraint("<= 1.2.3, >= 1.4") +if err != nil { + // Handle constraint not being parseable. +} + +v, err := semver.NewVersion("1.3") +if err != nil { + // Handle version not being parseable. +} + +// Validate a version against a constraint. +a, msgs := c.Validate(v) +// a is false +for _, m := range msgs { + fmt.Println(m) + + // Loops over the errors which would read + // "1.3 is greater than 1.2.3" + // "1.3 is less than 1.4" +} +``` + +## Contribute + +If you find an issue or want to contribute please file an [issue](https://github.com/Masterminds/semver/issues) +or [create a pull request](https://github.com/Masterminds/semver/pulls). + +## Security + +Security is an important consideration for this project. The project currently +uses the following tools to help discover security issues: + +* [CodeQL](https://codeql.github.com) +* [gosec](https://github.com/securego/gosec) +* Daily Fuzz testing + +If you believe you have found a security vulnerability you can privately disclose +it through the [GitHub security page](https://github.com/Masterminds/semver/security). diff --git a/vendor/github.com/Masterminds/semver/v3/SECURITY.md b/vendor/github.com/Masterminds/semver/v3/SECURITY.md new file mode 100644 index 00000000..a30a66b1 --- /dev/null +++ b/vendor/github.com/Masterminds/semver/v3/SECURITY.md @@ -0,0 +1,19 @@ +# Security Policy + +## Supported Versions + +The following versions of semver are currently supported: + +| Version | Supported | +| ------- | ------------------ | +| 3.x | :white_check_mark: | +| 2.x | :x: | +| 1.x | :x: | + +Fixes are only released for the latest minor version in the form of a patch release. + +## Reporting a Vulnerability + +You can privately disclose a vulnerability through GitHubs +[private vulnerability reporting](https://github.com/Masterminds/semver/security/advisories) +mechanism. diff --git a/vendor/github.com/Masterminds/semver/v3/collection.go b/vendor/github.com/Masterminds/semver/v3/collection.go new file mode 100644 index 00000000..a7823589 --- /dev/null +++ b/vendor/github.com/Masterminds/semver/v3/collection.go @@ -0,0 +1,24 @@ +package semver + +// Collection is a collection of Version instances and implements the sort +// interface. See the sort package for more details. +// https://golang.org/pkg/sort/ +type Collection []*Version + +// Len returns the length of a collection. The number of Version instances +// on the slice. +func (c Collection) Len() int { + return len(c) +} + +// Less is needed for the sort interface to compare two Version objects on the +// slice. If checks if one is less than the other. +func (c Collection) Less(i, j int) bool { + return c[i].LessThan(c[j]) +} + +// Swap is needed for the sort interface to replace the Version objects +// at two different positions in the slice. +func (c Collection) Swap(i, j int) { + c[i], c[j] = c[j], c[i] +} diff --git a/vendor/github.com/Masterminds/semver/v3/constraints.go b/vendor/github.com/Masterminds/semver/v3/constraints.go new file mode 100644 index 00000000..8b7a10f8 --- /dev/null +++ b/vendor/github.com/Masterminds/semver/v3/constraints.go @@ -0,0 +1,601 @@ +package semver + +import ( + "bytes" + "errors" + "fmt" + "regexp" + "strings" +) + +// Constraints is one or more constraint that a semantic version can be +// checked against. +type Constraints struct { + constraints [][]*constraint + containsPre []bool + + // IncludePrerelease specifies if pre-releases should be included in + // the results. Note, if a constraint range has a prerelease than + // prereleases will be included for that AND group even if this is + // set to false. + IncludePrerelease bool +} + +// NewConstraint returns a Constraints instance that a Version instance can +// be checked against. If there is a parse error it will be returned. +func NewConstraint(c string) (*Constraints, error) { + + // Rewrite - ranges into a comparison operation. + c = rewriteRange(c) + + ors := strings.Split(c, "||") + lenors := len(ors) + or := make([][]*constraint, lenors) + hasPre := make([]bool, lenors) + for k, v := range ors { + // Validate the segment + if !validConstraintRegex.MatchString(v) { + return nil, fmt.Errorf("improper constraint: %s", v) + } + + cs := findConstraintRegex.FindAllString(v, -1) + if cs == nil { + cs = append(cs, v) + } + result := make([]*constraint, len(cs)) + for i, s := range cs { + pc, err := parseConstraint(s) + if err != nil { + return nil, err + } + + // If one of the constraints has a prerelease record this. + // This information is used when checking all in an "and" + // group to ensure they all check for prereleases. + if pc.con.pre != "" { + hasPre[k] = true + } + + result[i] = pc + } + or[k] = result + } + + o := &Constraints{ + constraints: or, + containsPre: hasPre, + } + return o, nil +} + +// Check tests if a version satisfies the constraints. +func (cs Constraints) Check(v *Version) bool { + // TODO(mattfarina): For v4 of this library consolidate the Check and Validate + // functions as the underlying functions make that possible now. + // loop over the ORs and check the inner ANDs + for i, o := range cs.constraints { + joy := true + for _, c := range o { + if check, _ := c.check(v, (cs.IncludePrerelease || cs.containsPre[i])); !check { + joy = false + break + } + } + + if joy { + return true + } + } + + return false +} + +// Validate checks if a version satisfies a constraint. If not a slice of +// reasons for the failure are returned in addition to a bool. +func (cs Constraints) Validate(v *Version) (bool, []error) { + // loop over the ORs and check the inner ANDs + var e []error + + // Capture the prerelease message only once. When it happens the first time + // this var is marked + var prerelesase bool + for i, o := range cs.constraints { + joy := true + for _, c := range o { + // Before running the check handle the case there the version is + // a prerelease and the check is not searching for prereleases. + if !(cs.IncludePrerelease || cs.containsPre[i]) && v.pre != "" { + if !prerelesase { + em := fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v) + e = append(e, em) + prerelesase = true + } + joy = false + + } else { + + if _, err := c.check(v, (cs.IncludePrerelease || cs.containsPre[i])); err != nil { + e = append(e, err) + joy = false + } + } + } + + if joy { + return true, []error{} + } + } + + return false, e +} + +func (cs Constraints) String() string { + buf := make([]string, len(cs.constraints)) + var tmp bytes.Buffer + + for k, v := range cs.constraints { + tmp.Reset() + vlen := len(v) + for kk, c := range v { + tmp.WriteString(c.string()) + + // Space separate the AND conditions + if vlen > 1 && kk < vlen-1 { + tmp.WriteString(" ") + } + } + buf[k] = tmp.String() + } + + return strings.Join(buf, " || ") +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface. +func (cs *Constraints) UnmarshalText(text []byte) error { + temp, err := NewConstraint(string(text)) + if err != nil { + return err + } + + *cs = *temp + + return nil +} + +// MarshalText implements the encoding.TextMarshaler interface. +func (cs Constraints) MarshalText() ([]byte, error) { + return []byte(cs.String()), nil +} + +var constraintOps map[string]cfunc +var constraintRegex *regexp.Regexp +var constraintRangeRegex *regexp.Regexp + +// Used to find individual constraints within a multi-constraint string +var findConstraintRegex *regexp.Regexp + +// Used to validate an segment of ANDs is valid +var validConstraintRegex *regexp.Regexp + +const cvRegex string = `v?([0-9|x|X|\*]+)(\.[0-9|x|X|\*]+)?(\.[0-9|x|X|\*]+)?` + + `(-([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?` + + `(\+([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?` + +func init() { + constraintOps = map[string]cfunc{ + "": constraintTildeOrEqual, + "=": constraintTildeOrEqual, + "!=": constraintNotEqual, + ">": constraintGreaterThan, + "<": constraintLessThan, + ">=": constraintGreaterThanEqual, + "=>": constraintGreaterThanEqual, + "<=": constraintLessThanEqual, + "=<": constraintLessThanEqual, + "~": constraintTilde, + "~>": constraintTilde, + "^": constraintCaret, + } + + ops := `=||!=|>|<|>=|=>|<=|=<|~|~>|\^` + + constraintRegex = regexp.MustCompile(fmt.Sprintf( + `^\s*(%s)\s*(%s)\s*$`, + ops, + cvRegex)) + + constraintRangeRegex = regexp.MustCompile(fmt.Sprintf( + `\s*(%s)\s+-\s+(%s)\s*`, + cvRegex, cvRegex)) + + findConstraintRegex = regexp.MustCompile(fmt.Sprintf( + `(%s)\s*(%s)`, + ops, + cvRegex)) + + // The first time a constraint shows up will look slightly different from + // future times it shows up due to a leading space or comma in a given + // string. + validConstraintRegex = regexp.MustCompile(fmt.Sprintf( + `^(\s*(%s)\s*(%s)\s*)((?:\s+|,\s*)(%s)\s*(%s)\s*)*$`, + ops, + cvRegex, + ops, + cvRegex)) +} + +// An individual constraint +type constraint struct { + // The version used in the constraint check. For example, if a constraint + // is '<= 2.0.0' the con a version instance representing 2.0.0. + con *Version + + // The original parsed version (e.g., 4.x from != 4.x) + orig string + + // The original operator for the constraint + origfunc string + + // When an x is used as part of the version (e.g., 1.x) + minorDirty bool + dirty bool + patchDirty bool +} + +// Check if a version meets the constraint +func (c *constraint) check(v *Version, includePre bool) (bool, error) { + return constraintOps[c.origfunc](v, c, includePre) +} + +// String prints an individual constraint into a string +func (c *constraint) string() string { + return c.origfunc + c.orig +} + +type cfunc func(v *Version, c *constraint, includePre bool) (bool, error) + +func parseConstraint(c string) (*constraint, error) { + if len(c) > 0 { + m := constraintRegex.FindStringSubmatch(c) + if m == nil { + return nil, fmt.Errorf("improper constraint: %s", c) + } + + cs := &constraint{ + orig: m[2], + origfunc: m[1], + } + + ver := m[2] + minorDirty := false + patchDirty := false + dirty := false + if isX(m[3]) || m[3] == "" { + ver = fmt.Sprintf("0.0.0%s", m[6]) + dirty = true + } else if isX(strings.TrimPrefix(m[4], ".")) || m[4] == "" { + minorDirty = true + dirty = true + ver = fmt.Sprintf("%s.0.0%s", m[3], m[6]) + } else if isX(strings.TrimPrefix(m[5], ".")) || m[5] == "" { + dirty = true + patchDirty = true + ver = fmt.Sprintf("%s%s.0%s", m[3], m[4], m[6]) + } + + con, err := NewVersion(ver) + if err != nil { + + // The constraintRegex should catch any regex parsing errors. So, + // we should never get here. + return nil, errors.New("constraint parser error") + } + + cs.con = con + cs.minorDirty = minorDirty + cs.patchDirty = patchDirty + cs.dirty = dirty + + return cs, nil + } + + // The rest is the special case where an empty string was passed in which + // is equivalent to * or >=0.0.0 + con, err := StrictNewVersion("0.0.0") + if err != nil { + + // The constraintRegex should catch any regex parsing errors. So, + // we should never get here. + return nil, errors.New("constraint parser error") + } + + cs := &constraint{ + con: con, + orig: c, + origfunc: "", + minorDirty: false, + patchDirty: false, + dirty: true, + } + return cs, nil +} + +// Constraint functions +func constraintNotEqual(v *Version, c *constraint, includePre bool) (bool, error) { + // The existence of prereleases is checked at the group level and passed in. + // Exit early if the version has a prerelease but those are to be ignored. + if v.Prerelease() != "" && !includePre { + return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v) + } + + if c.dirty { + if c.con.Major() != v.Major() { + return true, nil + } + if c.con.Minor() != v.Minor() && !c.minorDirty { + return true, nil + } else if c.minorDirty { + return false, fmt.Errorf("%s is equal to %s", v, c.orig) + } else if c.con.Patch() != v.Patch() && !c.patchDirty { + return true, nil + } else if c.patchDirty { + // Need to handle prereleases if present + if v.Prerelease() != "" || c.con.Prerelease() != "" { + eq := comparePrerelease(v.Prerelease(), c.con.Prerelease()) != 0 + if eq { + return true, nil + } + return false, fmt.Errorf("%s is equal to %s", v, c.orig) + } + return false, fmt.Errorf("%s is equal to %s", v, c.orig) + } + } + + eq := v.Equal(c.con) + if eq { + return false, fmt.Errorf("%s is equal to %s", v, c.orig) + } + + return true, nil +} + +func constraintGreaterThan(v *Version, c *constraint, includePre bool) (bool, error) { + + // The existence of prereleases is checked at the group level and passed in. + // Exit early if the version has a prerelease but those are to be ignored. + if v.Prerelease() != "" && !includePre { + return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v) + } + + var eq bool + + if !c.dirty { + eq = v.Compare(c.con) == 1 + if eq { + return true, nil + } + return false, fmt.Errorf("%s is less than or equal to %s", v, c.orig) + } + + if v.Major() > c.con.Major() { + return true, nil + } else if v.Major() < c.con.Major() { + return false, fmt.Errorf("%s is less than or equal to %s", v, c.orig) + } else if c.minorDirty { + // This is a range case such as >11. When the version is something like + // 11.1.0 is it not > 11. For that we would need 12 or higher + return false, fmt.Errorf("%s is less than or equal to %s", v, c.orig) + } else if c.patchDirty { + // This is for ranges such as >11.1. A version of 11.1.1 is not greater + // which one of 11.2.1 is greater + eq = v.Minor() > c.con.Minor() + if eq { + return true, nil + } + return false, fmt.Errorf("%s is less than or equal to %s", v, c.orig) + } + + // If we have gotten here we are not comparing pre-preleases and can use the + // Compare function to accomplish that. + eq = v.Compare(c.con) == 1 + if eq { + return true, nil + } + return false, fmt.Errorf("%s is less than or equal to %s", v, c.orig) +} + +func constraintLessThan(v *Version, c *constraint, includePre bool) (bool, error) { + // The existence of prereleases is checked at the group level and passed in. + // Exit early if the version has a prerelease but those are to be ignored. + if v.Prerelease() != "" && !includePre { + return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v) + } + + eq := v.Compare(c.con) < 0 + if eq { + return true, nil + } + return false, fmt.Errorf("%s is greater than or equal to %s", v, c.orig) +} + +func constraintGreaterThanEqual(v *Version, c *constraint, includePre bool) (bool, error) { + + // The existence of prereleases is checked at the group level and passed in. + // Exit early if the version has a prerelease but those are to be ignored. + if v.Prerelease() != "" && !includePre { + return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v) + } + + eq := v.Compare(c.con) >= 0 + if eq { + return true, nil + } + return false, fmt.Errorf("%s is less than %s", v, c.orig) +} + +func constraintLessThanEqual(v *Version, c *constraint, includePre bool) (bool, error) { + // The existence of prereleases is checked at the group level and passed in. + // Exit early if the version has a prerelease but those are to be ignored. + if v.Prerelease() != "" && !includePre { + return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v) + } + + var eq bool + + if !c.dirty { + eq = v.Compare(c.con) <= 0 + if eq { + return true, nil + } + return false, fmt.Errorf("%s is greater than %s", v, c.orig) + } + + if v.Major() > c.con.Major() { + return false, fmt.Errorf("%s is greater than %s", v, c.orig) + } else if v.Major() == c.con.Major() && v.Minor() > c.con.Minor() && !c.minorDirty { + return false, fmt.Errorf("%s is greater than %s", v, c.orig) + } + + return true, nil +} + +// ~*, ~>* --> >= 0.0.0 (any) +// ~2, ~2.x, ~2.x.x, ~>2, ~>2.x ~>2.x.x --> >=2.0.0, <3.0.0 +// ~2.0, ~2.0.x, ~>2.0, ~>2.0.x --> >=2.0.0, <2.1.0 +// ~1.2, ~1.2.x, ~>1.2, ~>1.2.x --> >=1.2.0, <1.3.0 +// ~1.2.3, ~>1.2.3 --> >=1.2.3, <1.3.0 +// ~1.2.0, ~>1.2.0 --> >=1.2.0, <1.3.0 +func constraintTilde(v *Version, c *constraint, includePre bool) (bool, error) { + // The existence of prereleases is checked at the group level and passed in. + // Exit early if the version has a prerelease but those are to be ignored. + if v.Prerelease() != "" && !includePre { + return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v) + } + + if v.LessThan(c.con) { + return false, fmt.Errorf("%s is less than %s", v, c.orig) + } + + // ~0.0.0 is a special case where all constraints are accepted. It's + // equivalent to >= 0.0.0. + if c.con.Major() == 0 && c.con.Minor() == 0 && c.con.Patch() == 0 && + !c.minorDirty && !c.patchDirty { + return true, nil + } + + if v.Major() != c.con.Major() { + return false, fmt.Errorf("%s does not have same major version as %s", v, c.orig) + } + + if v.Minor() != c.con.Minor() && !c.minorDirty { + return false, fmt.Errorf("%s does not have same major and minor version as %s", v, c.orig) + } + + return true, nil +} + +// When there is a .x (dirty) status it automatically opts in to ~. Otherwise +// it's a straight = +func constraintTildeOrEqual(v *Version, c *constraint, includePre bool) (bool, error) { + // The existence of prereleases is checked at the group level and passed in. + // Exit early if the version has a prerelease but those are to be ignored. + if v.Prerelease() != "" && !includePre { + return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v) + } + + if c.dirty { + return constraintTilde(v, c, includePre) + } + + eq := v.Equal(c.con) + if eq { + return true, nil + } + + return false, fmt.Errorf("%s is not equal to %s", v, c.orig) +} + +// ^* --> (any) +// ^1.2.3 --> >=1.2.3 <2.0.0 +// ^1.2 --> >=1.2.0 <2.0.0 +// ^1 --> >=1.0.0 <2.0.0 +// ^0.2.3 --> >=0.2.3 <0.3.0 +// ^0.2 --> >=0.2.0 <0.3.0 +// ^0.0.3 --> >=0.0.3 <0.0.4 +// ^0.0 --> >=0.0.0 <0.1.0 +// ^0 --> >=0.0.0 <1.0.0 +func constraintCaret(v *Version, c *constraint, includePre bool) (bool, error) { + // The existence of prereleases is checked at the group level and passed in. + // Exit early if the version has a prerelease but those are to be ignored. + if v.Prerelease() != "" && !includePre { + return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v) + } + + // This less than handles prereleases + if v.LessThan(c.con) { + return false, fmt.Errorf("%s is less than %s", v, c.orig) + } + + var eq bool + + // ^ when the major > 0 is >=x.y.z < x+1 + if c.con.Major() > 0 || c.minorDirty { + + // ^ has to be within a major range for > 0. Everything less than was + // filtered out with the LessThan call above. This filters out those + // that greater but not within the same major range. + eq = v.Major() == c.con.Major() + if eq { + return true, nil + } + return false, fmt.Errorf("%s does not have same major version as %s", v, c.orig) + } + + // ^ when the major is 0 and minor > 0 is >=0.y.z < 0.y+1 + if c.con.Major() == 0 && v.Major() > 0 { + return false, fmt.Errorf("%s does not have same major version as %s", v, c.orig) + } + // If the con Minor is > 0 it is not dirty + if c.con.Minor() > 0 || c.patchDirty { + eq = v.Minor() == c.con.Minor() + if eq { + return true, nil + } + return false, fmt.Errorf("%s does not have same minor version as %s. Expected minor versions to match when constraint major version is 0", v, c.orig) + } + // ^ when the minor is 0 and minor > 0 is =0.0.z + if c.con.Minor() == 0 && v.Minor() > 0 { + return false, fmt.Errorf("%s does not have same minor version as %s", v, c.orig) + } + + // At this point the major is 0 and the minor is 0 and not dirty. The patch + // is not dirty so we need to check if they are equal. If they are not equal + eq = c.con.Patch() == v.Patch() + if eq { + return true, nil + } + return false, fmt.Errorf("%s does not equal %s. Expect version and constraint to equal when major and minor versions are 0", v, c.orig) +} + +func isX(x string) bool { + switch x { + case "x", "*", "X": + return true + default: + return false + } +} + +func rewriteRange(i string) string { + m := constraintRangeRegex.FindAllStringSubmatch(i, -1) + if m == nil { + return i + } + o := i + for _, v := range m { + t := fmt.Sprintf(">= %s, <= %s ", v[1], v[11]) + o = strings.Replace(o, v[0], t, 1) + } + + return o +} diff --git a/vendor/github.com/Masterminds/semver/v3/doc.go b/vendor/github.com/Masterminds/semver/v3/doc.go new file mode 100644 index 00000000..74f97caa --- /dev/null +++ b/vendor/github.com/Masterminds/semver/v3/doc.go @@ -0,0 +1,184 @@ +/* +Package semver provides the ability to work with Semantic Versions (http://semver.org) in Go. + +Specifically it provides the ability to: + + - Parse semantic versions + - Sort semantic versions + - Check if a semantic version fits within a set of constraints + - Optionally work with a `v` prefix + +# Parsing Semantic Versions + +There are two functions that can parse semantic versions. The `StrictNewVersion` +function only parses valid version 2 semantic versions as outlined in the +specification. The `NewVersion` function attempts to coerce a version into a +semantic version and parse it. For example, if there is a leading v or a version +listed without all 3 parts (e.g. 1.2) it will attempt to coerce it into a valid +semantic version (e.g., 1.2.0). In both cases a `Version` object is returned +that can be sorted, compared, and used in constraints. + +When parsing a version an optional error can be returned if there is an issue +parsing the version. For example, + + v, err := semver.NewVersion("1.2.3-beta.1+b345") + +The version object has methods to get the parts of the version, compare it to +other versions, convert the version back into a string, and get the original +string. For more details please see the documentation +at https://godoc.org/github.com/Masterminds/semver. + +# Sorting Semantic Versions + +A set of versions can be sorted using the `sort` package from the standard library. +For example, + + raw := []string{"1.2.3", "1.0", "1.3", "2", "0.4.2",} + vs := make([]*semver.Version, len(raw)) + for i, r := range raw { + v, err := semver.NewVersion(r) + if err != nil { + t.Errorf("Error parsing version: %s", err) + } + + vs[i] = v + } + + sort.Sort(semver.Collection(vs)) + +# Checking Version Constraints and Comparing Versions + +There are two methods for comparing versions. One uses comparison methods on +`Version` instances and the other is using Constraints. There are some important +differences to notes between these two methods of comparison. + + 1. When two versions are compared using functions such as `Compare`, `LessThan`, + and others it will follow the specification and always include prereleases + within the comparison. It will provide an answer valid with the comparison + spec section at https://semver.org/#spec-item-11 + 2. When constraint checking is used for checks or validation it will follow a + different set of rules that are common for ranges with tools like npm/js + and Rust/Cargo. This includes considering prereleases to be invalid if the + ranges does not include on. If you want to have it include pre-releases a + simple solution is to include `-0` in your range. + 3. Constraint ranges can have some complex rules including the shorthard use of + ~ and ^. For more details on those see the options below. + +There are differences between the two methods or checking versions because the +comparison methods on `Version` follow the specification while comparison ranges +are not part of the specification. Different packages and tools have taken it +upon themselves to come up with range rules. This has resulted in differences. +For example, npm/js and Cargo/Rust follow similar patterns which PHP has a +different pattern for ^. The comparison features in this package follow the +npm/js and Cargo/Rust lead because applications using it have followed similar +patters with their versions. + +Checking a version against version constraints is one of the most featureful +parts of the package. + + c, err := semver.NewConstraint(">= 1.2.3") + if err != nil { + // Handle constraint not being parsable. + } + + v, err := semver.NewVersion("1.3") + if err != nil { + // Handle version not being parsable. + } + // Check if the version meets the constraints. The a variable will be true. + a := c.Check(v) + +# Basic Comparisons + +There are two elements to the comparisons. First, a comparison string is a list +of comma or space separated AND comparisons. These are then separated by || (OR) +comparisons. For example, `">= 1.2 < 3.0.0 || >= 4.2.3"` is looking for a +comparison that's greater than or equal to 1.2 and less than 3.0.0 or is +greater than or equal to 4.2.3. This can also be written as +`">= 1.2, < 3.0.0 || >= 4.2.3"` + +The basic comparisons are: + + - `=`: equal (aliased to no operator) + - `!=`: not equal + - `>`: greater than + - `<`: less than + - `>=`: greater than or equal to + - `<=`: less than or equal to + +# Hyphen Range Comparisons + +There are multiple methods to handle ranges and the first is hyphens ranges. +These look like: + + - `1.2 - 1.4.5` which is equivalent to `>= 1.2, <= 1.4.5` + - `2.3.4 - 4.5` which is equivalent to `>= 2.3.4 <= 4.5` + +# Wildcards In Comparisons + +The `x`, `X`, and `*` characters can be used as a wildcard character. This works +for all comparison operators. When used on the `=` operator it falls +back to the tilde operation. For example, + + - `1.2.x` is equivalent to `>= 1.2.0 < 1.3.0` + - `>= 1.2.x` is equivalent to `>= 1.2.0` + - `<= 2.x` is equivalent to `<= 3` + - `*` is equivalent to `>= 0.0.0` + +Tilde Range Comparisons (Patch) + +The tilde (`~`) comparison operator is for patch level ranges when a minor +version is specified and major level changes when the minor number is missing. +For example, + + - `~1.2.3` is equivalent to `>= 1.2.3 < 1.3.0` + - `~1` is equivalent to `>= 1, < 2` + - `~2.3` is equivalent to `>= 2.3 < 2.4` + - `~1.2.x` is equivalent to `>= 1.2.0 < 1.3.0` + - `~1.x` is equivalent to `>= 1 < 2` + +Caret Range Comparisons (Major) + +The caret (`^`) comparison operator is for major level changes once a stable +(1.0.0) release has occurred. Prior to a 1.0.0 release the minor versions acts +as the API stability level. This is useful when comparisons of API versions as a +major change is API breaking. For example, + + - `^1.2.3` is equivalent to `>= 1.2.3, < 2.0.0` + - `^1.2.x` is equivalent to `>= 1.2.0, < 2.0.0` + - `^2.3` is equivalent to `>= 2.3, < 3` + - `^2.x` is equivalent to `>= 2.0.0, < 3` + - `^0.2.3` is equivalent to `>=0.2.3 <0.3.0` + - `^0.2` is equivalent to `>=0.2.0 <0.3.0` + - `^0.0.3` is equivalent to `>=0.0.3 <0.0.4` + - `^0.0` is equivalent to `>=0.0.0 <0.1.0` + - `^0` is equivalent to `>=0.0.0 <1.0.0` + +# Validation + +In addition to testing a version against a constraint, a version can be validated +against a constraint. When validation fails a slice of errors containing why a +version didn't meet the constraint is returned. For example, + + c, err := semver.NewConstraint("<= 1.2.3, >= 1.4") + if err != nil { + // Handle constraint not being parseable. + } + + v, _ := semver.NewVersion("1.3") + if err != nil { + // Handle version not being parseable. + } + + // Validate a version against a constraint. + a, msgs := c.Validate(v) + // a is false + for _, m := range msgs { + fmt.Println(m) + + // Loops over the errors which would read + // "1.3 is greater than 1.2.3" + // "1.3 is less than 1.4" + } +*/ +package semver diff --git a/vendor/github.com/Masterminds/semver/v3/version.go b/vendor/github.com/Masterminds/semver/v3/version.go new file mode 100644 index 00000000..7a3ba738 --- /dev/null +++ b/vendor/github.com/Masterminds/semver/v3/version.go @@ -0,0 +1,788 @@ +package semver + +import ( + "bytes" + "database/sql/driver" + "encoding/json" + "errors" + "fmt" + "regexp" + "strconv" + "strings" +) + +// The compiled version of the regex created at init() is cached here so it +// only needs to be created once. +var versionRegex *regexp.Regexp +var looseVersionRegex *regexp.Regexp + +// CoerceNewVersion sets if leading 0's are allowd in the version part. Leading 0's are +// not allowed in a valid semantic version. When set to true, NewVersion will coerce +// leading 0's into a valid version. +var CoerceNewVersion = true + +// DetailedNewVersionErrors specifies if detailed errors are returned from the NewVersion +// function. This is used when CoerceNewVersion is set to false. If set to false +// ErrInvalidSemVer is returned for an invalid version. This does not apply to +// StrictNewVersion. Setting this function to false returns errors more quickly. +var DetailedNewVersionErrors = true + +var ( + // ErrInvalidSemVer is returned a version is found to be invalid when + // being parsed. + ErrInvalidSemVer = errors.New("invalid semantic version") + + // ErrEmptyString is returned when an empty string is passed in for parsing. + ErrEmptyString = errors.New("version string empty") + + // ErrInvalidCharacters is returned when invalid characters are found as + // part of a version + ErrInvalidCharacters = errors.New("invalid characters in version") + + // ErrSegmentStartsZero is returned when a version segment starts with 0. + // This is invalid in SemVer. + ErrSegmentStartsZero = errors.New("version segment starts with 0") + + // ErrInvalidMetadata is returned when the metadata is an invalid format + ErrInvalidMetadata = errors.New("invalid metadata string") + + // ErrInvalidPrerelease is returned when the pre-release is an invalid format + ErrInvalidPrerelease = errors.New("invalid prerelease string") +) + +// semVerRegex is the regular expression used to parse a semantic version. +// This is not the official regex from the semver spec. It has been modified to allow for loose handling +// where versions like 2.1 are detected. +const semVerRegex string = `v?(0|[1-9]\d*)(?:\.(0|[1-9]\d*))?(?:\.(0|[1-9]\d*))?` + + `(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?` + + `(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?` + +// looseSemVerRegex is a regular expression that lets invalid semver expressions through +// with enough detail that certain errors can be checked for. +const looseSemVerRegex string = `v?([0-9]+)(\.[0-9]+)?(\.[0-9]+)?` + + `(-([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?` + + `(\+([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?` + +// Version represents a single semantic version. +type Version struct { + major, minor, patch uint64 + pre string + metadata string + original string +} + +func init() { + versionRegex = regexp.MustCompile("^" + semVerRegex + "$") + looseVersionRegex = regexp.MustCompile("^" + looseSemVerRegex + "$") +} + +const ( + num string = "0123456789" + allowed string = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-" + num +) + +// StrictNewVersion parses a given version and returns an instance of Version or +// an error if unable to parse the version. Only parses valid semantic versions. +// Performs checking that can find errors within the version. +// If you want to coerce a version such as 1 or 1.2 and parse it as the 1.x +// releases of semver did, use the NewVersion() function. +func StrictNewVersion(v string) (*Version, error) { + // Parsing here does not use RegEx in order to increase performance and reduce + // allocations. + + if len(v) == 0 { + return nil, ErrEmptyString + } + + // Split the parts into [0]major, [1]minor, and [2]patch,prerelease,build + parts := strings.SplitN(v, ".", 3) + if len(parts) != 3 { + return nil, ErrInvalidSemVer + } + + sv := &Version{ + original: v, + } + + // Extract build metadata + if strings.Contains(parts[2], "+") { + extra := strings.SplitN(parts[2], "+", 2) + sv.metadata = extra[1] + parts[2] = extra[0] + if err := validateMetadata(sv.metadata); err != nil { + return nil, err + } + } + + // Extract build prerelease + if strings.Contains(parts[2], "-") { + extra := strings.SplitN(parts[2], "-", 2) + sv.pre = extra[1] + parts[2] = extra[0] + if err := validatePrerelease(sv.pre); err != nil { + return nil, err + } + } + + // Validate the number segments are valid. This includes only having positive + // numbers and no leading 0's. + for _, p := range parts { + if !containsOnly(p, num) { + return nil, ErrInvalidCharacters + } + + if len(p) > 1 && p[0] == '0' { + return nil, ErrSegmentStartsZero + } + } + + // Extract major, minor, and patch + var err error + sv.major, err = strconv.ParseUint(parts[0], 10, 64) + if err != nil { + return nil, err + } + + sv.minor, err = strconv.ParseUint(parts[1], 10, 64) + if err != nil { + return nil, err + } + + sv.patch, err = strconv.ParseUint(parts[2], 10, 64) + if err != nil { + return nil, err + } + + return sv, nil +} + +// NewVersion parses a given version and returns an instance of Version or +// an error if unable to parse the version. If the version is SemVer-ish it +// attempts to convert it to SemVer. If you want to validate it was a strict +// semantic version at parse time see StrictNewVersion(). +func NewVersion(v string) (*Version, error) { + if CoerceNewVersion { + return coerceNewVersion(v) + } + m := versionRegex.FindStringSubmatch(v) + if m == nil { + + // Disabling detailed errors is first so that it is in the fast path. + if !DetailedNewVersionErrors { + return nil, ErrInvalidSemVer + } + + // Check for specific errors with the semver string and return a more detailed + // error. + m = looseVersionRegex.FindStringSubmatch(v) + if m == nil { + return nil, ErrInvalidSemVer + } + err := validateVersion(m) + if err != nil { + return nil, err + } + return nil, ErrInvalidSemVer + } + + sv := &Version{ + metadata: m[5], + pre: m[4], + original: v, + } + + var err error + sv.major, err = strconv.ParseUint(m[1], 10, 64) + if err != nil { + return nil, fmt.Errorf("error parsing version segment: %w", err) + } + + if m[2] != "" { + sv.minor, err = strconv.ParseUint(m[2], 10, 64) + if err != nil { + return nil, fmt.Errorf("error parsing version segment: %w", err) + } + } else { + sv.minor = 0 + } + + if m[3] != "" { + sv.patch, err = strconv.ParseUint(m[3], 10, 64) + if err != nil { + return nil, fmt.Errorf("error parsing version segment: %w", err) + } + } else { + sv.patch = 0 + } + + // Perform some basic due diligence on the extra parts to ensure they are + // valid. + + if sv.pre != "" { + if err = validatePrerelease(sv.pre); err != nil { + return nil, err + } + } + + if sv.metadata != "" { + if err = validateMetadata(sv.metadata); err != nil { + return nil, err + } + } + + return sv, nil +} + +func coerceNewVersion(v string) (*Version, error) { + m := looseVersionRegex.FindStringSubmatch(v) + if m == nil { + return nil, ErrInvalidSemVer + } + + sv := &Version{ + metadata: m[8], + pre: m[5], + original: v, + } + + var err error + sv.major, err = strconv.ParseUint(m[1], 10, 64) + if err != nil { + return nil, fmt.Errorf("error parsing version segment: %w", err) + } + + if m[2] != "" { + sv.minor, err = strconv.ParseUint(strings.TrimPrefix(m[2], "."), 10, 64) + if err != nil { + return nil, fmt.Errorf("error parsing version segment: %w", err) + } + } else { + sv.minor = 0 + } + + if m[3] != "" { + sv.patch, err = strconv.ParseUint(strings.TrimPrefix(m[3], "."), 10, 64) + if err != nil { + return nil, fmt.Errorf("error parsing version segment: %w", err) + } + } else { + sv.patch = 0 + } + + // Perform some basic due diligence on the extra parts to ensure they are + // valid. + + if sv.pre != "" { + if err = validatePrerelease(sv.pre); err != nil { + return nil, err + } + } + + if sv.metadata != "" { + if err = validateMetadata(sv.metadata); err != nil { + return nil, err + } + } + + return sv, nil +} + +// New creates a new instance of Version with each of the parts passed in as +// arguments instead of parsing a version string. +func New(major, minor, patch uint64, pre, metadata string) *Version { + v := Version{ + major: major, + minor: minor, + patch: patch, + pre: pre, + metadata: metadata, + original: "", + } + + v.original = v.String() + + return &v +} + +// MustParse parses a given version and panics on error. +func MustParse(v string) *Version { + sv, err := NewVersion(v) + if err != nil { + panic(err) + } + return sv +} + +// String converts a Version object to a string. +// Note, if the original version contained a leading v this version will not. +// See the Original() method to retrieve the original value. Semantic Versions +// don't contain a leading v per the spec. Instead it's optional on +// implementation. +func (v Version) String() string { + var buf bytes.Buffer + + fmt.Fprintf(&buf, "%d.%d.%d", v.major, v.minor, v.patch) + if v.pre != "" { + fmt.Fprintf(&buf, "-%s", v.pre) + } + if v.metadata != "" { + fmt.Fprintf(&buf, "+%s", v.metadata) + } + + return buf.String() +} + +// Original returns the original value passed in to be parsed. +func (v *Version) Original() string { + return v.original +} + +// Major returns the major version. +func (v Version) Major() uint64 { + return v.major +} + +// Minor returns the minor version. +func (v Version) Minor() uint64 { + return v.minor +} + +// Patch returns the patch version. +func (v Version) Patch() uint64 { + return v.patch +} + +// Prerelease returns the pre-release version. +func (v Version) Prerelease() string { + return v.pre +} + +// Metadata returns the metadata on the version. +func (v Version) Metadata() string { + return v.metadata +} + +// originalVPrefix returns the original 'v' prefix if any. +func (v Version) originalVPrefix() string { + // Note, only lowercase v is supported as a prefix by the parser. + if v.original != "" && v.original[:1] == "v" { + return v.original[:1] + } + return "" +} + +// IncPatch produces the next patch version. +// If the current version does not have prerelease/metadata information, +// it unsets metadata and prerelease values, increments patch number. +// If the current version has any of prerelease or metadata information, +// it unsets both values and keeps current patch value +func (v Version) IncPatch() Version { + vNext := v + // according to http://semver.org/#spec-item-9 + // Pre-release versions have a lower precedence than the associated normal version. + // according to http://semver.org/#spec-item-10 + // Build metadata SHOULD be ignored when determining version precedence. + if v.pre != "" { + vNext.metadata = "" + vNext.pre = "" + } else { + vNext.metadata = "" + vNext.pre = "" + vNext.patch = v.patch + 1 + } + vNext.original = v.originalVPrefix() + "" + vNext.String() + return vNext +} + +// IncMinor produces the next minor version. +// Sets patch to 0. +// Increments minor number. +// Unsets metadata. +// Unsets prerelease status. +func (v Version) IncMinor() Version { + vNext := v + vNext.metadata = "" + vNext.pre = "" + vNext.patch = 0 + vNext.minor = v.minor + 1 + vNext.original = v.originalVPrefix() + "" + vNext.String() + return vNext +} + +// IncMajor produces the next major version. +// Sets patch to 0. +// Sets minor to 0. +// Increments major number. +// Unsets metadata. +// Unsets prerelease status. +func (v Version) IncMajor() Version { + vNext := v + vNext.metadata = "" + vNext.pre = "" + vNext.patch = 0 + vNext.minor = 0 + vNext.major = v.major + 1 + vNext.original = v.originalVPrefix() + "" + vNext.String() + return vNext +} + +// SetPrerelease defines the prerelease value. +// Value must not include the required 'hyphen' prefix. +func (v Version) SetPrerelease(prerelease string) (Version, error) { + vNext := v + if len(prerelease) > 0 { + if err := validatePrerelease(prerelease); err != nil { + return vNext, err + } + } + vNext.pre = prerelease + vNext.original = v.originalVPrefix() + "" + vNext.String() + return vNext, nil +} + +// SetMetadata defines metadata value. +// Value must not include the required 'plus' prefix. +func (v Version) SetMetadata(metadata string) (Version, error) { + vNext := v + if len(metadata) > 0 { + if err := validateMetadata(metadata); err != nil { + return vNext, err + } + } + vNext.metadata = metadata + vNext.original = v.originalVPrefix() + "" + vNext.String() + return vNext, nil +} + +// LessThan tests if one version is less than another one. +func (v *Version) LessThan(o *Version) bool { + return v.Compare(o) < 0 +} + +// LessThanEqual tests if one version is less or equal than another one. +func (v *Version) LessThanEqual(o *Version) bool { + return v.Compare(o) <= 0 +} + +// GreaterThan tests if one version is greater than another one. +func (v *Version) GreaterThan(o *Version) bool { + return v.Compare(o) > 0 +} + +// GreaterThanEqual tests if one version is greater or equal than another one. +func (v *Version) GreaterThanEqual(o *Version) bool { + return v.Compare(o) >= 0 +} + +// Equal tests if two versions are equal to each other. +// Note, versions can be equal with different metadata since metadata +// is not considered part of the comparable version. +func (v *Version) Equal(o *Version) bool { + if v == o { + return true + } + if v == nil || o == nil { + return false + } + return v.Compare(o) == 0 +} + +// Compare compares this version to another one. It returns -1, 0, or 1 if +// the version smaller, equal, or larger than the other version. +// +// Versions are compared by X.Y.Z. Build metadata is ignored. Prerelease is +// lower than the version without a prerelease. Compare always takes into account +// prereleases. If you want to work with ranges using typical range syntaxes that +// skip prereleases if the range is not looking for them use constraints. +func (v *Version) Compare(o *Version) int { + // Compare the major, minor, and patch version for differences. If a + // difference is found return the comparison. + if d := compareSegment(v.Major(), o.Major()); d != 0 { + return d + } + if d := compareSegment(v.Minor(), o.Minor()); d != 0 { + return d + } + if d := compareSegment(v.Patch(), o.Patch()); d != 0 { + return d + } + + // At this point the major, minor, and patch versions are the same. + ps := v.pre + po := o.Prerelease() + + if ps == "" && po == "" { + return 0 + } + if ps == "" { + return 1 + } + if po == "" { + return -1 + } + + return comparePrerelease(ps, po) +} + +// UnmarshalJSON implements JSON.Unmarshaler interface. +func (v *Version) UnmarshalJSON(b []byte) error { + var s string + if err := json.Unmarshal(b, &s); err != nil { + return err + } + temp, err := NewVersion(s) + if err != nil { + return err + } + v.major = temp.major + v.minor = temp.minor + v.patch = temp.patch + v.pre = temp.pre + v.metadata = temp.metadata + v.original = temp.original + return nil +} + +// MarshalJSON implements JSON.Marshaler interface. +func (v Version) MarshalJSON() ([]byte, error) { + return json.Marshal(v.String()) +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface. +func (v *Version) UnmarshalText(text []byte) error { + temp, err := NewVersion(string(text)) + if err != nil { + return err + } + + *v = *temp + + return nil +} + +// MarshalText implements the encoding.TextMarshaler interface. +func (v Version) MarshalText() ([]byte, error) { + return []byte(v.String()), nil +} + +// Scan implements the SQL.Scanner interface. +func (v *Version) Scan(value interface{}) error { + var s string + s, _ = value.(string) + temp, err := NewVersion(s) + if err != nil { + return err + } + v.major = temp.major + v.minor = temp.minor + v.patch = temp.patch + v.pre = temp.pre + v.metadata = temp.metadata + v.original = temp.original + return nil +} + +// Value implements the Driver.Valuer interface. +func (v Version) Value() (driver.Value, error) { + return v.String(), nil +} + +func compareSegment(v, o uint64) int { + if v < o { + return -1 + } + if v > o { + return 1 + } + + return 0 +} + +func comparePrerelease(v, o string) int { + // split the prelease versions by their part. The separator, per the spec, + // is a . + sparts := strings.Split(v, ".") + oparts := strings.Split(o, ".") + + // Find the longer length of the parts to know how many loop iterations to + // go through. + slen := len(sparts) + olen := len(oparts) + + l := slen + if olen > slen { + l = olen + } + + // Iterate over each part of the prereleases to compare the differences. + for i := 0; i < l; i++ { + // Since the lentgh of the parts can be different we need to create + // a placeholder. This is to avoid out of bounds issues. + stemp := "" + if i < slen { + stemp = sparts[i] + } + + otemp := "" + if i < olen { + otemp = oparts[i] + } + + d := comparePrePart(stemp, otemp) + if d != 0 { + return d + } + } + + // Reaching here means two versions are of equal value but have different + // metadata (the part following a +). They are not identical in string form + // but the version comparison finds them to be equal. + return 0 +} + +func comparePrePart(s, o string) int { + // Fastpath if they are equal + if s == o { + return 0 + } + + // When s or o are empty we can use the other in an attempt to determine + // the response. + if s == "" { + if o != "" { + return -1 + } + return 1 + } + + if o == "" { + if s != "" { + return 1 + } + return -1 + } + + // When comparing strings "99" is greater than "103". To handle + // cases like this we need to detect numbers and compare them. According + // to the semver spec, numbers are always positive. If there is a - at the + // start like -99 this is to be evaluated as an alphanum. numbers always + // have precedence over alphanum. Parsing as Uints because negative numbers + // are ignored. + + oi, n1 := strconv.ParseUint(o, 10, 64) + si, n2 := strconv.ParseUint(s, 10, 64) + + // The case where both are strings compare the strings + if n1 != nil && n2 != nil { + if s > o { + return 1 + } + return -1 + } else if n1 != nil { + // o is a string and s is a number + return -1 + } else if n2 != nil { + // s is a string and o is a number + return 1 + } + // Both are numbers + if si > oi { + return 1 + } + return -1 +} + +// Like strings.ContainsAny but does an only instead of any. +func containsOnly(s string, comp string) bool { + return strings.IndexFunc(s, func(r rune) bool { + return !strings.ContainsRune(comp, r) + }) == -1 +} + +// From the spec, "Identifiers MUST comprise only +// ASCII alphanumerics and hyphen [0-9A-Za-z-]. Identifiers MUST NOT be empty. +// Numeric identifiers MUST NOT include leading zeroes.". These segments can +// be dot separated. +func validatePrerelease(p string) error { + eparts := strings.Split(p, ".") + for _, p := range eparts { + if p == "" { + return ErrInvalidPrerelease + } else if containsOnly(p, num) { + if len(p) > 1 && p[0] == '0' { + return ErrSegmentStartsZero + } + } else if !containsOnly(p, allowed) { + return ErrInvalidPrerelease + } + } + + return nil +} + +// From the spec, "Build metadata MAY be denoted by +// appending a plus sign and a series of dot separated identifiers immediately +// following the patch or pre-release version. Identifiers MUST comprise only +// ASCII alphanumerics and hyphen [0-9A-Za-z-]. Identifiers MUST NOT be empty." +func validateMetadata(m string) error { + eparts := strings.Split(m, ".") + for _, p := range eparts { + if p == "" { + return ErrInvalidMetadata + } else if !containsOnly(p, allowed) { + return ErrInvalidMetadata + } + } + return nil +} + +// validateVersion checks for common validation issues but may not catch all errors +func validateVersion(m []string) error { + var err error + var v string + if m[1] != "" { + if len(m[1]) > 1 && m[1][0] == '0' { + return ErrSegmentStartsZero + } + _, err = strconv.ParseUint(m[1], 10, 64) + if err != nil { + return fmt.Errorf("error parsing version segment: %w", err) + } + } + + if m[2] != "" { + v = strings.TrimPrefix(m[2], ".") + if len(v) > 1 && v[0] == '0' { + return ErrSegmentStartsZero + } + _, err = strconv.ParseUint(v, 10, 64) + if err != nil { + return fmt.Errorf("error parsing version segment: %w", err) + } + } + + if m[3] != "" { + v = strings.TrimPrefix(m[3], ".") + if len(v) > 1 && v[0] == '0' { + return ErrSegmentStartsZero + } + _, err = strconv.ParseUint(v, 10, 64) + if err != nil { + return fmt.Errorf("error parsing version segment: %w", err) + } + } + + if m[5] != "" { + if err = validatePrerelease(m[5]); err != nil { + return err + } + } + + if m[8] != "" { + if err = validateMetadata(m[8]); err != nil { + return err + } + } + + return nil +} diff --git a/vendor/github.com/Microsoft/go-winio/.gitattributes b/vendor/github.com/Microsoft/go-winio/.gitattributes deleted file mode 100644 index 94f480de..00000000 --- a/vendor/github.com/Microsoft/go-winio/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -* text=auto eol=lf \ No newline at end of file diff --git a/vendor/github.com/Microsoft/go-winio/.gitignore b/vendor/github.com/Microsoft/go-winio/.gitignore deleted file mode 100644 index 815e2066..00000000 --- a/vendor/github.com/Microsoft/go-winio/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -.vscode/ - -*.exe - -# testing -testdata - -# go workspaces -go.work -go.work.sum diff --git a/vendor/github.com/Microsoft/go-winio/.golangci.yml b/vendor/github.com/Microsoft/go-winio/.golangci.yml deleted file mode 100644 index faedfe93..00000000 --- a/vendor/github.com/Microsoft/go-winio/.golangci.yml +++ /dev/null @@ -1,147 +0,0 @@ -linters: - enable: - # style - - containedctx # struct contains a context - - dupl # duplicate code - - errname # erorrs are named correctly - - nolintlint # "//nolint" directives are properly explained - - revive # golint replacement - - unconvert # unnecessary conversions - - wastedassign - - # bugs, performance, unused, etc ... - - contextcheck # function uses a non-inherited context - - errorlint # errors not wrapped for 1.13 - - exhaustive # check exhaustiveness of enum switch statements - - gofmt # files are gofmt'ed - - gosec # security - - nilerr # returns nil even with non-nil error - - thelper # test helpers without t.Helper() - - unparam # unused function params - -issues: - exclude-dirs: - - pkg/etw/sample - - exclude-rules: - # err is very often shadowed in nested scopes - - linters: - - govet - text: '^shadow: declaration of "err" shadows declaration' - - # ignore long lines for skip autogen directives - - linters: - - revive - text: "^line-length-limit: " - source: "^//(go:generate|sys) " - - #TODO: remove after upgrading to go1.18 - # ignore comment spacing for nolint and sys directives - - linters: - - revive - text: "^comment-spacings: no space between comment delimiter and comment text" - source: "//(cspell:|nolint:|sys |todo)" - - # not on go 1.18 yet, so no any - - linters: - - revive - text: "^use-any: since GO 1.18 'interface{}' can be replaced by 'any'" - - # allow unjustified ignores of error checks in defer statements - - linters: - - nolintlint - text: "^directive `//nolint:errcheck` should provide explanation" - source: '^\s*defer ' - - # allow unjustified ignores of error lints for io.EOF - - linters: - - nolintlint - text: "^directive `//nolint:errorlint` should provide explanation" - source: '[=|!]= io.EOF' - - -linters-settings: - exhaustive: - default-signifies-exhaustive: true - govet: - enable-all: true - disable: - # struct order is often for Win32 compat - # also, ignore pointer bytes/GC issues for now until performance becomes an issue - - fieldalignment - nolintlint: - require-explanation: true - require-specific: true - revive: - # revive is more configurable than static check, so likely the preferred alternative to static-check - # (once the perf issue is solved: https://github.com/golangci/golangci-lint/issues/2997) - enable-all-rules: - true - # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md - rules: - # rules with required arguments - - name: argument-limit - disabled: true - - name: banned-characters - disabled: true - - name: cognitive-complexity - disabled: true - - name: cyclomatic - disabled: true - - name: file-header - disabled: true - - name: function-length - disabled: true - - name: function-result-limit - disabled: true - - name: max-public-structs - disabled: true - # geneally annoying rules - - name: add-constant # complains about any and all strings and integers - disabled: true - - name: confusing-naming # we frequently use "Foo()" and "foo()" together - disabled: true - - name: flag-parameter # excessive, and a common idiom we use - disabled: true - - name: unhandled-error # warns over common fmt.Print* and io.Close; rely on errcheck instead - disabled: true - # general config - - name: line-length-limit - arguments: - - 140 - - name: var-naming - arguments: - - [] - - - CID - - CRI - - CTRD - - DACL - - DLL - - DOS - - ETW - - FSCTL - - GCS - - GMSA - - HCS - - HV - - IO - - LCOW - - LDAP - - LPAC - - LTSC - - MMIO - - NT - - OCI - - PMEM - - PWSH - - RX - - SACl - - SID - - SMB - - TX - - VHD - - VHDX - - VMID - - VPCI - - WCOW - - WIM diff --git a/vendor/github.com/Microsoft/go-winio/CODEOWNERS b/vendor/github.com/Microsoft/go-winio/CODEOWNERS deleted file mode 100644 index ae1b4942..00000000 --- a/vendor/github.com/Microsoft/go-winio/CODEOWNERS +++ /dev/null @@ -1 +0,0 @@ - * @microsoft/containerplat diff --git a/vendor/github.com/Microsoft/go-winio/LICENSE b/vendor/github.com/Microsoft/go-winio/LICENSE deleted file mode 100644 index b8b569d7..00000000 --- a/vendor/github.com/Microsoft/go-winio/LICENSE +++ /dev/null @@ -1,22 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2015 Microsoft - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - diff --git a/vendor/github.com/Microsoft/go-winio/README.md b/vendor/github.com/Microsoft/go-winio/README.md deleted file mode 100644 index 7474b4f0..00000000 --- a/vendor/github.com/Microsoft/go-winio/README.md +++ /dev/null @@ -1,89 +0,0 @@ -# go-winio [![Build Status](https://github.com/microsoft/go-winio/actions/workflows/ci.yml/badge.svg)](https://github.com/microsoft/go-winio/actions/workflows/ci.yml) - -This repository contains utilities for efficiently performing Win32 IO operations in -Go. Currently, this is focused on accessing named pipes and other file handles, and -for using named pipes as a net transport. - -This code relies on IO completion ports to avoid blocking IO on system threads, allowing Go -to reuse the thread to schedule another goroutine. This limits support to Windows Vista and -newer operating systems. This is similar to the implementation of network sockets in Go's net -package. - -Please see the LICENSE file for licensing information. - -## Contributing - -This project welcomes contributions and suggestions. -Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that -you have the right to, and actually do, grant us the rights to use your contribution. -For details, visit [Microsoft CLA](https://cla.microsoft.com). - -When you submit a pull request, a CLA-bot will automatically determine whether you need to -provide a CLA and decorate the PR appropriately (e.g., label, comment). -Simply follow the instructions provided by the bot. -You will only need to do this once across all repos using our CLA. - -Additionally, the pull request pipeline requires the following steps to be performed before -mergining. - -### Code Sign-Off - -We require that contributors sign their commits using [`git commit --signoff`][git-commit-s] -to certify they either authored the work themselves or otherwise have permission to use it in this project. - -A range of commits can be signed off using [`git rebase --signoff`][git-rebase-s]. - -Please see [the developer certificate](https://developercertificate.org) for more info, -as well as to make sure that you can attest to the rules listed. -Our CI uses the DCO Github app to ensure that all commits in a given PR are signed-off. - -### Linting - -Code must pass a linting stage, which uses [`golangci-lint`][lint]. -The linting settings are stored in [`.golangci.yaml`](./.golangci.yaml), and can be run -automatically with VSCode by adding the following to your workspace or folder settings: - -```json - "go.lintTool": "golangci-lint", - "go.lintOnSave": "package", -``` - -Additional editor [integrations options are also available][lint-ide]. - -Alternatively, `golangci-lint` can be [installed locally][lint-install] and run from the repo root: - -```shell -# use . or specify a path to only lint a package -# to show all lint errors, use flags "--max-issues-per-linter=0 --max-same-issues=0" -> golangci-lint run ./... -``` - -### Go Generate - -The pipeline checks that auto-generated code, via `go generate`, are up to date. - -This can be done for the entire repo: - -```shell -> go generate ./... -``` - -## Code of Conduct - -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). -For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or -contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. - -## Special Thanks - -Thanks to [natefinch][natefinch] for the inspiration for this library. -See [npipe](https://github.com/natefinch/npipe) for another named pipe implementation. - -[lint]: https://golangci-lint.run/ -[lint-ide]: https://golangci-lint.run/usage/integrations/#editor-integration -[lint-install]: https://golangci-lint.run/usage/install/#local-installation - -[git-commit-s]: https://git-scm.com/docs/git-commit#Documentation/git-commit.txt--s -[git-rebase-s]: https://git-scm.com/docs/git-rebase#Documentation/git-rebase.txt---signoff - -[natefinch]: https://github.com/natefinch diff --git a/vendor/github.com/Microsoft/go-winio/SECURITY.md b/vendor/github.com/Microsoft/go-winio/SECURITY.md deleted file mode 100644 index 869fdfe2..00000000 --- a/vendor/github.com/Microsoft/go-winio/SECURITY.md +++ /dev/null @@ -1,41 +0,0 @@ - - -## Security - -Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). - -If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. - -## Reporting Security Issues - -**Please do not report security vulnerabilities through public GitHub issues.** - -Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). - -If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). - -You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). - -Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: - - * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) - * Full paths of source file(s) related to the manifestation of the issue - * The location of the affected source code (tag/branch/commit or direct URL) - * Any special configuration required to reproduce the issue - * Step-by-step instructions to reproduce the issue - * Proof-of-concept or exploit code (if possible) - * Impact of the issue, including how an attacker might exploit the issue - -This information will help us triage your report more quickly. - -If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. - -## Preferred Languages - -We prefer all communications to be in English. - -## Policy - -Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). - - diff --git a/vendor/github.com/Microsoft/go-winio/backup.go b/vendor/github.com/Microsoft/go-winio/backup.go deleted file mode 100644 index b54341da..00000000 --- a/vendor/github.com/Microsoft/go-winio/backup.go +++ /dev/null @@ -1,287 +0,0 @@ -//go:build windows -// +build windows - -package winio - -import ( - "encoding/binary" - "errors" - "fmt" - "io" - "os" - "runtime" - "unicode/utf16" - - "github.com/Microsoft/go-winio/internal/fs" - "golang.org/x/sys/windows" -) - -//sys backupRead(h windows.Handle, b []byte, bytesRead *uint32, abort bool, processSecurity bool, context *uintptr) (err error) = BackupRead -//sys backupWrite(h windows.Handle, b []byte, bytesWritten *uint32, abort bool, processSecurity bool, context *uintptr) (err error) = BackupWrite - -const ( - BackupData = uint32(iota + 1) - BackupEaData - BackupSecurity - BackupAlternateData - BackupLink - BackupPropertyData - BackupObjectId //revive:disable-line:var-naming ID, not Id - BackupReparseData - BackupSparseBlock - BackupTxfsData -) - -const ( - StreamSparseAttributes = uint32(8) -) - -//nolint:revive // var-naming: ALL_CAPS -const ( - WRITE_DAC = windows.WRITE_DAC - WRITE_OWNER = windows.WRITE_OWNER - ACCESS_SYSTEM_SECURITY = windows.ACCESS_SYSTEM_SECURITY -) - -// BackupHeader represents a backup stream of a file. -type BackupHeader struct { - //revive:disable-next-line:var-naming ID, not Id - Id uint32 // The backup stream ID - Attributes uint32 // Stream attributes - Size int64 // The size of the stream in bytes - Name string // The name of the stream (for BackupAlternateData only). - Offset int64 // The offset of the stream in the file (for BackupSparseBlock only). -} - -type win32StreamID struct { - StreamID uint32 - Attributes uint32 - Size uint64 - NameSize uint32 -} - -// BackupStreamReader reads from a stream produced by the BackupRead Win32 API and produces a series -// of BackupHeader values. -type BackupStreamReader struct { - r io.Reader - bytesLeft int64 -} - -// NewBackupStreamReader produces a BackupStreamReader from any io.Reader. -func NewBackupStreamReader(r io.Reader) *BackupStreamReader { - return &BackupStreamReader{r, 0} -} - -// Next returns the next backup stream and prepares for calls to Read(). It skips the remainder of the current stream if -// it was not completely read. -func (r *BackupStreamReader) Next() (*BackupHeader, error) { - if r.bytesLeft > 0 { //nolint:nestif // todo: flatten this - if s, ok := r.r.(io.Seeker); ok { - // Make sure Seek on io.SeekCurrent sometimes succeeds - // before trying the actual seek. - if _, err := s.Seek(0, io.SeekCurrent); err == nil { - if _, err = s.Seek(r.bytesLeft, io.SeekCurrent); err != nil { - return nil, err - } - r.bytesLeft = 0 - } - } - if _, err := io.Copy(io.Discard, r); err != nil { - return nil, err - } - } - var wsi win32StreamID - if err := binary.Read(r.r, binary.LittleEndian, &wsi); err != nil { - return nil, err - } - hdr := &BackupHeader{ - Id: wsi.StreamID, - Attributes: wsi.Attributes, - Size: int64(wsi.Size), - } - if wsi.NameSize != 0 { - name := make([]uint16, int(wsi.NameSize/2)) - if err := binary.Read(r.r, binary.LittleEndian, name); err != nil { - return nil, err - } - hdr.Name = windows.UTF16ToString(name) - } - if wsi.StreamID == BackupSparseBlock { - if err := binary.Read(r.r, binary.LittleEndian, &hdr.Offset); err != nil { - return nil, err - } - hdr.Size -= 8 - } - r.bytesLeft = hdr.Size - return hdr, nil -} - -// Read reads from the current backup stream. -func (r *BackupStreamReader) Read(b []byte) (int, error) { - if r.bytesLeft == 0 { - return 0, io.EOF - } - if int64(len(b)) > r.bytesLeft { - b = b[:r.bytesLeft] - } - n, err := r.r.Read(b) - r.bytesLeft -= int64(n) - if err == io.EOF { - err = io.ErrUnexpectedEOF - } else if r.bytesLeft == 0 && err == nil { - err = io.EOF - } - return n, err -} - -// BackupStreamWriter writes a stream compatible with the BackupWrite Win32 API. -type BackupStreamWriter struct { - w io.Writer - bytesLeft int64 -} - -// NewBackupStreamWriter produces a BackupStreamWriter on top of an io.Writer. -func NewBackupStreamWriter(w io.Writer) *BackupStreamWriter { - return &BackupStreamWriter{w, 0} -} - -// WriteHeader writes the next backup stream header and prepares for calls to Write(). -func (w *BackupStreamWriter) WriteHeader(hdr *BackupHeader) error { - if w.bytesLeft != 0 { - return fmt.Errorf("missing %d bytes", w.bytesLeft) - } - name := utf16.Encode([]rune(hdr.Name)) - wsi := win32StreamID{ - StreamID: hdr.Id, - Attributes: hdr.Attributes, - Size: uint64(hdr.Size), - NameSize: uint32(len(name) * 2), - } - if hdr.Id == BackupSparseBlock { - // Include space for the int64 block offset - wsi.Size += 8 - } - if err := binary.Write(w.w, binary.LittleEndian, &wsi); err != nil { - return err - } - if len(name) != 0 { - if err := binary.Write(w.w, binary.LittleEndian, name); err != nil { - return err - } - } - if hdr.Id == BackupSparseBlock { - if err := binary.Write(w.w, binary.LittleEndian, hdr.Offset); err != nil { - return err - } - } - w.bytesLeft = hdr.Size - return nil -} - -// Write writes to the current backup stream. -func (w *BackupStreamWriter) Write(b []byte) (int, error) { - if w.bytesLeft < int64(len(b)) { - return 0, fmt.Errorf("too many bytes by %d", int64(len(b))-w.bytesLeft) - } - n, err := w.w.Write(b) - w.bytesLeft -= int64(n) - return n, err -} - -// BackupFileReader provides an io.ReadCloser interface on top of the BackupRead Win32 API. -type BackupFileReader struct { - f *os.File - includeSecurity bool - ctx uintptr -} - -// NewBackupFileReader returns a new BackupFileReader from a file handle. If includeSecurity is true, -// Read will attempt to read the security descriptor of the file. -func NewBackupFileReader(f *os.File, includeSecurity bool) *BackupFileReader { - r := &BackupFileReader{f, includeSecurity, 0} - return r -} - -// Read reads a backup stream from the file by calling the Win32 API BackupRead(). -func (r *BackupFileReader) Read(b []byte) (int, error) { - var bytesRead uint32 - err := backupRead(windows.Handle(r.f.Fd()), b, &bytesRead, false, r.includeSecurity, &r.ctx) - if err != nil { - return 0, &os.PathError{Op: "BackupRead", Path: r.f.Name(), Err: err} - } - runtime.KeepAlive(r.f) - if bytesRead == 0 { - return 0, io.EOF - } - return int(bytesRead), nil -} - -// Close frees Win32 resources associated with the BackupFileReader. It does not close -// the underlying file. -func (r *BackupFileReader) Close() error { - if r.ctx != 0 { - _ = backupRead(windows.Handle(r.f.Fd()), nil, nil, true, false, &r.ctx) - runtime.KeepAlive(r.f) - r.ctx = 0 - } - return nil -} - -// BackupFileWriter provides an io.WriteCloser interface on top of the BackupWrite Win32 API. -type BackupFileWriter struct { - f *os.File - includeSecurity bool - ctx uintptr -} - -// NewBackupFileWriter returns a new BackupFileWriter from a file handle. If includeSecurity is true, -// Write() will attempt to restore the security descriptor from the stream. -func NewBackupFileWriter(f *os.File, includeSecurity bool) *BackupFileWriter { - w := &BackupFileWriter{f, includeSecurity, 0} - return w -} - -// Write restores a portion of the file using the provided backup stream. -func (w *BackupFileWriter) Write(b []byte) (int, error) { - var bytesWritten uint32 - err := backupWrite(windows.Handle(w.f.Fd()), b, &bytesWritten, false, w.includeSecurity, &w.ctx) - if err != nil { - return 0, &os.PathError{Op: "BackupWrite", Path: w.f.Name(), Err: err} - } - runtime.KeepAlive(w.f) - if int(bytesWritten) != len(b) { - return int(bytesWritten), errors.New("not all bytes could be written") - } - return len(b), nil -} - -// Close frees Win32 resources associated with the BackupFileWriter. It does not -// close the underlying file. -func (w *BackupFileWriter) Close() error { - if w.ctx != 0 { - _ = backupWrite(windows.Handle(w.f.Fd()), nil, nil, true, false, &w.ctx) - runtime.KeepAlive(w.f) - w.ctx = 0 - } - return nil -} - -// OpenForBackup opens a file or directory, potentially skipping access checks if the backup -// or restore privileges have been acquired. -// -// If the file opened was a directory, it cannot be used with Readdir(). -func OpenForBackup(path string, access uint32, share uint32, createmode uint32) (*os.File, error) { - h, err := fs.CreateFile(path, - fs.AccessMask(access), - fs.FileShareMode(share), - nil, - fs.FileCreationDisposition(createmode), - fs.FILE_FLAG_BACKUP_SEMANTICS|fs.FILE_FLAG_OPEN_REPARSE_POINT, - 0, - ) - if err != nil { - err = &os.PathError{Op: "open", Path: path, Err: err} - return nil, err - } - return os.NewFile(uintptr(h), path), nil -} diff --git a/vendor/github.com/Microsoft/go-winio/doc.go b/vendor/github.com/Microsoft/go-winio/doc.go deleted file mode 100644 index 1f5bfe2d..00000000 --- a/vendor/github.com/Microsoft/go-winio/doc.go +++ /dev/null @@ -1,22 +0,0 @@ -// This package provides utilities for efficiently performing Win32 IO operations in Go. -// Currently, this package is provides support for genreal IO and management of -// - named pipes -// - files -// - [Hyper-V sockets] -// -// This code is similar to Go's [net] package, and uses IO completion ports to avoid -// blocking IO on system threads, allowing Go to reuse the thread to schedule other goroutines. -// -// This limits support to Windows Vista and newer operating systems. -// -// Additionally, this package provides support for: -// - creating and managing GUIDs -// - writing to [ETW] -// - opening and manageing VHDs -// - parsing [Windows Image files] -// - auto-generating Win32 API code -// -// [Hyper-V sockets]: https://docs.microsoft.com/en-us/virtualization/hyper-v-on-windows/user-guide/make-integration-service -// [ETW]: https://docs.microsoft.com/en-us/windows-hardware/drivers/devtest/event-tracing-for-windows--etw- -// [Windows Image files]: https://docs.microsoft.com/en-us/windows-hardware/manufacture/desktop/work-with-windows-images -package winio diff --git a/vendor/github.com/Microsoft/go-winio/ea.go b/vendor/github.com/Microsoft/go-winio/ea.go deleted file mode 100644 index e104dbdf..00000000 --- a/vendor/github.com/Microsoft/go-winio/ea.go +++ /dev/null @@ -1,137 +0,0 @@ -package winio - -import ( - "bytes" - "encoding/binary" - "errors" -) - -type fileFullEaInformation struct { - NextEntryOffset uint32 - Flags uint8 - NameLength uint8 - ValueLength uint16 -} - -var ( - fileFullEaInformationSize = binary.Size(&fileFullEaInformation{}) - - errInvalidEaBuffer = errors.New("invalid extended attribute buffer") - errEaNameTooLarge = errors.New("extended attribute name too large") - errEaValueTooLarge = errors.New("extended attribute value too large") -) - -// ExtendedAttribute represents a single Windows EA. -type ExtendedAttribute struct { - Name string - Value []byte - Flags uint8 -} - -func parseEa(b []byte) (ea ExtendedAttribute, nb []byte, err error) { - var info fileFullEaInformation - err = binary.Read(bytes.NewReader(b), binary.LittleEndian, &info) - if err != nil { - err = errInvalidEaBuffer - return ea, nb, err - } - - nameOffset := fileFullEaInformationSize - nameLen := int(info.NameLength) - valueOffset := nameOffset + int(info.NameLength) + 1 - valueLen := int(info.ValueLength) - nextOffset := int(info.NextEntryOffset) - if valueLen+valueOffset > len(b) || nextOffset < 0 || nextOffset > len(b) { - err = errInvalidEaBuffer - return ea, nb, err - } - - ea.Name = string(b[nameOffset : nameOffset+nameLen]) - ea.Value = b[valueOffset : valueOffset+valueLen] - ea.Flags = info.Flags - if info.NextEntryOffset != 0 { - nb = b[info.NextEntryOffset:] - } - return ea, nb, err -} - -// DecodeExtendedAttributes decodes a list of EAs from a FILE_FULL_EA_INFORMATION -// buffer retrieved from BackupRead, ZwQueryEaFile, etc. -func DecodeExtendedAttributes(b []byte) (eas []ExtendedAttribute, err error) { - for len(b) != 0 { - ea, nb, err := parseEa(b) - if err != nil { - return nil, err - } - - eas = append(eas, ea) - b = nb - } - return eas, err -} - -func writeEa(buf *bytes.Buffer, ea *ExtendedAttribute, last bool) error { - if int(uint8(len(ea.Name))) != len(ea.Name) { - return errEaNameTooLarge - } - if int(uint16(len(ea.Value))) != len(ea.Value) { - return errEaValueTooLarge - } - entrySize := uint32(fileFullEaInformationSize + len(ea.Name) + 1 + len(ea.Value)) - withPadding := (entrySize + 3) &^ 3 - nextOffset := uint32(0) - if !last { - nextOffset = withPadding - } - info := fileFullEaInformation{ - NextEntryOffset: nextOffset, - Flags: ea.Flags, - NameLength: uint8(len(ea.Name)), - ValueLength: uint16(len(ea.Value)), - } - - err := binary.Write(buf, binary.LittleEndian, &info) - if err != nil { - return err - } - - _, err = buf.Write([]byte(ea.Name)) - if err != nil { - return err - } - - err = buf.WriteByte(0) - if err != nil { - return err - } - - _, err = buf.Write(ea.Value) - if err != nil { - return err - } - - _, err = buf.Write([]byte{0, 0, 0}[0 : withPadding-entrySize]) - if err != nil { - return err - } - - return nil -} - -// EncodeExtendedAttributes encodes a list of EAs into a FILE_FULL_EA_INFORMATION -// buffer for use with BackupWrite, ZwSetEaFile, etc. -func EncodeExtendedAttributes(eas []ExtendedAttribute) ([]byte, error) { - var buf bytes.Buffer - for i := range eas { - last := false - if i == len(eas)-1 { - last = true - } - - err := writeEa(&buf, &eas[i], last) - if err != nil { - return nil, err - } - } - return buf.Bytes(), nil -} diff --git a/vendor/github.com/Microsoft/go-winio/file.go b/vendor/github.com/Microsoft/go-winio/file.go deleted file mode 100644 index fe82a180..00000000 --- a/vendor/github.com/Microsoft/go-winio/file.go +++ /dev/null @@ -1,320 +0,0 @@ -//go:build windows -// +build windows - -package winio - -import ( - "errors" - "io" - "runtime" - "sync" - "sync/atomic" - "syscall" - "time" - - "golang.org/x/sys/windows" -) - -//sys cancelIoEx(file windows.Handle, o *windows.Overlapped) (err error) = CancelIoEx -//sys createIoCompletionPort(file windows.Handle, port windows.Handle, key uintptr, threadCount uint32) (newport windows.Handle, err error) = CreateIoCompletionPort -//sys getQueuedCompletionStatus(port windows.Handle, bytes *uint32, key *uintptr, o **ioOperation, timeout uint32) (err error) = GetQueuedCompletionStatus -//sys setFileCompletionNotificationModes(h windows.Handle, flags uint8) (err error) = SetFileCompletionNotificationModes -//sys wsaGetOverlappedResult(h windows.Handle, o *windows.Overlapped, bytes *uint32, wait bool, flags *uint32) (err error) = ws2_32.WSAGetOverlappedResult - -var ( - ErrFileClosed = errors.New("file has already been closed") - ErrTimeout = &timeoutError{} -) - -type timeoutError struct{} - -func (*timeoutError) Error() string { return "i/o timeout" } -func (*timeoutError) Timeout() bool { return true } -func (*timeoutError) Temporary() bool { return true } - -type timeoutChan chan struct{} - -var ioInitOnce sync.Once -var ioCompletionPort windows.Handle - -// ioResult contains the result of an asynchronous IO operation. -type ioResult struct { - bytes uint32 - err error -} - -// ioOperation represents an outstanding asynchronous Win32 IO. -type ioOperation struct { - o windows.Overlapped - ch chan ioResult -} - -func initIO() { - h, err := createIoCompletionPort(windows.InvalidHandle, 0, 0, 0xffffffff) - if err != nil { - panic(err) - } - ioCompletionPort = h - go ioCompletionProcessor(h) -} - -// win32File implements Reader, Writer, and Closer on a Win32 handle without blocking in a syscall. -// It takes ownership of this handle and will close it if it is garbage collected. -type win32File struct { - handle windows.Handle - wg sync.WaitGroup - wgLock sync.RWMutex - closing atomic.Bool - socket bool - readDeadline deadlineHandler - writeDeadline deadlineHandler -} - -type deadlineHandler struct { - setLock sync.Mutex - channel timeoutChan - channelLock sync.RWMutex - timer *time.Timer - timedout atomic.Bool -} - -// makeWin32File makes a new win32File from an existing file handle. -func makeWin32File(h windows.Handle) (*win32File, error) { - f := &win32File{handle: h} - ioInitOnce.Do(initIO) - _, err := createIoCompletionPort(h, ioCompletionPort, 0, 0xffffffff) - if err != nil { - return nil, err - } - err = setFileCompletionNotificationModes(h, windows.FILE_SKIP_COMPLETION_PORT_ON_SUCCESS|windows.FILE_SKIP_SET_EVENT_ON_HANDLE) - if err != nil { - return nil, err - } - f.readDeadline.channel = make(timeoutChan) - f.writeDeadline.channel = make(timeoutChan) - return f, nil -} - -// Deprecated: use NewOpenFile instead. -func MakeOpenFile(h syscall.Handle) (io.ReadWriteCloser, error) { - return NewOpenFile(windows.Handle(h)) -} - -func NewOpenFile(h windows.Handle) (io.ReadWriteCloser, error) { - // If we return the result of makeWin32File directly, it can result in an - // interface-wrapped nil, rather than a nil interface value. - f, err := makeWin32File(h) - if err != nil { - return nil, err - } - return f, nil -} - -// closeHandle closes the resources associated with a Win32 handle. -func (f *win32File) closeHandle() { - f.wgLock.Lock() - // Atomically set that we are closing, releasing the resources only once. - if !f.closing.Swap(true) { - f.wgLock.Unlock() - // cancel all IO and wait for it to complete - _ = cancelIoEx(f.handle, nil) - f.wg.Wait() - // at this point, no new IO can start - windows.Close(f.handle) - f.handle = 0 - } else { - f.wgLock.Unlock() - } -} - -// Close closes a win32File. -func (f *win32File) Close() error { - f.closeHandle() - return nil -} - -// IsClosed checks if the file has been closed. -func (f *win32File) IsClosed() bool { - return f.closing.Load() -} - -// prepareIO prepares for a new IO operation. -// The caller must call f.wg.Done() when the IO is finished, prior to Close() returning. -func (f *win32File) prepareIO() (*ioOperation, error) { - f.wgLock.RLock() - if f.closing.Load() { - f.wgLock.RUnlock() - return nil, ErrFileClosed - } - f.wg.Add(1) - f.wgLock.RUnlock() - c := &ioOperation{} - c.ch = make(chan ioResult) - return c, nil -} - -// ioCompletionProcessor processes completed async IOs forever. -func ioCompletionProcessor(h windows.Handle) { - for { - var bytes uint32 - var key uintptr - var op *ioOperation - err := getQueuedCompletionStatus(h, &bytes, &key, &op, windows.INFINITE) - if op == nil { - panic(err) - } - op.ch <- ioResult{bytes, err} - } -} - -// todo: helsaawy - create an asyncIO version that takes a context - -// asyncIO processes the return value from ReadFile or WriteFile, blocking until -// the operation has actually completed. -func (f *win32File) asyncIO(c *ioOperation, d *deadlineHandler, bytes uint32, err error) (int, error) { - if err != windows.ERROR_IO_PENDING { //nolint:errorlint // err is Errno - return int(bytes), err - } - - if f.closing.Load() { - _ = cancelIoEx(f.handle, &c.o) - } - - var timeout timeoutChan - if d != nil { - d.channelLock.Lock() - timeout = d.channel - d.channelLock.Unlock() - } - - var r ioResult - select { - case r = <-c.ch: - err = r.err - if err == windows.ERROR_OPERATION_ABORTED { //nolint:errorlint // err is Errno - if f.closing.Load() { - err = ErrFileClosed - } - } else if err != nil && f.socket { - // err is from Win32. Query the overlapped structure to get the winsock error. - var bytes, flags uint32 - err = wsaGetOverlappedResult(f.handle, &c.o, &bytes, false, &flags) - } - case <-timeout: - _ = cancelIoEx(f.handle, &c.o) - r = <-c.ch - err = r.err - if err == windows.ERROR_OPERATION_ABORTED { //nolint:errorlint // err is Errno - err = ErrTimeout - } - } - - // runtime.KeepAlive is needed, as c is passed via native - // code to ioCompletionProcessor, c must remain alive - // until the channel read is complete. - // todo: (de)allocate *ioOperation via win32 heap functions, instead of needing to KeepAlive? - runtime.KeepAlive(c) - return int(r.bytes), err -} - -// Read reads from a file handle. -func (f *win32File) Read(b []byte) (int, error) { - c, err := f.prepareIO() - if err != nil { - return 0, err - } - defer f.wg.Done() - - if f.readDeadline.timedout.Load() { - return 0, ErrTimeout - } - - var bytes uint32 - err = windows.ReadFile(f.handle, b, &bytes, &c.o) - n, err := f.asyncIO(c, &f.readDeadline, bytes, err) - runtime.KeepAlive(b) - - // Handle EOF conditions. - if err == nil && n == 0 && len(b) != 0 { - return 0, io.EOF - } else if err == windows.ERROR_BROKEN_PIPE { //nolint:errorlint // err is Errno - return 0, io.EOF - } - return n, err -} - -// Write writes to a file handle. -func (f *win32File) Write(b []byte) (int, error) { - c, err := f.prepareIO() - if err != nil { - return 0, err - } - defer f.wg.Done() - - if f.writeDeadline.timedout.Load() { - return 0, ErrTimeout - } - - var bytes uint32 - err = windows.WriteFile(f.handle, b, &bytes, &c.o) - n, err := f.asyncIO(c, &f.writeDeadline, bytes, err) - runtime.KeepAlive(b) - return n, err -} - -func (f *win32File) SetReadDeadline(deadline time.Time) error { - return f.readDeadline.set(deadline) -} - -func (f *win32File) SetWriteDeadline(deadline time.Time) error { - return f.writeDeadline.set(deadline) -} - -func (f *win32File) Flush() error { - return windows.FlushFileBuffers(f.handle) -} - -func (f *win32File) Fd() uintptr { - return uintptr(f.handle) -} - -func (d *deadlineHandler) set(deadline time.Time) error { - d.setLock.Lock() - defer d.setLock.Unlock() - - if d.timer != nil { - if !d.timer.Stop() { - <-d.channel - } - d.timer = nil - } - d.timedout.Store(false) - - select { - case <-d.channel: - d.channelLock.Lock() - d.channel = make(chan struct{}) - d.channelLock.Unlock() - default: - } - - if deadline.IsZero() { - return nil - } - - timeoutIO := func() { - d.timedout.Store(true) - close(d.channel) - } - - now := time.Now() - duration := deadline.Sub(now) - if deadline.After(now) { - // Deadline is in the future, set a timer to wait - d.timer = time.AfterFunc(duration, timeoutIO) - } else { - // Deadline is in the past. Cancel all pending IO now. - timeoutIO() - } - return nil -} diff --git a/vendor/github.com/Microsoft/go-winio/fileinfo.go b/vendor/github.com/Microsoft/go-winio/fileinfo.go deleted file mode 100644 index c860eb99..00000000 --- a/vendor/github.com/Microsoft/go-winio/fileinfo.go +++ /dev/null @@ -1,106 +0,0 @@ -//go:build windows -// +build windows - -package winio - -import ( - "os" - "runtime" - "unsafe" - - "golang.org/x/sys/windows" -) - -// FileBasicInfo contains file access time and file attributes information. -type FileBasicInfo struct { - CreationTime, LastAccessTime, LastWriteTime, ChangeTime windows.Filetime - FileAttributes uint32 - _ uint32 // padding -} - -// alignedFileBasicInfo is a FileBasicInfo, but aligned to uint64 by containing -// uint64 rather than windows.Filetime. Filetime contains two uint32s. uint64 -// alignment is necessary to pass this as FILE_BASIC_INFO. -type alignedFileBasicInfo struct { - CreationTime, LastAccessTime, LastWriteTime, ChangeTime uint64 - FileAttributes uint32 - _ uint32 // padding -} - -// GetFileBasicInfo retrieves times and attributes for a file. -func GetFileBasicInfo(f *os.File) (*FileBasicInfo, error) { - bi := &alignedFileBasicInfo{} - if err := windows.GetFileInformationByHandleEx( - windows.Handle(f.Fd()), - windows.FileBasicInfo, - (*byte)(unsafe.Pointer(bi)), - uint32(unsafe.Sizeof(*bi)), - ); err != nil { - return nil, &os.PathError{Op: "GetFileInformationByHandleEx", Path: f.Name(), Err: err} - } - runtime.KeepAlive(f) - // Reinterpret the alignedFileBasicInfo as a FileBasicInfo so it matches the - // public API of this module. The data may be unnecessarily aligned. - return (*FileBasicInfo)(unsafe.Pointer(bi)), nil -} - -// SetFileBasicInfo sets times and attributes for a file. -func SetFileBasicInfo(f *os.File, bi *FileBasicInfo) error { - // Create an alignedFileBasicInfo based on a FileBasicInfo. The copy is - // suitable to pass to GetFileInformationByHandleEx. - biAligned := *(*alignedFileBasicInfo)(unsafe.Pointer(bi)) - if err := windows.SetFileInformationByHandle( - windows.Handle(f.Fd()), - windows.FileBasicInfo, - (*byte)(unsafe.Pointer(&biAligned)), - uint32(unsafe.Sizeof(biAligned)), - ); err != nil { - return &os.PathError{Op: "SetFileInformationByHandle", Path: f.Name(), Err: err} - } - runtime.KeepAlive(f) - return nil -} - -// FileStandardInfo contains extended information for the file. -// FILE_STANDARD_INFO in WinBase.h -// https://docs.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-file_standard_info -type FileStandardInfo struct { - AllocationSize, EndOfFile int64 - NumberOfLinks uint32 - DeletePending, Directory bool -} - -// GetFileStandardInfo retrieves ended information for the file. -func GetFileStandardInfo(f *os.File) (*FileStandardInfo, error) { - si := &FileStandardInfo{} - if err := windows.GetFileInformationByHandleEx(windows.Handle(f.Fd()), - windows.FileStandardInfo, - (*byte)(unsafe.Pointer(si)), - uint32(unsafe.Sizeof(*si))); err != nil { - return nil, &os.PathError{Op: "GetFileInformationByHandleEx", Path: f.Name(), Err: err} - } - runtime.KeepAlive(f) - return si, nil -} - -// FileIDInfo contains the volume serial number and file ID for a file. This pair should be -// unique on a system. -type FileIDInfo struct { - VolumeSerialNumber uint64 - FileID [16]byte -} - -// GetFileID retrieves the unique (volume, file ID) pair for a file. -func GetFileID(f *os.File) (*FileIDInfo, error) { - fileID := &FileIDInfo{} - if err := windows.GetFileInformationByHandleEx( - windows.Handle(f.Fd()), - windows.FileIdInfo, - (*byte)(unsafe.Pointer(fileID)), - uint32(unsafe.Sizeof(*fileID)), - ); err != nil { - return nil, &os.PathError{Op: "GetFileInformationByHandleEx", Path: f.Name(), Err: err} - } - runtime.KeepAlive(f) - return fileID, nil -} diff --git a/vendor/github.com/Microsoft/go-winio/hvsock.go b/vendor/github.com/Microsoft/go-winio/hvsock.go deleted file mode 100644 index c4fdd9d4..00000000 --- a/vendor/github.com/Microsoft/go-winio/hvsock.go +++ /dev/null @@ -1,582 +0,0 @@ -//go:build windows -// +build windows - -package winio - -import ( - "context" - "errors" - "fmt" - "io" - "net" - "os" - "time" - "unsafe" - - "golang.org/x/sys/windows" - - "github.com/Microsoft/go-winio/internal/socket" - "github.com/Microsoft/go-winio/pkg/guid" -) - -const afHVSock = 34 // AF_HYPERV - -// Well known Service and VM IDs -// https://docs.microsoft.com/en-us/virtualization/hyper-v-on-windows/user-guide/make-integration-service#vmid-wildcards - -// HvsockGUIDWildcard is the wildcard VmId for accepting connections from all partitions. -func HvsockGUIDWildcard() guid.GUID { // 00000000-0000-0000-0000-000000000000 - return guid.GUID{} -} - -// HvsockGUIDBroadcast is the wildcard VmId for broadcasting sends to all partitions. -func HvsockGUIDBroadcast() guid.GUID { // ffffffff-ffff-ffff-ffff-ffffffffffff - return guid.GUID{ - Data1: 0xffffffff, - Data2: 0xffff, - Data3: 0xffff, - Data4: [8]uint8{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, - } -} - -// HvsockGUIDLoopback is the Loopback VmId for accepting connections to the same partition as the connector. -func HvsockGUIDLoopback() guid.GUID { // e0e16197-dd56-4a10-9195-5ee7a155a838 - return guid.GUID{ - Data1: 0xe0e16197, - Data2: 0xdd56, - Data3: 0x4a10, - Data4: [8]uint8{0x91, 0x95, 0x5e, 0xe7, 0xa1, 0x55, 0xa8, 0x38}, - } -} - -// HvsockGUIDSiloHost is the address of a silo's host partition: -// - The silo host of a hosted silo is the utility VM. -// - The silo host of a silo on a physical host is the physical host. -func HvsockGUIDSiloHost() guid.GUID { // 36bd0c5c-7276-4223-88ba-7d03b654c568 - return guid.GUID{ - Data1: 0x36bd0c5c, - Data2: 0x7276, - Data3: 0x4223, - Data4: [8]byte{0x88, 0xba, 0x7d, 0x03, 0xb6, 0x54, 0xc5, 0x68}, - } -} - -// HvsockGUIDChildren is the wildcard VmId for accepting connections from the connector's child partitions. -func HvsockGUIDChildren() guid.GUID { // 90db8b89-0d35-4f79-8ce9-49ea0ac8b7cd - return guid.GUID{ - Data1: 0x90db8b89, - Data2: 0xd35, - Data3: 0x4f79, - Data4: [8]uint8{0x8c, 0xe9, 0x49, 0xea, 0xa, 0xc8, 0xb7, 0xcd}, - } -} - -// HvsockGUIDParent is the wildcard VmId for accepting connections from the connector's parent partition. -// Listening on this VmId accepts connection from: -// - Inside silos: silo host partition. -// - Inside hosted silo: host of the VM. -// - Inside VM: VM host. -// - Physical host: Not supported. -func HvsockGUIDParent() guid.GUID { // a42e7cda-d03f-480c-9cc2-a4de20abb878 - return guid.GUID{ - Data1: 0xa42e7cda, - Data2: 0xd03f, - Data3: 0x480c, - Data4: [8]uint8{0x9c, 0xc2, 0xa4, 0xde, 0x20, 0xab, 0xb8, 0x78}, - } -} - -// hvsockVsockServiceTemplate is the Service GUID used for the VSOCK protocol. -func hvsockVsockServiceTemplate() guid.GUID { // 00000000-facb-11e6-bd58-64006a7986d3 - return guid.GUID{ - Data2: 0xfacb, - Data3: 0x11e6, - Data4: [8]uint8{0xbd, 0x58, 0x64, 0x00, 0x6a, 0x79, 0x86, 0xd3}, - } -} - -// An HvsockAddr is an address for a AF_HYPERV socket. -type HvsockAddr struct { - VMID guid.GUID - ServiceID guid.GUID -} - -type rawHvsockAddr struct { - Family uint16 - _ uint16 - VMID guid.GUID - ServiceID guid.GUID -} - -var _ socket.RawSockaddr = &rawHvsockAddr{} - -// Network returns the address's network name, "hvsock". -func (*HvsockAddr) Network() string { - return "hvsock" -} - -func (addr *HvsockAddr) String() string { - return fmt.Sprintf("%s:%s", &addr.VMID, &addr.ServiceID) -} - -// VsockServiceID returns an hvsock service ID corresponding to the specified AF_VSOCK port. -func VsockServiceID(port uint32) guid.GUID { - g := hvsockVsockServiceTemplate() // make a copy - g.Data1 = port - return g -} - -func (addr *HvsockAddr) raw() rawHvsockAddr { - return rawHvsockAddr{ - Family: afHVSock, - VMID: addr.VMID, - ServiceID: addr.ServiceID, - } -} - -func (addr *HvsockAddr) fromRaw(raw *rawHvsockAddr) { - addr.VMID = raw.VMID - addr.ServiceID = raw.ServiceID -} - -// Sockaddr returns a pointer to and the size of this struct. -// -// Implements the [socket.RawSockaddr] interface, and allows use in -// [socket.Bind] and [socket.ConnectEx]. -func (r *rawHvsockAddr) Sockaddr() (unsafe.Pointer, int32, error) { - return unsafe.Pointer(r), int32(unsafe.Sizeof(rawHvsockAddr{})), nil -} - -// Sockaddr interface allows use with `sockets.Bind()` and `.ConnectEx()`. -func (r *rawHvsockAddr) FromBytes(b []byte) error { - n := int(unsafe.Sizeof(rawHvsockAddr{})) - - if len(b) < n { - return fmt.Errorf("got %d, want %d: %w", len(b), n, socket.ErrBufferSize) - } - - copy(unsafe.Slice((*byte)(unsafe.Pointer(r)), n), b[:n]) - if r.Family != afHVSock { - return fmt.Errorf("got %d, want %d: %w", r.Family, afHVSock, socket.ErrAddrFamily) - } - - return nil -} - -// HvsockListener is a socket listener for the AF_HYPERV address family. -type HvsockListener struct { - sock *win32File - addr HvsockAddr -} - -var _ net.Listener = &HvsockListener{} - -// HvsockConn is a connected socket of the AF_HYPERV address family. -type HvsockConn struct { - sock *win32File - local, remote HvsockAddr -} - -var _ net.Conn = &HvsockConn{} - -func newHVSocket() (*win32File, error) { - fd, err := windows.Socket(afHVSock, windows.SOCK_STREAM, 1) - if err != nil { - return nil, os.NewSyscallError("socket", err) - } - f, err := makeWin32File(fd) - if err != nil { - windows.Close(fd) - return nil, err - } - f.socket = true - return f, nil -} - -// ListenHvsock listens for connections on the specified hvsock address. -func ListenHvsock(addr *HvsockAddr) (_ *HvsockListener, err error) { - l := &HvsockListener{addr: *addr} - - var sock *win32File - sock, err = newHVSocket() - if err != nil { - return nil, l.opErr("listen", err) - } - defer func() { - if err != nil { - _ = sock.Close() - } - }() - - sa := addr.raw() - err = socket.Bind(sock.handle, &sa) - if err != nil { - return nil, l.opErr("listen", os.NewSyscallError("socket", err)) - } - err = windows.Listen(sock.handle, 16) - if err != nil { - return nil, l.opErr("listen", os.NewSyscallError("listen", err)) - } - return &HvsockListener{sock: sock, addr: *addr}, nil -} - -func (l *HvsockListener) opErr(op string, err error) error { - return &net.OpError{Op: op, Net: "hvsock", Addr: &l.addr, Err: err} -} - -// Addr returns the listener's network address. -func (l *HvsockListener) Addr() net.Addr { - return &l.addr -} - -// Accept waits for the next connection and returns it. -func (l *HvsockListener) Accept() (_ net.Conn, err error) { - sock, err := newHVSocket() - if err != nil { - return nil, l.opErr("accept", err) - } - defer func() { - if sock != nil { - sock.Close() - } - }() - c, err := l.sock.prepareIO() - if err != nil { - return nil, l.opErr("accept", err) - } - defer l.sock.wg.Done() - - // AcceptEx, per documentation, requires an extra 16 bytes per address. - // - // https://docs.microsoft.com/en-us/windows/win32/api/mswsock/nf-mswsock-acceptex - const addrlen = uint32(16 + unsafe.Sizeof(rawHvsockAddr{})) - var addrbuf [addrlen * 2]byte - - var bytes uint32 - err = windows.AcceptEx(l.sock.handle, sock.handle, &addrbuf[0], 0 /* rxdatalen */, addrlen, addrlen, &bytes, &c.o) - if _, err = l.sock.asyncIO(c, nil, bytes, err); err != nil { - return nil, l.opErr("accept", os.NewSyscallError("acceptex", err)) - } - - conn := &HvsockConn{ - sock: sock, - } - // The local address returned in the AcceptEx buffer is the same as the Listener socket's - // address. However, the service GUID reported by GetSockName is different from the Listeners - // socket, and is sometimes the same as the local address of the socket that dialed the - // address, with the service GUID.Data1 incremented, but othertimes is different. - // todo: does the local address matter? is the listener's address or the actual address appropriate? - conn.local.fromRaw((*rawHvsockAddr)(unsafe.Pointer(&addrbuf[0]))) - conn.remote.fromRaw((*rawHvsockAddr)(unsafe.Pointer(&addrbuf[addrlen]))) - - // initialize the accepted socket and update its properties with those of the listening socket - if err = windows.Setsockopt(sock.handle, - windows.SOL_SOCKET, windows.SO_UPDATE_ACCEPT_CONTEXT, - (*byte)(unsafe.Pointer(&l.sock.handle)), int32(unsafe.Sizeof(l.sock.handle))); err != nil { - return nil, conn.opErr("accept", os.NewSyscallError("setsockopt", err)) - } - - sock = nil - return conn, nil -} - -// Close closes the listener, causing any pending Accept calls to fail. -func (l *HvsockListener) Close() error { - return l.sock.Close() -} - -// HvsockDialer configures and dials a Hyper-V Socket (ie, [HvsockConn]). -type HvsockDialer struct { - // Deadline is the time the Dial operation must connect before erroring. - Deadline time.Time - - // Retries is the number of additional connects to try if the connection times out, is refused, - // or the host is unreachable - Retries uint - - // RetryWait is the time to wait after a connection error to retry - RetryWait time.Duration - - rt *time.Timer // redial wait timer -} - -// Dial the Hyper-V socket at addr. -// -// See [HvsockDialer.Dial] for more information. -func Dial(ctx context.Context, addr *HvsockAddr) (conn *HvsockConn, err error) { - return (&HvsockDialer{}).Dial(ctx, addr) -} - -// Dial attempts to connect to the Hyper-V socket at addr, and returns a connection if successful. -// Will attempt (HvsockDialer).Retries if dialing fails, waiting (HvsockDialer).RetryWait between -// retries. -// -// Dialing can be cancelled either by providing (HvsockDialer).Deadline, or cancelling ctx. -func (d *HvsockDialer) Dial(ctx context.Context, addr *HvsockAddr) (conn *HvsockConn, err error) { - op := "dial" - // create the conn early to use opErr() - conn = &HvsockConn{ - remote: *addr, - } - - if !d.Deadline.IsZero() { - var cancel context.CancelFunc - ctx, cancel = context.WithDeadline(ctx, d.Deadline) - defer cancel() - } - - // preemptive timeout/cancellation check - if err = ctx.Err(); err != nil { - return nil, conn.opErr(op, err) - } - - sock, err := newHVSocket() - if err != nil { - return nil, conn.opErr(op, err) - } - defer func() { - if sock != nil { - sock.Close() - } - }() - - sa := addr.raw() - err = socket.Bind(sock.handle, &sa) - if err != nil { - return nil, conn.opErr(op, os.NewSyscallError("bind", err)) - } - - c, err := sock.prepareIO() - if err != nil { - return nil, conn.opErr(op, err) - } - defer sock.wg.Done() - var bytes uint32 - for i := uint(0); i <= d.Retries; i++ { - err = socket.ConnectEx( - sock.handle, - &sa, - nil, // sendBuf - 0, // sendDataLen - &bytes, - (*windows.Overlapped)(unsafe.Pointer(&c.o))) - _, err = sock.asyncIO(c, nil, bytes, err) - if i < d.Retries && canRedial(err) { - if err = d.redialWait(ctx); err == nil { - continue - } - } - break - } - if err != nil { - return nil, conn.opErr(op, os.NewSyscallError("connectex", err)) - } - - // update the connection properties, so shutdown can be used - if err = windows.Setsockopt( - sock.handle, - windows.SOL_SOCKET, - windows.SO_UPDATE_CONNECT_CONTEXT, - nil, // optvalue - 0, // optlen - ); err != nil { - return nil, conn.opErr(op, os.NewSyscallError("setsockopt", err)) - } - - // get the local name - var sal rawHvsockAddr - err = socket.GetSockName(sock.handle, &sal) - if err != nil { - return nil, conn.opErr(op, os.NewSyscallError("getsockname", err)) - } - conn.local.fromRaw(&sal) - - // one last check for timeout, since asyncIO doesn't check the context - if err = ctx.Err(); err != nil { - return nil, conn.opErr(op, err) - } - - conn.sock = sock - sock = nil - - return conn, nil -} - -// redialWait waits before attempting to redial, resetting the timer as appropriate. -func (d *HvsockDialer) redialWait(ctx context.Context) (err error) { - if d.RetryWait == 0 { - return nil - } - - if d.rt == nil { - d.rt = time.NewTimer(d.RetryWait) - } else { - // should already be stopped and drained - d.rt.Reset(d.RetryWait) - } - - select { - case <-ctx.Done(): - case <-d.rt.C: - return nil - } - - // stop and drain the timer - if !d.rt.Stop() { - <-d.rt.C - } - return ctx.Err() -} - -// assumes error is a plain, unwrapped windows.Errno provided by direct syscall. -func canRedial(err error) bool { - //nolint:errorlint // guaranteed to be an Errno - switch err { - case windows.WSAECONNREFUSED, windows.WSAENETUNREACH, windows.WSAETIMEDOUT, - windows.ERROR_CONNECTION_REFUSED, windows.ERROR_CONNECTION_UNAVAIL: - return true - default: - return false - } -} - -func (conn *HvsockConn) opErr(op string, err error) error { - // translate from "file closed" to "socket closed" - if errors.Is(err, ErrFileClosed) { - err = socket.ErrSocketClosed - } - return &net.OpError{Op: op, Net: "hvsock", Source: &conn.local, Addr: &conn.remote, Err: err} -} - -func (conn *HvsockConn) Read(b []byte) (int, error) { - c, err := conn.sock.prepareIO() - if err != nil { - return 0, conn.opErr("read", err) - } - defer conn.sock.wg.Done() - buf := windows.WSABuf{Buf: &b[0], Len: uint32(len(b))} - var flags, bytes uint32 - err = windows.WSARecv(conn.sock.handle, &buf, 1, &bytes, &flags, &c.o, nil) - n, err := conn.sock.asyncIO(c, &conn.sock.readDeadline, bytes, err) - if err != nil { - var eno windows.Errno - if errors.As(err, &eno) { - err = os.NewSyscallError("wsarecv", eno) - } - return 0, conn.opErr("read", err) - } else if n == 0 { - err = io.EOF - } - return n, err -} - -func (conn *HvsockConn) Write(b []byte) (int, error) { - t := 0 - for len(b) != 0 { - n, err := conn.write(b) - if err != nil { - return t + n, err - } - t += n - b = b[n:] - } - return t, nil -} - -func (conn *HvsockConn) write(b []byte) (int, error) { - c, err := conn.sock.prepareIO() - if err != nil { - return 0, conn.opErr("write", err) - } - defer conn.sock.wg.Done() - buf := windows.WSABuf{Buf: &b[0], Len: uint32(len(b))} - var bytes uint32 - err = windows.WSASend(conn.sock.handle, &buf, 1, &bytes, 0, &c.o, nil) - n, err := conn.sock.asyncIO(c, &conn.sock.writeDeadline, bytes, err) - if err != nil { - var eno windows.Errno - if errors.As(err, &eno) { - err = os.NewSyscallError("wsasend", eno) - } - return 0, conn.opErr("write", err) - } - return n, err -} - -// Close closes the socket connection, failing any pending read or write calls. -func (conn *HvsockConn) Close() error { - return conn.sock.Close() -} - -func (conn *HvsockConn) IsClosed() bool { - return conn.sock.IsClosed() -} - -// shutdown disables sending or receiving on a socket. -func (conn *HvsockConn) shutdown(how int) error { - if conn.IsClosed() { - return socket.ErrSocketClosed - } - - err := windows.Shutdown(conn.sock.handle, how) - if err != nil { - // If the connection was closed, shutdowns fail with "not connected" - if errors.Is(err, windows.WSAENOTCONN) || - errors.Is(err, windows.WSAESHUTDOWN) { - err = socket.ErrSocketClosed - } - return os.NewSyscallError("shutdown", err) - } - return nil -} - -// CloseRead shuts down the read end of the socket, preventing future read operations. -func (conn *HvsockConn) CloseRead() error { - err := conn.shutdown(windows.SHUT_RD) - if err != nil { - return conn.opErr("closeread", err) - } - return nil -} - -// CloseWrite shuts down the write end of the socket, preventing future write operations and -// notifying the other endpoint that no more data will be written. -func (conn *HvsockConn) CloseWrite() error { - err := conn.shutdown(windows.SHUT_WR) - if err != nil { - return conn.opErr("closewrite", err) - } - return nil -} - -// LocalAddr returns the local address of the connection. -func (conn *HvsockConn) LocalAddr() net.Addr { - return &conn.local -} - -// RemoteAddr returns the remote address of the connection. -func (conn *HvsockConn) RemoteAddr() net.Addr { - return &conn.remote -} - -// SetDeadline implements the net.Conn SetDeadline method. -func (conn *HvsockConn) SetDeadline(t time.Time) error { - // todo: implement `SetDeadline` for `win32File` - if err := conn.SetReadDeadline(t); err != nil { - return fmt.Errorf("set read deadline: %w", err) - } - if err := conn.SetWriteDeadline(t); err != nil { - return fmt.Errorf("set write deadline: %w", err) - } - return nil -} - -// SetReadDeadline implements the net.Conn SetReadDeadline method. -func (conn *HvsockConn) SetReadDeadline(t time.Time) error { - return conn.sock.SetReadDeadline(t) -} - -// SetWriteDeadline implements the net.Conn SetWriteDeadline method. -func (conn *HvsockConn) SetWriteDeadline(t time.Time) error { - return conn.sock.SetWriteDeadline(t) -} diff --git a/vendor/github.com/Microsoft/go-winio/internal/fs/doc.go b/vendor/github.com/Microsoft/go-winio/internal/fs/doc.go deleted file mode 100644 index 1f653881..00000000 --- a/vendor/github.com/Microsoft/go-winio/internal/fs/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// This package contains Win32 filesystem functionality. -package fs diff --git a/vendor/github.com/Microsoft/go-winio/internal/fs/fs.go b/vendor/github.com/Microsoft/go-winio/internal/fs/fs.go deleted file mode 100644 index 0cd9621d..00000000 --- a/vendor/github.com/Microsoft/go-winio/internal/fs/fs.go +++ /dev/null @@ -1,262 +0,0 @@ -//go:build windows - -package fs - -import ( - "golang.org/x/sys/windows" - - "github.com/Microsoft/go-winio/internal/stringbuffer" -) - -//go:generate go run github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go fs.go - -// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew -//sys CreateFile(name string, access AccessMask, mode FileShareMode, sa *windows.SecurityAttributes, createmode FileCreationDisposition, attrs FileFlagOrAttribute, templatefile windows.Handle) (handle windows.Handle, err error) [failretval==windows.InvalidHandle] = CreateFileW - -const NullHandle windows.Handle = 0 - -// AccessMask defines standard, specific, and generic rights. -// -// Used with CreateFile and NtCreateFile (and co.). -// -// Bitmask: -// 3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 -// 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 -// +---------------+---------------+-------------------------------+ -// |G|G|G|G|Resvd|A| StandardRights| SpecificRights | -// |R|W|E|A| |S| | | -// +-+-------------+---------------+-------------------------------+ -// -// GR Generic Read -// GW Generic Write -// GE Generic Exectue -// GA Generic All -// Resvd Reserved -// AS Access Security System -// -// https://learn.microsoft.com/en-us/windows/win32/secauthz/access-mask -// -// https://learn.microsoft.com/en-us/windows/win32/secauthz/generic-access-rights -// -// https://learn.microsoft.com/en-us/windows/win32/fileio/file-access-rights-constants -type AccessMask = windows.ACCESS_MASK - -//nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API. -const ( - // Not actually any. - // - // For CreateFile: "query certain metadata such as file, directory, or device attributes without accessing that file or device" - // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew#parameters - FILE_ANY_ACCESS AccessMask = 0 - - GENERIC_READ AccessMask = 0x8000_0000 - GENERIC_WRITE AccessMask = 0x4000_0000 - GENERIC_EXECUTE AccessMask = 0x2000_0000 - GENERIC_ALL AccessMask = 0x1000_0000 - ACCESS_SYSTEM_SECURITY AccessMask = 0x0100_0000 - - // Specific Object Access - // from ntioapi.h - - FILE_READ_DATA AccessMask = (0x0001) // file & pipe - FILE_LIST_DIRECTORY AccessMask = (0x0001) // directory - - FILE_WRITE_DATA AccessMask = (0x0002) // file & pipe - FILE_ADD_FILE AccessMask = (0x0002) // directory - - FILE_APPEND_DATA AccessMask = (0x0004) // file - FILE_ADD_SUBDIRECTORY AccessMask = (0x0004) // directory - FILE_CREATE_PIPE_INSTANCE AccessMask = (0x0004) // named pipe - - FILE_READ_EA AccessMask = (0x0008) // file & directory - FILE_READ_PROPERTIES AccessMask = FILE_READ_EA - - FILE_WRITE_EA AccessMask = (0x0010) // file & directory - FILE_WRITE_PROPERTIES AccessMask = FILE_WRITE_EA - - FILE_EXECUTE AccessMask = (0x0020) // file - FILE_TRAVERSE AccessMask = (0x0020) // directory - - FILE_DELETE_CHILD AccessMask = (0x0040) // directory - - FILE_READ_ATTRIBUTES AccessMask = (0x0080) // all - - FILE_WRITE_ATTRIBUTES AccessMask = (0x0100) // all - - FILE_ALL_ACCESS AccessMask = (STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0x1FF) - FILE_GENERIC_READ AccessMask = (STANDARD_RIGHTS_READ | FILE_READ_DATA | FILE_READ_ATTRIBUTES | FILE_READ_EA | SYNCHRONIZE) - FILE_GENERIC_WRITE AccessMask = (STANDARD_RIGHTS_WRITE | FILE_WRITE_DATA | FILE_WRITE_ATTRIBUTES | FILE_WRITE_EA | FILE_APPEND_DATA | SYNCHRONIZE) - FILE_GENERIC_EXECUTE AccessMask = (STANDARD_RIGHTS_EXECUTE | FILE_READ_ATTRIBUTES | FILE_EXECUTE | SYNCHRONIZE) - - SPECIFIC_RIGHTS_ALL AccessMask = 0x0000FFFF - - // Standard Access - // from ntseapi.h - - DELETE AccessMask = 0x0001_0000 - READ_CONTROL AccessMask = 0x0002_0000 - WRITE_DAC AccessMask = 0x0004_0000 - WRITE_OWNER AccessMask = 0x0008_0000 - SYNCHRONIZE AccessMask = 0x0010_0000 - - STANDARD_RIGHTS_REQUIRED AccessMask = 0x000F_0000 - - STANDARD_RIGHTS_READ AccessMask = READ_CONTROL - STANDARD_RIGHTS_WRITE AccessMask = READ_CONTROL - STANDARD_RIGHTS_EXECUTE AccessMask = READ_CONTROL - - STANDARD_RIGHTS_ALL AccessMask = 0x001F_0000 -) - -type FileShareMode uint32 - -//nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API. -const ( - FILE_SHARE_NONE FileShareMode = 0x00 - FILE_SHARE_READ FileShareMode = 0x01 - FILE_SHARE_WRITE FileShareMode = 0x02 - FILE_SHARE_DELETE FileShareMode = 0x04 - FILE_SHARE_VALID_FLAGS FileShareMode = 0x07 -) - -type FileCreationDisposition uint32 - -//nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API. -const ( - // from winbase.h - - CREATE_NEW FileCreationDisposition = 0x01 - CREATE_ALWAYS FileCreationDisposition = 0x02 - OPEN_EXISTING FileCreationDisposition = 0x03 - OPEN_ALWAYS FileCreationDisposition = 0x04 - TRUNCATE_EXISTING FileCreationDisposition = 0x05 -) - -// Create disposition values for NtCreate* -type NTFileCreationDisposition uint32 - -//nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API. -const ( - // From ntioapi.h - - FILE_SUPERSEDE NTFileCreationDisposition = 0x00 - FILE_OPEN NTFileCreationDisposition = 0x01 - FILE_CREATE NTFileCreationDisposition = 0x02 - FILE_OPEN_IF NTFileCreationDisposition = 0x03 - FILE_OVERWRITE NTFileCreationDisposition = 0x04 - FILE_OVERWRITE_IF NTFileCreationDisposition = 0x05 - FILE_MAXIMUM_DISPOSITION NTFileCreationDisposition = 0x05 -) - -// CreateFile and co. take flags or attributes together as one parameter. -// Define alias until we can use generics to allow both -// -// https://learn.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants -type FileFlagOrAttribute uint32 - -//nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API. -const ( - // from winnt.h - - FILE_FLAG_WRITE_THROUGH FileFlagOrAttribute = 0x8000_0000 - FILE_FLAG_OVERLAPPED FileFlagOrAttribute = 0x4000_0000 - FILE_FLAG_NO_BUFFERING FileFlagOrAttribute = 0x2000_0000 - FILE_FLAG_RANDOM_ACCESS FileFlagOrAttribute = 0x1000_0000 - FILE_FLAG_SEQUENTIAL_SCAN FileFlagOrAttribute = 0x0800_0000 - FILE_FLAG_DELETE_ON_CLOSE FileFlagOrAttribute = 0x0400_0000 - FILE_FLAG_BACKUP_SEMANTICS FileFlagOrAttribute = 0x0200_0000 - FILE_FLAG_POSIX_SEMANTICS FileFlagOrAttribute = 0x0100_0000 - FILE_FLAG_OPEN_REPARSE_POINT FileFlagOrAttribute = 0x0020_0000 - FILE_FLAG_OPEN_NO_RECALL FileFlagOrAttribute = 0x0010_0000 - FILE_FLAG_FIRST_PIPE_INSTANCE FileFlagOrAttribute = 0x0008_0000 -) - -// NtCreate* functions take a dedicated CreateOptions parameter. -// -// https://learn.microsoft.com/en-us/windows/win32/api/Winternl/nf-winternl-ntcreatefile -// -// https://learn.microsoft.com/en-us/windows/win32/devnotes/nt-create-named-pipe-file -type NTCreateOptions uint32 - -//nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API. -const ( - // From ntioapi.h - - FILE_DIRECTORY_FILE NTCreateOptions = 0x0000_0001 - FILE_WRITE_THROUGH NTCreateOptions = 0x0000_0002 - FILE_SEQUENTIAL_ONLY NTCreateOptions = 0x0000_0004 - FILE_NO_INTERMEDIATE_BUFFERING NTCreateOptions = 0x0000_0008 - - FILE_SYNCHRONOUS_IO_ALERT NTCreateOptions = 0x0000_0010 - FILE_SYNCHRONOUS_IO_NONALERT NTCreateOptions = 0x0000_0020 - FILE_NON_DIRECTORY_FILE NTCreateOptions = 0x0000_0040 - FILE_CREATE_TREE_CONNECTION NTCreateOptions = 0x0000_0080 - - FILE_COMPLETE_IF_OPLOCKED NTCreateOptions = 0x0000_0100 - FILE_NO_EA_KNOWLEDGE NTCreateOptions = 0x0000_0200 - FILE_DISABLE_TUNNELING NTCreateOptions = 0x0000_0400 - FILE_RANDOM_ACCESS NTCreateOptions = 0x0000_0800 - - FILE_DELETE_ON_CLOSE NTCreateOptions = 0x0000_1000 - FILE_OPEN_BY_FILE_ID NTCreateOptions = 0x0000_2000 - FILE_OPEN_FOR_BACKUP_INTENT NTCreateOptions = 0x0000_4000 - FILE_NO_COMPRESSION NTCreateOptions = 0x0000_8000 -) - -type FileSQSFlag = FileFlagOrAttribute - -//nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API. -const ( - // from winbase.h - - SECURITY_ANONYMOUS FileSQSFlag = FileSQSFlag(SecurityAnonymous << 16) - SECURITY_IDENTIFICATION FileSQSFlag = FileSQSFlag(SecurityIdentification << 16) - SECURITY_IMPERSONATION FileSQSFlag = FileSQSFlag(SecurityImpersonation << 16) - SECURITY_DELEGATION FileSQSFlag = FileSQSFlag(SecurityDelegation << 16) - - SECURITY_SQOS_PRESENT FileSQSFlag = 0x0010_0000 - SECURITY_VALID_SQOS_FLAGS FileSQSFlag = 0x001F_0000 -) - -// GetFinalPathNameByHandle flags -// -// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfinalpathnamebyhandlew#parameters -type GetFinalPathFlag uint32 - -//nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API. -const ( - GetFinalPathDefaultFlag GetFinalPathFlag = 0x0 - - FILE_NAME_NORMALIZED GetFinalPathFlag = 0x0 - FILE_NAME_OPENED GetFinalPathFlag = 0x8 - - VOLUME_NAME_DOS GetFinalPathFlag = 0x0 - VOLUME_NAME_GUID GetFinalPathFlag = 0x1 - VOLUME_NAME_NT GetFinalPathFlag = 0x2 - VOLUME_NAME_NONE GetFinalPathFlag = 0x4 -) - -// getFinalPathNameByHandle facilitates calling the Windows API GetFinalPathNameByHandle -// with the given handle and flags. It transparently takes care of creating a buffer of the -// correct size for the call. -// -// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfinalpathnamebyhandlew -func GetFinalPathNameByHandle(h windows.Handle, flags GetFinalPathFlag) (string, error) { - b := stringbuffer.NewWString() - //TODO: can loop infinitely if Win32 keeps returning the same (or a larger) n? - for { - n, err := windows.GetFinalPathNameByHandle(h, b.Pointer(), b.Cap(), uint32(flags)) - if err != nil { - return "", err - } - // If the buffer wasn't large enough, n will be the total size needed (including null terminator). - // Resize and try again. - if n > b.Cap() { - b.ResizeTo(n) - continue - } - // If the buffer is large enough, n will be the size not including the null terminator. - // Convert to a Go string and return. - return b.String(), nil - } -} diff --git a/vendor/github.com/Microsoft/go-winio/internal/fs/security.go b/vendor/github.com/Microsoft/go-winio/internal/fs/security.go deleted file mode 100644 index 81760ac6..00000000 --- a/vendor/github.com/Microsoft/go-winio/internal/fs/security.go +++ /dev/null @@ -1,12 +0,0 @@ -package fs - -// https://learn.microsoft.com/en-us/windows/win32/api/winnt/ne-winnt-security_impersonation_level -type SecurityImpersonationLevel int32 // C default enums underlying type is `int`, which is Go `int32` - -// Impersonation levels -const ( - SecurityAnonymous SecurityImpersonationLevel = 0 - SecurityIdentification SecurityImpersonationLevel = 1 - SecurityImpersonation SecurityImpersonationLevel = 2 - SecurityDelegation SecurityImpersonationLevel = 3 -) diff --git a/vendor/github.com/Microsoft/go-winio/internal/fs/zsyscall_windows.go b/vendor/github.com/Microsoft/go-winio/internal/fs/zsyscall_windows.go deleted file mode 100644 index a94e234c..00000000 --- a/vendor/github.com/Microsoft/go-winio/internal/fs/zsyscall_windows.go +++ /dev/null @@ -1,61 +0,0 @@ -//go:build windows - -// Code generated by 'go generate' using "github.com/Microsoft/go-winio/tools/mkwinsyscall"; DO NOT EDIT. - -package fs - -import ( - "syscall" - "unsafe" - - "golang.org/x/sys/windows" -) - -var _ unsafe.Pointer - -// Do the interface allocations only once for common -// Errno values. -const ( - errnoERROR_IO_PENDING = 997 -) - -var ( - errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING) - errERROR_EINVAL error = syscall.EINVAL -) - -// errnoErr returns common boxed Errno values, to prevent -// allocations at runtime. -func errnoErr(e syscall.Errno) error { - switch e { - case 0: - return errERROR_EINVAL - case errnoERROR_IO_PENDING: - return errERROR_IO_PENDING - } - return e -} - -var ( - modkernel32 = windows.NewLazySystemDLL("kernel32.dll") - - procCreateFileW = modkernel32.NewProc("CreateFileW") -) - -func CreateFile(name string, access AccessMask, mode FileShareMode, sa *windows.SecurityAttributes, createmode FileCreationDisposition, attrs FileFlagOrAttribute, templatefile windows.Handle) (handle windows.Handle, err error) { - var _p0 *uint16 - _p0, err = syscall.UTF16PtrFromString(name) - if err != nil { - return - } - return _CreateFile(_p0, access, mode, sa, createmode, attrs, templatefile) -} - -func _CreateFile(name *uint16, access AccessMask, mode FileShareMode, sa *windows.SecurityAttributes, createmode FileCreationDisposition, attrs FileFlagOrAttribute, templatefile windows.Handle) (handle windows.Handle, err error) { - r0, _, e1 := syscall.SyscallN(procCreateFileW.Addr(), uintptr(unsafe.Pointer(name)), uintptr(access), uintptr(mode), uintptr(unsafe.Pointer(sa)), uintptr(createmode), uintptr(attrs), uintptr(templatefile)) - handle = windows.Handle(r0) - if handle == windows.InvalidHandle { - err = errnoErr(e1) - } - return -} diff --git a/vendor/github.com/Microsoft/go-winio/internal/socket/rawaddr.go b/vendor/github.com/Microsoft/go-winio/internal/socket/rawaddr.go deleted file mode 100644 index 7e82f9af..00000000 --- a/vendor/github.com/Microsoft/go-winio/internal/socket/rawaddr.go +++ /dev/null @@ -1,20 +0,0 @@ -package socket - -import ( - "unsafe" -) - -// RawSockaddr allows structs to be used with [Bind] and [ConnectEx]. The -// struct must meet the Win32 sockaddr requirements specified here: -// https://docs.microsoft.com/en-us/windows/win32/winsock/sockaddr-2 -// -// Specifically, the struct size must be least larger than an int16 (unsigned short) -// for the address family. -type RawSockaddr interface { - // Sockaddr returns a pointer to the RawSockaddr and its struct size, allowing - // for the RawSockaddr's data to be overwritten by syscalls (if necessary). - // - // It is the callers responsibility to validate that the values are valid; invalid - // pointers or size can cause a panic. - Sockaddr() (unsafe.Pointer, int32, error) -} diff --git a/vendor/github.com/Microsoft/go-winio/internal/socket/socket.go b/vendor/github.com/Microsoft/go-winio/internal/socket/socket.go deleted file mode 100644 index 88580d97..00000000 --- a/vendor/github.com/Microsoft/go-winio/internal/socket/socket.go +++ /dev/null @@ -1,177 +0,0 @@ -//go:build windows - -package socket - -import ( - "errors" - "fmt" - "net" - "sync" - "syscall" - "unsafe" - - "github.com/Microsoft/go-winio/pkg/guid" - "golang.org/x/sys/windows" -) - -//go:generate go run github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go socket.go - -//sys getsockname(s windows.Handle, name unsafe.Pointer, namelen *int32) (err error) [failretval==socketError] = ws2_32.getsockname -//sys getpeername(s windows.Handle, name unsafe.Pointer, namelen *int32) (err error) [failretval==socketError] = ws2_32.getpeername -//sys bind(s windows.Handle, name unsafe.Pointer, namelen int32) (err error) [failretval==socketError] = ws2_32.bind - -const socketError = uintptr(^uint32(0)) - -var ( - // todo(helsaawy): create custom error types to store the desired vs actual size and addr family? - - ErrBufferSize = errors.New("buffer size") - ErrAddrFamily = errors.New("address family") - ErrInvalidPointer = errors.New("invalid pointer") - ErrSocketClosed = fmt.Errorf("socket closed: %w", net.ErrClosed) -) - -// todo(helsaawy): replace these with generics, ie: GetSockName[S RawSockaddr](s windows.Handle) (S, error) - -// GetSockName writes the local address of socket s to the [RawSockaddr] rsa. -// If rsa is not large enough, the [windows.WSAEFAULT] is returned. -func GetSockName(s windows.Handle, rsa RawSockaddr) error { - ptr, l, err := rsa.Sockaddr() - if err != nil { - return fmt.Errorf("could not retrieve socket pointer and size: %w", err) - } - - // although getsockname returns WSAEFAULT if the buffer is too small, it does not set - // &l to the correct size, so--apart from doubling the buffer repeatedly--there is no remedy - return getsockname(s, ptr, &l) -} - -// GetPeerName returns the remote address the socket is connected to. -// -// See [GetSockName] for more information. -func GetPeerName(s windows.Handle, rsa RawSockaddr) error { - ptr, l, err := rsa.Sockaddr() - if err != nil { - return fmt.Errorf("could not retrieve socket pointer and size: %w", err) - } - - return getpeername(s, ptr, &l) -} - -func Bind(s windows.Handle, rsa RawSockaddr) (err error) { - ptr, l, err := rsa.Sockaddr() - if err != nil { - return fmt.Errorf("could not retrieve socket pointer and size: %w", err) - } - - return bind(s, ptr, l) -} - -// "golang.org/x/sys/windows".ConnectEx and .Bind only accept internal implementations of the -// their sockaddr interface, so they cannot be used with HvsockAddr -// Replicate functionality here from -// https://cs.opensource.google/go/x/sys/+/master:windows/syscall_windows.go - -// The function pointers to `AcceptEx`, `ConnectEx` and `GetAcceptExSockaddrs` must be loaded at -// runtime via a WSAIoctl call: -// https://docs.microsoft.com/en-us/windows/win32/api/Mswsock/nc-mswsock-lpfn_connectex#remarks - -type runtimeFunc struct { - id guid.GUID - once sync.Once - addr uintptr - err error -} - -func (f *runtimeFunc) Load() error { - f.once.Do(func() { - var s windows.Handle - s, f.err = windows.Socket(windows.AF_INET, windows.SOCK_STREAM, windows.IPPROTO_TCP) - if f.err != nil { - return - } - defer windows.CloseHandle(s) //nolint:errcheck - - var n uint32 - f.err = windows.WSAIoctl(s, - windows.SIO_GET_EXTENSION_FUNCTION_POINTER, - (*byte)(unsafe.Pointer(&f.id)), - uint32(unsafe.Sizeof(f.id)), - (*byte)(unsafe.Pointer(&f.addr)), - uint32(unsafe.Sizeof(f.addr)), - &n, - nil, // overlapped - 0, // completionRoutine - ) - }) - return f.err -} - -var ( - // todo: add `AcceptEx` and `GetAcceptExSockaddrs` - WSAID_CONNECTEX = guid.GUID{ //revive:disable-line:var-naming ALL_CAPS - Data1: 0x25a207b9, - Data2: 0xddf3, - Data3: 0x4660, - Data4: [8]byte{0x8e, 0xe9, 0x76, 0xe5, 0x8c, 0x74, 0x06, 0x3e}, - } - - connectExFunc = runtimeFunc{id: WSAID_CONNECTEX} -) - -func ConnectEx( - fd windows.Handle, - rsa RawSockaddr, - sendBuf *byte, - sendDataLen uint32, - bytesSent *uint32, - overlapped *windows.Overlapped, -) error { - if err := connectExFunc.Load(); err != nil { - return fmt.Errorf("failed to load ConnectEx function pointer: %w", err) - } - ptr, n, err := rsa.Sockaddr() - if err != nil { - return err - } - return connectEx(fd, ptr, n, sendBuf, sendDataLen, bytesSent, overlapped) -} - -// BOOL LpfnConnectex( -// [in] SOCKET s, -// [in] const sockaddr *name, -// [in] int namelen, -// [in, optional] PVOID lpSendBuffer, -// [in] DWORD dwSendDataLength, -// [out] LPDWORD lpdwBytesSent, -// [in] LPOVERLAPPED lpOverlapped -// ) - -func connectEx( - s windows.Handle, - name unsafe.Pointer, - namelen int32, - sendBuf *byte, - sendDataLen uint32, - bytesSent *uint32, - overlapped *windows.Overlapped, -) (err error) { - r1, _, e1 := syscall.SyscallN(connectExFunc.addr, - uintptr(s), - uintptr(name), - uintptr(namelen), - uintptr(unsafe.Pointer(sendBuf)), - uintptr(sendDataLen), - uintptr(unsafe.Pointer(bytesSent)), - uintptr(unsafe.Pointer(overlapped)), - ) - - if r1 == 0 { - if e1 != 0 { - err = error(e1) - } else { - err = syscall.EINVAL - } - } - return err -} diff --git a/vendor/github.com/Microsoft/go-winio/internal/socket/zsyscall_windows.go b/vendor/github.com/Microsoft/go-winio/internal/socket/zsyscall_windows.go deleted file mode 100644 index e1504126..00000000 --- a/vendor/github.com/Microsoft/go-winio/internal/socket/zsyscall_windows.go +++ /dev/null @@ -1,69 +0,0 @@ -//go:build windows - -// Code generated by 'go generate' using "github.com/Microsoft/go-winio/tools/mkwinsyscall"; DO NOT EDIT. - -package socket - -import ( - "syscall" - "unsafe" - - "golang.org/x/sys/windows" -) - -var _ unsafe.Pointer - -// Do the interface allocations only once for common -// Errno values. -const ( - errnoERROR_IO_PENDING = 997 -) - -var ( - errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING) - errERROR_EINVAL error = syscall.EINVAL -) - -// errnoErr returns common boxed Errno values, to prevent -// allocations at runtime. -func errnoErr(e syscall.Errno) error { - switch e { - case 0: - return errERROR_EINVAL - case errnoERROR_IO_PENDING: - return errERROR_IO_PENDING - } - return e -} - -var ( - modws2_32 = windows.NewLazySystemDLL("ws2_32.dll") - - procbind = modws2_32.NewProc("bind") - procgetpeername = modws2_32.NewProc("getpeername") - procgetsockname = modws2_32.NewProc("getsockname") -) - -func bind(s windows.Handle, name unsafe.Pointer, namelen int32) (err error) { - r1, _, e1 := syscall.SyscallN(procbind.Addr(), uintptr(s), uintptr(name), uintptr(namelen)) - if r1 == socketError { - err = errnoErr(e1) - } - return -} - -func getpeername(s windows.Handle, name unsafe.Pointer, namelen *int32) (err error) { - r1, _, e1 := syscall.SyscallN(procgetpeername.Addr(), uintptr(s), uintptr(name), uintptr(unsafe.Pointer(namelen))) - if r1 == socketError { - err = errnoErr(e1) - } - return -} - -func getsockname(s windows.Handle, name unsafe.Pointer, namelen *int32) (err error) { - r1, _, e1 := syscall.SyscallN(procgetsockname.Addr(), uintptr(s), uintptr(name), uintptr(unsafe.Pointer(namelen))) - if r1 == socketError { - err = errnoErr(e1) - } - return -} diff --git a/vendor/github.com/Microsoft/go-winio/internal/stringbuffer/wstring.go b/vendor/github.com/Microsoft/go-winio/internal/stringbuffer/wstring.go deleted file mode 100644 index 42ebc019..00000000 --- a/vendor/github.com/Microsoft/go-winio/internal/stringbuffer/wstring.go +++ /dev/null @@ -1,132 +0,0 @@ -package stringbuffer - -import ( - "sync" - "unicode/utf16" -) - -// TODO: worth exporting and using in mkwinsyscall? - -// Uint16BufferSize is the buffer size in the pool, chosen somewhat arbitrarily to accommodate -// large path strings: -// MAX_PATH (260) + size of volume GUID prefix (49) + null terminator = 310. -const MinWStringCap = 310 - -// use *[]uint16 since []uint16 creates an extra allocation where the slice header -// is copied to heap and then referenced via pointer in the interface header that sync.Pool -// stores. -var pathPool = sync.Pool{ // if go1.18+ adds Pool[T], use that to store []uint16 directly - New: func() interface{} { - b := make([]uint16, MinWStringCap) - return &b - }, -} - -func newBuffer() []uint16 { return *(pathPool.Get().(*[]uint16)) } - -// freeBuffer copies the slice header data, and puts a pointer to that in the pool. -// This avoids taking a pointer to the slice header in WString, which can be set to nil. -func freeBuffer(b []uint16) { pathPool.Put(&b) } - -// WString is a wide string buffer ([]uint16) meant for storing UTF-16 encoded strings -// for interacting with Win32 APIs. -// Sizes are specified as uint32 and not int. -// -// It is not thread safe. -type WString struct { - // type-def allows casting to []uint16 directly, use struct to prevent that and allow adding fields in the future. - - // raw buffer - b []uint16 -} - -// NewWString returns a [WString] allocated from a shared pool with an -// initial capacity of at least [MinWStringCap]. -// Since the buffer may have been previously used, its contents are not guaranteed to be empty. -// -// The buffer should be freed via [WString.Free] -func NewWString() *WString { - return &WString{ - b: newBuffer(), - } -} - -func (b *WString) Free() { - if b.empty() { - return - } - freeBuffer(b.b) - b.b = nil -} - -// ResizeTo grows the buffer to at least c and returns the new capacity, freeing the -// previous buffer back into pool. -func (b *WString) ResizeTo(c uint32) uint32 { - // already sufficient (or n is 0) - if c <= b.Cap() { - return b.Cap() - } - - if c <= MinWStringCap { - c = MinWStringCap - } - // allocate at-least double buffer size, as is done in [bytes.Buffer] and other places - if c <= 2*b.Cap() { - c = 2 * b.Cap() - } - - b2 := make([]uint16, c) - if !b.empty() { - copy(b2, b.b) - freeBuffer(b.b) - } - b.b = b2 - return c -} - -// Buffer returns the underlying []uint16 buffer. -func (b *WString) Buffer() []uint16 { - if b.empty() { - return nil - } - return b.b -} - -// Pointer returns a pointer to the first uint16 in the buffer. -// If the [WString.Free] has already been called, the pointer will be nil. -func (b *WString) Pointer() *uint16 { - if b.empty() { - return nil - } - return &b.b[0] -} - -// String returns the returns the UTF-8 encoding of the UTF-16 string in the buffer. -// -// It assumes that the data is null-terminated. -func (b *WString) String() string { - // Using [windows.UTF16ToString] would require importing "golang.org/x/sys/windows" - // and would make this code Windows-only, which makes no sense. - // So copy UTF16ToString code into here. - // If other windows-specific code is added, switch to [windows.UTF16ToString] - - s := b.b - for i, v := range s { - if v == 0 { - s = s[:i] - break - } - } - return string(utf16.Decode(s)) -} - -// Cap returns the underlying buffer capacity. -func (b *WString) Cap() uint32 { - if b.empty() { - return 0 - } - return b.cap() -} - -func (b *WString) cap() uint32 { return uint32(cap(b.b)) } -func (b *WString) empty() bool { return b == nil || b.cap() == 0 } diff --git a/vendor/github.com/Microsoft/go-winio/pipe.go b/vendor/github.com/Microsoft/go-winio/pipe.go deleted file mode 100644 index a2da6639..00000000 --- a/vendor/github.com/Microsoft/go-winio/pipe.go +++ /dev/null @@ -1,586 +0,0 @@ -//go:build windows -// +build windows - -package winio - -import ( - "context" - "errors" - "fmt" - "io" - "net" - "os" - "runtime" - "time" - "unsafe" - - "golang.org/x/sys/windows" - - "github.com/Microsoft/go-winio/internal/fs" -) - -//sys connectNamedPipe(pipe windows.Handle, o *windows.Overlapped) (err error) = ConnectNamedPipe -//sys createNamedPipe(name string, flags uint32, pipeMode uint32, maxInstances uint32, outSize uint32, inSize uint32, defaultTimeout uint32, sa *windows.SecurityAttributes) (handle windows.Handle, err error) [failretval==windows.InvalidHandle] = CreateNamedPipeW -//sys disconnectNamedPipe(pipe windows.Handle) (err error) = DisconnectNamedPipe -//sys getNamedPipeInfo(pipe windows.Handle, flags *uint32, outSize *uint32, inSize *uint32, maxInstances *uint32) (err error) = GetNamedPipeInfo -//sys getNamedPipeHandleState(pipe windows.Handle, state *uint32, curInstances *uint32, maxCollectionCount *uint32, collectDataTimeout *uint32, userName *uint16, maxUserNameSize uint32) (err error) = GetNamedPipeHandleStateW -//sys ntCreateNamedPipeFile(pipe *windows.Handle, access ntAccessMask, oa *objectAttributes, iosb *ioStatusBlock, share ntFileShareMode, disposition ntFileCreationDisposition, options ntFileOptions, typ uint32, readMode uint32, completionMode uint32, maxInstances uint32, inboundQuota uint32, outputQuota uint32, timeout *int64) (status ntStatus) = ntdll.NtCreateNamedPipeFile -//sys rtlNtStatusToDosError(status ntStatus) (winerr error) = ntdll.RtlNtStatusToDosErrorNoTeb -//sys rtlDosPathNameToNtPathName(name *uint16, ntName *unicodeString, filePart uintptr, reserved uintptr) (status ntStatus) = ntdll.RtlDosPathNameToNtPathName_U -//sys rtlDefaultNpAcl(dacl *uintptr) (status ntStatus) = ntdll.RtlDefaultNpAcl - -type PipeConn interface { - net.Conn - Disconnect() error - Flush() error -} - -// type aliases for mkwinsyscall code -type ( - ntAccessMask = fs.AccessMask - ntFileShareMode = fs.FileShareMode - ntFileCreationDisposition = fs.NTFileCreationDisposition - ntFileOptions = fs.NTCreateOptions -) - -type ioStatusBlock struct { - Status, Information uintptr -} - -// typedef struct _OBJECT_ATTRIBUTES { -// ULONG Length; -// HANDLE RootDirectory; -// PUNICODE_STRING ObjectName; -// ULONG Attributes; -// PVOID SecurityDescriptor; -// PVOID SecurityQualityOfService; -// } OBJECT_ATTRIBUTES; -// -// https://learn.microsoft.com/en-us/windows/win32/api/ntdef/ns-ntdef-_object_attributes -type objectAttributes struct { - Length uintptr - RootDirectory uintptr - ObjectName *unicodeString - Attributes uintptr - SecurityDescriptor *securityDescriptor - SecurityQoS uintptr -} - -type unicodeString struct { - Length uint16 - MaximumLength uint16 - Buffer uintptr -} - -// typedef struct _SECURITY_DESCRIPTOR { -// BYTE Revision; -// BYTE Sbz1; -// SECURITY_DESCRIPTOR_CONTROL Control; -// PSID Owner; -// PSID Group; -// PACL Sacl; -// PACL Dacl; -// } SECURITY_DESCRIPTOR, *PISECURITY_DESCRIPTOR; -// -// https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-security_descriptor -type securityDescriptor struct { - Revision byte - Sbz1 byte - Control uint16 - Owner uintptr - Group uintptr - Sacl uintptr //revive:disable-line:var-naming SACL, not Sacl - Dacl uintptr //revive:disable-line:var-naming DACL, not Dacl -} - -type ntStatus int32 - -func (status ntStatus) Err() error { - if status >= 0 { - return nil - } - return rtlNtStatusToDosError(status) -} - -var ( - // ErrPipeListenerClosed is returned for pipe operations on listeners that have been closed. - ErrPipeListenerClosed = net.ErrClosed - - errPipeWriteClosed = errors.New("pipe has been closed for write") -) - -type win32Pipe struct { - *win32File - path string -} - -var _ PipeConn = (*win32Pipe)(nil) - -type win32MessageBytePipe struct { - win32Pipe - writeClosed bool - readEOF bool -} - -type pipeAddress string - -func (f *win32Pipe) LocalAddr() net.Addr { - return pipeAddress(f.path) -} - -func (f *win32Pipe) RemoteAddr() net.Addr { - return pipeAddress(f.path) -} - -func (f *win32Pipe) SetDeadline(t time.Time) error { - if err := f.SetReadDeadline(t); err != nil { - return err - } - return f.SetWriteDeadline(t) -} - -func (f *win32Pipe) Disconnect() error { - return disconnectNamedPipe(f.win32File.handle) -} - -// CloseWrite closes the write side of a message pipe in byte mode. -func (f *win32MessageBytePipe) CloseWrite() error { - if f.writeClosed { - return errPipeWriteClosed - } - err := f.win32File.Flush() - if err != nil { - return err - } - _, err = f.win32File.Write(nil) - if err != nil { - return err - } - f.writeClosed = true - return nil -} - -// Write writes bytes to a message pipe in byte mode. Zero-byte writes are ignored, since -// they are used to implement CloseWrite(). -func (f *win32MessageBytePipe) Write(b []byte) (int, error) { - if f.writeClosed { - return 0, errPipeWriteClosed - } - if len(b) == 0 { - return 0, nil - } - return f.win32File.Write(b) -} - -// Read reads bytes from a message pipe in byte mode. A read of a zero-byte message on a message -// mode pipe will return io.EOF, as will all subsequent reads. -func (f *win32MessageBytePipe) Read(b []byte) (int, error) { - if f.readEOF { - return 0, io.EOF - } - n, err := f.win32File.Read(b) - if err == io.EOF { //nolint:errorlint - // If this was the result of a zero-byte read, then - // it is possible that the read was due to a zero-size - // message. Since we are simulating CloseWrite with a - // zero-byte message, ensure that all future Read() calls - // also return EOF. - f.readEOF = true - } else if err == windows.ERROR_MORE_DATA { //nolint:errorlint // err is Errno - // ERROR_MORE_DATA indicates that the pipe's read mode is message mode - // and the message still has more bytes. Treat this as a success, since - // this package presents all named pipes as byte streams. - err = nil - } - return n, err -} - -func (pipeAddress) Network() string { - return "pipe" -} - -func (s pipeAddress) String() string { - return string(s) -} - -// tryDialPipe attempts to dial the pipe at `path` until `ctx` cancellation or timeout. -func tryDialPipe(ctx context.Context, path *string, access fs.AccessMask, impLevel PipeImpLevel) (windows.Handle, error) { - for { - select { - case <-ctx.Done(): - return windows.Handle(0), ctx.Err() - default: - h, err := fs.CreateFile(*path, - access, - 0, // mode - nil, // security attributes - fs.OPEN_EXISTING, - fs.FILE_FLAG_OVERLAPPED|fs.SECURITY_SQOS_PRESENT|fs.FileSQSFlag(impLevel), - 0, // template file handle - ) - if err == nil { - return h, nil - } - if err != windows.ERROR_PIPE_BUSY { //nolint:errorlint // err is Errno - return h, &os.PathError{Err: err, Op: "open", Path: *path} - } - // Wait 10 msec and try again. This is a rather simplistic - // view, as we always try each 10 milliseconds. - time.Sleep(10 * time.Millisecond) - } - } -} - -// DialPipe connects to a named pipe by path, timing out if the connection -// takes longer than the specified duration. If timeout is nil, then we use -// a default timeout of 2 seconds. (We do not use WaitNamedPipe.) -func DialPipe(path string, timeout *time.Duration) (net.Conn, error) { - var absTimeout time.Time - if timeout != nil { - absTimeout = time.Now().Add(*timeout) - } else { - absTimeout = time.Now().Add(2 * time.Second) - } - ctx, cancel := context.WithDeadline(context.Background(), absTimeout) - defer cancel() - conn, err := DialPipeContext(ctx, path) - if errors.Is(err, context.DeadlineExceeded) { - return nil, ErrTimeout - } - return conn, err -} - -// DialPipeContext attempts to connect to a named pipe by `path` until `ctx` -// cancellation or timeout. -func DialPipeContext(ctx context.Context, path string) (net.Conn, error) { - return DialPipeAccess(ctx, path, uint32(fs.GENERIC_READ|fs.GENERIC_WRITE)) -} - -// PipeImpLevel is an enumeration of impersonation levels that may be set -// when calling DialPipeAccessImpersonation. -type PipeImpLevel uint32 - -const ( - PipeImpLevelAnonymous = PipeImpLevel(fs.SECURITY_ANONYMOUS) - PipeImpLevelIdentification = PipeImpLevel(fs.SECURITY_IDENTIFICATION) - PipeImpLevelImpersonation = PipeImpLevel(fs.SECURITY_IMPERSONATION) - PipeImpLevelDelegation = PipeImpLevel(fs.SECURITY_DELEGATION) -) - -// DialPipeAccess attempts to connect to a named pipe by `path` with `access` until `ctx` -// cancellation or timeout. -func DialPipeAccess(ctx context.Context, path string, access uint32) (net.Conn, error) { - return DialPipeAccessImpLevel(ctx, path, access, PipeImpLevelAnonymous) -} - -// DialPipeAccessImpLevel attempts to connect to a named pipe by `path` with -// `access` at `impLevel` until `ctx` cancellation or timeout. The other -// DialPipe* implementations use PipeImpLevelAnonymous. -func DialPipeAccessImpLevel(ctx context.Context, path string, access uint32, impLevel PipeImpLevel) (net.Conn, error) { - var err error - var h windows.Handle - h, err = tryDialPipe(ctx, &path, fs.AccessMask(access), impLevel) - if err != nil { - return nil, err - } - - var flags uint32 - err = getNamedPipeInfo(h, &flags, nil, nil, nil) - if err != nil { - return nil, err - } - - f, err := makeWin32File(h) - if err != nil { - windows.Close(h) - return nil, err - } - - // If the pipe is in message mode, return a message byte pipe, which - // supports CloseWrite(). - if flags&windows.PIPE_TYPE_MESSAGE != 0 { - return &win32MessageBytePipe{ - win32Pipe: win32Pipe{win32File: f, path: path}, - }, nil - } - return &win32Pipe{win32File: f, path: path}, nil -} - -type acceptResponse struct { - f *win32File - err error -} - -type win32PipeListener struct { - firstHandle windows.Handle - path string - config PipeConfig - acceptCh chan (chan acceptResponse) - closeCh chan int - doneCh chan int -} - -func makeServerPipeHandle(path string, sd []byte, c *PipeConfig, first bool) (windows.Handle, error) { - path16, err := windows.UTF16FromString(path) - if err != nil { - return 0, &os.PathError{Op: "open", Path: path, Err: err} - } - - var oa objectAttributes - oa.Length = unsafe.Sizeof(oa) - - var ntPath unicodeString - if err := rtlDosPathNameToNtPathName(&path16[0], - &ntPath, - 0, - 0, - ).Err(); err != nil { - return 0, &os.PathError{Op: "open", Path: path, Err: err} - } - defer windows.LocalFree(windows.Handle(ntPath.Buffer)) //nolint:errcheck - oa.ObjectName = &ntPath - oa.Attributes = windows.OBJ_CASE_INSENSITIVE - - // The security descriptor is only needed for the first pipe. - if first { - if sd != nil { - //todo: does `sdb` need to be allocated on the heap, or can go allocate it? - l := uint32(len(sd)) - sdb, err := windows.LocalAlloc(0, l) - if err != nil { - return 0, fmt.Errorf("LocalAlloc for security descriptor with of length %d: %w", l, err) - } - defer windows.LocalFree(windows.Handle(sdb)) //nolint:errcheck - copy((*[0xffff]byte)(unsafe.Pointer(sdb))[:], sd) - oa.SecurityDescriptor = (*securityDescriptor)(unsafe.Pointer(sdb)) - } else { - // Construct the default named pipe security descriptor. - var dacl uintptr - if err := rtlDefaultNpAcl(&dacl).Err(); err != nil { - return 0, fmt.Errorf("getting default named pipe ACL: %w", err) - } - defer windows.LocalFree(windows.Handle(dacl)) //nolint:errcheck - - sdb := &securityDescriptor{ - Revision: 1, - Control: windows.SE_DACL_PRESENT, - Dacl: dacl, - } - oa.SecurityDescriptor = sdb - } - } - - typ := uint32(windows.FILE_PIPE_REJECT_REMOTE_CLIENTS) - if c.MessageMode { - typ |= windows.FILE_PIPE_MESSAGE_TYPE - } - - disposition := fs.FILE_OPEN - access := fs.GENERIC_READ | fs.GENERIC_WRITE | fs.SYNCHRONIZE - if first { - disposition = fs.FILE_CREATE - // By not asking for read or write access, the named pipe file system - // will put this pipe into an initially disconnected state, blocking - // client connections until the next call with first == false. - access = fs.SYNCHRONIZE - } - - timeout := int64(-50 * 10000) // 50ms - - var ( - h windows.Handle - iosb ioStatusBlock - ) - err = ntCreateNamedPipeFile(&h, - access, - &oa, - &iosb, - fs.FILE_SHARE_READ|fs.FILE_SHARE_WRITE, - disposition, - 0, - typ, - 0, - 0, - 0xffffffff, - uint32(c.InputBufferSize), - uint32(c.OutputBufferSize), - &timeout).Err() - if err != nil { - return 0, &os.PathError{Op: "open", Path: path, Err: err} - } - - runtime.KeepAlive(ntPath) - return h, nil -} - -func (l *win32PipeListener) makeServerPipe() (*win32File, error) { - h, err := makeServerPipeHandle(l.path, nil, &l.config, false) - if err != nil { - return nil, err - } - f, err := makeWin32File(h) - if err != nil { - windows.Close(h) - return nil, err - } - return f, nil -} - -func (l *win32PipeListener) makeConnectedServerPipe() (*win32File, error) { - p, err := l.makeServerPipe() - if err != nil { - return nil, err - } - - // Wait for the client to connect. - ch := make(chan error) - go func(p *win32File) { - ch <- connectPipe(p) - }(p) - - select { - case err = <-ch: - if err != nil { - p.Close() - p = nil - } - case <-l.closeCh: - // Abort the connect request by closing the handle. - p.Close() - p = nil - err = <-ch - if err == nil || err == ErrFileClosed { //nolint:errorlint // err is Errno - err = ErrPipeListenerClosed - } - } - return p, err -} - -func (l *win32PipeListener) listenerRoutine() { - closed := false - for !closed { - select { - case <-l.closeCh: - closed = true - case responseCh := <-l.acceptCh: - var ( - p *win32File - err error - ) - for { - p, err = l.makeConnectedServerPipe() - // If the connection was immediately closed by the client, try - // again. - if err != windows.ERROR_NO_DATA { //nolint:errorlint // err is Errno - break - } - } - responseCh <- acceptResponse{p, err} - closed = err == ErrPipeListenerClosed //nolint:errorlint // err is Errno - } - } - windows.Close(l.firstHandle) - l.firstHandle = 0 - // Notify Close() and Accept() callers that the handle has been closed. - close(l.doneCh) -} - -// PipeConfig contain configuration for the pipe listener. -type PipeConfig struct { - // SecurityDescriptor contains a Windows security descriptor in SDDL format. - SecurityDescriptor string - - // MessageMode determines whether the pipe is in byte or message mode. In either - // case the pipe is read in byte mode by default. The only practical difference in - // this implementation is that CloseWrite() is only supported for message mode pipes; - // CloseWrite() is implemented as a zero-byte write, but zero-byte writes are only - // transferred to the reader (and returned as io.EOF in this implementation) - // when the pipe is in message mode. - MessageMode bool - - // InputBufferSize specifies the size of the input buffer, in bytes. - InputBufferSize int32 - - // OutputBufferSize specifies the size of the output buffer, in bytes. - OutputBufferSize int32 -} - -// ListenPipe creates a listener on a Windows named pipe path, e.g. \\.\pipe\mypipe. -// The pipe must not already exist. -func ListenPipe(path string, c *PipeConfig) (net.Listener, error) { - var ( - sd []byte - err error - ) - if c == nil { - c = &PipeConfig{} - } - if c.SecurityDescriptor != "" { - sd, err = SddlToSecurityDescriptor(c.SecurityDescriptor) - if err != nil { - return nil, err - } - } - h, err := makeServerPipeHandle(path, sd, c, true) - if err != nil { - return nil, err - } - l := &win32PipeListener{ - firstHandle: h, - path: path, - config: *c, - acceptCh: make(chan (chan acceptResponse)), - closeCh: make(chan int), - doneCh: make(chan int), - } - go l.listenerRoutine() - return l, nil -} - -func connectPipe(p *win32File) error { - c, err := p.prepareIO() - if err != nil { - return err - } - defer p.wg.Done() - - err = connectNamedPipe(p.handle, &c.o) - _, err = p.asyncIO(c, nil, 0, err) - if err != nil && err != windows.ERROR_PIPE_CONNECTED { //nolint:errorlint // err is Errno - return err - } - return nil -} - -func (l *win32PipeListener) Accept() (net.Conn, error) { - ch := make(chan acceptResponse) - select { - case l.acceptCh <- ch: - response := <-ch - err := response.err - if err != nil { - return nil, err - } - if l.config.MessageMode { - return &win32MessageBytePipe{ - win32Pipe: win32Pipe{win32File: response.f, path: l.path}, - }, nil - } - return &win32Pipe{win32File: response.f, path: l.path}, nil - case <-l.doneCh: - return nil, ErrPipeListenerClosed - } -} - -func (l *win32PipeListener) Close() error { - select { - case l.closeCh <- 1: - <-l.doneCh - case <-l.doneCh: - } - return nil -} - -func (l *win32PipeListener) Addr() net.Addr { - return pipeAddress(l.path) -} diff --git a/vendor/github.com/Microsoft/go-winio/pkg/guid/guid.go b/vendor/github.com/Microsoft/go-winio/pkg/guid/guid.go deleted file mode 100644 index 48ce4e92..00000000 --- a/vendor/github.com/Microsoft/go-winio/pkg/guid/guid.go +++ /dev/null @@ -1,232 +0,0 @@ -// Package guid provides a GUID type. The backing structure for a GUID is -// identical to that used by the golang.org/x/sys/windows GUID type. -// There are two main binary encodings used for a GUID, the big-endian encoding, -// and the Windows (mixed-endian) encoding. See here for details: -// https://en.wikipedia.org/wiki/Universally_unique_identifier#Encoding -package guid - -import ( - "crypto/rand" - "crypto/sha1" //nolint:gosec // not used for secure application - "encoding" - "encoding/binary" - "fmt" - "strconv" -) - -//go:generate go run golang.org/x/tools/cmd/stringer -type=Variant -trimprefix=Variant -linecomment - -// Variant specifies which GUID variant (or "type") of the GUID. It determines -// how the entirety of the rest of the GUID is interpreted. -type Variant uint8 - -// The variants specified by RFC 4122 section 4.1.1. -const ( - // VariantUnknown specifies a GUID variant which does not conform to one of - // the variant encodings specified in RFC 4122. - VariantUnknown Variant = iota - VariantNCS - VariantRFC4122 // RFC 4122 - VariantMicrosoft - VariantFuture -) - -// Version specifies how the bits in the GUID were generated. For instance, a -// version 4 GUID is randomly generated, and a version 5 is generated from the -// hash of an input string. -type Version uint8 - -func (v Version) String() string { - return strconv.FormatUint(uint64(v), 10) -} - -var _ = (encoding.TextMarshaler)(GUID{}) -var _ = (encoding.TextUnmarshaler)(&GUID{}) - -// NewV4 returns a new version 4 (pseudorandom) GUID, as defined by RFC 4122. -func NewV4() (GUID, error) { - var b [16]byte - if _, err := rand.Read(b[:]); err != nil { - return GUID{}, err - } - - g := FromArray(b) - g.setVersion(4) // Version 4 means randomly generated. - g.setVariant(VariantRFC4122) - - return g, nil -} - -// NewV5 returns a new version 5 (generated from a string via SHA-1 hashing) -// GUID, as defined by RFC 4122. The RFC is unclear on the encoding of the name, -// and the sample code treats it as a series of bytes, so we do the same here. -// -// Some implementations, such as those found on Windows, treat the name as a -// big-endian UTF16 stream of bytes. If that is desired, the string can be -// encoded as such before being passed to this function. -func NewV5(namespace GUID, name []byte) (GUID, error) { - b := sha1.New() //nolint:gosec // not used for secure application - namespaceBytes := namespace.ToArray() - b.Write(namespaceBytes[:]) - b.Write(name) - - a := [16]byte{} - copy(a[:], b.Sum(nil)) - - g := FromArray(a) - g.setVersion(5) // Version 5 means generated from a string. - g.setVariant(VariantRFC4122) - - return g, nil -} - -func fromArray(b [16]byte, order binary.ByteOrder) GUID { - var g GUID - g.Data1 = order.Uint32(b[0:4]) - g.Data2 = order.Uint16(b[4:6]) - g.Data3 = order.Uint16(b[6:8]) - copy(g.Data4[:], b[8:16]) - return g -} - -func (g GUID) toArray(order binary.ByteOrder) [16]byte { - b := [16]byte{} - order.PutUint32(b[0:4], g.Data1) - order.PutUint16(b[4:6], g.Data2) - order.PutUint16(b[6:8], g.Data3) - copy(b[8:16], g.Data4[:]) - return b -} - -// FromArray constructs a GUID from a big-endian encoding array of 16 bytes. -func FromArray(b [16]byte) GUID { - return fromArray(b, binary.BigEndian) -} - -// ToArray returns an array of 16 bytes representing the GUID in big-endian -// encoding. -func (g GUID) ToArray() [16]byte { - return g.toArray(binary.BigEndian) -} - -// FromWindowsArray constructs a GUID from a Windows encoding array of bytes. -func FromWindowsArray(b [16]byte) GUID { - return fromArray(b, binary.LittleEndian) -} - -// ToWindowsArray returns an array of 16 bytes representing the GUID in Windows -// encoding. -func (g GUID) ToWindowsArray() [16]byte { - return g.toArray(binary.LittleEndian) -} - -func (g GUID) String() string { - return fmt.Sprintf( - "%08x-%04x-%04x-%04x-%012x", - g.Data1, - g.Data2, - g.Data3, - g.Data4[:2], - g.Data4[2:]) -} - -// FromString parses a string containing a GUID and returns the GUID. The only -// format currently supported is the `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` -// format. -func FromString(s string) (GUID, error) { - if len(s) != 36 { - return GUID{}, fmt.Errorf("invalid GUID %q", s) - } - if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' { - return GUID{}, fmt.Errorf("invalid GUID %q", s) - } - - var g GUID - - data1, err := strconv.ParseUint(s[0:8], 16, 32) - if err != nil { - return GUID{}, fmt.Errorf("invalid GUID %q", s) - } - g.Data1 = uint32(data1) - - data2, err := strconv.ParseUint(s[9:13], 16, 16) - if err != nil { - return GUID{}, fmt.Errorf("invalid GUID %q", s) - } - g.Data2 = uint16(data2) - - data3, err := strconv.ParseUint(s[14:18], 16, 16) - if err != nil { - return GUID{}, fmt.Errorf("invalid GUID %q", s) - } - g.Data3 = uint16(data3) - - for i, x := range []int{19, 21, 24, 26, 28, 30, 32, 34} { - v, err := strconv.ParseUint(s[x:x+2], 16, 8) - if err != nil { - return GUID{}, fmt.Errorf("invalid GUID %q", s) - } - g.Data4[i] = uint8(v) - } - - return g, nil -} - -func (g *GUID) setVariant(v Variant) { - d := g.Data4[0] - switch v { - case VariantNCS: - d = (d & 0x7f) - case VariantRFC4122: - d = (d & 0x3f) | 0x80 - case VariantMicrosoft: - d = (d & 0x1f) | 0xc0 - case VariantFuture: - d = (d & 0x0f) | 0xe0 - case VariantUnknown: - fallthrough - default: - panic(fmt.Sprintf("invalid variant: %d", v)) - } - g.Data4[0] = d -} - -// Variant returns the GUID variant, as defined in RFC 4122. -func (g GUID) Variant() Variant { - b := g.Data4[0] - if b&0x80 == 0 { - return VariantNCS - } else if b&0xc0 == 0x80 { - return VariantRFC4122 - } else if b&0xe0 == 0xc0 { - return VariantMicrosoft - } else if b&0xe0 == 0xe0 { - return VariantFuture - } - return VariantUnknown -} - -func (g *GUID) setVersion(v Version) { - g.Data3 = (g.Data3 & 0x0fff) | (uint16(v) << 12) -} - -// Version returns the GUID version, as defined in RFC 4122. -func (g GUID) Version() Version { - return Version((g.Data3 & 0xF000) >> 12) -} - -// MarshalText returns the textual representation of the GUID. -func (g GUID) MarshalText() ([]byte, error) { - return []byte(g.String()), nil -} - -// UnmarshalText takes the textual representation of a GUID, and unmarhals it -// into this GUID. -func (g *GUID) UnmarshalText(text []byte) error { - g2, err := FromString(string(text)) - if err != nil { - return err - } - *g = g2 - return nil -} diff --git a/vendor/github.com/Microsoft/go-winio/pkg/guid/guid_nonwindows.go b/vendor/github.com/Microsoft/go-winio/pkg/guid/guid_nonwindows.go deleted file mode 100644 index 805bd354..00000000 --- a/vendor/github.com/Microsoft/go-winio/pkg/guid/guid_nonwindows.go +++ /dev/null @@ -1,16 +0,0 @@ -//go:build !windows -// +build !windows - -package guid - -// GUID represents a GUID/UUID. It has the same structure as -// golang.org/x/sys/windows.GUID so that it can be used with functions expecting -// that type. It is defined as its own type as that is only available to builds -// targeted at `windows`. The representation matches that used by native Windows -// code. -type GUID struct { - Data1 uint32 - Data2 uint16 - Data3 uint16 - Data4 [8]byte -} diff --git a/vendor/github.com/Microsoft/go-winio/pkg/guid/guid_windows.go b/vendor/github.com/Microsoft/go-winio/pkg/guid/guid_windows.go deleted file mode 100644 index 27e45ee5..00000000 --- a/vendor/github.com/Microsoft/go-winio/pkg/guid/guid_windows.go +++ /dev/null @@ -1,13 +0,0 @@ -//go:build windows -// +build windows - -package guid - -import "golang.org/x/sys/windows" - -// GUID represents a GUID/UUID. It has the same structure as -// golang.org/x/sys/windows.GUID so that it can be used with functions expecting -// that type. It is defined as its own type so that stringification and -// marshaling can be supported. The representation matches that used by native -// Windows code. -type GUID windows.GUID diff --git a/vendor/github.com/Microsoft/go-winio/pkg/guid/variant_string.go b/vendor/github.com/Microsoft/go-winio/pkg/guid/variant_string.go deleted file mode 100644 index 4076d313..00000000 --- a/vendor/github.com/Microsoft/go-winio/pkg/guid/variant_string.go +++ /dev/null @@ -1,27 +0,0 @@ -// Code generated by "stringer -type=Variant -trimprefix=Variant -linecomment"; DO NOT EDIT. - -package guid - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[VariantUnknown-0] - _ = x[VariantNCS-1] - _ = x[VariantRFC4122-2] - _ = x[VariantMicrosoft-3] - _ = x[VariantFuture-4] -} - -const _Variant_name = "UnknownNCSRFC 4122MicrosoftFuture" - -var _Variant_index = [...]uint8{0, 7, 10, 18, 27, 33} - -func (i Variant) String() string { - if i >= Variant(len(_Variant_index)-1) { - return "Variant(" + strconv.FormatInt(int64(i), 10) + ")" - } - return _Variant_name[_Variant_index[i]:_Variant_index[i+1]] -} diff --git a/vendor/github.com/Microsoft/go-winio/privilege.go b/vendor/github.com/Microsoft/go-winio/privilege.go deleted file mode 100644 index d9b90b6e..00000000 --- a/vendor/github.com/Microsoft/go-winio/privilege.go +++ /dev/null @@ -1,196 +0,0 @@ -//go:build windows -// +build windows - -package winio - -import ( - "bytes" - "encoding/binary" - "fmt" - "runtime" - "sync" - "unicode/utf16" - - "golang.org/x/sys/windows" -) - -//sys adjustTokenPrivileges(token windows.Token, releaseAll bool, input *byte, outputSize uint32, output *byte, requiredSize *uint32) (success bool, err error) [true] = advapi32.AdjustTokenPrivileges -//sys impersonateSelf(level uint32) (err error) = advapi32.ImpersonateSelf -//sys revertToSelf() (err error) = advapi32.RevertToSelf -//sys openThreadToken(thread windows.Handle, accessMask uint32, openAsSelf bool, token *windows.Token) (err error) = advapi32.OpenThreadToken -//sys getCurrentThread() (h windows.Handle) = GetCurrentThread -//sys lookupPrivilegeValue(systemName string, name string, luid *uint64) (err error) = advapi32.LookupPrivilegeValueW -//sys lookupPrivilegeName(systemName string, luid *uint64, buffer *uint16, size *uint32) (err error) = advapi32.LookupPrivilegeNameW -//sys lookupPrivilegeDisplayName(systemName string, name *uint16, buffer *uint16, size *uint32, languageId *uint32) (err error) = advapi32.LookupPrivilegeDisplayNameW - -const ( - //revive:disable-next-line:var-naming ALL_CAPS - SE_PRIVILEGE_ENABLED = windows.SE_PRIVILEGE_ENABLED - - //revive:disable-next-line:var-naming ALL_CAPS - ERROR_NOT_ALL_ASSIGNED windows.Errno = windows.ERROR_NOT_ALL_ASSIGNED - - SeBackupPrivilege = "SeBackupPrivilege" - SeRestorePrivilege = "SeRestorePrivilege" - SeSecurityPrivilege = "SeSecurityPrivilege" -) - -var ( - privNames = make(map[string]uint64) - privNameMutex sync.Mutex -) - -// PrivilegeError represents an error enabling privileges. -type PrivilegeError struct { - privileges []uint64 -} - -func (e *PrivilegeError) Error() string { - s := "Could not enable privilege " - if len(e.privileges) > 1 { - s = "Could not enable privileges " - } - for i, p := range e.privileges { - if i != 0 { - s += ", " - } - s += `"` - s += getPrivilegeName(p) - s += `"` - } - return s -} - -// RunWithPrivilege enables a single privilege for a function call. -func RunWithPrivilege(name string, fn func() error) error { - return RunWithPrivileges([]string{name}, fn) -} - -// RunWithPrivileges enables privileges for a function call. -func RunWithPrivileges(names []string, fn func() error) error { - privileges, err := mapPrivileges(names) - if err != nil { - return err - } - runtime.LockOSThread() - defer runtime.UnlockOSThread() - token, err := newThreadToken() - if err != nil { - return err - } - defer releaseThreadToken(token) - err = adjustPrivileges(token, privileges, SE_PRIVILEGE_ENABLED) - if err != nil { - return err - } - return fn() -} - -func mapPrivileges(names []string) ([]uint64, error) { - privileges := make([]uint64, 0, len(names)) - privNameMutex.Lock() - defer privNameMutex.Unlock() - for _, name := range names { - p, ok := privNames[name] - if !ok { - err := lookupPrivilegeValue("", name, &p) - if err != nil { - return nil, err - } - privNames[name] = p - } - privileges = append(privileges, p) - } - return privileges, nil -} - -// EnableProcessPrivileges enables privileges globally for the process. -func EnableProcessPrivileges(names []string) error { - return enableDisableProcessPrivilege(names, SE_PRIVILEGE_ENABLED) -} - -// DisableProcessPrivileges disables privileges globally for the process. -func DisableProcessPrivileges(names []string) error { - return enableDisableProcessPrivilege(names, 0) -} - -func enableDisableProcessPrivilege(names []string, action uint32) error { - privileges, err := mapPrivileges(names) - if err != nil { - return err - } - - p := windows.CurrentProcess() - var token windows.Token - err = windows.OpenProcessToken(p, windows.TOKEN_ADJUST_PRIVILEGES|windows.TOKEN_QUERY, &token) - if err != nil { - return err - } - - defer token.Close() - return adjustPrivileges(token, privileges, action) -} - -func adjustPrivileges(token windows.Token, privileges []uint64, action uint32) error { - var b bytes.Buffer - _ = binary.Write(&b, binary.LittleEndian, uint32(len(privileges))) - for _, p := range privileges { - _ = binary.Write(&b, binary.LittleEndian, p) - _ = binary.Write(&b, binary.LittleEndian, action) - } - prevState := make([]byte, b.Len()) - reqSize := uint32(0) - success, err := adjustTokenPrivileges(token, false, &b.Bytes()[0], uint32(len(prevState)), &prevState[0], &reqSize) - if !success { - return err - } - if err == ERROR_NOT_ALL_ASSIGNED { //nolint:errorlint // err is Errno - return &PrivilegeError{privileges} - } - return nil -} - -func getPrivilegeName(luid uint64) string { - var nameBuffer [256]uint16 - bufSize := uint32(len(nameBuffer)) - err := lookupPrivilegeName("", &luid, &nameBuffer[0], &bufSize) - if err != nil { - return fmt.Sprintf("", luid) - } - - var displayNameBuffer [256]uint16 - displayBufSize := uint32(len(displayNameBuffer)) - var langID uint32 - err = lookupPrivilegeDisplayName("", &nameBuffer[0], &displayNameBuffer[0], &displayBufSize, &langID) - if err != nil { - return fmt.Sprintf("", string(utf16.Decode(nameBuffer[:bufSize]))) - } - - return string(utf16.Decode(displayNameBuffer[:displayBufSize])) -} - -func newThreadToken() (windows.Token, error) { - err := impersonateSelf(windows.SecurityImpersonation) - if err != nil { - return 0, err - } - - var token windows.Token - err = openThreadToken(getCurrentThread(), windows.TOKEN_ADJUST_PRIVILEGES|windows.TOKEN_QUERY, false, &token) - if err != nil { - rerr := revertToSelf() - if rerr != nil { - panic(rerr) - } - return 0, err - } - return token, nil -} - -func releaseThreadToken(h windows.Token) { - err := revertToSelf() - if err != nil { - panic(err) - } - h.Close() -} diff --git a/vendor/github.com/Microsoft/go-winio/reparse.go b/vendor/github.com/Microsoft/go-winio/reparse.go deleted file mode 100644 index 67d1a104..00000000 --- a/vendor/github.com/Microsoft/go-winio/reparse.go +++ /dev/null @@ -1,131 +0,0 @@ -//go:build windows -// +build windows - -package winio - -import ( - "bytes" - "encoding/binary" - "fmt" - "strings" - "unicode/utf16" - "unsafe" -) - -const ( - reparseTagMountPoint = 0xA0000003 - reparseTagSymlink = 0xA000000C -) - -type reparseDataBuffer struct { - ReparseTag uint32 - ReparseDataLength uint16 - Reserved uint16 - SubstituteNameOffset uint16 - SubstituteNameLength uint16 - PrintNameOffset uint16 - PrintNameLength uint16 -} - -// ReparsePoint describes a Win32 symlink or mount point. -type ReparsePoint struct { - Target string - IsMountPoint bool -} - -// UnsupportedReparsePointError is returned when trying to decode a non-symlink or -// mount point reparse point. -type UnsupportedReparsePointError struct { - Tag uint32 -} - -func (e *UnsupportedReparsePointError) Error() string { - return fmt.Sprintf("unsupported reparse point %x", e.Tag) -} - -// DecodeReparsePoint decodes a Win32 REPARSE_DATA_BUFFER structure containing either a symlink -// or a mount point. -func DecodeReparsePoint(b []byte) (*ReparsePoint, error) { - tag := binary.LittleEndian.Uint32(b[0:4]) - return DecodeReparsePointData(tag, b[8:]) -} - -func DecodeReparsePointData(tag uint32, b []byte) (*ReparsePoint, error) { - isMountPoint := false - switch tag { - case reparseTagMountPoint: - isMountPoint = true - case reparseTagSymlink: - default: - return nil, &UnsupportedReparsePointError{tag} - } - nameOffset := 8 + binary.LittleEndian.Uint16(b[4:6]) - if !isMountPoint { - nameOffset += 4 - } - nameLength := binary.LittleEndian.Uint16(b[6:8]) - name := make([]uint16, nameLength/2) - err := binary.Read(bytes.NewReader(b[nameOffset:nameOffset+nameLength]), binary.LittleEndian, &name) - if err != nil { - return nil, err - } - return &ReparsePoint{string(utf16.Decode(name)), isMountPoint}, nil -} - -func isDriveLetter(c byte) bool { - return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') -} - -// EncodeReparsePoint encodes a Win32 REPARSE_DATA_BUFFER structure describing a symlink or -// mount point. -func EncodeReparsePoint(rp *ReparsePoint) []byte { - // Generate an NT path and determine if this is a relative path. - var ntTarget string - relative := false - if strings.HasPrefix(rp.Target, `\\?\`) { - ntTarget = `\??\` + rp.Target[4:] - } else if strings.HasPrefix(rp.Target, `\\`) { - ntTarget = `\??\UNC\` + rp.Target[2:] - } else if len(rp.Target) >= 2 && isDriveLetter(rp.Target[0]) && rp.Target[1] == ':' { - ntTarget = `\??\` + rp.Target - } else { - ntTarget = rp.Target - relative = true - } - - // The paths must be NUL-terminated even though they are counted strings. - target16 := utf16.Encode([]rune(rp.Target + "\x00")) - ntTarget16 := utf16.Encode([]rune(ntTarget + "\x00")) - - size := int(unsafe.Sizeof(reparseDataBuffer{})) - 8 - size += len(ntTarget16)*2 + len(target16)*2 - - tag := uint32(reparseTagMountPoint) - if !rp.IsMountPoint { - tag = reparseTagSymlink - size += 4 // Add room for symlink flags - } - - data := reparseDataBuffer{ - ReparseTag: tag, - ReparseDataLength: uint16(size), - SubstituteNameOffset: 0, - SubstituteNameLength: uint16((len(ntTarget16) - 1) * 2), - PrintNameOffset: uint16(len(ntTarget16) * 2), - PrintNameLength: uint16((len(target16) - 1) * 2), - } - - var b bytes.Buffer - _ = binary.Write(&b, binary.LittleEndian, &data) - if !rp.IsMountPoint { - flags := uint32(0) - if relative { - flags |= 1 - } - _ = binary.Write(&b, binary.LittleEndian, flags) - } - - _ = binary.Write(&b, binary.LittleEndian, ntTarget16) - _ = binary.Write(&b, binary.LittleEndian, target16) - return b.Bytes() -} diff --git a/vendor/github.com/Microsoft/go-winio/sd.go b/vendor/github.com/Microsoft/go-winio/sd.go deleted file mode 100644 index c3685e98..00000000 --- a/vendor/github.com/Microsoft/go-winio/sd.go +++ /dev/null @@ -1,133 +0,0 @@ -//go:build windows -// +build windows - -package winio - -import ( - "errors" - "fmt" - "unsafe" - - "golang.org/x/sys/windows" -) - -//sys lookupAccountName(systemName *uint16, accountName string, sid *byte, sidSize *uint32, refDomain *uint16, refDomainSize *uint32, sidNameUse *uint32) (err error) = advapi32.LookupAccountNameW -//sys lookupAccountSid(systemName *uint16, sid *byte, name *uint16, nameSize *uint32, refDomain *uint16, refDomainSize *uint32, sidNameUse *uint32) (err error) = advapi32.LookupAccountSidW -//sys convertSidToStringSid(sid *byte, str **uint16) (err error) = advapi32.ConvertSidToStringSidW -//sys convertStringSidToSid(str *uint16, sid **byte) (err error) = advapi32.ConvertStringSidToSidW - -type AccountLookupError struct { - Name string - Err error -} - -func (e *AccountLookupError) Error() string { - if e.Name == "" { - return "lookup account: empty account name specified" - } - var s string - switch { - case errors.Is(e.Err, windows.ERROR_INVALID_SID): - s = "the security ID structure is invalid" - case errors.Is(e.Err, windows.ERROR_NONE_MAPPED): - s = "not found" - default: - s = e.Err.Error() - } - return "lookup account " + e.Name + ": " + s -} - -func (e *AccountLookupError) Unwrap() error { return e.Err } - -type SddlConversionError struct { - Sddl string - Err error -} - -func (e *SddlConversionError) Error() string { - return "convert " + e.Sddl + ": " + e.Err.Error() -} - -func (e *SddlConversionError) Unwrap() error { return e.Err } - -// LookupSidByName looks up the SID of an account by name -// -//revive:disable-next-line:var-naming SID, not Sid -func LookupSidByName(name string) (sid string, err error) { - if name == "" { - return "", &AccountLookupError{name, windows.ERROR_NONE_MAPPED} - } - - var sidSize, sidNameUse, refDomainSize uint32 - err = lookupAccountName(nil, name, nil, &sidSize, nil, &refDomainSize, &sidNameUse) - if err != nil && err != windows.ERROR_INSUFFICIENT_BUFFER { //nolint:errorlint // err is Errno - return "", &AccountLookupError{name, err} - } - sidBuffer := make([]byte, sidSize) - refDomainBuffer := make([]uint16, refDomainSize) - err = lookupAccountName(nil, name, &sidBuffer[0], &sidSize, &refDomainBuffer[0], &refDomainSize, &sidNameUse) - if err != nil { - return "", &AccountLookupError{name, err} - } - var strBuffer *uint16 - err = convertSidToStringSid(&sidBuffer[0], &strBuffer) - if err != nil { - return "", &AccountLookupError{name, err} - } - sid = windows.UTF16ToString((*[0xffff]uint16)(unsafe.Pointer(strBuffer))[:]) - _, _ = windows.LocalFree(windows.Handle(unsafe.Pointer(strBuffer))) - return sid, nil -} - -// LookupNameBySid looks up the name of an account by SID -// -//revive:disable-next-line:var-naming SID, not Sid -func LookupNameBySid(sid string) (name string, err error) { - if sid == "" { - return "", &AccountLookupError{sid, windows.ERROR_NONE_MAPPED} - } - - sidBuffer, err := windows.UTF16PtrFromString(sid) - if err != nil { - return "", &AccountLookupError{sid, err} - } - - var sidPtr *byte - if err = convertStringSidToSid(sidBuffer, &sidPtr); err != nil { - return "", &AccountLookupError{sid, err} - } - defer windows.LocalFree(windows.Handle(unsafe.Pointer(sidPtr))) //nolint:errcheck - - var nameSize, refDomainSize, sidNameUse uint32 - err = lookupAccountSid(nil, sidPtr, nil, &nameSize, nil, &refDomainSize, &sidNameUse) - if err != nil && err != windows.ERROR_INSUFFICIENT_BUFFER { //nolint:errorlint // err is Errno - return "", &AccountLookupError{sid, err} - } - - nameBuffer := make([]uint16, nameSize) - refDomainBuffer := make([]uint16, refDomainSize) - err = lookupAccountSid(nil, sidPtr, &nameBuffer[0], &nameSize, &refDomainBuffer[0], &refDomainSize, &sidNameUse) - if err != nil { - return "", &AccountLookupError{sid, err} - } - - name = windows.UTF16ToString(nameBuffer) - return name, nil -} - -func SddlToSecurityDescriptor(sddl string) ([]byte, error) { - sd, err := windows.SecurityDescriptorFromString(sddl) - if err != nil { - return nil, &SddlConversionError{Sddl: sddl, Err: err} - } - b := unsafe.Slice((*byte)(unsafe.Pointer(sd)), sd.Length()) - return b, nil -} - -func SecurityDescriptorToSddl(sd []byte) (string, error) { - if l := int(unsafe.Sizeof(windows.SECURITY_DESCRIPTOR{})); len(sd) < l { - return "", fmt.Errorf("SecurityDescriptor (%d) smaller than expected (%d): %w", len(sd), l, windows.ERROR_INCORRECT_SIZE) - } - s := (*windows.SECURITY_DESCRIPTOR)(unsafe.Pointer(&sd[0])) - return s.String(), nil -} diff --git a/vendor/github.com/Microsoft/go-winio/syscall.go b/vendor/github.com/Microsoft/go-winio/syscall.go deleted file mode 100644 index a6ca111b..00000000 --- a/vendor/github.com/Microsoft/go-winio/syscall.go +++ /dev/null @@ -1,5 +0,0 @@ -//go:build windows - -package winio - -//go:generate go run github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go ./*.go diff --git a/vendor/github.com/Microsoft/go-winio/zsyscall_windows.go b/vendor/github.com/Microsoft/go-winio/zsyscall_windows.go deleted file mode 100644 index 89b66eda..00000000 --- a/vendor/github.com/Microsoft/go-winio/zsyscall_windows.go +++ /dev/null @@ -1,378 +0,0 @@ -//go:build windows - -// Code generated by 'go generate' using "github.com/Microsoft/go-winio/tools/mkwinsyscall"; DO NOT EDIT. - -package winio - -import ( - "syscall" - "unsafe" - - "golang.org/x/sys/windows" -) - -var _ unsafe.Pointer - -// Do the interface allocations only once for common -// Errno values. -const ( - errnoERROR_IO_PENDING = 997 -) - -var ( - errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING) - errERROR_EINVAL error = syscall.EINVAL -) - -// errnoErr returns common boxed Errno values, to prevent -// allocations at runtime. -func errnoErr(e syscall.Errno) error { - switch e { - case 0: - return errERROR_EINVAL - case errnoERROR_IO_PENDING: - return errERROR_IO_PENDING - } - return e -} - -var ( - modadvapi32 = windows.NewLazySystemDLL("advapi32.dll") - modkernel32 = windows.NewLazySystemDLL("kernel32.dll") - modntdll = windows.NewLazySystemDLL("ntdll.dll") - modws2_32 = windows.NewLazySystemDLL("ws2_32.dll") - - procAdjustTokenPrivileges = modadvapi32.NewProc("AdjustTokenPrivileges") - procConvertSidToStringSidW = modadvapi32.NewProc("ConvertSidToStringSidW") - procConvertStringSidToSidW = modadvapi32.NewProc("ConvertStringSidToSidW") - procImpersonateSelf = modadvapi32.NewProc("ImpersonateSelf") - procLookupAccountNameW = modadvapi32.NewProc("LookupAccountNameW") - procLookupAccountSidW = modadvapi32.NewProc("LookupAccountSidW") - procLookupPrivilegeDisplayNameW = modadvapi32.NewProc("LookupPrivilegeDisplayNameW") - procLookupPrivilegeNameW = modadvapi32.NewProc("LookupPrivilegeNameW") - procLookupPrivilegeValueW = modadvapi32.NewProc("LookupPrivilegeValueW") - procOpenThreadToken = modadvapi32.NewProc("OpenThreadToken") - procRevertToSelf = modadvapi32.NewProc("RevertToSelf") - procBackupRead = modkernel32.NewProc("BackupRead") - procBackupWrite = modkernel32.NewProc("BackupWrite") - procCancelIoEx = modkernel32.NewProc("CancelIoEx") - procConnectNamedPipe = modkernel32.NewProc("ConnectNamedPipe") - procCreateIoCompletionPort = modkernel32.NewProc("CreateIoCompletionPort") - procCreateNamedPipeW = modkernel32.NewProc("CreateNamedPipeW") - procDisconnectNamedPipe = modkernel32.NewProc("DisconnectNamedPipe") - procGetCurrentThread = modkernel32.NewProc("GetCurrentThread") - procGetNamedPipeHandleStateW = modkernel32.NewProc("GetNamedPipeHandleStateW") - procGetNamedPipeInfo = modkernel32.NewProc("GetNamedPipeInfo") - procGetQueuedCompletionStatus = modkernel32.NewProc("GetQueuedCompletionStatus") - procSetFileCompletionNotificationModes = modkernel32.NewProc("SetFileCompletionNotificationModes") - procNtCreateNamedPipeFile = modntdll.NewProc("NtCreateNamedPipeFile") - procRtlDefaultNpAcl = modntdll.NewProc("RtlDefaultNpAcl") - procRtlDosPathNameToNtPathName_U = modntdll.NewProc("RtlDosPathNameToNtPathName_U") - procRtlNtStatusToDosErrorNoTeb = modntdll.NewProc("RtlNtStatusToDosErrorNoTeb") - procWSAGetOverlappedResult = modws2_32.NewProc("WSAGetOverlappedResult") -) - -func adjustTokenPrivileges(token windows.Token, releaseAll bool, input *byte, outputSize uint32, output *byte, requiredSize *uint32) (success bool, err error) { - var _p0 uint32 - if releaseAll { - _p0 = 1 - } - r0, _, e1 := syscall.SyscallN(procAdjustTokenPrivileges.Addr(), uintptr(token), uintptr(_p0), uintptr(unsafe.Pointer(input)), uintptr(outputSize), uintptr(unsafe.Pointer(output)), uintptr(unsafe.Pointer(requiredSize))) - success = r0 != 0 - if true { - err = errnoErr(e1) - } - return -} - -func convertSidToStringSid(sid *byte, str **uint16) (err error) { - r1, _, e1 := syscall.SyscallN(procConvertSidToStringSidW.Addr(), uintptr(unsafe.Pointer(sid)), uintptr(unsafe.Pointer(str))) - if r1 == 0 { - err = errnoErr(e1) - } - return -} - -func convertStringSidToSid(str *uint16, sid **byte) (err error) { - r1, _, e1 := syscall.SyscallN(procConvertStringSidToSidW.Addr(), uintptr(unsafe.Pointer(str)), uintptr(unsafe.Pointer(sid))) - if r1 == 0 { - err = errnoErr(e1) - } - return -} - -func impersonateSelf(level uint32) (err error) { - r1, _, e1 := syscall.SyscallN(procImpersonateSelf.Addr(), uintptr(level)) - if r1 == 0 { - err = errnoErr(e1) - } - return -} - -func lookupAccountName(systemName *uint16, accountName string, sid *byte, sidSize *uint32, refDomain *uint16, refDomainSize *uint32, sidNameUse *uint32) (err error) { - var _p0 *uint16 - _p0, err = syscall.UTF16PtrFromString(accountName) - if err != nil { - return - } - return _lookupAccountName(systemName, _p0, sid, sidSize, refDomain, refDomainSize, sidNameUse) -} - -func _lookupAccountName(systemName *uint16, accountName *uint16, sid *byte, sidSize *uint32, refDomain *uint16, refDomainSize *uint32, sidNameUse *uint32) (err error) { - r1, _, e1 := syscall.SyscallN(procLookupAccountNameW.Addr(), uintptr(unsafe.Pointer(systemName)), uintptr(unsafe.Pointer(accountName)), uintptr(unsafe.Pointer(sid)), uintptr(unsafe.Pointer(sidSize)), uintptr(unsafe.Pointer(refDomain)), uintptr(unsafe.Pointer(refDomainSize)), uintptr(unsafe.Pointer(sidNameUse))) - if r1 == 0 { - err = errnoErr(e1) - } - return -} - -func lookupAccountSid(systemName *uint16, sid *byte, name *uint16, nameSize *uint32, refDomain *uint16, refDomainSize *uint32, sidNameUse *uint32) (err error) { - r1, _, e1 := syscall.SyscallN(procLookupAccountSidW.Addr(), uintptr(unsafe.Pointer(systemName)), uintptr(unsafe.Pointer(sid)), uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(nameSize)), uintptr(unsafe.Pointer(refDomain)), uintptr(unsafe.Pointer(refDomainSize)), uintptr(unsafe.Pointer(sidNameUse))) - if r1 == 0 { - err = errnoErr(e1) - } - return -} - -func lookupPrivilegeDisplayName(systemName string, name *uint16, buffer *uint16, size *uint32, languageId *uint32) (err error) { - var _p0 *uint16 - _p0, err = syscall.UTF16PtrFromString(systemName) - if err != nil { - return - } - return _lookupPrivilegeDisplayName(_p0, name, buffer, size, languageId) -} - -func _lookupPrivilegeDisplayName(systemName *uint16, name *uint16, buffer *uint16, size *uint32, languageId *uint32) (err error) { - r1, _, e1 := syscall.SyscallN(procLookupPrivilegeDisplayNameW.Addr(), uintptr(unsafe.Pointer(systemName)), uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(buffer)), uintptr(unsafe.Pointer(size)), uintptr(unsafe.Pointer(languageId))) - if r1 == 0 { - err = errnoErr(e1) - } - return -} - -func lookupPrivilegeName(systemName string, luid *uint64, buffer *uint16, size *uint32) (err error) { - var _p0 *uint16 - _p0, err = syscall.UTF16PtrFromString(systemName) - if err != nil { - return - } - return _lookupPrivilegeName(_p0, luid, buffer, size) -} - -func _lookupPrivilegeName(systemName *uint16, luid *uint64, buffer *uint16, size *uint32) (err error) { - r1, _, e1 := syscall.SyscallN(procLookupPrivilegeNameW.Addr(), uintptr(unsafe.Pointer(systemName)), uintptr(unsafe.Pointer(luid)), uintptr(unsafe.Pointer(buffer)), uintptr(unsafe.Pointer(size))) - if r1 == 0 { - err = errnoErr(e1) - } - return -} - -func lookupPrivilegeValue(systemName string, name string, luid *uint64) (err error) { - var _p0 *uint16 - _p0, err = syscall.UTF16PtrFromString(systemName) - if err != nil { - return - } - var _p1 *uint16 - _p1, err = syscall.UTF16PtrFromString(name) - if err != nil { - return - } - return _lookupPrivilegeValue(_p0, _p1, luid) -} - -func _lookupPrivilegeValue(systemName *uint16, name *uint16, luid *uint64) (err error) { - r1, _, e1 := syscall.SyscallN(procLookupPrivilegeValueW.Addr(), uintptr(unsafe.Pointer(systemName)), uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(luid))) - if r1 == 0 { - err = errnoErr(e1) - } - return -} - -func openThreadToken(thread windows.Handle, accessMask uint32, openAsSelf bool, token *windows.Token) (err error) { - var _p0 uint32 - if openAsSelf { - _p0 = 1 - } - r1, _, e1 := syscall.SyscallN(procOpenThreadToken.Addr(), uintptr(thread), uintptr(accessMask), uintptr(_p0), uintptr(unsafe.Pointer(token))) - if r1 == 0 { - err = errnoErr(e1) - } - return -} - -func revertToSelf() (err error) { - r1, _, e1 := syscall.SyscallN(procRevertToSelf.Addr()) - if r1 == 0 { - err = errnoErr(e1) - } - return -} - -func backupRead(h windows.Handle, b []byte, bytesRead *uint32, abort bool, processSecurity bool, context *uintptr) (err error) { - var _p0 *byte - if len(b) > 0 { - _p0 = &b[0] - } - var _p1 uint32 - if abort { - _p1 = 1 - } - var _p2 uint32 - if processSecurity { - _p2 = 1 - } - r1, _, e1 := syscall.SyscallN(procBackupRead.Addr(), uintptr(h), uintptr(unsafe.Pointer(_p0)), uintptr(len(b)), uintptr(unsafe.Pointer(bytesRead)), uintptr(_p1), uintptr(_p2), uintptr(unsafe.Pointer(context))) - if r1 == 0 { - err = errnoErr(e1) - } - return -} - -func backupWrite(h windows.Handle, b []byte, bytesWritten *uint32, abort bool, processSecurity bool, context *uintptr) (err error) { - var _p0 *byte - if len(b) > 0 { - _p0 = &b[0] - } - var _p1 uint32 - if abort { - _p1 = 1 - } - var _p2 uint32 - if processSecurity { - _p2 = 1 - } - r1, _, e1 := syscall.SyscallN(procBackupWrite.Addr(), uintptr(h), uintptr(unsafe.Pointer(_p0)), uintptr(len(b)), uintptr(unsafe.Pointer(bytesWritten)), uintptr(_p1), uintptr(_p2), uintptr(unsafe.Pointer(context))) - if r1 == 0 { - err = errnoErr(e1) - } - return -} - -func cancelIoEx(file windows.Handle, o *windows.Overlapped) (err error) { - r1, _, e1 := syscall.SyscallN(procCancelIoEx.Addr(), uintptr(file), uintptr(unsafe.Pointer(o))) - if r1 == 0 { - err = errnoErr(e1) - } - return -} - -func connectNamedPipe(pipe windows.Handle, o *windows.Overlapped) (err error) { - r1, _, e1 := syscall.SyscallN(procConnectNamedPipe.Addr(), uintptr(pipe), uintptr(unsafe.Pointer(o))) - if r1 == 0 { - err = errnoErr(e1) - } - return -} - -func createIoCompletionPort(file windows.Handle, port windows.Handle, key uintptr, threadCount uint32) (newport windows.Handle, err error) { - r0, _, e1 := syscall.SyscallN(procCreateIoCompletionPort.Addr(), uintptr(file), uintptr(port), uintptr(key), uintptr(threadCount)) - newport = windows.Handle(r0) - if newport == 0 { - err = errnoErr(e1) - } - return -} - -func createNamedPipe(name string, flags uint32, pipeMode uint32, maxInstances uint32, outSize uint32, inSize uint32, defaultTimeout uint32, sa *windows.SecurityAttributes) (handle windows.Handle, err error) { - var _p0 *uint16 - _p0, err = syscall.UTF16PtrFromString(name) - if err != nil { - return - } - return _createNamedPipe(_p0, flags, pipeMode, maxInstances, outSize, inSize, defaultTimeout, sa) -} - -func _createNamedPipe(name *uint16, flags uint32, pipeMode uint32, maxInstances uint32, outSize uint32, inSize uint32, defaultTimeout uint32, sa *windows.SecurityAttributes) (handle windows.Handle, err error) { - r0, _, e1 := syscall.SyscallN(procCreateNamedPipeW.Addr(), uintptr(unsafe.Pointer(name)), uintptr(flags), uintptr(pipeMode), uintptr(maxInstances), uintptr(outSize), uintptr(inSize), uintptr(defaultTimeout), uintptr(unsafe.Pointer(sa))) - handle = windows.Handle(r0) - if handle == windows.InvalidHandle { - err = errnoErr(e1) - } - return -} - -func disconnectNamedPipe(pipe windows.Handle) (err error) { - r1, _, e1 := syscall.SyscallN(procDisconnectNamedPipe.Addr(), uintptr(pipe)) - if r1 == 0 { - err = errnoErr(e1) - } - return -} - -func getCurrentThread() (h windows.Handle) { - r0, _, _ := syscall.SyscallN(procGetCurrentThread.Addr()) - h = windows.Handle(r0) - return -} - -func getNamedPipeHandleState(pipe windows.Handle, state *uint32, curInstances *uint32, maxCollectionCount *uint32, collectDataTimeout *uint32, userName *uint16, maxUserNameSize uint32) (err error) { - r1, _, e1 := syscall.SyscallN(procGetNamedPipeHandleStateW.Addr(), uintptr(pipe), uintptr(unsafe.Pointer(state)), uintptr(unsafe.Pointer(curInstances)), uintptr(unsafe.Pointer(maxCollectionCount)), uintptr(unsafe.Pointer(collectDataTimeout)), uintptr(unsafe.Pointer(userName)), uintptr(maxUserNameSize)) - if r1 == 0 { - err = errnoErr(e1) - } - return -} - -func getNamedPipeInfo(pipe windows.Handle, flags *uint32, outSize *uint32, inSize *uint32, maxInstances *uint32) (err error) { - r1, _, e1 := syscall.SyscallN(procGetNamedPipeInfo.Addr(), uintptr(pipe), uintptr(unsafe.Pointer(flags)), uintptr(unsafe.Pointer(outSize)), uintptr(unsafe.Pointer(inSize)), uintptr(unsafe.Pointer(maxInstances))) - if r1 == 0 { - err = errnoErr(e1) - } - return -} - -func getQueuedCompletionStatus(port windows.Handle, bytes *uint32, key *uintptr, o **ioOperation, timeout uint32) (err error) { - r1, _, e1 := syscall.SyscallN(procGetQueuedCompletionStatus.Addr(), uintptr(port), uintptr(unsafe.Pointer(bytes)), uintptr(unsafe.Pointer(key)), uintptr(unsafe.Pointer(o)), uintptr(timeout)) - if r1 == 0 { - err = errnoErr(e1) - } - return -} - -func setFileCompletionNotificationModes(h windows.Handle, flags uint8) (err error) { - r1, _, e1 := syscall.SyscallN(procSetFileCompletionNotificationModes.Addr(), uintptr(h), uintptr(flags)) - if r1 == 0 { - err = errnoErr(e1) - } - return -} - -func ntCreateNamedPipeFile(pipe *windows.Handle, access ntAccessMask, oa *objectAttributes, iosb *ioStatusBlock, share ntFileShareMode, disposition ntFileCreationDisposition, options ntFileOptions, typ uint32, readMode uint32, completionMode uint32, maxInstances uint32, inboundQuota uint32, outputQuota uint32, timeout *int64) (status ntStatus) { - r0, _, _ := syscall.SyscallN(procNtCreateNamedPipeFile.Addr(), uintptr(unsafe.Pointer(pipe)), uintptr(access), uintptr(unsafe.Pointer(oa)), uintptr(unsafe.Pointer(iosb)), uintptr(share), uintptr(disposition), uintptr(options), uintptr(typ), uintptr(readMode), uintptr(completionMode), uintptr(maxInstances), uintptr(inboundQuota), uintptr(outputQuota), uintptr(unsafe.Pointer(timeout))) - status = ntStatus(r0) - return -} - -func rtlDefaultNpAcl(dacl *uintptr) (status ntStatus) { - r0, _, _ := syscall.SyscallN(procRtlDefaultNpAcl.Addr(), uintptr(unsafe.Pointer(dacl))) - status = ntStatus(r0) - return -} - -func rtlDosPathNameToNtPathName(name *uint16, ntName *unicodeString, filePart uintptr, reserved uintptr) (status ntStatus) { - r0, _, _ := syscall.SyscallN(procRtlDosPathNameToNtPathName_U.Addr(), uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(ntName)), uintptr(filePart), uintptr(reserved)) - status = ntStatus(r0) - return -} - -func rtlNtStatusToDosError(status ntStatus) (winerr error) { - r0, _, _ := syscall.SyscallN(procRtlNtStatusToDosErrorNoTeb.Addr(), uintptr(status)) - if r0 != 0 { - winerr = syscall.Errno(r0) - } - return -} - -func wsaGetOverlappedResult(h windows.Handle, o *windows.Overlapped, bytes *uint32, wait bool, flags *uint32) (err error) { - var _p0 uint32 - if wait { - _p0 = 1 - } - r1, _, e1 := syscall.SyscallN(procWSAGetOverlappedResult.Addr(), uintptr(h), uintptr(unsafe.Pointer(o)), uintptr(unsafe.Pointer(bytes)), uintptr(_p0), uintptr(unsafe.Pointer(flags))) - if r1 == 0 { - err = errnoErr(e1) - } - return -} diff --git a/vendor/github.com/beorn7/perks/LICENSE b/vendor/github.com/beorn7/perks/LICENSE new file mode 100644 index 00000000..339177be --- /dev/null +++ b/vendor/github.com/beorn7/perks/LICENSE @@ -0,0 +1,20 @@ +Copyright (C) 2013 Blake Mizerany + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/beorn7/perks/quantile/exampledata.txt b/vendor/github.com/beorn7/perks/quantile/exampledata.txt new file mode 100644 index 00000000..1602287d --- /dev/null +++ b/vendor/github.com/beorn7/perks/quantile/exampledata.txt @@ -0,0 +1,2388 @@ +8 +5 +26 +12 +5 +235 +13 +6 +28 +30 +3 +3 +3 +3 +5 +2 +33 +7 +2 +4 +7 +12 +14 +5 +8 +3 +10 +4 +5 +3 +6 +6 +209 +20 +3 +10 +14 +3 +4 +6 +8 +5 +11 +7 +3 +2 +3 +3 +212 +5 +222 +4 +10 +10 +5 +6 +3 +8 +3 +10 +254 +220 +2 +3 +5 +24 +5 +4 +222 +7 +3 +3 +223 +8 +15 +12 +14 +14 +3 +2 +2 +3 +13 +3 +11 +4 +4 +6 +5 +7 +13 +5 +3 +5 +2 +5 +3 +5 +2 +7 +15 +17 +14 +3 +6 +6 +3 +17 +5 +4 +7 +6 +4 +4 +8 +6 +8 +3 +9 +3 +6 +3 +4 +5 +3 +3 +660 +4 +6 +10 +3 +6 +3 +2 +5 +13 +2 +4 +4 +10 +4 +8 +4 +3 +7 +9 +9 +3 +10 +37 +3 +13 +4 +12 +3 +6 +10 +8 +5 +21 +2 +3 +8 +3 +2 +3 +3 +4 +12 +2 +4 +8 +8 +4 +3 +2 +20 +1 +6 +32 +2 +11 +6 +18 +3 +8 +11 +3 +212 +3 +4 +2 +6 +7 +12 +11 +3 +2 +16 +10 +6 +4 +6 +3 +2 +7 +3 +2 +2 +2 +2 +5 +6 +4 +3 +10 +3 +4 +6 +5 +3 +4 +4 +5 +6 +4 +3 +4 +4 +5 +7 +5 +5 +3 +2 +7 +2 +4 +12 +4 +5 +6 +2 +4 +4 +8 +4 +15 +13 +7 +16 +5 +3 +23 +5 +5 +7 +3 +2 +9 +8 +7 +5 +8 +11 +4 +10 +76 +4 +47 +4 +3 +2 +7 +4 +2 +3 +37 +10 +4 +2 +20 +5 +4 +4 +10 +10 +4 +3 +7 +23 +240 +7 +13 +5 +5 +3 +3 +2 +5 +4 +2 +8 +7 +19 +2 +23 +8 +7 +2 +5 +3 +8 +3 +8 +13 +5 +5 +5 +2 +3 +23 +4 +9 +8 +4 +3 +3 +5 +220 +2 +3 +4 +6 +14 +3 +53 +6 +2 +5 +18 +6 +3 +219 +6 +5 +2 +5 +3 +6 +5 +15 +4 +3 +17 +3 +2 +4 +7 +2 +3 +3 +4 +4 +3 +2 +664 +6 +3 +23 +5 +5 +16 +5 +8 +2 +4 +2 +24 +12 +3 +2 +3 +5 +8 +3 +5 +4 +3 +14 +3 +5 +8 +2 +3 +7 +9 +4 +2 +3 +6 +8 +4 +3 +4 +6 +5 +3 +3 +6 +3 +19 +4 +4 +6 +3 +6 +3 +5 +22 +5 +4 +4 +3 +8 +11 +4 +9 +7 +6 +13 +4 +4 +4 +6 +17 +9 +3 +3 +3 +4 +3 +221 +5 +11 +3 +4 +2 +12 +6 +3 +5 +7 +5 +7 +4 +9 +7 +14 +37 +19 +217 +16 +3 +5 +2 +2 +7 +19 +7 +6 +7 +4 +24 +5 +11 +4 +7 +7 +9 +13 +3 +4 +3 +6 +28 +4 +4 +5 +5 +2 +5 +6 +4 +4 +6 +10 +5 +4 +3 +2 +3 +3 +6 +5 +5 +4 +3 +2 +3 +7 +4 +6 +18 +16 +8 +16 +4 +5 +8 +6 +9 +13 +1545 +6 +215 +6 +5 +6 +3 +45 +31 +5 +2 +2 +4 +3 +3 +2 +5 +4 +3 +5 +7 +7 +4 +5 +8 +5 +4 +749 +2 +31 +9 +11 +2 +11 +5 +4 +4 +7 +9 +11 +4 +5 +4 +7 +3 +4 +6 +2 +15 +3 +4 +3 +4 +3 +5 +2 +13 +5 +5 +3 +3 +23 +4 +4 +5 +7 +4 +13 +2 +4 +3 +4 +2 +6 +2 +7 +3 +5 +5 +3 +29 +5 +4 +4 +3 +10 +2 +3 +79 +16 +6 +6 +7 +7 +3 +5 +5 +7 +4 +3 +7 +9 +5 +6 +5 +9 +6 +3 +6 +4 +17 +2 +10 +9 +3 +6 +2 +3 +21 +22 +5 +11 +4 +2 +17 +2 +224 +2 +14 +3 +4 +4 +2 +4 +4 +4 +4 +5 +3 +4 +4 +10 +2 +6 +3 +3 +5 +7 +2 +7 +5 +6 +3 +218 +2 +2 +5 +2 +6 +3 +5 +222 +14 +6 +33 +3 +2 +5 +3 +3 +3 +9 +5 +3 +3 +2 +7 +4 +3 +4 +3 +5 +6 +5 +26 +4 +13 +9 +7 +3 +221 +3 +3 +4 +4 +4 +4 +2 +18 +5 +3 +7 +9 +6 +8 +3 +10 +3 +11 +9 +5 +4 +17 +5 +5 +6 +6 +3 +2 +4 +12 +17 +6 +7 +218 +4 +2 +4 +10 +3 +5 +15 +3 +9 +4 +3 +3 +6 +29 +3 +3 +4 +5 +5 +3 +8 +5 +6 +6 +7 +5 +3 +5 +3 +29 +2 +31 +5 +15 +24 +16 +5 +207 +4 +3 +3 +2 +15 +4 +4 +13 +5 +5 +4 +6 +10 +2 +7 +8 +4 +6 +20 +5 +3 +4 +3 +12 +12 +5 +17 +7 +3 +3 +3 +6 +10 +3 +5 +25 +80 +4 +9 +3 +2 +11 +3 +3 +2 +3 +8 +7 +5 +5 +19 +5 +3 +3 +12 +11 +2 +6 +5 +5 +5 +3 +3 +3 +4 +209 +14 +3 +2 +5 +19 +4 +4 +3 +4 +14 +5 +6 +4 +13 +9 +7 +4 +7 +10 +2 +9 +5 +7 +2 +8 +4 +6 +5 +5 +222 +8 +7 +12 +5 +216 +3 +4 +4 +6 +3 +14 +8 +7 +13 +4 +3 +3 +3 +3 +17 +5 +4 +3 +33 +6 +6 +33 +7 +5 +3 +8 +7 +5 +2 +9 +4 +2 +233 +24 +7 +4 +8 +10 +3 +4 +15 +2 +16 +3 +3 +13 +12 +7 +5 +4 +207 +4 +2 +4 +27 +15 +2 +5 +2 +25 +6 +5 +5 +6 +13 +6 +18 +6 +4 +12 +225 +10 +7 +5 +2 +2 +11 +4 +14 +21 +8 +10 +3 +5 +4 +232 +2 +5 +5 +3 +7 +17 +11 +6 +6 +23 +4 +6 +3 +5 +4 +2 +17 +3 +6 +5 +8 +3 +2 +2 +14 +9 +4 +4 +2 +5 +5 +3 +7 +6 +12 +6 +10 +3 +6 +2 +2 +19 +5 +4 +4 +9 +2 +4 +13 +3 +5 +6 +3 +6 +5 +4 +9 +6 +3 +5 +7 +3 +6 +6 +4 +3 +10 +6 +3 +221 +3 +5 +3 +6 +4 +8 +5 +3 +6 +4 +4 +2 +54 +5 +6 +11 +3 +3 +4 +4 +4 +3 +7 +3 +11 +11 +7 +10 +6 +13 +223 +213 +15 +231 +7 +3 +7 +228 +2 +3 +4 +4 +5 +6 +7 +4 +13 +3 +4 +5 +3 +6 +4 +6 +7 +2 +4 +3 +4 +3 +3 +6 +3 +7 +3 +5 +18 +5 +6 +8 +10 +3 +3 +3 +2 +4 +2 +4 +4 +5 +6 +6 +4 +10 +13 +3 +12 +5 +12 +16 +8 +4 +19 +11 +2 +4 +5 +6 +8 +5 +6 +4 +18 +10 +4 +2 +216 +6 +6 +6 +2 +4 +12 +8 +3 +11 +5 +6 +14 +5 +3 +13 +4 +5 +4 +5 +3 +28 +6 +3 +7 +219 +3 +9 +7 +3 +10 +6 +3 +4 +19 +5 +7 +11 +6 +15 +19 +4 +13 +11 +3 +7 +5 +10 +2 +8 +11 +2 +6 +4 +6 +24 +6 +3 +3 +3 +3 +6 +18 +4 +11 +4 +2 +5 +10 +8 +3 +9 +5 +3 +4 +5 +6 +2 +5 +7 +4 +4 +14 +6 +4 +4 +5 +5 +7 +2 +4 +3 +7 +3 +3 +6 +4 +5 +4 +4 +4 +3 +3 +3 +3 +8 +14 +2 +3 +5 +3 +2 +4 +5 +3 +7 +3 +3 +18 +3 +4 +4 +5 +7 +3 +3 +3 +13 +5 +4 +8 +211 +5 +5 +3 +5 +2 +5 +4 +2 +655 +6 +3 +5 +11 +2 +5 +3 +12 +9 +15 +11 +5 +12 +217 +2 +6 +17 +3 +3 +207 +5 +5 +4 +5 +9 +3 +2 +8 +5 +4 +3 +2 +5 +12 +4 +14 +5 +4 +2 +13 +5 +8 +4 +225 +4 +3 +4 +5 +4 +3 +3 +6 +23 +9 +2 +6 +7 +233 +4 +4 +6 +18 +3 +4 +6 +3 +4 +4 +2 +3 +7 +4 +13 +227 +4 +3 +5 +4 +2 +12 +9 +17 +3 +7 +14 +6 +4 +5 +21 +4 +8 +9 +2 +9 +25 +16 +3 +6 +4 +7 +8 +5 +2 +3 +5 +4 +3 +3 +5 +3 +3 +3 +2 +3 +19 +2 +4 +3 +4 +2 +3 +4 +4 +2 +4 +3 +3 +3 +2 +6 +3 +17 +5 +6 +4 +3 +13 +5 +3 +3 +3 +4 +9 +4 +2 +14 +12 +4 +5 +24 +4 +3 +37 +12 +11 +21 +3 +4 +3 +13 +4 +2 +3 +15 +4 +11 +4 +4 +3 +8 +3 +4 +4 +12 +8 +5 +3 +3 +4 +2 +220 +3 +5 +223 +3 +3 +3 +10 +3 +15 +4 +241 +9 +7 +3 +6 +6 +23 +4 +13 +7 +3 +4 +7 +4 +9 +3 +3 +4 +10 +5 +5 +1 +5 +24 +2 +4 +5 +5 +6 +14 +3 +8 +2 +3 +5 +13 +13 +3 +5 +2 +3 +15 +3 +4 +2 +10 +4 +4 +4 +5 +5 +3 +5 +3 +4 +7 +4 +27 +3 +6 +4 +15 +3 +5 +6 +6 +5 +4 +8 +3 +9 +2 +6 +3 +4 +3 +7 +4 +18 +3 +11 +3 +3 +8 +9 +7 +24 +3 +219 +7 +10 +4 +5 +9 +12 +2 +5 +4 +4 +4 +3 +3 +19 +5 +8 +16 +8 +6 +22 +3 +23 +3 +242 +9 +4 +3 +3 +5 +7 +3 +3 +5 +8 +3 +7 +5 +14 +8 +10 +3 +4 +3 +7 +4 +6 +7 +4 +10 +4 +3 +11 +3 +7 +10 +3 +13 +6 +8 +12 +10 +5 +7 +9 +3 +4 +7 +7 +10 +8 +30 +9 +19 +4 +3 +19 +15 +4 +13 +3 +215 +223 +4 +7 +4 +8 +17 +16 +3 +7 +6 +5 +5 +4 +12 +3 +7 +4 +4 +13 +4 +5 +2 +5 +6 +5 +6 +6 +7 +10 +18 +23 +9 +3 +3 +6 +5 +2 +4 +2 +7 +3 +3 +2 +5 +5 +14 +10 +224 +6 +3 +4 +3 +7 +5 +9 +3 +6 +4 +2 +5 +11 +4 +3 +3 +2 +8 +4 +7 +4 +10 +7 +3 +3 +18 +18 +17 +3 +3 +3 +4 +5 +3 +3 +4 +12 +7 +3 +11 +13 +5 +4 +7 +13 +5 +4 +11 +3 +12 +3 +6 +4 +4 +21 +4 +6 +9 +5 +3 +10 +8 +4 +6 +4 +4 +6 +5 +4 +8 +6 +4 +6 +4 +4 +5 +9 +6 +3 +4 +2 +9 +3 +18 +2 +4 +3 +13 +3 +6 +6 +8 +7 +9 +3 +2 +16 +3 +4 +6 +3 +2 +33 +22 +14 +4 +9 +12 +4 +5 +6 +3 +23 +9 +4 +3 +5 +5 +3 +4 +5 +3 +5 +3 +10 +4 +5 +5 +8 +4 +4 +6 +8 +5 +4 +3 +4 +6 +3 +3 +3 +5 +9 +12 +6 +5 +9 +3 +5 +3 +2 +2 +2 +18 +3 +2 +21 +2 +5 +4 +6 +4 +5 +10 +3 +9 +3 +2 +10 +7 +3 +6 +6 +4 +4 +8 +12 +7 +3 +7 +3 +3 +9 +3 +4 +5 +4 +4 +5 +5 +10 +15 +4 +4 +14 +6 +227 +3 +14 +5 +216 +22 +5 +4 +2 +2 +6 +3 +4 +2 +9 +9 +4 +3 +28 +13 +11 +4 +5 +3 +3 +2 +3 +3 +5 +3 +4 +3 +5 +23 +26 +3 +4 +5 +6 +4 +6 +3 +5 +5 +3 +4 +3 +2 +2 +2 +7 +14 +3 +6 +7 +17 +2 +2 +15 +14 +16 +4 +6 +7 +13 +6 +4 +5 +6 +16 +3 +3 +28 +3 +6 +15 +3 +9 +2 +4 +6 +3 +3 +22 +4 +12 +6 +7 +2 +5 +4 +10 +3 +16 +6 +9 +2 +5 +12 +7 +5 +5 +5 +5 +2 +11 +9 +17 +4 +3 +11 +7 +3 +5 +15 +4 +3 +4 +211 +8 +7 +5 +4 +7 +6 +7 +6 +3 +6 +5 +6 +5 +3 +4 +4 +26 +4 +6 +10 +4 +4 +3 +2 +3 +3 +4 +5 +9 +3 +9 +4 +4 +5 +5 +8 +2 +4 +2 +3 +8 +4 +11 +19 +5 +8 +6 +3 +5 +6 +12 +3 +2 +4 +16 +12 +3 +4 +4 +8 +6 +5 +6 +6 +219 +8 +222 +6 +16 +3 +13 +19 +5 +4 +3 +11 +6 +10 +4 +7 +7 +12 +5 +3 +3 +5 +6 +10 +3 +8 +2 +5 +4 +7 +2 +4 +4 +2 +12 +9 +6 +4 +2 +40 +2 +4 +10 +4 +223 +4 +2 +20 +6 +7 +24 +5 +4 +5 +2 +20 +16 +6 +5 +13 +2 +3 +3 +19 +3 +2 +4 +5 +6 +7 +11 +12 +5 +6 +7 +7 +3 +5 +3 +5 +3 +14 +3 +4 +4 +2 +11 +1 +7 +3 +9 +6 +11 +12 +5 +8 +6 +221 +4 +2 +12 +4 +3 +15 +4 +5 +226 +7 +218 +7 +5 +4 +5 +18 +4 +5 +9 +4 +4 +2 +9 +18 +18 +9 +5 +6 +6 +3 +3 +7 +3 +5 +4 +4 +4 +12 +3 +6 +31 +5 +4 +7 +3 +6 +5 +6 +5 +11 +2 +2 +11 +11 +6 +7 +5 +8 +7 +10 +5 +23 +7 +4 +3 +5 +34 +2 +5 +23 +7 +3 +6 +8 +4 +4 +4 +2 +5 +3 +8 +5 +4 +8 +25 +2 +3 +17 +8 +3 +4 +8 +7 +3 +15 +6 +5 +7 +21 +9 +5 +6 +6 +5 +3 +2 +3 +10 +3 +6 +3 +14 +7 +4 +4 +8 +7 +8 +2 +6 +12 +4 +213 +6 +5 +21 +8 +2 +5 +23 +3 +11 +2 +3 +6 +25 +2 +3 +6 +7 +6 +6 +4 +4 +6 +3 +17 +9 +7 +6 +4 +3 +10 +7 +2 +3 +3 +3 +11 +8 +3 +7 +6 +4 +14 +36 +3 +4 +3 +3 +22 +13 +21 +4 +2 +7 +4 +4 +17 +15 +3 +7 +11 +2 +4 +7 +6 +209 +6 +3 +2 +2 +24 +4 +9 +4 +3 +3 +3 +29 +2 +2 +4 +3 +3 +5 +4 +6 +3 +3 +2 +4 diff --git a/vendor/github.com/beorn7/perks/quantile/stream.go b/vendor/github.com/beorn7/perks/quantile/stream.go new file mode 100644 index 00000000..d7d14f8e --- /dev/null +++ b/vendor/github.com/beorn7/perks/quantile/stream.go @@ -0,0 +1,316 @@ +// Package quantile computes approximate quantiles over an unbounded data +// stream within low memory and CPU bounds. +// +// A small amount of accuracy is traded to achieve the above properties. +// +// Multiple streams can be merged before calling Query to generate a single set +// of results. This is meaningful when the streams represent the same type of +// data. See Merge and Samples. +// +// For more detailed information about the algorithm used, see: +// +// Effective Computation of Biased Quantiles over Data Streams +// +// http://www.cs.rutgers.edu/~muthu/bquant.pdf +package quantile + +import ( + "math" + "sort" +) + +// Sample holds an observed value and meta information for compression. JSON +// tags have been added for convenience. +type Sample struct { + Value float64 `json:",string"` + Width float64 `json:",string"` + Delta float64 `json:",string"` +} + +// Samples represents a slice of samples. It implements sort.Interface. +type Samples []Sample + +func (a Samples) Len() int { return len(a) } +func (a Samples) Less(i, j int) bool { return a[i].Value < a[j].Value } +func (a Samples) Swap(i, j int) { a[i], a[j] = a[j], a[i] } + +type invariant func(s *stream, r float64) float64 + +// NewLowBiased returns an initialized Stream for low-biased quantiles +// (e.g. 0.01, 0.1, 0.5) where the needed quantiles are not known a priori, but +// error guarantees can still be given even for the lower ranks of the data +// distribution. +// +// The provided epsilon is a relative error, i.e. the true quantile of a value +// returned by a query is guaranteed to be within (1±Epsilon)*Quantile. +// +// See http://www.cs.rutgers.edu/~muthu/bquant.pdf for time, space, and error +// properties. +func NewLowBiased(epsilon float64) *Stream { + ƒ := func(s *stream, r float64) float64 { + return 2 * epsilon * r + } + return newStream(ƒ) +} + +// NewHighBiased returns an initialized Stream for high-biased quantiles +// (e.g. 0.01, 0.1, 0.5) where the needed quantiles are not known a priori, but +// error guarantees can still be given even for the higher ranks of the data +// distribution. +// +// The provided epsilon is a relative error, i.e. the true quantile of a value +// returned by a query is guaranteed to be within 1-(1±Epsilon)*(1-Quantile). +// +// See http://www.cs.rutgers.edu/~muthu/bquant.pdf for time, space, and error +// properties. +func NewHighBiased(epsilon float64) *Stream { + ƒ := func(s *stream, r float64) float64 { + return 2 * epsilon * (s.n - r) + } + return newStream(ƒ) +} + +// NewTargeted returns an initialized Stream concerned with a particular set of +// quantile values that are supplied a priori. Knowing these a priori reduces +// space and computation time. The targets map maps the desired quantiles to +// their absolute errors, i.e. the true quantile of a value returned by a query +// is guaranteed to be within (Quantile±Epsilon). +// +// See http://www.cs.rutgers.edu/~muthu/bquant.pdf for time, space, and error properties. +func NewTargeted(targetMap map[float64]float64) *Stream { + // Convert map to slice to avoid slow iterations on a map. + // ƒ is called on the hot path, so converting the map to a slice + // beforehand results in significant CPU savings. + targets := targetMapToSlice(targetMap) + + ƒ := func(s *stream, r float64) float64 { + var m = math.MaxFloat64 + var f float64 + for _, t := range targets { + if t.quantile*s.n <= r { + f = (2 * t.epsilon * r) / t.quantile + } else { + f = (2 * t.epsilon * (s.n - r)) / (1 - t.quantile) + } + if f < m { + m = f + } + } + return m + } + return newStream(ƒ) +} + +type target struct { + quantile float64 + epsilon float64 +} + +func targetMapToSlice(targetMap map[float64]float64) []target { + targets := make([]target, 0, len(targetMap)) + + for quantile, epsilon := range targetMap { + t := target{ + quantile: quantile, + epsilon: epsilon, + } + targets = append(targets, t) + } + + return targets +} + +// Stream computes quantiles for a stream of float64s. It is not thread-safe by +// design. Take care when using across multiple goroutines. +type Stream struct { + *stream + b Samples + sorted bool +} + +func newStream(ƒ invariant) *Stream { + x := &stream{ƒ: ƒ} + return &Stream{x, make(Samples, 0, 500), true} +} + +// Insert inserts v into the stream. +func (s *Stream) Insert(v float64) { + s.insert(Sample{Value: v, Width: 1}) +} + +func (s *Stream) insert(sample Sample) { + s.b = append(s.b, sample) + s.sorted = false + if len(s.b) == cap(s.b) { + s.flush() + } +} + +// Query returns the computed qth percentiles value. If s was created with +// NewTargeted, and q is not in the set of quantiles provided a priori, Query +// will return an unspecified result. +func (s *Stream) Query(q float64) float64 { + if !s.flushed() { + // Fast path when there hasn't been enough data for a flush; + // this also yields better accuracy for small sets of data. + l := len(s.b) + if l == 0 { + return 0 + } + i := int(math.Ceil(float64(l) * q)) + if i > 0 { + i -= 1 + } + s.maybeSort() + return s.b[i].Value + } + s.flush() + return s.stream.query(q) +} + +// Merge merges samples into the underlying streams samples. This is handy when +// merging multiple streams from separate threads, database shards, etc. +// +// ATTENTION: This method is broken and does not yield correct results. The +// underlying algorithm is not capable of merging streams correctly. +func (s *Stream) Merge(samples Samples) { + sort.Sort(samples) + s.stream.merge(samples) +} + +// Reset reinitializes and clears the list reusing the samples buffer memory. +func (s *Stream) Reset() { + s.stream.reset() + s.b = s.b[:0] +} + +// Samples returns stream samples held by s. +func (s *Stream) Samples() Samples { + if !s.flushed() { + return s.b + } + s.flush() + return s.stream.samples() +} + +// Count returns the total number of samples observed in the stream +// since initialization. +func (s *Stream) Count() int { + return len(s.b) + s.stream.count() +} + +func (s *Stream) flush() { + s.maybeSort() + s.stream.merge(s.b) + s.b = s.b[:0] +} + +func (s *Stream) maybeSort() { + if !s.sorted { + s.sorted = true + sort.Sort(s.b) + } +} + +func (s *Stream) flushed() bool { + return len(s.stream.l) > 0 +} + +type stream struct { + n float64 + l []Sample + ƒ invariant +} + +func (s *stream) reset() { + s.l = s.l[:0] + s.n = 0 +} + +func (s *stream) insert(v float64) { + s.merge(Samples{{v, 1, 0}}) +} + +func (s *stream) merge(samples Samples) { + // TODO(beorn7): This tries to merge not only individual samples, but + // whole summaries. The paper doesn't mention merging summaries at + // all. Unittests show that the merging is inaccurate. Find out how to + // do merges properly. + var r float64 + i := 0 + for _, sample := range samples { + for ; i < len(s.l); i++ { + c := s.l[i] + if c.Value > sample.Value { + // Insert at position i. + s.l = append(s.l, Sample{}) + copy(s.l[i+1:], s.l[i:]) + s.l[i] = Sample{ + sample.Value, + sample.Width, + math.Max(sample.Delta, math.Floor(s.ƒ(s, r))-1), + // TODO(beorn7): How to calculate delta correctly? + } + i++ + goto inserted + } + r += c.Width + } + s.l = append(s.l, Sample{sample.Value, sample.Width, 0}) + i++ + inserted: + s.n += sample.Width + r += sample.Width + } + s.compress() +} + +func (s *stream) count() int { + return int(s.n) +} + +func (s *stream) query(q float64) float64 { + t := math.Ceil(q * s.n) + t += math.Ceil(s.ƒ(s, t) / 2) + p := s.l[0] + var r float64 + for _, c := range s.l[1:] { + r += p.Width + if r+c.Width+c.Delta > t { + return p.Value + } + p = c + } + return p.Value +} + +func (s *stream) compress() { + if len(s.l) < 2 { + return + } + x := s.l[len(s.l)-1] + xi := len(s.l) - 1 + r := s.n - 1 - x.Width + + for i := len(s.l) - 2; i >= 0; i-- { + c := s.l[i] + if c.Width+x.Width+x.Delta <= s.ƒ(s, r) { + x.Width += c.Width + s.l[xi] = x + // Remove element at i. + copy(s.l[i:], s.l[i+1:]) + s.l = s.l[:len(s.l)-1] + xi -= 1 + } else { + x = c + xi = i + } + r -= c.Width + } +} + +func (s *stream) samples() Samples { + samples := make(Samples, len(s.l)) + copy(samples, s.l) + return samples +} diff --git a/vendor/github.com/cespare/xxhash/v2/LICENSE.txt b/vendor/github.com/cespare/xxhash/v2/LICENSE.txt new file mode 100644 index 00000000..24b53065 --- /dev/null +++ b/vendor/github.com/cespare/xxhash/v2/LICENSE.txt @@ -0,0 +1,22 @@ +Copyright (c) 2016 Caleb Spare + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/cespare/xxhash/v2/README.md b/vendor/github.com/cespare/xxhash/v2/README.md new file mode 100644 index 00000000..33c88305 --- /dev/null +++ b/vendor/github.com/cespare/xxhash/v2/README.md @@ -0,0 +1,74 @@ +# xxhash + +[![Go Reference](https://pkg.go.dev/badge/github.com/cespare/xxhash/v2.svg)](https://pkg.go.dev/github.com/cespare/xxhash/v2) +[![Test](https://github.com/cespare/xxhash/actions/workflows/test.yml/badge.svg)](https://github.com/cespare/xxhash/actions/workflows/test.yml) + +xxhash is a Go implementation of the 64-bit [xxHash] algorithm, XXH64. This is a +high-quality hashing algorithm that is much faster than anything in the Go +standard library. + +This package provides a straightforward API: + +``` +func Sum64(b []byte) uint64 +func Sum64String(s string) uint64 +type Digest struct{ ... } + func New() *Digest +``` + +The `Digest` type implements hash.Hash64. Its key methods are: + +``` +func (*Digest) Write([]byte) (int, error) +func (*Digest) WriteString(string) (int, error) +func (*Digest) Sum64() uint64 +``` + +The package is written with optimized pure Go and also contains even faster +assembly implementations for amd64 and arm64. If desired, the `purego` build tag +opts into using the Go code even on those architectures. + +[xxHash]: http://cyan4973.github.io/xxHash/ + +## Compatibility + +This package is in a module and the latest code is in version 2 of the module. +You need a version of Go with at least "minimal module compatibility" to use +github.com/cespare/xxhash/v2: + +* 1.9.7+ for Go 1.9 +* 1.10.3+ for Go 1.10 +* Go 1.11 or later + +I recommend using the latest release of Go. + +## Benchmarks + +Here are some quick benchmarks comparing the pure-Go and assembly +implementations of Sum64. + +| input size | purego | asm | +| ---------- | --------- | --------- | +| 4 B | 1.3 GB/s | 1.2 GB/s | +| 16 B | 2.9 GB/s | 3.5 GB/s | +| 100 B | 6.9 GB/s | 8.1 GB/s | +| 4 KB | 11.7 GB/s | 16.7 GB/s | +| 10 MB | 12.0 GB/s | 17.3 GB/s | + +These numbers were generated on Ubuntu 20.04 with an Intel Xeon Platinum 8252C +CPU using the following commands under Go 1.19.2: + +``` +benchstat <(go test -tags purego -benchtime 500ms -count 15 -bench 'Sum64$') +benchstat <(go test -benchtime 500ms -count 15 -bench 'Sum64$') +``` + +## Projects using this package + +- [InfluxDB](https://github.com/influxdata/influxdb) +- [Prometheus](https://github.com/prometheus/prometheus) +- [VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics) +- [FreeCache](https://github.com/coocood/freecache) +- [FastCache](https://github.com/VictoriaMetrics/fastcache) +- [Ristretto](https://github.com/dgraph-io/ristretto) +- [Badger](https://github.com/dgraph-io/badger) diff --git a/vendor/github.com/cespare/xxhash/v2/testall.sh b/vendor/github.com/cespare/xxhash/v2/testall.sh new file mode 100644 index 00000000..94b9c443 --- /dev/null +++ b/vendor/github.com/cespare/xxhash/v2/testall.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -eu -o pipefail + +# Small convenience script for running the tests with various combinations of +# arch/tags. This assumes we're running on amd64 and have qemu available. + +go test ./... +go test -tags purego ./... +GOARCH=arm64 go test +GOARCH=arm64 go test -tags purego diff --git a/vendor/github.com/cespare/xxhash/v2/xxhash.go b/vendor/github.com/cespare/xxhash/v2/xxhash.go new file mode 100644 index 00000000..78bddf1c --- /dev/null +++ b/vendor/github.com/cespare/xxhash/v2/xxhash.go @@ -0,0 +1,243 @@ +// Package xxhash implements the 64-bit variant of xxHash (XXH64) as described +// at http://cyan4973.github.io/xxHash/. +package xxhash + +import ( + "encoding/binary" + "errors" + "math/bits" +) + +const ( + prime1 uint64 = 11400714785074694791 + prime2 uint64 = 14029467366897019727 + prime3 uint64 = 1609587929392839161 + prime4 uint64 = 9650029242287828579 + prime5 uint64 = 2870177450012600261 +) + +// Store the primes in an array as well. +// +// The consts are used when possible in Go code to avoid MOVs but we need a +// contiguous array for the assembly code. +var primes = [...]uint64{prime1, prime2, prime3, prime4, prime5} + +// Digest implements hash.Hash64. +// +// Note that a zero-valued Digest is not ready to receive writes. +// Call Reset or create a Digest using New before calling other methods. +type Digest struct { + v1 uint64 + v2 uint64 + v3 uint64 + v4 uint64 + total uint64 + mem [32]byte + n int // how much of mem is used +} + +// New creates a new Digest with a zero seed. +func New() *Digest { + return NewWithSeed(0) +} + +// NewWithSeed creates a new Digest with the given seed. +func NewWithSeed(seed uint64) *Digest { + var d Digest + d.ResetWithSeed(seed) + return &d +} + +// Reset clears the Digest's state so that it can be reused. +// It uses a seed value of zero. +func (d *Digest) Reset() { + d.ResetWithSeed(0) +} + +// ResetWithSeed clears the Digest's state so that it can be reused. +// It uses the given seed to initialize the state. +func (d *Digest) ResetWithSeed(seed uint64) { + d.v1 = seed + prime1 + prime2 + d.v2 = seed + prime2 + d.v3 = seed + d.v4 = seed - prime1 + d.total = 0 + d.n = 0 +} + +// Size always returns 8 bytes. +func (d *Digest) Size() int { return 8 } + +// BlockSize always returns 32 bytes. +func (d *Digest) BlockSize() int { return 32 } + +// Write adds more data to d. It always returns len(b), nil. +func (d *Digest) Write(b []byte) (n int, err error) { + n = len(b) + d.total += uint64(n) + + memleft := d.mem[d.n&(len(d.mem)-1):] + + if d.n+n < 32 { + // This new data doesn't even fill the current block. + copy(memleft, b) + d.n += n + return + } + + if d.n > 0 { + // Finish off the partial block. + c := copy(memleft, b) + d.v1 = round(d.v1, u64(d.mem[0:8])) + d.v2 = round(d.v2, u64(d.mem[8:16])) + d.v3 = round(d.v3, u64(d.mem[16:24])) + d.v4 = round(d.v4, u64(d.mem[24:32])) + b = b[c:] + d.n = 0 + } + + if len(b) >= 32 { + // One or more full blocks left. + nw := writeBlocks(d, b) + b = b[nw:] + } + + // Store any remaining partial block. + copy(d.mem[:], b) + d.n = len(b) + + return +} + +// Sum appends the current hash to b and returns the resulting slice. +func (d *Digest) Sum(b []byte) []byte { + s := d.Sum64() + return append( + b, + byte(s>>56), + byte(s>>48), + byte(s>>40), + byte(s>>32), + byte(s>>24), + byte(s>>16), + byte(s>>8), + byte(s), + ) +} + +// Sum64 returns the current hash. +func (d *Digest) Sum64() uint64 { + var h uint64 + + if d.total >= 32 { + v1, v2, v3, v4 := d.v1, d.v2, d.v3, d.v4 + h = rol1(v1) + rol7(v2) + rol12(v3) + rol18(v4) + h = mergeRound(h, v1) + h = mergeRound(h, v2) + h = mergeRound(h, v3) + h = mergeRound(h, v4) + } else { + h = d.v3 + prime5 + } + + h += d.total + + b := d.mem[:d.n&(len(d.mem)-1)] + for ; len(b) >= 8; b = b[8:] { + k1 := round(0, u64(b[:8])) + h ^= k1 + h = rol27(h)*prime1 + prime4 + } + if len(b) >= 4 { + h ^= uint64(u32(b[:4])) * prime1 + h = rol23(h)*prime2 + prime3 + b = b[4:] + } + for ; len(b) > 0; b = b[1:] { + h ^= uint64(b[0]) * prime5 + h = rol11(h) * prime1 + } + + h ^= h >> 33 + h *= prime2 + h ^= h >> 29 + h *= prime3 + h ^= h >> 32 + + return h +} + +const ( + magic = "xxh\x06" + marshaledSize = len(magic) + 8*5 + 32 +) + +// MarshalBinary implements the encoding.BinaryMarshaler interface. +func (d *Digest) MarshalBinary() ([]byte, error) { + b := make([]byte, 0, marshaledSize) + b = append(b, magic...) + b = appendUint64(b, d.v1) + b = appendUint64(b, d.v2) + b = appendUint64(b, d.v3) + b = appendUint64(b, d.v4) + b = appendUint64(b, d.total) + b = append(b, d.mem[:d.n]...) + b = b[:len(b)+len(d.mem)-d.n] + return b, nil +} + +// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. +func (d *Digest) UnmarshalBinary(b []byte) error { + if len(b) < len(magic) || string(b[:len(magic)]) != magic { + return errors.New("xxhash: invalid hash state identifier") + } + if len(b) != marshaledSize { + return errors.New("xxhash: invalid hash state size") + } + b = b[len(magic):] + b, d.v1 = consumeUint64(b) + b, d.v2 = consumeUint64(b) + b, d.v3 = consumeUint64(b) + b, d.v4 = consumeUint64(b) + b, d.total = consumeUint64(b) + copy(d.mem[:], b) + d.n = int(d.total % uint64(len(d.mem))) + return nil +} + +func appendUint64(b []byte, x uint64) []byte { + var a [8]byte + binary.LittleEndian.PutUint64(a[:], x) + return append(b, a[:]...) +} + +func consumeUint64(b []byte) ([]byte, uint64) { + x := u64(b) + return b[8:], x +} + +func u64(b []byte) uint64 { return binary.LittleEndian.Uint64(b) } +func u32(b []byte) uint32 { return binary.LittleEndian.Uint32(b) } + +func round(acc, input uint64) uint64 { + acc += input * prime2 + acc = rol31(acc) + acc *= prime1 + return acc +} + +func mergeRound(acc, val uint64) uint64 { + val = round(0, val) + acc ^= val + acc = acc*prime1 + prime4 + return acc +} + +func rol1(x uint64) uint64 { return bits.RotateLeft64(x, 1) } +func rol7(x uint64) uint64 { return bits.RotateLeft64(x, 7) } +func rol11(x uint64) uint64 { return bits.RotateLeft64(x, 11) } +func rol12(x uint64) uint64 { return bits.RotateLeft64(x, 12) } +func rol18(x uint64) uint64 { return bits.RotateLeft64(x, 18) } +func rol23(x uint64) uint64 { return bits.RotateLeft64(x, 23) } +func rol27(x uint64) uint64 { return bits.RotateLeft64(x, 27) } +func rol31(x uint64) uint64 { return bits.RotateLeft64(x, 31) } diff --git a/vendor/github.com/cespare/xxhash/v2/xxhash_amd64.s b/vendor/github.com/cespare/xxhash/v2/xxhash_amd64.s new file mode 100644 index 00000000..3e8b1325 --- /dev/null +++ b/vendor/github.com/cespare/xxhash/v2/xxhash_amd64.s @@ -0,0 +1,209 @@ +//go:build !appengine && gc && !purego +// +build !appengine +// +build gc +// +build !purego + +#include "textflag.h" + +// Registers: +#define h AX +#define d AX +#define p SI // pointer to advance through b +#define n DX +#define end BX // loop end +#define v1 R8 +#define v2 R9 +#define v3 R10 +#define v4 R11 +#define x R12 +#define prime1 R13 +#define prime2 R14 +#define prime4 DI + +#define round(acc, x) \ + IMULQ prime2, x \ + ADDQ x, acc \ + ROLQ $31, acc \ + IMULQ prime1, acc + +// round0 performs the operation x = round(0, x). +#define round0(x) \ + IMULQ prime2, x \ + ROLQ $31, x \ + IMULQ prime1, x + +// mergeRound applies a merge round on the two registers acc and x. +// It assumes that prime1, prime2, and prime4 have been loaded. +#define mergeRound(acc, x) \ + round0(x) \ + XORQ x, acc \ + IMULQ prime1, acc \ + ADDQ prime4, acc + +// blockLoop processes as many 32-byte blocks as possible, +// updating v1, v2, v3, and v4. It assumes that there is at least one block +// to process. +#define blockLoop() \ +loop: \ + MOVQ +0(p), x \ + round(v1, x) \ + MOVQ +8(p), x \ + round(v2, x) \ + MOVQ +16(p), x \ + round(v3, x) \ + MOVQ +24(p), x \ + round(v4, x) \ + ADDQ $32, p \ + CMPQ p, end \ + JLE loop + +// func Sum64(b []byte) uint64 +TEXT ·Sum64(SB), NOSPLIT|NOFRAME, $0-32 + // Load fixed primes. + MOVQ ·primes+0(SB), prime1 + MOVQ ·primes+8(SB), prime2 + MOVQ ·primes+24(SB), prime4 + + // Load slice. + MOVQ b_base+0(FP), p + MOVQ b_len+8(FP), n + LEAQ (p)(n*1), end + + // The first loop limit will be len(b)-32. + SUBQ $32, end + + // Check whether we have at least one block. + CMPQ n, $32 + JLT noBlocks + + // Set up initial state (v1, v2, v3, v4). + MOVQ prime1, v1 + ADDQ prime2, v1 + MOVQ prime2, v2 + XORQ v3, v3 + XORQ v4, v4 + SUBQ prime1, v4 + + blockLoop() + + MOVQ v1, h + ROLQ $1, h + MOVQ v2, x + ROLQ $7, x + ADDQ x, h + MOVQ v3, x + ROLQ $12, x + ADDQ x, h + MOVQ v4, x + ROLQ $18, x + ADDQ x, h + + mergeRound(h, v1) + mergeRound(h, v2) + mergeRound(h, v3) + mergeRound(h, v4) + + JMP afterBlocks + +noBlocks: + MOVQ ·primes+32(SB), h + +afterBlocks: + ADDQ n, h + + ADDQ $24, end + CMPQ p, end + JG try4 + +loop8: + MOVQ (p), x + ADDQ $8, p + round0(x) + XORQ x, h + ROLQ $27, h + IMULQ prime1, h + ADDQ prime4, h + + CMPQ p, end + JLE loop8 + +try4: + ADDQ $4, end + CMPQ p, end + JG try1 + + MOVL (p), x + ADDQ $4, p + IMULQ prime1, x + XORQ x, h + + ROLQ $23, h + IMULQ prime2, h + ADDQ ·primes+16(SB), h + +try1: + ADDQ $4, end + CMPQ p, end + JGE finalize + +loop1: + MOVBQZX (p), x + ADDQ $1, p + IMULQ ·primes+32(SB), x + XORQ x, h + ROLQ $11, h + IMULQ prime1, h + + CMPQ p, end + JL loop1 + +finalize: + MOVQ h, x + SHRQ $33, x + XORQ x, h + IMULQ prime2, h + MOVQ h, x + SHRQ $29, x + XORQ x, h + IMULQ ·primes+16(SB), h + MOVQ h, x + SHRQ $32, x + XORQ x, h + + MOVQ h, ret+24(FP) + RET + +// func writeBlocks(d *Digest, b []byte) int +TEXT ·writeBlocks(SB), NOSPLIT|NOFRAME, $0-40 + // Load fixed primes needed for round. + MOVQ ·primes+0(SB), prime1 + MOVQ ·primes+8(SB), prime2 + + // Load slice. + MOVQ b_base+8(FP), p + MOVQ b_len+16(FP), n + LEAQ (p)(n*1), end + SUBQ $32, end + + // Load vN from d. + MOVQ s+0(FP), d + MOVQ 0(d), v1 + MOVQ 8(d), v2 + MOVQ 16(d), v3 + MOVQ 24(d), v4 + + // We don't need to check the loop condition here; this function is + // always called with at least one block of data to process. + blockLoop() + + // Copy vN back to d. + MOVQ v1, 0(d) + MOVQ v2, 8(d) + MOVQ v3, 16(d) + MOVQ v4, 24(d) + + // The number of bytes written is p minus the old base pointer. + SUBQ b_base+8(FP), p + MOVQ p, ret+32(FP) + + RET diff --git a/vendor/github.com/cespare/xxhash/v2/xxhash_arm64.s b/vendor/github.com/cespare/xxhash/v2/xxhash_arm64.s new file mode 100644 index 00000000..7e3145a2 --- /dev/null +++ b/vendor/github.com/cespare/xxhash/v2/xxhash_arm64.s @@ -0,0 +1,183 @@ +//go:build !appengine && gc && !purego +// +build !appengine +// +build gc +// +build !purego + +#include "textflag.h" + +// Registers: +#define digest R1 +#define h R2 // return value +#define p R3 // input pointer +#define n R4 // input length +#define nblocks R5 // n / 32 +#define prime1 R7 +#define prime2 R8 +#define prime3 R9 +#define prime4 R10 +#define prime5 R11 +#define v1 R12 +#define v2 R13 +#define v3 R14 +#define v4 R15 +#define x1 R20 +#define x2 R21 +#define x3 R22 +#define x4 R23 + +#define round(acc, x) \ + MADD prime2, acc, x, acc \ + ROR $64-31, acc \ + MUL prime1, acc + +// round0 performs the operation x = round(0, x). +#define round0(x) \ + MUL prime2, x \ + ROR $64-31, x \ + MUL prime1, x + +#define mergeRound(acc, x) \ + round0(x) \ + EOR x, acc \ + MADD acc, prime4, prime1, acc + +// blockLoop processes as many 32-byte blocks as possible, +// updating v1, v2, v3, and v4. It assumes that n >= 32. +#define blockLoop() \ + LSR $5, n, nblocks \ + PCALIGN $16 \ + loop: \ + LDP.P 16(p), (x1, x2) \ + LDP.P 16(p), (x3, x4) \ + round(v1, x1) \ + round(v2, x2) \ + round(v3, x3) \ + round(v4, x4) \ + SUB $1, nblocks \ + CBNZ nblocks, loop + +// func Sum64(b []byte) uint64 +TEXT ·Sum64(SB), NOSPLIT|NOFRAME, $0-32 + LDP b_base+0(FP), (p, n) + + LDP ·primes+0(SB), (prime1, prime2) + LDP ·primes+16(SB), (prime3, prime4) + MOVD ·primes+32(SB), prime5 + + CMP $32, n + CSEL LT, prime5, ZR, h // if n < 32 { h = prime5 } else { h = 0 } + BLT afterLoop + + ADD prime1, prime2, v1 + MOVD prime2, v2 + MOVD $0, v3 + NEG prime1, v4 + + blockLoop() + + ROR $64-1, v1, x1 + ROR $64-7, v2, x2 + ADD x1, x2 + ROR $64-12, v3, x3 + ROR $64-18, v4, x4 + ADD x3, x4 + ADD x2, x4, h + + mergeRound(h, v1) + mergeRound(h, v2) + mergeRound(h, v3) + mergeRound(h, v4) + +afterLoop: + ADD n, h + + TBZ $4, n, try8 + LDP.P 16(p), (x1, x2) + + round0(x1) + + // NOTE: here and below, sequencing the EOR after the ROR (using a + // rotated register) is worth a small but measurable speedup for small + // inputs. + ROR $64-27, h + EOR x1 @> 64-27, h, h + MADD h, prime4, prime1, h + + round0(x2) + ROR $64-27, h + EOR x2 @> 64-27, h, h + MADD h, prime4, prime1, h + +try8: + TBZ $3, n, try4 + MOVD.P 8(p), x1 + + round0(x1) + ROR $64-27, h + EOR x1 @> 64-27, h, h + MADD h, prime4, prime1, h + +try4: + TBZ $2, n, try2 + MOVWU.P 4(p), x2 + + MUL prime1, x2 + ROR $64-23, h + EOR x2 @> 64-23, h, h + MADD h, prime3, prime2, h + +try2: + TBZ $1, n, try1 + MOVHU.P 2(p), x3 + AND $255, x3, x1 + LSR $8, x3, x2 + + MUL prime5, x1 + ROR $64-11, h + EOR x1 @> 64-11, h, h + MUL prime1, h + + MUL prime5, x2 + ROR $64-11, h + EOR x2 @> 64-11, h, h + MUL prime1, h + +try1: + TBZ $0, n, finalize + MOVBU (p), x4 + + MUL prime5, x4 + ROR $64-11, h + EOR x4 @> 64-11, h, h + MUL prime1, h + +finalize: + EOR h >> 33, h + MUL prime2, h + EOR h >> 29, h + MUL prime3, h + EOR h >> 32, h + + MOVD h, ret+24(FP) + RET + +// func writeBlocks(d *Digest, b []byte) int +TEXT ·writeBlocks(SB), NOSPLIT|NOFRAME, $0-40 + LDP ·primes+0(SB), (prime1, prime2) + + // Load state. Assume v[1-4] are stored contiguously. + MOVD d+0(FP), digest + LDP 0(digest), (v1, v2) + LDP 16(digest), (v3, v4) + + LDP b_base+8(FP), (p, n) + + blockLoop() + + // Store updated state. + STP (v1, v2), 0(digest) + STP (v3, v4), 16(digest) + + BIC $31, n + MOVD n, ret+32(FP) + RET diff --git a/vendor/github.com/cespare/xxhash/v2/xxhash_asm.go b/vendor/github.com/cespare/xxhash/v2/xxhash_asm.go new file mode 100644 index 00000000..78f95f25 --- /dev/null +++ b/vendor/github.com/cespare/xxhash/v2/xxhash_asm.go @@ -0,0 +1,15 @@ +//go:build (amd64 || arm64) && !appengine && gc && !purego +// +build amd64 arm64 +// +build !appengine +// +build gc +// +build !purego + +package xxhash + +// Sum64 computes the 64-bit xxHash digest of b with a zero seed. +// +//go:noescape +func Sum64(b []byte) uint64 + +//go:noescape +func writeBlocks(d *Digest, b []byte) int diff --git a/vendor/github.com/cespare/xxhash/v2/xxhash_other.go b/vendor/github.com/cespare/xxhash/v2/xxhash_other.go new file mode 100644 index 00000000..118e49e8 --- /dev/null +++ b/vendor/github.com/cespare/xxhash/v2/xxhash_other.go @@ -0,0 +1,76 @@ +//go:build (!amd64 && !arm64) || appengine || !gc || purego +// +build !amd64,!arm64 appengine !gc purego + +package xxhash + +// Sum64 computes the 64-bit xxHash digest of b with a zero seed. +func Sum64(b []byte) uint64 { + // A simpler version would be + // d := New() + // d.Write(b) + // return d.Sum64() + // but this is faster, particularly for small inputs. + + n := len(b) + var h uint64 + + if n >= 32 { + v1 := primes[0] + prime2 + v2 := prime2 + v3 := uint64(0) + v4 := -primes[0] + for len(b) >= 32 { + v1 = round(v1, u64(b[0:8:len(b)])) + v2 = round(v2, u64(b[8:16:len(b)])) + v3 = round(v3, u64(b[16:24:len(b)])) + v4 = round(v4, u64(b[24:32:len(b)])) + b = b[32:len(b):len(b)] + } + h = rol1(v1) + rol7(v2) + rol12(v3) + rol18(v4) + h = mergeRound(h, v1) + h = mergeRound(h, v2) + h = mergeRound(h, v3) + h = mergeRound(h, v4) + } else { + h = prime5 + } + + h += uint64(n) + + for ; len(b) >= 8; b = b[8:] { + k1 := round(0, u64(b[:8])) + h ^= k1 + h = rol27(h)*prime1 + prime4 + } + if len(b) >= 4 { + h ^= uint64(u32(b[:4])) * prime1 + h = rol23(h)*prime2 + prime3 + b = b[4:] + } + for ; len(b) > 0; b = b[1:] { + h ^= uint64(b[0]) * prime5 + h = rol11(h) * prime1 + } + + h ^= h >> 33 + h *= prime2 + h ^= h >> 29 + h *= prime3 + h ^= h >> 32 + + return h +} + +func writeBlocks(d *Digest, b []byte) int { + v1, v2, v3, v4 := d.v1, d.v2, d.v3, d.v4 + n := len(b) + for len(b) >= 32 { + v1 = round(v1, u64(b[0:8:len(b)])) + v2 = round(v2, u64(b[8:16:len(b)])) + v3 = round(v3, u64(b[16:24:len(b)])) + v4 = round(v4, u64(b[24:32:len(b)])) + b = b[32:len(b):len(b)] + } + d.v1, d.v2, d.v3, d.v4 = v1, v2, v3, v4 + return n - len(b) +} diff --git a/vendor/github.com/cespare/xxhash/v2/xxhash_safe.go b/vendor/github.com/cespare/xxhash/v2/xxhash_safe.go new file mode 100644 index 00000000..05f5e7df --- /dev/null +++ b/vendor/github.com/cespare/xxhash/v2/xxhash_safe.go @@ -0,0 +1,16 @@ +//go:build appengine +// +build appengine + +// This file contains the safe implementations of otherwise unsafe-using code. + +package xxhash + +// Sum64String computes the 64-bit xxHash digest of s with a zero seed. +func Sum64String(s string) uint64 { + return Sum64([]byte(s)) +} + +// WriteString adds more data to d. It always returns len(s), nil. +func (d *Digest) WriteString(s string) (n int, err error) { + return d.Write([]byte(s)) +} diff --git a/vendor/github.com/cespare/xxhash/v2/xxhash_unsafe.go b/vendor/github.com/cespare/xxhash/v2/xxhash_unsafe.go new file mode 100644 index 00000000..cf9d42ae --- /dev/null +++ b/vendor/github.com/cespare/xxhash/v2/xxhash_unsafe.go @@ -0,0 +1,58 @@ +//go:build !appengine +// +build !appengine + +// This file encapsulates usage of unsafe. +// xxhash_safe.go contains the safe implementations. + +package xxhash + +import ( + "unsafe" +) + +// In the future it's possible that compiler optimizations will make these +// XxxString functions unnecessary by realizing that calls such as +// Sum64([]byte(s)) don't need to copy s. See https://go.dev/issue/2205. +// If that happens, even if we keep these functions they can be replaced with +// the trivial safe code. + +// NOTE: The usual way of doing an unsafe string-to-[]byte conversion is: +// +// var b []byte +// bh := (*reflect.SliceHeader)(unsafe.Pointer(&b)) +// bh.Data = (*reflect.StringHeader)(unsafe.Pointer(&s)).Data +// bh.Len = len(s) +// bh.Cap = len(s) +// +// Unfortunately, as of Go 1.15.3 the inliner's cost model assigns a high enough +// weight to this sequence of expressions that any function that uses it will +// not be inlined. Instead, the functions below use a different unsafe +// conversion designed to minimize the inliner weight and allow both to be +// inlined. There is also a test (TestInlining) which verifies that these are +// inlined. +// +// See https://github.com/golang/go/issues/42739 for discussion. + +// Sum64String computes the 64-bit xxHash digest of s with a zero seed. +// It may be faster than Sum64([]byte(s)) by avoiding a copy. +func Sum64String(s string) uint64 { + b := *(*[]byte)(unsafe.Pointer(&sliceHeader{s, len(s)})) + return Sum64(b) +} + +// WriteString adds more data to d. It always returns len(s), nil. +// It may be faster than Write([]byte(s)) by avoiding a copy. +func (d *Digest) WriteString(s string) (n int, err error) { + d.Write(*(*[]byte)(unsafe.Pointer(&sliceHeader{s, len(s)}))) + // d.Write always returns len(s), nil. + // Ignoring the return output and returning these fixed values buys a + // savings of 6 in the inliner's cost model. + return len(s), nil +} + +// sliceHeader is similar to reflect.SliceHeader, but it assumes that the layout +// of the first two words is the same as the layout of a string. +type sliceHeader struct { + s string + cap int +} diff --git a/vendor/github.com/containernetworking/cni/libcni/api.go b/vendor/github.com/containernetworking/cni/libcni/api.go index 7e52bd83..6ac26949 100644 --- a/vendor/github.com/containernetworking/cni/libcni/api.go +++ b/vendor/github.com/containernetworking/cni/libcni/api.go @@ -14,23 +14,33 @@ package libcni +// Note this is the actual implementation of the CNI specification, which +// is reflected in the SPEC.md file. +// it is typically bundled into runtime providers (i.e. containerd or cri-o would use this +// before calling runc or hcsshim). It is also bundled into CNI providers as well, for example, +// to add an IP to a container, to parse the configuration of the CNI and so on. + import ( "context" "encoding/json" + "errors" "fmt" - "io/ioutil" "os" "path/filepath" + "sort" "strings" "github.com/containernetworking/cni/pkg/invoke" "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/types/create" "github.com/containernetworking/cni/pkg/utils" "github.com/containernetworking/cni/pkg/version" ) var ( CacheDir = "/var/lib/cni" + // slightly awkward wording to preserve anyone matching on error strings + ErrorCheckNotSupp = fmt.Errorf("does not support the CHECK command") ) const ( @@ -57,17 +67,37 @@ type RuntimeConf struct { CacheDir string } -type NetworkConfig struct { - Network *types.NetConf +// Use PluginConfig instead of NetworkConfig, the NetworkConfig +// backwards-compat alias will be removed in a future release. +type NetworkConfig = PluginConfig + +type PluginConfig struct { + Network *types.PluginConf Bytes []byte } type NetworkConfigList struct { - Name string - CNIVersion string - DisableCheck bool - Plugins []*NetworkConfig - Bytes []byte + Name string + CNIVersion string + DisableCheck bool + DisableGC bool + LoadOnlyInlinedPlugins bool + Plugins []*PluginConfig + Bytes []byte +} + +type NetworkAttachment struct { + ContainerID string + Network string + IfName string + Config []byte + NetNS string + CniArgs [][2]string + CapabilityArgs map[string]interface{} +} + +type GCArgs struct { + ValidAttachments []types.GCAttachment } type CNI interface { @@ -77,14 +107,21 @@ type CNI interface { GetNetworkListCachedResult(net *NetworkConfigList, rt *RuntimeConf) (types.Result, error) GetNetworkListCachedConfig(net *NetworkConfigList, rt *RuntimeConf) ([]byte, *RuntimeConf, error) - AddNetwork(ctx context.Context, net *NetworkConfig, rt *RuntimeConf) (types.Result, error) - CheckNetwork(ctx context.Context, net *NetworkConfig, rt *RuntimeConf) error - DelNetwork(ctx context.Context, net *NetworkConfig, rt *RuntimeConf) error - GetNetworkCachedResult(net *NetworkConfig, rt *RuntimeConf) (types.Result, error) - GetNetworkCachedConfig(net *NetworkConfig, rt *RuntimeConf) ([]byte, *RuntimeConf, error) + AddNetwork(ctx context.Context, net *PluginConfig, rt *RuntimeConf) (types.Result, error) + CheckNetwork(ctx context.Context, net *PluginConfig, rt *RuntimeConf) error + DelNetwork(ctx context.Context, net *PluginConfig, rt *RuntimeConf) error + GetNetworkCachedResult(net *PluginConfig, rt *RuntimeConf) (types.Result, error) + GetNetworkCachedConfig(net *PluginConfig, rt *RuntimeConf) ([]byte, *RuntimeConf, error) ValidateNetworkList(ctx context.Context, net *NetworkConfigList) ([]string, error) - ValidateNetwork(ctx context.Context, net *NetworkConfig) ([]string, error) + ValidateNetwork(ctx context.Context, net *PluginConfig) ([]string, error) + + GCNetworkList(ctx context.Context, net *NetworkConfigList, args *GCArgs) error + GetStatusNetworkList(ctx context.Context, net *NetworkConfigList) error + + GetCachedAttachments(containerID string) ([]*NetworkAttachment, error) + + GetVersionInfo(ctx context.Context, pluginType string) (version.PluginInfo, error) } type CNIConfig struct { @@ -115,7 +152,7 @@ func NewCNIConfigWithCacheDir(path []string, cacheDir string, exec invoke.Exec) } } -func buildOneConfig(name, cniVersion string, orig *NetworkConfig, prevResult types.Result, rt *RuntimeConf) (*NetworkConfig, error) { +func buildOneConfig(name, cniVersion string, orig *PluginConfig, prevResult types.Result, rt *RuntimeConf) (*PluginConfig, error) { var err error inject := map[string]interface{}{ @@ -132,8 +169,11 @@ func buildOneConfig(name, cniVersion string, orig *NetworkConfig, prevResult typ if err != nil { return nil, err } + if rt != nil { + return injectRuntimeConfig(orig, rt) + } - return injectRuntimeConfig(orig, rt) + return orig, nil } // This function takes a libcni RuntimeConf structure and injects values into @@ -148,7 +188,7 @@ func buildOneConfig(name, cniVersion string, orig *NetworkConfig, prevResult typ // capabilities include "portMappings", and the CapabilityArgs map includes a // "portMappings" key, that key and its value are added to the "runtimeConfig" // dictionary to be passed to the plugin's stdin. -func injectRuntimeConfig(orig *NetworkConfig, rt *RuntimeConf) (*NetworkConfig, error) { +func injectRuntimeConfig(orig *PluginConfig, rt *RuntimeConf) (*PluginConfig, error) { var err error rc := make(map[string]interface{}) @@ -188,6 +228,7 @@ type cachedInfo struct { Config []byte `json:"config"` IfName string `json:"ifName"` NetworkName string `json:"networkName"` + NetNS string `json:"netns,omitempty"` CniArgs [][2]string `json:"cniArgs,omitempty"` CapabilityArgs map[string]interface{} `json:"capabilityArgs,omitempty"` RawResult map[string]interface{} `json:"result,omitempty"` @@ -222,6 +263,7 @@ func (c *CNIConfig) cacheAdd(result types.Result, config []byte, netName string, Config: config, IfName: rt.IfName, NetworkName: netName, + NetNS: rt.NetNS, CniArgs: rt.Args, CapabilityArgs: rt.CapabilityArgs, } @@ -247,11 +289,11 @@ func (c *CNIConfig) cacheAdd(result types.Result, config []byte, netName string, if err != nil { return err } - if err := os.MkdirAll(filepath.Dir(fname), 0700); err != nil { + if err := os.MkdirAll(filepath.Dir(fname), 0o700); err != nil { return err } - return ioutil.WriteFile(fname, newBytes, 0600) + return os.WriteFile(fname, newBytes, 0o600) } func (c *CNIConfig) cacheDel(netName string, rt *RuntimeConf) error { @@ -270,7 +312,7 @@ func (c *CNIConfig) getCachedConfig(netName string, rt *RuntimeConf) ([]byte, *R if err != nil { return nil, nil, err } - bytes, err = ioutil.ReadFile(fname) + bytes, err = os.ReadFile(fname) if err != nil { // Ignore read errors; the cached result may not exist on-disk return nil, nil, nil @@ -278,7 +320,7 @@ func (c *CNIConfig) getCachedConfig(netName string, rt *RuntimeConf) ([]byte, *R unmarshaled := cachedInfo{} if err := json.Unmarshal(bytes, &unmarshaled); err != nil { - return nil, nil, fmt.Errorf("failed to unmarshal cached network %q config: %v", netName, err) + return nil, nil, fmt.Errorf("failed to unmarshal cached network %q config: %w", netName, err) } if unmarshaled.Kind != CNICacheV1 { return nil, nil, fmt.Errorf("read cached network %q config has wrong kind: %v", netName, unmarshaled.Kind) @@ -298,21 +340,14 @@ func (c *CNIConfig) getLegacyCachedResult(netName, cniVersion string, rt *Runtim if err != nil { return nil, err } - data, err := ioutil.ReadFile(fname) + data, err := os.ReadFile(fname) if err != nil { // Ignore read errors; the cached result may not exist on-disk return nil, nil } - // Read the version of the cached result - decoder := version.ConfigDecoder{} - resultCniVersion, err := decoder.Decode(data) - if err != nil { - return nil, err - } - - // Ensure we can understand the result - result, err := version.NewResult(resultCniVersion, data) + // Load the cached result + result, err := create.CreateFromBytes(data) if err != nil { return nil, err } @@ -322,10 +357,10 @@ func (c *CNIConfig) getLegacyCachedResult(netName, cniVersion string, rt *Runtim // should match the config version unless the config was changed // while the container was running. result, err = result.GetAsVersion(cniVersion) - if err != nil && resultCniVersion != cniVersion { - return nil, fmt.Errorf("failed to convert cached result version %q to config version %q: %v", resultCniVersion, cniVersion, err) + if err != nil { + return nil, fmt.Errorf("failed to convert cached result to config version %q: %w", cniVersion, err) } - return result, err + return result, nil } func (c *CNIConfig) getCachedResult(netName, cniVersion string, rt *RuntimeConf) (types.Result, error) { @@ -333,7 +368,7 @@ func (c *CNIConfig) getCachedResult(netName, cniVersion string, rt *RuntimeConf) if err != nil { return nil, err } - fdata, err := ioutil.ReadFile(fname) + fdata, err := os.ReadFile(fname) if err != nil { // Ignore read errors; the cached result may not exist on-disk return nil, nil @@ -346,18 +381,11 @@ func (c *CNIConfig) getCachedResult(netName, cniVersion string, rt *RuntimeConf) newBytes, err := json.Marshal(&cachedInfo.RawResult) if err != nil { - return nil, fmt.Errorf("failed to marshal cached network %q config: %v", netName, err) - } - - // Read the version of the cached result - decoder := version.ConfigDecoder{} - resultCniVersion, err := decoder.Decode(newBytes) - if err != nil { - return nil, err + return nil, fmt.Errorf("failed to marshal cached network %q config: %w", netName, err) } - // Ensure we can understand the result - result, err := version.NewResult(resultCniVersion, newBytes) + // Load the cached result + result, err := create.CreateFromBytes(newBytes) if err != nil { return nil, err } @@ -367,10 +395,10 @@ func (c *CNIConfig) getCachedResult(netName, cniVersion string, rt *RuntimeConf) // should match the config version unless the config was changed // while the container was running. result, err = result.GetAsVersion(cniVersion) - if err != nil && resultCniVersion != cniVersion { - return nil, fmt.Errorf("failed to convert cached result version %q to config version %q: %v", resultCniVersion, cniVersion, err) + if err != nil { + return nil, fmt.Errorf("failed to convert cached result to config version %q: %w", cniVersion, err) } - return result, err + return result, nil } // GetNetworkListCachedResult returns the cached Result of the previous @@ -381,7 +409,7 @@ func (c *CNIConfig) GetNetworkListCachedResult(list *NetworkConfigList, rt *Runt // GetNetworkCachedResult returns the cached Result of the previous // AddNetwork() operation for a network, or an error. -func (c *CNIConfig) GetNetworkCachedResult(net *NetworkConfig, rt *RuntimeConf) (types.Result, error) { +func (c *CNIConfig) GetNetworkCachedResult(net *PluginConfig, rt *RuntimeConf) (types.Result, error) { return c.getCachedResult(net.Network.Name, net.Network.CNIVersion, rt) } @@ -393,11 +421,73 @@ func (c *CNIConfig) GetNetworkListCachedConfig(list *NetworkConfigList, rt *Runt // GetNetworkCachedConfig copies the input RuntimeConf to output // RuntimeConf with fields updated with info from the cached Config. -func (c *CNIConfig) GetNetworkCachedConfig(net *NetworkConfig, rt *RuntimeConf) ([]byte, *RuntimeConf, error) { +func (c *CNIConfig) GetNetworkCachedConfig(net *PluginConfig, rt *RuntimeConf) ([]byte, *RuntimeConf, error) { return c.getCachedConfig(net.Network.Name, rt) } -func (c *CNIConfig) addNetwork(ctx context.Context, name, cniVersion string, net *NetworkConfig, prevResult types.Result, rt *RuntimeConf) (types.Result, error) { +// GetCachedAttachments returns a list of network attachments from the cache. +// The returned list will be filtered by the containerID if the value is not empty. +func (c *CNIConfig) GetCachedAttachments(containerID string) ([]*NetworkAttachment, error) { + dirPath := filepath.Join(c.getCacheDir(&RuntimeConf{}), "results") + entries, err := os.ReadDir(dirPath) + if err != nil { + if os.IsNotExist(err) { + return nil, nil + } + return nil, err + } + + fileNames := make([]string, 0, len(entries)) + for _, e := range entries { + fileNames = append(fileNames, e.Name()) + } + sort.Strings(fileNames) + + attachments := []*NetworkAttachment{} + for _, fname := range fileNames { + if len(containerID) > 0 { + part := fmt.Sprintf("-%s-", containerID) + pos := strings.Index(fname, part) + if pos <= 0 || pos+len(part) >= len(fname) { + continue + } + } + + cacheFile := filepath.Join(dirPath, fname) + bytes, err := os.ReadFile(cacheFile) + if err != nil { + continue + } + + cachedInfo := cachedInfo{} + + if err := json.Unmarshal(bytes, &cachedInfo); err != nil { + continue + } + if cachedInfo.Kind != CNICacheV1 { + continue + } + if len(containerID) > 0 && cachedInfo.ContainerID != containerID { + continue + } + if cachedInfo.IfName == "" || cachedInfo.NetworkName == "" { + continue + } + + attachments = append(attachments, &NetworkAttachment{ + ContainerID: cachedInfo.ContainerID, + Network: cachedInfo.NetworkName, + IfName: cachedInfo.IfName, + Config: cachedInfo.Config, + NetNS: cachedInfo.NetNS, + CniArgs: cachedInfo.CniArgs, + CapabilityArgs: cachedInfo.CapabilityArgs, + }) + } + return attachments, nil +} + +func (c *CNIConfig) addNetwork(ctx context.Context, name, cniVersion string, net *PluginConfig, prevResult types.Result, rt *RuntimeConf) (types.Result, error) { c.ensureExec() pluginPath, err := c.exec.FindInPath(net.Network.Type, c.Path) if err != nil { @@ -428,18 +518,18 @@ func (c *CNIConfig) AddNetworkList(ctx context.Context, list *NetworkConfigList, for _, net := range list.Plugins { result, err = c.addNetwork(ctx, list.Name, list.CNIVersion, net, result, rt) if err != nil { - return nil, err + return nil, fmt.Errorf("plugin %s failed (add): %w", pluginDescription(net.Network), err) } } if err = c.cacheAdd(result, list.Bytes, list.Name, rt); err != nil { - return nil, fmt.Errorf("failed to set network %q cached result: %v", list.Name, err) + return nil, fmt.Errorf("failed to set network %q cached result: %w", list.Name, err) } return result, nil } -func (c *CNIConfig) checkNetwork(ctx context.Context, name, cniVersion string, net *NetworkConfig, prevResult types.Result, rt *RuntimeConf) error { +func (c *CNIConfig) checkNetwork(ctx context.Context, name, cniVersion string, net *PluginConfig, prevResult types.Result, rt *RuntimeConf) error { c.ensureExec() pluginPath, err := c.exec.FindInPath(net.Network.Type, c.Path) if err != nil { @@ -460,7 +550,7 @@ func (c *CNIConfig) CheckNetworkList(ctx context.Context, list *NetworkConfigLis if gtet, err := version.GreaterThanOrEqualTo(list.CNIVersion, "0.4.0"); err != nil { return err } else if !gtet { - return fmt.Errorf("configuration version %q does not support the CHECK command", list.CNIVersion) + return fmt.Errorf("configuration version %q %w", list.CNIVersion, ErrorCheckNotSupp) } if list.DisableCheck { @@ -469,7 +559,7 @@ func (c *CNIConfig) CheckNetworkList(ctx context.Context, list *NetworkConfigLis cachedResult, err := c.getCachedResult(list.Name, list.CNIVersion, rt) if err != nil { - return fmt.Errorf("failed to get network %q cached result: %v", list.Name, err) + return fmt.Errorf("failed to get network %q cached result: %w", list.Name, err) } for _, net := range list.Plugins { @@ -481,7 +571,7 @@ func (c *CNIConfig) CheckNetworkList(ctx context.Context, list *NetworkConfigLis return nil } -func (c *CNIConfig) delNetwork(ctx context.Context, name, cniVersion string, net *NetworkConfig, prevResult types.Result, rt *RuntimeConf) error { +func (c *CNIConfig) delNetwork(ctx context.Context, name, cniVersion string, net *PluginConfig, prevResult types.Result, rt *RuntimeConf) error { c.ensureExec() pluginPath, err := c.exec.FindInPath(net.Network.Type, c.Path) if err != nil { @@ -504,55 +594,69 @@ func (c *CNIConfig) DelNetworkList(ctx context.Context, list *NetworkConfigList, if gtet, err := version.GreaterThanOrEqualTo(list.CNIVersion, "0.4.0"); err != nil { return err } else if gtet { - cachedResult, err = c.getCachedResult(list.Name, list.CNIVersion, rt) - if err != nil { - return fmt.Errorf("failed to get network %q cached result: %v", list.Name, err) + if cachedResult, err = c.getCachedResult(list.Name, list.CNIVersion, rt); err != nil { + _ = c.cacheDel(list.Name, rt) + cachedResult = nil } } for i := len(list.Plugins) - 1; i >= 0; i-- { net := list.Plugins[i] if err := c.delNetwork(ctx, list.Name, list.CNIVersion, net, cachedResult, rt); err != nil { - return err + return fmt.Errorf("plugin %s failed (delete): %w", pluginDescription(net.Network), err) } } + _ = c.cacheDel(list.Name, rt) return nil } +func pluginDescription(net *types.PluginConf) string { + if net == nil { + return "" + } + pluginType := net.Type + out := fmt.Sprintf("type=%q", pluginType) + name := net.Name + if name != "" { + out += fmt.Sprintf(" name=%q", name) + } + return out +} + // AddNetwork executes the plugin with the ADD command -func (c *CNIConfig) AddNetwork(ctx context.Context, net *NetworkConfig, rt *RuntimeConf) (types.Result, error) { +func (c *CNIConfig) AddNetwork(ctx context.Context, net *PluginConfig, rt *RuntimeConf) (types.Result, error) { result, err := c.addNetwork(ctx, net.Network.Name, net.Network.CNIVersion, net, nil, rt) if err != nil { return nil, err } if err = c.cacheAdd(result, net.Bytes, net.Network.Name, rt); err != nil { - return nil, fmt.Errorf("failed to set network %q cached result: %v", net.Network.Name, err) + return nil, fmt.Errorf("failed to set network %q cached result: %w", net.Network.Name, err) } return result, nil } // CheckNetwork executes the plugin with the CHECK command -func (c *CNIConfig) CheckNetwork(ctx context.Context, net *NetworkConfig, rt *RuntimeConf) error { +func (c *CNIConfig) CheckNetwork(ctx context.Context, net *PluginConfig, rt *RuntimeConf) error { // CHECK was added in CNI spec version 0.4.0 and higher if gtet, err := version.GreaterThanOrEqualTo(net.Network.CNIVersion, "0.4.0"); err != nil { return err } else if !gtet { - return fmt.Errorf("configuration version %q does not support the CHECK command", net.Network.CNIVersion) + return fmt.Errorf("configuration version %q %w", net.Network.CNIVersion, ErrorCheckNotSupp) } cachedResult, err := c.getCachedResult(net.Network.Name, net.Network.CNIVersion, rt) if err != nil { - return fmt.Errorf("failed to get network %q cached result: %v", net.Network.Name, err) + return fmt.Errorf("failed to get network %q cached result: %w", net.Network.Name, err) } return c.checkNetwork(ctx, net.Network.Name, net.Network.CNIVersion, net, cachedResult, rt) } // DelNetwork executes the plugin with the DEL command -func (c *CNIConfig) DelNetwork(ctx context.Context, net *NetworkConfig, rt *RuntimeConf) error { +func (c *CNIConfig) DelNetwork(ctx context.Context, net *PluginConfig, rt *RuntimeConf) error { var cachedResult types.Result // Cached result on DEL was added in CNI spec version 0.4.0 and higher @@ -561,7 +665,7 @@ func (c *CNIConfig) DelNetwork(ctx context.Context, net *NetworkConfig, rt *Runt } else if gtet { cachedResult, err = c.getCachedResult(net.Network.Name, net.Network.CNIVersion, rt) if err != nil { - return fmt.Errorf("failed to get network %q cached result: %v", net.Network.Name, err) + return fmt.Errorf("failed to get network %q cached result: %w", net.Network.Name, err) } } @@ -612,7 +716,7 @@ func (c *CNIConfig) ValidateNetworkList(ctx context.Context, list *NetworkConfig // ValidateNetwork checks that a configuration is reasonably valid. // It uses the same logic as ValidateNetworkList) // Returns a list of capabilities -func (c *CNIConfig) ValidateNetwork(ctx context.Context, net *NetworkConfig) ([]string, error) { +func (c *CNIConfig) ValidateNetwork(ctx context.Context, net *PluginConfig) ([]string, error) { caps := []string{} for c, ok := range net.Network.Capabilities { if ok { @@ -660,6 +764,129 @@ func (c *CNIConfig) GetVersionInfo(ctx context.Context, pluginType string) (vers return invoke.GetVersionInfo(ctx, pluginPath, c.exec) } +// GCNetworkList will do two things +// - dump the list of cached attachments, and issue deletes as necessary +// - issue a GC to the underlying plugins (if the version is high enough) +func (c *CNIConfig) GCNetworkList(ctx context.Context, list *NetworkConfigList, args *GCArgs) error { + // If DisableGC is set, then don't bother GCing at all. + if list.DisableGC { + return nil + } + + // First, get the list of cached attachments + cachedAttachments, err := c.GetCachedAttachments("") + if err != nil { + return nil + } + + var validAttachments map[types.GCAttachment]interface{} + if args != nil { + validAttachments = make(map[types.GCAttachment]interface{}, len(args.ValidAttachments)) + for _, a := range args.ValidAttachments { + validAttachments[a] = nil + } + } + + var errs []error + + for _, cachedAttachment := range cachedAttachments { + if cachedAttachment.Network != list.Name { + continue + } + // we found this attachment + gca := types.GCAttachment{ + ContainerID: cachedAttachment.ContainerID, + IfName: cachedAttachment.IfName, + } + if _, ok := validAttachments[gca]; ok { + continue + } + // otherwise, this attachment wasn't valid and we should issue a CNI DEL + rt := RuntimeConf{ + ContainerID: cachedAttachment.ContainerID, + NetNS: cachedAttachment.NetNS, + IfName: cachedAttachment.IfName, + Args: cachedAttachment.CniArgs, + CapabilityArgs: cachedAttachment.CapabilityArgs, + } + if err := c.DelNetworkList(ctx, list, &rt); err != nil { + errs = append(errs, fmt.Errorf("failed to delete stale attachment %s %s: %w", rt.ContainerID, rt.IfName, err)) + } + } + + // now, if the version supports it, issue a GC + if gt, _ := version.GreaterThanOrEqualTo(list.CNIVersion, "1.1.0"); gt { + inject := map[string]interface{}{ + "name": list.Name, + "cniVersion": list.CNIVersion, + } + if args != nil { + inject["cni.dev/valid-attachments"] = args.ValidAttachments + // #1101: spec used incorrect variable name + inject["cni.dev/attachments"] = args.ValidAttachments + } + + for _, plugin := range list.Plugins { + // build config here + pluginConfig, err := InjectConf(plugin, inject) + if err != nil { + errs = append(errs, fmt.Errorf("failed to generate configuration to GC plugin %s: %w", plugin.Network.Type, err)) + } + if err := c.gcNetwork(ctx, pluginConfig); err != nil { + errs = append(errs, fmt.Errorf("failed to GC plugin %s: %w", plugin.Network.Type, err)) + } + } + } + + return errors.Join(errs...) +} + +func (c *CNIConfig) gcNetwork(ctx context.Context, net *PluginConfig) error { + c.ensureExec() + pluginPath, err := c.exec.FindInPath(net.Network.Type, c.Path) + if err != nil { + return err + } + args := c.args("GC", &RuntimeConf{}) + + return invoke.ExecPluginWithoutResult(ctx, pluginPath, net.Bytes, args, c.exec) +} + +func (c *CNIConfig) GetStatusNetworkList(ctx context.Context, list *NetworkConfigList) error { + // If the version doesn't support status, abort. + if gt, _ := version.GreaterThanOrEqualTo(list.CNIVersion, "1.1.0"); !gt { + return nil + } + + inject := map[string]interface{}{ + "name": list.Name, + "cniVersion": list.CNIVersion, + } + + for _, plugin := range list.Plugins { + // build config here + pluginConfig, err := InjectConf(plugin, inject) + if err != nil { + return fmt.Errorf("failed to generate configuration to get plugin STATUS %s: %w", plugin.Network.Type, err) + } + if err := c.getStatusNetwork(ctx, pluginConfig); err != nil { + return err // Don't collect errors here, so we return a clean error code. + } + } + return nil +} + +func (c *CNIConfig) getStatusNetwork(ctx context.Context, net *PluginConfig) error { + c.ensureExec() + pluginPath, err := c.exec.FindInPath(net.Network.Type, c.Path) + if err != nil { + return err + } + args := c.args("STATUS", &RuntimeConf{}) + + return invoke.ExecPluginWithoutResult(ctx, pluginPath, net.Bytes, args, c.exec) +} + // ===== func (c *CNIConfig) args(action string, rt *RuntimeConf) *invoke.Args { return &invoke.Args{ diff --git a/vendor/github.com/containernetworking/cni/libcni/conf.go b/vendor/github.com/containernetworking/cni/libcni/conf.go index d8920cf8..7f8482e7 100644 --- a/vendor/github.com/containernetworking/cni/libcni/conf.go +++ b/vendor/github.com/containernetworking/cni/libcni/conf.go @@ -16,11 +16,16 @@ package libcni import ( "encoding/json" + "errors" "fmt" - "io/ioutil" "os" "path/filepath" + "slices" "sort" + "strings" + + "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/version" ) type NotFoundError struct { @@ -40,10 +45,17 @@ func (e NoConfigsFoundError) Error() string { return fmt.Sprintf(`no net configurations found in %s`, e.Dir) } -func ConfFromBytes(bytes []byte) (*NetworkConfig, error) { - conf := &NetworkConfig{Bytes: bytes} - if err := json.Unmarshal(bytes, &conf.Network); err != nil { - return nil, fmt.Errorf("error parsing configuration: %s", err) +// This will not validate that the plugins actually belong to the netconfig by ensuring +// that they are loaded from a directory named after the networkName, relative to the network config. +// +// Since here we are just accepting raw bytes, the caller is responsible for ensuring that the plugin +// config provided here actually "belongs" to the networkconfig in question. +func NetworkPluginConfFromBytes(pluginConfBytes []byte) (*PluginConfig, error) { + // TODO why are we creating a struct that holds both the byte representation and the deserialized + // representation, and returning that, instead of just returning the deserialized representation? + conf := &PluginConfig{Bytes: pluginConfBytes, Network: &types.PluginConf{}} + if err := json.Unmarshal(pluginConfBytes, conf.Network); err != nil { + return nil, fmt.Errorf("error parsing configuration: %w", err) } if conf.Network.Type == "" { return nil, fmt.Errorf("error parsing configuration: missing 'type'") @@ -51,18 +63,36 @@ func ConfFromBytes(bytes []byte) (*NetworkConfig, error) { return conf, nil } -func ConfFromFile(filename string) (*NetworkConfig, error) { - bytes, err := ioutil.ReadFile(filename) +// Given a path to a directory containing a network configuration, and the name of a network, +// loads all plugin definitions found at path `networkConfPath/networkName/*.conf` +func NetworkPluginConfsFromFiles(networkConfPath, networkName string) ([]*PluginConfig, error) { + var pConfs []*PluginConfig + + pluginConfPath := filepath.Join(networkConfPath, networkName) + + pluginConfFiles, err := ConfFiles(pluginConfPath, []string{".conf"}) if err != nil { - return nil, fmt.Errorf("error reading %s: %s", filename, err) + return nil, fmt.Errorf("failed to read plugin config files in %s: %w", pluginConfPath, err) } - return ConfFromBytes(bytes) + + for _, pluginConfFile := range pluginConfFiles { + pluginConfBytes, err := os.ReadFile(pluginConfFile) + if err != nil { + return nil, fmt.Errorf("error reading %s: %w", pluginConfFile, err) + } + pluginConf, err := NetworkPluginConfFromBytes(pluginConfBytes) + if err != nil { + return nil, err + } + pConfs = append(pConfs, pluginConf) + } + return pConfs, nil } -func ConfListFromBytes(bytes []byte) (*NetworkConfigList, error) { +func NetworkConfFromBytes(confBytes []byte) (*NetworkConfigList, error) { rawList := make(map[string]interface{}) - if err := json.Unmarshal(bytes, &rawList); err != nil { - return nil, fmt.Errorf("error parsing configuration list: %s", err) + if err := json.Unmarshal(confBytes, &rawList); err != nil { + return nil, fmt.Errorf("error parsing configuration list: %w", err) } rawName, ok := rawList["name"] @@ -83,26 +113,115 @@ func ConfListFromBytes(bytes []byte) (*NetworkConfigList, error) { } } - disableCheck := false - if rawDisableCheck, ok := rawList["disableCheck"]; ok { - disableCheck, ok = rawDisableCheck.(bool) + rawVersions, ok := rawList["cniVersions"] + if ok { + // Parse the current package CNI version + rvs, ok := rawVersions.([]interface{}) + if !ok { + return nil, fmt.Errorf("error parsing configuration list: invalid type for cniVersions: %T", rvs) + } + vs := make([]string, 0, len(rvs)) + for i, rv := range rvs { + v, ok := rv.(string) + if !ok { + return nil, fmt.Errorf("error parsing configuration list: invalid type for cniVersions index %d: %T", i, rv) + } + gt, err := version.GreaterThan(v, version.Current()) + if err != nil { + return nil, fmt.Errorf("error parsing configuration list: invalid cniVersions entry %s at index %d: %w", v, i, err) + } else if !gt { + // Skip versions "greater" than this implementation of the spec + vs = append(vs, v) + } + } + + // if cniVersion was already set, append it to the list for sorting. + if cniVersion != "" { + gt, err := version.GreaterThan(cniVersion, version.Current()) + if err != nil { + return nil, fmt.Errorf("error parsing configuration list: invalid cniVersion %s: %w", cniVersion, err) + } else if !gt { + // ignore any versions higher than the current implemented spec version + vs = append(vs, cniVersion) + } + } + slices.SortFunc[[]string](vs, func(v1, v2 string) int { + if v1 == v2 { + return 0 + } + if gt, _ := version.GreaterThan(v1, v2); gt { + return 1 + } + return -1 + }) + if len(vs) > 0 { + cniVersion = vs[len(vs)-1] + } + } + + readBool := func(key string) (bool, error) { + rawVal, ok := rawList[key] + if !ok { + return false, nil + } + if b, ok := rawVal.(bool); ok { + return b, nil + } + + s, ok := rawVal.(string) if !ok { - return nil, fmt.Errorf("error parsing configuration list: invalid disableCheck type %T", rawDisableCheck) + return false, fmt.Errorf("error parsing configuration list: invalid type %T for %s", rawVal, key) } + s = strings.ToLower(s) + switch s { + case "false": + return false, nil + case "true": + return true, nil + } + return false, fmt.Errorf("error parsing configuration list: invalid value %q for %s", s, key) + } + + disableCheck, err := readBool("disableCheck") + if err != nil { + return nil, err + } + + disableGC, err := readBool("disableGC") + if err != nil { + return nil, err + } + + loadOnlyInlinedPlugins, err := readBool("loadOnlyInlinedPlugins") + if err != nil { + return nil, err } list := &NetworkConfigList{ - Name: name, - DisableCheck: disableCheck, - CNIVersion: cniVersion, - Bytes: bytes, + Name: name, + DisableCheck: disableCheck, + DisableGC: disableGC, + LoadOnlyInlinedPlugins: loadOnlyInlinedPlugins, + CNIVersion: cniVersion, + Bytes: confBytes, } var plugins []interface{} plug, ok := rawList["plugins"] - if !ok { - return nil, fmt.Errorf("error parsing configuration list: no 'plugins' key") + // We can have a `plugins` list key in the main conf, + // We can also have `loadOnlyInlinedPlugins == true` + // + // If `plugins` is there, then `loadOnlyInlinedPlugins` can be true + // + // If plugins is NOT there, then `loadOnlyInlinedPlugins` cannot be true + // + // We have to have at least some plugins. + if !ok && loadOnlyInlinedPlugins { + return nil, fmt.Errorf("error parsing configuration list: `loadOnlyInlinedPlugins` is true, and no 'plugins' key") + } else if !ok && !loadOnlyInlinedPlugins { + return list, nil } + plugins, ok = plug.([]interface{}) if !ok { return nil, fmt.Errorf("error parsing configuration list: invalid 'plugins' type %T", plug) @@ -114,32 +233,76 @@ func ConfListFromBytes(bytes []byte) (*NetworkConfigList, error) { for i, conf := range plugins { newBytes, err := json.Marshal(conf) if err != nil { - return nil, fmt.Errorf("failed to marshal plugin config %d: %v", i, err) + return nil, fmt.Errorf("failed to marshal plugin config %d: %w", i, err) } netConf, err := ConfFromBytes(newBytes) if err != nil { - return nil, fmt.Errorf("failed to parse plugin config %d: %v", i, err) + return nil, fmt.Errorf("failed to parse plugin config %d: %w", i, err) } list.Plugins = append(list.Plugins, netConf) } - return list, nil } -func ConfListFromFile(filename string) (*NetworkConfigList, error) { - bytes, err := ioutil.ReadFile(filename) +func NetworkConfFromFile(filename string) (*NetworkConfigList, error) { + bytes, err := os.ReadFile(filename) + if err != nil { + return nil, fmt.Errorf("error reading %s: %w", filename, err) + } + + conf, err := NetworkConfFromBytes(bytes) + if err != nil { + return nil, err + } + + if !conf.LoadOnlyInlinedPlugins { + plugins, err := NetworkPluginConfsFromFiles(filepath.Dir(filename), conf.Name) + if err != nil { + return nil, err + } + conf.Plugins = append(conf.Plugins, plugins...) + } + + if len(conf.Plugins) == 0 { + // Having 0 plugins for a given network is not necessarily a problem, + // but return as error for caller to decide, since they tried to load + return nil, fmt.Errorf("no plugin configs found") + } + return conf, nil +} + +// Deprecated: This file format is no longer supported, use NetworkConfXXX and NetworkPluginXXX functions +func ConfFromBytes(bytes []byte) (*NetworkConfig, error) { + return NetworkPluginConfFromBytes(bytes) +} + +// Deprecated: This file format is no longer supported, use NetworkConfXXX and NetworkPluginXXX functions +func ConfFromFile(filename string) (*NetworkConfig, error) { + bytes, err := os.ReadFile(filename) if err != nil { - return nil, fmt.Errorf("error reading %s: %s", filename, err) + return nil, fmt.Errorf("error reading %s: %w", filename, err) } - return ConfListFromBytes(bytes) + return ConfFromBytes(bytes) +} + +func ConfListFromBytes(bytes []byte) (*NetworkConfigList, error) { + return NetworkConfFromBytes(bytes) +} + +func ConfListFromFile(filename string) (*NetworkConfigList, error) { + return NetworkConfFromFile(filename) } +// ConfFiles simply returns a slice of all files in the provided directory +// with extensions matching the provided set. func ConfFiles(dir string, extensions []string) ([]string, error) { // In part, adapted from rkt/networking/podenv.go#listFiles - files, err := ioutil.ReadDir(dir) + files, err := os.ReadDir(dir) switch { case err == nil: // break case os.IsNotExist(err): + // If folder not there, return no error - only return an + // error if we cannot read contents or there are no contents. return nil, nil default: return nil, err @@ -160,6 +323,7 @@ func ConfFiles(dir string, extensions []string) ([]string, error) { return confFiles, nil } +// Deprecated: This file format is no longer supported, use NetworkConfXXX and NetworkPluginXXX functions func LoadConf(dir, name string) (*NetworkConfig, error) { files, err := ConfFiles(dir, []string{".conf", ".json"}) switch { @@ -183,6 +347,15 @@ func LoadConf(dir, name string) (*NetworkConfig, error) { } func LoadConfList(dir, name string) (*NetworkConfigList, error) { + return LoadNetworkConf(dir, name) +} + +// LoadNetworkConf looks at all the network configs in a given dir, +// loads and parses them all, and returns the first one with an extension of `.conf` +// that matches the provided network name predicate. +func LoadNetworkConf(dir, name string) (*NetworkConfigList, error) { + // TODO this .conflist/.conf extension thing is confusing and inexact + // for implementors. We should pick one extension for everything and stick with it. files, err := ConfFiles(dir, []string{".conflist"}) if err != nil { return nil, err @@ -190,7 +363,7 @@ func LoadConfList(dir, name string) (*NetworkConfigList, error) { sort.Strings(files) for _, confFile := range files { - conf, err := ConfListFromFile(confFile) + conf, err := NetworkConfFromFile(confFile) if err != nil { return nil, err } @@ -199,12 +372,13 @@ func LoadConfList(dir, name string) (*NetworkConfigList, error) { } } - // Try and load a network configuration file (instead of list) + // Deprecated: Try and load a network configuration file (instead of list) // from the same name, then upconvert. singleConf, err := LoadConf(dir, name) if err != nil { // A little extra logic so the error makes sense - if _, ok := err.(NoConfigsFoundError); len(files) != 0 && ok { + var ncfErr NoConfigsFoundError + if len(files) != 0 && errors.As(err, &ncfErr) { // Config lists found but no config files found return nil, NotFoundError{dir, name} } @@ -214,11 +388,12 @@ func LoadConfList(dir, name string) (*NetworkConfigList, error) { return ConfListFromConf(singleConf) } -func InjectConf(original *NetworkConfig, newValues map[string]interface{}) (*NetworkConfig, error) { +// InjectConf takes a PluginConfig and inserts additional values into it, ensuring the result is serializable. +func InjectConf(original *PluginConfig, newValues map[string]interface{}) (*PluginConfig, error) { config := make(map[string]interface{}) err := json.Unmarshal(original.Bytes, &config) if err != nil { - return nil, fmt.Errorf("unmarshal existing network bytes: %s", err) + return nil, fmt.Errorf("unmarshal existing network bytes: %w", err) } for key, value := range newValues { @@ -238,12 +413,14 @@ func InjectConf(original *NetworkConfig, newValues map[string]interface{}) (*Net return nil, err } - return ConfFromBytes(newBytes) + return NetworkPluginConfFromBytes(newBytes) } // ConfListFromConf "upconverts" a network config in to a NetworkConfigList, // with the single network as the only entry in the list. -func ConfListFromConf(original *NetworkConfig) (*NetworkConfigList, error) { +// +// Deprecated: Non-conflist file formats are unsupported, use NetworkConfXXX and NetworkPluginXXX functions +func ConfListFromConf(original *PluginConfig) (*NetworkConfigList, error) { // Re-deserialize the config's json, then make a raw map configlist. // This may seem a bit strange, but it's to make the Bytes fields // actually make sense. Otherwise, the generated json is littered with diff --git a/vendor/github.com/containernetworking/cni/pkg/invoke/delegate.go b/vendor/github.com/containernetworking/cni/pkg/invoke/delegate.go index 8defe4dd..c8b548e7 100644 --- a/vendor/github.com/containernetworking/cni/pkg/invoke/delegate.go +++ b/vendor/github.com/containernetworking/cni/pkg/invoke/delegate.go @@ -51,25 +51,34 @@ func DelegateAdd(ctx context.Context, delegatePlugin string, netconf []byte, exe // DelegateCheck calls the given delegate plugin with the CNI CHECK action and // JSON configuration func DelegateCheck(ctx context.Context, delegatePlugin string, netconf []byte, exec Exec) error { + return delegateNoResult(ctx, delegatePlugin, netconf, exec, "CHECK") +} + +func delegateNoResult(ctx context.Context, delegatePlugin string, netconf []byte, exec Exec, verb string) error { pluginPath, realExec, err := delegateCommon(delegatePlugin, exec) if err != nil { return err } - // DelegateCheck will override the original CNI_COMMAND env from process with CHECK - return ExecPluginWithoutResult(ctx, pluginPath, netconf, delegateArgs("CHECK"), realExec) + return ExecPluginWithoutResult(ctx, pluginPath, netconf, delegateArgs(verb), realExec) } // DelegateDel calls the given delegate plugin with the CNI DEL action and // JSON configuration func DelegateDel(ctx context.Context, delegatePlugin string, netconf []byte, exec Exec) error { - pluginPath, realExec, err := delegateCommon(delegatePlugin, exec) - if err != nil { - return err - } + return delegateNoResult(ctx, delegatePlugin, netconf, exec, "DEL") +} - // DelegateDel will override the original CNI_COMMAND env from process with DEL - return ExecPluginWithoutResult(ctx, pluginPath, netconf, delegateArgs("DEL"), realExec) +// DelegateStatus calls the given delegate plugin with the CNI STATUS action and +// JSON configuration +func DelegateStatus(ctx context.Context, delegatePlugin string, netconf []byte, exec Exec) error { + return delegateNoResult(ctx, delegatePlugin, netconf, exec, "STATUS") +} + +// DelegateGC calls the given delegate plugin with the CNI GC action and +// JSON configuration +func DelegateGC(ctx context.Context, delegatePlugin string, netconf []byte, exec Exec) error { + return delegateNoResult(ctx, delegatePlugin, netconf, exec, "GC") } // return CNIArgs used by delegation diff --git a/vendor/github.com/containernetworking/cni/pkg/invoke/exec.go b/vendor/github.com/containernetworking/cni/pkg/invoke/exec.go index 8e6d30b8..a5e015fc 100644 --- a/vendor/github.com/containernetworking/cni/pkg/invoke/exec.go +++ b/vendor/github.com/containernetworking/cni/pkg/invoke/exec.go @@ -16,10 +16,12 @@ package invoke import ( "context" + "encoding/json" "fmt" "os" "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/types/create" "github.com/containernetworking/cni/pkg/version" ) @@ -32,21 +34,64 @@ type Exec interface { Decode(jsonBytes []byte) (version.PluginInfo, error) } +// Plugin must return result in same version as specified in netconf; but +// for backwards compatibility reasons if the result version is empty use +// config version (rather than technically correct 0.1.0). +// https://github.com/containernetworking/cni/issues/895 +func fixupResultVersion(netconf, result []byte) (string, []byte, error) { + versionDecoder := &version.ConfigDecoder{} + confVersion, err := versionDecoder.Decode(netconf) + if err != nil { + return "", nil, err + } + + var rawResult map[string]interface{} + if err := json.Unmarshal(result, &rawResult); err != nil { + return "", nil, fmt.Errorf("failed to unmarshal raw result: %w", err) + } + + // plugin output of "null" is successfully unmarshalled, but results in a nil + // map which causes a panic when the confVersion is assigned below. + if rawResult == nil { + rawResult = make(map[string]interface{}) + } + + // Manually decode Result version; we need to know whether its cniVersion + // is empty, while built-in decoders (correctly) substitute 0.1.0 for an + // empty version per the CNI spec. + if resultVerRaw, ok := rawResult["cniVersion"]; ok { + resultVer, ok := resultVerRaw.(string) + if ok && resultVer != "" { + return resultVer, result, nil + } + } + + // If the cniVersion is not present or empty, assume the result is + // the same CNI spec version as the config + rawResult["cniVersion"] = confVersion + newBytes, err := json.Marshal(rawResult) + if err != nil { + return "", nil, fmt.Errorf("failed to remarshal fixed result: %w", err) + } + + return confVersion, newBytes, nil +} + // For example, a testcase could pass an instance of the following fakeExec // object to ExecPluginWithResult() to verify the incoming stdin and environment // and provide a tailored response: // -//import ( +// import ( // "encoding/json" // "path" // "strings" -//) +// ) // -//type fakeExec struct { +// type fakeExec struct { // version.PluginDecoder -//} +// } // -//func (f *fakeExec) ExecPlugin(pluginPath string, stdinData []byte, environ []string) ([]byte, error) { +// func (f *fakeExec) ExecPlugin(pluginPath string, stdinData []byte, environ []string) ([]byte, error) { // net := &types.NetConf{} // err := json.Unmarshal(stdinData, net) // if err != nil { @@ -64,14 +109,14 @@ type Exec interface { // } // } // return []byte("{\"CNIVersion\":\"0.4.0\"}"), nil -//} +// } // -//func (f *fakeExec) FindInPath(plugin string, paths []string) (string, error) { +// func (f *fakeExec) FindInPath(plugin string, paths []string) (string, error) { // if len(paths) > 0 { // return path.Join(paths[0], plugin), nil // } // return "", fmt.Errorf("failed to find plugin %s in paths %v", plugin, paths) -//} +// } func ExecPluginWithResult(ctx context.Context, pluginPath string, netconf []byte, args CNIArgs, exec Exec) (types.Result, error) { if exec == nil { @@ -83,14 +128,12 @@ func ExecPluginWithResult(ctx context.Context, pluginPath string, netconf []byte return nil, err } - // Plugin must return result in same version as specified in netconf - versionDecoder := &version.ConfigDecoder{} - confVersion, err := versionDecoder.Decode(netconf) + resultVersion, fixedBytes, err := fixupResultVersion(netconf, stdoutBytes) if err != nil { return nil, err } - return version.NewResult(confVersion, stdoutBytes) + return create.Create(resultVersion, fixedBytes) } func ExecPluginWithoutResult(ctx context.Context, pluginPath string, netconf []byte, args CNIArgs, exec Exec) error { diff --git a/vendor/github.com/containernetworking/cni/pkg/invoke/os_unix.go b/vendor/github.com/containernetworking/cni/pkg/invoke/os_unix.go index 9bcfb455..ed0999bd 100644 --- a/vendor/github.com/containernetworking/cni/pkg/invoke/os_unix.go +++ b/vendor/github.com/containernetworking/cni/pkg/invoke/os_unix.go @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris // +build darwin dragonfly freebsd linux netbsd openbsd solaris package invoke diff --git a/vendor/github.com/containernetworking/cni/pkg/ns/ns_darwin.go b/vendor/github.com/containernetworking/cni/pkg/ns/ns_darwin.go new file mode 100644 index 00000000..cffe1361 --- /dev/null +++ b/vendor/github.com/containernetworking/cni/pkg/ns/ns_darwin.go @@ -0,0 +1,21 @@ +// Copyright 2022 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ns + +import "github.com/containernetworking/cni/pkg/types" + +func CheckNetNS(nsPath string) (bool, *types.Error) { + return false, nil +} diff --git a/vendor/github.com/containernetworking/cni/pkg/ns/ns_linux.go b/vendor/github.com/containernetworking/cni/pkg/ns/ns_linux.go new file mode 100644 index 00000000..3d58e75d --- /dev/null +++ b/vendor/github.com/containernetworking/cni/pkg/ns/ns_linux.go @@ -0,0 +1,50 @@ +// Copyright 2022 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ns + +import ( + "runtime" + + "github.com/vishvananda/netns" + + "github.com/containernetworking/cni/pkg/types" +) + +// Returns an object representing the current OS thread's network namespace +func getCurrentNS() (netns.NsHandle, error) { + // Lock the thread in case other goroutine executes in it and changes its + // network namespace after getCurrentThreadNetNSPath(), otherwise it might + // return an unexpected network namespace. + runtime.LockOSThread() + defer runtime.UnlockOSThread() + return netns.Get() +} + +func CheckNetNS(nsPath string) (bool, *types.Error) { + ns, err := netns.GetFromPath(nsPath) + // Let plugins check whether nsPath from args is valid. Also support CNI DEL for empty nsPath as already-deleted nsPath. + if err != nil { + return false, nil + } + defer ns.Close() + + pluginNS, err := getCurrentNS() + if err != nil { + return false, types.NewError(types.ErrInvalidNetNS, "get plugin's netns failed", "") + } + defer pluginNS.Close() + + return pluginNS.Equal(ns), nil +} diff --git a/vendor/github.com/containernetworking/cni/pkg/ns/ns_windows.go b/vendor/github.com/containernetworking/cni/pkg/ns/ns_windows.go new file mode 100644 index 00000000..cffe1361 --- /dev/null +++ b/vendor/github.com/containernetworking/cni/pkg/ns/ns_windows.go @@ -0,0 +1,21 @@ +// Copyright 2022 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ns + +import "github.com/containernetworking/cni/pkg/types" + +func CheckNetNS(nsPath string) (bool, *types.Error) { + return false, nil +} diff --git a/vendor/github.com/containernetworking/cni/pkg/skel/skel.go b/vendor/github.com/containernetworking/cni/pkg/skel/skel.go new file mode 100644 index 00000000..f29cf345 --- /dev/null +++ b/vendor/github.com/containernetworking/cni/pkg/skel/skel.go @@ -0,0 +1,439 @@ +// Copyright 2014-2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package skel provides skeleton code for a CNI plugin. +// In particular, it implements argument parsing and validation. +package skel + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "os" + "strings" + + "github.com/containernetworking/cni/pkg/ns" + "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/utils" + "github.com/containernetworking/cni/pkg/version" +) + +// CmdArgs captures all the arguments passed in to the plugin +// via both env vars and stdin +type CmdArgs struct { + ContainerID string + Netns string + IfName string + Args string + Path string + NetnsOverride string + StdinData []byte +} + +type dispatcher struct { + Getenv func(string) string + Stdin io.Reader + Stdout io.Writer + Stderr io.Writer + + ConfVersionDecoder version.ConfigDecoder + VersionReconciler version.Reconciler +} + +type reqForCmdEntry map[string]bool + +func (t *dispatcher) getCmdArgsFromEnv() (string, *CmdArgs, *types.Error) { + var cmd, contID, netns, ifName, args, path, netnsOverride string + + vars := []struct { + name string + val *string + reqForCmd reqForCmdEntry + validateFn func(string) *types.Error + }{ + { + "CNI_COMMAND", + &cmd, + reqForCmdEntry{ + "ADD": true, + "CHECK": true, + "DEL": true, + "GC": true, + "STATUS": true, + }, + nil, + }, + { + "CNI_CONTAINERID", + &contID, + reqForCmdEntry{ + "ADD": true, + "CHECK": true, + "DEL": true, + }, + utils.ValidateContainerID, + }, + { + "CNI_NETNS", + &netns, + reqForCmdEntry{ + "ADD": true, + "CHECK": true, + "DEL": false, + }, + nil, + }, + { + "CNI_IFNAME", + &ifName, + reqForCmdEntry{ + "ADD": true, + "CHECK": true, + "DEL": true, + }, + utils.ValidateInterfaceName, + }, + { + "CNI_ARGS", + &args, + reqForCmdEntry{ + "ADD": false, + "CHECK": false, + "DEL": false, + }, + nil, + }, + { + "CNI_PATH", + &path, + reqForCmdEntry{ + "ADD": true, + "CHECK": true, + "DEL": true, + "GC": true, + "STATUS": true, + }, + nil, + }, + { + "CNI_NETNS_OVERRIDE", + &netnsOverride, + reqForCmdEntry{ + "ADD": false, + "CHECK": false, + "DEL": false, + }, + nil, + }, + } + + argsMissing := make([]string, 0) + for _, v := range vars { + *v.val = t.Getenv(v.name) + if *v.val == "" { + if v.reqForCmd[cmd] || v.name == "CNI_COMMAND" { + argsMissing = append(argsMissing, v.name) + } + } else if v.reqForCmd[cmd] && v.validateFn != nil { + if err := v.validateFn(*v.val); err != nil { + return "", nil, err + } + } + } + + if len(argsMissing) > 0 { + joined := strings.Join(argsMissing, ",") + return "", nil, types.NewError(types.ErrInvalidEnvironmentVariables, fmt.Sprintf("required env variables [%s] missing", joined), "") + } + + if cmd == "VERSION" { + t.Stdin = bytes.NewReader(nil) + } + + stdinData, err := io.ReadAll(t.Stdin) + if err != nil { + return "", nil, types.NewError(types.ErrIOFailure, fmt.Sprintf("error reading from stdin: %v", err), "") + } + + if cmd != "VERSION" { + if err := validateConfig(stdinData); err != nil { + return "", nil, err + } + } + + cmdArgs := &CmdArgs{ + ContainerID: contID, + Netns: netns, + IfName: ifName, + Args: args, + Path: path, + StdinData: stdinData, + NetnsOverride: netnsOverride, + } + return cmd, cmdArgs, nil +} + +func (t *dispatcher) checkVersionAndCall(cmdArgs *CmdArgs, pluginVersionInfo version.PluginInfo, toCall func(*CmdArgs) error) *types.Error { + configVersion, err := t.ConfVersionDecoder.Decode(cmdArgs.StdinData) + if err != nil { + return types.NewError(types.ErrDecodingFailure, err.Error(), "") + } + verErr := t.VersionReconciler.Check(configVersion, pluginVersionInfo) + if verErr != nil { + return types.NewError(types.ErrIncompatibleCNIVersion, "incompatible CNI versions", verErr.Details()) + } + + if toCall == nil { + return nil + } + + if err = toCall(cmdArgs); err != nil { + var e *types.Error + if errors.As(err, &e) { + // don't wrap Error in Error + return e + } + return types.NewError(types.ErrInternal, err.Error(), "") + } + + return nil +} + +func validateConfig(jsonBytes []byte) *types.Error { + var conf struct { + Name string `json:"name"` + } + if err := json.Unmarshal(jsonBytes, &conf); err != nil { + return types.NewError(types.ErrDecodingFailure, fmt.Sprintf("error unmarshall network config: %v", err), "") + } + if conf.Name == "" { + return types.NewError(types.ErrInvalidNetworkConfig, "missing network name", "") + } + if err := utils.ValidateNetworkName(conf.Name); err != nil { + return err + } + return nil +} + +func (t *dispatcher) pluginMain(funcs CNIFuncs, versionInfo version.PluginInfo, about string) *types.Error { + cmd, cmdArgs, err := t.getCmdArgsFromEnv() + if err != nil { + // Print the about string to stderr when no command is set + if err.Code == types.ErrInvalidEnvironmentVariables && t.Getenv("CNI_COMMAND") == "" && about != "" { + _, _ = fmt.Fprintln(t.Stderr, about) + _, _ = fmt.Fprintf(t.Stderr, "CNI protocol versions supported: %s\n", strings.Join(versionInfo.SupportedVersions(), ", ")) + return nil + } + return err + } + + switch cmd { + case "ADD": + err = t.checkVersionAndCall(cmdArgs, versionInfo, funcs.Add) + if err != nil { + return err + } + if strings.ToUpper(cmdArgs.NetnsOverride) != "TRUE" && cmdArgs.NetnsOverride != "1" { + isPluginNetNS, checkErr := ns.CheckNetNS(cmdArgs.Netns) + if checkErr != nil { + return checkErr + } else if isPluginNetNS { + return types.NewError(types.ErrInvalidNetNS, "plugin's netns and netns from CNI_NETNS should not be the same", "") + } + } + case "CHECK": + configVersion, err := t.ConfVersionDecoder.Decode(cmdArgs.StdinData) + if err != nil { + return types.NewError(types.ErrDecodingFailure, err.Error(), "") + } + if gtet, err := version.GreaterThanOrEqualTo(configVersion, "0.4.0"); err != nil { + return types.NewError(types.ErrDecodingFailure, err.Error(), "") + } else if !gtet { + return types.NewError(types.ErrIncompatibleCNIVersion, "config version does not allow CHECK", "") + } + for _, pluginVersion := range versionInfo.SupportedVersions() { + gtet, err := version.GreaterThanOrEqualTo(pluginVersion, configVersion) + if err != nil { + return types.NewError(types.ErrDecodingFailure, err.Error(), "") + } else if gtet { + if err := t.checkVersionAndCall(cmdArgs, versionInfo, funcs.Check); err != nil { + return err + } + return nil + } + } + return types.NewError(types.ErrIncompatibleCNIVersion, "plugin version does not allow CHECK", "") + case "DEL": + err = t.checkVersionAndCall(cmdArgs, versionInfo, funcs.Del) + if err != nil { + return err + } + if strings.ToUpper(cmdArgs.NetnsOverride) != "TRUE" && cmdArgs.NetnsOverride != "1" { + isPluginNetNS, checkErr := ns.CheckNetNS(cmdArgs.Netns) + if checkErr != nil { + return checkErr + } else if isPluginNetNS { + return types.NewError(types.ErrInvalidNetNS, "plugin's netns and netns from CNI_NETNS should not be the same", "") + } + } + case "GC": + configVersion, err := t.ConfVersionDecoder.Decode(cmdArgs.StdinData) + if err != nil { + return types.NewError(types.ErrDecodingFailure, err.Error(), "") + } + if gtet, err := version.GreaterThanOrEqualTo(configVersion, "1.1.0"); err != nil { + return types.NewError(types.ErrDecodingFailure, err.Error(), "") + } else if !gtet { + return types.NewError(types.ErrIncompatibleCNIVersion, "config version does not allow GC", "") + } + for _, pluginVersion := range versionInfo.SupportedVersions() { + gtet, err := version.GreaterThanOrEqualTo(pluginVersion, configVersion) + if err != nil { + return types.NewError(types.ErrDecodingFailure, err.Error(), "") + } else if gtet { + if err := t.checkVersionAndCall(cmdArgs, versionInfo, funcs.GC); err != nil { + return err + } + return nil + } + } + return types.NewError(types.ErrIncompatibleCNIVersion, "plugin version does not allow GC", "") + case "STATUS": + configVersion, err := t.ConfVersionDecoder.Decode(cmdArgs.StdinData) + if err != nil { + return types.NewError(types.ErrDecodingFailure, err.Error(), "") + } + if gtet, err := version.GreaterThanOrEqualTo(configVersion, "1.1.0"); err != nil { + return types.NewError(types.ErrDecodingFailure, err.Error(), "") + } else if !gtet { + return types.NewError(types.ErrIncompatibleCNIVersion, "config version does not allow STATUS", "") + } + for _, pluginVersion := range versionInfo.SupportedVersions() { + gtet, err := version.GreaterThanOrEqualTo(pluginVersion, configVersion) + if err != nil { + return types.NewError(types.ErrDecodingFailure, err.Error(), "") + } else if gtet { + if err := t.checkVersionAndCall(cmdArgs, versionInfo, funcs.Status); err != nil { + return err + } + return nil + } + } + return types.NewError(types.ErrIncompatibleCNIVersion, "plugin version does not allow STATUS", "") + case "VERSION": + if err := versionInfo.Encode(t.Stdout); err != nil { + return types.NewError(types.ErrIOFailure, err.Error(), "") + } + default: + return types.NewError(types.ErrInvalidEnvironmentVariables, fmt.Sprintf("unknown CNI_COMMAND: %v", cmd), "") + } + + return err +} + +// PluginMainWithError is the core "main" for a plugin. It accepts +// callback functions for add, check, and del CNI commands and returns an error. +// +// The caller must also specify what CNI spec versions the plugin supports. +// +// It is the responsibility of the caller to check for non-nil error return. +// +// For a plugin to comply with the CNI spec, it must print any error to stdout +// as JSON and then exit with nonzero status code. +// +// To let this package automatically handle errors and call os.Exit(1) for you, +// use PluginMain() instead. +// +// Deprecated: Use github.com/containernetworking/cni/pkg/skel.PluginMainFuncsWithError instead. +func PluginMainWithError(cmdAdd, cmdCheck, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo, about string) *types.Error { + return PluginMainFuncsWithError(CNIFuncs{Add: cmdAdd, Check: cmdCheck, Del: cmdDel}, versionInfo, about) +} + +// CNIFuncs contains a group of callback command funcs to be passed in as +// parameters to the core "main" for a plugin. +type CNIFuncs struct { + Add func(_ *CmdArgs) error + Del func(_ *CmdArgs) error + Check func(_ *CmdArgs) error + GC func(_ *CmdArgs) error + Status func(_ *CmdArgs) error +} + +// PluginMainFuncsWithError is the core "main" for a plugin. It accepts +// callback functions defined within CNIFuncs and returns an error. +// +// The caller must also specify what CNI spec versions the plugin supports. +// +// It is the responsibility of the caller to check for non-nil error return. +// +// For a plugin to comply with the CNI spec, it must print any error to stdout +// as JSON and then exit with nonzero status code. +// +// To let this package automatically handle errors and call os.Exit(1) for you, +// use PluginMainFuncs() instead. +func PluginMainFuncsWithError(funcs CNIFuncs, versionInfo version.PluginInfo, about string) *types.Error { + return (&dispatcher{ + Getenv: os.Getenv, + Stdin: os.Stdin, + Stdout: os.Stdout, + Stderr: os.Stderr, + }).pluginMain(funcs, versionInfo, about) +} + +// PluginMainFuncs is the core "main" for a plugin which includes automatic error handling. +// This is a newer alternative func to PluginMain which abstracts CNI commands within a +// CNIFuncs interface. +// +// The caller must also specify what CNI spec versions the plugin supports. +// +// The caller can specify an "about" string, which is printed on stderr +// when no CNI_COMMAND is specified. The recommended output is "CNI plugin v" +// +// When an error occurs in any func in CNIFuncs, PluginMainFuncs will print the error +// as JSON to stdout and call os.Exit(1). +// +// To have more control over error handling, use PluginMainFuncsWithError() instead. +func PluginMainFuncs(funcs CNIFuncs, versionInfo version.PluginInfo, about string) { + if e := PluginMainFuncsWithError(funcs, versionInfo, about); e != nil { + if err := e.Print(); err != nil { + log.Print("Error writing error JSON to stdout: ", err) + } + os.Exit(1) + } +} + +// PluginMain is the core "main" for a plugin which includes automatic error handling. +// +// The caller must also specify what CNI spec versions the plugin supports. +// +// The caller can specify an "about" string, which is printed on stderr +// when no CNI_COMMAND is specified. The recommended output is "CNI plugin v" +// +// When an error occurs in either cmdAdd, cmdCheck, or cmdDel, PluginMain will print the error +// as JSON to stdout and call os.Exit(1). +// +// To have more control over error handling, use PluginMainWithError() instead. +// +// Deprecated: Use github.com/containernetworking/cni/pkg/skel.PluginMainFuncs instead. +func PluginMain(cmdAdd, cmdCheck, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo, about string) { + if e := PluginMainWithError(cmdAdd, cmdCheck, cmdDel, versionInfo, about); e != nil { + if err := e.Print(); err != nil { + log.Print("Error writing error JSON to stdout: ", err) + } + os.Exit(1) + } +} diff --git a/vendor/github.com/containernetworking/cni/pkg/types/020/types.go b/vendor/github.com/containernetworking/cni/pkg/types/020/types.go index 36f31678..99b151ff 100644 --- a/vendor/github.com/containernetworking/cni/pkg/types/020/types.go +++ b/vendor/github.com/containernetworking/cni/pkg/types/020/types.go @@ -22,25 +22,47 @@ import ( "os" "github.com/containernetworking/cni/pkg/types" + convert "github.com/containernetworking/cni/pkg/types/internal" ) const ImplementedSpecVersion string = "0.2.0" -var SupportedVersions = []string{"", "0.1.0", ImplementedSpecVersion} +var supportedVersions = []string{"", "0.1.0", ImplementedSpecVersion} + +// Register converters for all versions less than the implemented spec version +func init() { + convert.RegisterConverter("0.1.0", []string{ImplementedSpecVersion}, convertFrom010) + convert.RegisterConverter(ImplementedSpecVersion, []string{"0.1.0"}, convertTo010) + + // Creator + convert.RegisterCreator(supportedVersions, NewResult) +} // Compatibility types for CNI version 0.1.0 and 0.2.0 +// NewResult creates a new Result object from JSON data. The JSON data +// must be compatible with the CNI versions implemented by this type. func NewResult(data []byte) (types.Result, error) { result := &Result{} if err := json.Unmarshal(data, result); err != nil { return nil, err } - return result, nil + for _, v := range supportedVersions { + if result.CNIVersion == v { + if result.CNIVersion == "" { + result.CNIVersion = "0.1.0" + } + return result, nil + } + } + return nil, fmt.Errorf("result type supports %v but unmarshalled CNIVersion is %q", + supportedVersions, result.CNIVersion) } +// GetResult converts the given Result object to the ImplementedSpecVersion +// and returns the concrete type or an error func GetResult(r types.Result) (*Result, error) { - // We expect version 0.1.0/0.2.0 results - result020, err := r.GetAsVersion(ImplementedSpecVersion) + result020, err := convert.Convert(r, ImplementedSpecVersion) if err != nil { return nil, err } @@ -51,6 +73,32 @@ func GetResult(r types.Result) (*Result, error) { return result, nil } +func convertFrom010(from types.Result, toVersion string) (types.Result, error) { + if toVersion != "0.2.0" { + panic("only converts to version 0.2.0") + } + fromResult := from.(*Result) + return &Result{ + CNIVersion: ImplementedSpecVersion, + IP4: fromResult.IP4.Copy(), + IP6: fromResult.IP6.Copy(), + DNS: *fromResult.DNS.Copy(), + }, nil +} + +func convertTo010(from types.Result, toVersion string) (types.Result, error) { + if toVersion != "0.1.0" { + panic("only converts to version 0.1.0") + } + fromResult := from.(*Result) + return &Result{ + CNIVersion: "0.1.0", + IP4: fromResult.IP4.Copy(), + IP6: fromResult.IP6.Copy(), + DNS: *fromResult.DNS.Copy(), + }, nil +} + // Result is what gets returned from the plugin (via stdout) to the caller type Result struct { CNIVersion string `json:"cniVersion,omitempty"` @@ -60,17 +108,16 @@ type Result struct { } func (r *Result) Version() string { - return ImplementedSpecVersion + return r.CNIVersion } func (r *Result) GetAsVersion(version string) (types.Result, error) { - for _, supportedVersion := range SupportedVersions { - if version == supportedVersion { - r.CNIVersion = version - return r, nil - } + // If the creator of the result did not set the CNIVersion, assume it + // should be the highest spec version implemented by this Result + if r.CNIVersion == "" { + r.CNIVersion = ImplementedSpecVersion } - return nil, fmt.Errorf("cannot convert version %q to %s", SupportedVersions, version) + return convert.Convert(r, version) } func (r *Result) Print() error { @@ -93,6 +140,22 @@ type IPConfig struct { Routes []types.Route } +func (i *IPConfig) Copy() *IPConfig { + if i == nil { + return nil + } + + var routes []types.Route + for _, fromRoute := range i.Routes { + routes = append(routes, *fromRoute.Copy()) + } + return &IPConfig{ + IP: i.IP, + Gateway: i.Gateway, + Routes: routes, + } +} + // net.IPNet is not JSON (un)marshallable so this duality is needed // for our custom IPNet type diff --git a/vendor/github.com/containernetworking/cni/pkg/types/040/types.go b/vendor/github.com/containernetworking/cni/pkg/types/040/types.go new file mode 100644 index 00000000..3633b0ea --- /dev/null +++ b/vendor/github.com/containernetworking/cni/pkg/types/040/types.go @@ -0,0 +1,306 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types040 + +import ( + "encoding/json" + "fmt" + "io" + "net" + "os" + + "github.com/containernetworking/cni/pkg/types" + types020 "github.com/containernetworking/cni/pkg/types/020" + convert "github.com/containernetworking/cni/pkg/types/internal" +) + +const ImplementedSpecVersion string = "0.4.0" + +var supportedVersions = []string{"0.3.0", "0.3.1", ImplementedSpecVersion} + +// Register converters for all versions less than the implemented spec version +func init() { + // Up-converters + convert.RegisterConverter("0.1.0", supportedVersions, convertFrom02x) + convert.RegisterConverter("0.2.0", supportedVersions, convertFrom02x) + convert.RegisterConverter("0.3.0", supportedVersions, convertInternal) + convert.RegisterConverter("0.3.1", supportedVersions, convertInternal) + + // Down-converters + convert.RegisterConverter("0.4.0", []string{"0.3.0", "0.3.1"}, convertInternal) + convert.RegisterConverter("0.4.0", []string{"0.1.0", "0.2.0"}, convertTo02x) + convert.RegisterConverter("0.3.1", []string{"0.1.0", "0.2.0"}, convertTo02x) + convert.RegisterConverter("0.3.0", []string{"0.1.0", "0.2.0"}, convertTo02x) + + // Creator + convert.RegisterCreator(supportedVersions, NewResult) +} + +func NewResult(data []byte) (types.Result, error) { + result := &Result{} + if err := json.Unmarshal(data, result); err != nil { + return nil, err + } + for _, v := range supportedVersions { + if result.CNIVersion == v { + return result, nil + } + } + return nil, fmt.Errorf("result type supports %v but unmarshalled CNIVersion is %q", + supportedVersions, result.CNIVersion) +} + +func GetResult(r types.Result) (*Result, error) { + resultCurrent, err := r.GetAsVersion(ImplementedSpecVersion) + if err != nil { + return nil, err + } + result, ok := resultCurrent.(*Result) + if !ok { + return nil, fmt.Errorf("failed to convert result") + } + return result, nil +} + +func NewResultFromResult(result types.Result) (*Result, error) { + newResult, err := convert.Convert(result, ImplementedSpecVersion) + if err != nil { + return nil, err + } + return newResult.(*Result), nil +} + +// Result is what gets returned from the plugin (via stdout) to the caller +type Result struct { + CNIVersion string `json:"cniVersion,omitempty"` + Interfaces []*Interface `json:"interfaces,omitempty"` + IPs []*IPConfig `json:"ips,omitempty"` + Routes []*types.Route `json:"routes,omitempty"` + DNS types.DNS `json:"dns,omitempty"` +} + +func convert020IPConfig(from *types020.IPConfig, ipVersion string) *IPConfig { + return &IPConfig{ + Version: ipVersion, + Address: from.IP, + Gateway: from.Gateway, + } +} + +func convertFrom02x(from types.Result, toVersion string) (types.Result, error) { + fromResult := from.(*types020.Result) + toResult := &Result{ + CNIVersion: toVersion, + DNS: *fromResult.DNS.Copy(), + Routes: []*types.Route{}, + } + if fromResult.IP4 != nil { + toResult.IPs = append(toResult.IPs, convert020IPConfig(fromResult.IP4, "4")) + for _, fromRoute := range fromResult.IP4.Routes { + toResult.Routes = append(toResult.Routes, fromRoute.Copy()) + } + } + + if fromResult.IP6 != nil { + toResult.IPs = append(toResult.IPs, convert020IPConfig(fromResult.IP6, "6")) + for _, fromRoute := range fromResult.IP6.Routes { + toResult.Routes = append(toResult.Routes, fromRoute.Copy()) + } + } + + return toResult, nil +} + +func convertInternal(from types.Result, toVersion string) (types.Result, error) { + fromResult := from.(*Result) + toResult := &Result{ + CNIVersion: toVersion, + DNS: *fromResult.DNS.Copy(), + Routes: []*types.Route{}, + } + for _, fromIntf := range fromResult.Interfaces { + toResult.Interfaces = append(toResult.Interfaces, fromIntf.Copy()) + } + for _, fromIPC := range fromResult.IPs { + toResult.IPs = append(toResult.IPs, fromIPC.Copy()) + } + for _, fromRoute := range fromResult.Routes { + toResult.Routes = append(toResult.Routes, fromRoute.Copy()) + } + return toResult, nil +} + +func convertTo02x(from types.Result, toVersion string) (types.Result, error) { + fromResult := from.(*Result) + toResult := &types020.Result{ + CNIVersion: toVersion, + DNS: *fromResult.DNS.Copy(), + } + + for _, fromIP := range fromResult.IPs { + // Only convert the first IP address of each version as 0.2.0 + // and earlier cannot handle multiple IP addresses + if fromIP.Version == "4" && toResult.IP4 == nil { + toResult.IP4 = &types020.IPConfig{ + IP: fromIP.Address, + Gateway: fromIP.Gateway, + } + } else if fromIP.Version == "6" && toResult.IP6 == nil { + toResult.IP6 = &types020.IPConfig{ + IP: fromIP.Address, + Gateway: fromIP.Gateway, + } + } + if toResult.IP4 != nil && toResult.IP6 != nil { + break + } + } + + for _, fromRoute := range fromResult.Routes { + is4 := fromRoute.Dst.IP.To4() != nil + if is4 && toResult.IP4 != nil { + toResult.IP4.Routes = append(toResult.IP4.Routes, types.Route{ + Dst: fromRoute.Dst, + GW: fromRoute.GW, + }) + } else if !is4 && toResult.IP6 != nil { + toResult.IP6.Routes = append(toResult.IP6.Routes, types.Route{ + Dst: fromRoute.Dst, + GW: fromRoute.GW, + }) + } + } + + // 0.2.0 and earlier require at least one IP address in the Result + if toResult.IP4 == nil && toResult.IP6 == nil { + return nil, fmt.Errorf("cannot convert: no valid IP addresses") + } + + return toResult, nil +} + +func (r *Result) Version() string { + return r.CNIVersion +} + +func (r *Result) GetAsVersion(version string) (types.Result, error) { + // If the creator of the result did not set the CNIVersion, assume it + // should be the highest spec version implemented by this Result + if r.CNIVersion == "" { + r.CNIVersion = ImplementedSpecVersion + } + return convert.Convert(r, version) +} + +func (r *Result) Print() error { + return r.PrintTo(os.Stdout) +} + +func (r *Result) PrintTo(writer io.Writer) error { + data, err := json.MarshalIndent(r, "", " ") + if err != nil { + return err + } + _, err = writer.Write(data) + return err +} + +// Interface contains values about the created interfaces +type Interface struct { + Name string `json:"name"` + Mac string `json:"mac,omitempty"` + Sandbox string `json:"sandbox,omitempty"` +} + +func (i *Interface) String() string { + return fmt.Sprintf("%+v", *i) +} + +func (i *Interface) Copy() *Interface { + if i == nil { + return nil + } + newIntf := *i + return &newIntf +} + +// Int returns a pointer to the int value passed in. Used to +// set the IPConfig.Interface field. +func Int(v int) *int { + return &v +} + +// IPConfig contains values necessary to configure an IP address on an interface +type IPConfig struct { + // IP version, either "4" or "6" + Version string + // Index into Result structs Interfaces list + Interface *int + Address net.IPNet + Gateway net.IP +} + +func (i *IPConfig) String() string { + return fmt.Sprintf("%+v", *i) +} + +func (i *IPConfig) Copy() *IPConfig { + if i == nil { + return nil + } + + ipc := &IPConfig{ + Version: i.Version, + Address: i.Address, + Gateway: i.Gateway, + } + if i.Interface != nil { + intf := *i.Interface + ipc.Interface = &intf + } + return ipc +} + +// JSON (un)marshallable types +type ipConfig struct { + Version string `json:"version"` + Interface *int `json:"interface,omitempty"` + Address types.IPNet `json:"address"` + Gateway net.IP `json:"gateway,omitempty"` +} + +func (c *IPConfig) MarshalJSON() ([]byte, error) { + ipc := ipConfig{ + Version: c.Version, + Interface: c.Interface, + Address: types.IPNet(c.Address), + Gateway: c.Gateway, + } + + return json.Marshal(ipc) +} + +func (c *IPConfig) UnmarshalJSON(data []byte) error { + ipc := ipConfig{} + if err := json.Unmarshal(data, &ipc); err != nil { + return err + } + + c.Version = ipc.Version + c.Interface = ipc.Interface + c.Address = net.IPNet(ipc.Address) + c.Gateway = ipc.Gateway + return nil +} diff --git a/vendor/github.com/containernetworking/cni/pkg/types/100/types.go b/vendor/github.com/containernetworking/cni/pkg/types/100/types.go new file mode 100644 index 00000000..f58b9120 --- /dev/null +++ b/vendor/github.com/containernetworking/cni/pkg/types/100/types.go @@ -0,0 +1,352 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types100 + +import ( + "encoding/json" + "fmt" + "io" + "net" + "os" + + "github.com/containernetworking/cni/pkg/types" + types040 "github.com/containernetworking/cni/pkg/types/040" + convert "github.com/containernetworking/cni/pkg/types/internal" +) + +// The types did not change between v1.0 and v1.1 +const ImplementedSpecVersion string = "1.1.0" + +var supportedVersions = []string{"1.0.0", "1.1.0"} + +// Register converters for all versions less than the implemented spec version +func init() { + // Up-converters + convert.RegisterConverter("0.1.0", supportedVersions, convertFrom02x) + convert.RegisterConverter("0.2.0", supportedVersions, convertFrom02x) + convert.RegisterConverter("0.3.0", supportedVersions, convertFrom04x) + convert.RegisterConverter("0.3.1", supportedVersions, convertFrom04x) + convert.RegisterConverter("0.4.0", supportedVersions, convertFrom04x) + convert.RegisterConverter("1.0.0", []string{"1.1.0"}, convertFrom100) + + // Down-converters + convert.RegisterConverter("1.0.0", []string{"0.3.0", "0.3.1", "0.4.0"}, convertTo04x) + convert.RegisterConverter("1.0.0", []string{"0.1.0", "0.2.0"}, convertTo02x) + convert.RegisterConverter("1.1.0", []string{"0.3.0", "0.3.1", "0.4.0"}, convertTo04x) + convert.RegisterConverter("1.1.0", []string{"0.1.0", "0.2.0"}, convertTo02x) + convert.RegisterConverter("1.1.0", []string{"1.0.0"}, convertFrom100) + + // Creator + convert.RegisterCreator(supportedVersions, NewResult) +} + +func NewResult(data []byte) (types.Result, error) { + result := &Result{} + if err := json.Unmarshal(data, result); err != nil { + return nil, err + } + for _, v := range supportedVersions { + if result.CNIVersion == v { + return result, nil + } + } + return nil, fmt.Errorf("result type supports %v but unmarshalled CNIVersion is %q", + supportedVersions, result.CNIVersion) +} + +func GetResult(r types.Result) (*Result, error) { + resultCurrent, err := r.GetAsVersion(ImplementedSpecVersion) + if err != nil { + return nil, err + } + result, ok := resultCurrent.(*Result) + if !ok { + return nil, fmt.Errorf("failed to convert result") + } + return result, nil +} + +func NewResultFromResult(result types.Result) (*Result, error) { + newResult, err := convert.Convert(result, ImplementedSpecVersion) + if err != nil { + return nil, err + } + return newResult.(*Result), nil +} + +// Result is what gets returned from the plugin (via stdout) to the caller +type Result struct { + CNIVersion string `json:"cniVersion,omitempty"` + Interfaces []*Interface `json:"interfaces,omitempty"` + IPs []*IPConfig `json:"ips,omitempty"` + Routes []*types.Route `json:"routes,omitempty"` + DNS types.DNS `json:"dns,omitempty"` +} + +// Note: DNS should be omit if DNS is empty but default Marshal function +// will output empty structure hence need to write a Marshal function +func (r *Result) MarshalJSON() ([]byte, error) { + // use type alias to escape recursion for json.Marshal() to MarshalJSON() + type fixObjType = Result + + bytes, err := json.Marshal(fixObjType(*r)) //nolint:all + if err != nil { + return nil, err + } + + fixupObj := make(map[string]interface{}) + if err := json.Unmarshal(bytes, &fixupObj); err != nil { + return nil, err + } + + if r.DNS.IsEmpty() { + delete(fixupObj, "dns") + } + + return json.Marshal(fixupObj) +} + +// convertFrom100 does nothing except set the version; the types are the same +func convertFrom100(from types.Result, toVersion string) (types.Result, error) { + fromResult := from.(*Result) + + result := &Result{ + CNIVersion: toVersion, + Interfaces: fromResult.Interfaces, + IPs: fromResult.IPs, + Routes: fromResult.Routes, + DNS: fromResult.DNS, + } + return result, nil +} + +func convertFrom02x(from types.Result, toVersion string) (types.Result, error) { + result040, err := convert.Convert(from, "0.4.0") + if err != nil { + return nil, err + } + result100, err := convertFrom04x(result040, toVersion) + if err != nil { + return nil, err + } + return result100, nil +} + +func convertIPConfigFrom040(from *types040.IPConfig) *IPConfig { + to := &IPConfig{ + Address: from.Address, + Gateway: from.Gateway, + } + if from.Interface != nil { + intf := *from.Interface + to.Interface = &intf + } + return to +} + +func convertInterfaceFrom040(from *types040.Interface) *Interface { + return &Interface{ + Name: from.Name, + Mac: from.Mac, + Sandbox: from.Sandbox, + } +} + +func convertFrom04x(from types.Result, toVersion string) (types.Result, error) { + fromResult := from.(*types040.Result) + toResult := &Result{ + CNIVersion: toVersion, + DNS: *fromResult.DNS.Copy(), + Routes: []*types.Route{}, + } + for _, fromIntf := range fromResult.Interfaces { + toResult.Interfaces = append(toResult.Interfaces, convertInterfaceFrom040(fromIntf)) + } + for _, fromIPC := range fromResult.IPs { + toResult.IPs = append(toResult.IPs, convertIPConfigFrom040(fromIPC)) + } + for _, fromRoute := range fromResult.Routes { + toResult.Routes = append(toResult.Routes, fromRoute.Copy()) + } + return toResult, nil +} + +func convertIPConfigTo040(from *IPConfig) *types040.IPConfig { + version := "6" + if from.Address.IP.To4() != nil { + version = "4" + } + to := &types040.IPConfig{ + Version: version, + Address: from.Address, + Gateway: from.Gateway, + } + if from.Interface != nil { + intf := *from.Interface + to.Interface = &intf + } + return to +} + +func convertInterfaceTo040(from *Interface) *types040.Interface { + return &types040.Interface{ + Name: from.Name, + Mac: from.Mac, + Sandbox: from.Sandbox, + } +} + +func convertTo04x(from types.Result, toVersion string) (types.Result, error) { + fromResult := from.(*Result) + toResult := &types040.Result{ + CNIVersion: toVersion, + DNS: *fromResult.DNS.Copy(), + Routes: []*types.Route{}, + } + for _, fromIntf := range fromResult.Interfaces { + toResult.Interfaces = append(toResult.Interfaces, convertInterfaceTo040(fromIntf)) + } + for _, fromIPC := range fromResult.IPs { + toResult.IPs = append(toResult.IPs, convertIPConfigTo040(fromIPC)) + } + for _, fromRoute := range fromResult.Routes { + toResult.Routes = append(toResult.Routes, fromRoute.Copy()) + } + return toResult, nil +} + +func convertTo02x(from types.Result, toVersion string) (types.Result, error) { + // First convert to 0.4.0 + result040, err := convertTo04x(from, "0.4.0") + if err != nil { + return nil, err + } + result02x, err := convert.Convert(result040, toVersion) + if err != nil { + return nil, err + } + return result02x, nil +} + +func (r *Result) Version() string { + return r.CNIVersion +} + +func (r *Result) GetAsVersion(version string) (types.Result, error) { + // If the creator of the result did not set the CNIVersion, assume it + // should be the highest spec version implemented by this Result + if r.CNIVersion == "" { + r.CNIVersion = ImplementedSpecVersion + } + return convert.Convert(r, version) +} + +func (r *Result) Print() error { + return r.PrintTo(os.Stdout) +} + +func (r *Result) PrintTo(writer io.Writer) error { + data, err := json.MarshalIndent(r, "", " ") + if err != nil { + return err + } + _, err = writer.Write(data) + return err +} + +// Interface contains values about the created interfaces +type Interface struct { + Name string `json:"name"` + Mac string `json:"mac,omitempty"` + Mtu int `json:"mtu,omitempty"` + Sandbox string `json:"sandbox,omitempty"` + SocketPath string `json:"socketPath,omitempty"` + PciID string `json:"pciID,omitempty"` +} + +func (i *Interface) String() string { + return fmt.Sprintf("%+v", *i) +} + +func (i *Interface) Copy() *Interface { + if i == nil { + return nil + } + newIntf := *i + return &newIntf +} + +// Int returns a pointer to the int value passed in. Used to +// set the IPConfig.Interface field. +func Int(v int) *int { + return &v +} + +// IPConfig contains values necessary to configure an IP address on an interface +type IPConfig struct { + // Index into Result structs Interfaces list + Interface *int + Address net.IPNet + Gateway net.IP +} + +func (i *IPConfig) String() string { + return fmt.Sprintf("%+v", *i) +} + +func (i *IPConfig) Copy() *IPConfig { + if i == nil { + return nil + } + + ipc := &IPConfig{ + Address: i.Address, + Gateway: i.Gateway, + } + if i.Interface != nil { + intf := *i.Interface + ipc.Interface = &intf + } + return ipc +} + +// JSON (un)marshallable types +type ipConfig struct { + Interface *int `json:"interface,omitempty"` + Address types.IPNet `json:"address"` + Gateway net.IP `json:"gateway,omitempty"` +} + +func (c *IPConfig) MarshalJSON() ([]byte, error) { + ipc := ipConfig{ + Interface: c.Interface, + Address: types.IPNet(c.Address), + Gateway: c.Gateway, + } + + return json.Marshal(ipc) +} + +func (c *IPConfig) UnmarshalJSON(data []byte) error { + ipc := ipConfig{} + if err := json.Unmarshal(data, &ipc); err != nil { + return err + } + + c.Interface = ipc.Interface + c.Address = net.IPNet(ipc.Address) + c.Gateway = ipc.Gateway + return nil +} diff --git a/vendor/github.com/containernetworking/cni/pkg/types/args.go b/vendor/github.com/containernetworking/cni/pkg/types/args.go index 4eac6489..68a602bf 100644 --- a/vendor/github.com/containernetworking/cni/pkg/types/args.go +++ b/vendor/github.com/containernetworking/cni/pkg/types/args.go @@ -26,8 +26,8 @@ import ( type UnmarshallableBool bool // UnmarshalText implements the encoding.TextUnmarshaler interface. -// Returns boolean true if the string is "1" or "[Tt]rue" -// Returns boolean false if the string is "0" or "[Ff]alse" +// Returns boolean true if the string is "1" or "true" or "True" +// Returns boolean false if the string is "0" or "false" or "False” func (b *UnmarshallableBool) UnmarshalText(data []byte) error { s := strings.ToLower(string(data)) switch s { @@ -91,16 +91,26 @@ func LoadArgs(args string, container interface{}) error { unknownArgs = append(unknownArgs, pair) continue } - keyFieldIface := keyField.Addr().Interface() - u, ok := keyFieldIface.(encoding.TextUnmarshaler) + + var keyFieldInterface interface{} + switch { + case keyField.Kind() == reflect.Ptr: + keyField.Set(reflect.New(keyField.Type().Elem())) + keyFieldInterface = keyField.Interface() + case keyField.CanAddr() && keyField.Addr().CanInterface(): + keyFieldInterface = keyField.Addr().Interface() + default: + return UnmarshalableArgsError{fmt.Errorf("field '%s' has no valid interface", keyString)} + } + u, ok := keyFieldInterface.(encoding.TextUnmarshaler) if !ok { return UnmarshalableArgsError{fmt.Errorf( "ARGS: cannot unmarshal into field '%s' - type '%s' does not implement encoding.TextUnmarshaler", - keyString, reflect.TypeOf(keyFieldIface))} + keyString, reflect.TypeOf(keyFieldInterface))} } err := u.UnmarshalText([]byte(valueString)) if err != nil { - return fmt.Errorf("ARGS: error parsing value of pair %q: %v)", pair, err) + return fmt.Errorf("ARGS: error parsing value of pair %q: %w", pair, err) } } diff --git a/vendor/github.com/containernetworking/cni/pkg/types/create/create.go b/vendor/github.com/containernetworking/cni/pkg/types/create/create.go new file mode 100644 index 00000000..452cb622 --- /dev/null +++ b/vendor/github.com/containernetworking/cni/pkg/types/create/create.go @@ -0,0 +1,59 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package create + +import ( + "encoding/json" + "fmt" + + "github.com/containernetworking/cni/pkg/types" + _ "github.com/containernetworking/cni/pkg/types/020" + _ "github.com/containernetworking/cni/pkg/types/040" + _ "github.com/containernetworking/cni/pkg/types/100" + convert "github.com/containernetworking/cni/pkg/types/internal" +) + +// DecodeVersion returns the CNI version from CNI configuration or result JSON, +// or an error if the operation could not be performed. +func DecodeVersion(jsonBytes []byte) (string, error) { + var conf struct { + CNIVersion string `json:"cniVersion"` + } + err := json.Unmarshal(jsonBytes, &conf) + if err != nil { + return "", fmt.Errorf("decoding version from network config: %w", err) + } + if conf.CNIVersion == "" { + return "0.1.0", nil + } + return conf.CNIVersion, nil +} + +// Create creates a CNI Result using the given JSON with the expected +// version, or an error if the creation could not be performed +func Create(version string, bytes []byte) (types.Result, error) { + return convert.Create(version, bytes) +} + +// CreateFromBytes creates a CNI Result from the given JSON, automatically +// detecting the CNI spec version of the result. An error is returned if the +// operation could not be performed. +func CreateFromBytes(bytes []byte) (types.Result, error) { + version, err := DecodeVersion(bytes) + if err != nil { + return nil, err + } + return convert.Create(version, bytes) +} diff --git a/vendor/github.com/containernetworking/cni/pkg/types/current/types.go b/vendor/github.com/containernetworking/cni/pkg/types/current/types.go deleted file mode 100644 index 754cc6e7..00000000 --- a/vendor/github.com/containernetworking/cni/pkg/types/current/types.go +++ /dev/null @@ -1,276 +0,0 @@ -// Copyright 2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package current - -import ( - "encoding/json" - "fmt" - "io" - "net" - "os" - - "github.com/containernetworking/cni/pkg/types" - "github.com/containernetworking/cni/pkg/types/020" -) - -const ImplementedSpecVersion string = "0.4.0" - -var SupportedVersions = []string{"0.3.0", "0.3.1", ImplementedSpecVersion} - -func NewResult(data []byte) (types.Result, error) { - result := &Result{} - if err := json.Unmarshal(data, result); err != nil { - return nil, err - } - return result, nil -} - -func GetResult(r types.Result) (*Result, error) { - resultCurrent, err := r.GetAsVersion(ImplementedSpecVersion) - if err != nil { - return nil, err - } - result, ok := resultCurrent.(*Result) - if !ok { - return nil, fmt.Errorf("failed to convert result") - } - return result, nil -} - -var resultConverters = []struct { - versions []string - convert func(types.Result) (*Result, error) -}{ - {types020.SupportedVersions, convertFrom020}, - {SupportedVersions, convertFrom030}, -} - -func convertFrom020(result types.Result) (*Result, error) { - oldResult, err := types020.GetResult(result) - if err != nil { - return nil, err - } - - newResult := &Result{ - CNIVersion: ImplementedSpecVersion, - DNS: oldResult.DNS, - Routes: []*types.Route{}, - } - - if oldResult.IP4 != nil { - newResult.IPs = append(newResult.IPs, &IPConfig{ - Version: "4", - Address: oldResult.IP4.IP, - Gateway: oldResult.IP4.Gateway, - }) - for _, route := range oldResult.IP4.Routes { - newResult.Routes = append(newResult.Routes, &types.Route{ - Dst: route.Dst, - GW: route.GW, - }) - } - } - - if oldResult.IP6 != nil { - newResult.IPs = append(newResult.IPs, &IPConfig{ - Version: "6", - Address: oldResult.IP6.IP, - Gateway: oldResult.IP6.Gateway, - }) - for _, route := range oldResult.IP6.Routes { - newResult.Routes = append(newResult.Routes, &types.Route{ - Dst: route.Dst, - GW: route.GW, - }) - } - } - - return newResult, nil -} - -func convertFrom030(result types.Result) (*Result, error) { - newResult, ok := result.(*Result) - if !ok { - return nil, fmt.Errorf("failed to convert result") - } - newResult.CNIVersion = ImplementedSpecVersion - return newResult, nil -} - -func NewResultFromResult(result types.Result) (*Result, error) { - version := result.Version() - for _, converter := range resultConverters { - for _, supportedVersion := range converter.versions { - if version == supportedVersion { - return converter.convert(result) - } - } - } - return nil, fmt.Errorf("unsupported CNI result22 version %q", version) -} - -// Result is what gets returned from the plugin (via stdout) to the caller -type Result struct { - CNIVersion string `json:"cniVersion,omitempty"` - Interfaces []*Interface `json:"interfaces,omitempty"` - IPs []*IPConfig `json:"ips,omitempty"` - Routes []*types.Route `json:"routes,omitempty"` - DNS types.DNS `json:"dns,omitempty"` -} - -// Convert to the older 0.2.0 CNI spec Result type -func (r *Result) convertTo020() (*types020.Result, error) { - oldResult := &types020.Result{ - CNIVersion: types020.ImplementedSpecVersion, - DNS: r.DNS, - } - - for _, ip := range r.IPs { - // Only convert the first IP address of each version as 0.2.0 - // and earlier cannot handle multiple IP addresses - if ip.Version == "4" && oldResult.IP4 == nil { - oldResult.IP4 = &types020.IPConfig{ - IP: ip.Address, - Gateway: ip.Gateway, - } - } else if ip.Version == "6" && oldResult.IP6 == nil { - oldResult.IP6 = &types020.IPConfig{ - IP: ip.Address, - Gateway: ip.Gateway, - } - } - - if oldResult.IP4 != nil && oldResult.IP6 != nil { - break - } - } - - for _, route := range r.Routes { - is4 := route.Dst.IP.To4() != nil - if is4 && oldResult.IP4 != nil { - oldResult.IP4.Routes = append(oldResult.IP4.Routes, types.Route{ - Dst: route.Dst, - GW: route.GW, - }) - } else if !is4 && oldResult.IP6 != nil { - oldResult.IP6.Routes = append(oldResult.IP6.Routes, types.Route{ - Dst: route.Dst, - GW: route.GW, - }) - } - } - - if oldResult.IP4 == nil && oldResult.IP6 == nil { - return nil, fmt.Errorf("cannot convert: no valid IP addresses") - } - - return oldResult, nil -} - -func (r *Result) Version() string { - return ImplementedSpecVersion -} - -func (r *Result) GetAsVersion(version string) (types.Result, error) { - switch version { - case "0.3.0", "0.3.1", ImplementedSpecVersion: - r.CNIVersion = version - return r, nil - case types020.SupportedVersions[0], types020.SupportedVersions[1], types020.SupportedVersions[2]: - return r.convertTo020() - } - return nil, fmt.Errorf("cannot convert version 0.3.x to %q", version) -} - -func (r *Result) Print() error { - return r.PrintTo(os.Stdout) -} - -func (r *Result) PrintTo(writer io.Writer) error { - data, err := json.MarshalIndent(r, "", " ") - if err != nil { - return err - } - _, err = writer.Write(data) - return err -} - -// Convert this old version result to the current CNI version result -func (r *Result) Convert() (*Result, error) { - return r, nil -} - -// Interface contains values about the created interfaces -type Interface struct { - Name string `json:"name"` - Mac string `json:"mac,omitempty"` - Sandbox string `json:"sandbox,omitempty"` -} - -func (i *Interface) String() string { - return fmt.Sprintf("%+v", *i) -} - -// Int returns a pointer to the int value passed in. Used to -// set the IPConfig.Interface field. -func Int(v int) *int { - return &v -} - -// IPConfig contains values necessary to configure an IP address on an interface -type IPConfig struct { - // IP version, either "4" or "6" - Version string - // Index into Result structs Interfaces list - Interface *int - Address net.IPNet - Gateway net.IP -} - -func (i *IPConfig) String() string { - return fmt.Sprintf("%+v", *i) -} - -// JSON (un)marshallable types -type ipConfig struct { - Version string `json:"version"` - Interface *int `json:"interface,omitempty"` - Address types.IPNet `json:"address"` - Gateway net.IP `json:"gateway,omitempty"` -} - -func (c *IPConfig) MarshalJSON() ([]byte, error) { - ipc := ipConfig{ - Version: c.Version, - Interface: c.Interface, - Address: types.IPNet(c.Address), - Gateway: c.Gateway, - } - - return json.Marshal(ipc) -} - -func (c *IPConfig) UnmarshalJSON(data []byte) error { - ipc := ipConfig{} - if err := json.Unmarshal(data, &ipc); err != nil { - return err - } - - c.Version = ipc.Version - c.Interface = ipc.Interface - c.Address = net.IPNet(ipc.Address) - c.Gateway = ipc.Gateway - return nil -} diff --git a/vendor/github.com/containernetworking/cni/pkg/types/internal/convert.go b/vendor/github.com/containernetworking/cni/pkg/types/internal/convert.go new file mode 100644 index 00000000..bdbe4b0a --- /dev/null +++ b/vendor/github.com/containernetworking/cni/pkg/types/internal/convert.go @@ -0,0 +1,92 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package convert + +import ( + "fmt" + + "github.com/containernetworking/cni/pkg/types" +) + +// ConvertFn should convert from the given arbitrary Result type into a +// Result implementing CNI specification version passed in toVersion. +// The function is guaranteed to be passed a Result type matching the +// fromVersion it was registered with, and is guaranteed to be +// passed a toVersion matching one of the toVersions it was registered with. +type ConvertFn func(from types.Result, toVersion string) (types.Result, error) + +type converter struct { + // fromVersion is the CNI Result spec version that convertFn accepts + fromVersion string + // toVersions is a list of versions that convertFn can convert to + toVersions []string + convertFn ConvertFn +} + +var converters []*converter + +func findConverter(fromVersion, toVersion string) *converter { + for _, c := range converters { + if c.fromVersion == fromVersion { + for _, v := range c.toVersions { + if v == toVersion { + return c + } + } + } + } + return nil +} + +// Convert converts a CNI Result to the requested CNI specification version, +// or returns an error if the conversion could not be performed or failed +func Convert(from types.Result, toVersion string) (types.Result, error) { + if toVersion == "" { + toVersion = "0.1.0" + } + + fromVersion := from.Version() + + // Shortcut for same version + if fromVersion == toVersion { + return from, nil + } + + // Otherwise find the right converter + c := findConverter(fromVersion, toVersion) + if c == nil { + return nil, fmt.Errorf("no converter for CNI result version %s to %s", + fromVersion, toVersion) + } + return c.convertFn(from, toVersion) +} + +// RegisterConverter registers a CNI Result converter. SHOULD NOT BE CALLED +// EXCEPT FROM CNI ITSELF. +func RegisterConverter(fromVersion string, toVersions []string, convertFn ConvertFn) { + // Make sure there is no converter already registered for these + // from and to versions + for _, v := range toVersions { + if findConverter(fromVersion, v) != nil { + panic(fmt.Sprintf("converter already registered for %s to %s", + fromVersion, v)) + } + } + converters = append(converters, &converter{ + fromVersion: fromVersion, + toVersions: toVersions, + convertFn: convertFn, + }) +} diff --git a/vendor/github.com/containernetworking/cni/pkg/types/internal/create.go b/vendor/github.com/containernetworking/cni/pkg/types/internal/create.go new file mode 100644 index 00000000..96363091 --- /dev/null +++ b/vendor/github.com/containernetworking/cni/pkg/types/internal/create.go @@ -0,0 +1,66 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package convert + +import ( + "fmt" + + "github.com/containernetworking/cni/pkg/types" +) + +type ResultFactoryFunc func([]byte) (types.Result, error) + +type creator struct { + // CNI Result spec versions that createFn can create a Result for + versions []string + createFn ResultFactoryFunc +} + +var creators []*creator + +func findCreator(version string) *creator { + for _, c := range creators { + for _, v := range c.versions { + if v == version { + return c + } + } + } + return nil +} + +// Create creates a CNI Result using the given JSON, or an error if the creation +// could not be performed +func Create(version string, bytes []byte) (types.Result, error) { + if c := findCreator(version); c != nil { + return c.createFn(bytes) + } + return nil, fmt.Errorf("unsupported CNI result version %q", version) +} + +// RegisterCreator registers a CNI Result creator. SHOULD NOT BE CALLED +// EXCEPT FROM CNI ITSELF. +func RegisterCreator(versions []string, createFn ResultFactoryFunc) { + // Make sure there is no creator already registered for these versions + for _, v := range versions { + if findCreator(v) != nil { + panic(fmt.Sprintf("creator already registered for %s", v)) + } + } + creators = append(creators, &creator{ + versions: versions, + createFn: createFn, + }) +} diff --git a/vendor/github.com/containernetworking/cni/pkg/types/types.go b/vendor/github.com/containernetworking/cni/pkg/types/types.go index 3fa757a5..f4b3ce35 100644 --- a/vendor/github.com/containernetworking/cni/pkg/types/types.go +++ b/vendor/github.com/containernetworking/cni/pkg/types/types.go @@ -56,35 +56,74 @@ func (n *IPNet) UnmarshalJSON(data []byte) error { return nil } -// NetConf describes a network. -type NetConf struct { +// Use PluginConf instead of NetConf, the NetConf +// backwards-compat alias will be removed in a future release. +type NetConf = PluginConf + +// PluginConf describes a plugin configuration for a specific network. +type PluginConf struct { CNIVersion string `json:"cniVersion,omitempty"` Name string `json:"name,omitempty"` Type string `json:"type,omitempty"` Capabilities map[string]bool `json:"capabilities,omitempty"` IPAM IPAM `json:"ipam,omitempty"` - DNS DNS `json:"dns"` + DNS DNS `json:"dns,omitempty"` RawPrevResult map[string]interface{} `json:"prevResult,omitempty"` PrevResult Result `json:"-"` + + // ValidAttachments is only supplied when executing a GC operation + ValidAttachments []GCAttachment `json:"cni.dev/valid-attachments,omitempty"` +} + +// GCAttachment is the parameters to a GC call -- namely, +// the container ID and ifname pair that represents a +// still-valid attachment. +type GCAttachment struct { + ContainerID string `json:"containerID"` + IfName string `json:"ifname"` +} + +// Note: DNS should be omit if DNS is empty but default Marshal function +// will output empty structure hence need to write a Marshal function +func (n *PluginConf) MarshalJSON() ([]byte, error) { + bytes, err := json.Marshal(*n) + if err != nil { + return nil, err + } + + fixupObj := make(map[string]interface{}) + if err := json.Unmarshal(bytes, &fixupObj); err != nil { + return nil, err + } + + if n.DNS.IsEmpty() { + delete(fixupObj, "dns") + } + + return json.Marshal(fixupObj) } type IPAM struct { Type string `json:"type,omitempty"` } +// IsEmpty returns true if IPAM structure has no value, otherwise return false +func (i *IPAM) IsEmpty() bool { + return i.Type == "" +} + // NetConfList describes an ordered list of networks. type NetConfList struct { CNIVersion string `json:"cniVersion,omitempty"` - Name string `json:"name,omitempty"` - DisableCheck bool `json:"disableCheck,omitempty"` - Plugins []*NetConf `json:"plugins,omitempty"` + Name string `json:"name,omitempty"` + DisableCheck bool `json:"disableCheck,omitempty"` + DisableGC bool `json:"disableGC,omitempty"` + Plugins []*PluginConf `json:"plugins,omitempty"` } -type ResultFactoryFunc func([]byte) (Result, error) - // Result is an interface that provides the result of plugin execution type Result interface { // The highest CNI specification result version the result supports @@ -118,17 +157,79 @@ type DNS struct { Options []string `json:"options,omitempty"` } +// IsEmpty returns true if DNS structure has no value, otherwise return false +func (d *DNS) IsEmpty() bool { + if len(d.Nameservers) == 0 && d.Domain == "" && len(d.Search) == 0 && len(d.Options) == 0 { + return true + } + return false +} + +func (d *DNS) Copy() *DNS { + if d == nil { + return nil + } + + to := &DNS{Domain: d.Domain} + to.Nameservers = append(to.Nameservers, d.Nameservers...) + to.Search = append(to.Search, d.Search...) + to.Options = append(to.Options, d.Options...) + return to +} + type Route struct { - Dst net.IPNet - GW net.IP + Dst net.IPNet + GW net.IP + MTU int + AdvMSS int + Priority int + Table *int + Scope *int } func (r *Route) String() string { - return fmt.Sprintf("%+v", *r) + table := "" + if r.Table != nil { + table = fmt.Sprintf("%d", *r.Table) + } + + scope := "" + if r.Scope != nil { + scope = fmt.Sprintf("%d", *r.Scope) + } + + return fmt.Sprintf("{Dst:%+v GW:%v MTU:%d AdvMSS:%d Priority:%d Table:%s Scope:%s}", r.Dst, r.GW, r.MTU, r.AdvMSS, r.Priority, table, scope) +} + +func (r *Route) Copy() *Route { + if r == nil { + return nil + } + + route := &Route{ + Dst: r.Dst, + GW: r.GW, + MTU: r.MTU, + AdvMSS: r.AdvMSS, + Priority: r.Priority, + Scope: r.Scope, + } + + if r.Table != nil { + table := *r.Table + route.Table = &table + } + + if r.Scope != nil { + scope := *r.Scope + route.Scope = &scope + } + + return route } // Well known error codes -// see https://github.com/containernetworking/cni/blob/master/SPEC.md#well-known-error-codes +// see https://github.com/containernetworking/cni/blob/main/SPEC.md#well-known-error-codes const ( ErrUnknown uint = iota // 0 ErrIncompatibleCNIVersion // 1 @@ -138,6 +239,7 @@ const ( ErrIOFailure // 5 ErrDecodingFailure // 6 ErrInvalidNetworkConfig // 7 + ErrInvalidNetNS // 8 ErrTryAgainLater uint = 11 ErrInternal uint = 999 ) @@ -173,8 +275,13 @@ func (e *Error) Print() error { // JSON (un)marshallable types type route struct { - Dst IPNet `json:"dst"` - GW net.IP `json:"gw,omitempty"` + Dst IPNet `json:"dst"` + GW net.IP `json:"gw,omitempty"` + MTU int `json:"mtu,omitempty"` + AdvMSS int `json:"advmss,omitempty"` + Priority int `json:"priority,omitempty"` + Table *int `json:"table,omitempty"` + Scope *int `json:"scope,omitempty"` } func (r *Route) UnmarshalJSON(data []byte) error { @@ -185,13 +292,24 @@ func (r *Route) UnmarshalJSON(data []byte) error { r.Dst = net.IPNet(rt.Dst) r.GW = rt.GW + r.MTU = rt.MTU + r.AdvMSS = rt.AdvMSS + r.Priority = rt.Priority + r.Table = rt.Table + r.Scope = rt.Scope + return nil } func (r Route) MarshalJSON() ([]byte, error) { rt := route{ - Dst: IPNet(r.Dst), - GW: r.GW, + Dst: IPNet(r.Dst), + GW: r.GW, + MTU: r.MTU, + AdvMSS: r.AdvMSS, + Priority: r.Priority, + Table: r.Table, + Scope: r.Scope, } return json.Marshal(rt) diff --git a/vendor/github.com/containernetworking/cni/pkg/utils/utils.go b/vendor/github.com/containernetworking/cni/pkg/utils/utils.go index b8ec3887..1981d255 100644 --- a/vendor/github.com/containernetworking/cni/pkg/utils/utils.go +++ b/vendor/github.com/containernetworking/cni/pkg/utils/utils.go @@ -36,7 +36,6 @@ var cniReg = regexp.MustCompile(`^` + cniValidNameChars + `*$`) // ValidateContainerID will validate that the supplied containerID is not empty does not contain invalid characters func ValidateContainerID(containerID string) *types.Error { - if containerID == "" { return types.NewError(types.ErrUnknownContainer, "missing containerID", "") } @@ -48,7 +47,6 @@ func ValidateContainerID(containerID string) *types.Error { // ValidateNetworkName will validate that the supplied networkName does not contain invalid characters func ValidateNetworkName(networkName string) *types.Error { - if networkName == "" { return types.NewError(types.ErrInvalidNetworkConfig, "missing network name:", "") } @@ -58,11 +56,11 @@ func ValidateNetworkName(networkName string) *types.Error { return nil } -// ValidateInterfaceName will validate the interface name based on the three rules below +// ValidateInterfaceName will validate the interface name based on the four rules below // 1. The name must not be empty // 2. The name must be less than 16 characters // 3. The name must not be "." or ".." -// 3. The name must not contain / or : or any whitespace characters +// 4. The name must not contain / or : or any whitespace characters // ref to https://github.com/torvalds/linux/blob/master/net/core/dev.c#L1024 func ValidateInterfaceName(ifName string) *types.Error { if len(ifName) == 0 { diff --git a/vendor/github.com/containernetworking/cni/pkg/version/conf.go b/vendor/github.com/containernetworking/cni/pkg/version/conf.go index 3cca58bb..808c33b8 100644 --- a/vendor/github.com/containernetworking/cni/pkg/version/conf.go +++ b/vendor/github.com/containernetworking/cni/pkg/version/conf.go @@ -15,23 +15,12 @@ package version import ( - "encoding/json" - "fmt" + "github.com/containernetworking/cni/pkg/types/create" ) // ConfigDecoder can decode the CNI version available in network config data type ConfigDecoder struct{} func (*ConfigDecoder) Decode(jsonBytes []byte) (string, error) { - var conf struct { - CNIVersion string `json:"cniVersion"` - } - err := json.Unmarshal(jsonBytes, &conf) - if err != nil { - return "", fmt.Errorf("decoding version from network config: %s", err) - } - if conf.CNIVersion == "" { - return "0.1.0", nil - } - return conf.CNIVersion, nil + return create.DecodeVersion(jsonBytes) } diff --git a/vendor/github.com/containernetworking/cni/pkg/version/plugin.go b/vendor/github.com/containernetworking/cni/pkg/version/plugin.go index 1df42724..e3bd375b 100644 --- a/vendor/github.com/containernetworking/cni/pkg/version/plugin.go +++ b/vendor/github.com/containernetworking/cni/pkg/version/plugin.go @@ -68,7 +68,7 @@ func (*PluginDecoder) Decode(jsonBytes []byte) (PluginInfo, error) { var info pluginInfo err := json.Unmarshal(jsonBytes, &info) if err != nil { - return nil, fmt.Errorf("decoding version info: %s", err) + return nil, fmt.Errorf("decoding version info: %w", err) } if info.CNIVersion_ == "" { return nil, fmt.Errorf("decoding version info: missing field cniVersion") @@ -86,8 +86,8 @@ func (*PluginDecoder) Decode(jsonBytes []byte) (PluginInfo, error) { // minor, and micro numbers or returns an error func ParseVersion(version string) (int, int, int, error) { var major, minor, micro int - if version == "" { - return -1, -1, -1, fmt.Errorf("invalid version %q: the version is empty", version) + if version == "" { // special case: no version declared == v0.1.0 + return 0, 1, 0, nil } parts := strings.Split(version, ".") @@ -97,20 +97,20 @@ func ParseVersion(version string) (int, int, int, error) { major, err := strconv.Atoi(parts[0]) if err != nil { - return -1, -1, -1, fmt.Errorf("failed to convert major version part %q: %v", parts[0], err) + return -1, -1, -1, fmt.Errorf("failed to convert major version part %q: %w", parts[0], err) } if len(parts) >= 2 { minor, err = strconv.Atoi(parts[1]) if err != nil { - return -1, -1, -1, fmt.Errorf("failed to convert minor version part %q: %v", parts[1], err) + return -1, -1, -1, fmt.Errorf("failed to convert minor version part %q: %w", parts[1], err) } } if len(parts) >= 3 { micro, err = strconv.Atoi(parts[2]) if err != nil { - return -1, -1, -1, fmt.Errorf("failed to convert micro version part %q: %v", parts[2], err) + return -1, -1, -1, fmt.Errorf("failed to convert micro version part %q: %w", parts[2], err) } } @@ -142,3 +142,27 @@ func GreaterThanOrEqualTo(version, otherVersion string) (bool, error) { } return false, nil } + +// GreaterThan returns true if the first version is greater than the second +func GreaterThan(version, otherVersion string) (bool, error) { + firstMajor, firstMinor, firstMicro, err := ParseVersion(version) + if err != nil { + return false, err + } + + secondMajor, secondMinor, secondMicro, err := ParseVersion(otherVersion) + if err != nil { + return false, err + } + + if firstMajor > secondMajor { + return true, nil + } else if firstMajor == secondMajor { + if firstMinor > secondMinor { + return true, nil + } else if firstMinor == secondMinor && firstMicro > secondMicro { + return true, nil + } + } + return false, nil +} diff --git a/vendor/github.com/containernetworking/cni/pkg/version/version.go b/vendor/github.com/containernetworking/cni/pkg/version/version.go index 8f3508e6..cfb6a12f 100644 --- a/vendor/github.com/containernetworking/cni/pkg/version/version.go +++ b/vendor/github.com/containernetworking/cni/pkg/version/version.go @@ -19,13 +19,12 @@ import ( "fmt" "github.com/containernetworking/cni/pkg/types" - "github.com/containernetworking/cni/pkg/types/020" - "github.com/containernetworking/cni/pkg/types/current" + "github.com/containernetworking/cni/pkg/types/create" ) // Current reports the version of the CNI spec implemented by this library func Current() string { - return "0.4.0" + return "1.1.0" } // Legacy PluginInfo describes a plugin that is backwards compatible with the @@ -35,48 +34,56 @@ func Current() string { // // Any future CNI spec versions which meet this definition should be added to // this list. -var Legacy = PluginSupports("0.1.0", "0.2.0") -var All = PluginSupports("0.1.0", "0.2.0", "0.3.0", "0.3.1", "0.4.0") +var ( + Legacy = PluginSupports("0.1.0", "0.2.0") + All = PluginSupports("0.1.0", "0.2.0", "0.3.0", "0.3.1", "0.4.0", "1.0.0", "1.1.0") +) -var resultFactories = []struct { - supportedVersions []string - newResult types.ResultFactoryFunc -}{ - {current.SupportedVersions, current.NewResult}, - {types020.SupportedVersions, types020.NewResult}, +// VersionsFrom returns a list of versions starting from min, inclusive +func VersionsStartingFrom(min string) PluginInfo { + out := []string{} + // cheat, just assume ordered + ok := false + for _, v := range All.SupportedVersions() { + if !ok && v == min { + ok = true + } + if ok { + out = append(out, v) + } + } + return PluginSupports(out...) } // Finds a Result object matching the requested version (if any) and asks // that object to parse the plugin result, returning an error if parsing failed. func NewResult(version string, resultBytes []byte) (types.Result, error) { - reconciler := &Reconciler{} - for _, resultFactory := range resultFactories { - err := reconciler.CheckRaw(version, resultFactory.supportedVersions) - if err == nil { - // Result supports this version - return resultFactory.newResult(resultBytes) - } - } - - return nil, fmt.Errorf("unsupported CNI result version %q", version) + return create.Create(version, resultBytes) } // ParsePrevResult parses a prevResult in a NetConf structure and sets // the NetConf's PrevResult member to the parsed Result object. -func ParsePrevResult(conf *types.NetConf) error { +func ParsePrevResult(conf *types.PluginConf) error { if conf.RawPrevResult == nil { return nil } + // Prior to 1.0.0, Result types may not marshal a CNIVersion. Since the + // result version must match the config version, if the Result's version + // is empty, inject the config version. + if ver, ok := conf.RawPrevResult["CNIVersion"]; !ok || ver == "" { + conf.RawPrevResult["CNIVersion"] = conf.CNIVersion + } + resultBytes, err := json.Marshal(conf.RawPrevResult) if err != nil { - return fmt.Errorf("could not serialize prevResult: %v", err) + return fmt.Errorf("could not serialize prevResult: %w", err) } conf.RawPrevResult = nil - conf.PrevResult, err = NewResult(conf.CNIVersion, resultBytes) + conf.PrevResult, err = create.Create(conf.CNIVersion, resultBytes) if err != nil { - return fmt.Errorf("could not parse prevResult: %v", err) + return fmt.Errorf("could not parse prevResult: %w", err) } return nil diff --git a/vendor/github.com/containernetworking/plugins/pkg/ns/README.md b/vendor/github.com/containernetworking/plugins/pkg/ns/README.md index 1e265c7a..e5fef2db 100644 --- a/vendor/github.com/containernetworking/plugins/pkg/ns/README.md +++ b/vendor/github.com/containernetworking/plugins/pkg/ns/README.md @@ -13,10 +13,10 @@ The `ns.Do()` method provides **partial** control over network namespaces for yo ```go err = targetNs.Do(func(hostNs ns.NetNS) error { + linkAttrs := netlink.NewLinkAttrs() + linkAttrs.Name = "dummy0" dummy := &netlink.Dummy{ - LinkAttrs: netlink.LinkAttrs{ - Name: "dummy0", - }, + LinkAttrs: linkAttrs, } return netlink.LinkAdd(dummy) }) diff --git a/vendor/github.com/containernetworking/plugins/pkg/ns/ns_linux.go b/vendor/github.com/containernetworking/plugins/pkg/ns/ns_linux.go index a34f9717..5a6aaa33 100644 --- a/vendor/github.com/containernetworking/plugins/pkg/ns/ns_linux.go +++ b/vendor/github.com/containernetworking/plugins/pkg/ns/ns_linux.go @@ -26,6 +26,15 @@ import ( // Returns an object representing the current OS thread's network namespace func GetCurrentNS() (NetNS, error) { + // Lock the thread in case other goroutine executes in it and changes its + // network namespace after getCurrentThreadNetNSPath(), otherwise it might + // return an unexpected network namespace. + runtime.LockOSThread() + defer runtime.UnlockOSThread() + return getCurrentNSNoLock() +} + +func getCurrentNSNoLock() (NetNS, error) { return GetNS(getCurrentThreadNetNSPath()) } @@ -101,8 +110,8 @@ var _ NetNS = &netNS{} const ( // https://github.com/torvalds/linux/blob/master/include/uapi/linux/magic.h - NSFS_MAGIC = 0x6e736673 - PROCFS_MAGIC = 0x9fa0 + NSFS_MAGIC = unix.NSFS_MAGIC + PROCFS_MAGIC = unix.PROC_SUPER_MAGIC ) type NSPathNotExistErr struct{ msg string } @@ -147,6 +156,54 @@ func GetNS(nspath string) (NetNS, error) { return &netNS{file: fd}, nil } +// Returns a new empty NetNS. +// Calling Close() let the kernel garbage collect the network namespace. +func TempNetNS() (NetNS, error) { + var tempNS NetNS + var err error + var wg sync.WaitGroup + wg.Add(1) + + // Create the new namespace in a new goroutine so that if we later fail + // to switch the namespace back to the original one, we can safely + // leave the thread locked to die without a risk of the current thread + // left lingering with incorrect namespace. + go func() { + defer wg.Done() + runtime.LockOSThread() + + var threadNS NetNS + // save a handle to current network namespace + threadNS, err = getCurrentNSNoLock() + if err != nil { + err = fmt.Errorf("failed to open current namespace: %v", err) + return + } + defer threadNS.Close() + + // create the temporary network namespace + err = unix.Unshare(unix.CLONE_NEWNET) + if err != nil { + return + } + + // get a handle to the temporary network namespace + tempNS, err = getCurrentNSNoLock() + + err2 := threadNS.Set() + if err2 == nil { + // Unlock the current thread only when we successfully switched back + // to the original namespace; otherwise leave the thread locked which + // will force the runtime to scrap the current thread, that is maybe + // not as optimal but at least always safe to do. + runtime.UnlockOSThread() + } + }() + + wg.Wait() + return tempNS, err +} + func (ns *netNS) Path() string { return ns.file.Name() } @@ -168,7 +225,7 @@ func (ns *netNS) Do(toRun func(NetNS) error) error { } containedCall := func(hostNS NetNS) error { - threadNS, err := GetCurrentNS() + threadNS, err := getCurrentNSNoLock() if err != nil { return fmt.Errorf("failed to open current netns: %v", err) } diff --git a/vendor/github.com/containernetworking/plugins/pkg/testutils/bad_reader.go b/vendor/github.com/containernetworking/plugins/pkg/testutils/bad_reader.go new file mode 100644 index 00000000..56a09fd2 --- /dev/null +++ b/vendor/github.com/containernetworking/plugins/pkg/testutils/bad_reader.go @@ -0,0 +1,33 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package testutils + +import "errors" + +// BadReader is an io.Reader which always errors +type BadReader struct { + Error error +} + +func (r *BadReader) Read(_ []byte) (int, error) { + if r.Error != nil { + return 0, r.Error + } + return 0, errors.New("banana") +} + +func (r *BadReader) Close() error { + return nil +} diff --git a/vendor/github.com/containernetworking/plugins/pkg/testutils/cmd.go b/vendor/github.com/containernetworking/plugins/pkg/testutils/cmd.go new file mode 100644 index 00000000..276f9e5a --- /dev/null +++ b/vendor/github.com/containernetworking/plugins/pkg/testutils/cmd.go @@ -0,0 +1,125 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package testutils + +import ( + "io" + "os" + + "github.com/containernetworking/cni/pkg/skel" + "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/version" +) + +func envCleanup() { + os.Unsetenv("CNI_COMMAND") + os.Unsetenv("CNI_PATH") + os.Unsetenv("CNI_NETNS") + os.Unsetenv("CNI_IFNAME") + os.Unsetenv("CNI_CONTAINERID") + os.Unsetenv("CNI_NETNS_OVERRIDE") +} + +func CmdAdd(cniNetns, cniContainerID, cniIfname string, conf []byte, f func() error) (types.Result, []byte, error) { + os.Setenv("CNI_COMMAND", "ADD") + os.Setenv("CNI_PATH", os.Getenv("PATH")) + os.Setenv("CNI_NETNS", cniNetns) + os.Setenv("CNI_IFNAME", cniIfname) + os.Setenv("CNI_CONTAINERID", cniContainerID) + os.Setenv("CNI_NETNS_OVERRIDE", "1") + defer envCleanup() + + // Redirect stdout to capture plugin result + oldStdout := os.Stdout + r, w, err := os.Pipe() + if err != nil { + return nil, nil, err + } + + os.Stdout = w + err = f() + w.Close() + + var out []byte + if err == nil { + out, err = io.ReadAll(r) + } + os.Stdout = oldStdout + + // Return errors after restoring stdout so Ginkgo will correctly + // emit verbose error information on stdout + if err != nil { + return nil, nil, err + } + + // Plugin must return result in same version as specified in netconf + versionDecoder := &version.ConfigDecoder{} + confVersion, err := versionDecoder.Decode(conf) + if err != nil { + return nil, nil, err + } + + result, err := version.NewResult(confVersion, out) + if err != nil { + return nil, nil, err + } + + return result, out, nil +} + +func CmdAddWithArgs(args *skel.CmdArgs, f func() error) (types.Result, []byte, error) { + return CmdAdd(args.Netns, args.ContainerID, args.IfName, args.StdinData, f) +} + +func CmdCheck(cniNetns, cniContainerID, cniIfname string, f func() error) error { + os.Setenv("CNI_COMMAND", "CHECK") + os.Setenv("CNI_PATH", os.Getenv("PATH")) + os.Setenv("CNI_NETNS", cniNetns) + os.Setenv("CNI_IFNAME", cniIfname) + os.Setenv("CNI_CONTAINERID", cniContainerID) + os.Setenv("CNI_NETNS_OVERRIDE", "1") + defer envCleanup() + + return f() +} + +func CmdCheckWithArgs(args *skel.CmdArgs, f func() error) error { + return CmdCheck(args.Netns, args.ContainerID, args.IfName, f) +} + +func CmdDel(cniNetns, cniContainerID, cniIfname string, f func() error) error { + os.Setenv("CNI_COMMAND", "DEL") + os.Setenv("CNI_PATH", os.Getenv("PATH")) + os.Setenv("CNI_NETNS", cniNetns) + os.Setenv("CNI_IFNAME", cniIfname) + os.Setenv("CNI_CONTAINERID", cniContainerID) + os.Setenv("CNI_NETNS_OVERRIDE", "1") + defer envCleanup() + + return f() +} + +func CmdDelWithArgs(args *skel.CmdArgs, f func() error) error { + return CmdDel(args.Netns, args.ContainerID, args.IfName, f) +} + +func CmdStatus(f func() error) error { + os.Setenv("CNI_COMMAND", "STATUS") + os.Setenv("CNI_PATH", os.Getenv("PATH")) + os.Setenv("CNI_NETNS_OVERRIDE", "1") + defer envCleanup() + + return f() +} diff --git a/vendor/github.com/containernetworking/plugins/pkg/testutils/dns.go b/vendor/github.com/containernetworking/plugins/pkg/testutils/dns.go new file mode 100644 index 00000000..bd0de0a8 --- /dev/null +++ b/vendor/github.com/containernetworking/plugins/pkg/testutils/dns.go @@ -0,0 +1,59 @@ +// Copyright 2019 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package testutils + +import ( + "fmt" + "os" + "strings" + + "github.com/containernetworking/cni/pkg/types" +) + +// TmpResolvConf will create a temporary file and write the provided DNS settings to +// it in the resolv.conf format. It returns the path of the created temporary file or +// an error if any occurs while creating/writing the file. It is the caller's +// responsibility to remove the file. +func TmpResolvConf(dnsConf types.DNS) (string, error) { + f, err := os.CreateTemp("", "cni_test_resolv.conf") + if err != nil { + return "", fmt.Errorf("failed to get temp file for CNI test resolv.conf: %v", err) + } + defer f.Close() + + path := f.Name() + defer func() { + if err != nil { + os.RemoveAll(path) + } + }() + + // see "man 5 resolv.conf" for the format of resolv.conf + var resolvConfLines []string + for _, nameserver := range dnsConf.Nameservers { + resolvConfLines = append(resolvConfLines, fmt.Sprintf("nameserver %s", nameserver)) + } + resolvConfLines = append(resolvConfLines, fmt.Sprintf("domain %s", dnsConf.Domain)) + resolvConfLines = append(resolvConfLines, fmt.Sprintf("search %s", strings.Join(dnsConf.Search, " "))) + resolvConfLines = append(resolvConfLines, fmt.Sprintf("options %s", strings.Join(dnsConf.Options, " "))) + + resolvConf := strings.Join(resolvConfLines, "\n") + _, err = f.Write([]byte(resolvConf)) + if err != nil { + return "", fmt.Errorf("failed to write temp resolv.conf for CNI test: %v", err) + } + + return path, err +} diff --git a/vendor/github.com/containernetworking/plugins/pkg/testutils/netns_linux.go b/vendor/github.com/containernetworking/plugins/pkg/testutils/netns_linux.go new file mode 100644 index 00000000..b467eb3c --- /dev/null +++ b/vendor/github.com/containernetworking/plugins/pkg/testutils/netns_linux.go @@ -0,0 +1,176 @@ +// Copyright 2018 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package testutils + +import ( + "crypto/rand" + "fmt" + "os" + "path" + "runtime" + "strings" + "sync" + "syscall" + + "golang.org/x/sys/unix" + + "github.com/containernetworking/plugins/pkg/ns" +) + +func getNsRunDir() string { + xdgRuntimeDir := os.Getenv("XDG_RUNTIME_DIR") + + /// If XDG_RUNTIME_DIR is set, check if the current user owns /var/run. If + // the owner is different, we are most likely running in a user namespace. + // In that case use $XDG_RUNTIME_DIR/netns as runtime dir. + if xdgRuntimeDir != "" { + if s, err := os.Stat("/var/run"); err == nil { + st, ok := s.Sys().(*syscall.Stat_t) + if ok && int(st.Uid) != os.Geteuid() { + return path.Join(xdgRuntimeDir, "netns") + } + } + } + + return "/var/run/netns" +} + +// Creates a new persistent (bind-mounted) network namespace and returns an object +// representing that namespace, without switching to it. +func NewNS() (ns.NetNS, error) { + nsRunDir := getNsRunDir() + + b := make([]byte, 16) + _, err := rand.Read(b) + if err != nil { + return nil, fmt.Errorf("failed to generate random netns name: %v", err) + } + + // Create the directory for mounting network namespaces + // This needs to be a shared mountpoint in case it is mounted in to + // other namespaces (containers) + err = os.MkdirAll(nsRunDir, 0o755) + if err != nil { + return nil, err + } + + // Remount the namespace directory shared. This will fail if it is not + // already a mountpoint, so bind-mount it on to itself to "upgrade" it + // to a mountpoint. + err = unix.Mount("", nsRunDir, "none", unix.MS_SHARED|unix.MS_REC, "") + if err != nil { + if err != unix.EINVAL { + return nil, fmt.Errorf("mount --make-rshared %s failed: %q", nsRunDir, err) + } + + // Recursively remount /var/run/netns on itself. The recursive flag is + // so that any existing netns bindmounts are carried over. + err = unix.Mount(nsRunDir, nsRunDir, "none", unix.MS_BIND|unix.MS_REC, "") + if err != nil { + return nil, fmt.Errorf("mount --rbind %s %s failed: %q", nsRunDir, nsRunDir, err) + } + + // Now we can make it shared + err = unix.Mount("", nsRunDir, "none", unix.MS_SHARED|unix.MS_REC, "") + if err != nil { + return nil, fmt.Errorf("mount --make-rshared %s failed: %q", nsRunDir, err) + } + + } + + nsName := fmt.Sprintf("cnitest-%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]) + + // create an empty file at the mount point + nsPath := path.Join(nsRunDir, nsName) + mountPointFd, err := os.Create(nsPath) + if err != nil { + return nil, err + } + mountPointFd.Close() + + // Ensure the mount point is cleaned up on errors; if the namespace + // was successfully mounted this will have no effect because the file + // is in-use + defer os.RemoveAll(nsPath) + + var wg sync.WaitGroup + wg.Add(1) + + // do namespace work in a dedicated goroutine, so that we can safely + // Lock/Unlock OSThread without upsetting the lock/unlock state of + // the caller of this function + go (func() { + defer wg.Done() + runtime.LockOSThread() + // Don't unlock. By not unlocking, golang will kill the OS thread when the + // goroutine is done (for go1.10+) + + var origNS ns.NetNS + origNS, err = ns.GetNS(getCurrentThreadNetNSPath()) + if err != nil { + return + } + defer origNS.Close() + + // create a new netns on the current thread + err = unix.Unshare(unix.CLONE_NEWNET) + if err != nil { + return + } + + // Put this thread back to the orig ns, since it might get reused (pre go1.10) + defer origNS.Set() + + // bind mount the netns from the current thread (from /proc) onto the + // mount point. This causes the namespace to persist, even when there + // are no threads in the ns. + err = unix.Mount(getCurrentThreadNetNSPath(), nsPath, "none", unix.MS_BIND, "") + if err != nil { + err = fmt.Errorf("failed to bind mount ns at %s: %v", nsPath, err) + } + })() + wg.Wait() + + if err != nil { + return nil, fmt.Errorf("failed to create namespace: %v", err) + } + + return ns.GetNS(nsPath) +} + +// UnmountNS unmounts the NS held by the netns object +func UnmountNS(ns ns.NetNS) error { + nsPath := ns.Path() + // Only unmount if it's been bind-mounted (don't touch namespaces in /proc...) + if strings.HasPrefix(nsPath, getNsRunDir()) { + if err := unix.Unmount(nsPath, 0); err != nil { + return fmt.Errorf("failed to unmount NS: at %s: %v", nsPath, err) + } + + if err := os.Remove(nsPath); err != nil { + return fmt.Errorf("failed to remove ns path %s: %v", nsPath, err) + } + } + + return nil +} + +// getCurrentThreadNetNSPath copied from pkg/ns +func getCurrentThreadNetNSPath() string { + // /proc/self/ns/net returns the namespace of the main thread, not + // of whatever thread this goroutine is running on. Make sure we + // use the thread's net namespace since the thread is switching around + return fmt.Sprintf("/proc/%d/task/%d/ns/net", os.Getpid(), unix.Gettid()) +} diff --git a/vendor/github.com/containernetworking/plugins/pkg/testutils/ping.go b/vendor/github.com/containernetworking/plugins/pkg/testutils/ping.go new file mode 100644 index 00000000..8c47c3d7 --- /dev/null +++ b/vendor/github.com/containernetworking/plugins/pkg/testutils/ping.go @@ -0,0 +1,61 @@ +// Copyright 2017 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package testutils + +import ( + "bytes" + "fmt" + "net" + "os/exec" + "strconv" + "syscall" +) + +// Ping shells out to the `ping` command. Returns nil if successful. +func Ping(saddr, daddr string, timeoutSec int) error { + ip := net.ParseIP(saddr) + if ip == nil { + return fmt.Errorf("failed to parse IP %q", saddr) + } + + bin := "ping6" + if ip.To4() != nil { + bin = "ping" + } + + args := []string{ + "-c", "1", + "-W", strconv.Itoa(timeoutSec), + "-I", saddr, + daddr, + } + + cmd := exec.Command(bin, args...) + var stderr bytes.Buffer + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + switch e := err.(type) { + case *exec.ExitError: + return fmt.Errorf("%v exit status %d: %s", + args, e.Sys().(syscall.WaitStatus).ExitStatus(), + stderr.String()) + default: + return err + } + } + + return nil +} diff --git a/vendor/github.com/containernetworking/plugins/pkg/testutils/testing.go b/vendor/github.com/containernetworking/plugins/pkg/testutils/testing.go new file mode 100644 index 00000000..9f5140fc --- /dev/null +++ b/vendor/github.com/containernetworking/plugins/pkg/testutils/testing.go @@ -0,0 +1,61 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package testutils + +import ( + "github.com/containernetworking/cni/pkg/version" +) + +// AllSpecVersions contains all CNI spec version numbers +var AllSpecVersions = [...]string{"0.1.0", "0.2.0", "0.3.0", "0.3.1", "0.4.0", "1.0.0", "1.1.0"} + +// SpecVersionHasIPVersion returns true if the given CNI specification version +// includes the "version" field in the IP address elements +func SpecVersionHasIPVersion(ver string) bool { + for _, i := range []string{"0.3.0", "0.3.1", "0.4.0"} { + if ver == i { + return true + } + } + return false +} + +// SpecVersionHasCHECK returns true if the given CNI specification version +// supports the CHECK command +func SpecVersionHasCHECK(ver string) bool { + ok, _ := version.GreaterThanOrEqualTo(ver, "0.4.0") + return ok +} + +// SpecVersionHasSTATUS returns true if the given CNI specification version +// supports the STATUS command +func SpecVersionHasSTATUS(ver string) bool { + ok, _ := version.GreaterThanOrEqualTo(ver, "1.1.0") + return ok +} + +// SpecVersionHasChaining returns true if the given CNI specification version +// supports plugin chaining +func SpecVersionHasChaining(ver string) bool { + ok, _ := version.GreaterThanOrEqualTo(ver, "0.3.0") + return ok +} + +// SpecVersionHasMultipleIPs returns true if the given CNI specification version +// supports more than one IP address of each family +func SpecVersionHasMultipleIPs(ver string) bool { + ok, _ := version.GreaterThanOrEqualTo(ver, "0.3.0") + return ok +} diff --git a/vendor/github.com/emicklei/go-restful/v3/.travis.yml b/vendor/github.com/emicklei/go-restful/v3/.travis.yml deleted file mode 100644 index 3a0bf5ff..00000000 --- a/vendor/github.com/emicklei/go-restful/v3/.travis.yml +++ /dev/null @@ -1,13 +0,0 @@ -language: go - -go: - - 1.x - -before_install: - - go test -v - -script: - - go test -race -coverprofile=coverage.txt -covermode=atomic - -after_success: - - bash <(curl -s https://codecov.io/bash) \ No newline at end of file diff --git a/vendor/github.com/emicklei/go-restful/v3/CHANGES.md b/vendor/github.com/emicklei/go-restful/v3/CHANGES.md index 6f24dfff..4fcd920a 100644 --- a/vendor/github.com/emicklei/go-restful/v3/CHANGES.md +++ b/vendor/github.com/emicklei/go-restful/v3/CHANGES.md @@ -1,5 +1,9 @@ # Change history of go-restful +## [v3.13.0] - 2025-08-14 + +- optimize performance of path matching in CurlyRouter ( thanks @wenhuang, Wen Huang) + ## [v3.12.2] - 2025-02-21 - allow empty payloads in post,put,patch, issue #580 ( thanks @liggitt, Jordan Liggitt) diff --git a/vendor/github.com/emicklei/go-restful/v3/README.md b/vendor/github.com/emicklei/go-restful/v3/README.md index 3fb40d19..50a79ab6 100644 --- a/vendor/github.com/emicklei/go-restful/v3/README.md +++ b/vendor/github.com/emicklei/go-restful/v3/README.md @@ -84,6 +84,7 @@ func (u UserResource) findUser(request *restful.Request, response *restful.Respo - Configurable (trace) logging - Customizable gzip/deflate readers and writers using CompressorProvider registration - Inject your own http.Handler using the `HttpMiddlewareHandlerToFilter` function +- Added `SetPathTokenCacheEnabled` and `SetCustomVerbCacheEnabled` to disable regexp caching (default=true) ## How to customize There are several hooks to customize the behavior of the go-restful package. diff --git a/vendor/github.com/emicklei/go-restful/v3/curly.go b/vendor/github.com/emicklei/go-restful/v3/curly.go index 6fd2bcd5..eec43bfd 100644 --- a/vendor/github.com/emicklei/go-restful/v3/curly.go +++ b/vendor/github.com/emicklei/go-restful/v3/curly.go @@ -9,11 +9,35 @@ import ( "regexp" "sort" "strings" + "sync" ) // CurlyRouter expects Routes with paths that contain zero or more parameters in curly brackets. type CurlyRouter struct{} +var ( + regexCache sync.Map // Cache for compiled regex patterns + pathTokenCacheEnabled = true // Enable/disable path token regex caching +) + +// SetPathTokenCacheEnabled enables or disables path token regex caching for CurlyRouter. +// When disabled, regex patterns will be compiled on every request. +// When enabled (default), compiled regex patterns are cached for better performance. +func SetPathTokenCacheEnabled(enabled bool) { + pathTokenCacheEnabled = enabled +} + +// getCachedRegexp retrieves a compiled regex from the cache if found and valid. +// Returns the regex and true if found and valid, nil and false otherwise. +func getCachedRegexp(cache *sync.Map, pattern string) (*regexp.Regexp, bool) { + if cached, found := cache.Load(pattern); found { + if regex, ok := cached.(*regexp.Regexp); ok { + return regex, true + } + } + return nil, false +} + // SelectRoute is part of the Router interface and returns the best match // for the WebService and its Route for the given Request. func (c CurlyRouter) SelectRoute( @@ -113,8 +137,28 @@ func (c CurlyRouter) regularMatchesPathToken(routeToken string, colon int, reque } return true, true } - matched, err := regexp.MatchString(regPart, requestToken) - return (matched && err == nil), false + + // Check cache first (if enabled) + if pathTokenCacheEnabled { + if regex, found := getCachedRegexp(®exCache, regPart); found { + matched := regex.MatchString(requestToken) + return matched, false + } + } + + // Compile the regex + regex, err := regexp.Compile(regPart) + if err != nil { + return false, false + } + + // Cache the regex (if enabled) + if pathTokenCacheEnabled { + regexCache.Store(regPart, regex) + } + + matched := regex.MatchString(requestToken) + return matched, false } var jsr311Router = RouterJSR311{} @@ -168,7 +212,7 @@ func (c CurlyRouter) computeWebserviceScore(requestTokens []string, routeTokens if matchesToken { score++ // extra score for regex match } - } + } } else { // not a parameter if eachRequestToken != eachRouteToken { diff --git a/vendor/github.com/emicklei/go-restful/v3/custom_verb.go b/vendor/github.com/emicklei/go-restful/v3/custom_verb.go index bfc17efd..0b98eeb0 100644 --- a/vendor/github.com/emicklei/go-restful/v3/custom_verb.go +++ b/vendor/github.com/emicklei/go-restful/v3/custom_verb.go @@ -1,14 +1,28 @@ package restful +// Copyright 2025 Ernest Micklei. All rights reserved. +// Use of this source code is governed by a license +// that can be found in the LICENSE file. + import ( "fmt" "regexp" + "sync" ) var ( - customVerbReg = regexp.MustCompile(":([A-Za-z]+)$") + customVerbReg = regexp.MustCompile(":([A-Za-z]+)$") + customVerbCache sync.Map // Cache for compiled custom verb regexes + customVerbCacheEnabled = true // Enable/disable custom verb regex caching ) +// SetCustomVerbCacheEnabled enables or disables custom verb regex caching. +// When disabled, custom verb regex patterns will be compiled on every request. +// When enabled (default), compiled custom verb regex patterns are cached for better performance. +func SetCustomVerbCacheEnabled(enabled bool) { + customVerbCacheEnabled = enabled +} + func hasCustomVerb(routeToken string) bool { return customVerbReg.MatchString(routeToken) } @@ -20,7 +34,23 @@ func isMatchCustomVerb(routeToken string, pathToken string) bool { } customVerb := rs[1] - specificVerbReg := regexp.MustCompile(fmt.Sprintf(":%s$", customVerb)) + regexPattern := fmt.Sprintf(":%s$", customVerb) + + // Check cache first (if enabled) + if customVerbCacheEnabled { + if specificVerbReg, found := getCachedRegexp(&customVerbCache, regexPattern); found { + return specificVerbReg.MatchString(pathToken) + } + } + + // Compile the regex + specificVerbReg := regexp.MustCompile(regexPattern) + + // Cache the regex (if enabled) + if customVerbCacheEnabled { + customVerbCache.Store(regexPattern, specificVerbReg) + } + return specificVerbReg.MatchString(pathToken) } diff --git a/vendor/github.com/emicklei/go-restful/v3/doc.go b/vendor/github.com/emicklei/go-restful/v3/doc.go index 69b13057..80809225 100644 --- a/vendor/github.com/emicklei/go-restful/v3/doc.go +++ b/vendor/github.com/emicklei/go-restful/v3/doc.go @@ -1,7 +1,7 @@ /* Package restful , a lean package for creating REST-style WebServices without magic. -WebServices and Routes +### WebServices and Routes A WebService has a collection of Route objects that dispatch incoming Http Requests to a function calls. Typically, a WebService has a root path (e.g. /users) and defines common MIME types for its routes. @@ -30,14 +30,14 @@ The (*Request, *Response) arguments provide functions for reading information fr See the example https://github.com/emicklei/go-restful/blob/v3/examples/user-resource/restful-user-resource.go with a full implementation. -Regular expression matching Routes +### Regular expression matching Routes A Route parameter can be specified using the format "uri/{var[:regexp]}" or the special version "uri/{var:*}" for matching the tail of the path. For example, /persons/{name:[A-Z][A-Z]} can be used to restrict values for the parameter "name" to only contain capital alphabetic characters. Regular expressions must use the standard Go syntax as described in the regexp package. (https://code.google.com/p/re2/wiki/Syntax) This feature requires the use of a CurlyRouter. -Containers +### Containers A Container holds a collection of WebServices, Filters and a http.ServeMux for multiplexing http requests. Using the statements "restful.Add(...) and restful.Filter(...)" will register WebServices and Filters to the Default Container. @@ -47,7 +47,7 @@ You can create your own Container and create a new http.Server for that particul container := restful.NewContainer() server := &http.Server{Addr: ":8081", Handler: container} -Filters +### Filters A filter dynamically intercepts requests and responses to transform or use the information contained in the requests or responses. You can use filters to perform generic logging, measurement, authentication, redirect, set response headers etc. @@ -60,22 +60,21 @@ Use the following statement to pass the request,response pair to the next filter chain.ProcessFilter(req, resp) -Container Filters +### Container Filters These are processed before any registered WebService. // install a (global) filter for the default container (processed before any webservice) restful.Filter(globalLogging) -WebService Filters +### WebService Filters These are processed before any Route of a WebService. // install a webservice filter (processed before any route) ws.Filter(webserviceLogging).Filter(measureTime) - -Route Filters +### Route Filters These are processed before calling the function associated with the Route. @@ -84,7 +83,7 @@ These are processed before calling the function associated with the Route. See the example https://github.com/emicklei/go-restful/blob/v3/examples/filters/restful-filters.go with full implementations. -Response Encoding +### Response Encoding Two encodings are supported: gzip and deflate. To enable this for all responses: @@ -95,20 +94,20 @@ Alternatively, you can create a Filter that performs the encoding and install it See the example https://github.com/emicklei/go-restful/blob/v3/examples/encoding/restful-encoding-filter.go -OPTIONS support +### OPTIONS support By installing a pre-defined container filter, your Webservice(s) can respond to the OPTIONS Http request. Filter(OPTIONSFilter()) -CORS +### CORS By installing the filter of a CrossOriginResourceSharing (CORS), your WebService(s) can handle CORS requests. cors := CrossOriginResourceSharing{ExposeHeaders: []string{"X-My-Header"}, CookiesAllowed: false, Container: DefaultContainer} Filter(cors.Filter) -Error Handling +### Error Handling Unexpected things happen. If a request cannot be processed because of a failure, your service needs to tell via the response what happened and why. For this reason HTTP status codes exist and it is important to use the correct code in every exceptional situation. @@ -137,11 +136,11 @@ The request does not have or has an unknown Accept Header set for this operation The request does not have or has an unknown Content-Type Header set for this operation. -ServiceError +### ServiceError In addition to setting the correct (error) Http status code, you can choose to write a ServiceError message on the response. -Performance options +### Performance options This package has several options that affect the performance of your service. It is important to understand them and how you can change it. @@ -156,30 +155,27 @@ Default value is true If content encoding is enabled then the default strategy for getting new gzip/zlib writers and readers is to use a sync.Pool. Because writers are expensive structures, performance is even more improved when using a preloaded cache. You can also inject your own implementation. -Trouble shooting +### Trouble shooting This package has the means to produce detail logging of the complete Http request matching process and filter invocation. Enabling this feature requires you to set an implementation of restful.StdLogger (e.g. log.Logger) instance such as: restful.TraceLogger(log.New(os.Stdout, "[restful] ", log.LstdFlags|log.Lshortfile)) -Logging +### Logging The restful.SetLogger() method allows you to override the logger used by the package. By default restful uses the standard library `log` package and logs to stdout. Different logging packages are supported as long as they conform to `StdLogger` interface defined in the `log` sub-package, writing an adapter for your preferred package is simple. -Resources +### Resources -[project]: https://github.com/emicklei/go-restful +(c) 2012-2025, http://ernestmicklei.com. MIT License +[project]: https://github.com/emicklei/go-restful [examples]: https://github.com/emicklei/go-restful/blob/master/examples - -[design]: http://ernestmicklei.com/2012/11/11/go-restful-api-design/ - +[design]: http://ernestmicklei.com/2012/11/11/go-restful-api-design/ [showcases]: https://github.com/emicklei/mora, https://github.com/emicklei/landskape - -(c) 2012-2015, http://ernestmicklei.com. MIT License */ package restful diff --git a/vendor/github.com/evanphx/json-patch/v5/LICENSE b/vendor/github.com/evanphx/json-patch/v5/LICENSE new file mode 100644 index 00000000..df76d7d7 --- /dev/null +++ b/vendor/github.com/evanphx/json-patch/v5/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) 2014, Evan Phoenix +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +* Neither the name of the Evan Phoenix nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/evanphx/json-patch/v5/errors.go b/vendor/github.com/evanphx/json-patch/v5/errors.go new file mode 100644 index 00000000..75304b44 --- /dev/null +++ b/vendor/github.com/evanphx/json-patch/v5/errors.go @@ -0,0 +1,38 @@ +package jsonpatch + +import "fmt" + +// AccumulatedCopySizeError is an error type returned when the accumulated size +// increase caused by copy operations in a patch operation has exceeded the +// limit. +type AccumulatedCopySizeError struct { + limit int64 + accumulated int64 +} + +// NewAccumulatedCopySizeError returns an AccumulatedCopySizeError. +func NewAccumulatedCopySizeError(l, a int64) *AccumulatedCopySizeError { + return &AccumulatedCopySizeError{limit: l, accumulated: a} +} + +// Error implements the error interface. +func (a *AccumulatedCopySizeError) Error() string { + return fmt.Sprintf("Unable to complete the copy, the accumulated size increase of copy is %d, exceeding the limit %d", a.accumulated, a.limit) +} + +// ArraySizeError is an error type returned when the array size has exceeded +// the limit. +type ArraySizeError struct { + limit int + size int +} + +// NewArraySizeError returns an ArraySizeError. +func NewArraySizeError(l, s int) *ArraySizeError { + return &ArraySizeError{limit: l, size: s} +} + +// Error implements the error interface. +func (a *ArraySizeError) Error() string { + return fmt.Sprintf("Unable to create array of size %d, limit is %d", a.size, a.limit) +} diff --git a/vendor/github.com/evanphx/json-patch/v5/internal/json/decode.go b/vendor/github.com/evanphx/json-patch/v5/internal/json/decode.go new file mode 100644 index 00000000..e9bb0efe --- /dev/null +++ b/vendor/github.com/evanphx/json-patch/v5/internal/json/decode.go @@ -0,0 +1,1385 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Represents JSON data structure using native Go types: booleans, floats, +// strings, arrays, and maps. + +package json + +import ( + "encoding" + "encoding/base64" + "fmt" + "reflect" + "strconv" + "strings" + "sync" + "unicode" + "unicode/utf16" + "unicode/utf8" +) + +// Unmarshal parses the JSON-encoded data and stores the result +// in the value pointed to by v. If v is nil or not a pointer, +// Unmarshal returns an InvalidUnmarshalError. +// +// Unmarshal uses the inverse of the encodings that +// Marshal uses, allocating maps, slices, and pointers as necessary, +// with the following additional rules: +// +// To unmarshal JSON into a pointer, Unmarshal first handles the case of +// the JSON being the JSON literal null. In that case, Unmarshal sets +// the pointer to nil. Otherwise, Unmarshal unmarshals the JSON into +// the value pointed at by the pointer. If the pointer is nil, Unmarshal +// allocates a new value for it to point to. +// +// To unmarshal JSON into a value implementing the Unmarshaler interface, +// Unmarshal calls that value's UnmarshalJSON method, including +// when the input is a JSON null. +// Otherwise, if the value implements encoding.TextUnmarshaler +// and the input is a JSON quoted string, Unmarshal calls that value's +// UnmarshalText method with the unquoted form of the string. +// +// To unmarshal JSON into a struct, Unmarshal matches incoming object +// keys to the keys used by Marshal (either the struct field name or its tag), +// preferring an exact match but also accepting a case-insensitive match. By +// default, object keys which don't have a corresponding struct field are +// ignored (see Decoder.DisallowUnknownFields for an alternative). +// +// To unmarshal JSON into an interface value, +// Unmarshal stores one of these in the interface value: +// +// bool, for JSON booleans +// float64, for JSON numbers +// string, for JSON strings +// []interface{}, for JSON arrays +// map[string]interface{}, for JSON objects +// nil for JSON null +// +// To unmarshal a JSON array into a slice, Unmarshal resets the slice length +// to zero and then appends each element to the slice. +// As a special case, to unmarshal an empty JSON array into a slice, +// Unmarshal replaces the slice with a new empty slice. +// +// To unmarshal a JSON array into a Go array, Unmarshal decodes +// JSON array elements into corresponding Go array elements. +// If the Go array is smaller than the JSON array, +// the additional JSON array elements are discarded. +// If the JSON array is smaller than the Go array, +// the additional Go array elements are set to zero values. +// +// To unmarshal a JSON object into a map, Unmarshal first establishes a map to +// use. If the map is nil, Unmarshal allocates a new map. Otherwise Unmarshal +// reuses the existing map, keeping existing entries. Unmarshal then stores +// key-value pairs from the JSON object into the map. The map's key type must +// either be any string type, an integer, implement json.Unmarshaler, or +// implement encoding.TextUnmarshaler. +// +// If the JSON-encoded data contain a syntax error, Unmarshal returns a SyntaxError. +// +// If a JSON value is not appropriate for a given target type, +// or if a JSON number overflows the target type, Unmarshal +// skips that field and completes the unmarshaling as best it can. +// If no more serious errors are encountered, Unmarshal returns +// an UnmarshalTypeError describing the earliest such error. In any +// case, it's not guaranteed that all the remaining fields following +// the problematic one will be unmarshaled into the target object. +// +// The JSON null value unmarshals into an interface, map, pointer, or slice +// by setting that Go value to nil. Because null is often used in JSON to mean +// “not present,” unmarshaling a JSON null into any other Go type has no effect +// on the value and produces no error. +// +// When unmarshaling quoted strings, invalid UTF-8 or +// invalid UTF-16 surrogate pairs are not treated as an error. +// Instead, they are replaced by the Unicode replacement +// character U+FFFD. +func Unmarshal(data []byte, v any) error { + // Check for well-formedness. + // Avoids filling out half a data structure + // before discovering a JSON syntax error. + d := ds.Get().(*decodeState) + defer ds.Put(d) + //var d decodeState + d.useNumber = true + err := checkValid(data, &d.scan) + if err != nil { + return err + } + + d.init(data) + return d.unmarshal(v) +} + +var ds = sync.Pool{ + New: func() any { + return new(decodeState) + }, +} + +func UnmarshalWithKeys(data []byte, v any) ([]string, error) { + // Check for well-formedness. + // Avoids filling out half a data structure + // before discovering a JSON syntax error. + + d := ds.Get().(*decodeState) + defer ds.Put(d) + //var d decodeState + d.useNumber = true + err := checkValid(data, &d.scan) + if err != nil { + return nil, err + } + + d.init(data) + err = d.unmarshal(v) + if err != nil { + return nil, err + } + + return d.lastKeys, nil +} + +func UnmarshalValid(data []byte, v any) error { + // Check for well-formedness. + // Avoids filling out half a data structure + // before discovering a JSON syntax error. + d := ds.Get().(*decodeState) + defer ds.Put(d) + //var d decodeState + d.useNumber = true + + d.init(data) + return d.unmarshal(v) +} + +func UnmarshalValidWithKeys(data []byte, v any) ([]string, error) { + // Check for well-formedness. + // Avoids filling out half a data structure + // before discovering a JSON syntax error. + + d := ds.Get().(*decodeState) + defer ds.Put(d) + //var d decodeState + d.useNumber = true + + d.init(data) + err := d.unmarshal(v) + if err != nil { + return nil, err + } + + return d.lastKeys, nil +} + +// Unmarshaler is the interface implemented by types +// that can unmarshal a JSON description of themselves. +// The input can be assumed to be a valid encoding of +// a JSON value. UnmarshalJSON must copy the JSON data +// if it wishes to retain the data after returning. +// +// By convention, to approximate the behavior of Unmarshal itself, +// Unmarshalers implement UnmarshalJSON([]byte("null")) as a no-op. +type Unmarshaler interface { + UnmarshalJSON([]byte) error +} + +// An UnmarshalTypeError describes a JSON value that was +// not appropriate for a value of a specific Go type. +type UnmarshalTypeError struct { + Value string // description of JSON value - "bool", "array", "number -5" + Type reflect.Type // type of Go value it could not be assigned to + Offset int64 // error occurred after reading Offset bytes + Struct string // name of the struct type containing the field + Field string // the full path from root node to the field +} + +func (e *UnmarshalTypeError) Error() string { + if e.Struct != "" || e.Field != "" { + return "json: cannot unmarshal " + e.Value + " into Go struct field " + e.Struct + "." + e.Field + " of type " + e.Type.String() + } + return "json: cannot unmarshal " + e.Value + " into Go value of type " + e.Type.String() +} + +// An UnmarshalFieldError describes a JSON object key that +// led to an unexported (and therefore unwritable) struct field. +// +// Deprecated: No longer used; kept for compatibility. +type UnmarshalFieldError struct { + Key string + Type reflect.Type + Field reflect.StructField +} + +func (e *UnmarshalFieldError) Error() string { + return "json: cannot unmarshal object key " + strconv.Quote(e.Key) + " into unexported field " + e.Field.Name + " of type " + e.Type.String() +} + +// An InvalidUnmarshalError describes an invalid argument passed to Unmarshal. +// (The argument to Unmarshal must be a non-nil pointer.) +type InvalidUnmarshalError struct { + Type reflect.Type +} + +func (e *InvalidUnmarshalError) Error() string { + if e.Type == nil { + return "json: Unmarshal(nil)" + } + + if e.Type.Kind() != reflect.Pointer { + return "json: Unmarshal(non-pointer " + e.Type.String() + ")" + } + return "json: Unmarshal(nil " + e.Type.String() + ")" +} + +func (d *decodeState) unmarshal(v any) error { + rv := reflect.ValueOf(v) + if rv.Kind() != reflect.Pointer || rv.IsNil() { + return &InvalidUnmarshalError{reflect.TypeOf(v)} + } + + d.scan.reset() + d.scanWhile(scanSkipSpace) + // We decode rv not rv.Elem because the Unmarshaler interface + // test must be applied at the top level of the value. + err := d.value(rv) + if err != nil { + return d.addErrorContext(err) + } + return d.savedError +} + +// A Number represents a JSON number literal. +type Number string + +// String returns the literal text of the number. +func (n Number) String() string { return string(n) } + +// Float64 returns the number as a float64. +func (n Number) Float64() (float64, error) { + return strconv.ParseFloat(string(n), 64) +} + +// Int64 returns the number as an int64. +func (n Number) Int64() (int64, error) { + return strconv.ParseInt(string(n), 10, 64) +} + +// An errorContext provides context for type errors during decoding. +type errorContext struct { + Struct reflect.Type + FieldStack []string +} + +// decodeState represents the state while decoding a JSON value. +type decodeState struct { + data []byte + off int // next read offset in data + opcode int // last read result + scan scanner + errorContext *errorContext + savedError error + useNumber bool + disallowUnknownFields bool + lastKeys []string +} + +// readIndex returns the position of the last byte read. +func (d *decodeState) readIndex() int { + return d.off - 1 +} + +// phasePanicMsg is used as a panic message when we end up with something that +// shouldn't happen. It can indicate a bug in the JSON decoder, or that +// something is editing the data slice while the decoder executes. +const phasePanicMsg = "JSON decoder out of sync - data changing underfoot?" + +func (d *decodeState) init(data []byte) *decodeState { + d.data = data + d.off = 0 + d.savedError = nil + if d.errorContext != nil { + d.errorContext.Struct = nil + // Reuse the allocated space for the FieldStack slice. + d.errorContext.FieldStack = d.errorContext.FieldStack[:0] + } + return d +} + +// saveError saves the first err it is called with, +// for reporting at the end of the unmarshal. +func (d *decodeState) saveError(err error) { + if d.savedError == nil { + d.savedError = d.addErrorContext(err) + } +} + +// addErrorContext returns a new error enhanced with information from d.errorContext +func (d *decodeState) addErrorContext(err error) error { + if d.errorContext != nil && (d.errorContext.Struct != nil || len(d.errorContext.FieldStack) > 0) { + switch err := err.(type) { + case *UnmarshalTypeError: + err.Struct = d.errorContext.Struct.Name() + err.Field = strings.Join(d.errorContext.FieldStack, ".") + } + } + return err +} + +// skip scans to the end of what was started. +func (d *decodeState) skip() { + s, data, i := &d.scan, d.data, d.off + depth := len(s.parseState) + for { + op := s.step(s, data[i]) + i++ + if len(s.parseState) < depth { + d.off = i + d.opcode = op + return + } + } +} + +// scanNext processes the byte at d.data[d.off]. +func (d *decodeState) scanNext() { + if d.off < len(d.data) { + d.opcode = d.scan.step(&d.scan, d.data[d.off]) + d.off++ + } else { + d.opcode = d.scan.eof() + d.off = len(d.data) + 1 // mark processed EOF with len+1 + } +} + +// scanWhile processes bytes in d.data[d.off:] until it +// receives a scan code not equal to op. +func (d *decodeState) scanWhile(op int) { + s, data, i := &d.scan, d.data, d.off + for i < len(data) { + newOp := s.step(s, data[i]) + i++ + if newOp != op { + d.opcode = newOp + d.off = i + return + } + } + + d.off = len(data) + 1 // mark processed EOF with len+1 + d.opcode = d.scan.eof() +} + +// rescanLiteral is similar to scanWhile(scanContinue), but it specialises the +// common case where we're decoding a literal. The decoder scans the input +// twice, once for syntax errors and to check the length of the value, and the +// second to perform the decoding. +// +// Only in the second step do we use decodeState to tokenize literals, so we +// know there aren't any syntax errors. We can take advantage of that knowledge, +// and scan a literal's bytes much more quickly. +func (d *decodeState) rescanLiteral() { + data, i := d.data, d.off +Switch: + switch data[i-1] { + case '"': // string + for ; i < len(data); i++ { + switch data[i] { + case '\\': + i++ // escaped char + case '"': + i++ // tokenize the closing quote too + break Switch + } + } + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-': // number + for ; i < len(data); i++ { + switch data[i] { + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + '.', 'e', 'E', '+', '-': + default: + break Switch + } + } + case 't': // true + i += len("rue") + case 'f': // false + i += len("alse") + case 'n': // null + i += len("ull") + } + if i < len(data) { + d.opcode = stateEndValue(&d.scan, data[i]) + } else { + d.opcode = scanEnd + } + d.off = i + 1 +} + +// value consumes a JSON value from d.data[d.off-1:], decoding into v, and +// reads the following byte ahead. If v is invalid, the value is discarded. +// The first byte of the value has been read already. +func (d *decodeState) value(v reflect.Value) error { + switch d.opcode { + default: + panic(phasePanicMsg) + + case scanBeginArray: + if v.IsValid() { + if err := d.array(v); err != nil { + return err + } + } else { + d.skip() + } + d.scanNext() + + case scanBeginObject: + if v.IsValid() { + if err := d.object(v); err != nil { + return err + } + } else { + d.skip() + } + d.scanNext() + + case scanBeginLiteral: + // All bytes inside literal return scanContinue op code. + start := d.readIndex() + d.rescanLiteral() + + if v.IsValid() { + if err := d.literalStore(d.data[start:d.readIndex()], v, false); err != nil { + return err + } + } + } + return nil +} + +type unquotedValue struct{} + +// valueQuoted is like value but decodes a +// quoted string literal or literal null into an interface value. +// If it finds anything other than a quoted string literal or null, +// valueQuoted returns unquotedValue{}. +func (d *decodeState) valueQuoted() any { + switch d.opcode { + default: + panic(phasePanicMsg) + + case scanBeginArray, scanBeginObject: + d.skip() + d.scanNext() + + case scanBeginLiteral: + v := d.literalInterface() + switch v.(type) { + case nil, string: + return v + } + } + return unquotedValue{} +} + +// indirect walks down v allocating pointers as needed, +// until it gets to a non-pointer. +// If it encounters an Unmarshaler, indirect stops and returns that. +// If decodingNull is true, indirect stops at the first settable pointer so it +// can be set to nil. +func indirect(v reflect.Value, decodingNull bool) (Unmarshaler, encoding.TextUnmarshaler, reflect.Value) { + // Issue #24153 indicates that it is generally not a guaranteed property + // that you may round-trip a reflect.Value by calling Value.Addr().Elem() + // and expect the value to still be settable for values derived from + // unexported embedded struct fields. + // + // The logic below effectively does this when it first addresses the value + // (to satisfy possible pointer methods) and continues to dereference + // subsequent pointers as necessary. + // + // After the first round-trip, we set v back to the original value to + // preserve the original RW flags contained in reflect.Value. + v0 := v + haveAddr := false + + // If v is a named type and is addressable, + // start with its address, so that if the type has pointer methods, + // we find them. + if v.Kind() != reflect.Pointer && v.Type().Name() != "" && v.CanAddr() { + haveAddr = true + v = v.Addr() + } + for { + // Load value from interface, but only if the result will be + // usefully addressable. + if v.Kind() == reflect.Interface && !v.IsNil() { + e := v.Elem() + if e.Kind() == reflect.Pointer && !e.IsNil() && (!decodingNull || e.Elem().Kind() == reflect.Pointer) { + haveAddr = false + v = e + continue + } + } + + if v.Kind() != reflect.Pointer { + break + } + + if decodingNull && v.CanSet() { + break + } + + // Prevent infinite loop if v is an interface pointing to its own address: + // var v interface{} + // v = &v + if v.Elem().Kind() == reflect.Interface && v.Elem().Elem() == v { + v = v.Elem() + break + } + if v.IsNil() { + v.Set(reflect.New(v.Type().Elem())) + } + if v.Type().NumMethod() > 0 && v.CanInterface() { + if u, ok := v.Interface().(Unmarshaler); ok { + return u, nil, reflect.Value{} + } + if !decodingNull { + if u, ok := v.Interface().(encoding.TextUnmarshaler); ok { + return nil, u, reflect.Value{} + } + } + } + + if haveAddr { + v = v0 // restore original value after round-trip Value.Addr().Elem() + haveAddr = false + } else { + v = v.Elem() + } + } + return nil, nil, v +} + +// array consumes an array from d.data[d.off-1:], decoding into v. +// The first byte of the array ('[') has been read already. +func (d *decodeState) array(v reflect.Value) error { + // Check for unmarshaler. + u, ut, pv := indirect(v, false) + if u != nil { + start := d.readIndex() + d.skip() + return u.UnmarshalJSON(d.data[start:d.off]) + } + if ut != nil { + d.saveError(&UnmarshalTypeError{Value: "array", Type: v.Type(), Offset: int64(d.off)}) + d.skip() + return nil + } + v = pv + + // Check type of target. + switch v.Kind() { + case reflect.Interface: + if v.NumMethod() == 0 { + // Decoding into nil interface? Switch to non-reflect code. + ai := d.arrayInterface() + v.Set(reflect.ValueOf(ai)) + return nil + } + // Otherwise it's invalid. + fallthrough + default: + d.saveError(&UnmarshalTypeError{Value: "array", Type: v.Type(), Offset: int64(d.off)}) + d.skip() + return nil + case reflect.Array, reflect.Slice: + break + } + + i := 0 + for { + // Look ahead for ] - can only happen on first iteration. + d.scanWhile(scanSkipSpace) + if d.opcode == scanEndArray { + break + } + + // Get element of array, growing if necessary. + if v.Kind() == reflect.Slice { + // Grow slice if necessary + if i >= v.Cap() { + newcap := v.Cap() + v.Cap()/2 + if newcap < 4 { + newcap = 4 + } + newv := reflect.MakeSlice(v.Type(), v.Len(), newcap) + reflect.Copy(newv, v) + v.Set(newv) + } + if i >= v.Len() { + v.SetLen(i + 1) + } + } + + if i < v.Len() { + // Decode into element. + if err := d.value(v.Index(i)); err != nil { + return err + } + } else { + // Ran out of fixed array: skip. + if err := d.value(reflect.Value{}); err != nil { + return err + } + } + i++ + + // Next token must be , or ]. + if d.opcode == scanSkipSpace { + d.scanWhile(scanSkipSpace) + } + if d.opcode == scanEndArray { + break + } + if d.opcode != scanArrayValue { + panic(phasePanicMsg) + } + } + + if i < v.Len() { + if v.Kind() == reflect.Array { + // Array. Zero the rest. + z := reflect.Zero(v.Type().Elem()) + for ; i < v.Len(); i++ { + v.Index(i).Set(z) + } + } else { + v.SetLen(i) + } + } + if i == 0 && v.Kind() == reflect.Slice { + v.Set(reflect.MakeSlice(v.Type(), 0, 0)) + } + return nil +} + +var nullLiteral = []byte("null") +var textUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem() + +// object consumes an object from d.data[d.off-1:], decoding into v. +// The first byte ('{') of the object has been read already. +func (d *decodeState) object(v reflect.Value) error { + // Check for unmarshaler. + u, ut, pv := indirect(v, false) + if u != nil { + start := d.readIndex() + d.skip() + return u.UnmarshalJSON(d.data[start:d.off]) + } + if ut != nil { + d.saveError(&UnmarshalTypeError{Value: "object", Type: v.Type(), Offset: int64(d.off)}) + d.skip() + return nil + } + v = pv + t := v.Type() + + // Decoding into nil interface? Switch to non-reflect code. + if v.Kind() == reflect.Interface && v.NumMethod() == 0 { + oi := d.objectInterface() + v.Set(reflect.ValueOf(oi)) + return nil + } + + var fields structFields + + // Check type of target: + // struct or + // map[T1]T2 where T1 is string, an integer type, + // or an encoding.TextUnmarshaler + switch v.Kind() { + case reflect.Map: + // Map key must either have string kind, have an integer kind, + // or be an encoding.TextUnmarshaler. + switch t.Key().Kind() { + case reflect.String, + reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + default: + if !reflect.PointerTo(t.Key()).Implements(textUnmarshalerType) { + d.saveError(&UnmarshalTypeError{Value: "object", Type: t, Offset: int64(d.off)}) + d.skip() + return nil + } + } + if v.IsNil() { + v.Set(reflect.MakeMap(t)) + } + case reflect.Struct: + fields = cachedTypeFields(t) + // ok + default: + d.saveError(&UnmarshalTypeError{Value: "object", Type: t, Offset: int64(d.off)}) + d.skip() + return nil + } + + var mapElem reflect.Value + var origErrorContext errorContext + if d.errorContext != nil { + origErrorContext = *d.errorContext + } + + var keys []string + + for { + // Read opening " of string key or closing }. + d.scanWhile(scanSkipSpace) + if d.opcode == scanEndObject { + // closing } - can only happen on first iteration. + break + } + if d.opcode != scanBeginLiteral { + panic(phasePanicMsg) + } + + // Read key. + start := d.readIndex() + d.rescanLiteral() + item := d.data[start:d.readIndex()] + key, ok := unquoteBytes(item) + if !ok { + panic(phasePanicMsg) + } + + keys = append(keys, string(key)) + + // Figure out field corresponding to key. + var subv reflect.Value + destring := false // whether the value is wrapped in a string to be decoded first + + if v.Kind() == reflect.Map { + elemType := t.Elem() + if !mapElem.IsValid() { + mapElem = reflect.New(elemType).Elem() + } else { + mapElem.Set(reflect.Zero(elemType)) + } + subv = mapElem + } else { + var f *field + if i, ok := fields.nameIndex[string(key)]; ok { + // Found an exact name match. + f = &fields.list[i] + } else { + // Fall back to the expensive case-insensitive + // linear search. + for i := range fields.list { + ff := &fields.list[i] + if ff.equalFold(ff.nameBytes, key) { + f = ff + break + } + } + } + if f != nil { + subv = v + destring = f.quoted + for _, i := range f.index { + if subv.Kind() == reflect.Pointer { + if subv.IsNil() { + // If a struct embeds a pointer to an unexported type, + // it is not possible to set a newly allocated value + // since the field is unexported. + // + // See https://golang.org/issue/21357 + if !subv.CanSet() { + d.saveError(fmt.Errorf("json: cannot set embedded pointer to unexported struct: %v", subv.Type().Elem())) + // Invalidate subv to ensure d.value(subv) skips over + // the JSON value without assigning it to subv. + subv = reflect.Value{} + destring = false + break + } + subv.Set(reflect.New(subv.Type().Elem())) + } + subv = subv.Elem() + } + subv = subv.Field(i) + } + if d.errorContext == nil { + d.errorContext = new(errorContext) + } + d.errorContext.FieldStack = append(d.errorContext.FieldStack, f.name) + d.errorContext.Struct = t + } else if d.disallowUnknownFields { + d.saveError(fmt.Errorf("json: unknown field %q", key)) + } + } + + // Read : before value. + if d.opcode == scanSkipSpace { + d.scanWhile(scanSkipSpace) + } + if d.opcode != scanObjectKey { + panic(phasePanicMsg) + } + d.scanWhile(scanSkipSpace) + + if destring { + switch qv := d.valueQuoted().(type) { + case nil: + if err := d.literalStore(nullLiteral, subv, false); err != nil { + return err + } + case string: + if err := d.literalStore([]byte(qv), subv, true); err != nil { + return err + } + default: + d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal unquoted value into %v", subv.Type())) + } + } else { + if err := d.value(subv); err != nil { + return err + } + } + + // Write value back to map; + // if using struct, subv points into struct already. + if v.Kind() == reflect.Map { + kt := t.Key() + var kv reflect.Value + switch { + case reflect.PointerTo(kt).Implements(textUnmarshalerType): + kv = reflect.New(kt) + if err := d.literalStore(item, kv, true); err != nil { + return err + } + kv = kv.Elem() + case kt.Kind() == reflect.String: + kv = reflect.ValueOf(key).Convert(kt) + default: + switch kt.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + s := string(key) + n, err := strconv.ParseInt(s, 10, 64) + if err != nil || reflect.Zero(kt).OverflowInt(n) { + d.saveError(&UnmarshalTypeError{Value: "number " + s, Type: kt, Offset: int64(start + 1)}) + break + } + kv = reflect.ValueOf(n).Convert(kt) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + s := string(key) + n, err := strconv.ParseUint(s, 10, 64) + if err != nil || reflect.Zero(kt).OverflowUint(n) { + d.saveError(&UnmarshalTypeError{Value: "number " + s, Type: kt, Offset: int64(start + 1)}) + break + } + kv = reflect.ValueOf(n).Convert(kt) + default: + panic("json: Unexpected key type") // should never occur + } + } + if kv.IsValid() { + v.SetMapIndex(kv, subv) + } + } + + // Next token must be , or }. + if d.opcode == scanSkipSpace { + d.scanWhile(scanSkipSpace) + } + if d.errorContext != nil { + // Reset errorContext to its original state. + // Keep the same underlying array for FieldStack, to reuse the + // space and avoid unnecessary allocs. + d.errorContext.FieldStack = d.errorContext.FieldStack[:len(origErrorContext.FieldStack)] + d.errorContext.Struct = origErrorContext.Struct + } + if d.opcode == scanEndObject { + break + } + if d.opcode != scanObjectValue { + panic(phasePanicMsg) + } + } + + if v.Kind() == reflect.Map { + d.lastKeys = keys + } + return nil +} + +// convertNumber converts the number literal s to a float64 or a Number +// depending on the setting of d.useNumber. +func (d *decodeState) convertNumber(s string) (any, error) { + if d.useNumber { + return Number(s), nil + } + f, err := strconv.ParseFloat(s, 64) + if err != nil { + return nil, &UnmarshalTypeError{Value: "number " + s, Type: reflect.TypeOf(0.0), Offset: int64(d.off)} + } + return f, nil +} + +var numberType = reflect.TypeOf(Number("")) + +// literalStore decodes a literal stored in item into v. +// +// fromQuoted indicates whether this literal came from unwrapping a +// string from the ",string" struct tag option. this is used only to +// produce more helpful error messages. +func (d *decodeState) literalStore(item []byte, v reflect.Value, fromQuoted bool) error { + // Check for unmarshaler. + if len(item) == 0 { + //Empty string given + d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) + return nil + } + isNull := item[0] == 'n' // null + u, ut, pv := indirect(v, isNull) + if u != nil { + return u.UnmarshalJSON(item) + } + if ut != nil { + if item[0] != '"' { + if fromQuoted { + d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) + return nil + } + val := "number" + switch item[0] { + case 'n': + val = "null" + case 't', 'f': + val = "bool" + } + d.saveError(&UnmarshalTypeError{Value: val, Type: v.Type(), Offset: int64(d.readIndex())}) + return nil + } + s, ok := unquoteBytes(item) + if !ok { + if fromQuoted { + return fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type()) + } + panic(phasePanicMsg) + } + return ut.UnmarshalText(s) + } + + v = pv + + switch c := item[0]; c { + case 'n': // null + // The main parser checks that only true and false can reach here, + // but if this was a quoted string input, it could be anything. + if fromQuoted && string(item) != "null" { + d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) + break + } + switch v.Kind() { + case reflect.Interface, reflect.Pointer, reflect.Map, reflect.Slice: + v.Set(reflect.Zero(v.Type())) + // otherwise, ignore null for primitives/string + } + case 't', 'f': // true, false + value := item[0] == 't' + // The main parser checks that only true and false can reach here, + // but if this was a quoted string input, it could be anything. + if fromQuoted && string(item) != "true" && string(item) != "false" { + d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) + break + } + switch v.Kind() { + default: + if fromQuoted { + d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) + } else { + d.saveError(&UnmarshalTypeError{Value: "bool", Type: v.Type(), Offset: int64(d.readIndex())}) + } + case reflect.Bool: + v.SetBool(value) + case reflect.Interface: + if v.NumMethod() == 0 { + v.Set(reflect.ValueOf(value)) + } else { + d.saveError(&UnmarshalTypeError{Value: "bool", Type: v.Type(), Offset: int64(d.readIndex())}) + } + } + + case '"': // string + s, ok := unquoteBytes(item) + if !ok { + if fromQuoted { + return fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type()) + } + panic(phasePanicMsg) + } + switch v.Kind() { + default: + d.saveError(&UnmarshalTypeError{Value: "string", Type: v.Type(), Offset: int64(d.readIndex())}) + case reflect.Slice: + if v.Type().Elem().Kind() != reflect.Uint8 { + d.saveError(&UnmarshalTypeError{Value: "string", Type: v.Type(), Offset: int64(d.readIndex())}) + break + } + b := make([]byte, base64.StdEncoding.DecodedLen(len(s))) + n, err := base64.StdEncoding.Decode(b, s) + if err != nil { + d.saveError(err) + break + } + v.SetBytes(b[:n]) + case reflect.String: + if v.Type() == numberType && !isValidNumber(string(s)) { + return fmt.Errorf("json: invalid number literal, trying to unmarshal %q into Number", item) + } + v.SetString(string(s)) + case reflect.Interface: + if v.NumMethod() == 0 { + v.Set(reflect.ValueOf(string(s))) + } else { + d.saveError(&UnmarshalTypeError{Value: "string", Type: v.Type(), Offset: int64(d.readIndex())}) + } + } + + default: // number + if c != '-' && (c < '0' || c > '9') { + if fromQuoted { + return fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type()) + } + panic(phasePanicMsg) + } + s := string(item) + switch v.Kind() { + default: + if v.Kind() == reflect.String && v.Type() == numberType { + // s must be a valid number, because it's + // already been tokenized. + v.SetString(s) + break + } + if fromQuoted { + return fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type()) + } + d.saveError(&UnmarshalTypeError{Value: "number", Type: v.Type(), Offset: int64(d.readIndex())}) + case reflect.Interface: + n, err := d.convertNumber(s) + if err != nil { + d.saveError(err) + break + } + if v.NumMethod() != 0 { + d.saveError(&UnmarshalTypeError{Value: "number", Type: v.Type(), Offset: int64(d.readIndex())}) + break + } + v.Set(reflect.ValueOf(n)) + + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + n, err := strconv.ParseInt(s, 10, 64) + if err != nil || v.OverflowInt(n) { + d.saveError(&UnmarshalTypeError{Value: "number " + s, Type: v.Type(), Offset: int64(d.readIndex())}) + break + } + v.SetInt(n) + + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + n, err := strconv.ParseUint(s, 10, 64) + if err != nil || v.OverflowUint(n) { + d.saveError(&UnmarshalTypeError{Value: "number " + s, Type: v.Type(), Offset: int64(d.readIndex())}) + break + } + v.SetUint(n) + + case reflect.Float32, reflect.Float64: + n, err := strconv.ParseFloat(s, v.Type().Bits()) + if err != nil || v.OverflowFloat(n) { + d.saveError(&UnmarshalTypeError{Value: "number " + s, Type: v.Type(), Offset: int64(d.readIndex())}) + break + } + v.SetFloat(n) + } + } + return nil +} + +// The xxxInterface routines build up a value to be stored +// in an empty interface. They are not strictly necessary, +// but they avoid the weight of reflection in this common case. + +// valueInterface is like value but returns interface{} +func (d *decodeState) valueInterface() (val any) { + switch d.opcode { + default: + panic(phasePanicMsg) + case scanBeginArray: + val = d.arrayInterface() + d.scanNext() + case scanBeginObject: + val = d.objectInterface() + d.scanNext() + case scanBeginLiteral: + val = d.literalInterface() + } + return +} + +// arrayInterface is like array but returns []interface{}. +func (d *decodeState) arrayInterface() []any { + var v = make([]any, 0) + for { + // Look ahead for ] - can only happen on first iteration. + d.scanWhile(scanSkipSpace) + if d.opcode == scanEndArray { + break + } + + v = append(v, d.valueInterface()) + + // Next token must be , or ]. + if d.opcode == scanSkipSpace { + d.scanWhile(scanSkipSpace) + } + if d.opcode == scanEndArray { + break + } + if d.opcode != scanArrayValue { + panic(phasePanicMsg) + } + } + return v +} + +// objectInterface is like object but returns map[string]interface{}. +func (d *decodeState) objectInterface() map[string]any { + m := make(map[string]any) + for { + // Read opening " of string key or closing }. + d.scanWhile(scanSkipSpace) + if d.opcode == scanEndObject { + // closing } - can only happen on first iteration. + break + } + if d.opcode != scanBeginLiteral { + panic(phasePanicMsg) + } + + // Read string key. + start := d.readIndex() + d.rescanLiteral() + item := d.data[start:d.readIndex()] + key, ok := unquote(item) + if !ok { + panic(phasePanicMsg) + } + + // Read : before value. + if d.opcode == scanSkipSpace { + d.scanWhile(scanSkipSpace) + } + if d.opcode != scanObjectKey { + panic(phasePanicMsg) + } + d.scanWhile(scanSkipSpace) + + // Read value. + m[key] = d.valueInterface() + + // Next token must be , or }. + if d.opcode == scanSkipSpace { + d.scanWhile(scanSkipSpace) + } + if d.opcode == scanEndObject { + break + } + if d.opcode != scanObjectValue { + panic(phasePanicMsg) + } + } + return m +} + +// literalInterface consumes and returns a literal from d.data[d.off-1:] and +// it reads the following byte ahead. The first byte of the literal has been +// read already (that's how the caller knows it's a literal). +func (d *decodeState) literalInterface() any { + // All bytes inside literal return scanContinue op code. + start := d.readIndex() + d.rescanLiteral() + + item := d.data[start:d.readIndex()] + + switch c := item[0]; c { + case 'n': // null + return nil + + case 't', 'f': // true, false + return c == 't' + + case '"': // string + s, ok := unquote(item) + if !ok { + panic(phasePanicMsg) + } + return s + + default: // number + if c != '-' && (c < '0' || c > '9') { + panic(phasePanicMsg) + } + n, err := d.convertNumber(string(item)) + if err != nil { + d.saveError(err) + } + return n + } +} + +// getu4 decodes \uXXXX from the beginning of s, returning the hex value, +// or it returns -1. +func getu4(s []byte) rune { + if len(s) < 6 || s[0] != '\\' || s[1] != 'u' { + return -1 + } + var r rune + for _, c := range s[2:6] { + switch { + case '0' <= c && c <= '9': + c = c - '0' + case 'a' <= c && c <= 'f': + c = c - 'a' + 10 + case 'A' <= c && c <= 'F': + c = c - 'A' + 10 + default: + return -1 + } + r = r*16 + rune(c) + } + return r +} + +// unquote converts a quoted JSON string literal s into an actual string t. +// The rules are different than for Go, so cannot use strconv.Unquote. +func unquote(s []byte) (t string, ok bool) { + s, ok = unquoteBytes(s) + t = string(s) + return +} + +func unquoteBytes(s []byte) (t []byte, ok bool) { + if len(s) < 2 || s[0] != '"' || s[len(s)-1] != '"' { + return + } + s = s[1 : len(s)-1] + + // Check for unusual characters. If there are none, + // then no unquoting is needed, so return a slice of the + // original bytes. + r := 0 + for r < len(s) { + c := s[r] + if c == '\\' || c == '"' || c < ' ' { + break + } + if c < utf8.RuneSelf { + r++ + continue + } + rr, size := utf8.DecodeRune(s[r:]) + if rr == utf8.RuneError && size == 1 { + break + } + r += size + } + if r == len(s) { + return s, true + } + + b := make([]byte, len(s)+2*utf8.UTFMax) + w := copy(b, s[0:r]) + for r < len(s) { + // Out of room? Can only happen if s is full of + // malformed UTF-8 and we're replacing each + // byte with RuneError. + if w >= len(b)-2*utf8.UTFMax { + nb := make([]byte, (len(b)+utf8.UTFMax)*2) + copy(nb, b[0:w]) + b = nb + } + switch c := s[r]; { + case c == '\\': + r++ + if r >= len(s) { + return + } + switch s[r] { + default: + return + case '"', '\\', '/', '\'': + b[w] = s[r] + r++ + w++ + case 'b': + b[w] = '\b' + r++ + w++ + case 'f': + b[w] = '\f' + r++ + w++ + case 'n': + b[w] = '\n' + r++ + w++ + case 'r': + b[w] = '\r' + r++ + w++ + case 't': + b[w] = '\t' + r++ + w++ + case 'u': + r-- + rr := getu4(s[r:]) + if rr < 0 { + return + } + r += 6 + if utf16.IsSurrogate(rr) { + rr1 := getu4(s[r:]) + if dec := utf16.DecodeRune(rr, rr1); dec != unicode.ReplacementChar { + // A valid pair; consume. + r += 6 + w += utf8.EncodeRune(b[w:], dec) + break + } + // Invalid surrogate; fall back to replacement rune. + rr = unicode.ReplacementChar + } + w += utf8.EncodeRune(b[w:], rr) + } + + // Quote, control characters are invalid. + case c == '"', c < ' ': + return + + // ASCII + case c < utf8.RuneSelf: + b[w] = c + r++ + w++ + + // Coerce to well-formed UTF-8. + default: + rr, size := utf8.DecodeRune(s[r:]) + r += size + w += utf8.EncodeRune(b[w:], rr) + } + } + return b[0:w], true +} diff --git a/vendor/github.com/evanphx/json-patch/v5/internal/json/encode.go b/vendor/github.com/evanphx/json-patch/v5/internal/json/encode.go new file mode 100644 index 00000000..2e6eca44 --- /dev/null +++ b/vendor/github.com/evanphx/json-patch/v5/internal/json/encode.go @@ -0,0 +1,1486 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package json implements encoding and decoding of JSON as defined in +// RFC 7159. The mapping between JSON and Go values is described +// in the documentation for the Marshal and Unmarshal functions. +// +// See "JSON and Go" for an introduction to this package: +// https://golang.org/doc/articles/json_and_go.html +package json + +import ( + "bytes" + "encoding" + "encoding/base64" + "fmt" + "math" + "reflect" + "sort" + "strconv" + "strings" + "sync" + "unicode" + "unicode/utf8" +) + +// Marshal returns the JSON encoding of v. +// +// Marshal traverses the value v recursively. +// If an encountered value implements the Marshaler interface +// and is not a nil pointer, Marshal calls its MarshalJSON method +// to produce JSON. If no MarshalJSON method is present but the +// value implements encoding.TextMarshaler instead, Marshal calls +// its MarshalText method and encodes the result as a JSON string. +// The nil pointer exception is not strictly necessary +// but mimics a similar, necessary exception in the behavior of +// UnmarshalJSON. +// +// Otherwise, Marshal uses the following type-dependent default encodings: +// +// Boolean values encode as JSON booleans. +// +// Floating point, integer, and Number values encode as JSON numbers. +// +// String values encode as JSON strings coerced to valid UTF-8, +// replacing invalid bytes with the Unicode replacement rune. +// So that the JSON will be safe to embed inside HTML