Compare commits

..

No commits in common. "main" and "spec-v0.4.0" have entirely different histories.

102 changed files with 2277 additions and 7121 deletions

17
.appveyor.yml Normal file
View File

@ -0,0 +1,17 @@
clone_folder: c:\gopath\src\github.com\containernetworking\cni
environment:
GOPATH: c:\gopath
install:
- echo %PATH%
- echo %GOPATH%
- set PATH=%GOPATH%\bin;c:\go\bin;%PATH%
- go version
- go env
build: off
test_script:
- go get -t ./...
- go test -v ./...

2
.gitattributes vendored
View File

@ -1,2 +0,0 @@
# Don't rewrite line endings
*.go -text

View File

@ -1,7 +0,0 @@
FROM alpine:3.20
RUN apk add --no-cache curl jq
COPY entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

View File

@ -1,11 +0,0 @@
name: 'Re-Test'
description: 'Re-Runs the last workflow for a PR'
inputs:
token:
description: 'GitHub API Token'
required: true
runs:
using: 'docker'
image: 'Dockerfile'
env:
GITHUB_TOKEN: ${{ inputs.token }}

View File

@ -1,45 +0,0 @@
#!/bin/sh
set -ex
if ! jq -e '.issue.pull_request' ${GITHUB_EVENT_PATH}; then
echo "Not a PR... Exiting."
exit 0
fi
if [ "$(jq -r '.comment.body' ${GITHUB_EVENT_PATH})" != "/retest" ]; then
echo "Nothing to do... Exiting."
exit 0
fi
PR_URL=$(jq -r '.issue.pull_request.url' ${GITHUB_EVENT_PATH})
curl --request GET \
--url "${PR_URL}" \
--header "authorization: Bearer ${GITHUB_TOKEN}" \
--header "content-type: application/json" > pr.json
ACTOR=$(jq -r '.user.login' pr.json)
BRANCH=$(jq -r '.head.ref' pr.json)
curl --request GET \
--url "https://api.github.com/repos/${GITHUB_REPOSITORY}/actions/runs?event=pull_request&actor=${ACTOR}&branch=${BRANCH}" \
--header "authorization: Bearer ${GITHUB_TOKEN}" \
--header "content-type: application/json" | jq '.workflow_runs | max_by(.run_number)' > run.json
RERUN_URL=$(jq -r '.rerun_url' run.json)
curl --request POST \
--url "${RERUN_URL}" \
--header "authorization: Bearer ${GITHUB_TOKEN}" \
--header "content-type: application/json"
REACTION_URL="$(jq -r '.comment.url' ${GITHUB_EVENT_PATH})/reactions"
curl --request POST \
--url "${REACTION_URL}" \
--header "authorization: Bearer ${GITHUB_TOKEN}" \
--header "accept: application/vnd.github.squirrel-girl-preview+json" \
--header "content-type: application/json" \
--data '{ "content" : "rocket" }'

View File

@ -1,27 +0,0 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "docker"
directory: "/.github/actions/retest-action"
schedule:
interval: "weekly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "weekly"
groups:
golang:
patterns:
- "*"
- package-ecosystem: "gomod"
directory: "/plugins/debug"
schedule:
interval: "weekly"

View File

@ -1,17 +0,0 @@
name: commands
on:
issue_comment:
types: [created]
jobs:
retest:
if: github.repository == 'containernetworking/cni'
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Re-Test Action
uses: ./.github/actions/retest-action
with:
token: ${{ secrets.REPO_ACCESS_TOKEN }}

View File

@ -1,40 +0,0 @@
name: Scorecard supply-chain security
on:
branch_protection_rule:
push:
branches:
- main
schedule:
- cron: 29 15 * * 0
permissions: read-all
jobs:
analysis:
name: Scorecard analysis
permissions:
id-token: write
security-events: write
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
persist-credentials: false
- name: Run analysis
uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1
with:
results_file: results.sarif
results_format: sarif
publish_results: true
- name: Upload artifact
uses: actions/upload-artifact@97a0fba1372883ab732affbe8f94b823f91727db # v3.pre.node20
with:
name: SARIF file
path: results.sarif
retention-days: 5
- name: Upload to code-scanning
uses: github/codeql-action/upload-sarif@8214744c546c1e5c8f03dde8fab3a7353211988d # v3.26.7
with:
sarif_file: results.sarif

View File

@ -1,96 +0,0 @@
---
name: test
on: ["push", "pull_request"]
env:
GO_VERSION: "1.22"
LINUX_ARCHES: "amd64 386 arm arm64 s390x mips64le ppc64le"
jobs:
lint:
name: Lint
permissions:
contents: read
pull-requests: read
runs-on: ubuntu-latest
steps:
- name: setup go
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
with:
go-version: ${{ env.GO_VERSION }}
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: ibiqlik/action-yamllint@2576378a8e339169678f9939646ee3ee325e845c # v3.1.1
with:
format: auto
config_file: .yamllint.yaml
- uses: golangci/golangci-lint-action@aaa42aa0628b4ae2578232a66b541047968fac86 # v6.1.0
with:
args: --verbose
version: v1.57.1
build:
name: Build all linux architectures
needs: lint
runs-on: ubuntu-latest
steps:
- name: setup go
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
with:
go-version: ${{ env.GO_VERSION }}
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Build on all supported architectures
run: |
set -e
for arch in ${LINUX_ARCHES}; do
echo "Building for arch $arch"
GOARCH=$arch go build ./...
done
test-linux:
name: Run tests on Linux amd64
needs: build
runs-on: ubuntu-latest
steps:
- name: setup go
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
with:
go-version: ${{ env.GO_VERSION }}
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Install test binaries
run: |
go install github.com/mattn/goveralls@v0.0.12
go install github.com/modocache/gover@latest
- name: test
run: COVERALLS=1 ./test.sh
- env:
COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
name: Send coverage to coveralls
run: |
PATH=$PATH:$(go env GOPATH)/bin
gover
goveralls -coverprofile=gover.coverprofile -service=github
test-win:
name: Build and run tests on Windows
needs: build
runs-on: windows-latest
steps:
- name: setup go
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
with:
go-version: ${{ env.GO_VERSION }}
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: test
run: bash ./test.sh

3
.gitignore vendored
View File

@ -1,5 +1,6 @@
.idea/ .idea/
bin/
gopath/
*.sw[ponm] *.sw[ponm]
.vagrant .vagrant
release-* release-*
cnitool/cnitool

View File

@ -1,30 +0,0 @@
linters:
enable:
- contextcheck
- errcheck
- errorlint
- gci
- ginkgolinter
- gocritic
- gofumpt
- govet
- ineffassign
- misspell
- nolintlint
- nonamedreturns
- predeclared
- staticcheck
- typecheck
- unconvert
- unused
- whitespace
linters-settings:
gci:
sections:
- standard
- default
- prefix(github.com/containernetworking)
run:
timeout: 5m

37
.travis.yml Normal file
View File

@ -0,0 +1,37 @@
language: go
dist: trusty
go:
- 1.10.x
- 1.11.x
env:
matrix:
- TARGET=amd64
- TARGET=arm
- TARGET=arm64
- TARGET=ppc64le
- TARGET=s390x
matrix:
fast_finish: true
install:
- go get golang.org/x/tools/cmd/cover
- go get github.com/modocache/gover
- go get github.com/mattn/goveralls
- go get -t ./...
script:
- >
if [ "${TARGET}" == "amd64" ]; then
GOARCH="${TARGET}" ./test.sh;
else
GOARCH="${TARGET}" go list ./... | xargs -n1 go build -v -o /dev/null
fi
notifications:
email: false
git:
depth: 9999999

View File

@ -1,10 +0,0 @@
---
extends: default
rules:
document-start: disable
line-length: disable
truthy:
ignore: |
.github/workflows/*.yml
.github/workflows/*.yaml

View File

@ -1,3 +1,4 @@
# Community Code of Conduct ## Community Code of Conduct
CNI follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md).
CNI follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md).

View File

@ -7,19 +7,18 @@ it easier to get your contribution accepted.
We gratefully welcome improvements to documentation as well as to code. We gratefully welcome improvements to documentation as well as to code.
## Certificate of Origin # Certificate of Origin
By contributing to this project you agree to the Developer Certificate of By contributing to this project you agree to the Developer Certificate of
Origin (DCO). This document was created by the Linux Kernel community and is a Origin (DCO). This document was created by the Linux Kernel community and is a
simple statement that you, as a contributor, have the legal right to make the simple statement that you, as a contributor, have the legal right to make the
contribution. See the [DCO](DCO) file for details. contribution. See the [DCO](DCO) file for details.
## Email and Chat # Email and Chat
The project uses the cni-dev email list, IRC chat, and Slack: The project uses the cni-dev email list and IRC chat:
- Email: [cni-dev](https://groups.google.com/forum/#!forum/cni-dev) - Email: [cni-dev](https://groups.google.com/forum/#!forum/cni-dev)
- IRC: #[containernetworking](irc://irc.freenode.net:6667/#containernetworking) channel on [freenode.net](https://freenode.net/) - IRC: #[containernetworking](irc://irc.freenode.org:6667/#containernetworking) channel on freenode.org
- Slack: #cni on the [CNCF slack](https://slack.cncf.io/). NOTE: the previous CNI Slack (containernetworking.slack.com) has been sunsetted.
Please avoid emailing maintainers found in the MAINTAINERS file directly. They Please avoid emailing maintainers found in the MAINTAINERS file directly. They
are very busy and read the mailing lists. are very busy and read the mailing lists.
@ -34,20 +33,19 @@ are very busy and read the mailing lists.
This is a rough outline of how to prepare a contribution: This is a rough outline of how to prepare a contribution:
- Create a topic branch from where you want to base your work (usually branched from main). - Create a topic branch from where you want to base your work (usually branched from master).
- Make commits of logical units. - Make commits of logical units.
- Make sure your commit messages are in the proper format (see below). - Make sure your commit messages are in the proper format (see below).
- Push your changes to a topic branch in your fork of the repository. - Push your changes to a topic branch in your fork of the repository.
- If you changed code: - If you changed code:
- add automated tests to cover your changes, using the [Ginkgo](https://onsi.github.io/ginkgo/) & [Gomega](https://onsi.github.io/gomega/) style - add automated tests to cover your changes, using the [Ginkgo](https://onsi.github.io/ginkgo/) & [Gomega](https://onsi.github.io/gomega/) style
- if the package did not previously have any test coverage, add it to the list - if the package did not previously have any test coverage, add it to the list
of `TESTABLE` packages in the `test.sh` script. of `TESTABLE` packages in the `test.sh` script.
- run the full test script and ensure it passes - run the full test script and ensure it passes
- Make sure any new code files have a license header (this is now enforced by automated tests) - Make sure any new code files have a license header (this is now enforced by automated tests)
- Submit a pull request to the original repository. - Submit a pull request to the original repository.
## How to run the test suite ## How to run the test suite
We generally require test coverage of any new features or bug fixes. We generally require test coverage of any new features or bug fixes.
Here's how you can run the test suite on any system (even Mac or Windows) using Here's how you can run the test suite on any system (even Mac or Windows) using
@ -64,19 +62,19 @@ cd /go/src/github.com/containernetworking/cni
./test.sh ./test.sh
# to focus on a particular test suite # to focus on a particular test suite
cd libcni cd plugins/main/loopback
go test go test
``` ```
## Acceptance policy # Acceptance policy
These things will make a PR more likely to be accepted: These things will make a PR more likely to be accepted:
- a well-described requirement * a well-described requirement
- tests for new code * tests for new code
- tests for old code! * tests for old code!
- new code and tests follow the conventions in old code and tests * new code and tests follow the conventions in old code and tests
- a good commit message (see below) * a good commit message (see below)
In general, we will merge a PR once two maintainers have endorsed it. In general, we will merge a PR once two maintainers have endorsed it.
Trivial changes (e.g., corrections to spelling) may get waved through. Trivial changes (e.g., corrections to spelling) may get waved through.
@ -88,7 +86,7 @@ We follow a rough convention for commit messages that is designed to answer two
questions: what changed and why. The subject line should feature the what and questions: what changed and why. The subject line should feature the what and
the body of the commit should describe the why. the body of the commit should describe the why.
```md ```
scripts: add the test-cluster command scripts: add the test-cluster command
this uses tmux to setup a test cluster that you can easily kill and this uses tmux to setup a test cluster that you can easily kill and
@ -99,7 +97,7 @@ Fixes #38
The format can be described more formally as follows: The format can be described more formally as follows:
```md ```
<subsystem>: <what changed> <subsystem>: <what changed>
<BLANK LINE> <BLANK LINE>
<why this change was made> <why this change was made>
@ -113,7 +111,6 @@ This allows the message to be easier to read on GitHub as well as in various
git tools. git tools.
## 3rd party plugins ## 3rd party plugins
So you've built a CNI plugin. Where should it live? So you've built a CNI plugin. Where should it live?
Short answer: We'd be happy to link to it from our [list of 3rd party plugins](README.md#3rd-party-plugins). Short answer: We'd be happy to link to it from our [list of 3rd party plugins](README.md#3rd-party-plugins).

View File

@ -10,7 +10,7 @@ Establishing these conventions allows plugins to work across multiple runtimes.
## Plugins ## Plugins
* Plugin authors should aim to support these conventions where it makes sense for their plugin. This means they are more likely to "just work" with a wider range of runtimes. * Plugin authors should aim to support these conventions where it makes sense for their plugin. This means they are more likely to "just work" with a wider range of runtimes.
* Plugins should accept arguments according to these conventions if they implement the same basic functionality as other plugins. If plugins have shared functionality that isn't covered by these conventions then a PR should be opened against this document. * Plugins should accept arguments according to these conventions if they implement the same basic functionality as other plugins. If plugins have shared functionality that isn't coverered by these conventions then a PR should be opened against this document.
## Runtimes ## Runtimes
* Runtime authors should follow these conventions if they want to pass additional information to plugins. This will allow the extra information to be consumed by the widest range of plugins. * Runtime authors should follow these conventions if they want to pass additional information to plugins. This will allow the extra information to be consumed by the widest range of plugins.
@ -20,7 +20,7 @@ Establishing these conventions allows plugins to work across multiple runtimes.
Additional conventions can be created by creating PRs which modify this document. Additional conventions can be created by creating PRs which modify this document.
## Dynamic Plugin specific fields (Capabilities / Runtime Configuration) ## Dynamic Plugin specific fields (Capabilities / Runtime Configuration)
[Plugin specific fields](SPEC.md#network-configuration) formed part of the original CNI spec and have been present since the initial release. [Plugin specific fields](https://github.com/containernetworking/cni/blob/master/SPEC.md#network-configuration) formed part of the original CNI spec and have been present since the initial release.
> Plugins may define additional fields that they accept and may generate an error if called with unknown fields. The exception to this is the args field may be used to pass arbitrary data which may be ignored by plugins. > Plugins may define additional fields that they accept and may generate an error if called with unknown fields. The exception to this is the args field may be used to pass arbitrary data which may be ignored by plugins.
A plugin can define any additional fields it needs to work properly. It should return an error if it can't act on fields that were expected or where the field values were malformed. A plugin can define any additional fields it needs to work properly. It should return an error if it can't act on fields that were expected or where the field values were malformed.
@ -32,7 +32,7 @@ This method of passing information to a plugin is recommended when the following
Dynamic information (i.e. data that a runtime fills out) should be placed in a `runtimeConfig` section. Plugins can request Dynamic information (i.e. data that a runtime fills out) should be placed in a `runtimeConfig` section. Plugins can request
that the runtime insert this dynamic configuration by explicitly listing their `capabilities` in the network configuration. that the runtime insert this dynamic configuration by explicitly listing their `capabilities` in the network configuration.
For example, the configuration for a port mapping plugin might look like this to an operator (it should be included as part of a [network configuration list](SPEC.md#network-configuration-lists). For example, the configuration for a port mapping plugin might look like this to an operator (it should be included as part of a [network configuration list](https://github.com/containernetworking/cni/blob/master/SPEC.md#network-configuration-lists).
```json ```json
{ {
"name" : "ExamplePlugin", "name" : "ExamplePlugin",
@ -60,16 +60,11 @@ But the runtime would fill in the mappings so the plugin itself would receive so
| port mappings | Pass mapping from ports on the host to ports in the container network namespace. | `portMappings` | A list of portmapping entries.<br/> <pre>[<br/> { "hostPort": 8080, "containerPort": 80, "protocol": "tcp" },<br /> { "hostPort": 8000, "containerPort": 8001, "protocol": "udp" }<br /> ]<br /></pre> | kubernetes | CNI `portmap` plugin | | port mappings | Pass mapping from ports on the host to ports in the container network namespace. | `portMappings` | A list of portmapping entries.<br/> <pre>[<br/> { "hostPort": 8080, "containerPort": 80, "protocol": "tcp" },<br /> { "hostPort": 8000, "containerPort": 8001, "protocol": "udp" }<br /> ]<br /></pre> | kubernetes | CNI `portmap` plugin |
| ip ranges | Dynamically configure the IP range(s) for address allocation. Runtimes that manage IP pools, but not individual IP addresses, can pass these to plugins. | `ipRanges` | The same as the `ranges` key for `host-local` - a list of lists of subnets. The outer list is the number of IPs to allocate, and the inner list is a pool of subnets for each allocation. <br/><pre>[<br/> [<br/> { "subnet": "10.1.2.0/24", "rangeStart": "10.1.2.3", "rangeEnd": 10.1.2.99", "gateway": "10.1.2.254" } <br/> ]<br/>]</pre> | none | CNI `host-local` plugin | | ip ranges | Dynamically configure the IP range(s) for address allocation. Runtimes that manage IP pools, but not individual IP addresses, can pass these to plugins. | `ipRanges` | The same as the `ranges` key for `host-local` - a list of lists of subnets. The outer list is the number of IPs to allocate, and the inner list is a pool of subnets for each allocation. <br/><pre>[<br/> [<br/> { "subnet": "10.1.2.0/24", "rangeStart": "10.1.2.3", "rangeEnd": 10.1.2.99", "gateway": "10.1.2.254" } <br/> ]<br/>]</pre> | none | CNI `host-local` plugin |
| bandwidth limits | Dynamically configure interface bandwidth limits | `bandwidth` | Desired bandwidth limits. Rates are in bits per second, burst values are in bits. <pre> { "ingressRate": 2048, "ingressBurst": 1600, "egressRate": 4096, "egressBurst": 1600 } </pre> | none | CNI `bandwidth` plugin | | bandwidth limits | Dynamically configure interface bandwidth limits | `bandwidth` | Desired bandwidth limits. Rates are in bits per second, burst values are in bits. <pre> { "ingressRate": 2048, "ingressBurst": 1600, "egressRate": 4096, "egressBurst": 1600 } </pre> | none | CNI `bandwidth` plugin |
| dns | Dynamically configure dns according to runtime | `dns` | Dictionary containing a list of `servers` (string entries), a list of `searches` (string entries), a list of `options` (string entries). <pre>{ <br> "searches" : [ "internal.yoyodyne.net", "corp.tyrell.net" ] <br> "servers": [ "8.8.8.8", "10.0.0.10" ] <br />} </pre> | kubernetes | CNI `win-bridge` plugin, CNI `win-overlay` plugin | | Dns | Dymaically configure dns according to runtime | `dns` | Dictionary containing a list of `servers` (string entries), a list of `searches` (string entries), a list of `options` (string entries). <pre>{ <br> "searches" : [ "internal.yoyodyne.net", "corp.tyrell.net" ] <br> "servers": [ "8.8.8.8", "10.0.0.10" ] <br />} </pre> | kubernetes | CNI `win-bridge` plugin, CNI `win-overlay` plugin |
| ips | Dynamically allocate IPs for container interface. Runtime which has the ability of address allocation can pass these to plugins. | `ips` | A list of `IP` (\<ip\>\[/\<prefix\>\]). <pre> [ "192.168.0.1", 10.10.0.1/24", "3ffe:ffff:0:01ff::2", "3ffe:ffff:0:01ff::1/64" ] </pre> The plugin may require the IP address to include a prefix length. | none | CNI `static` plugin, CNI `host-local` plugin |
| mac | Dynamically assign MAC. Runtime can pass this to plugins which need MAC as input. | `mac` | `MAC` (string entry). <pre> "c2:11:22:33:44:55" </pre> | none | CNI `tuning` plugin |
| infiniband guid | Dynamically assign Infiniband GUID to network interface. Runtime can pass this to plugins which need Infiniband GUID as input. | `infinibandGUID` | `GUID` (string entry). <pre> "c2:11:22:33:44:55:66:77" </pre> | none | CNI [`ib-sriov-cni`](https://github.com/Mellanox/ib-sriov-cni) plugin |
| device id | Provide device identifier which is associated with the network to allow the CNI plugin to perform device dependent network configurations. | `deviceID` | `deviceID` (string entry). <pre> "0000:04:00.5" </pre> | none | CNI `host-device` plugin |
| aliases | Provide a list of names that will be mapped to the IP addresses assigned to this interface. Other containers on the same network may use one of these names to access the container.| `aliases` | List of `alias` (string entry). <pre> ["my-container", "primary-db"] </pre> | none | CNI `alias` plugin |
| cgroup path | Provide the cgroup path for pod as requested by CNI plugins. | `cgroupPath` | `cgroupPath` (string entry). <pre>"/kubelet.slice/kubelet-kubepods.slice/kubelet-kubepods-burstable.slice/kubelet-kubepods-burstable-pod28ce45bc_63f8_48a3_a99b_cfb9e63c856c.slice" </pre> | none | CNI `host-local` plugin |
## "args" in network config ## "args" in network config
`args` in [network config](SPEC.md#network-configuration) were reserved as a field in the `0.2.0` release of the CNI spec. `args` in [network config](https://github.com/containernetworking/cni/blob/master/SPEC.md#network-configuration) were introduced as an optional field into the `0.2.0` release of the CNI spec. The first CNI code release that it appeared in was `v0.4.0`.
> args (dictionary): Optional additional arguments provided by the container runtime. For example a dictionary of labels could be passed to CNI plugins by adding them to a labels field under args. > args (dictionary): Optional additional arguments provided by the container runtime. For example a dictionary of labels could be passed to CNI plugins by adding them to a labels field under args.
`args` provide a way of providing more structured data than the flat strings that CNI_ARGS can support. `args` provide a way of providing more structured data than the flat strings that CNI_ARGS can support.
@ -81,7 +76,7 @@ This method of passing information to a plugin is recommended when the informati
The conventions documented here are all namespaced under `cni` so they don't conflict with any existing `args`. The conventions documented here are all namespaced under `cni` so they don't conflict with any existing `args`.
For example: For example:
```jsonc ```json
{ {
"cniVersion":"0.2.0", "cniVersion":"0.2.0",
"name":"net", "name":"net",
@ -90,9 +85,9 @@ For example:
"labels": [{"key": "app", "value": "myapp"}] "labels": [{"key": "app", "value": "myapp"}]
} }
}, },
// <REST OF CNI CONFIG HERE> <REST OF CNI CONFIG HERE>
"ipam":{ "ipam":{
// <IPAM CONFIG HERE> <IPAM CONFIG HERE>
} }
} }
``` ```
@ -100,7 +95,7 @@ For example:
| Area | Purpose| Spec and Example | Runtime implementations | Plugin Implementations | | Area | Purpose| Spec and Example | Runtime implementations | Plugin Implementations |
| ----- | ------ | ------------ | ----------------------- | ---------------------- | | ----- | ------ | ------------ | ----------------------- | ---------------------- |
| labels | Pass`key=value` labels to plugins | <pre>"labels" : [<br /> { "key" : "app", "value" : "myapp" },<br /> { "key" : "env", "value" : "prod" }<br />] </pre> | none | none | | labels | Pass`key=value` labels to plugins | <pre>"labels" : [<br /> { "key" : "app", "value" : "myapp" },<br /> { "key" : "env", "value" : "prod" }<br />] </pre> | none | none |
| ips | Request specific IPs | Spec:<pre>"ips": ["\<ip\>[/\<prefix\>]", ...]</pre>Examples:<pre>"ips": ["10.2.2.42/24", "2001:db8::5"]</pre> The plugin may require the IP address to include a prefix length. | none | host-local, static | | ips | Request static IPs | Spec:<pre>"ips": ["\<ip\>[/\<prefix\>]", ...]</pre>Examples:<pre>"ips": ["10.2.2.42/24", "2001:db8::5"]</pre>The plugin may require the IP address to include a prefix length. | none | host-local |
## CNI_ARGS ## CNI_ARGS
CNI_ARGS formed part of the original CNI spec and have been present since the initial release. CNI_ARGS formed part of the original CNI spec and have been present since the initial release.
@ -110,7 +105,7 @@ The use of `CNI_ARGS` is deprecated and "args" should be used instead. If a runt
| Field | Purpose| Spec and Example | Runtime implementations | Plugin Implementations | | Field | Purpose| Spec and Example | Runtime implementations | Plugin Implementations |
| ------ | ------ | ---------------- | ----------------------- | ---------------------- | | ------ | ------ | ---------------- | ----------------------- | ---------------------- |
| IP | Request a specific IP from IPAM plugins | Spec:<pre>IP=\<ip\>[/\<prefix\>]</pre>Example: <pre>IP=192.168.10.4/24</pre> The plugin may require the IP addresses to include a prefix length. | *rkt* supports passing additional arguments to plugins and the [documentation](https://coreos.com/rkt/docs/latest/networking/overriding-defaults.html) suggests IP can be used. | host-local, static | | IP | Request a specific IP from IPAM plugins | Spec:<pre>IP=\<ip\>[/\<prefix\>]</pre>Example: <pre>IP=192.168.10.4/24</pre>The plugin may require the IP addresses to include a prefix length. | *rkt* supports passing additional arguments to plugins and the [documentation](https://coreos.com/rkt/docs/latest/networking/overriding-defaults.html) suggests IP can be used. | host-local (since version v0.2.0) supports the field for IPv4 only - [documentation](https://github.com/containernetworking/plugins/tree/master/plugins/ipam/host-local#supported-arguments).|
## Chained Plugins ## Chained Plugins
If plugins are agnostic about the type of interface created, they SHOULD work in a chained mode and configure existing interfaces. Plugins MAY also create the desired interface when not run in a chain. If plugins are agnostic about the type of interface created, they SHOULD work in a chained mode and configure existing interfaces. Plugins MAY also create the desired interface when not run in a chain.

View File

@ -1,11 +1,9 @@
# Overview # Overview
The `cnitool` is a utility that can be used to test a CNI plugin The `cnitool` is a utility that can be used to test a CNI plugin
without the need for a container runtime. The `cnitool` takes a without the need for a container runtime. The `cnitool` takes a
`network name` and a `network namespace` and a command to `ADD` or `network name` and a `network namespace` and a command to `ADD` or
`DEL`,.i.e, attach or detach containers from a network. The `cnitool` `DEL`,.i.e, attach or detach containers from a network. The `cnitool`
relies on the following environment variables to operate properly: relies on the following environment variables to operate properly:
* `NETCONFPATH`: This environment variable needs to be set to a * `NETCONFPATH`: This environment variable needs to be set to a
directory. It defaults to `/etc/cni/net.d`. The `cnitool` searches directory. It defaults to `/etc/cni/net.d`. The `cnitool` searches
for CNI configuration files in this directory with the extension for CNI configuration files in this directory with the extension
@ -13,7 +11,6 @@ relies on the following environment variables to operate properly:
this directory and if it finds a CNI configuration with the `network this directory and if it finds a CNI configuration with the `network
name` given to the cnitool it returns the corresponding CNI name` given to the cnitool it returns the corresponding CNI
configuration, else it returns `nil`. configuration, else it returns `nil`.
* `CNI_PATH`: For a given CNI configuration `cnitool` will search for * `CNI_PATH`: For a given CNI configuration `cnitool` will search for
the corresponding CNI plugin in this path. the corresponding CNI plugin in this path.
For the full documentation of `cnitool` see the [cnitool docs](../cnitool/README.md)

View File

@ -1,45 +1,12 @@
# How to Upgrade to CNI Specification v1.0 # How to upgrade to CNI Specification v0.3.1
CNI v1.0 has the following changes:
- non-List configurations are removed
- the `version` field in the `interfaces` array was redundant and is removed
## libcni Changes in CNI v1.0
**`/pkg/types/current` no longer exists**
This means that runtimes need to explicitly select a version they support.
This reduces code breakage when revendoring cni into other projects and
returns the decision on which CNI Spec versions a plugin supports to the
plugin's authors.
For example, your Go imports might look like
```go
import (
cniv1 "github.com/containernetworking/cni/pkg/types/100"
)
```
# Changes in CNI v0.4
CNI v0.4 has the following important changes:
- A new verb, "CHECK", was added. Runtimes can now ask plugins to verify the status of a container's attachment
- A new configuration flag, `disableCheck`, which indicates to the runtime that configuration should not be CHECK'ed
No changes were made to the result type.
# How to upgrade to CNI Specification v0.3.0 and later
The 0.3.0 specification contained a small error. The Result structure's `ip` field should have been renamed to `ips` to be consistent with the IPAM result structure definition; this rename was missed when updating the Result to accommodate multiple IP addresses and interfaces. All first-party CNI plugins (bridge, host-local, etc) were updated to use `ips` (and thus be inconsistent with the 0.3.0 specification) and most other plugins have not been updated to the 0.3.0 specification yet, so few (if any) users should be impacted by this change. The 0.3.0 specification contained a small error. The Result structure's `ip` field should have been renamed to `ips` to be consistent with the IPAM result structure definition; this rename was missed when updating the Result to accommodate multiple IP addresses and interfaces. All first-party CNI plugins (bridge, host-local, etc) were updated to use `ips` (and thus be inconsistent with the 0.3.0 specification) and most other plugins have not been updated to the 0.3.0 specification yet, so few (if any) users should be impacted by this change.
The 0.3.1 specification corrects the `Result` structure to use the `ips` field name as originally intended. This is the only change between 0.3.0 and 0.3.1. The 0.3.1 specification corrects the Result structure to use the `ips` field name as originally intended. This is the only change between 0.3.0 and 0.3.1.
Version 0.3.0 of the [CNI Specification](https://github.com/containernetworking/cni/blob/spec-v0.3.0/SPEC.md) provides rich information # How to upgrade to CNI Specification v0.3.0
Version 0.3.0 of the [CNI Specification](../SPEC.md) provides rich information
about container network configuration, including details of network interfaces about container network configuration, including details of network interfaces
and support for multiple IP addresses. and support for multiple IP addresses.
@ -64,12 +31,12 @@ ensure that the configuration files specify a `cniVersion` field and that the
version there is supported by your container runtime and CNI plugins. version there is supported by your container runtime and CNI plugins.
Configuration files without a version field should be given version 0.2.0. Configuration files without a version field should be given version 0.2.0.
The CNI spec includes example configuration files for The CNI spec includes example configuration files for
[single plugins](SPEC.md#example-configurations) [single plugins](https://github.com/containernetworking/cni/blob/master/SPEC.md#example-configurations)
and for [lists of chained plugins](SPEC.md#example-configurations). and for [lists of chained plugins](https://github.com/containernetworking/cni/blob/master/SPEC.md#example-configurations).
Consult the documentation for your runtime and plugins to determine what Consult the documentation for your runtime and plugins to determine what
CNI spec versions they support. Test any plugin upgrades before deploying to CNI spec versions they support. Test any plugin upgrades before deploying to
production. You may find [cnitool](https://github.com/containernetworking/cni/tree/main/cnitool) production. You may find [cnitool](https://github.com/containernetworking/cni/tree/master/cnitool)
useful. Specifically, your configuration version should be the lowest common useful. Specifically, your configuration version should be the lowest common
version supported by your plugins. version supported by your plugins.
@ -79,7 +46,7 @@ This section provides guidance for upgrading plugins to CNI Spec Version 0.3.0.
### General guidance for all plugins (language agnostic) ### General guidance for all plugins (language agnostic)
To provide the smoothest upgrade path, **existing plugins should support To provide the smoothest upgrade path, **existing plugins should support
multiple versions of the CNI spec**. In particular, plugins with existing multiple versions of the CNI spec**. In particular, plugins with existing
installed bases should add support for CNI spec version 1.0.0 while maintaining installed bases should add support for CNI spec version 0.3.0 while maintaining
compatibility with older versions. compatibility with older versions.
To do this, two changes are required. First, a plugin should advertise which To do this, two changes are required. First, a plugin should advertise which
@ -88,13 +55,13 @@ command with the following JSON data:
```json ```json
{ {
"cniVersion": "1.0.0", "cniVersion": "0.3.0",
"supportedVersions": [ "0.1.0", "0.2.0", "0.3.0", "0.3.1", "0.4.0", "1.0.0" ] "supportedVersions": [ "0.1.0", "0.2.0", "0.3.0" ]
} }
``` ```
Second, for the `ADD` command, a plugin must respect the `cniVersion` field Second, for the `ADD` command, a plugin must respect the `cniVersion` field
provided in the [network configuration JSON](SPEC.md#network-configuration). provided in the [network configuration JSON](https://github.com/containernetworking/cni/blob/master/SPEC.md#network-configuration).
That field is a request for the plugin to return results of a particular format: That field is a request for the plugin to return results of a particular format:
- If the `cniVersion` field is not present, then spec v0.2.0 should be assumed - If the `cniVersion` field is not present, then spec v0.2.0 should be assumed
@ -102,11 +69,11 @@ That field is a request for the plugin to return results of a particular format:
- If the plugin doesn't support the version, the plugin must error. - If the plugin doesn't support the version, the plugin must error.
- Otherwise, the plugin must return a [CNI Result](SPEC.md#result) - Otherwise, the plugin must return a [CNI Result](https://github.com/containernetworking/cni/blob/master/SPEC.md#result)
in the format requested. in the format requested.
Result formats for older CNI spec versions are available in the Result formats for older CNI spec versions are available in the
[git history for SPEC.md](https://github.com/containernetworking/cni/commits/main/SPEC.md). [git history for SPEC.md](https://github.com/containernetworking/cni/commits/master/SPEC.md).
For example, suppose a plugin, via its `VERSION` response, advertises CNI specification For example, suppose a plugin, via its `VERSION` response, advertises CNI specification
support for v0.2.0 and v0.3.0. When it receives `cniVersion` key of `0.2.0`, support for v0.2.0 and v0.3.0. When it receives `cniVersion` key of `0.2.0`,
@ -121,15 +88,15 @@ require some changes now, but should more-easily handle spec changes and
new features going forward. new features going forward.
For plugin authors, the biggest change is that `types.Result` is now an For plugin authors, the biggest change is that `types.Result` is now an
interface implemented by concrete struct types in the `types/100`, interface implemented by concrete struct types in the `types/current` and
`types/040`, and `types/020` subpackages. `types/020` subpackages.
Internally, plugins should use the latest spec version (eg `types/100`) structs, Internally, plugins should use the `types/current` structs, and convert
and convert to or from specific versions when required. A typical plugin will to or from specific versions when required. A typical plugin will only need
only need to do a single conversion when it is about to complete and to do a single conversion. That is when it is about to complete and needs to
needs to print the result JSON in the requested `cniVersion` format to stdout. print the result JSON in the correct format to stdout. The library
The library function `types.PrintResult()` simplifies this by converting and function `types.PrintResult()` simplifies this by converting and printing in
printing in a single call. a single call.
Additionally, the plugin should advertise which CNI Spec versions it supports Additionally, the plugin should advertise which CNI Spec versions it supports
via the 3rd argument to `skel.PluginMain()`. via the 3rd argument to `skel.PluginMain()`.
@ -140,7 +107,7 @@ Here is some example code
import ( import (
"github.com/containernetworking/cni/pkg/skel" "github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types"
current "github.com/containernetworking/cni/pkg/types/100" "github.com/containernetworking/cni/pkg/types/current"
"github.com/containernetworking/cni/pkg/version" "github.com/containernetworking/cni/pkg/version"
) )
@ -169,7 +136,7 @@ func cmdAdd(args *skel.CmdArgs) error {
} }
func main() { func main() {
skel.PluginMain(cmdAdd, cmdDel, version.All) skel.PluginMain(cmdAdd, cmdDel, version.PluginSupports("0.1.0", "0.2.0", "0.3.0"))
} }
``` ```
@ -182,13 +149,13 @@ result, err := current.NewResultFromResult(ipamResult)
``` ```
Other examples of spec v0.3.0-compatible plugins are the Other examples of spec v0.3.0-compatible plugins are the
[main plugins in this repo](https://github.com/containernetworking/plugins/) [main plugins in this repo](https://github.com/containernetworking/plugins/tree/master/plugins)
## For Runtime Authors ## For Runtime Authors
This section provides guidance for upgrading container runtimes to support This section provides guidance for upgrading container runtimes to support
CNI Spec Version 0.3.0 and later. CNI Spec Version 0.3.0.
### General guidance for all runtimes (language agnostic) ### General guidance for all runtimes (language agnostic)
@ -196,22 +163,22 @@ CNI Spec Version 0.3.0 and later.
To provide the smoothest upgrade path and support the broadest range of CNI To provide the smoothest upgrade path and support the broadest range of CNI
plugins, **container runtimes should support multiple versions of the CNI spec**. plugins, **container runtimes should support multiple versions of the CNI spec**.
In particular, runtimes with existing installed bases should add support for CNI In particular, runtimes with existing installed bases should add support for CNI
spec version 0.3.0 and later while maintaining compatibility with older versions. spec version 0.3.0 while maintaining compatibility with older versions.
To support multiple versions of the CNI spec, runtimes should be able to To support multiple versions of the CNI spec, runtimes should be able to
call both new and legacy plugins, and handle the results from either. call both new and legacy plugins, and handle the results from either.
When calling a plugin, the runtime must request that the plugin respond in a When calling a plugin, the runtime must request that the plugin respond in a
particular format by specifying the `cniVersion` field in the particular format by specifying the `cniVersion` field in the
[Network Configuration](SPEC.md#network-configuration) [Network Configuration](https://github.com/containernetworking/cni/blob/master/SPEC.md#network-configuration)
JSON block. The plugin will then respond with JSON block. The plugin will then respond with
a [Result](SPEC.md#result) a [Result](https://github.com/containernetworking/cni/blob/master/SPEC.md#result)
in the format defined by that CNI spec version, and the runtime must parse in the format defined by that CNI spec version, and the runtime must parse
and handle this result. and handle this result.
#### Handle errors due to version incompatibility #### Handle errors due to version incompatibility
Plugins may respond with error indicating that they don't support the requested Plugins may respond with error indicating that they don't support the requested
CNI version (see [Well-known Error Codes](SPEC.md#well-known-error-codes)), CNI version (see [Well-known Error Codes](https://github.com/containernetworking/cni/blob/master/SPEC.md#well-known-error-codes)),
e.g. e.g.
```json ```json
{ {
@ -230,17 +197,16 @@ added in CNI spec v0.2.0, so older plugins may not respect it. In the absence
of a successful response to `VERSION`, assume that the plugin only supports of a successful response to `VERSION`, assume that the plugin only supports
CNI spec v0.1.0. CNI spec v0.1.0.
#### Handle missing data in v0.3.0 and later results #### Handle missing data in v0.3.0 results
The Result for the `ADD` command in CNI spec version 0.3.0 and later includes The Result for the `ADD` command in CNI spec version 0.3.0 includes a new field
a new field `interfaces`. An IP address in the `ip` field may describe which `interfaces`. An IP address in the `ip` field may describe which interface
interface it is assigned to, by placing a numeric index in the `interface` it is assigned to, by placing a numeric index in the `interface` subfield.
subfield.
However, some plugins which are v0.3.0 and later compatible may nonetheless However, some plugins which are v0.3.0 compatible may nonetheless omit the
omit the `interfaces` field and/or set the `interface` index value to `-1`. `interfaces` field and/or set the `interface` index value to `-1`. Runtimes
Runtimes should gracefully handle this situation, unless they have good reason should gracefully handle this situation, unless they have good reason to rely
to rely on the existence of the interface data. In that case, provide the user on the existence of the interface data. In that case, provide the user an
an error message that helps diagnose the issue. error message that helps diagnose the issue.
### Specific guidance for container runtimes written in Go ### Specific guidance for container runtimes written in Go
Container runtimes written in Go may leverage the Go language packages in this Container runtimes written in Go may leverage the Go language packages in this
@ -257,18 +223,17 @@ other packages, such as the high-level `libcni` package, have been updated to us
this interface. Concrete types are now per-version subpackages. The `types/current` this interface. Concrete types are now per-version subpackages. The `types/current`
subpackage contains the latest (spec v0.3.0) types. subpackage contains the latest (spec v0.3.0) types.
When up-converting older result types to spec v0.3.0 and later, fields new in When up-converting older result types to spec v0.3.0, fields new in
spec v0.3.0 and later (like `interfaces`) may be empty. Conversely, when spec v0.3.0 (like `interfaces`) may be empty. Conversely, when
down-converting v0.3.0 and later results to an older version, any data in down-converting v0.3.0 results to an older version, any data in those fields
those fields will be lost. will be lost.
| From | 0.1 | 0.2 | 0.3 |
|--------|-----|-----|-----|
| To 0.1 | ✔ | ✔ | x |
| To 0.2 | ✔ | ✔ | x |
| To 0.3 | ✴ | ✴ | ✔ |
| From | 0.1 | 0.2 | 0.3 | 0.4 | 1.0 |
|--------|-----|-----|-----|-----|-----|
| To 0.1 | ✔ | ✔ | x | x | x |
| To 0.2 | ✔ | ✔ | x | x | x |
| To 0.3 | ✴ | ✴ | ✔ | ✔ | ✔ |
| To 0.4 | ✴ | ✴ | ✔ | ✔ | ✔ |
| To 1.0 | ✴ | ✴ | ✔ | ✔ | ✔ |
Key: Key:
> ✔ : lossless conversion <br> > ✔ : lossless conversion <br>

View File

@ -1,14 +1,6 @@
Bruce Ma <brucema19901024@gmail.com> (@mars1024) Bryan Boreham <bryan@weave.works> (@bboreham)
Casey Callendrello <cdc@isovalent.com> (@squeed) Casey Callendrello <cdc@redhat.com> (@squeed)
Michael Cambria <mcambria@redhat.com> (@mccv1r0)
Michael Zappa <Michael.Zappa@gmail.com> (@MikeZappa87)
Tomofumi Hayashi <s1061123@gmail.com> (@s1061123)
Lionel Jouin <lionel.jouin@est.tech> (@LionelJouin)
Ben Leggett <benjamin@edera.dev> (@bleggett)
Marcelo Guerrero <guerrero.viveros@gmail.com> (@mlguerrero12)
Doug Smith <douglas.kipp.smith@gmail.com> (@dougbtv)
Emeritus:
Dan Williams <dcbw@redhat.com> (@dcbw) Dan Williams <dcbw@redhat.com> (@dcbw)
Gabe Rosenhouse <grosenhouse@pivotal.io> (@rosenhouse)
Matt Dupre <matt@tigera.io> (@matthewdupre) Matt Dupre <matt@tigera.io> (@matthewdupre)
Piotr Skamruk <piotr.skamruk@gmail.com> (@jellonek) Stefan Junker <stefan.junker@coreos.com> (@steveeJ)

View File

@ -1 +0,0 @@
include mk/lint.mk

View File

@ -1,11 +1,21 @@
[![Linux Build Status](https://travis-ci.org/containernetworking/cni.svg?branch=master)](https://travis-ci.org/containernetworking/cni)
[![Windows Build Status](https://ci.appveyor.com/api/projects/status/wtrkou8oow7x533e/branch/master?svg=true)](https://ci.appveyor.com/project/cni-bot/cni/branch/master)
[![Coverage Status](https://coveralls.io/repos/github/containernetworking/cni/badge.svg?branch=master)](https://coveralls.io/github/containernetworking/cni?branch=master)
[![Slack Status](https://cryptic-tundra-43194.herokuapp.com/badge.svg)](https://cryptic-tundra-43194.herokuapp.com/)
![CNI Logo](logo.png) ![CNI Logo](logo.png)
--- ---
# CNI - the Container Network Interface # Community Sync Meeting
[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/2446/badge)](https://bestpractices.coreinfrastructure.org/projects/2446) There is a community sync meeting for users and developers every 1-2 months. The next meeting will help on a Google Hangout and the link is in the [agenda](https://docs.google.com/document/d/10ECyT2mBGewsJUcmYmS8QNo1AcNgy2ZIe2xS7lShYhE/edit?usp=sharing) (Notes from previous meeting are also in this doc).
[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/containernetworking/cni/badge)](https://securityscorecards.dev/viewer/?uri=github.com/containernetworking/cni)
The next meeting will be held on *Wednesday, January 30th, 2019* at *4:00pm UTC / 11:00am EDT / 8:00am PDT* [Add to Calendar](https://www.worldtimebuddy.com/?qm=1&lid=100,5,2643743,5391959&h=100&date=2019-01-30&sln=16-17).
---
# CNI - the Container Network Interface
## What is CNI? ## What is CNI?
@ -18,19 +28,6 @@ As well as the [specification](SPEC.md), this repository contains the Go source
The template code makes it straight-forward to create a CNI plugin for an existing container networking project. The template code makes it straight-forward to create a CNI plugin for an existing container networking project.
CNI also makes a good framework for creating a new container networking project from scratch. CNI also makes a good framework for creating a new container networking project from scratch.
Here are the recordings of two sessions that the CNI maintainers hosted at KubeCon/CloudNativeCon 2019:
- [Introduction to CNI](https://youtu.be/YjjrQiJOyME)
- [CNI deep dive](https://youtu.be/zChkx-AB5Xc)
## Contributing to CNI
We welcome contributions, including [bug reports](https://github.com/containernetworking/cni/issues), and code and documentation improvements.
If you intend to contribute to code or documentation, please read [CONTRIBUTING.md](CONTRIBUTING.md). Also see the [contact section](#contact) in this README.
The CNI project has a [weekly meeting](https://meet.jit.si/CNIMaintainersMeeting). It takes place Mondays at 11:00 US/Eastern. All are welcome to join.
## Why develop CNI? ## Why develop CNI?
Application containers on Linux are a rapidly evolving area, and within this area networking is not well addressed as it is highly environment-specific. Application containers on Linux are a rapidly evolving area, and within this area networking is not well addressed as it is highly environment-specific.
@ -40,7 +37,8 @@ To avoid duplication, we think it is prudent to define a common interface betwee
## Who is using CNI? ## Who is using CNI?
### Container runtimes ### Container runtimes
- [Kubernetes - a system to simplify container operations](https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/network-plugins/) - [rkt - container engine](https://coreos.com/blog/rkt-cni-networking.html)
- [Kubernetes - a system to simplify container operations](https://kubernetes.io/docs/admin/network-plugins/)
- [OpenShift - Kubernetes with additional enterprise features](https://github.com/openshift/origin/blob/master/docs/openshift_networking_requirements.md) - [OpenShift - Kubernetes with additional enterprise features](https://github.com/openshift/origin/blob/master/docs/openshift_networking_requirements.md)
- [Cloud Foundry - a platform for cloud applications](https://github.com/cloudfoundry-incubator/cf-networking-release) - [Cloud Foundry - a platform for cloud applications](https://github.com/cloudfoundry-incubator/cf-networking-release)
- [Apache Mesos - a distributed systems kernel](https://github.com/apache/mesos/blob/master/docs/cni.md) - [Apache Mesos - a distributed systems kernel](https://github.com/apache/mesos/blob/master/docs/cni.md)
@ -49,14 +47,17 @@ To avoid duplication, we think it is prudent to define a common interface betwee
- [OpenSVC - orchestrator for legacy and containerized application stacks](https://docs.opensvc.com/latest/fr/agent.configure.cni.html) - [OpenSVC - orchestrator for legacy and containerized application stacks](https://docs.opensvc.com/latest/fr/agent.configure.cni.html)
### 3rd party plugins ### 3rd party plugins
- [Project Calico - a layer 3 virtual network](https://github.com/projectcalico/calico) - [Project Calico - a layer 3 virtual network](https://github.com/projectcalico/calico-cni)
- [Weave - a multi-host Docker network](https://github.com/weaveworks/weave)
- [Contiv Networking - policy networking for various use cases](https://github.com/contiv/netplugin) - [Contiv Networking - policy networking for various use cases](https://github.com/contiv/netplugin)
- [SR-IOV](https://github.com/hustcat/sriov-cni) - [SR-IOV](https://github.com/hustcat/sriov-cni)
- [Cilium - eBPF & XDP for containers](https://github.com/cilium/cilium) - [Cilium - BPF & XDP for containers](https://github.com/cilium/cilium)
- [Multus - a Multi plugin](https://github.com/k8snetworkplumbingwg/multus-cni) - [Infoblox - enterprise IP address management for containers](https://github.com/infobloxopen/cni-infoblox)
- [Multus - a Multi plugin](https://github.com/Intel-Corp/multus-cni)
- [Romana - Layer 3 CNI plugin supporting network policy for Kubernetes](https://github.com/romana/kube) - [Romana - Layer 3 CNI plugin supporting network policy for Kubernetes](https://github.com/romana/kube)
- [CNI-Genie - generic CNI network plugin](https://github.com/Huawei-PaaS/CNI-Genie) - [CNI-Genie - generic CNI network plugin](https://github.com/Huawei-PaaS/CNI-Genie)
- [Nuage CNI - Nuage Networks SDN plugin for network policy kubernetes support ](https://github.com/nuagenetworks/nuage-cni) - [Nuage CNI - Nuage Networks SDN plugin for network policy kubernetes support ](https://github.com/nuagenetworks/nuage-cni)
- [Silk - a CNI plugin designed for Cloud Foundry](https://github.com/cloudfoundry-incubator/silk)
- [Linen - a CNI plugin designed for overlay networks with Open vSwitch and fit in SDN/OpenFlow network environment](https://github.com/John-Lin/linen-cni) - [Linen - a CNI plugin designed for overlay networks with Open vSwitch and fit in SDN/OpenFlow network environment](https://github.com/John-Lin/linen-cni)
- [Vhostuser - a Dataplane network plugin - Supports OVS-DPDK & VPP](https://github.com/intel/vhost-user-net-plugin) - [Vhostuser - a Dataplane network plugin - Supports OVS-DPDK & VPP](https://github.com/intel/vhost-user-net-plugin)
- [Amazon ECS CNI Plugins - a collection of CNI Plugins to configure containers with Amazon EC2 elastic network interfaces (ENIs)](https://github.com/aws/amazon-ecs-cni-plugins) - [Amazon ECS CNI Plugins - a collection of CNI Plugins to configure containers with Amazon EC2 elastic network interfaces (ENIs)](https://github.com/aws/amazon-ecs-cni-plugins)
@ -65,19 +66,16 @@ To avoid duplication, we think it is prudent to define a common interface betwee
- [Juniper Contrail](https://www.juniper.net/cloud) / [TungstenFabric](https://tungstenfabric.io) - Provides overlay SDN solution, delivering multicloud networking, hybrid cloud networking, simultaneous overlay-underlay support, network policy enforcement, network isolation, service chaining and flexible load balancing - [Juniper Contrail](https://www.juniper.net/cloud) / [TungstenFabric](https://tungstenfabric.io) - Provides overlay SDN solution, delivering multicloud networking, hybrid cloud networking, simultaneous overlay-underlay support, network policy enforcement, network isolation, service chaining and flexible load balancing
- [Knitter - a CNI plugin supporting multiple networking for Kubernetes](https://github.com/ZTE/Knitter) - [Knitter - a CNI plugin supporting multiple networking for Kubernetes](https://github.com/ZTE/Knitter)
- [DANM - a CNI-compliant networking solution for TelCo workloads running on Kubernetes](https://github.com/nokia/danm) - [DANM - a CNI-compliant networking solution for TelCo workloads running on Kubernetes](https://github.com/nokia/danm)
- [cni-route-override - a meta CNI plugin that override route information](https://github.com/redhat-nfvpe/cni-route-override) - [VMware NSX a CNI plugin that enables automated NSX L2/L3 networking and L4/L7 Load Balancing; network isolation at the pod, node, and cluster level; and zero-trust security policy for your Kubernetes cluster.](https://docs.vmware.com/en/VMware-NSX-T/2.2/com.vmware.nsxt.ncp_kubernetes.doc/GUID-6AFA724E-BB62-4693-B95C-321E8DDEA7E1.html)
- [Terway - a collection of CNI Plugins based on alibaba cloud VPC/ECS network product](https://github.com/AliyunContainerService/terway)
- [Cisco ACI CNI - for on-prem and cloud container networking with consistent policy and security model.](https://github.com/noironetworks/aci-containers)
- [Kube-OVN - a CNI plugin that bases on OVN/OVS and provides advanced features like subnet, static ip, ACL, QoS, etc.](https://github.com/kubeovn/kube-ovn)
- [Project Antrea - an Open vSwitch k8s CNI](https://github.com/vmware-tanzu/antrea)
- [Azure CNI - a CNI plugin that natively extends Azure Virtual Networks to containers](https://github.com/Azure/azure-container-networking)
- [Hybridnet - a CNI plugin designed for hybrid clouds which provides both overlay and underlay networking for containers in one or more clusters. Overlay and underlay containers can run on the same node and have cluster-wide bidirectional network connectivity.](https://github.com/alibaba/hybridnet)
- [Spiderpool - An IP Address Management (IPAM) CNI plugin of Kubernetes for managing static ip for underlay network](https://github.com/spidernet-io/spiderpool)
- [AWS VPC CNI - Networking plugin for pod networking in Kubernetes using Elastic Network Interfaces on AWS](https://github.com/aws/amazon-vpc-cni-k8s)
The CNI team also maintains some [core plugins in a separate repository](https://github.com/containernetworking/plugins). The CNI team also maintains some [core plugins in a separate repository](https://github.com/containernetworking/plugins).
## Contributing to CNI
We welcome contributions, including [bug reports](https://github.com/containernetworking/cni/issues), and code and documentation improvements.
If you intend to contribute to code or documentation, please read [CONTRIBUTING.md](CONTRIBUTING.md). Also see the [contact section](#contact) in this README.
## How do I use CNI? ## How do I use CNI?
### Requirements ### Requirements
@ -191,30 +189,17 @@ lo Link encap:Local Loopback
## What might CNI do in the future? ## What might CNI do in the future?
CNI currently covers a wide range of needs for network configuration due to its simple model and API. CNI currently covers a wide range of needs for network configuration due to it simple model and API.
However, in the future CNI might want to branch out into other directions: However, in the future CNI might want to branch out into other directions:
- Dynamic updates to existing network configuration - Dynamic updates to existing network configuration
- Dynamic policies for network bandwidth and firewall rules - Dynamic policies for network bandwidth and firewall rules
If these topics are of interest, please contact the team via the mailing list or IRC and find some like-minded people in the community to put a proposal together. If these topics of are interest, please contact the team via the mailing list or IRC and find some like-minded people in the community to put a proposal together.
## Where are the binaries?
The plugins moved to a separate repo:
https://github.com/containernetworking/plugins, and the releases there
include binaries and checksums.
Prior to release 0.7.0 the `cni` release also included a `cnitool`
binary; as this is a developer tool we suggest you build it yourself.
## Contact ## Contact
For any questions about CNI, please reach out via: For any questions about CNI, please reach out on the mailing list:
- Email: [cni-dev](https://groups.google.com/forum/#!forum/cni-dev) - Email: [cni-dev](https://groups.google.com/forum/#!forum/cni-dev)
- IRC: #[containernetworking](irc://irc.freenode.net:6667/#containernetworking) channel on [freenode.net](https://freenode.net/) - IRC: #[containernetworking](irc://irc.freenode.org:6667/#containernetworking) channel on freenode.org
- Slack: #cni on the [CNCF slack](https://slack.cncf.io/). NOTE: the previous CNI Slack (containernetworking.slack.com) has been sunsetted. - Slack: [containernetworking.slack.com](https://cryptic-tundra-43194.herokuapp.com)
## Security
If you have a _security_ issue to report, please do so privately to the email addresses listed in the [MAINTAINERS](MAINTAINERS) file.

View File

@ -1,19 +1,34 @@
# Release process # Release process
## Resulting artifacts
Creating a new release produces the following artifacts:
- Binaries (stored in the `release-<TAG>` directory) :
- `cni-<PLATFORM>-<VERSION>.tgz` binaries
- `cni-<VERSION>.tgz` binary (copy of amd64 platform binary)
- `sha1`, `sha256` and `sha512` files for the above files.
## Preparing for a release ## Preparing for a release
1. Releases are performed by maintainers and should usually be discussed and planned at a maintainer meeting.
- Choose the version number. It should be prefixed with `v`, e.g. `v1.2.3`
- Take a quick scan through the PRs and issues to make sure there isn't anything crucial that _must_ be in the next release.
- Create a draft of the release note
- Discuss the level of testing that's needed and create a test plan if sensible
- Check what version of `go` is used in the build container, updating it if there's a new stable release.
Releases are performed by maintainers and should usually be discussed and planned at a maintainer meeting. ## Creating the release artifacts
- Choose the version number. It should be prefixed with `v`, e.g. `v1.2.3`
- Take a quick scan through the PRs and issues to make sure there isn't anything crucial that _must_ be in the next release.
- Create a draft of the release note
- Discuss the level of testing that's needed and create a test plan if sensible
- Check what version of `go` is used in the build container, updating it if there's a new stable release.
## Publishing the release
1. Make sure you are on the master branch and don't have any local uncommitted changes. 1. Make sure you are on the master branch and don't have any local uncommitted changes.
1. Create a signed tag for the release `git tag -s $VERSION` (Ensure that GPG keys are created and added to GitHub) 1. Create a signed tag for the release `git tag -s $VERSION` (Ensure that GPG keys are created and added to GitHub)
1. Run the release script from the root of the repository
- `scripts/release.sh`
- The script requires Docker and ensures that a consistent environment is used.
- The artifacts will now be present in the `release-<TAG>` directory.
1. Test these binaries according to the test plan.
## Publishing the release
1. Push the tag to git `git push origin <TAG>` 1. Push the tag to git `git push origin <TAG>`
1. Create a release on Github, using the tag which was just pushed. 1. Create a release on Github, using the tag which was just pushed.
1. Attach all the artifacts from the release directory.
1. Add the release note to the release. 1. Add the release note to the release.
1. Announce the release on at least the CNI mailing, IRC and Slack. 1. Announce the release on at least the CNI mailing, IRC and Slack.

View File

@ -5,14 +5,19 @@ The list below is not complete, and we advise to get the current project state f
## CNI Milestones ## CNI Milestones
### [v0.6.0](https://github.com/containernetworking/cni/milestones/v0.6.0)
- Plugin composition functionality
- IPv6 support
- Strategy and tooling for backwards compatibility
- Integrate build artefact generation with CI
### [v1.0.0](https://github.com/containernetworking/cni/milestones/v1.0.0) ### [v1.0.0](https://github.com/containernetworking/cni/milestones/v1.0.0)
- Targeted for April 2020
- More precise specification language - More precise specification language
- CHECK action
- Conformance test suite for CNI plugins (both reference and 3rd party)
- Stable SPEC - Stable SPEC
- Complete test coverage - Complete test coverage
### Beyond v1.0.0
- Conformance test suite for CNI plugins (both reference and 3rd party)
- Signed release binaries - Signed release binaries

1535
SPEC.md

File diff suppressed because it is too large Load Diff

22
Vagrantfile vendored Normal file
View File

@ -0,0 +1,22 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
Vagrant.configure(2) do |config|
config.vm.box = "bento/ubuntu-16.04"
config.vm.synced_folder ".", "/go/src/github.com/containernetworking/cni"
config.vm.provision "shell", inline: <<-SHELL
set -e -x -u
apt-get update -y || (sleep 40 && apt-get update -y)
apt-get install -y git
wget -qO- https://storage.googleapis.com/golang/go1.11.1.linux-amd64.tar.gz | tar -C /usr/local -xz
echo 'export GOPATH=/go; export PATH=/usr/local/go/bin:$GOPATH/bin:$PATH' >> /root/.bashrc
eval `tail -n1 /root/.bashrc`
cd /go/src/github.com/containernetworking/cni
SHELL
end

View File

@ -3,34 +3,15 @@
`cnitool` is a simple program that executes a CNI configuration. It will `cnitool` is a simple program that executes a CNI configuration. It will
add or remove an interface in an already-created network namespace. add or remove an interface in an already-created network namespace.
## Environment Variables
* `NETCONFPATH`: This environment variable needs to be set to a
directory. It defaults to `/etc/cni/net.d`. The `cnitool` searches
for CNI configuration files in this directory according to the following priorities:
1. Search files with the extension `*.conflist`, representing a list of plugin configurations.
2. If there are no `*.conflist` files in the directory, search files with the extension `*.conf` or `*.json`,
representing a single plugin configuration.
It loads all the CNI configuration files in
this directory and if it finds a CNI configuration with the `network
name` given to the cnitool it returns the corresponding CNI
configuration, else it returns `nil`.
* `CNI_PATH`: For a given CNI configuration `cnitool` will search for
the corresponding CNI plugin in this path.
## Example invocation ## Example invocation
First, install cnitool: First, install cnitool:
```bash ```
go get github.com/containernetworking/cni
go install github.com/containernetworking/cni/cnitool go install github.com/containernetworking/cni/cnitool
``` ```
Then, check out and build the plugins. All commands should be run from this directory. Then, check out and build the plugins. All commands should be run from this directory.
```
```bash
git clone https://github.com/containernetworking/plugins.git git clone https://github.com/containernetworking/plugins.git
cd plugins cd plugins
./build_linux.sh ./build_linux.sh
@ -39,39 +20,30 @@ cd plugins
``` ```
Create a network configuration Create a network configuration
```
```bash echo '{"cniVersion":"0.3.1","name":"myptp","type":"ptp","ipMasq":true,"ipam":{"type":"host-local","subnet":"172.16.29.0/24","routes":[{"dst":"0.0.0.0/0"}]}}' | sudo tee /etc/cni/net.d/10-myptp.conf
echo '{"cniVersion":"0.4.0","name":"myptp","type":"ptp","ipMasq":true,"ipam":{"type":"host-local","subnet":"172.16.29.0/24","routes":[{"dst":"0.0.0.0/0"}]}}' | sudo tee /etc/cni/net.d/10-myptp.conf
``` ```
Create a network namespace. This will be called `testing`: Create a network namespace. This will be called `testing`:
```bash ```
sudo ip netns add testing sudo ip netns add testing
``` ```
Add the container to the network: Add the container to the network:
```
```bash
sudo CNI_PATH=./bin cnitool add myptp /var/run/netns/testing sudo CNI_PATH=./bin cnitool add myptp /var/run/netns/testing
``` ```
Check whether the container's networking is as expected (ONLY for spec v0.4.0+):
```bash
sudo CNI_PATH=./bin cnitool check myptp /var/run/netns/testing
```
Test that it works: Test that it works:
```
```bash
sudo ip -n testing addr sudo ip -n testing addr
sudo ip netns exec testing ping -c 1 4.2.2.2 sudo ip netns exec testing ping -c 1 4.2.2.2
``` ```
And clean up: And clean up:
```
```bash
sudo CNI_PATH=./bin cnitool del myptp /var/run/netns/testing sudo CNI_PATH=./bin cnitool del myptp /var/run/netns/testing
sudo ip netns del testing sudo ip netns del testing
``` ```

View File

@ -26,7 +26,6 @@ import (
"github.com/containernetworking/cni/libcni" "github.com/containernetworking/cni/libcni"
) )
// Protocol parameters are passed to the plugins via OS environment variables.
const ( const (
EnvCNIPath = "CNI_PATH" EnvCNIPath = "CNI_PATH"
EnvNetDir = "NETCONFPATH" EnvNetDir = "NETCONFPATH"
@ -36,11 +35,9 @@ const (
DefaultNetDir = "/etc/cni/net.d" DefaultNetDir = "/etc/cni/net.d"
CmdAdd = "add" CmdAdd = "add"
CmdCheck = "check" CmdCheck = "check"
CmdDel = "del" CmdDel = "del"
CmdGC = "gc"
CmdStatus = "status"
) )
func parseArgs(args string) ([][2]string, error) { func parseArgs(args string) ([][2]string, error) {
@ -62,13 +59,14 @@ func parseArgs(args string) ([][2]string, error) {
func main() { func main() {
if len(os.Args) < 4 { if len(os.Args) < 4 {
usage() usage()
return
} }
netdir := os.Getenv(EnvNetDir) netdir := os.Getenv(EnvNetDir)
if netdir == "" { if netdir == "" {
netdir = DefaultNetDir netdir = DefaultNetDir
} }
netconf, err := libcni.LoadNetworkConf(netdir, os.Args[2]) netconf, err := libcni.LoadConfList(netdir, os.Args[2])
if err != nil { if err != nil {
exit(err) exit(err)
} }
@ -127,23 +125,16 @@ func main() {
exit(err) exit(err)
case CmdDel: case CmdDel:
exit(cninet.DelNetworkList(context.TODO(), netconf, rt)) exit(cninet.DelNetworkList(context.TODO(), netconf, rt))
case CmdGC:
// Currently just invoke GC without args, hence all network interface should be GC'ed!
exit(cninet.GCNetworkList(context.TODO(), netconf, nil))
case CmdStatus:
exit(cninet.GetStatusNetworkList(context.TODO(), netconf))
} }
} }
func usage() { func usage() {
exe := filepath.Base(os.Args[0]) exe := filepath.Base(os.Args[0])
fmt.Fprintf(os.Stderr, "%s: Add, check, remove, gc or status network interfaces from a network namespace\n", exe) fmt.Fprintf(os.Stderr, "%s: Add, check, or remove network interfaces from a network namespace\n", exe)
fmt.Fprintf(os.Stderr, " %s add <net> <netns>\n", exe) fmt.Fprintf(os.Stderr, " %s add <net> <netns>\n", exe)
fmt.Fprintf(os.Stderr, " %s check <net> <netns>\n", exe) fmt.Fprintf(os.Stderr, " %s check <net> <netns>\n", exe)
fmt.Fprintf(os.Stderr, " %s del <net> <netns>\n", exe) fmt.Fprintf(os.Stderr, " %s del <net> <netns>\n", exe)
fmt.Fprintf(os.Stderr, " %s gc <net> <netns>\n", exe)
fmt.Fprintf(os.Stderr, " %s status <net> <netns>\n", exe)
os.Exit(1) os.Exit(1)
} }

22
go.mod
View File

@ -1,22 +0,0 @@
module github.com/containernetworking/cni
go 1.21
require (
github.com/onsi/ginkgo/v2 v2.20.1
github.com/onsi/gomega v1.34.1
github.com/vishvananda/netns v0.0.4
)
require (
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/sys v0.23.0 // indirect
golang.org/x/text v0.17.0 // indirect
golang.org/x/tools v0.24.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

36
go.sum
View File

@ -1,36 +0,0 @@
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/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-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/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k=
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
github.com/onsi/ginkgo/v2 v2.20.1 h1:YlVIbqct+ZmnEph770q9Q7NVAz4wwIiVNahee6JyUzo=
github.com/onsi/ginkgo/v2 v2.20.1/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
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/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -14,37 +14,22 @@
package libcni 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 ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"sort"
"strings" "strings"
"github.com/containernetworking/cni/pkg/invoke" "github.com/containernetworking/cni/pkg/invoke"
"github.com/containernetworking/cni/pkg/types" "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" "github.com/containernetworking/cni/pkg/version"
) )
var ( var (
CacheDir = "/var/lib/cni" CacheDir = "/var/lib/cni"
// slightly awkward wording to preserve anyone matching on error strings
ErrorCheckNotSupp = fmt.Errorf("does not support the CHECK command")
)
const (
CNICacheV1 = "cniCacheV1"
) )
// A RuntimeConf holds the arguments to one invocation of a CNI plugin // A RuntimeConf holds the arguments to one invocation of a CNI plugin
@ -63,71 +48,40 @@ type RuntimeConf struct {
// to the plugin // to the plugin
CapabilityArgs map[string]interface{} CapabilityArgs map[string]interface{}
// DEPRECATED. Will be removed in a future release. // A cache directory in which to library data. Defaults to CacheDir
CacheDir string CacheDir string
} }
// Use PluginConfig instead of NetworkConfig, the NetworkConfig type NetworkConfig struct {
// backwards-compat alias will be removed in a future release. Network *types.NetConf
type NetworkConfig = PluginConfig
type PluginConfig struct {
Network *types.PluginConf
Bytes []byte Bytes []byte
} }
type NetworkConfigList struct { type NetworkConfigList struct {
Name string Name string
CNIVersion string CNIVersion string
DisableCheck bool DisableCheck bool
DisableGC bool Plugins []*NetworkConfig
LoadOnlyInlinedPlugins bool Bytes []byte
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 { type CNI interface {
AddNetworkList(ctx context.Context, net *NetworkConfigList, rt *RuntimeConf) (types.Result, error) AddNetworkList(ctx context.Context, net *NetworkConfigList, rt *RuntimeConf) (types.Result, error)
CheckNetworkList(ctx context.Context, net *NetworkConfigList, rt *RuntimeConf) error CheckNetworkList(ctx context.Context, net *NetworkConfigList, rt *RuntimeConf) error
DelNetworkList(ctx context.Context, net *NetworkConfigList, rt *RuntimeConf) error DelNetworkList(ctx context.Context, net *NetworkConfigList, rt *RuntimeConf) error
GetNetworkListCachedResult(net *NetworkConfigList, rt *RuntimeConf) (types.Result, error)
GetNetworkListCachedConfig(net *NetworkConfigList, rt *RuntimeConf) ([]byte, *RuntimeConf, error)
AddNetwork(ctx context.Context, net *PluginConfig, rt *RuntimeConf) (types.Result, error) AddNetwork(ctx context.Context, net *NetworkConfig, rt *RuntimeConf) (types.Result, error)
CheckNetwork(ctx context.Context, net *PluginConfig, rt *RuntimeConf) error CheckNetwork(ctx context.Context, net *NetworkConfig, rt *RuntimeConf) error
DelNetwork(ctx context.Context, net *PluginConfig, rt *RuntimeConf) error DelNetwork(ctx context.Context, net *NetworkConfig, rt *RuntimeConf) error
GetNetworkCachedResult(net *PluginConfig, rt *RuntimeConf) (types.Result, error) GetNetworkCachedResult(net *NetworkConfig, rt *RuntimeConf) (types.Result, error)
GetNetworkCachedConfig(net *PluginConfig, rt *RuntimeConf) ([]byte, *RuntimeConf, error)
ValidateNetworkList(ctx context.Context, net *NetworkConfigList) ([]string, error) ValidateNetworkList(ctx context.Context, net *NetworkConfigList) ([]string, error)
ValidateNetwork(ctx context.Context, net *PluginConfig) ([]string, error) ValidateNetwork(ctx context.Context, net *NetworkConfig) ([]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 { type CNIConfig struct {
Path []string Path []string
exec invoke.Exec exec invoke.Exec
cacheDir string
} }
// CNIConfig implements the CNI interface // CNIConfig implements the CNI interface
@ -137,22 +91,13 @@ var _ CNI = &CNIConfig{}
// in the given paths and use the given exec interface to run those plugins, // in the given paths and use the given exec interface to run those plugins,
// or if the exec interface is not given, will use a default exec handler. // or if the exec interface is not given, will use a default exec handler.
func NewCNIConfig(path []string, exec invoke.Exec) *CNIConfig { func NewCNIConfig(path []string, exec invoke.Exec) *CNIConfig {
return NewCNIConfigWithCacheDir(path, "", exec)
}
// NewCNIConfigWithCacheDir returns a new CNIConfig object that will search for plugins
// in the given paths use the given exec interface to run those plugins,
// or if the exec interface is not given, will use a default exec handler.
// The given cache directory will be used for temporary data storage when needed.
func NewCNIConfigWithCacheDir(path []string, cacheDir string, exec invoke.Exec) *CNIConfig {
return &CNIConfig{ return &CNIConfig{
Path: path, Path: path,
cacheDir: cacheDir, exec: exec,
exec: exec,
} }
} }
func buildOneConfig(name, cniVersion string, orig *PluginConfig, prevResult types.Result, rt *RuntimeConf) (*PluginConfig, error) { func buildOneConfig(name, cniVersion string, orig *NetworkConfig, prevResult types.Result, rt *RuntimeConf) (*NetworkConfig, error) {
var err error var err error
inject := map[string]interface{}{ inject := map[string]interface{}{
@ -169,11 +114,8 @@ func buildOneConfig(name, cniVersion string, orig *PluginConfig, prevResult type
if err != nil { if err != nil {
return nil, err return nil, err
} }
if rt != nil {
return injectRuntimeConfig(orig, rt)
}
return orig, nil return injectRuntimeConfig(orig, rt)
} }
// This function takes a libcni RuntimeConf structure and injects values into // This function takes a libcni RuntimeConf structure and injects values into
@ -188,7 +130,7 @@ func buildOneConfig(name, cniVersion string, orig *PluginConfig, prevResult type
// capabilities include "portMappings", and the CapabilityArgs map includes a // capabilities include "portMappings", and the CapabilityArgs map includes a
// "portMappings" key, that key and its value are added to the "runtimeConfig" // "portMappings" key, that key and its value are added to the "runtimeConfig"
// dictionary to be passed to the plugin's stdin. // dictionary to be passed to the plugin's stdin.
func injectRuntimeConfig(orig *PluginConfig, rt *RuntimeConf) (*PluginConfig, error) { func injectRuntimeConfig(orig *NetworkConfig, rt *RuntimeConf) (*NetworkConfig, error) {
var err error var err error
rc := make(map[string]interface{}) rc := make(map[string]interface{})
@ -222,132 +164,48 @@ func (c *CNIConfig) ensureExec() invoke.Exec {
return c.exec return c.exec
} }
type cachedInfo struct { func getResultCacheFilePath(netName string, rt *RuntimeConf) string {
Kind string `json:"kind"` cacheDir := rt.CacheDir
ContainerID string `json:"containerId"` if cacheDir == "" {
Config []byte `json:"config"` cacheDir = CacheDir
IfName string `json:"ifName"` }
NetworkName string `json:"networkName"` return filepath.Join(cacheDir, "results", fmt.Sprintf("%s-%s-%s", netName, rt.ContainerID, rt.IfName))
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"`
Result types.Result `json:"-"`
} }
// getCacheDir returns the cache directory in this order: func setCachedResult(result types.Result, netName string, rt *RuntimeConf) error {
// 1) global cacheDir from CNIConfig object
// 2) deprecated cacheDir from RuntimeConf object
// 3) fall back to default cache directory
func (c *CNIConfig) getCacheDir(rt *RuntimeConf) string {
if c.cacheDir != "" {
return c.cacheDir
}
if rt.CacheDir != "" {
return rt.CacheDir
}
return CacheDir
}
func (c *CNIConfig) getCacheFilePath(netName string, rt *RuntimeConf) (string, error) {
if netName == "" || rt.ContainerID == "" || rt.IfName == "" {
return "", fmt.Errorf("cache file path requires network name (%q), container ID (%q), and interface name (%q)", netName, rt.ContainerID, rt.IfName)
}
return filepath.Join(c.getCacheDir(rt), "results", fmt.Sprintf("%s-%s-%s", netName, rt.ContainerID, rt.IfName)), nil
}
func (c *CNIConfig) cacheAdd(result types.Result, config []byte, netName string, rt *RuntimeConf) error {
cached := cachedInfo{
Kind: CNICacheV1,
ContainerID: rt.ContainerID,
Config: config,
IfName: rt.IfName,
NetworkName: netName,
NetNS: rt.NetNS,
CniArgs: rt.Args,
CapabilityArgs: rt.CapabilityArgs,
}
// We need to get type.Result into cachedInfo as JSON map
// Marshal to []byte, then Unmarshal into cached.RawResult
data, err := json.Marshal(result) data, err := json.Marshal(result)
if err != nil { if err != nil {
return err return err
} }
fname := getResultCacheFilePath(netName, rt)
err = json.Unmarshal(data, &cached.RawResult) if err := os.MkdirAll(filepath.Dir(fname), 0700); err != nil {
if err != nil {
return err return err
} }
return ioutil.WriteFile(fname, data, 0600)
newBytes, err := json.Marshal(&cached)
if err != nil {
return err
}
fname, err := c.getCacheFilePath(netName, rt)
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(fname), 0o700); err != nil {
return err
}
return os.WriteFile(fname, newBytes, 0o600)
} }
func (c *CNIConfig) cacheDel(netName string, rt *RuntimeConf) error { func delCachedResult(netName string, rt *RuntimeConf) error {
fname, err := c.getCacheFilePath(netName, rt) fname := getResultCacheFilePath(netName, rt)
if err != nil {
// Ignore error
return nil
}
return os.Remove(fname) return os.Remove(fname)
} }
func (c *CNIConfig) getCachedConfig(netName string, rt *RuntimeConf) ([]byte, *RuntimeConf, error) { func getCachedResult(netName, cniVersion string, rt *RuntimeConf) (types.Result, error) {
var bytes []byte fname := getResultCacheFilePath(netName, rt)
data, err := ioutil.ReadFile(fname)
fname, err := c.getCacheFilePath(netName, rt)
if err != nil {
return nil, nil, err
}
bytes, err = os.ReadFile(fname)
if err != nil {
// Ignore read errors; the cached result may not exist on-disk
return nil, nil, nil
}
unmarshaled := cachedInfo{}
if err := json.Unmarshal(bytes, &unmarshaled); err != nil {
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)
}
newRt := *rt
if unmarshaled.CniArgs != nil {
newRt.Args = unmarshaled.CniArgs
}
newRt.CapabilityArgs = unmarshaled.CapabilityArgs
return unmarshaled.Config, &newRt, nil
}
func (c *CNIConfig) getLegacyCachedResult(netName, cniVersion string, rt *RuntimeConf) (types.Result, error) {
fname, err := c.getCacheFilePath(netName, rt)
if err != nil {
return nil, err
}
data, err := os.ReadFile(fname)
if err != nil { if err != nil {
// Ignore read errors; the cached result may not exist on-disk // Ignore read errors; the cached result may not exist on-disk
return nil, nil return nil, nil
} }
// Load the cached result // Read the version of the cached result
result, err := create.CreateFromBytes(data) 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)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -357,151 +215,30 @@ func (c *CNIConfig) getLegacyCachedResult(netName, cniVersion string, rt *Runtim
// should match the config version unless the config was changed // should match the config version unless the config was changed
// while the container was running. // while the container was running.
result, err = result.GetAsVersion(cniVersion) result, err = result.GetAsVersion(cniVersion)
if err != nil { if err != nil && resultCniVersion != cniVersion {
return nil, fmt.Errorf("failed to convert cached result to config version %q: %w", cniVersion, err) return nil, fmt.Errorf("failed to convert cached result version %q to config version %q: %v", resultCniVersion, cniVersion, err)
} }
return result, nil return result, err
}
func (c *CNIConfig) getCachedResult(netName, cniVersion string, rt *RuntimeConf) (types.Result, error) {
fname, err := c.getCacheFilePath(netName, rt)
if err != nil {
return nil, err
}
fdata, err := os.ReadFile(fname)
if err != nil {
// Ignore read errors; the cached result may not exist on-disk
return nil, nil
}
cachedInfo := cachedInfo{}
if err := json.Unmarshal(fdata, &cachedInfo); err != nil || cachedInfo.Kind != CNICacheV1 {
return c.getLegacyCachedResult(netName, cniVersion, rt)
}
newBytes, err := json.Marshal(&cachedInfo.RawResult)
if err != nil {
return nil, fmt.Errorf("failed to marshal cached network %q config: %w", netName, err)
}
// Load the cached result
result, err := create.CreateFromBytes(newBytes)
if err != nil {
return nil, err
}
// Convert to the config version to ensure plugins get prevResult
// in the same version as the config. The cached result version
// should match the config version unless the config was changed
// while the container was running.
result, err = result.GetAsVersion(cniVersion)
if err != nil {
return nil, fmt.Errorf("failed to convert cached result to config version %q: %w", cniVersion, err)
}
return result, nil
} }
// GetNetworkListCachedResult returns the cached Result of the previous // GetNetworkListCachedResult returns the cached Result of the previous
// AddNetworkList() operation for a network list, or an error. // previous AddNetworkList() operation for a network list, or an error.
func (c *CNIConfig) GetNetworkListCachedResult(list *NetworkConfigList, rt *RuntimeConf) (types.Result, error) { func (c *CNIConfig) GetNetworkListCachedResult(list *NetworkConfigList, rt *RuntimeConf) (types.Result, error) {
return c.getCachedResult(list.Name, list.CNIVersion, rt) return getCachedResult(list.Name, list.CNIVersion, rt)
} }
// GetNetworkCachedResult returns the cached Result of the previous // GetNetworkCachedResult returns the cached Result of the previous
// AddNetwork() operation for a network, or an error. // previous AddNetwork() operation for a network, or an error.
func (c *CNIConfig) GetNetworkCachedResult(net *PluginConfig, rt *RuntimeConf) (types.Result, error) { func (c *CNIConfig) GetNetworkCachedResult(net *NetworkConfig, rt *RuntimeConf) (types.Result, error) {
return c.getCachedResult(net.Network.Name, net.Network.CNIVersion, rt) return getCachedResult(net.Network.Name, net.Network.CNIVersion, rt)
} }
// GetNetworkListCachedConfig copies the input RuntimeConf to output func (c *CNIConfig) addNetwork(ctx context.Context, name, cniVersion string, net *NetworkConfig, prevResult types.Result, rt *RuntimeConf) (types.Result, error) {
// RuntimeConf with fields updated with info from the cached Config.
func (c *CNIConfig) GetNetworkListCachedConfig(list *NetworkConfigList, rt *RuntimeConf) ([]byte, *RuntimeConf, error) {
return c.getCachedConfig(list.Name, rt)
}
// GetNetworkCachedConfig copies the input RuntimeConf to output
// RuntimeConf with fields updated with info from the cached Config.
func (c *CNIConfig) GetNetworkCachedConfig(net *PluginConfig, rt *RuntimeConf) ([]byte, *RuntimeConf, error) {
return c.getCachedConfig(net.Network.Name, rt)
}
// 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() c.ensureExec()
pluginPath, err := c.exec.FindInPath(net.Network.Type, c.Path) pluginPath, err := c.exec.FindInPath(net.Network.Type, c.Path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err := utils.ValidateContainerID(rt.ContainerID); err != nil {
return nil, err
}
if err := utils.ValidateNetworkName(name); err != nil {
return nil, err
}
if err := utils.ValidateInterfaceName(rt.IfName); err != nil {
return nil, err
}
newConf, err := buildOneConfig(name, cniVersion, net, prevResult, rt) newConf, err := buildOneConfig(name, cniVersion, net, prevResult, rt)
if err != nil { if err != nil {
@ -518,18 +255,18 @@ func (c *CNIConfig) AddNetworkList(ctx context.Context, list *NetworkConfigList,
for _, net := range list.Plugins { for _, net := range list.Plugins {
result, err = c.addNetwork(ctx, list.Name, list.CNIVersion, net, result, rt) result, err = c.addNetwork(ctx, list.Name, list.CNIVersion, net, result, rt)
if err != nil { if err != nil {
return nil, fmt.Errorf("plugin %s failed (add): %w", pluginDescription(net.Network), err) return nil, err
} }
} }
if err = c.cacheAdd(result, list.Bytes, list.Name, rt); err != nil { if err = setCachedResult(result, list.Name, rt); err != nil {
return nil, fmt.Errorf("failed to set network %q cached result: %w", list.Name, err) return nil, fmt.Errorf("failed to set network %q cached result: %v", list.Name, err)
} }
return result, nil return result, nil
} }
func (c *CNIConfig) checkNetwork(ctx context.Context, name, cniVersion string, net *PluginConfig, prevResult types.Result, rt *RuntimeConf) error { func (c *CNIConfig) checkNetwork(ctx context.Context, name, cniVersion string, net *NetworkConfig, prevResult types.Result, rt *RuntimeConf) error {
c.ensureExec() c.ensureExec()
pluginPath, err := c.exec.FindInPath(net.Network.Type, c.Path) pluginPath, err := c.exec.FindInPath(net.Network.Type, c.Path)
if err != nil { if err != nil {
@ -550,16 +287,16 @@ func (c *CNIConfig) CheckNetworkList(ctx context.Context, list *NetworkConfigLis
if gtet, err := version.GreaterThanOrEqualTo(list.CNIVersion, "0.4.0"); err != nil { if gtet, err := version.GreaterThanOrEqualTo(list.CNIVersion, "0.4.0"); err != nil {
return err return err
} else if !gtet { } else if !gtet {
return fmt.Errorf("configuration version %q %w", list.CNIVersion, ErrorCheckNotSupp) return fmt.Errorf("configuration version %q does not support the CHECK command", list.CNIVersion)
} }
if list.DisableCheck { if list.DisableCheck {
return nil return nil
} }
cachedResult, err := c.getCachedResult(list.Name, list.CNIVersion, rt) cachedResult, err := getCachedResult(list.Name, list.CNIVersion, rt)
if err != nil { if err != nil {
return fmt.Errorf("failed to get network %q cached result: %w", list.Name, err) return fmt.Errorf("failed to get network %q cached result: %v", list.Name, err)
} }
for _, net := range list.Plugins { for _, net := range list.Plugins {
@ -571,7 +308,7 @@ func (c *CNIConfig) CheckNetworkList(ctx context.Context, list *NetworkConfigLis
return nil return nil
} }
func (c *CNIConfig) delNetwork(ctx context.Context, name, cniVersion string, net *PluginConfig, prevResult types.Result, rt *RuntimeConf) error { func (c *CNIConfig) delNetwork(ctx context.Context, name, cniVersion string, net *NetworkConfig, prevResult types.Result, rt *RuntimeConf) error {
c.ensureExec() c.ensureExec()
pluginPath, err := c.exec.FindInPath(net.Network.Type, c.Path) pluginPath, err := c.exec.FindInPath(net.Network.Type, c.Path)
if err != nil { if err != nil {
@ -594,85 +331,71 @@ func (c *CNIConfig) DelNetworkList(ctx context.Context, list *NetworkConfigList,
if gtet, err := version.GreaterThanOrEqualTo(list.CNIVersion, "0.4.0"); err != nil { if gtet, err := version.GreaterThanOrEqualTo(list.CNIVersion, "0.4.0"); err != nil {
return err return err
} else if gtet { } else if gtet {
if cachedResult, err = c.getCachedResult(list.Name, list.CNIVersion, rt); err != nil { cachedResult, err = getCachedResult(list.Name, list.CNIVersion, rt)
_ = c.cacheDel(list.Name, rt) if err != nil {
cachedResult = nil return fmt.Errorf("failed to get network %q cached result: %v", list.Name, err)
} }
} }
for i := len(list.Plugins) - 1; i >= 0; i-- { for i := len(list.Plugins) - 1; i >= 0; i-- {
net := list.Plugins[i] net := list.Plugins[i]
if err := c.delNetwork(ctx, list.Name, list.CNIVersion, net, cachedResult, rt); err != nil { if err := c.delNetwork(ctx, list.Name, list.CNIVersion, net, cachedResult, rt); err != nil {
return fmt.Errorf("plugin %s failed (delete): %w", pluginDescription(net.Network), err) return err
} }
} }
_ = delCachedResult(list.Name, rt)
_ = c.cacheDel(list.Name, rt)
return nil return nil
} }
func pluginDescription(net *types.PluginConf) string {
if net == nil {
return "<missing>"
}
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 // AddNetwork executes the plugin with the ADD command
func (c *CNIConfig) AddNetwork(ctx context.Context, net *PluginConfig, rt *RuntimeConf) (types.Result, error) { func (c *CNIConfig) AddNetwork(ctx context.Context, net *NetworkConfig, rt *RuntimeConf) (types.Result, error) {
result, err := c.addNetwork(ctx, net.Network.Name, net.Network.CNIVersion, net, nil, rt) result, err := c.addNetwork(ctx, net.Network.Name, net.Network.CNIVersion, net, nil, rt)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err = c.cacheAdd(result, net.Bytes, net.Network.Name, rt); err != nil { if err = setCachedResult(result, net.Network.Name, rt); err != nil {
return nil, fmt.Errorf("failed to set network %q cached result: %w", net.Network.Name, err) return nil, fmt.Errorf("failed to set network %q cached result: %v", net.Network.Name, err)
} }
return result, nil return result, nil
} }
// CheckNetwork executes the plugin with the CHECK command // CheckNetwork executes the plugin with the CHECK command
func (c *CNIConfig) CheckNetwork(ctx context.Context, net *PluginConfig, rt *RuntimeConf) error { func (c *CNIConfig) CheckNetwork(ctx context.Context, net *NetworkConfig, rt *RuntimeConf) error {
// CHECK was added in CNI spec version 0.4.0 and higher // 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 { if gtet, err := version.GreaterThanOrEqualTo(net.Network.CNIVersion, "0.4.0"); err != nil {
return err return err
} else if !gtet { } else if !gtet {
return fmt.Errorf("configuration version %q %w", net.Network.CNIVersion, ErrorCheckNotSupp) return fmt.Errorf("configuration version %q does not support the CHECK command", net.Network.CNIVersion)
} }
cachedResult, err := c.getCachedResult(net.Network.Name, net.Network.CNIVersion, rt) cachedResult, err := getCachedResult(net.Network.Name, net.Network.CNIVersion, rt)
if err != nil { if err != nil {
return fmt.Errorf("failed to get network %q cached result: %w", net.Network.Name, err) return fmt.Errorf("failed to get network %q cached result: %v", net.Network.Name, err)
} }
return c.checkNetwork(ctx, net.Network.Name, net.Network.CNIVersion, net, cachedResult, rt) return c.checkNetwork(ctx, net.Network.Name, net.Network.CNIVersion, net, cachedResult, rt)
} }
// DelNetwork executes the plugin with the DEL command // DelNetwork executes the plugin with the DEL command
func (c *CNIConfig) DelNetwork(ctx context.Context, net *PluginConfig, rt *RuntimeConf) error { func (c *CNIConfig) DelNetwork(ctx context.Context, net *NetworkConfig, rt *RuntimeConf) error {
var cachedResult types.Result var cachedResult types.Result
// Cached result on DEL was added in CNI spec version 0.4.0 and higher // Cached result on DEL was added in CNI spec version 0.4.0 and higher
if gtet, err := version.GreaterThanOrEqualTo(net.Network.CNIVersion, "0.4.0"); err != nil { if gtet, err := version.GreaterThanOrEqualTo(net.Network.CNIVersion, "0.4.0"); err != nil {
return err return err
} else if gtet { } else if gtet {
cachedResult, err = c.getCachedResult(net.Network.Name, net.Network.CNIVersion, rt) cachedResult, err = getCachedResult(net.Network.Name, net.Network.CNIVersion, rt)
if err != nil { if err != nil {
return fmt.Errorf("failed to get network %q cached result: %w", net.Network.Name, err) return fmt.Errorf("failed to get network %q cached result: %v", net.Network.Name, err)
} }
} }
if err := c.delNetwork(ctx, net.Network.Name, net.Network.CNIVersion, net, cachedResult, rt); err != nil { if err := c.delNetwork(ctx, net.Network.Name, net.Network.CNIVersion, net, cachedResult, rt); err != nil {
return err return err
} }
_ = c.cacheDel(net.Network.Name, rt) _ = delCachedResult(net.Network.Name, rt)
return nil return nil
} }
@ -716,7 +439,7 @@ func (c *CNIConfig) ValidateNetworkList(ctx context.Context, list *NetworkConfig
// ValidateNetwork checks that a configuration is reasonably valid. // ValidateNetwork checks that a configuration is reasonably valid.
// It uses the same logic as ValidateNetworkList) // It uses the same logic as ValidateNetworkList)
// Returns a list of capabilities // Returns a list of capabilities
func (c *CNIConfig) ValidateNetwork(ctx context.Context, net *PluginConfig) ([]string, error) { func (c *CNIConfig) ValidateNetwork(ctx context.Context, net *NetworkConfig) ([]string, error) {
caps := []string{} caps := []string{}
for c, ok := range net.Network.Capabilities { for c, ok := range net.Network.Capabilities {
if ok { if ok {
@ -731,14 +454,10 @@ func (c *CNIConfig) ValidateNetwork(ctx context.Context, net *PluginConfig) ([]s
// validatePlugin checks that an individual plugin's configuration is sane // validatePlugin checks that an individual plugin's configuration is sane
func (c *CNIConfig) validatePlugin(ctx context.Context, pluginName, expectedVersion string) error { func (c *CNIConfig) validatePlugin(ctx context.Context, pluginName, expectedVersion string) error {
c.ensureExec() pluginPath, err := invoke.FindInPath(pluginName, c.Path)
pluginPath, err := c.exec.FindInPath(pluginName, c.Path)
if err != nil { if err != nil {
return err return err
} }
if expectedVersion == "" {
expectedVersion = "0.1.0"
}
vi, err := invoke.GetVersionInfo(ctx, pluginPath, c.exec) vi, err := invoke.GetVersionInfo(ctx, pluginPath, c.exec)
if err != nil { if err != nil {
@ -764,129 +483,6 @@ func (c *CNIConfig) GetVersionInfo(ctx context.Context, pluginType string) (vers
return invoke.GetVersionInfo(ctx, pluginPath, c.exec) 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 { func (c *CNIConfig) args(action string, rt *RuntimeConf) *invoke.Args {
return &invoke.Args{ return &invoke.Args{

File diff suppressed because it is too large Load Diff

View File

@ -17,18 +17,18 @@ package libcni_test
import ( import (
"context" "context"
"fmt" "fmt"
"io/ioutil"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings" "strings"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gexec"
"github.com/containernetworking/cni/libcni" "github.com/containernetworking/cni/libcni"
"github.com/containernetworking/cni/pkg/version/legacy_examples" "github.com/containernetworking/cni/pkg/version/legacy_examples"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gexec"
) )
var _ = Describe("Backwards compatibility", func() { var _ = Describe("Backwards compatibility", func() {
@ -36,7 +36,7 @@ var _ = Describe("Backwards compatibility", func() {
BeforeEach(func() { BeforeEach(func() {
var err error var err error
cacheDirPath, err = os.MkdirTemp("", "cni_cachedir") cacheDirPath, err = ioutil.TempDir("", "cni_cachedir")
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
}) })
@ -57,18 +57,16 @@ var _ = Describe("Backwards compatibility", func() {
ContainerID: "some-container-id", ContainerID: "some-container-id",
NetNS: "/some/netns/path", NetNS: "/some/netns/path",
IfName: "eth0", IfName: "eth0",
CacheDir: cacheDirPath,
} }
cniConfig := libcni.NewCNIConfigWithCacheDir([]string{filepath.Dir(pluginPath)}, cacheDirPath, nil) cniConfig := libcni.NewCNIConfig([]string{filepath.Dir(pluginPath)}, nil)
result, err := cniConfig.AddNetwork(context.TODO(), netConf, runtimeConf) result, err := cniConfig.AddNetwork(context.TODO(), netConf, runtimeConf)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(result).To(Equal(legacy_examples.ExpectedResult)) Expect(result).To(Equal(legacy_examples.ExpectedResult))
err = cniConfig.DelNetwork(context.TODO(), netConf, runtimeConf)
Expect(err).NotTo(HaveOccurred())
Expect(os.RemoveAll(pluginPath)).To(Succeed()) Expect(os.RemoveAll(pluginPath)).To(Succeed())
}) })

View File

@ -16,16 +16,11 @@ package libcni
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"slices"
"sort" "sort"
"strings"
"github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/version"
) )
type NotFoundError struct { type NotFoundError struct {
@ -45,17 +40,10 @@ func (e NoConfigsFoundError) Error() string {
return fmt.Sprintf(`no net configurations found in %s`, e.Dir) return fmt.Sprintf(`no net configurations found in %s`, e.Dir)
} }
// This will not validate that the plugins actually belong to the netconfig by ensuring func ConfFromBytes(bytes []byte) (*NetworkConfig, error) {
// that they are loaded from a directory named after the networkName, relative to the network config. conf := &NetworkConfig{Bytes: bytes}
// if err := json.Unmarshal(bytes, &conf.Network); err != nil {
// Since here we are just accepting raw bytes, the caller is responsible for ensuring that the plugin return nil, fmt.Errorf("error parsing configuration: %s", err)
// 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 == "" { if conf.Network.Type == "" {
return nil, fmt.Errorf("error parsing configuration: missing 'type'") return nil, fmt.Errorf("error parsing configuration: missing 'type'")
@ -63,36 +51,18 @@ func NetworkPluginConfFromBytes(pluginConfBytes []byte) (*PluginConfig, error) {
return conf, nil return conf, nil
} }
// Given a path to a directory containing a network configuration, and the name of a network, func ConfFromFile(filename string) (*NetworkConfig, error) {
// loads all plugin definitions found at path `networkConfPath/networkName/*.conf` bytes, err := ioutil.ReadFile(filename)
func NetworkPluginConfsFromFiles(networkConfPath, networkName string) ([]*PluginConfig, error) {
var pConfs []*PluginConfig
pluginConfPath := filepath.Join(networkConfPath, networkName)
pluginConfFiles, err := ConfFiles(pluginConfPath, []string{".conf"})
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to read plugin config files in %s: %w", pluginConfPath, err) return nil, fmt.Errorf("error reading %s: %s", filename, 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 NetworkConfFromBytes(confBytes []byte) (*NetworkConfigList, error) { func ConfListFromBytes(bytes []byte) (*NetworkConfigList, error) {
rawList := make(map[string]interface{}) rawList := make(map[string]interface{})
if err := json.Unmarshal(confBytes, &rawList); err != nil { if err := json.Unmarshal(bytes, &rawList); err != nil {
return nil, fmt.Errorf("error parsing configuration list: %w", err) return nil, fmt.Errorf("error parsing configuration list: %s", err)
} }
rawName, ok := rawList["name"] rawName, ok := rawList["name"]
@ -113,115 +83,26 @@ func NetworkConfFromBytes(confBytes []byte) (*NetworkConfigList, error) {
} }
} }
rawVersions, ok := rawList["cniVersions"] disableCheck := false
if ok { if rawDisableCheck, ok := rawList["disableCheck"]; ok {
// Parse the current package CNI version disableCheck, ok = rawDisableCheck.(bool)
rvs, ok := rawVersions.([]interface{})
if !ok { if !ok {
return nil, fmt.Errorf("error parsing configuration list: invalid type for cniVersions: %T", rvs) return nil, fmt.Errorf("error parsing configuration list: invalid disableCheck type %T", rawDisableCheck)
} }
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 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{ list := &NetworkConfigList{
Name: name, Name: name,
DisableCheck: disableCheck, DisableCheck: disableCheck,
DisableGC: disableGC, CNIVersion: cniVersion,
LoadOnlyInlinedPlugins: loadOnlyInlinedPlugins, Bytes: bytes,
CNIVersion: cniVersion,
Bytes: confBytes,
} }
var plugins []interface{} var plugins []interface{}
plug, ok := rawList["plugins"] plug, ok := rawList["plugins"]
// We can have a `plugins` list key in the main conf, if !ok {
// We can also have `loadOnlyInlinedPlugins == true` return nil, fmt.Errorf("error parsing configuration list: no 'plugins' key")
//
// 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{}) plugins, ok = plug.([]interface{})
if !ok { if !ok {
return nil, fmt.Errorf("error parsing configuration list: invalid 'plugins' type %T", plug) return nil, fmt.Errorf("error parsing configuration list: invalid 'plugins' type %T", plug)
@ -233,76 +114,32 @@ func NetworkConfFromBytes(confBytes []byte) (*NetworkConfigList, error) {
for i, conf := range plugins { for i, conf := range plugins {
newBytes, err := json.Marshal(conf) newBytes, err := json.Marshal(conf)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to marshal plugin config %d: %w", i, err) return nil, fmt.Errorf("Failed to marshal plugin config %d: %v", i, err)
} }
netConf, err := ConfFromBytes(newBytes) netConf, err := ConfFromBytes(newBytes)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse plugin config %d: %w", i, err) return nil, fmt.Errorf("Failed to parse plugin config %d: %v", i, err)
} }
list.Plugins = append(list.Plugins, netConf) list.Plugins = append(list.Plugins, netConf)
} }
return list, nil return list, nil
} }
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: %w", filename, err)
}
return ConfFromBytes(bytes)
}
func ConfListFromBytes(bytes []byte) (*NetworkConfigList, error) {
return NetworkConfFromBytes(bytes)
}
func ConfListFromFile(filename string) (*NetworkConfigList, error) { func ConfListFromFile(filename string) (*NetworkConfigList, error) {
return NetworkConfFromFile(filename) bytes, err := ioutil.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("error reading %s: %s", filename, err)
}
return ConfListFromBytes(bytes)
} }
// 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) { func ConfFiles(dir string, extensions []string) ([]string, error) {
// In part, adapted from rkt/networking/podenv.go#listFiles // In part, adapted from rkt/networking/podenv.go#listFiles
files, err := os.ReadDir(dir) files, err := ioutil.ReadDir(dir)
switch { switch {
case err == nil: // break case err == nil: // break
case os.IsNotExist(err): 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 return nil, nil
default: default:
return nil, err return nil, err
@ -323,7 +160,6 @@ func ConfFiles(dir string, extensions []string) ([]string, error) {
return confFiles, nil return confFiles, nil
} }
// Deprecated: This file format is no longer supported, use NetworkConfXXX and NetworkPluginXXX functions
func LoadConf(dir, name string) (*NetworkConfig, error) { func LoadConf(dir, name string) (*NetworkConfig, error) {
files, err := ConfFiles(dir, []string{".conf", ".json"}) files, err := ConfFiles(dir, []string{".conf", ".json"})
switch { switch {
@ -347,15 +183,6 @@ func LoadConf(dir, name string) (*NetworkConfig, error) {
} }
func LoadConfList(dir, name string) (*NetworkConfigList, 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"}) files, err := ConfFiles(dir, []string{".conflist"})
if err != nil { if err != nil {
return nil, err return nil, err
@ -363,7 +190,7 @@ func LoadNetworkConf(dir, name string) (*NetworkConfigList, error) {
sort.Strings(files) sort.Strings(files)
for _, confFile := range files { for _, confFile := range files {
conf, err := NetworkConfFromFile(confFile) conf, err := ConfListFromFile(confFile)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -372,13 +199,12 @@ func LoadNetworkConf(dir, name string) (*NetworkConfigList, error) {
} }
} }
// Deprecated: Try and load a network configuration file (instead of list) // Try and load a network configuration file (instead of list)
// from the same name, then upconvert. // from the same name, then upconvert.
singleConf, err := LoadConf(dir, name) singleConf, err := LoadConf(dir, name)
if err != nil { if err != nil {
// A little extra logic so the error makes sense // A little extra logic so the error makes sense
var ncfErr NoConfigsFoundError if _, ok := err.(NoConfigsFoundError); len(files) != 0 && ok {
if len(files) != 0 && errors.As(err, &ncfErr) {
// Config lists found but no config files found // Config lists found but no config files found
return nil, NotFoundError{dir, name} return nil, NotFoundError{dir, name}
} }
@ -388,12 +214,11 @@ func LoadNetworkConf(dir, name string) (*NetworkConfigList, error) {
return ConfListFromConf(singleConf) return ConfListFromConf(singleConf)
} }
// InjectConf takes a PluginConfig and inserts additional values into it, ensuring the result is serializable. func InjectConf(original *NetworkConfig, newValues map[string]interface{}) (*NetworkConfig, error) {
func InjectConf(original *PluginConfig, newValues map[string]interface{}) (*PluginConfig, error) {
config := make(map[string]interface{}) config := make(map[string]interface{})
err := json.Unmarshal(original.Bytes, &config) err := json.Unmarshal(original.Bytes, &config)
if err != nil { if err != nil {
return nil, fmt.Errorf("unmarshal existing network bytes: %w", err) return nil, fmt.Errorf("unmarshal existing network bytes: %s", err)
} }
for key, value := range newValues { for key, value := range newValues {
@ -413,14 +238,12 @@ func InjectConf(original *PluginConfig, newValues map[string]interface{}) (*Plug
return nil, err return nil, err
} }
return NetworkPluginConfFromBytes(newBytes) return ConfFromBytes(newBytes)
} }
// ConfListFromConf "upconverts" a network config in to a NetworkConfigList, // ConfListFromConf "upconverts" a network config in to a NetworkConfigList,
// with the single network as the only entry in the list. // 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. // 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 // This may seem a bit strange, but it's to make the Bytes fields
// actually make sense. Otherwise, the generated json is littered with // actually make sense. Otherwise, the generated json is littered with

View File

@ -15,16 +15,14 @@
package libcni_test package libcni_test
import ( import (
"fmt" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"strings"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/containernetworking/cni/libcni" "github.com/containernetworking/cni/libcni"
"github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
) )
var _ = Describe("Loading configuration from disk", func() { var _ = Describe("Loading configuration from disk", func() {
@ -36,11 +34,11 @@ var _ = Describe("Loading configuration from disk", func() {
BeforeEach(func() { BeforeEach(func() {
var err error var err error
configDir, err = os.MkdirTemp("", "plugin-conf") configDir, err = ioutil.TempDir("", "plugin-conf")
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
pluginConfig = []byte(`{ "name": "some-plugin", "type": "foobar", "some-key": "some-value" }`) pluginConfig = []byte(`{ "name": "some-plugin", "type": "foobar", "some-key": "some-value" }`)
Expect(os.WriteFile(filepath.Join(configDir, "50-whatever.conf"), pluginConfig, 0o600)).To(Succeed()) Expect(ioutil.WriteFile(filepath.Join(configDir, "50-whatever.conf"), pluginConfig, 0600)).To(Succeed())
}) })
AfterEach(func() { AfterEach(func() {
@ -50,8 +48,8 @@ var _ = Describe("Loading configuration from disk", func() {
It("finds the network config file for the plugin of the given type", func() { It("finds the network config file for the plugin of the given type", func() {
netConfig, err := libcni.LoadConf(configDir, "some-plugin") netConfig, err := libcni.LoadConf(configDir, "some-plugin")
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(netConfig).To(Equal(&libcni.PluginConfig{ Expect(netConfig).To(Equal(&libcni.NetworkConfig{
Network: &types.PluginConf{ Network: &types.NetConf{
Name: "some-plugin", Name: "some-plugin",
Type: "foobar", Type: "foobar",
}, },
@ -74,13 +72,13 @@ var _ = Describe("Loading configuration from disk", func() {
BeforeEach(func() { BeforeEach(func() {
Expect(os.Remove(configDir + "/50-whatever.conf")).To(Succeed()) Expect(os.Remove(configDir + "/50-whatever.conf")).To(Succeed())
pluginConfig = []byte(`{ "name": "some-plugin", "some-key": "some-value", "type": "foobar" }`) pluginConfig = []byte(`{ "name": "some-plugin", "some-key": "some-value", "type": "foobar" }`)
Expect(os.WriteFile(filepath.Join(configDir, "50-whatever.json"), pluginConfig, 0o600)).To(Succeed()) Expect(ioutil.WriteFile(filepath.Join(configDir, "50-whatever.json"), pluginConfig, 0600)).To(Succeed())
}) })
It("finds the network config file for the plugin of the given type", func() { It("finds the network config file for the plugin of the given type", func() {
netConfig, err := libcni.LoadConf(configDir, "some-plugin") netConfig, err := libcni.LoadConf(configDir, "some-plugin")
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(netConfig).To(Equal(&libcni.PluginConfig{ Expect(netConfig).To(Equal(&libcni.NetworkConfig{
Network: &types.PluginConf{ Network: &types.NetConf{
Name: "some-plugin", Name: "some-plugin",
Type: "foobar", Type: "foobar",
}, },
@ -98,7 +96,7 @@ var _ = Describe("Loading configuration from disk", func() {
Context("when a config file is malformed", func() { Context("when a config file is malformed", func() {
BeforeEach(func() { BeforeEach(func() {
Expect(os.WriteFile(filepath.Join(configDir, "00-bad.conf"), []byte(`{`), 0o600)).To(Succeed()) Expect(ioutil.WriteFile(filepath.Join(configDir, "00-bad.conf"), []byte(`{`), 0600)).To(Succeed())
}) })
It("returns a useful error", func() { It("returns a useful error", func() {
@ -110,10 +108,10 @@ var _ = Describe("Loading configuration from disk", func() {
Context("when the config is in a nested subdir", func() { Context("when the config is in a nested subdir", func() {
BeforeEach(func() { BeforeEach(func() {
subdir := filepath.Join(configDir, "subdir1", "subdir2") subdir := filepath.Join(configDir, "subdir1", "subdir2")
Expect(os.MkdirAll(subdir, 0o700)).To(Succeed()) Expect(os.MkdirAll(subdir, 0700)).To(Succeed())
pluginConfig = []byte(`{ "name": "deep", "some-key": "some-value" }`) pluginConfig = []byte(`{ "name": "deep", "some-key": "some-value" }`)
Expect(os.WriteFile(filepath.Join(subdir, "90-deep.conf"), pluginConfig, 0o600)).To(Succeed()) Expect(ioutil.WriteFile(filepath.Join(subdir, "90-deep.conf"), pluginConfig, 0600)).To(Succeed())
}) })
It("will not find the config", func() { It("will not find the config", func() {
@ -128,11 +126,11 @@ var _ = Describe("Loading configuration from disk", func() {
BeforeEach(func() { BeforeEach(func() {
var err error var err error
configDir, err = os.MkdirTemp("", "plugin-conf") configDir, err = ioutil.TempDir("", "plugin-conf")
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
pluginConfig := []byte(`{ "name": "some-plugin", "type": "noop", "cniVersion": "0.3.1", "capabilities": { "portMappings": true, "somethingElse": true, "noCapability": false } }`) pluginConfig := []byte(`{ "name": "some-plugin", "type": "noop", "cniVersion": "0.3.1", "capabilities": { "portMappings": true, "somethingElse": true, "noCapability": false } }`)
Expect(os.WriteFile(filepath.Join(configDir, "50-whatever.conf"), pluginConfig, 0o600)).To(Succeed()) Expect(ioutil.WriteFile(filepath.Join(configDir, "50-whatever.conf"), pluginConfig, 0600)).To(Succeed())
}) })
AfterEach(func() { AfterEach(func() {
@ -162,12 +160,12 @@ var _ = Describe("Loading configuration from disk", func() {
var fileName, configDir string var fileName, configDir string
BeforeEach(func() { BeforeEach(func() {
var err error var err error
configDir, err = os.MkdirTemp("", "plugin-conf") configDir, err = ioutil.TempDir("", "plugin-conf")
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
fileName = filepath.Join(configDir, "50-whatever.conf") fileName = filepath.Join(configDir, "50-whatever.conf")
pluginConfig := []byte(`{ "name": "some-plugin", "some-key": "some-value" }`) pluginConfig := []byte(`{ "name": "some-plugin", "some-key": "some-value" }`)
Expect(os.WriteFile(fileName, pluginConfig, 0o600)).To(Succeed()) Expect(ioutil.WriteFile(fileName, pluginConfig, 0600)).To(Succeed())
}) })
AfterEach(func() { AfterEach(func() {
@ -181,7 +179,7 @@ var _ = Describe("Loading configuration from disk", func() {
}) })
}) })
Describe("NetworkPluginConfFromBytes", func() { Describe("ConfFromBytes", func() {
Context("when the config is missing 'type'", func() { Context("when the config is missing 'type'", func() {
It("returns a useful error", func() { It("returns a useful error", func() {
_, err := libcni.ConfFromBytes([]byte(`{ "name": "some-plugin", "some-key": "some-value" }`)) _, err := libcni.ConfFromBytes([]byte(`{ "name": "some-plugin", "some-key": "some-value" }`))
@ -190,7 +188,7 @@ var _ = Describe("Loading configuration from disk", func() {
}) })
}) })
Describe("LoadNetworkConf", func() { Describe("LoadConfList", func() {
var ( var (
configDir string configDir string
configList []byte configList []byte
@ -198,11 +196,11 @@ var _ = Describe("Loading configuration from disk", func() {
BeforeEach(func() { BeforeEach(func() {
var err error var err error
configDir, err = os.MkdirTemp("", "plugin-conf") configDir, err = ioutil.TempDir("", "plugin-conf")
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
configList = []byte(`{ configList = []byte(`{
"name": "some-network", "name": "some-list",
"cniVersion": "0.2.0", "cniVersion": "0.2.0",
"disableCheck": true, "disableCheck": true,
"plugins": [ "plugins": [
@ -220,7 +218,7 @@ var _ = Describe("Loading configuration from disk", func() {
} }
] ]
}`) }`)
Expect(os.WriteFile(filepath.Join(configDir, "50-whatever.conflist"), configList, 0o600)).To(Succeed()) Expect(ioutil.WriteFile(filepath.Join(configDir, "50-whatever.conflist"), configList, 0600)).To(Succeed())
}) })
AfterEach(func() { AfterEach(func() {
@ -228,23 +226,23 @@ var _ = Describe("Loading configuration from disk", func() {
}) })
It("finds the network config file for the plugin of the given type", func() { It("finds the network config file for the plugin of the given type", func() {
netConfigList, err := libcni.LoadNetworkConf(configDir, "some-network") netConfigList, err := libcni.LoadConfList(configDir, "some-list")
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(netConfigList).To(Equal(&libcni.NetworkConfigList{ Expect(netConfigList).To(Equal(&libcni.NetworkConfigList{
Name: "some-network", Name: "some-list",
CNIVersion: "0.2.0", CNIVersion: "0.2.0",
DisableCheck: true, DisableCheck: true,
Plugins: []*libcni.PluginConfig{ Plugins: []*libcni.NetworkConfig{
{ {
Network: &types.PluginConf{Type: "host-local"}, Network: &types.NetConf{Type: "host-local"},
Bytes: []byte(`{"subnet":"10.0.0.1/24","type":"host-local"}`), Bytes: []byte(`{"subnet":"10.0.0.1/24","type":"host-local"}`),
}, },
{ {
Network: &types.PluginConf{Type: "bridge"}, Network: &types.NetConf{Type: "bridge"},
Bytes: []byte(`{"mtu":1400,"type":"bridge"}`), Bytes: []byte(`{"mtu":1400,"type":"bridge"}`),
}, },
{ {
Network: &types.PluginConf{Type: "port-forwarding"}, Network: &types.NetConf{Type: "port-forwarding"},
Bytes: []byte(`{"ports":{"20.0.0.1:8080":"80"},"type":"port-forwarding"}`), Bytes: []byte(`{"ports":{"20.0.0.1:8080":"80"},"type":"port-forwarding"}`),
}, },
}, },
@ -255,25 +253,25 @@ var _ = Describe("Loading configuration from disk", func() {
Context("when there is a config file with the same name as the list", func() { Context("when there is a config file with the same name as the list", func() {
BeforeEach(func() { BeforeEach(func() {
configFile := []byte(`{ configFile := []byte(`{
"name": "some-network", "name": "some-list",
"cniVersion": "0.2.0", "cniVersion": "0.2.0",
"type": "bridge" "type": "bridge"
}`) }`)
Expect(os.WriteFile(filepath.Join(configDir, "49-whatever.conf"), configFile, 0o600)).To(Succeed()) Expect(ioutil.WriteFile(filepath.Join(configDir, "49-whatever.conf"), configFile, 0600)).To(Succeed())
}) })
It("Loads the config list first", func() { It("Loads the config list first", func() {
netConfigList, err := libcni.LoadNetworkConf(configDir, "some-network") netConfigList, err := libcni.LoadConfList(configDir, "some-list")
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(netConfigList.Plugins).To(HaveLen(3)) Expect(len(netConfigList.Plugins)).To(Equal(3))
}) })
It("falls back to the config file", func() { It("falls back to the config file", func() {
Expect(os.Remove(filepath.Join(configDir, "50-whatever.conflist"))).To(Succeed()) Expect(os.Remove(filepath.Join(configDir, "50-whatever.conflist"))).To(Succeed())
netConfigList, err := libcni.LoadNetworkConf(configDir, "some-network") netConfigList, err := libcni.LoadConfList(configDir, "some-list")
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(netConfigList.Plugins).To(HaveLen(1)) Expect(len(netConfigList.Plugins)).To(Equal(1))
Expect(netConfigList.Plugins[0].Network.Type).To(Equal("bridge")) Expect(netConfigList.Plugins[0].Network.Type).To(Equal("bridge"))
}) })
}) })
@ -284,25 +282,25 @@ var _ = Describe("Loading configuration from disk", func() {
}) })
It("returns a useful error", func() { It("returns a useful error", func() {
_, err := libcni.LoadNetworkConf(configDir, "some-network") _, err := libcni.LoadConfList(configDir, "some-plugin")
Expect(err).To(MatchError(libcni.NoConfigsFoundError{Dir: configDir})) Expect(err).To(MatchError(libcni.NoConfigsFoundError{Dir: configDir}))
}) })
}) })
Context("when there is no config for the desired network name", func() { Context("when there is no config for the desired plugin list", func() {
It("returns a useful error", func() { It("returns a useful error", func() {
_, err := libcni.LoadNetworkConf(configDir, "some-other-network") _, err := libcni.LoadConfList(configDir, "some-other-plugin")
Expect(err).To(MatchError(libcni.NotFoundError{Dir: configDir, Name: "some-other-network"})) Expect(err).To(MatchError(libcni.NotFoundError{Dir: configDir, Name: "some-other-plugin"}))
}) })
}) })
Context("when a config file is malformed", func() { Context("when a config file is malformed", func() {
BeforeEach(func() { BeforeEach(func() {
Expect(os.WriteFile(filepath.Join(configDir, "00-bad.conflist"), []byte(`{`), 0o600)).To(Succeed()) Expect(ioutil.WriteFile(filepath.Join(configDir, "00-bad.conflist"), []byte(`{`), 0600)).To(Succeed())
}) })
It("returns a useful error", func() { It("returns a useful error", func() {
_, err := libcni.LoadNetworkConf(configDir, "some-plugin") _, err := libcni.LoadConfList(configDir, "some-plugin")
Expect(err).To(MatchError(`error parsing configuration list: unexpected end of JSON input`)) Expect(err).To(MatchError(`error parsing configuration list: unexpected end of JSON input`))
}) })
}) })
@ -310,7 +308,7 @@ var _ = Describe("Loading configuration from disk", func() {
Context("when the config is in a nested subdir", func() { Context("when the config is in a nested subdir", func() {
BeforeEach(func() { BeforeEach(func() {
subdir := filepath.Join(configDir, "subdir1", "subdir2") subdir := filepath.Join(configDir, "subdir1", "subdir2")
Expect(os.MkdirAll(subdir, 0o700)).To(Succeed()) Expect(os.MkdirAll(subdir, 0700)).To(Succeed())
configList = []byte(`{ configList = []byte(`{
"name": "deep", "name": "deep",
@ -322,309 +320,37 @@ var _ = Describe("Loading configuration from disk", func() {
}, },
] ]
}`) }`)
Expect(os.WriteFile(filepath.Join(subdir, "90-deep.conflist"), configList, 0o600)).To(Succeed()) Expect(ioutil.WriteFile(filepath.Join(subdir, "90-deep.conflist"), configList, 0600)).To(Succeed())
}) })
It("will not find the config", func() { It("will not find the config", func() {
_, err := libcni.LoadNetworkConf(configDir, "deep") _, err := libcni.LoadConfList(configDir, "deep")
Expect(err).To(MatchError(HavePrefix("no net configuration with name"))) Expect(err).To(MatchError(HavePrefix("no net configuration with name")))
}) })
}) })
Context("when disableCheck is a string not a boolean", func() {
It("will read a 'true' value and convert to boolean", func() {
configList = []byte(`{
"name": "some-network",
"cniVersion": "0.4.0",
"disableCheck": "true",
"plugins": [
{
"type": "host-local",
"subnet": "10.0.0.1/24"
}
]
}`)
Expect(os.WriteFile(filepath.Join(configDir, "50-whatever.conflist"), configList, 0o600)).To(Succeed())
netConfigList, err := libcni.LoadNetworkConf(configDir, "some-network")
Expect(err).NotTo(HaveOccurred())
Expect(netConfigList.DisableCheck).To(BeTrue())
})
It("will read a 'false' value and convert to boolean", func() {
configList = []byte(`{
"name": "some-network",
"cniVersion": "0.4.0",
"disableCheck": "false",
"plugins": [
{
"type": "host-local",
"subnet": "10.0.0.1/24"
}
]
}`)
Expect(os.WriteFile(filepath.Join(configDir, "50-whatever.conflist"), configList, 0o600)).To(Succeed())
netConfigList, err := libcni.LoadNetworkConf(configDir, "some-network")
Expect(err).NotTo(HaveOccurred())
Expect(netConfigList.DisableCheck).To(BeFalse())
})
It("will return an error on an unrecognized value", func() {
const badValue string = "adsfasdfasf"
configList = []byte(fmt.Sprintf(`{
"name": "some-network",
"cniVersion": "0.4.0",
"disableCheck": "%s",
"plugins": [
{
"type": "host-local",
"subnet": "10.0.0.1/24"
}
]
}`, badValue))
Expect(os.WriteFile(filepath.Join(configDir, "50-whatever.conflist"), configList, 0o600)).To(Succeed())
_, err := libcni.LoadNetworkConf(configDir, "some-network")
Expect(err).To(MatchError(fmt.Sprintf("error parsing configuration list: invalid value \"%s\" for disableCheck", badValue)))
})
})
Context("for loadOnlyInlinedPlugins", func() {
It("the value will be parsed", func() {
configList = []byte(`{
"name": "some-network",
"cniVersion": "0.4.0",
"loadOnlyInlinedPlugins": true,
"plugins": [
{
"type": "host-local",
"subnet": "10.0.0.1/24"
}
]
}`)
Expect(os.WriteFile(filepath.Join(configDir, "50-whatever.conflist"), configList, 0o600)).To(Succeed())
dirPluginConf := []byte(`{
"type": "bro-check-out-my-plugin",
"subnet": "10.0.0.1/24"
}`)
subDir := filepath.Join(configDir, "some-network")
Expect(os.MkdirAll(subDir, 0o700)).To(Succeed())
Expect(os.WriteFile(filepath.Join(subDir, "funky-second-plugin.conf"), dirPluginConf, 0o600)).To(Succeed())
netConfigList, err := libcni.LoadNetworkConf(configDir, "some-network")
Expect(err).NotTo(HaveOccurred())
Expect(netConfigList.LoadOnlyInlinedPlugins).To(BeTrue())
})
It("the value will be false if not in config", func() {
configList = []byte(`{
"name": "some-network",
"cniVersion": "0.4.0",
"plugins": [
{
"type": "host-local",
"subnet": "10.0.0.1/24"
}
]
}`)
Expect(os.WriteFile(filepath.Join(configDir, "50-whatever.conflist"), configList, 0o600)).To(Succeed())
netConfigList, err := libcni.LoadNetworkConf(configDir, "some-network")
Expect(err).NotTo(HaveOccurred())
Expect(netConfigList.LoadOnlyInlinedPlugins).To(BeFalse())
})
It("will return an error on an unrecognized value", func() {
const badValue string = "sphagnum"
configList = []byte(fmt.Sprintf(`{
"name": "some-network",
"cniVersion": "0.4.0",
"loadOnlyInlinedPlugins": "%s",
"plugins": [
{
"type": "host-local",
"subnet": "10.0.0.1/24"
}
]
}`, badValue))
Expect(os.WriteFile(filepath.Join(configDir, "50-whatever.conflist"), configList, 0o600)).To(Succeed())
_, err := libcni.LoadNetworkConf(configDir, "some-network")
Expect(err).To(MatchError(fmt.Sprintf(`error parsing configuration list: invalid value "%s" for loadOnlyInlinedPlugins`, badValue)))
})
It("will return an error if `plugins` is missing and `loadOnlyInlinedPlugins` is `true`", func() {
configList = []byte(`{
"name": "some-network",
"cniVersion": "0.4.0",
"loadOnlyInlinedPlugins": true
}`)
Expect(os.WriteFile(filepath.Join(configDir, "50-whatever.conflist"), configList, 0o600)).To(Succeed())
_, err := libcni.LoadNetworkConf(configDir, "some-network")
Expect(err).To(MatchError("error parsing configuration list: `loadOnlyInlinedPlugins` is true, and no 'plugins' key"))
})
It("will return no error if `plugins` is missing and `loadOnlyInlinedPlugins` is false", func() {
configList = []byte(`{
"name": "some-network",
"cniVersion": "0.4.0",
"loadOnlyInlinedPlugins": false
}`)
Expect(os.WriteFile(filepath.Join(configDir, "50-whatever.conflist"), configList, 0o600)).To(Succeed())
dirPluginConf := []byte(`{
"type": "bro-check-out-my-plugin",
"subnet": "10.0.0.1/24"
}`)
subDir := filepath.Join(configDir, "some-network")
Expect(os.MkdirAll(subDir, 0o700)).To(Succeed())
Expect(os.WriteFile(filepath.Join(subDir, "funky-second-plugin.conf"), dirPluginConf, 0o600)).To(Succeed())
netConfigList, err := libcni.LoadNetworkConf(configDir, "some-network")
Expect(err).NotTo(HaveOccurred())
Expect(netConfigList.LoadOnlyInlinedPlugins).To(BeFalse())
Expect(netConfigList.Plugins).To(HaveLen(1))
})
It("will return error if `loadOnlyInlinedPlugins` is implicitly false + no conf plugin is defined, but no plugins subfolder with network name exists", func() {
configList = []byte(`{
"name": "some-network",
"cniVersion": "0.4.0"
}`)
Expect(os.WriteFile(filepath.Join(configDir, "50-whatever.conflist"), configList, 0o600)).To(Succeed())
_, err := libcni.LoadNetworkConf(configDir, "some-network")
Expect(err).To(MatchError("no plugin configs found"))
})
It("will return NO error if `loadOnlyInlinedPlugins` is implicitly false + at least 1 conf plugin is defined, but no plugins subfolder with network name exists", func() {
configList = []byte(`{
"name": "some-network",
"cniVersion": "0.4.0",
"plugins": [
{
"type": "host-local",
"subnet": "10.0.0.1/24"
}
]
}`)
Expect(os.WriteFile(filepath.Join(configDir, "50-whatever.conflist"), configList, 0o600)).To(Succeed())
_, err := libcni.LoadNetworkConf(configDir, "some-network")
Expect(err).NotTo(HaveOccurred())
})
It("will return NO error if `loadOnlyInlinedPlugins` is implicitly false + at least 1 conf plugin is defined and network name subfolder exists, but is empty/unreadable", func() {
configList = []byte(`{
"name": "some-network",
"cniVersion": "0.4.0",
"plugins": [
{
"type": "host-local",
"subnet": "10.0.0.1/24"
}
]
}`)
Expect(os.WriteFile(filepath.Join(configDir, "50-whatever.conflist"), configList, 0o600)).To(Succeed())
subDir := filepath.Join(configDir, "some-network")
Expect(os.MkdirAll(subDir, 0o700)).To(Succeed())
_, err := libcni.LoadNetworkConf(configDir, "some-network")
Expect(err).NotTo(HaveOccurred())
})
It("will merge loaded and inlined plugin lists if both `plugins` is set and `loadOnlyInlinedPlugins` is false", func() {
configList = []byte(`{
"name": "some-network",
"cniVersion": "0.4.0",
"plugins": [
{
"type": "host-local",
"subnet": "10.0.0.1/24"
}
]
}`)
dirPluginConf := []byte(`{
"type": "bro-check-out-my-plugin",
"subnet": "10.0.0.1/24"
}`)
Expect(os.WriteFile(filepath.Join(configDir, "50-whatever.conflist"), configList, 0o600)).To(Succeed())
subDir := filepath.Join(configDir, "some-network")
Expect(os.MkdirAll(subDir, 0o700)).To(Succeed())
Expect(os.WriteFile(filepath.Join(subDir, "funky-second-plugin.conf"), dirPluginConf, 0o600)).To(Succeed())
netConfigList, err := libcni.LoadNetworkConf(configDir, "some-network")
Expect(err).NotTo(HaveOccurred())
Expect(netConfigList.LoadOnlyInlinedPlugins).To(BeFalse())
Expect(netConfigList.Plugins).To(HaveLen(2))
})
It("will ignore loaded plugins if `plugins` is set and `loadOnlyInlinedPlugins` is true", func() {
configList = []byte(`{
"name": "some-network",
"cniVersion": "0.4.0",
"loadOnlyInlinedPlugins": true,
"plugins": [
{
"type": "host-local",
"subnet": "10.0.0.1/24"
}
]
}`)
dirPluginConf := []byte(`{
"type": "bro-check-out-my-plugin",
"subnet": "10.0.0.1/24"
}`)
Expect(os.WriteFile(filepath.Join(configDir, "50-whatever.conflist"), configList, 0o600)).To(Succeed())
subDir := filepath.Join(configDir, "some-network")
Expect(os.MkdirAll(subDir, 0o700)).To(Succeed())
Expect(os.WriteFile(filepath.Join(subDir, "funky-second-plugin.conf"), dirPluginConf, 0o600)).To(Succeed())
netConfigList, err := libcni.LoadNetworkConf(configDir, "some-network")
Expect(err).NotTo(HaveOccurred())
Expect(netConfigList.LoadOnlyInlinedPlugins).To(BeTrue())
Expect(netConfigList.Plugins).To(HaveLen(1))
Expect(netConfigList.Plugins[0].Network.Type).To(Equal("host-local"))
})
})
}) })
Describe("NetworkConfFromFile", func() { Describe("ConfListFromFile", func() {
Context("when the file cannot be opened", func() { Context("when the file cannot be opened", func() {
It("returns a useful error", func() { It("returns a useful error", func() {
_, err := libcni.NetworkConfFromFile("/tmp/nope/not-here") _, err := libcni.ConfListFromFile("/tmp/nope/not-here")
Expect(err).To(MatchError(HavePrefix(`error reading /tmp/nope/not-here: open /tmp/nope/not-here`))) Expect(err).To(MatchError(HavePrefix(`error reading /tmp/nope/not-here: open /tmp/nope/not-here`)))
}) })
}) })
}) })
Describe("InjectConf", func() { Describe("InjectConf", func() {
var testNetConfig *libcni.PluginConfig var testNetConfig *libcni.NetworkConfig
BeforeEach(func() { BeforeEach(func() {
testNetConfig = &libcni.PluginConfig{ testNetConfig = &libcni.NetworkConfig{Network: &types.NetConf{Name: "some-plugin", Type: "foobar"},
Network: &types.PluginConf{Name: "some-plugin", Type: "foobar"}, Bytes: []byte(`{ "name": "some-plugin", "type": "foobar" }`)}
Bytes: []byte(`{ "name": "some-plugin", "type": "foobar" }`),
}
}) })
Context("when function parameters are incorrect", func() { Context("when function parameters are incorrect", func() {
It("returns unmarshal error", func() { It("returns unmarshal error", func() {
conf := &libcni.PluginConfig{ conf := &libcni.NetworkConfig{Network: &types.NetConf{Name: "some-plugin"},
Network: &types.PluginConf{Name: "some-plugin"}, Bytes: []byte(`{ cc cc cc}`)}
Bytes: []byte(`{ cc cc cc}`),
}
_, err := libcni.InjectConf(conf, map[string]interface{}{"": nil}) _, err := libcni.InjectConf(conf, map[string]interface{}{"": nil})
Expect(err).To(MatchError(HavePrefix(`unmarshal existing network bytes`))) Expect(err).To(MatchError(HavePrefix(`unmarshal existing network bytes`)))
@ -647,8 +373,8 @@ var _ = Describe("Loading configuration from disk", func() {
resultConfig, err := libcni.InjectConf(testNetConfig, map[string]interface{}{"test": "test"}) resultConfig, err := libcni.InjectConf(testNetConfig, map[string]interface{}{"test": "test"})
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(resultConfig).To(Equal(&libcni.PluginConfig{ Expect(resultConfig).To(Equal(&libcni.NetworkConfig{
Network: &types.PluginConf{ Network: &types.NetConf{
Name: "some-plugin", Name: "some-plugin",
Type: "foobar", Type: "foobar",
}, },
@ -665,8 +391,8 @@ var _ = Describe("Loading configuration from disk", func() {
resultConfig, err = libcni.InjectConf(resultConfig, map[string]interface{}{"test": "changedValue"}) resultConfig, err = libcni.InjectConf(resultConfig, map[string]interface{}{"test": "changedValue"})
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(resultConfig).To(Equal(&libcni.PluginConfig{ Expect(resultConfig).To(Equal(&libcni.NetworkConfig{
Network: &types.PluginConf{ Network: &types.NetConf{
Name: "some-plugin", Name: "some-plugin",
Type: "foobar", Type: "foobar",
}, },
@ -683,8 +409,8 @@ var _ = Describe("Loading configuration from disk", func() {
resultConfig, err = libcni.InjectConf(resultConfig, map[string]interface{}{"test": "test"}) resultConfig, err = libcni.InjectConf(resultConfig, map[string]interface{}{"test": "test"})
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(resultConfig).To(Equal(&libcni.PluginConfig{ Expect(resultConfig).To(Equal(&libcni.NetworkConfig{
Network: &types.PluginConf{ Network: &types.NetConf{
Name: "some-plugin", Name: "some-plugin",
Type: "foobar", Type: "foobar",
}, },
@ -693,6 +419,7 @@ var _ = Describe("Loading configuration from disk", func() {
}) })
It("adds sub-fields of NetworkConfig.Network to the config", func() { It("adds sub-fields of NetworkConfig.Network to the config", func() {
expectedPluginConfig := []byte(`{"dns":{"domain":"local","nameservers":["server1","server2"]},"name":"some-plugin","type":"bridge"}`) expectedPluginConfig := []byte(`{"dns":{"domain":"local","nameservers":["server1","server2"]},"name":"some-plugin","type":"bridge"}`)
servers := []string{"server1", "server2"} servers := []string{"server1", "server2"}
newDNS := &types.DNS{Nameservers: servers, Domain: "local"} newDNS := &types.DNS{Nameservers: servers, Domain: "local"}
@ -705,8 +432,8 @@ var _ = Describe("Loading configuration from disk", func() {
resultConfig, err = libcni.InjectConf(resultConfig, map[string]interface{}{"type": "bridge"}) resultConfig, err = libcni.InjectConf(resultConfig, map[string]interface{}{"type": "bridge"})
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(resultConfig).To(Equal(&libcni.PluginConfig{ Expect(resultConfig).To(Equal(&libcni.NetworkConfig{
Network: &types.PluginConf{Name: "some-plugin", Type: "bridge", DNS: types.DNS{Nameservers: servers, Domain: "local"}}, Network: &types.NetConf{Name: "some-plugin", Type: "bridge", DNS: types.DNS{Nameservers: servers, Domain: "local"}},
Bytes: expectedPluginConfig, Bytes: expectedPluginConfig,
})) }))
}) })
@ -714,55 +441,8 @@ var _ = Describe("Loading configuration from disk", func() {
}) })
}) })
var _ = Describe("NetworkConfFromBytes", func() {
Describe("Version selection", func() {
makeConfig := func(versions ...string) []byte {
// ugly fake json encoding, but whatever
vs := []string{}
for _, v := range versions {
vs = append(vs, fmt.Sprintf(`"%s"`, v))
}
return []byte(fmt.Sprintf(`{"name": "test", "cniVersions": [%s], "plugins": [{"type": "foo"}]}`, strings.Join(vs, ",")))
}
It("correctly selects the maximum version", func() {
conf, err := libcni.NetworkConfFromBytes(makeConfig("1.1.0", "0.4.0", "1.0.0"))
Expect(err).NotTo(HaveOccurred())
Expect(conf.CNIVersion).To(Equal("1.1.0"))
})
It("selects the highest version supported by libcni", func() {
conf, err := libcni.NetworkConfFromBytes(makeConfig("99.0.0", "1.1.0", "0.4.0", "1.0.0"))
Expect(err).NotTo(HaveOccurred())
Expect(conf.CNIVersion).To(Equal("1.1.0"))
})
It("fails when invalid versions are specified", func() {
_, err := libcni.NetworkConfFromBytes(makeConfig("1.1.0", "0.4.0", "1.0.f"))
Expect(err).To(HaveOccurred())
})
It("falls back to cniVersion", func() {
conf, err := libcni.NetworkConfFromBytes([]byte(`{"name": "test", "cniVersion": "1.2.3", "plugins": [{"type": "foo"}]}`))
Expect(err).NotTo(HaveOccurred())
Expect(conf.CNIVersion).To(Equal("1.2.3"))
})
It("merges cniVersions and cniVersion", func() {
conf, err := libcni.NetworkConfFromBytes([]byte(`{"name": "test", "cniVersion": "1.0.0", "cniVersions": ["0.1.0", "0.4.0"], "plugins": [{"type": "foo"}]}`))
Expect(err).NotTo(HaveOccurred())
Expect(conf.CNIVersion).To(Equal("1.0.0"))
})
It("handles an empty cniVersions array", func() {
conf, err := libcni.NetworkConfFromBytes([]byte(`{"name": "test", "cniVersions": [], "plugins": [{"type": "foo"}]}`))
Expect(err).NotTo(HaveOccurred())
Expect(conf.CNIVersion).To(Equal(""))
})
})
})
var _ = Describe("ConfListFromConf", func() { var _ = Describe("ConfListFromConf", func() {
var testNetConfig *libcni.PluginConfig var testNetConfig *libcni.NetworkConfig
BeforeEach(func() { BeforeEach(func() {
pb := []byte(`{"name":"some-plugin","cniVersion":"0.3.1", "type":"foobar"}`) pb := []byte(`{"name":"some-plugin","cniVersion":"0.3.1", "type":"foobar"}`)
@ -784,15 +464,16 @@ var _ = Describe("ConfListFromConf", func() {
Expect(ncl).To(Equal(&libcni.NetworkConfigList{ Expect(ncl).To(Equal(&libcni.NetworkConfigList{
Name: "some-plugin", Name: "some-plugin",
CNIVersion: "0.3.1", CNIVersion: "0.3.1",
Plugins: []*libcni.PluginConfig{testNetConfig}, Plugins: []*libcni.NetworkConfig{testNetConfig},
})) }))
// Test that the json unmarshals to the same data //Test that the json unmarshals to the same data
ncl2, err := libcni.NetworkConfFromBytes(bytes) ncl2, err := libcni.ConfListFromBytes(bytes)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
ncl2.Bytes = nil ncl2.Bytes = nil
ncl2.Plugins[0].Bytes = nil ncl2.Plugins[0].Bytes = nil
Expect(ncl2).To(Equal(ncl)) Expect(ncl2).To(Equal(ncl))
}) })
}) })

View File

@ -17,11 +17,12 @@ package libcni_test
import ( import (
"encoding/json" "encoding/json"
"path/filepath" "path/filepath"
"testing"
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"github.com/onsi/gomega/gexec" "github.com/onsi/gomega/gexec"
"testing"
) )
func TestLibcni(t *testing.T) { func TestLibcni(t *testing.T) {
@ -34,12 +35,11 @@ var pluginPackages = map[string]string{
"sleep": "github.com/containernetworking/cni/plugins/test/sleep", "sleep": "github.com/containernetworking/cni/plugins/test/sleep",
} }
var ( var pluginPaths map[string]string
pluginPaths map[string]string var pluginDirs []string // array of plugin dirs
pluginDirs []string // array of plugin dirs
)
var _ = SynchronizedBeforeSuite(func() []byte { var _ = SynchronizedBeforeSuite(func() []byte {
paths := map[string]string{} paths := map[string]string{}
for name, packagePath := range pluginPackages { for name, packagePath := range pluginPackages {
execPath, err := gexec.Build(packagePath) execPath, err := gexec.Build(packagePath)

View File

@ -1,6 +0,0 @@
#!/bin/bash
set -e
GOLANGCI_LINT_VERSION="v1.57.1"
go install "github.com/golangci/golangci-lint/cmd/golangci-lint@${GOLANGCI_LINT_VERSION}"

View File

@ -1,14 +0,0 @@
.PHONY: lint
lint: golangci/install golangci/lint
.PHONY: golangci/install
golangci/install:
./mk/dependencies/golangci.sh
.PHONY: golangci/lint
golangci/lint:
golangci-lint run --verbose
.PHONY: golangci/fix
golangci/fix:
golangci-lint run --verbose --fix

View File

@ -15,7 +15,6 @@
package invoke package invoke
import ( import (
"fmt"
"os" "os"
"strings" "strings"
) )
@ -23,8 +22,6 @@ import (
type CNIArgs interface { type CNIArgs interface {
// For use with os/exec; i.e., return nil to inherit the // For use with os/exec; i.e., return nil to inherit the
// environment from this process // environment from this process
// For use in delegation; inherit the environment from this
// process and allow overrides
AsEnv() []string AsEnv() []string
} }
@ -32,7 +29,7 @@ type inherited struct{}
var inheritArgsFromEnv inherited var inheritArgsFromEnv inherited
func (*inherited) AsEnv() []string { func (_ *inherited) AsEnv() []string {
return nil return nil
} }
@ -60,17 +57,17 @@ func (args *Args) AsEnv() []string {
pluginArgsStr = stringify(args.PluginArgs) pluginArgsStr = stringify(args.PluginArgs)
} }
// Duplicated values which come first will be overridden, so we must put the // Ensure that the custom values are first, so any value present in
// custom values in the end to avoid being overridden by the process environments. // the process environment won't override them.
env = append(env, env = append([]string{
"CNI_COMMAND="+args.Command, "CNI_COMMAND=" + args.Command,
"CNI_CONTAINERID="+args.ContainerID, "CNI_CONTAINERID=" + args.ContainerID,
"CNI_NETNS="+args.NetNS, "CNI_NETNS=" + args.NetNS,
"CNI_ARGS="+pluginArgsStr, "CNI_ARGS=" + pluginArgsStr,
"CNI_IFNAME="+args.IfName, "CNI_IFNAME=" + args.IfName,
"CNI_PATH="+args.Path, "CNI_PATH=" + args.Path,
) }, env...)
return dedupEnv(env) return env
} }
// taken from rkt/networking/net_plugin.go // taken from rkt/networking/net_plugin.go
@ -83,46 +80,3 @@ func stringify(pluginArgs [][2]string) string {
return strings.Join(entries, ";") return strings.Join(entries, ";")
} }
// DelegateArgs implements the CNIArgs interface
// used for delegation to inherit from environments
// and allow some overrides like CNI_COMMAND
var _ CNIArgs = &DelegateArgs{}
type DelegateArgs struct {
Command string
}
func (d *DelegateArgs) AsEnv() []string {
env := os.Environ()
// The custom values should come in the end to override the existing
// process environment of the same key.
env = append(env,
"CNI_COMMAND="+d.Command,
)
return dedupEnv(env)
}
// dedupEnv returns a copy of env with any duplicates removed, in favor of later values.
// Items not of the normal environment "key=value" form are preserved unchanged.
func dedupEnv(env []string) []string {
out := make([]string, 0, len(env))
envMap := map[string]string{}
for _, kv := range env {
// find the first "=" in environment, if not, just keep it
eq := strings.Index(kv, "=")
if eq < 0 {
out = append(out, kv)
continue
}
envMap[kv[:eq]] = kv[eq+1:]
}
for k, v := range envMap {
out = append(out, fmt.Sprintf("%s=%s", k, v))
}
return out
}

View File

@ -17,24 +17,15 @@ package invoke_test
import ( import (
"os" "os"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/containernetworking/cni/pkg/invoke" "github.com/containernetworking/cni/pkg/invoke"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
) )
var _ = Describe("CNIArgs AsEnv", func() { var _ = Describe("Args", func() {
Describe("Args AsEnv", func() { Describe("AsEnv", func() {
BeforeEach(func() { It("places the CNI_ environment variables ahead of any ambient variables", func() {
os.Setenv("CNI_COMMAND", "DEL")
os.Setenv("CNI_IFNAME", "eth0")
os.Setenv("CNI_CONTAINERID", "id")
os.Setenv("CNI_ARGS", "args")
os.Setenv("CNI_NETNS", "testns")
os.Setenv("CNI_PATH", "testpath")
})
It("places the CNI environment variables in the end to be prepended", func() {
args := invoke.Args{ args := invoke.Args{
Command: "ADD", Command: "ADD",
ContainerID: "some-container-id", ContainerID: "some-container-id",
@ -46,94 +37,24 @@ var _ = Describe("CNIArgs AsEnv", func() {
IfName: "eth7", IfName: "eth7",
Path: "/some/cni/path", Path: "/some/cni/path",
} }
const numCNIEnvVars = 6
latentEnvs := os.Environ() latentVars := os.Environ()
numLatentEnvs := len(latentEnvs)
cniEnvs := args.AsEnv() cniEnv := args.AsEnv()
Expect(cniEnvs).To(HaveLen(numLatentEnvs)) Expect(cniEnv).To(HaveLen(len(latentVars) + numCNIEnvVars))
Expect(cniEnv[0:numCNIEnvVars]).To(Equal([]string{
"CNI_COMMAND=ADD",
"CNI_CONTAINERID=some-container-id",
"CNI_NETNS=/some/netns/path",
"CNI_ARGS=KEY1=VALUE1;KEY2=VALUE2",
"CNI_IFNAME=eth7",
"CNI_PATH=/some/cni/path",
}))
Expect(inStringSlice("CNI_COMMAND=ADD", cniEnvs)).To(BeTrue()) for i := range latentVars {
Expect(inStringSlice("CNI_IFNAME=eth7", cniEnvs)).To(BeTrue()) Expect(cniEnv[numCNIEnvVars+i]).To(Equal(latentVars[i]))
Expect(inStringSlice("CNI_CONTAINERID=some-container-id", cniEnvs)).To(BeTrue())
Expect(inStringSlice("CNI_NETNS=/some/netns/path", cniEnvs)).To(BeTrue())
Expect(inStringSlice("CNI_ARGS=KEY1=VALUE1;KEY2=VALUE2", cniEnvs)).To(BeTrue())
Expect(inStringSlice("CNI_PATH=/some/cni/path", cniEnvs)).To(BeTrue())
Expect(inStringSlice("CNI_COMMAND=DEL", cniEnvs)).To(BeFalse())
Expect(inStringSlice("CNI_IFNAME=eth0", cniEnvs)).To(BeFalse())
Expect(inStringSlice("CNI_CONTAINERID=id", cniEnvs)).To(BeFalse())
Expect(inStringSlice("CNI_NETNS=testns", cniEnvs)).To(BeFalse())
Expect(inStringSlice("CNI_ARGS=args", cniEnvs)).To(BeFalse())
Expect(inStringSlice("CNI_PATH=testpath", cniEnvs)).To(BeFalse())
})
AfterEach(func() {
os.Unsetenv("CNI_COMMAND")
os.Unsetenv("CNI_IFNAME")
os.Unsetenv("CNI_CONTAINERID")
os.Unsetenv("CNI_ARGS")
os.Unsetenv("CNI_NETNS")
os.Unsetenv("CNI_PATH")
})
})
Describe("DelegateArgs AsEnv", func() {
BeforeEach(func() {
os.Unsetenv("CNI_COMMAND")
})
It("override CNI_COMMAND if it already exists in environment variables", func() {
os.Setenv("CNI_COMMAND", "DEL")
delegateArgs := invoke.DelegateArgs{
Command: "ADD",
} }
latentEnvs := os.Environ()
numLatentEnvs := len(latentEnvs)
cniEnvs := delegateArgs.AsEnv()
Expect(cniEnvs).To(HaveLen(numLatentEnvs))
Expect(inStringSlice("CNI_COMMAND=ADD", cniEnvs)).To(BeTrue())
Expect(inStringSlice("CNI_COMMAND=DEL", cniEnvs)).To(BeFalse())
})
It("append CNI_COMMAND if it does not exist in environment variables", func() {
delegateArgs := invoke.DelegateArgs{
Command: "ADD",
}
latentEnvs := os.Environ()
numLatentEnvs := len(latentEnvs)
cniEnvs := delegateArgs.AsEnv()
Expect(cniEnvs).To(HaveLen(numLatentEnvs + 1))
Expect(inStringSlice("CNI_COMMAND=ADD", cniEnvs)).To(BeTrue())
})
AfterEach(func() {
os.Unsetenv("CNI_COMMAND")
})
})
Describe("inherited AsEnv", func() {
It("return nil string slice if we call AsEnv of inherited", func() {
inheritedArgs := invoke.ArgsFromEnv()
var nilSlice []string = nil
Expect(inheritedArgs.AsEnv()).To(Equal(nilSlice))
}) })
}) })
}) })
func inStringSlice(in string, slice []string) bool {
for _, s := range slice {
if in == s {
return true
}
}
return false
}

View File

@ -16,17 +16,22 @@ package invoke
import ( import (
"context" "context"
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types"
) )
func delegateCommon(delegatePlugin string, exec Exec) (string, Exec, error) { func delegateCommon(expectedCommand, delegatePlugin string, exec Exec) (string, Exec, error) {
if exec == nil { if exec == nil {
exec = defaultExec exec = defaultExec
} }
if os.Getenv("CNI_COMMAND") != expectedCommand {
return "", nil, fmt.Errorf("CNI_COMMAND is not " + expectedCommand)
}
paths := filepath.SplitList(os.Getenv("CNI_PATH")) paths := filepath.SplitList(os.Getenv("CNI_PATH"))
pluginPath, err := exec.FindInPath(delegatePlugin, paths) pluginPath, err := exec.FindInPath(delegatePlugin, paths)
if err != nil { if err != nil {
@ -39,51 +44,32 @@ func delegateCommon(delegatePlugin string, exec Exec) (string, Exec, error) {
// DelegateAdd calls the given delegate plugin with the CNI ADD action and // DelegateAdd calls the given delegate plugin with the CNI ADD action and
// JSON configuration // JSON configuration
func DelegateAdd(ctx context.Context, delegatePlugin string, netconf []byte, exec Exec) (types.Result, error) { func DelegateAdd(ctx context.Context, delegatePlugin string, netconf []byte, exec Exec) (types.Result, error) {
pluginPath, realExec, err := delegateCommon(delegatePlugin, exec) pluginPath, realExec, err := delegateCommon("ADD", delegatePlugin, exec)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// DelegateAdd will override the original "CNI_COMMAND" env from process with ADD return ExecPluginWithResult(ctx, pluginPath, netconf, ArgsFromEnv(), realExec)
return ExecPluginWithResult(ctx, pluginPath, netconf, delegateArgs("ADD"), realExec)
} }
// DelegateCheck calls the given delegate plugin with the CNI CHECK action and // DelegateCheck calls the given delegate plugin with the CNI CHECK action and
// JSON configuration // JSON configuration
func DelegateCheck(ctx context.Context, delegatePlugin string, netconf []byte, exec Exec) error { func DelegateCheck(ctx context.Context, delegatePlugin string, netconf []byte, exec Exec) error {
return delegateNoResult(ctx, delegatePlugin, netconf, exec, "CHECK") pluginPath, realExec, err := delegateCommon("CHECK", delegatePlugin, exec)
}
func delegateNoResult(ctx context.Context, delegatePlugin string, netconf []byte, exec Exec, verb string) error {
pluginPath, realExec, err := delegateCommon(delegatePlugin, exec)
if err != nil { if err != nil {
return err return err
} }
return ExecPluginWithoutResult(ctx, pluginPath, netconf, delegateArgs(verb), realExec) return ExecPluginWithoutResult(ctx, pluginPath, netconf, ArgsFromEnv(), realExec)
} }
// DelegateDel calls the given delegate plugin with the CNI DEL action and // DelegateDel calls the given delegate plugin with the CNI DEL action and
// JSON configuration // JSON configuration
func DelegateDel(ctx context.Context, delegatePlugin string, netconf []byte, exec Exec) error { func DelegateDel(ctx context.Context, delegatePlugin string, netconf []byte, exec Exec) error {
return delegateNoResult(ctx, delegatePlugin, netconf, exec, "DEL") pluginPath, realExec, err := delegateCommon("DEL", delegatePlugin, exec)
} if err != nil {
return err
// 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
func delegateArgs(action string) *DelegateArgs {
return &DelegateArgs{
Command: action,
} }
return ExecPluginWithoutResult(ctx, pluginPath, netconf, ArgsFromEnv(), realExec)
} }

View File

@ -17,17 +17,17 @@ package invoke_test
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"io/ioutil"
"net" "net"
"os" "os"
"path/filepath" "path/filepath"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/containernetworking/cni/pkg/invoke" "github.com/containernetworking/cni/pkg/invoke"
current "github.com/containernetworking/cni/pkg/types/100" "github.com/containernetworking/cni/pkg/types/current"
"github.com/containernetworking/cni/pkg/version"
"github.com/containernetworking/cni/plugins/test/noop/debug" "github.com/containernetworking/cni/plugins/test/noop/debug"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
) )
var _ = Describe("Delegate", func() { var _ = Describe("Delegate", func() {
@ -43,13 +43,14 @@ var _ = Describe("Delegate", func() {
BeforeEach(func() { BeforeEach(func() {
netConf, _ = json.Marshal(map[string]string{ netConf, _ = json.Marshal(map[string]string{
"name": "delegate-test", "name": "delegate-test",
"cniVersion": version.Current(), "cniVersion": "0.4.0",
}) })
expectedResult = &current.Result{ expectedResult = &current.Result{
CNIVersion: current.ImplementedSpecVersion, CNIVersion: "0.4.0",
IPs: []*current.IPConfig{ IPs: []*current.IPConfig{
{ {
Version: "4",
Address: net.IPNet{ Address: net.IPNet{
IP: net.ParseIP("10.1.2.3"), IP: net.ParseIP("10.1.2.3"),
Mask: net.CIDRMask(24, 32), Mask: net.CIDRMask(24, 32),
@ -59,7 +60,7 @@ var _ = Describe("Delegate", func() {
} }
expectedResultBytes, _ := json.Marshal(expectedResult) expectedResultBytes, _ := json.Marshal(expectedResult)
debugFile, err := os.CreateTemp("", "cni_debug") debugFile, err := ioutil.TempFile("", "cni_debug")
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(debugFile.Close()).To(Succeed()) Expect(debugFile.Close()).To(Succeed())
debugFileName = debugFile.Name() debugFileName = debugFile.Name()
@ -100,23 +101,14 @@ var _ = Describe("Delegate", func() {
Expect(pluginInvocation.CmdArgs.IfName).To(Equal("eth7")) Expect(pluginInvocation.CmdArgs.IfName).To(Equal("eth7"))
}) })
Context("if the ADD delegation runs on an existing non-ADD command, ", func() { Context("if the delegation isn't part of an existing ADD command", func() {
BeforeEach(func() { BeforeEach(func() {
os.Setenv("CNI_COMMAND", "NOPE") os.Setenv("CNI_COMMAND", "NOPE")
}) })
It("aborts and returns a useful error", func() { It("aborts and returns a useful error", func() {
result, err := invoke.DelegateAdd(ctx, pluginName, netConf, nil) _, err := invoke.DelegateAdd(ctx, pluginName, netConf, nil)
Expect(err).NotTo(HaveOccurred()) Expect(err).To(MatchError("CNI_COMMAND is not ADD"))
Expect(result).To(Equal(expectedResult))
pluginInvocation, err := debug.ReadDebug(debugFileName)
Expect(err).NotTo(HaveOccurred())
Expect(pluginInvocation.Command).To(Equal("ADD"))
Expect(pluginInvocation.CmdArgs.IfName).To(Equal("eth7"))
// check the original env
Expect(os.Getenv("CNI_COMMAND")).To(Equal("NOPE"))
}) })
}) })
@ -147,22 +139,14 @@ var _ = Describe("Delegate", func() {
Expect(pluginInvocation.CmdArgs.IfName).To(Equal("eth7")) Expect(pluginInvocation.CmdArgs.IfName).To(Equal("eth7"))
}) })
Context("if the CHECK delegation runs on an existing non-CHECK command", func() { Context("if the delegation isn't part of an existing CHECK command", func() {
BeforeEach(func() { BeforeEach(func() {
os.Setenv("CNI_COMMAND", "NOPE") os.Setenv("CNI_COMMAND", "NOPE")
}) })
It("aborts and returns a useful error", func() { It("aborts and returns a useful error", func() {
err := invoke.DelegateCheck(ctx, pluginName, netConf, nil) err := invoke.DelegateCheck(ctx, pluginName, netConf, nil)
Expect(err).NotTo(HaveOccurred()) Expect(err).To(MatchError("CNI_COMMAND is not CHECK"))
pluginInvocation, err := debug.ReadDebug(debugFileName)
Expect(err).NotTo(HaveOccurred())
Expect(pluginInvocation.Command).To(Equal("CHECK"))
Expect(pluginInvocation.CmdArgs.IfName).To(Equal("eth7"))
// check the original env
Expect(os.Getenv("CNI_COMMAND")).To(Equal("NOPE"))
}) })
}) })
@ -193,22 +177,14 @@ var _ = Describe("Delegate", func() {
Expect(pluginInvocation.CmdArgs.IfName).To(Equal("eth7")) Expect(pluginInvocation.CmdArgs.IfName).To(Equal("eth7"))
}) })
Context("if the DEL delegation runs on an existing non-DEL command", func() { Context("if the delegation isn't part of an existing DEL command", func() {
BeforeEach(func() { BeforeEach(func() {
os.Setenv("CNI_COMMAND", "NOPE") os.Setenv("CNI_COMMAND", "NOPE")
}) })
It("aborts and returns a useful error", func() { It("aborts and returns a useful error", func() {
err := invoke.DelegateDel(ctx, pluginName, netConf, nil) err := invoke.DelegateDel(ctx, pluginName, netConf, nil)
Expect(err).NotTo(HaveOccurred()) Expect(err).To(MatchError("CNI_COMMAND is not DEL"))
pluginInvocation, err := debug.ReadDebug(debugFileName)
Expect(err).NotTo(HaveOccurred())
Expect(pluginInvocation.Command).To(Equal("DEL"))
Expect(pluginInvocation.CmdArgs.IfName).To(Equal("eth7"))
// check the original env
Expect(os.Getenv("CNI_COMMAND")).To(Equal("NOPE"))
}) })
}) })
@ -223,48 +199,4 @@ var _ = Describe("Delegate", func() {
}) })
}) })
}) })
Describe("DelegateStatus", func() {
BeforeEach(func() {
os.Setenv("CNI_COMMAND", "STATUS")
})
It("finds and execs the named plugin", func() {
err := invoke.DelegateStatus(ctx, pluginName, netConf, nil)
Expect(err).NotTo(HaveOccurred())
pluginInvocation, err := debug.ReadDebug(debugFileName)
Expect(err).NotTo(HaveOccurred())
Expect(pluginInvocation.Command).To(Equal("STATUS"))
})
Context("if the STATUS delegation runs on an existing non-STATUS command", func() {
BeforeEach(func() {
os.Setenv("CNI_COMMAND", "NOPE")
})
It("aborts and returns a useful error", func() {
err := invoke.DelegateStatus(ctx, pluginName, netConf, nil)
Expect(err).NotTo(HaveOccurred())
pluginInvocation, err := debug.ReadDebug(debugFileName)
Expect(err).NotTo(HaveOccurred())
Expect(pluginInvocation.Command).To(Equal("STATUS"))
// check the original env
Expect(os.Getenv("CNI_COMMAND")).To(Equal("NOPE"))
})
})
Context("when the plugin cannot be found", func() {
BeforeEach(func() {
pluginName = "non-existent-plugin"
})
It("returns a useful error", func() {
err := invoke.DelegateStatus(ctx, pluginName, netConf, nil)
Expect(err).To(MatchError(HavePrefix("failed to find plugin")))
})
})
})
}) })

View File

@ -16,12 +16,10 @@ package invoke
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"os" "os"
"github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/types/create"
"github.com/containernetworking/cni/pkg/version" "github.com/containernetworking/cni/pkg/version"
) )
@ -34,64 +32,21 @@ type Exec interface {
Decode(jsonBytes []byte) (version.PluginInfo, error) 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 // For example, a testcase could pass an instance of the following fakeExec
// object to ExecPluginWithResult() to verify the incoming stdin and environment // object to ExecPluginWithResult() to verify the incoming stdin and environment
// and provide a tailored response: // and provide a tailored response:
// //
// import ( //import (
// "encoding/json" // "encoding/json"
// "path" // "path"
// "strings" // "strings"
// ) //)
// //
// type fakeExec struct { //type fakeExec struct {
// version.PluginDecoder // 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{} // net := &types.NetConf{}
// err := json.Unmarshal(stdinData, net) // err := json.Unmarshal(stdinData, net)
// if err != nil { // if err != nil {
@ -109,14 +64,14 @@ func fixupResultVersion(netconf, result []byte) (string, []byte, error) {
// } // }
// } // }
// return []byte("{\"CNIVersion\":\"0.4.0\"}"), nil // 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 { // if len(paths) > 0 {
// return path.Join(paths[0], plugin), nil // return path.Join(paths[0], plugin), nil
// } // }
// return "", fmt.Errorf("failed to find plugin %s in paths %v", plugin, paths) // 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) { func ExecPluginWithResult(ctx context.Context, pluginPath string, netconf []byte, args CNIArgs, exec Exec) (types.Result, error) {
if exec == nil { if exec == nil {
@ -128,12 +83,14 @@ func ExecPluginWithResult(ctx context.Context, pluginPath string, netconf []byte
return nil, err return nil, err
} }
resultVersion, fixedBytes, err := fixupResultVersion(netconf, stdoutBytes) // Plugin must return result in same version as specified in netconf
versionDecoder := &version.ConfigDecoder{}
confVersion, err := versionDecoder.Decode(netconf)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return create.Create(resultVersion, fixedBytes) return version.NewResult(confVersion, stdoutBytes)
} }
func ExecPluginWithoutResult(ctx context.Context, pluginPath string, netconf []byte, args CNIArgs, exec Exec) error { func ExecPluginWithoutResult(ctx context.Context, pluginPath string, netconf []byte, args CNIArgs, exec Exec) error {

View File

@ -19,13 +19,13 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/containernetworking/cni/pkg/invoke" "github.com/containernetworking/cni/pkg/invoke"
"github.com/containernetworking/cni/pkg/invoke/fakes" "github.com/containernetworking/cni/pkg/invoke/fakes"
current "github.com/containernetworking/cni/pkg/types/100" "github.com/containernetworking/cni/pkg/types/current"
"github.com/containernetworking/cni/pkg/version" "github.com/containernetworking/cni/pkg/version"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
) )
var _ = Describe("Executing a plugin, unit tests", func() { var _ = Describe("Executing a plugin, unit tests", func() {
@ -42,7 +42,7 @@ var _ = Describe("Executing a plugin, unit tests", func() {
BeforeEach(func() { BeforeEach(func() {
rawExec = &fakes.RawExec{} rawExec = &fakes.RawExec{}
rawExec.ExecPluginCall.Returns.ResultBytes = []byte(`{ "cniVersion": "0.3.1", "ips": [ { "version": "4", "address": "1.2.3.4/24" } ] }`) rawExec.ExecPluginCall.Returns.ResultBytes = []byte(`{ "ips": [ { "version": "4", "address": "1.2.3.4/24" } ] }`)
versionDecoder = &fakes.VersionDecoder{} versionDecoder = &fakes.VersionDecoder{}
versionDecoder.DecodeCall.Returns.PluginInfo = version.PluginSupports("0.42.0") versionDecoder.DecodeCall.Returns.PluginInfo = version.PluginSupports("0.42.0")
@ -68,13 +68,12 @@ var _ = Describe("Executing a plugin, unit tests", func() {
result, err := current.GetResult(r) result, err := current.GetResult(r)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(result.IPs).To(HaveLen(1)) Expect(len(result.IPs)).To(Equal(1))
Expect(result.IPs[0].Address.IP.String()).To(Equal("1.2.3.4")) Expect(result.IPs[0].Address.IP.String()).To(Equal("1.2.3.4"))
}) })
It("passes its arguments through to the rawExec", func() { It("passes its arguments through to the rawExec", func() {
_, err := invoke.ExecPluginWithResult(ctx, pluginPath, netconf, cniargs, pluginExec) invoke.ExecPluginWithResult(ctx, pluginPath, netconf, cniargs, pluginExec)
Expect(err).NotTo(HaveOccurred())
Expect(rawExec.ExecPluginCall.Received.PluginPath).To(Equal(pluginPath)) Expect(rawExec.ExecPluginCall.Received.PluginPath).To(Equal(pluginPath))
Expect(rawExec.ExecPluginCall.Received.StdinData).To(Equal(netconf)) Expect(rawExec.ExecPluginCall.Received.StdinData).To(Equal(netconf))
Expect(rawExec.ExecPluginCall.Received.Environ).To(Equal([]string{"SOME=ENV"})) Expect(rawExec.ExecPluginCall.Received.Environ).To(Equal([]string{"SOME=ENV"}))
@ -97,44 +96,11 @@ var _ = Describe("Executing a plugin, unit tests", func() {
_, err := invoke.ExecPluginWithResult(ctx, pluginPath, netconf, cniargs, nil) _, err := invoke.ExecPluginWithResult(ctx, pluginPath, netconf, cniargs, nil)
Expect(err).To(HaveOccurred()) Expect(err).To(HaveOccurred())
}) })
It("assumes config version if result version is missing", func() {
rawExec.ExecPluginCall.Returns.ResultBytes = []byte(`{ "ips": [ { "version": "4", "address": "1.2.3.4/24" } ] }`)
r, err := invoke.ExecPluginWithResult(ctx, pluginPath, netconf, cniargs, pluginExec)
Expect(err).NotTo(HaveOccurred())
Expect(r.Version()).To(Equal("0.3.1"))
result, err := current.GetResult(r)
Expect(err).NotTo(HaveOccurred())
Expect(result.IPs).To(HaveLen(1))
Expect(result.IPs[0].Address.IP.String()).To(Equal("1.2.3.4"))
})
It("assumes config version if result version is empty", func() {
rawExec.ExecPluginCall.Returns.ResultBytes = []byte(`{ "cniVersion": "", "ips": [ { "version": "4", "address": "1.2.3.4/24" } ] }`)
r, err := invoke.ExecPluginWithResult(ctx, pluginPath, netconf, cniargs, pluginExec)
Expect(err).NotTo(HaveOccurred())
Expect(r.Version()).To(Equal("0.3.1"))
result, err := current.GetResult(r)
Expect(err).NotTo(HaveOccurred())
Expect(result.IPs).To(HaveLen(1))
Expect(result.IPs[0].Address.IP.String()).To(Equal("1.2.3.4"))
})
It("assumes 0.1.0 if config and result version are empty", func() {
netconf = []byte(`{ "some": "stdin" }`)
rawExec.ExecPluginCall.Returns.ResultBytes = []byte(`{ "some": "version-info" }`)
r, err := invoke.ExecPluginWithResult(ctx, pluginPath, netconf, cniargs, pluginExec)
Expect(err).NotTo(HaveOccurred())
Expect(r.Version()).To(Equal("0.1.0"))
})
}) })
Describe("without returning a result", func() { Describe("without returning a result", func() {
It("passes its arguments through to the rawExec", func() { It("passes its arguments through to the rawExec", func() {
err := invoke.ExecPluginWithoutResult(ctx, pluginPath, netconf, cniargs, pluginExec) invoke.ExecPluginWithoutResult(ctx, pluginPath, netconf, cniargs, pluginExec)
Expect(err).NotTo(HaveOccurred())
Expect(rawExec.ExecPluginCall.Received.PluginPath).To(Equal(pluginPath)) Expect(rawExec.ExecPluginCall.Received.PluginPath).To(Equal(pluginPath))
Expect(rawExec.ExecPluginCall.Received.StdinData).To(Equal(netconf)) Expect(rawExec.ExecPluginCall.Received.StdinData).To(Equal(netconf))
Expect(rawExec.ExecPluginCall.Received.Environ).To(Equal([]string{"SOME=ENV"})) Expect(rawExec.ExecPluginCall.Received.Environ).To(Equal([]string{"SOME=ENV"}))
@ -165,8 +131,7 @@ var _ = Describe("Executing a plugin, unit tests", func() {
}) })
It("execs the plugin with the command VERSION", func() { It("execs the plugin with the command VERSION", func() {
_, err := invoke.GetVersionInfo(ctx, pluginPath, pluginExec) invoke.GetVersionInfo(ctx, pluginPath, pluginExec)
Expect(err).NotTo(HaveOccurred())
Expect(rawExec.ExecPluginCall.Received.PluginPath).To(Equal(pluginPath)) Expect(rawExec.ExecPluginCall.Received.PluginPath).To(Equal(pluginPath))
Expect(rawExec.ExecPluginCall.Received.Environ).To(ContainElement("CNI_COMMAND=VERSION")) Expect(rawExec.ExecPluginCall.Received.Environ).To(ContainElement("CNI_COMMAND=VERSION"))
expectedStdin, _ := json.Marshal(map[string]string{"cniVersion": version.Current()}) expectedStdin, _ := json.Marshal(map[string]string{"cniVersion": version.Current()})
@ -202,8 +167,7 @@ var _ = Describe("Executing a plugin, unit tests", func() {
}) })
It("sets dummy values for env vars required by very old plugins", func() { It("sets dummy values for env vars required by very old plugins", func() {
_, err := invoke.GetVersionInfo(ctx, pluginPath, pluginExec) invoke.GetVersionInfo(ctx, pluginPath, pluginExec)
Expect(err).NotTo(HaveOccurred())
env := rawExec.ExecPluginCall.Received.Environ env := rawExec.ExecPluginCall.Received.Environ
Expect(env).To(ContainElement("CNI_NETNS=dummy")) Expect(env).To(ContainElement("CNI_NETNS=dummy"))

View File

@ -18,7 +18,6 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings"
) )
// FindInPath returns the full path of the plugin by searching in the provided path // FindInPath returns the full path of the plugin by searching in the provided path
@ -27,10 +26,6 @@ func FindInPath(plugin string, paths []string) (string, error) {
return "", fmt.Errorf("no plugin name provided") return "", fmt.Errorf("no plugin name provided")
} }
if strings.ContainsRune(plugin, os.PathSeparator) {
return "", fmt.Errorf("invalid plugin name: %s", plugin)
}
if len(paths) == 0 { if len(paths) == 0 {
return "", fmt.Errorf("no paths provided") return "", fmt.Errorf("no paths provided")
} }

View File

@ -16,14 +16,14 @@ package invoke_test
import ( import (
"fmt" "fmt"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/containernetworking/cni/pkg/invoke" "github.com/containernetworking/cni/pkg/invoke"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
) )
var _ = Describe("FindInPath", func() { var _ = Describe("FindInPath", func() {
@ -37,17 +37,17 @@ var _ = Describe("FindInPath", func() {
) )
BeforeEach(func() { BeforeEach(func() {
tempDir, err := os.MkdirTemp("", "cni-find") tempDir, err := ioutil.TempDir("", "cni-find")
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
plugin, err := os.CreateTemp(tempDir, "a-cni-plugin") plugin, err := ioutil.TempFile(tempDir, "a-cni-plugin")
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
plugin2Name := "a-plugin-with-extension" + invoke.ExecutableFileExtensions[0] plugin2Name := "a-plugin-with-extension" + invoke.ExecutableFileExtensions[0]
plugin2, err := os.Create(filepath.Join(tempDir, plugin2Name)) plugin2, err := os.Create(filepath.Join(tempDir, plugin2Name))
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
anotherTempDir, err = os.MkdirTemp("", "nothing-here") anotherTempDir, err = ioutil.TempDir("", "nothing-here")
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
multiplePaths = []string{anotherTempDir, tempDir} multiplePaths = []string{anotherTempDir, tempDir}
@ -99,13 +99,5 @@ var _ = Describe("FindInPath", func() {
Expect(err).To(MatchError(fmt.Sprintf("failed to find plugin %q in path %s", pluginName, pathsWithNothing))) Expect(err).To(MatchError(fmt.Sprintf("failed to find plugin %q in path %s", pluginName, pathsWithNothing)))
}) })
}) })
Context("When the plugin contains a directory separator", func() {
It("returns an error", func() {
bogusPlugin := ".." + string(os.PathSeparator) + "pluginname"
_, err := invoke.FindInPath(bogusPlugin, []string{anotherTempDir})
Expect(err).To(MatchError("invalid plugin name: " + bogusPlugin))
})
})
}) })
}) })

View File

@ -16,16 +16,18 @@ package invoke_test
import ( import (
"context" "context"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/containernetworking/cni/pkg/invoke" "github.com/containernetworking/cni/pkg/invoke"
"github.com/containernetworking/cni/pkg/version" "github.com/containernetworking/cni/pkg/version"
"github.com/containernetworking/cni/pkg/version/testhelpers" "github.com/containernetworking/cni/pkg/version/testhelpers"
. "github.com/onsi/ginkgo"
. "github.com/onsi/ginkgo/extensions/table"
. "github.com/onsi/gomega"
) )
var _ = Describe("GetVersion, integration tests", func() { var _ = Describe("GetVersion, integration tests", func() {
@ -35,7 +37,7 @@ var _ = Describe("GetVersion, integration tests", func() {
) )
BeforeEach(func() { BeforeEach(func() {
pluginDir, err := os.MkdirTemp("", "plugins") pluginDir, err := ioutil.TempDir("", "plugins")
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
pluginPath = filepath.Join(pluginDir, "test-plugin") pluginPath = filepath.Join(pluginDir, "test-plugin")
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
@ -127,7 +129,5 @@ func c(_ *skel.CmdArgs) error { fmt.Println("{}"); return nil }
func main() { skel.PluginMain(c, c) } func main() { skel.PluginMain(c, c) }
` `
const ( const git_ref_v010 = "2c482f4"
git_ref_v010 = "2c482f4" const git_ref_v020_no_custom_versions = "349d66d"
git_ref_v020_no_custom_versions = "349d66d"
)

View File

@ -15,11 +15,11 @@
package invoke_test package invoke_test
import ( import (
"testing" . "github.com/onsi/ginkgo"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"github.com/onsi/gomega/gexec" "github.com/onsi/gomega/gexec"
"testing"
) )
func TestInvoke(t *testing.T) { func TestInvoke(t *testing.T) {

View File

@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
// +build darwin dragonfly freebsd linux netbsd openbsd solaris // +build darwin dragonfly freebsd linux netbsd openbsd solaris
package invoke package invoke

View File

@ -21,8 +21,6 @@ import (
"fmt" "fmt"
"io" "io"
"os/exec" "os/exec"
"strings"
"time"
"github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types"
) )
@ -33,54 +31,30 @@ type RawExec struct {
func (e *RawExec) ExecPlugin(ctx context.Context, pluginPath string, stdinData []byte, environ []string) ([]byte, error) { func (e *RawExec) ExecPlugin(ctx context.Context, pluginPath string, stdinData []byte, environ []string) ([]byte, error) {
stdout := &bytes.Buffer{} stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{}
c := exec.CommandContext(ctx, pluginPath) c := exec.CommandContext(ctx, pluginPath)
c.Env = environ c.Env = environ
c.Stdin = bytes.NewBuffer(stdinData) c.Stdin = bytes.NewBuffer(stdinData)
c.Stdout = stdout c.Stdout = stdout
c.Stderr = stderr c.Stderr = e.Stderr
if err := c.Run(); err != nil {
// Retry the command on "text file busy" errors return nil, pluginErr(err, stdout.Bytes())
for i := 0; i <= 5; i++ {
err := c.Run()
// Command succeeded
if err == nil {
break
}
// If the plugin is currently about to be written, then we wait a
// second and try it again
if strings.Contains(err.Error(), "text file busy") {
time.Sleep(time.Second)
continue
}
// All other errors except than the busy text file
return nil, e.pluginErr(err, stdout.Bytes(), stderr.Bytes())
} }
// Copy stderr to caller's buffer in case plugin printed to both
// stdout and stderr for some reason. Ignore failures as stderr is
// only informational.
if e.Stderr != nil && stderr.Len() > 0 {
_, _ = stderr.WriteTo(e.Stderr)
}
return stdout.Bytes(), nil return stdout.Bytes(), nil
} }
func (e *RawExec) pluginErr(err error, stdout, stderr []byte) error { func pluginErr(err error, output []byte) error {
emsg := types.Error{} if _, ok := err.(*exec.ExitError); ok {
if len(stdout) == 0 { emsg := types.Error{}
if len(stderr) == 0 { if len(output) == 0 {
emsg.Msg = fmt.Sprintf("netplugin failed with no error message: %v", err) emsg.Msg = "netplugin failed with no error message"
} else { } else if perr := json.Unmarshal(output, &emsg); perr != nil {
emsg.Msg = fmt.Sprintf("netplugin failed: %q", string(stderr)) emsg.Msg = fmt.Sprintf("netplugin failed but error parsing its diagnostic message %q: %v", string(output), perr)
} }
} else if perr := json.Unmarshal(stdout, &emsg); perr != nil { return &emsg
emsg.Msg = fmt.Sprintf("netplugin failed but error parsing its diagnostic message %q: %v", string(stdout), perr)
} }
return &emsg
return err
} }
func (e *RawExec) FindInPath(plugin string, paths []string) (string, error) { func (e *RawExec) FindInPath(plugin string, paths []string) (string, error) {

View File

@ -17,13 +17,15 @@ package invoke_test
import ( import (
"bytes" "bytes"
"context" "context"
"io/ioutil"
"os" "os"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/containernetworking/cni/pkg/invoke" "github.com/containernetworking/cni/pkg/invoke"
noop_debug "github.com/containernetworking/cni/plugins/test/noop/debug" noop_debug "github.com/containernetworking/cni/plugins/test/noop/debug"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
) )
var _ = Describe("RawExec", func() { var _ = Describe("RawExec", func() {
@ -39,7 +41,7 @@ var _ = Describe("RawExec", func() {
const reportResult = `{ "some": "result" }` const reportResult = `{ "some": "result" }`
BeforeEach(func() { BeforeEach(func() {
debugFile, err := os.CreateTemp("", "cni_debug") debugFile, err := ioutil.TempFile("", "cni_debug")
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(debugFile.Close()).To(Succeed()) Expect(debugFile.Close()).To(Succeed())
debugFileName = debugFile.Name() debugFileName = debugFile.Name()
@ -103,39 +105,14 @@ var _ = Describe("RawExec", func() {
Context("when the plugin errors", func() { Context("when the plugin errors", func() {
BeforeEach(func() { BeforeEach(func() {
debug.ReportResult = "" debug.ReportError = "banana"
})
Context("and writes valid error JSON to stdout", func() {
It("wraps and returns the error", func() {
debug.ReportError = "banana"
Expect(debug.WriteDebug(debugFileName)).To(Succeed())
_, err := execer.ExecPlugin(ctx, pathToPlugin, stdin, environ)
Expect(err).To(HaveOccurred())
Expect(err).To(MatchError("banana"))
})
})
Context("and writes to stderr", func() {
It("returns an error message with stderr output", func() {
debug.ExitWithCode = 1
Expect(debug.WriteDebug(debugFileName)).To(Succeed())
_, err := execer.ExecPlugin(ctx, pathToPlugin, stdin, environ)
Expect(err).To(HaveOccurred())
Expect(err).To(MatchError(`netplugin failed: "some stderr message"`))
})
})
})
Context("when the plugin errors with no output on stdout or stderr", func() {
It("returns the exec error message", func() {
debug.ExitWithCode = 1
debug.ReportResult = ""
debug.ReportStderr = ""
Expect(debug.WriteDebug(debugFileName)).To(Succeed()) Expect(debug.WriteDebug(debugFileName)).To(Succeed())
})
It("wraps and returns the error", func() {
_, err := execer.ExecPlugin(ctx, pathToPlugin, stdin, environ) _, err := execer.ExecPlugin(ctx, pathToPlugin, stdin, environ)
Expect(err).To(HaveOccurred()) Expect(err).To(HaveOccurred())
Expect(err).To(MatchError("netplugin failed with no error message: exit status 1")) Expect(err).To(MatchError("banana"))
}) })
}) })

View File

@ -1,21 +0,0 @@
// 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
}

View File

@ -1,50 +0,0 @@
// 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
}

View File

@ -1,21 +0,0 @@
// 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
}

View File

@ -19,29 +19,26 @@ package skel
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"log" "log"
"os" "os"
"strings" "strings"
"github.com/containernetworking/cni/pkg/ns"
"github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/utils"
"github.com/containernetworking/cni/pkg/version" "github.com/containernetworking/cni/pkg/version"
) )
// CmdArgs captures all the arguments passed in to the plugin // CmdArgs captures all the arguments passed in to the plugin
// via both env vars and stdin // via both env vars and stdin
type CmdArgs struct { type CmdArgs struct {
ContainerID string ContainerID string
Netns string Netns string
IfName string IfName string
Args string Args string
Path string Path string
NetnsOverride string StdinData []byte
StdinData []byte
} }
type dispatcher struct { type dispatcher struct {
@ -56,26 +53,31 @@ type dispatcher struct {
type reqForCmdEntry map[string]bool type reqForCmdEntry map[string]bool
func (t *dispatcher) getCmdArgsFromEnv() (string, *CmdArgs, *types.Error) { // internal only error to indicate lack of required environment variables
var cmd, contID, netns, ifName, args, path, netnsOverride string type missingEnvError struct {
msg string
}
func (e missingEnvError) Error() string {
return e.msg
}
func (t *dispatcher) getCmdArgsFromEnv() (string, *CmdArgs, error) {
var cmd, contID, netns, ifName, args, path string
vars := []struct { vars := []struct {
name string name string
val *string val *string
reqForCmd reqForCmdEntry reqForCmd reqForCmdEntry
validateFn func(string) *types.Error
}{ }{
{ {
"CNI_COMMAND", "CNI_COMMAND",
&cmd, &cmd,
reqForCmdEntry{ reqForCmdEntry{
"ADD": true, "ADD": true,
"CHECK": true, "CHECK": true,
"DEL": true, "DEL": true,
"GC": true,
"STATUS": true,
}, },
nil,
}, },
{ {
"CNI_CONTAINERID", "CNI_CONTAINERID",
@ -85,7 +87,6 @@ func (t *dispatcher) getCmdArgsFromEnv() (string, *CmdArgs, *types.Error) {
"CHECK": true, "CHECK": true,
"DEL": true, "DEL": true,
}, },
utils.ValidateContainerID,
}, },
{ {
"CNI_NETNS", "CNI_NETNS",
@ -95,7 +96,6 @@ func (t *dispatcher) getCmdArgsFromEnv() (string, *CmdArgs, *types.Error) {
"CHECK": true, "CHECK": true,
"DEL": false, "DEL": false,
}, },
nil,
}, },
{ {
"CNI_IFNAME", "CNI_IFNAME",
@ -105,7 +105,6 @@ func (t *dispatcher) getCmdArgsFromEnv() (string, *CmdArgs, *types.Error) {
"CHECK": true, "CHECK": true,
"DEL": true, "DEL": true,
}, },
utils.ValidateInterfaceName,
}, },
{ {
"CNI_ARGS", "CNI_ARGS",
@ -115,29 +114,15 @@ func (t *dispatcher) getCmdArgsFromEnv() (string, *CmdArgs, *types.Error) {
"CHECK": false, "CHECK": false,
"DEL": false, "DEL": false,
}, },
nil,
}, },
{ {
"CNI_PATH", "CNI_PATH",
&path, &path,
reqForCmdEntry{ reqForCmdEntry{
"ADD": true, "ADD": true,
"CHECK": true, "CHECK": true,
"DEL": true, "DEL": true,
"GC": true,
"STATUS": true,
}, },
nil,
},
{
"CNI_NETNS_OVERRIDE",
&netnsOverride,
reqForCmdEntry{
"ADD": false,
"CHECK": false,
"DEL": false,
},
nil,
}, },
} }
@ -148,201 +133,136 @@ func (t *dispatcher) getCmdArgsFromEnv() (string, *CmdArgs, *types.Error) {
if v.reqForCmd[cmd] || v.name == "CNI_COMMAND" { if v.reqForCmd[cmd] || v.name == "CNI_COMMAND" {
argsMissing = append(argsMissing, v.name) 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 { if len(argsMissing) > 0 {
joined := strings.Join(argsMissing, ",") joined := strings.Join(argsMissing, ",")
return "", nil, types.NewError(types.ErrInvalidEnvironmentVariables, fmt.Sprintf("required env variables [%s] missing", joined), "") return "", nil, missingEnvError{fmt.Sprintf("required env variables [%s] missing", joined)}
} }
if cmd == "VERSION" { if cmd == "VERSION" {
t.Stdin = bytes.NewReader(nil) t.Stdin = bytes.NewReader(nil)
} }
stdinData, err := io.ReadAll(t.Stdin) stdinData, err := ioutil.ReadAll(t.Stdin)
if err != nil { if err != nil {
return "", nil, types.NewError(types.ErrIOFailure, fmt.Sprintf("error reading from stdin: %v", err), "") return "", nil, fmt.Errorf("error reading from stdin: %v", err)
}
if cmd != "VERSION" {
if err := validateConfig(stdinData); err != nil {
return "", nil, err
}
} }
cmdArgs := &CmdArgs{ cmdArgs := &CmdArgs{
ContainerID: contID, ContainerID: contID,
Netns: netns, Netns: netns,
IfName: ifName, IfName: ifName,
Args: args, Args: args,
Path: path, Path: path,
StdinData: stdinData, StdinData: stdinData,
NetnsOverride: netnsOverride,
} }
return cmd, cmdArgs, nil return cmd, cmdArgs, nil
} }
func (t *dispatcher) checkVersionAndCall(cmdArgs *CmdArgs, pluginVersionInfo version.PluginInfo, toCall func(*CmdArgs) error) *types.Error { func createTypedError(f string, args ...interface{}) *types.Error {
return &types.Error{
Code: 100,
Msg: fmt.Sprintf(f, args...),
}
}
func (t *dispatcher) checkVersionAndCall(cmdArgs *CmdArgs, pluginVersionInfo version.PluginInfo, toCall func(*CmdArgs) error) error {
configVersion, err := t.ConfVersionDecoder.Decode(cmdArgs.StdinData) configVersion, err := t.ConfVersionDecoder.Decode(cmdArgs.StdinData)
if err != nil { if err != nil {
return types.NewError(types.ErrDecodingFailure, err.Error(), "") return err
} }
verErr := t.VersionReconciler.Check(configVersion, pluginVersionInfo) verErr := t.VersionReconciler.Check(configVersion, pluginVersionInfo)
if verErr != nil { if verErr != nil {
return types.NewError(types.ErrIncompatibleCNIVersion, "incompatible CNI versions", verErr.Details()) return &types.Error{
} Code: types.ErrIncompatibleCNIVersion,
Msg: "incompatible CNI versions",
if toCall == nil { Details: verErr.Details(),
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 return toCall(cmdArgs)
} }
func validateConfig(jsonBytes []byte) *types.Error { func validateConfig(jsonBytes []byte) error {
var conf struct { var conf struct {
Name string `json:"name"` Name string `json:"name"`
} }
if err := json.Unmarshal(jsonBytes, &conf); err != nil { if err := json.Unmarshal(jsonBytes, &conf); err != nil {
return types.NewError(types.ErrDecodingFailure, fmt.Sprintf("error unmarshall network config: %v", err), "") return fmt.Errorf("error reading network config: %s", err)
} }
if conf.Name == "" { if conf.Name == "" {
return types.NewError(types.ErrInvalidNetworkConfig, "missing network name", "") return fmt.Errorf("missing network name")
}
if err := utils.ValidateNetworkName(conf.Name); err != nil {
return err
} }
return nil return nil
} }
func (t *dispatcher) pluginMain(funcs CNIFuncs, versionInfo version.PluginInfo, about string) *types.Error { func (t *dispatcher) pluginMain(cmdAdd, cmdCheck, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo, about string) *types.Error {
cmd, cmdArgs, err := t.getCmdArgsFromEnv() cmd, cmdArgs, err := t.getCmdArgsFromEnv()
if err != nil { if err != nil {
// Print the about string to stderr when no command is set // Print the about string to stderr when no command is set
if err.Code == types.ErrInvalidEnvironmentVariables && t.Getenv("CNI_COMMAND") == "" && about != "" { if _, ok := err.(missingEnvError); ok && t.Getenv("CNI_COMMAND") == "" && about != "" {
_, _ = fmt.Fprintln(t.Stderr, about) fmt.Fprintln(t.Stderr, about)
_, _ = fmt.Fprintf(t.Stderr, "CNI protocol versions supported: %s\n", strings.Join(versionInfo.SupportedVersions(), ", "))
return nil return nil
} }
return err return createTypedError(err.Error())
}
if cmd != "VERSION" {
err = validateConfig(cmdArgs.StdinData)
if err != nil {
return createTypedError(err.Error())
}
} }
switch cmd { switch cmd {
case "ADD": case "ADD":
err = t.checkVersionAndCall(cmdArgs, versionInfo, funcs.Add) err = t.checkVersionAndCall(cmdArgs, versionInfo, cmdAdd)
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": case "CHECK":
configVersion, err := t.ConfVersionDecoder.Decode(cmdArgs.StdinData) configVersion, err := t.ConfVersionDecoder.Decode(cmdArgs.StdinData)
if err != nil { if err != nil {
return types.NewError(types.ErrDecodingFailure, err.Error(), "") return createTypedError(err.Error())
} }
if gtet, err := version.GreaterThanOrEqualTo(configVersion, "0.4.0"); err != nil { if gtet, err := version.GreaterThanOrEqualTo(configVersion, "0.4.0"); err != nil {
return types.NewError(types.ErrDecodingFailure, err.Error(), "") return createTypedError(err.Error())
} else if !gtet { } else if !gtet {
return types.NewError(types.ErrIncompatibleCNIVersion, "config version does not allow CHECK", "") return &types.Error{
Code: types.ErrIncompatibleCNIVersion,
Msg: "config version does not allow CHECK",
}
} }
for _, pluginVersion := range versionInfo.SupportedVersions() { for _, pluginVersion := range versionInfo.SupportedVersions() {
gtet, err := version.GreaterThanOrEqualTo(pluginVersion, configVersion) gtet, err := version.GreaterThanOrEqualTo(pluginVersion, configVersion)
if err != nil { if err != nil {
return types.NewError(types.ErrDecodingFailure, err.Error(), "") return createTypedError(err.Error())
} else if gtet { } else if gtet {
if err := t.checkVersionAndCall(cmdArgs, versionInfo, funcs.Check); err != nil { if err := t.checkVersionAndCall(cmdArgs, versionInfo, cmdCheck); err != nil {
return err return createTypedError(err.Error())
} }
return nil return nil
} }
} }
return types.NewError(types.ErrIncompatibleCNIVersion, "plugin version does not allow CHECK", "") return &types.Error{
Code: types.ErrIncompatibleCNIVersion,
Msg: "plugin version does not allow CHECK",
}
case "DEL": case "DEL":
err = t.checkVersionAndCall(cmdArgs, versionInfo, funcs.Del) err = t.checkVersionAndCall(cmdArgs, versionInfo, cmdDel)
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": case "VERSION":
if err := versionInfo.Encode(t.Stdout); err != nil { err = versionInfo.Encode(t.Stdout)
return types.NewError(types.ErrIOFailure, err.Error(), "")
}
default: default:
return types.NewError(types.ErrInvalidEnvironmentVariables, fmt.Sprintf("unknown CNI_COMMAND: %v", cmd), "") return createTypedError("unknown CNI_COMMAND: %v", cmd)
} }
return err if err != nil {
if e, ok := err.(*types.Error); ok {
// don't wrap Error in Error
return e
}
return createTypedError(err.Error())
}
return nil
} }
// PluginMainWithError is the core "main" for a plugin. It accepts // PluginMainWithError is the core "main" for a plugin. It accepts
@ -357,63 +277,13 @@ func (t *dispatcher) pluginMain(funcs CNIFuncs, versionInfo version.PluginInfo,
// //
// To let this package automatically handle errors and call os.Exit(1) for you, // To let this package automatically handle errors and call os.Exit(1) for you,
// use PluginMain() instead. // 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 { 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{ return (&dispatcher{
Getenv: os.Getenv, Getenv: os.Getenv,
Stdin: os.Stdin, Stdin: os.Stdin,
Stdout: os.Stdout, Stdout: os.Stdout,
Stderr: os.Stderr, Stderr: os.Stderr,
}).pluginMain(funcs, versionInfo, about) }).pluginMain(cmdAdd, cmdCheck, cmdDel, 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 <foo> v<version>"
//
// 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. // PluginMain is the core "main" for a plugin which includes automatic error handling.
@ -427,8 +297,6 @@ func PluginMainFuncs(funcs CNIFuncs, versionInfo version.PluginInfo, about strin
// as JSON to stdout and call os.Exit(1). // as JSON to stdout and call os.Exit(1).
// //
// To have more control over error handling, use PluginMainWithError() instead. // 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) { func PluginMain(cmdAdd, cmdCheck, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo, about string) {
if e := PluginMainWithError(cmdAdd, cmdCheck, cmdDel, versionInfo, about); e != nil { if e := PluginMainWithError(cmdAdd, cmdCheck, cmdDel, versionInfo, about); e != nil {
if err := e.Print(); err != nil { if err := e.Print(); err != nil {

View File

@ -15,10 +15,10 @@
package skel package skel
import ( import (
"testing" . "github.com/onsi/ginkgo"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"testing"
) )
func TestSkel(t *testing.T) { func TestSkel(t *testing.T) {

View File

@ -20,11 +20,13 @@ import (
"fmt" "fmt"
"strings" "strings"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/types/current"
"github.com/containernetworking/cni/pkg/version" "github.com/containernetworking/cni/pkg/version"
. "github.com/onsi/ginkgo"
. "github.com/onsi/ginkgo/extensions/table"
. "github.com/onsi/gomega"
) )
type fakeCmd struct { type fakeCmd struct {
@ -45,14 +47,13 @@ func (c *fakeCmd) Func(args *CmdArgs) error {
var _ = Describe("dispatching to the correct callback", func() { var _ = Describe("dispatching to the correct callback", func() {
var ( var (
environment map[string]string environment map[string]string
stdinData string stdinData string
stdout, stderr *bytes.Buffer stdout, stderr *bytes.Buffer
cmdAdd, cmdCheck, cmdDel, cmdGC *fakeCmd cmdAdd, cmdCheck, cmdDel *fakeCmd
dispatch *dispatcher dispatch *dispatcher
expectedCmdArgs *CmdArgs expectedCmdArgs *CmdArgs
versionInfo version.PluginInfo versionInfo version.PluginInfo
funcs CNIFuncs
) )
BeforeEach(func() { BeforeEach(func() {
@ -68,7 +69,7 @@ var _ = Describe("dispatching to the correct callback", func() {
stdinData = `{ "name":"skel-test", "some": "config", "cniVersion": "9.8.7" }` stdinData = `{ "name":"skel-test", "some": "config", "cniVersion": "9.8.7" }`
stdout = &bytes.Buffer{} stdout = &bytes.Buffer{}
stderr = &bytes.Buffer{} stderr = &bytes.Buffer{}
versionInfo = version.PluginSupports("9.8.7", "10.0.0") versionInfo = version.PluginSupports("9.8.7")
dispatch = &dispatcher{ dispatch = &dispatcher{
Getenv: func(key string) string { return environment[key] }, Getenv: func(key string) string { return environment[key] },
Stdin: strings.NewReader(stdinData), Stdin: strings.NewReader(stdinData),
@ -78,14 +79,6 @@ var _ = Describe("dispatching to the correct callback", func() {
cmdAdd = &fakeCmd{} cmdAdd = &fakeCmd{}
cmdCheck = &fakeCmd{} cmdCheck = &fakeCmd{}
cmdDel = &fakeCmd{} cmdDel = &fakeCmd{}
cmdGC = &fakeCmd{}
funcs = CNIFuncs{
Add: cmdAdd.Func,
Del: cmdDel.Func,
Check: cmdCheck.Func,
GC: cmdGC.Func,
}
expectedCmdArgs = &CmdArgs{ expectedCmdArgs = &CmdArgs{
ContainerID: "some-container-id", ContainerID: "some-container-id",
Netns: "/some/netns/path", Netns: "/some/netns/path",
@ -96,13 +89,13 @@ var _ = Describe("dispatching to the correct callback", func() {
} }
}) })
envVarChecker := func(envVar string, isRequired bool) { var envVarChecker = func(envVar string, isRequired bool) {
delete(environment, envVar) delete(environment, envVar)
err := dispatch.pluginMain(funcs, versionInfo, "") err := dispatch.pluginMain(cmdAdd.Func, cmdCheck.Func, cmdDel.Func, versionInfo, "")
if isRequired { if isRequired {
Expect(err).To(Equal(&types.Error{ Expect(err).To(Equal(&types.Error{
Code: types.ErrInvalidEnvironmentVariables, Code: 100,
Msg: "required env variables [" + envVar + "] missing", Msg: "required env variables [" + envVar + "] missing",
})) }))
} else { } else {
@ -111,17 +104,8 @@ var _ = Describe("dispatching to the correct callback", func() {
} }
Context("when the CNI_COMMAND is ADD", func() { Context("when the CNI_COMMAND is ADD", func() {
expectedCmdArgs = &CmdArgs{
ContainerID: "some-container-id",
Netns: "/some/netns/path",
IfName: "eth0",
Args: "some;extra;args",
Path: "/some/cni/path",
StdinData: []byte(stdinData),
}
It("extracts env vars and stdin data and calls cmdAdd", func() { It("extracts env vars and stdin data and calls cmdAdd", func() {
err := dispatch.pluginMain(funcs, versionInfo, "") err := dispatch.pluginMain(cmdAdd.Func, cmdCheck.Func, cmdDel.Func, versionInfo, "")
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(cmdAdd.CallCount).To(Equal(1)) Expect(cmdAdd.CallCount).To(Equal(1))
@ -130,93 +114,8 @@ var _ = Describe("dispatching to the correct callback", func() {
Expect(cmdAdd.Received.CmdArgs).To(Equal(expectedCmdArgs)) Expect(cmdAdd.Received.CmdArgs).To(Equal(expectedCmdArgs))
}) })
It("returns an error when containerID has invalid characters", func() {
environment["CNI_CONTAINERID"] = "some-%%container-id"
err := dispatch.pluginMain(funcs, versionInfo, "")
Expect(err).To(HaveOccurred())
Expect(err).To(Equal(&types.Error{
Code: types.ErrInvalidEnvironmentVariables,
Msg: "invalid characters in containerID",
Details: "some-%%container-id",
}))
})
Context("return errors when interface name is invalid", func() {
It("interface name is too long", func() {
environment["CNI_IFNAME"] = "1234567890123456"
err := dispatch.pluginMain(funcs, versionInfo, "")
Expect(err).To(HaveOccurred())
Expect(err).To(Equal(&types.Error{
Code: types.ErrInvalidEnvironmentVariables,
Msg: "interface name is too long",
Details: "interface name should be less than 16 characters",
}))
})
It("interface name is .", func() {
environment["CNI_IFNAME"] = "."
err := dispatch.pluginMain(funcs, versionInfo, "")
Expect(err).To(HaveOccurred())
Expect(err).To(Equal(&types.Error{
Code: types.ErrInvalidEnvironmentVariables,
Msg: "interface name is . or ..",
Details: "",
}))
})
It("interface name is ..", func() {
environment["CNI_IFNAME"] = ".."
err := dispatch.pluginMain(funcs, versionInfo, "")
Expect(err).To(HaveOccurred())
Expect(err).To(Equal(&types.Error{
Code: types.ErrInvalidEnvironmentVariables,
Msg: "interface name is . or ..",
Details: "",
}))
})
It("interface name contains invalid characters /", func() {
environment["CNI_IFNAME"] = "test/test"
err := dispatch.pluginMain(funcs, versionInfo, "")
Expect(err).To(HaveOccurred())
Expect(err).To(Equal(&types.Error{
Code: types.ErrInvalidEnvironmentVariables,
Msg: "interface name contains / or : or whitespace characters",
Details: "",
}))
})
It("interface name contains invalid characters :", func() {
environment["CNI_IFNAME"] = "test:test"
err := dispatch.pluginMain(funcs, versionInfo, "")
Expect(err).To(HaveOccurred())
Expect(err).To(Equal(&types.Error{
Code: types.ErrInvalidEnvironmentVariables,
Msg: "interface name contains / or : or whitespace characters",
Details: "",
}))
})
It("interface name contains invalid characters whitespace", func() {
environment["CNI_IFNAME"] = "test test"
err := dispatch.pluginMain(funcs, versionInfo, "")
Expect(err).To(HaveOccurred())
Expect(err).To(Equal(&types.Error{
Code: types.ErrInvalidEnvironmentVariables,
Msg: "interface name contains / or : or whitespace characters",
Details: "",
}))
})
})
It("does not call cmdCheck or cmdDel", func() { It("does not call cmdCheck or cmdDel", func() {
err := dispatch.pluginMain(funcs, versionInfo, "") err := dispatch.pluginMain(cmdAdd.Func, cmdCheck.Func, cmdDel.Func, versionInfo, "")
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(cmdCheck.CallCount).To(Equal(0)) Expect(cmdCheck.CallCount).To(Equal(0))
@ -240,11 +139,11 @@ var _ = Describe("dispatching to the correct callback", func() {
}) })
It("reports that all of them are missing, not just the first", func() { It("reports that all of them are missing, not just the first", func() {
err := dispatch.pluginMain(funcs, versionInfo, "") err := dispatch.pluginMain(cmdAdd.Func, cmdCheck.Func, cmdDel.Func, versionInfo, "")
Expect(err).To(HaveOccurred()) Expect(err).To(HaveOccurred())
Expect(err).To(Equal(&types.Error{ Expect(err).To(Equal(&types.Error{
Code: types.ErrInvalidEnvironmentVariables, Code: 100,
Msg: "required env variables [CNI_NETNS,CNI_IFNAME,CNI_PATH] missing", Msg: "required env variables [CNI_NETNS,CNI_IFNAME,CNI_PATH] missing",
})) }))
}) })
@ -262,7 +161,8 @@ var _ = Describe("dispatching to the correct callback", func() {
}) })
It("infers the config is 0.1.0 and calls the cmdAdd callback", func() { It("infers the config is 0.1.0 and calls the cmdAdd callback", func() {
err := dispatch.pluginMain(funcs, versionInfo, "")
err := dispatch.pluginMain(cmdAdd.Func, cmdCheck.Func, cmdDel.Func, versionInfo, "")
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(cmdAdd.CallCount).To(Equal(1)) Expect(cmdAdd.CallCount).To(Equal(1))
@ -276,15 +176,14 @@ var _ = Describe("dispatching to the correct callback", func() {
}) })
It("immediately returns a useful error", func() { It("immediately returns a useful error", func() {
err := dispatch.pluginMain(funcs, versionInfo, "") err := dispatch.pluginMain(cmdAdd.Func, cmdCheck.Func, cmdDel.Func, versionInfo, "")
Expect(err.Code).To(Equal(types.ErrIncompatibleCNIVersion)) // see https://github.com/containernetworking/cni/blob/main/SPEC.md#well-known-error-codes Expect(err.Code).To(Equal(types.ErrIncompatibleCNIVersion)) // see https://github.com/containernetworking/cni/blob/master/SPEC.md#well-known-error-codes
Expect(err.Msg).To(Equal("incompatible CNI versions")) Expect(err.Msg).To(Equal("incompatible CNI versions"))
Expect(err.Details).To(Equal(`config is "0.1.0", plugin supports ["4.3.2"]`)) Expect(err.Details).To(Equal(`config is "0.1.0", plugin supports ["4.3.2"]`))
}) })
It("does not call either callback", func() { It("does not call either callback", func() {
err := dispatch.pluginMain(funcs, versionInfo, "") dispatch.pluginMain(cmdAdd.Func, cmdCheck.Func, cmdDel.Func, versionInfo, "")
Expect(err).To(HaveOccurred())
Expect(cmdAdd.CallCount).To(Equal(0)) Expect(cmdAdd.CallCount).To(Equal(0))
Expect(cmdCheck.CallCount).To(Equal(0)) Expect(cmdCheck.CallCount).To(Equal(0))
Expect(cmdDel.CallCount).To(Equal(0)) Expect(cmdDel.CallCount).To(Equal(0))
@ -299,7 +198,7 @@ var _ = Describe("dispatching to the correct callback", func() {
}) })
It("extracts env vars and stdin data and calls cmdCheck", func() { It("extracts env vars and stdin data and calls cmdCheck", func() {
err := dispatch.pluginMain(funcs, versionInfo, "") err := dispatch.pluginMain(cmdAdd.Func, cmdCheck.Func, cmdDel.Func, versionInfo, "")
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(cmdAdd.CallCount).To(Equal(0)) Expect(cmdAdd.CallCount).To(Equal(0))
@ -309,7 +208,7 @@ var _ = Describe("dispatching to the correct callback", func() {
}) })
It("does not call cmdAdd or cmdDel", func() { It("does not call cmdAdd or cmdDel", func() {
err := dispatch.pluginMain(funcs, versionInfo, "") err := dispatch.pluginMain(cmdAdd.Func, cmdCheck.Func, cmdDel.Func, versionInfo, "")
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(cmdAdd.CallCount).To(Equal(0)) Expect(cmdAdd.CallCount).To(Equal(0))
@ -333,11 +232,11 @@ var _ = Describe("dispatching to the correct callback", func() {
}) })
It("reports that all of them are missing, not just the first", func() { It("reports that all of them are missing, not just the first", func() {
err := dispatch.pluginMain(funcs, versionInfo, "") err := dispatch.pluginMain(cmdAdd.Func, cmdCheck.Func, cmdDel.Func, versionInfo, "")
Expect(err).To(HaveOccurred()) Expect(err).To(HaveOccurred())
Expect(err).To(Equal(&types.Error{ Expect(err).To(Equal(&types.Error{
Code: types.ErrInvalidEnvironmentVariables, Code: 100,
Msg: "required env variables [CNI_NETNS,CNI_IFNAME,CNI_PATH] missing", Msg: "required env variables [CNI_NETNS,CNI_IFNAME,CNI_PATH] missing",
})) }))
}) })
@ -346,8 +245,8 @@ var _ = Describe("dispatching to the correct callback", func() {
Context("when cniVersion is less than 0.4.0", func() { Context("when cniVersion is less than 0.4.0", func() {
It("immediately returns a useful error", func() { It("immediately returns a useful error", func() {
dispatch.Stdin = strings.NewReader(`{ "name": "skel-test", "cniVersion": "0.3.0", "some": "config" }`) dispatch.Stdin = strings.NewReader(`{ "name": "skel-test", "cniVersion": "0.3.0", "some": "config" }`)
err := dispatch.pluginMain(funcs, versionInfo, "") err := dispatch.pluginMain(cmdAdd.Func, cmdCheck.Func, cmdDel.Func, versionInfo, "")
Expect(err.Code).To(Equal(types.ErrIncompatibleCNIVersion)) // see https://github.com/containernetworking/cni/blob/main/SPEC.md#well-known-error-codes Expect(err.Code).To(Equal(types.ErrIncompatibleCNIVersion)) // see https://github.com/containernetworking/cni/blob/master/SPEC.md#well-known-error-codes
Expect(err.Msg).To(Equal("config version does not allow CHECK")) Expect(err.Msg).To(Equal("config version does not allow CHECK"))
Expect(cmdAdd.CallCount).To(Equal(0)) Expect(cmdAdd.CallCount).To(Equal(0))
Expect(cmdCheck.CallCount).To(Equal(0)) Expect(cmdCheck.CallCount).To(Equal(0))
@ -359,8 +258,8 @@ var _ = Describe("dispatching to the correct callback", func() {
It("immediately returns a useful error", func() { It("immediately returns a useful error", func() {
dispatch.Stdin = strings.NewReader(`{ "name": "skel-test", "cniVersion": "0.4.0", "some": "config" }`) dispatch.Stdin = strings.NewReader(`{ "name": "skel-test", "cniVersion": "0.4.0", "some": "config" }`)
versionInfo = version.PluginSupports("0.1.0", "0.2.0", "0.3.0") versionInfo = version.PluginSupports("0.1.0", "0.2.0", "0.3.0")
err := dispatch.pluginMain(funcs, versionInfo, "") err := dispatch.pluginMain(cmdAdd.Func, cmdCheck.Func, cmdDel.Func, versionInfo, "")
Expect(err.Code).To(Equal(types.ErrIncompatibleCNIVersion)) // see https://github.com/containernetworking/cni/blob/main/SPEC.md#well-known-error-codes Expect(err.Code).To(Equal(types.ErrIncompatibleCNIVersion)) // see https://github.com/containernetworking/cni/blob/master/SPEC.md#well-known-error-codes
Expect(err.Msg).To(Equal("plugin version does not allow CHECK")) Expect(err.Msg).To(Equal("plugin version does not allow CHECK"))
Expect(cmdAdd.CallCount).To(Equal(0)) Expect(cmdAdd.CallCount).To(Equal(0))
Expect(cmdCheck.CallCount).To(Equal(0)) Expect(cmdCheck.CallCount).To(Equal(0))
@ -370,22 +269,10 @@ var _ = Describe("dispatching to the correct callback", func() {
Context("when the config has a bad version", func() { Context("when the config has a bad version", func() {
It("immediately returns a useful error", func() { It("immediately returns a useful error", func() {
dispatch.Stdin = strings.NewReader(`{ "cniVersion": "adsfsadf", "some": "config", "name": "test" }`) dispatch.Stdin = strings.NewReader(`{ "cniVersion": "adsfsadf", "some": "config" }`)
versionInfo = version.PluginSupports("0.1.0", "0.2.0", "0.3.0") versionInfo = version.PluginSupports("0.1.0", "0.2.0", "0.3.0")
err := dispatch.pluginMain(funcs, versionInfo, "") err := dispatch.pluginMain(cmdAdd.Func, cmdCheck.Func, cmdDel.Func, versionInfo, "")
Expect(err.Code).To(Equal(types.ErrDecodingFailure)) Expect(err.Code).To(Equal(uint(100)))
Expect(cmdAdd.CallCount).To(Equal(0))
Expect(cmdCheck.CallCount).To(Equal(0))
Expect(cmdDel.CallCount).To(Equal(0))
})
})
Context("when the config has a bad name", func() {
It("immediately returns invalid network config", func() {
dispatch.Stdin = strings.NewReader(`{ "cniVersion": "0.4.0", "some": "config", "name": "te%%st" }`)
versionInfo = version.PluginSupports("0.1.0", "0.2.0", "0.3.0", "0.4.0")
err := dispatch.pluginMain(funcs, versionInfo, "")
Expect(err.Code).To(Equal(types.ErrInvalidNetworkConfig))
Expect(cmdAdd.CallCount).To(Equal(0)) Expect(cmdAdd.CallCount).To(Equal(0))
Expect(cmdCheck.CallCount).To(Equal(0)) Expect(cmdCheck.CallCount).To(Equal(0))
Expect(cmdDel.CallCount).To(Equal(0)) Expect(cmdDel.CallCount).To(Equal(0))
@ -394,10 +281,10 @@ var _ = Describe("dispatching to the correct callback", func() {
Context("when the plugin has a bad version", func() { Context("when the plugin has a bad version", func() {
It("immediately returns a useful error", func() { It("immediately returns a useful error", func() {
dispatch.Stdin = strings.NewReader(`{ "cniVersion": "0.4.0", "some": "config", "name": "test" }`) dispatch.Stdin = strings.NewReader(`{ "cniVersion": "0.4.0", "some": "config" }`)
versionInfo = version.PluginSupports("0.1.0", "0.2.0", "adsfasdf") versionInfo = version.PluginSupports("0.1.0", "0.2.0", "adsfasdf")
err := dispatch.pluginMain(funcs, versionInfo, "") err := dispatch.pluginMain(cmdAdd.Func, cmdCheck.Func, cmdDel.Func, versionInfo, "")
Expect(err.Code).To(Equal(types.ErrDecodingFailure)) Expect(err.Code).To(Equal(uint(100)))
Expect(cmdAdd.CallCount).To(Equal(0)) Expect(cmdAdd.CallCount).To(Equal(0))
Expect(cmdCheck.CallCount).To(Equal(0)) Expect(cmdCheck.CallCount).To(Equal(0))
Expect(cmdDel.CallCount).To(Equal(0)) Expect(cmdDel.CallCount).To(Equal(0))
@ -405,132 +292,13 @@ var _ = Describe("dispatching to the correct callback", func() {
}) })
}) })
Context("when the CNI_COMMAND is GC", func() {
BeforeEach(func() {
environment["CNI_COMMAND"] = "GC"
delete(environment, "CNI_NETNS")
delete(environment, "CNI_IFNAME")
delete(environment, "CNI_CONTAINERID")
delete(environment, "CNI_ARGS")
expectedCmdArgs = &CmdArgs{
Path: "/some/cni/path",
StdinData: []byte(stdinData),
}
dispatch = &dispatcher{
Getenv: func(key string) string {
return environment[key]
},
Stdin: strings.NewReader(stdinData),
Stdout: stdout,
Stderr: stderr,
}
})
It("extracts env vars and stdin data and calls cmdGC", func() {
err := dispatch.pluginMain(funcs, versionInfo, "")
Expect(err).NotTo(HaveOccurred())
Expect(cmdAdd.CallCount).To(Equal(0))
Expect(cmdCheck.CallCount).To(Equal(0))
Expect(cmdDel.CallCount).To(Equal(0))
Expect(cmdGC.CallCount).To(Equal(1))
Expect(cmdGC.Received.CmdArgs).To(Equal(expectedCmdArgs))
})
It("does not call cmdAdd or cmdDel", func() {
err := dispatch.pluginMain(funcs, versionInfo, "")
Expect(err).NotTo(HaveOccurred())
Expect(cmdAdd.CallCount).To(Equal(0))
Expect(cmdDel.CallCount).To(Equal(0))
})
DescribeTable("required / optional env vars", envVarChecker,
Entry("command", "CNI_COMMAND", true),
Entry("container id", "CNI_CONTAINERID", false),
Entry("net ns", "CNI_NETNS", false),
Entry("if name", "CNI_IFNAME", false),
Entry("args", "CNI_ARGS", false),
Entry("path", "CNI_PATH", true),
)
Context("when multiple required env vars are missing", func() {
BeforeEach(func() {
delete(environment, "CNI_PATH")
})
It("reports that all of them are missing, not just the first", func() {
err := dispatch.pluginMain(funcs, versionInfo, "")
Expect(err).To(HaveOccurred())
Expect(err).To(Equal(&types.Error{
Code: types.ErrInvalidEnvironmentVariables,
Msg: "required env variables [CNI_PATH] missing",
}))
})
})
Context("when cniVersion is less than 1.1.0", func() {
It("immediately returns a useful error", func() {
dispatch.Stdin = strings.NewReader(`{ "name": "skel-test", "cniVersion": "0.3.0", "some": "config" }`)
err := dispatch.pluginMain(funcs, versionInfo, "")
Expect(err.Code).To(Equal(types.ErrIncompatibleCNIVersion)) // see https://github.com/containernetworking/cni/blob/main/SPEC.md#well-known-error-codes
Expect(err.Msg).To(Equal("config version does not allow GC"))
Expect(cmdGC.CallCount).To(Equal(0))
})
})
Context("when plugin does not support 1.1.0", func() {
It("immediately returns a useful error", func() {
dispatch.Stdin = strings.NewReader(`{ "name": "skel-test", "cniVersion": "1.1.0", "some": "config" }`)
versionInfo = version.PluginSupports("0.1.0", "0.2.0", "0.3.0")
err := dispatch.pluginMain(funcs, versionInfo, "")
Expect(err.Code).To(Equal(types.ErrIncompatibleCNIVersion)) // see https://github.com/containernetworking/cni/blob/main/SPEC.md#well-known-error-codes
Expect(err.Msg).To(Equal("plugin version does not allow GC"))
Expect(cmdGC.CallCount).To(Equal(0))
})
})
Context("when the config has a bad version", func() {
It("immediately returns a useful error", func() {
dispatch.Stdin = strings.NewReader(`{ "cniVersion": "adsfsadf", "some": "config", "name": "test" }`)
versionInfo = version.PluginSupports("1.1.0")
err := dispatch.pluginMain(funcs, versionInfo, "")
Expect(err.Code).To(Equal(types.ErrDecodingFailure))
Expect(cmdGC.CallCount).To(Equal(0))
})
})
Context("when the config has a bad name", func() {
It("immediately returns invalid network config", func() {
dispatch.Stdin = strings.NewReader(`{ "cniVersion": "0.4.0", "some": "config", "name": "te%%st" }`)
versionInfo = version.PluginSupports("1.1.0")
err := dispatch.pluginMain(funcs, versionInfo, "")
Expect(err.Code).To(Equal(types.ErrInvalidNetworkConfig))
Expect(cmdGC.CallCount).To(Equal(0))
})
})
Context("when the plugin has a bad version", func() {
It("immediately returns a useful error", func() {
dispatch.Stdin = strings.NewReader(`{ "cniVersion": "1.1.0", "some": "config", "name": "test" }`)
versionInfo = version.PluginSupports("0.1.0", "0.2.0", "adsfasdf")
err := dispatch.pluginMain(funcs, versionInfo, "")
Expect(err.Code).To(Equal(types.ErrDecodingFailure))
Expect(cmdGC.CallCount).To(Equal(0))
})
})
})
Context("when the CNI_COMMAND is DEL", func() { Context("when the CNI_COMMAND is DEL", func() {
BeforeEach(func() { BeforeEach(func() {
environment["CNI_COMMAND"] = "DEL" environment["CNI_COMMAND"] = "DEL"
}) })
It("calls cmdDel with the env vars and stdin data", func() { It("calls cmdDel with the env vars and stdin data", func() {
err := dispatch.pluginMain(funcs, versionInfo, "") err := dispatch.pluginMain(cmdAdd.Func, cmdCheck.Func, cmdDel.Func, versionInfo, "")
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(cmdDel.CallCount).To(Equal(1)) Expect(cmdDel.CallCount).To(Equal(1))
@ -538,7 +306,7 @@ var _ = Describe("dispatching to the correct callback", func() {
}) })
It("does not call cmdAdd", func() { It("does not call cmdAdd", func() {
err := dispatch.pluginMain(funcs, versionInfo, "") err := dispatch.pluginMain(cmdAdd.Func, cmdCheck.Func, cmdDel.Func, versionInfo, "")
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(cmdAdd.CallCount).To(Equal(0)) Expect(cmdAdd.CallCount).To(Equal(0))
@ -560,17 +328,17 @@ var _ = Describe("dispatching to the correct callback", func() {
}) })
It("prints the version to stdout", func() { It("prints the version to stdout", func() {
err := dispatch.pluginMain(funcs, versionInfo, "") err := dispatch.pluginMain(cmdAdd.Func, cmdCheck.Func, cmdDel.Func, versionInfo, "")
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(stdout).To(MatchJSON(fmt.Sprintf(`{ Expect(stdout).To(MatchJSON(fmt.Sprintf(`{
"cniVersion": "%s", "cniVersion": "%s",
"supportedVersions": ["9.8.7", "10.0.0"] "supportedVersions": ["9.8.7"]
}`, version.Current()))) }`, current.ImplementedSpecVersion)))
}) })
It("does not call cmdAdd or cmdDel", func() { It("does not call cmdAdd or cmdDel", func() {
err := dispatch.pluginMain(funcs, versionInfo, "") err := dispatch.pluginMain(cmdAdd.Func, cmdCheck.Func, cmdDel.Func, versionInfo, "")
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(cmdAdd.CallCount).To(Equal(0)) Expect(cmdAdd.CallCount).To(Equal(0))
@ -590,14 +358,14 @@ var _ = Describe("dispatching to the correct callback", func() {
r := &BadReader{} r := &BadReader{}
dispatch.Stdin = r dispatch.Stdin = r
err := dispatch.pluginMain(funcs, versionInfo, "") err := dispatch.pluginMain(cmdAdd.Func, cmdCheck.Func, cmdDel.Func, versionInfo, "")
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(r.ReadCount).To(Equal(0)) Expect(r.ReadCount).To(Equal(0))
Expect(stdout).To(MatchJSON(fmt.Sprintf(`{ Expect(stdout).To(MatchJSON(fmt.Sprintf(`{
"cniVersion": "%s", "cniVersion": "%s",
"supportedVersions": ["9.8.7", "10.0.0"] "supportedVersions": ["9.8.7"]
}`, version.Current()))) }`, current.ImplementedSpecVersion)))
}) })
}) })
@ -607,25 +375,24 @@ var _ = Describe("dispatching to the correct callback", func() {
}) })
It("does not call any cmd callback", func() { It("does not call any cmd callback", func() {
err := dispatch.pluginMain(funcs, versionInfo, "") dispatch.pluginMain(cmdAdd.Func, cmdCheck.Func, cmdDel.Func, versionInfo, "")
Expect(err).To(HaveOccurred())
Expect(cmdAdd.CallCount).To(Equal(0)) Expect(cmdAdd.CallCount).To(Equal(0))
Expect(cmdDel.CallCount).To(Equal(0)) Expect(cmdDel.CallCount).To(Equal(0))
}) })
It("returns an error", func() { It("returns an error", func() {
err := dispatch.pluginMain(funcs, versionInfo, "") err := dispatch.pluginMain(cmdAdd.Func, cmdCheck.Func, cmdDel.Func, versionInfo, "")
Expect(err).To(Equal(&types.Error{ Expect(err).To(Equal(&types.Error{
Code: types.ErrInvalidEnvironmentVariables, Code: 100,
Msg: "unknown CNI_COMMAND: NOPE", Msg: "unknown CNI_COMMAND: NOPE",
})) }))
}) })
It("prints the about string when the command is blank", func() { It("prints the about string when the command is blank", func() {
environment["CNI_COMMAND"] = "" environment["CNI_COMMAND"] = ""
err := dispatch.pluginMain(funcs, versionInfo, "test framework v42") dispatch.pluginMain(cmdAdd.Func, cmdCheck.Func, cmdDel.Func, versionInfo, "test framework v42")
Expect(err).NotTo(HaveOccurred())
Expect(stderr.String()).To(ContainSubstring("test framework v42")) Expect(stderr.String()).To(ContainSubstring("test framework v42"))
}) })
}) })
@ -633,24 +400,24 @@ var _ = Describe("dispatching to the correct callback", func() {
Context("when the CNI_COMMAND is missing", func() { Context("when the CNI_COMMAND is missing", func() {
It("prints the about string to stderr", func() { It("prints the about string to stderr", func() {
environment = map[string]string{} environment = map[string]string{}
err := dispatch.pluginMain(funcs, versionInfo, "AWESOME PLUGIN") err := dispatch.pluginMain(cmdAdd.Func, cmdCheck.Func, cmdDel.Func, versionInfo, "AWESOME PLUGIN")
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(cmdAdd.CallCount).To(Equal(0)) Expect(cmdAdd.CallCount).To(Equal(0))
Expect(cmdDel.CallCount).To(Equal(0)) Expect(cmdDel.CallCount).To(Equal(0))
log := stderr.String() log := stderr.String()
Expect(log).To(Equal("AWESOME PLUGIN\nCNI protocol versions supported: 9.8.7, 10.0.0\n")) Expect(log).To(Equal("AWESOME PLUGIN\n"))
}) })
It("fails if there is no about string", func() { It("fails if there is no about string", func() {
environment = map[string]string{} environment = map[string]string{}
err := dispatch.pluginMain(funcs, versionInfo, "") err := dispatch.pluginMain(cmdAdd.Func, cmdCheck.Func, cmdDel.Func, versionInfo, "")
Expect(err).To(HaveOccurred()) Expect(err).To(HaveOccurred())
Expect(cmdAdd.CallCount).To(Equal(0)) Expect(cmdAdd.CallCount).To(Equal(0))
Expect(cmdDel.CallCount).To(Equal(0)) Expect(cmdDel.CallCount).To(Equal(0))
Expect(err).To(Equal(&types.Error{ Expect(err).To(Equal(&types.Error{
Code: types.ErrInvalidEnvironmentVariables, Code: 100,
Msg: "required env variables [CNI_COMMAND] missing", Msg: "required env variables [CNI_COMMAND] missing",
})) }))
}) })
@ -662,17 +429,17 @@ var _ = Describe("dispatching to the correct callback", func() {
}) })
It("does not call any cmd callback", func() { It("does not call any cmd callback", func() {
err := dispatch.pluginMain(funcs, versionInfo, "") dispatch.pluginMain(cmdAdd.Func, cmdCheck.Func, cmdDel.Func, versionInfo, "")
Expect(err).To(HaveOccurred())
Expect(cmdAdd.CallCount).To(Equal(0)) Expect(cmdAdd.CallCount).To(Equal(0))
Expect(cmdDel.CallCount).To(Equal(0)) Expect(cmdDel.CallCount).To(Equal(0))
}) })
It("wraps and returns the error", func() { It("wraps and returns the error", func() {
err := dispatch.pluginMain(funcs, versionInfo, "") err := dispatch.pluginMain(cmdAdd.Func, cmdCheck.Func, cmdDel.Func, versionInfo, "")
Expect(err).To(Equal(&types.Error{ Expect(err).To(Equal(&types.Error{
Code: types.ErrIOFailure, Code: 100,
Msg: "error reading from stdin: banana", Msg: "error reading from stdin: banana",
})) }))
}) })
@ -688,7 +455,7 @@ var _ = Describe("dispatching to the correct callback", func() {
}) })
It("returns the error as-is", func() { It("returns the error as-is", func() {
err := dispatch.pluginMain(funcs, versionInfo, "") err := dispatch.pluginMain(cmdAdd.Func, cmdCheck.Func, cmdDel.Func, versionInfo, "")
Expect(err).To(Equal(&types.Error{ Expect(err).To(Equal(&types.Error{
Code: 1234, Code: 1234,
@ -703,10 +470,10 @@ var _ = Describe("dispatching to the correct callback", func() {
}) })
It("wraps and returns the error", func() { It("wraps and returns the error", func() {
err := dispatch.pluginMain(funcs, versionInfo, "") err := dispatch.pluginMain(cmdAdd.Func, cmdCheck.Func, cmdDel.Func, versionInfo, "")
Expect(err).To(Equal(&types.Error{ Expect(err).To(Equal(&types.Error{
Code: types.ErrInternal, Code: 100,
Msg: "potato", Msg: "potato",
})) }))
}) })

View File

@ -22,47 +22,25 @@ import (
"os" "os"
"github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types"
convert "github.com/containernetworking/cni/pkg/types/internal"
) )
const ImplementedSpecVersion string = "0.2.0" 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 // 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) { func NewResult(data []byte) (types.Result, error) {
result := &Result{} result := &Result{}
if err := json.Unmarshal(data, result); err != nil { if err := json.Unmarshal(data, result); err != nil {
return nil, err return nil, err
} }
for _, v := range supportedVersions { return result, nil
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) { func GetResult(r types.Result) (*Result, error) {
result020, err := convert.Convert(r, ImplementedSpecVersion) // We expect version 0.1.0/0.2.0 results
result020, err := r.GetAsVersion(ImplementedSpecVersion)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -73,32 +51,6 @@ func GetResult(r types.Result) (*Result, error) {
return result, nil 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 // Result is what gets returned from the plugin (via stdout) to the caller
type Result struct { type Result struct {
CNIVersion string `json:"cniVersion,omitempty"` CNIVersion string `json:"cniVersion,omitempty"`
@ -108,16 +60,17 @@ type Result struct {
} }
func (r *Result) Version() string { func (r *Result) Version() string {
return r.CNIVersion return ImplementedSpecVersion
} }
func (r *Result) GetAsVersion(version string) (types.Result, error) { func (r *Result) GetAsVersion(version string) (types.Result, error) {
// If the creator of the result did not set the CNIVersion, assume it for _, supportedVersion := range SupportedVersions {
// should be the highest spec version implemented by this Result if version == supportedVersion {
if r.CNIVersion == "" { r.CNIVersion = version
r.CNIVersion = ImplementedSpecVersion return r, nil
}
} }
return convert.Convert(r, version) return nil, fmt.Errorf("cannot convert version %q to %s", SupportedVersions, version)
} }
func (r *Result) Print() error { func (r *Result) Print() error {
@ -133,6 +86,20 @@ func (r *Result) PrintTo(writer io.Writer) error {
return err return err
} }
// String returns a formatted string in the form of "[IP4: $1,][ IP6: $2,] DNS: $3" where
// $1 represents the receiver's IPv4, $2 represents the receiver's IPv6 and $3 the
// receiver's DNS. If $1 or $2 are nil, they won't be present in the returned string.
func (r *Result) String() string {
var str string
if r.IP4 != nil {
str = fmt.Sprintf("IP4:%+v, ", *r.IP4)
}
if r.IP6 != nil {
str += fmt.Sprintf("IP6:%+v, ", *r.IP6)
}
return fmt.Sprintf("%sDNS:%+v", str, r.DNS)
}
// IPConfig contains values necessary to configure an interface // IPConfig contains values necessary to configure an interface
type IPConfig struct { type IPConfig struct {
IP net.IPNet IP net.IPNet
@ -140,22 +107,6 @@ type IPConfig struct {
Routes []types.Route 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 // net.IPNet is not JSON (un)marshallable so this duality is needed
// for our custom IPNet type // for our custom IPNet type

View File

@ -15,10 +15,10 @@
package types020_test package types020_test
import ( import (
"testing" . "github.com/onsi/ginkgo"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"testing"
) )
func TestTypes010(t *testing.T) { func TestTypes010(t *testing.T) {

View File

@ -15,64 +15,81 @@
package types020_test package types020_test
import ( import (
"encoding/json" "io/ioutil"
"fmt"
"net" "net"
"os"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/types/020" "github.com/containernetworking/cni/pkg/types/020"
"github.com/containernetworking/cni/pkg/types/create"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
) )
func testResult(resultCNIVersion, jsonCNIVersion string) (*types020.Result, string) { var _ = Describe("Ensures compatibility with the 0.1.0/0.2.0 spec", func() {
ipv4, err := types.ParseCIDR("1.2.3.30/24") It("correctly encodes a 0.1.0/0.2.0 Result", func() {
Expect(err).NotTo(HaveOccurred()) ipv4, err := types.ParseCIDR("1.2.3.30/24")
Expect(ipv4).NotTo(BeNil()) Expect(err).NotTo(HaveOccurred())
Expect(ipv4).NotTo(BeNil())
routegwv4, routev4, err := net.ParseCIDR("15.5.6.8/24") routegwv4, routev4, err := net.ParseCIDR("15.5.6.8/24")
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(routev4).NotTo(BeNil()) Expect(routev4).NotTo(BeNil())
Expect(routegwv4).NotTo(BeNil()) Expect(routegwv4).NotTo(BeNil())
ipv6, err := types.ParseCIDR("abcd:1234:ffff::cdde/64") ipv6, err := types.ParseCIDR("abcd:1234:ffff::cdde/64")
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(ipv6).NotTo(BeNil()) Expect(ipv6).NotTo(BeNil())
routegwv6, routev6, err := net.ParseCIDR("1111:dddd::aaaa/80") routegwv6, routev6, err := net.ParseCIDR("1111:dddd::aaaa/80")
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(routev6).NotTo(BeNil()) Expect(routev6).NotTo(BeNil())
Expect(routegwv6).NotTo(BeNil()) Expect(routegwv6).NotTo(BeNil())
// Set every field of the struct to ensure source compatibility // Set every field of the struct to ensure source compatibility
res := &types020.Result{ res := types020.Result{
CNIVersion: resultCNIVersion, CNIVersion: types020.ImplementedSpecVersion,
IP4: &types020.IPConfig{ IP4: &types020.IPConfig{
IP: *ipv4, IP: *ipv4,
Gateway: net.ParseIP("1.2.3.1"), Gateway: net.ParseIP("1.2.3.1"),
Routes: []types.Route{ Routes: []types.Route{
{Dst: *routev4, GW: routegwv4}, {Dst: *routev4, GW: routegwv4},
},
}, },
}, IP6: &types020.IPConfig{
IP6: &types020.IPConfig{ IP: *ipv6,
IP: *ipv6, Gateway: net.ParseIP("abcd:1234:ffff::1"),
Gateway: net.ParseIP("abcd:1234:ffff::1"), Routes: []types.Route{
Routes: []types.Route{ {Dst: *routev6, GW: routegwv6},
{Dst: *routev6, GW: routegwv6}, },
}, },
}, DNS: types.DNS{
DNS: types.DNS{ Nameservers: []string{"1.2.3.4", "1::cafe"},
Nameservers: []string{"1.2.3.4", "1::cafe"}, Domain: "acompany.com",
Domain: "acompany.com", Search: []string{"somedomain.com", "otherdomain.net"},
Search: []string{"somedomain.com", "otherdomain.net"}, Options: []string{"foo", "bar"},
Options: []string{"foo", "bar"}, },
}, }
}
json := fmt.Sprintf(`{ Expect(res.String()).To(Equal("IP4:{IP:{IP:1.2.3.30 Mask:ffffff00} Gateway:1.2.3.1 Routes:[{Dst:{IP:15.5.6.0 Mask:ffffff00} GW:15.5.6.8}]}, IP6:{IP:{IP:abcd:1234:ffff::cdde Mask:ffffffffffffffff0000000000000000} Gateway:abcd:1234:ffff::1 Routes:[{Dst:{IP:1111:dddd:: Mask:ffffffffffffffffffff000000000000} GW:1111:dddd::aaaa}]}, DNS:{Nameservers:[1.2.3.4 1::cafe] Domain:acompany.com Search:[somedomain.com otherdomain.net] Options:[foo bar]}"))
"cniVersion": "%s",
// Redirect stdout to capture JSON result
oldStdout := os.Stdout
r, w, err := os.Pipe()
Expect(err).NotTo(HaveOccurred())
os.Stdout = w
err = res.Print()
w.Close()
Expect(err).NotTo(HaveOccurred())
// parse the result
out, err := ioutil.ReadAll(r)
os.Stdout = oldStdout
Expect(err).NotTo(HaveOccurred())
Expect(string(out)).To(Equal(`{
"cniVersion": "0.2.0",
"ip4": { "ip4": {
"ip": "1.2.3.30/24", "ip": "1.2.3.30/24",
"gateway": "1.2.3.1", "gateway": "1.2.3.1",
@ -108,50 +125,6 @@ func testResult(resultCNIVersion, jsonCNIVersion string) (*types020.Result, stri
"bar" "bar"
] ]
} }
}`, jsonCNIVersion) }`))
return res, json
}
var _ = Describe("Ensures compatibility with the 0.1.0/0.2.0 spec", func() {
It("correctly encodes a 0.2.0 Result", func() {
res, expectedJSON := testResult(types020.ImplementedSpecVersion, types020.ImplementedSpecVersion)
out, err := json.Marshal(res)
Expect(err).NotTo(HaveOccurred())
Expect(out).To(MatchJSON(expectedJSON))
})
It("correctly encodes a 0.1.0 Result", func() {
res, expectedJSON := testResult("0.1.0", "0.1.0")
out, err := json.Marshal(res)
Expect(err).NotTo(HaveOccurred())
Expect(out).To(MatchJSON(expectedJSON))
})
It("converts a 0.2.0 result to 0.1.0", func() {
res, expectedJSON := testResult(types020.ImplementedSpecVersion, "0.1.0")
res010, err := res.GetAsVersion("0.1.0")
Expect(err).NotTo(HaveOccurred())
out, err := json.Marshal(res010)
Expect(err).NotTo(HaveOccurred())
Expect(out).To(MatchJSON(expectedJSON))
})
It("converts a 0.1.0 result to 0.2.0", func() {
res, expectedJSON := testResult("0.1.0", types020.ImplementedSpecVersion)
res020, err := res.GetAsVersion(types020.ImplementedSpecVersion)
Expect(err).NotTo(HaveOccurred())
out, err := json.Marshal(res020)
Expect(err).NotTo(HaveOccurred())
Expect(out).To(MatchJSON(expectedJSON))
})
It("creates a 0.1.0 result passing CNIVersion ''", func() {
_, expectedJSON := testResult("", "")
resT, err := create.Create("", []byte(expectedJSON))
Expect(err).NotTo(HaveOccurred())
res010, ok := resT.(*types020.Result)
Expect(ok).To(BeTrue())
Expect(res010.CNIVersion).To(Equal("0.1.0"))
}) })
}) })

View File

@ -1,306 +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 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
}

View File

@ -1,352 +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 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
}

View File

@ -1,27 +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 types100_test
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestTypesCurrent(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Current Types Suite")
}

View File

@ -1,345 +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 types100_test
import (
"encoding/json"
"io"
"net"
"os"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/containernetworking/cni/pkg/types"
current "github.com/containernetworking/cni/pkg/types/100"
)
func testResult() *current.Result {
ipv4, err := types.ParseCIDR("1.2.3.30/24")
Expect(err).NotTo(HaveOccurred())
Expect(ipv4).NotTo(BeNil())
routegwv4, routev4, err := net.ParseCIDR("15.5.6.8/24")
Expect(err).NotTo(HaveOccurred())
Expect(routev4).NotTo(BeNil())
Expect(routegwv4).NotTo(BeNil())
ipv6, err := types.ParseCIDR("abcd:1234:ffff::cdde/64")
Expect(err).NotTo(HaveOccurred())
Expect(ipv6).NotTo(BeNil())
routegwv6, routev6, err := net.ParseCIDR("1111:dddd::aaaa/80")
Expect(err).NotTo(HaveOccurred())
Expect(routev6).NotTo(BeNil())
Expect(routegwv6).NotTo(BeNil())
// Set every field of the struct to ensure source compatibility
return &current.Result{
CNIVersion: current.ImplementedSpecVersion,
Interfaces: []*current.Interface{
{
Name: "eth0",
Mac: "00:11:22:33:44:55",
Mtu: 1500,
Sandbox: "/proc/3553/ns/net",
PciID: "8086:9a01",
SocketPath: "/path/to/vhost/fd",
},
},
IPs: []*current.IPConfig{
{
Interface: current.Int(0),
Address: *ipv4,
Gateway: net.ParseIP("1.2.3.1"),
},
{
Interface: current.Int(0),
Address: *ipv6,
Gateway: net.ParseIP("abcd:1234:ffff::1"),
},
},
Routes: []*types.Route{
{Dst: *routev4, GW: routegwv4},
{Dst: *routev6, GW: routegwv6},
},
DNS: types.DNS{
Nameservers: []string{"1.2.3.4", "1::cafe"},
Domain: "acompany.com",
Search: []string{"somedomain.com", "otherdomain.net"},
Options: []string{"foo", "bar"},
},
}
}
var _ = Describe("Current types operations", func() {
It("correctly encodes a 1.1.0 Result", func() {
res := testResult()
// Redirect stdout to capture JSON result
oldStdout := os.Stdout
r, w, err := os.Pipe()
Expect(err).NotTo(HaveOccurred())
os.Stdout = w
err = res.Print()
w.Close()
Expect(err).NotTo(HaveOccurred())
// parse the result
out, err := io.ReadAll(r)
os.Stdout = oldStdout
Expect(err).NotTo(HaveOccurred())
Expect(string(out)).To(MatchJSON(`{
"cniVersion": "` + current.ImplementedSpecVersion + `",
"interfaces": [
{
"name": "eth0",
"mac": "00:11:22:33:44:55",
"mtu": 1500,
"sandbox": "/proc/3553/ns/net",
"pciID": "8086:9a01",
"socketPath": "/path/to/vhost/fd"
}
],
"ips": [
{
"interface": 0,
"address": "1.2.3.30/24",
"gateway": "1.2.3.1"
},
{
"interface": 0,
"address": "abcd:1234:ffff::cdde/64",
"gateway": "abcd:1234:ffff::1"
}
],
"routes": [
{
"dst": "15.5.6.0/24",
"gw": "15.5.6.8"
},
{
"dst": "1111:dddd::/80",
"gw": "1111:dddd::aaaa"
}
],
"dns": {
"nameservers": [
"1.2.3.4",
"1::cafe"
],
"domain": "acompany.com",
"search": [
"somedomain.com",
"otherdomain.net"
],
"options": [
"foo",
"bar"
]
}
}`))
})
It("correctly converts a 1.0.0 Result to 1.1.0", func() {
tr, err := testResult().GetAsVersion("1.0.0")
Expect(err).NotTo(HaveOccurred())
trv1, ok := tr.(*current.Result)
Expect(ok).To(BeTrue())
// 1.0.0 and 1.1.0 should be the same except for CNI version
Expect(trv1.CNIVersion).To(Equal("1.0.0"))
// If we convert 1.0.0 back to 1.1.0 it should be identical
trv11, err := trv1.GetAsVersion("1.1.0")
Expect(err).NotTo(HaveOccurred())
Expect(trv11).To(Equal(testResult()))
})
It("correctly encodes a 0.1.0 Result", func() {
res, err := testResult().GetAsVersion("0.1.0")
Expect(err).NotTo(HaveOccurred())
// Redirect stdout to capture JSON result
oldStdout := os.Stdout
r, w, err := os.Pipe()
Expect(err).NotTo(HaveOccurred())
os.Stdout = w
err = res.Print()
w.Close()
Expect(err).NotTo(HaveOccurred())
// parse the result
out, err := io.ReadAll(r)
os.Stdout = oldStdout
Expect(err).NotTo(HaveOccurred())
Expect(string(out)).To(MatchJSON(`{
"cniVersion": "0.1.0",
"ip4": {
"ip": "1.2.3.30/24",
"gateway": "1.2.3.1",
"routes": [
{
"dst": "15.5.6.0/24",
"gw": "15.5.6.8"
}
]
},
"ip6": {
"ip": "abcd:1234:ffff::cdde/64",
"gateway": "abcd:1234:ffff::1",
"routes": [
{
"dst": "1111:dddd::/80",
"gw": "1111:dddd::aaaa"
}
]
},
"dns": {
"nameservers": [
"1.2.3.4",
"1::cafe"
],
"domain": "acompany.com",
"search": [
"somedomain.com",
"otherdomain.net"
],
"options": [
"foo",
"bar"
]
}
}`))
})
It("correctly encodes a 0.4.0 Result", func() {
res, err := testResult().GetAsVersion("0.4.0")
Expect(err).NotTo(HaveOccurred())
// Redirect stdout to capture JSON result
oldStdout := os.Stdout
r, w, err := os.Pipe()
Expect(err).NotTo(HaveOccurred())
os.Stdout = w
err = res.Print()
w.Close()
Expect(err).NotTo(HaveOccurred())
// parse the result
out, err := io.ReadAll(r)
os.Stdout = oldStdout
Expect(err).NotTo(HaveOccurred())
Expect(string(out)).To(MatchJSON(`{
"cniVersion": "0.4.0",
"interfaces": [
{
"name": "eth0",
"mac": "00:11:22:33:44:55",
"sandbox": "/proc/3553/ns/net"
}
],
"ips": [
{
"interface": 0,
"version": "4",
"address": "1.2.3.30/24",
"gateway": "1.2.3.1"
},
{
"interface": 0,
"version": "6",
"address": "abcd:1234:ffff::cdde/64",
"gateway": "abcd:1234:ffff::1"
}
],
"routes": [
{
"dst": "15.5.6.0/24",
"gw": "15.5.6.8"
},
{
"dst": "1111:dddd::/80",
"gw": "1111:dddd::aaaa"
}
],
"dns": {
"nameservers": [
"1.2.3.4",
"1::cafe"
],
"domain": "acompany.com",
"search": [
"somedomain.com",
"otherdomain.net"
],
"options": [
"foo",
"bar"
]
}
}`))
})
It("correctly marshals and unmarshals interface index 0", func() {
ipc := &current.IPConfig{
Interface: current.Int(0),
Address: net.IPNet{
IP: net.ParseIP("10.1.2.3"),
Mask: net.IPv4Mask(255, 255, 255, 0),
},
}
jsonBytes, err := json.Marshal(ipc)
Expect(err).NotTo(HaveOccurred())
Expect(jsonBytes).To(MatchJSON(`{
"interface": 0,
"address": "10.1.2.3/24"
}`))
recovered := &current.IPConfig{}
Expect(json.Unmarshal(jsonBytes, recovered)).To(Succeed())
Expect(recovered).To(Equal(ipc))
})
Context("when unmarshalling json fails", func() {
It("returns an error", func() {
recovered := &current.IPConfig{}
err := json.Unmarshal([]byte(`{"address": 5}`), recovered)
Expect(err).To(MatchError(HavePrefix("json: cannot unmarshal")))
})
})
It("correctly marshals a missing interface index", func() {
ipc := &current.IPConfig{
Address: net.IPNet{
IP: net.ParseIP("10.1.2.3"),
Mask: net.IPv4Mask(255, 255, 255, 0),
},
}
json, err := json.Marshal(ipc)
Expect(err).NotTo(HaveOccurred())
Expect(json).To(MatchJSON(`{
"address": "10.1.2.3/24"
}`))
})
})

View File

@ -26,8 +26,8 @@ import (
type UnmarshallableBool bool type UnmarshallableBool bool
// UnmarshalText implements the encoding.TextUnmarshaler interface. // UnmarshalText implements the encoding.TextUnmarshaler interface.
// Returns boolean true if the string is "1" or "true" or "True" // Returns boolean true if the string is "1" or "[Tt]rue"
// Returns boolean false if the string is "0" or "false" or "False” // Returns boolean false if the string is "0" or "[Ff]alse"
func (b *UnmarshallableBool) UnmarshalText(data []byte) error { func (b *UnmarshallableBool) UnmarshalText(data []byte) error {
s := strings.ToLower(string(data)) s := strings.ToLower(string(data))
switch s { switch s {
@ -36,7 +36,7 @@ func (b *UnmarshallableBool) UnmarshalText(data []byte) error {
case "0", "false": case "0", "false":
*b = false *b = false
default: default:
return fmt.Errorf("boolean unmarshal error: invalid input %s", s) return fmt.Errorf("Boolean unmarshal error: invalid input %s", s)
} }
return nil return nil
} }
@ -91,26 +91,16 @@ func LoadArgs(args string, container interface{}) error {
unknownArgs = append(unknownArgs, pair) unknownArgs = append(unknownArgs, pair)
continue continue
} }
keyFieldIface := keyField.Addr().Interface()
var keyFieldInterface interface{} u, ok := keyFieldIface.(encoding.TextUnmarshaler)
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 { if !ok {
return UnmarshalableArgsError{fmt.Errorf( return UnmarshalableArgsError{fmt.Errorf(
"ARGS: cannot unmarshal into field '%s' - type '%s' does not implement encoding.TextUnmarshaler", "ARGS: cannot unmarshal into field '%s' - type '%s' does not implement encoding.TextUnmarshaler",
keyString, reflect.TypeOf(keyFieldInterface))} keyString, reflect.TypeOf(keyFieldIface))}
} }
err := u.UnmarshalText([]byte(valueString)) err := u.UnmarshalText([]byte(valueString))
if err != nil { if err != nil {
return fmt.Errorf("ARGS: error parsing value of pair %q: %w", pair, err) return fmt.Errorf("ARGS: error parsing value of pair %q: %v)", pair, err)
} }
} }

View File

@ -15,13 +15,13 @@
package types_test package types_test
import ( import (
"net"
"reflect" "reflect"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
. "github.com/containernetworking/cni/pkg/types" . "github.com/containernetworking/cni/pkg/types"
. "github.com/onsi/ginkgo"
. "github.com/onsi/ginkgo/extensions/table"
. "github.com/onsi/gomega"
) )
var _ = Describe("UnmarshallableBool UnmarshalText", func() { var _ = Describe("UnmarshallableBool UnmarshalText", func() {
@ -126,37 +126,7 @@ var _ = Describe("LoadArgs", func() {
}{} }{}
err := LoadArgs("IP=10.0.0.0/24", &conf) err := LoadArgs("IP=10.0.0.0/24", &conf)
Expect(err).To(HaveOccurred()) Expect(err).To(HaveOccurred())
})
})
Context("When loading known arguments", func() {
It("should succeed if argument is marshallable value type", func() {
conf := struct {
IP net.IP
CommonArgs
}{}
err := LoadArgs("IP=10.0.0.0", &conf)
Expect(err).NotTo(HaveOccurred())
Expect(conf.IP.String()).To(Equal("10.0.0.0"))
})
It("should succeed if argument is marshallable pointer type", func() {
conf := struct {
IP *net.IP
CommonArgs
}{}
err := LoadArgs("IP=10.0.0.0", &conf)
Expect(err).NotTo(HaveOccurred())
Expect(conf.IP.String()).To(Equal("10.0.0.0"))
})
It("should fail if argument is pointer of marshallable pointer type", func() {
conf := struct {
IP **net.IP
CommonArgs
}{}
err := LoadArgs("IP=10.0.0.0", &conf)
Expect(err).To(HaveOccurred())
}) })
}) })
}) })

View File

@ -1,59 +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 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)
}

293
pkg/types/current/types.go Normal file
View File

@ -0,0 +1,293 @@
// 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
}
// String returns a formatted string in the form of "[Interfaces: $1,][ IP: $2,] DNS: $3" where
// $1 represents the receiver's Interfaces, $2 represents the receiver's IP addresses and $3 the
// receiver's DNS. If $1 or $2 are nil, they won't be present in the returned string.
func (r *Result) String() string {
var str string
if len(r.Interfaces) > 0 {
str += fmt.Sprintf("Interfaces:%+v, ", r.Interfaces)
}
if len(r.IPs) > 0 {
str += fmt.Sprintf("IP:%+v, ", r.IPs)
}
if len(r.Routes) > 0 {
str += fmt.Sprintf("Routes:%+v, ", r.Routes)
}
return fmt.Sprintf("%sDNS:%+v", str, r.DNS)
}
// 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
}

View File

@ -12,13 +12,13 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package types040_test package current_test
import ( import (
"testing" . "github.com/onsi/ginkgo"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"testing"
) )
func TestTypesCurrent(t *testing.T) { func TestTypesCurrent(t *testing.T) {

View File

@ -12,23 +12,23 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package types040_test package current_test
import ( import (
"encoding/json" "encoding/json"
"io" "github.com/containernetworking/cni/pkg/types/020"
"io/ioutil"
"net" "net"
"os" "os"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types"
types020 "github.com/containernetworking/cni/pkg/types/020" "github.com/containernetworking/cni/pkg/types/current"
types040 "github.com/containernetworking/cni/pkg/types/040"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
) )
func testResult() *types040.Result { func testResult() *current.Result {
ipv4, err := types.ParseCIDR("1.2.3.30/24") ipv4, err := types.ParseCIDR("1.2.3.30/24")
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(ipv4).NotTo(BeNil()) Expect(ipv4).NotTo(BeNil())
@ -48,25 +48,25 @@ func testResult() *types040.Result {
Expect(routegwv6).NotTo(BeNil()) Expect(routegwv6).NotTo(BeNil())
// Set every field of the struct to ensure source compatibility // Set every field of the struct to ensure source compatibility
return &types040.Result{ return &current.Result{
CNIVersion: "0.3.1", CNIVersion: "0.3.1",
Interfaces: []*types040.Interface{ Interfaces: []*current.Interface{
{ {
Name: "eth0", Name: "eth0",
Mac: "00:11:22:33:44:55", Mac: "00:11:22:33:44:55",
Sandbox: "/proc/3553/ns/net", Sandbox: "/proc/3553/ns/net",
}, },
}, },
IPs: []*types040.IPConfig{ IPs: []*current.IPConfig{
{ {
Version: "4", Version: "4",
Interface: types040.Int(0), Interface: current.Int(0),
Address: *ipv4, Address: *ipv4,
Gateway: net.ParseIP("1.2.3.1"), Gateway: net.ParseIP("1.2.3.1"),
}, },
{ {
Version: "6", Version: "6",
Interface: types040.Int(0), Interface: current.Int(0),
Address: *ipv6, Address: *ipv6,
Gateway: net.ParseIP("abcd:1234:ffff::1"), Gateway: net.ParseIP("abcd:1234:ffff::1"),
}, },
@ -84,7 +84,7 @@ func testResult() *types040.Result {
} }
} }
var _ = Describe("040 types operations", func() { var _ = Describe("Current types operations", func() {
It("correctly encodes a 0.3.x Result", func() { It("correctly encodes a 0.3.x Result", func() {
res := testResult() res := testResult()
@ -99,7 +99,7 @@ var _ = Describe("040 types operations", func() {
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
// parse the result // parse the result
out, err := io.ReadAll(r) out, err := ioutil.ReadAll(r)
os.Stdout = oldStdout os.Stdout = oldStdout
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
@ -158,6 +158,8 @@ var _ = Describe("040 types operations", func() {
res, err := testResult().GetAsVersion("0.1.0") res, err := testResult().GetAsVersion("0.1.0")
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(res.String()).To(Equal("IP4:{IP:{IP:1.2.3.30 Mask:ffffff00} Gateway:1.2.3.1 Routes:[{Dst:{IP:15.5.6.0 Mask:ffffff00} GW:15.5.6.8}]}, IP6:{IP:{IP:abcd:1234:ffff::cdde Mask:ffffffffffffffff0000000000000000} Gateway:abcd:1234:ffff::1 Routes:[{Dst:{IP:1111:dddd:: Mask:ffffffffffffffffffff000000000000} GW:1111:dddd::aaaa}]}, DNS:{Nameservers:[1.2.3.4 1::cafe] Domain:acompany.com Search:[somedomain.com otherdomain.net] Options:[foo bar]}"))
// Redirect stdout to capture JSON result // Redirect stdout to capture JSON result
oldStdout := os.Stdout oldStdout := os.Stdout
r, w, err := os.Pipe() r, w, err := os.Pipe()
@ -169,12 +171,12 @@ var _ = Describe("040 types operations", func() {
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
// parse the result // parse the result
out, err := io.ReadAll(r) out, err := ioutil.ReadAll(r)
os.Stdout = oldStdout os.Stdout = oldStdout
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(string(out)).To(MatchJSON(`{ Expect(string(out)).To(MatchJSON(`{
"cniVersion": "0.1.0", "cniVersion": "0.2.0",
"ip4": { "ip4": {
"ip": "1.2.3.30/24", "ip": "1.2.3.30/24",
"gateway": "1.2.3.1", "gateway": "1.2.3.1",
@ -257,8 +259,10 @@ var _ = Describe("040 types operations", func() {
}, },
} }
// Convert to 040 Expect(res.String()).To(Equal("IP4:{IP:{IP:1.2.3.30 Mask:ffffff00} Gateway:1.2.3.1 Routes:[{Dst:{IP:15.5.6.0 Mask:ffffff00} GW:15.5.6.8}]}, IP6:{IP:{IP:abcd:1234:ffff::cdde Mask:ffffffffffffffff0000000000000000} Gateway:abcd:1234:ffff::1 Routes:[{Dst:{IP:1111:dddd:: Mask:ffffffffffffffffffff000000000000} GW:1111:dddd::aaaa}]}, DNS:{Nameservers:[1.2.3.4 1::cafe] Domain:acompany.com Search:[somedomain.com otherdomain.net] Options:[foo bar]}"))
newRes, err := types040.NewResultFromResult(res)
// Convert to current
newRes, err := current.NewResultFromResult(res)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
// Convert back to 0.2.0 // Convert back to 0.2.0
oldRes, err := newRes.GetAsVersion("0.2.0") oldRes, err := newRes.GetAsVersion("0.2.0")
@ -314,8 +318,10 @@ var _ = Describe("040 types operations", func() {
}, },
} }
// Convert to 040 Expect(res.String()).To(Equal("IP4:{IP:{IP:1.2.3.30 Mask:ffffff00} Gateway:1.2.3.1 Routes:[{Dst:{IP:15.5.6.0 Mask:ffffff00} GW:<nil>}]}, IP6:{IP:{IP:abcd:1234:ffff::cdde Mask:ffffffffffffffff0000000000000000} Gateway:abcd:1234:ffff::1 Routes:[{Dst:{IP:1111:dddd:: Mask:ffffffffffffffffffff000000000000} GW:<nil>}]}, DNS:{Nameservers:[1.2.3.4 1::cafe] Domain:acompany.com Search:[somedomain.com otherdomain.net] Options:[foo bar]}"))
newRes, err := types040.NewResultFromResult(res)
// Convert to current
newRes, err := current.NewResultFromResult(res)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
// Convert back to 0.2.0 // Convert back to 0.2.0
oldRes, err := newRes.GetAsVersion("0.2.0") oldRes, err := newRes.GetAsVersion("0.2.0")
@ -330,9 +336,9 @@ var _ = Describe("040 types operations", func() {
}) })
It("correctly marshals and unmarshals interface index 0", func() { It("correctly marshals and unmarshals interface index 0", func() {
ipc := &types040.IPConfig{ ipc := &current.IPConfig{
Version: "4", Version: "4",
Interface: types040.Int(0), Interface: current.Int(0),
Address: net.IPNet{ Address: net.IPNet{
IP: net.ParseIP("10.1.2.3"), IP: net.ParseIP("10.1.2.3"),
Mask: net.IPv4Mask(255, 255, 255, 0), Mask: net.IPv4Mask(255, 255, 255, 0),
@ -347,29 +353,21 @@ var _ = Describe("040 types operations", func() {
"address": "10.1.2.3/24" "address": "10.1.2.3/24"
}`)) }`))
recovered := &types040.IPConfig{} recovered := &current.IPConfig{}
Expect(json.Unmarshal(jsonBytes, recovered)).To(Succeed()) Expect(json.Unmarshal(jsonBytes, &recovered)).To(Succeed())
Expect(recovered).To(Equal(ipc)) Expect(recovered).To(Equal(ipc))
}) })
It("fails when downconverting a config to 0.2.0 that has no IPs", func() {
res := testResult()
res.IPs = nil
res.Routes = nil
_, err := res.GetAsVersion("0.2.0")
Expect(err).To(MatchError("cannot convert: no valid IP addresses"))
})
Context("when unmarshalling json fails", func() { Context("when unmarshalling json fails", func() {
It("returns an error", func() { It("returns an error", func() {
recovered := &types040.IPConfig{} recovered := &current.IPConfig{}
err := json.Unmarshal([]byte(`{"address": 5}`), recovered) err := json.Unmarshal([]byte(`{"address": 5}`), &recovered)
Expect(err).To(MatchError(HavePrefix("json: cannot unmarshal"))) Expect(err).To(MatchError(HavePrefix("json: cannot unmarshal")))
}) })
}) })
It("correctly marshals a missing interface index", func() { It("correctly marshals a missing interface index", func() {
ipc := &types040.IPConfig{ ipc := &current.IPConfig{
Version: "4", Version: "4",
Address: net.IPNet{ Address: net.IPNet{
IP: net.ParseIP("10.1.2.3"), IP: net.ParseIP("10.1.2.3"),

View File

@ -1,92 +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 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,
})
}

View File

@ -1,66 +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 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,
})
}

View File

@ -16,6 +16,7 @@ package types
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"net" "net"
@ -56,74 +57,35 @@ func (n *IPNet) UnmarshalJSON(data []byte) error {
return nil return nil
} }
// Use PluginConf instead of NetConf, the NetConf // NetConf describes a network.
// backwards-compat alias will be removed in a future release. type NetConf struct {
type NetConf = PluginConf
// PluginConf describes a plugin configuration for a specific network.
type PluginConf struct {
CNIVersion string `json:"cniVersion,omitempty"` CNIVersion string `json:"cniVersion,omitempty"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
Type string `json:"type,omitempty"` Type string `json:"type,omitempty"`
Capabilities map[string]bool `json:"capabilities,omitempty"` Capabilities map[string]bool `json:"capabilities,omitempty"`
IPAM IPAM `json:"ipam,omitempty"` IPAM IPAM `json:"ipam,omitempty"`
DNS DNS `json:"dns,omitempty"` DNS DNS `json:"dns"`
RawPrevResult map[string]interface{} `json:"prevResult,omitempty"` RawPrevResult map[string]interface{} `json:"prevResult,omitempty"`
PrevResult Result `json:"-"` 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 IPAM struct {
Type string `json:"type,omitempty"` 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. // NetConfList describes an ordered list of networks.
type NetConfList struct { type NetConfList struct {
CNIVersion string `json:"cniVersion,omitempty"` CNIVersion string `json:"cniVersion,omitempty"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
DisableCheck bool `json:"disableCheck,omitempty"` DisableCheck bool `json:"disableCheck,omitempty"`
DisableGC bool `json:"disableGC,omitempty"` Plugins []*NetConf `json:"plugins,omitempty"`
Plugins []*PluginConf `json:"plugins,omitempty"`
} }
type ResultFactoryFunc func([]byte) (Result, error)
// Result is an interface that provides the result of plugin execution // Result is an interface that provides the result of plugin execution
type Result interface { type Result interface {
// The highest CNI specification result version the result supports // The highest CNI specification result version the result supports
@ -139,6 +101,9 @@ type Result interface {
// Prints the result in JSON format to provided writer // Prints the result in JSON format to provided writer
PrintTo(writer io.Writer) error PrintTo(writer io.Writer) error
// Returns a JSON string representation of the result
String() string
} }
func PrintResult(result Result, version string) error { func PrintResult(result Result, version string) error {
@ -157,93 +122,21 @@ type DNS struct {
Options []string `json:"options,omitempty"` 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 { type Route struct {
Dst net.IPNet Dst net.IPNet
GW net.IP GW net.IP
MTU int
AdvMSS int
Priority int
Table *int
Scope *int
} }
func (r *Route) String() string { func (r *Route) String() string {
table := "<nil>" return fmt.Sprintf("%+v", *r)
if r.Table != nil {
table = fmt.Sprintf("%d", *r.Table)
}
scope := "<nil>"
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 // Well known error codes
// see https://github.com/containernetworking/cni/blob/main/SPEC.md#error // see https://github.com/containernetworking/cni/blob/master/SPEC.md#well-known-error-codes
const ( const (
ErrUnknown uint = iota // 0 ErrUnknown uint = iota // 0
ErrIncompatibleCNIVersion // 1 ErrIncompatibleCNIVersion // 1
ErrUnsupportedField // 2 ErrUnsupportedField // 2
ErrUnknownContainer // 3
ErrInvalidEnvironmentVariables // 4
ErrIOFailure // 5
ErrDecodingFailure // 6
ErrInvalidNetworkConfig // 7
ErrInvalidNetNS // 8
ErrTryAgainLater uint = 11
ErrPluginNotAvailable uint = 50
ErrLimitedConnectivity uint = 51
ErrInternal uint = 999
) )
type Error struct { type Error struct {
@ -252,14 +145,6 @@ type Error struct {
Details string `json:"details,omitempty"` Details string `json:"details,omitempty"`
} }
func NewError(code uint, msg, details string) *Error {
return &Error{
Code: code,
Msg: msg,
Details: details,
}
}
func (e *Error) Error() string { func (e *Error) Error() string {
details := "" details := ""
if e.Details != "" { if e.Details != "" {
@ -277,13 +162,8 @@ func (e *Error) Print() error {
// JSON (un)marshallable types // JSON (un)marshallable types
type route struct { type route struct {
Dst IPNet `json:"dst"` Dst IPNet `json:"dst"`
GW net.IP `json:"gw,omitempty"` 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 { func (r *Route) UnmarshalJSON(data []byte) error {
@ -294,24 +174,13 @@ func (r *Route) UnmarshalJSON(data []byte) error {
r.Dst = net.IPNet(rt.Dst) r.Dst = net.IPNet(rt.Dst)
r.GW = rt.GW 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 return nil
} }
func (r Route) MarshalJSON() ([]byte, error) { func (r Route) MarshalJSON() ([]byte, error) {
rt := route{ rt := route{
Dst: IPNet(r.Dst), Dst: IPNet(r.Dst),
GW: r.GW, GW: r.GW,
MTU: r.MTU,
AdvMSS: r.AdvMSS,
Priority: r.Priority,
Table: r.Table,
Scope: r.Scope,
} }
return json.Marshal(rt) return json.Marshal(rt)
@ -325,3 +194,6 @@ func prettyPrint(obj interface{}) error {
_, err = os.Stdout.Write(data) _, err = os.Stdout.Write(data)
return err return err
} }
// NotImplementedError is used to indicate that a method is not implemented for the given platform
var NotImplementedError = errors.New("Not Implemented")

View File

@ -15,10 +15,10 @@
package types_test package types_test
import ( import (
"testing" . "github.com/onsi/ginkgo"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"testing"
) )
func TestTypes(t *testing.T) { func TestTypes(t *testing.T) {

View File

@ -18,15 +18,14 @@ import (
"encoding/json" "encoding/json"
"net" "net"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types"
types040 "github.com/containernetworking/cni/pkg/types/040" . "github.com/onsi/ginkgo"
current "github.com/containernetworking/cni/pkg/types/100" . "github.com/onsi/ginkgo/extensions/table"
. "github.com/onsi/gomega"
) )
var _ = Describe("Types", func() { var _ = Describe("Types", func() {
Describe("ParseCIDR", func() { Describe("ParseCIDR", func() {
DescribeTable("Parse and stringify", DescribeTable("Parse and stringify",
func(input, expectedIP string, expectedMask int) { func(input, expectedIP string, expectedMask int) {
@ -88,19 +87,14 @@ var _ = Describe("Types", func() {
IP: net.ParseIP("1.2.3.0"), IP: net.ParseIP("1.2.3.0"),
Mask: net.CIDRMask(24, 32), Mask: net.CIDRMask(24, 32),
}, },
GW: net.ParseIP("1.2.3.1"), GW: net.ParseIP("1.2.3.1"),
MTU: 1500,
AdvMSS: 1340,
Priority: 100,
Table: types040.Int(50),
Scope: types040.Int(253),
} }
}) })
It("marshals and unmarshals to JSON", func() { It("marshals and unmarshals to JSON", func() {
jsonBytes, err := json.Marshal(example) jsonBytes, err := json.Marshal(example)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(jsonBytes).To(MatchJSON(`{ "dst": "1.2.3.0/24", "gw": "1.2.3.1", "mtu": 1500, "advmss": 1340, "priority": 100, "table": 50, "scope": 253 }`)) Expect(jsonBytes).To(MatchJSON(`{ "dst": "1.2.3.0/24", "gw": "1.2.3.1" }`))
var unmarshaled types.Route var unmarshaled types.Route
Expect(json.Unmarshal(jsonBytes, &unmarshaled)).To(Succeed()) Expect(json.Unmarshal(jsonBytes, &unmarshaled)).To(Succeed())
@ -116,7 +110,7 @@ var _ = Describe("Types", func() {
}) })
It("formats as a string with a hex mask", func() { It("formats as a string with a hex mask", func() {
Expect(example.String()).To(Equal(`{Dst:{IP:1.2.3.0 Mask:ffffff00} GW:1.2.3.1 MTU:1500 AdvMSS:1340 Priority:100 Table:50 Scope:253}`)) Expect(example.String()).To(Equal(`{Dst:{IP:1.2.3.0 Mask:ffffff00} GW:1.2.3.1}`))
}) })
}) })
@ -143,59 +137,5 @@ var _ = Describe("Types", func() {
}) })
}) })
}) })
It("NewError method", func() {
err := types.NewError(1234, "some message", "some details")
Expect(err).To(Equal(example))
})
})
Describe("Result conversion", func() {
var result *current.Result
BeforeEach(func() {
ipv4, err := types.ParseCIDR("1.2.3.30/24")
Expect(err).NotTo(HaveOccurred())
Expect(ipv4).NotTo(BeNil())
ipv6, err := types.ParseCIDR("abcd:1234:ffff::cdde/64")
Expect(err).NotTo(HaveOccurred())
Expect(ipv6).NotTo(BeNil())
result = &current.Result{
CNIVersion: "1.0.0",
Interfaces: []*current.Interface{
{
Name: "eth0",
Mac: "00:11:22:33:44:55",
Sandbox: "/proc/3553/ns/net",
PciID: "8086:9a01",
SocketPath: "/path/to/vhost/fd",
},
},
IPs: []*current.IPConfig{
{
Interface: current.Int(0),
Address: *ipv4,
Gateway: net.ParseIP("1.2.3.1"),
},
{
Interface: current.Int(0),
Address: *ipv6,
Gateway: net.ParseIP("abcd:1234:ffff::1"),
},
},
DNS: types.DNS{
Nameservers: []string{"1.2.3.4", "1::cafe"},
Domain: "acompany.com",
Search: []string{"somedomain.com", "otherdomain.net"},
Options: []string{"foo", "bar"},
},
}
})
It("can create a CNIVersion '' (0.1.0) result", func() {
newResult, err := result.GetAsVersion("")
Expect(err).NotTo(HaveOccurred())
Expect(newResult.Version()).To(Equal("0.1.0"))
})
}) })
}) })

View File

@ -1,82 +0,0 @@
// 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 utils
import (
"bytes"
"fmt"
"regexp"
"unicode"
"github.com/containernetworking/cni/pkg/types"
)
const (
// cniValidNameChars is the regexp used to validate valid characters in
// containerID and networkName
cniValidNameChars = `[a-zA-Z0-9][a-zA-Z0-9_.\-]`
// maxInterfaceNameLength is the length max of a valid interface name
maxInterfaceNameLength = 15
)
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", "")
}
if !cniReg.MatchString(containerID) {
return types.NewError(types.ErrInvalidEnvironmentVariables, "invalid characters in containerID", containerID)
}
return nil
}
// 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:", "")
}
if !cniReg.MatchString(networkName) {
return types.NewError(types.ErrInvalidNetworkConfig, "invalid characters found in network name", networkName)
}
return nil
}
// 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 ".."
// 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 {
return types.NewError(types.ErrInvalidEnvironmentVariables, "interface name is empty", "")
}
if len(ifName) > maxInterfaceNameLength {
return types.NewError(types.ErrInvalidEnvironmentVariables, "interface name is too long", fmt.Sprintf("interface name should be less than %d characters", maxInterfaceNameLength+1))
}
if ifName == "." || ifName == ".." {
return types.NewError(types.ErrInvalidEnvironmentVariables, "interface name is . or ..", "")
}
for _, r := range bytes.Runes([]byte(ifName)) {
if r == '/' || r == ':' || unicode.IsSpace(r) {
return types.NewError(types.ErrInvalidEnvironmentVariables, "interface name contains / or : or whitespace characters", "")
}
}
return nil
}

View File

@ -1,131 +0,0 @@
// 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 utils_test
import (
"reflect"
"testing"
"github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/utils"
)
func TestValidateContainerID(t *testing.T) {
testData := []struct {
description string
containerID string
err *types.Error
}{
{
description: "empty containerID",
containerID: "",
err: types.NewError(types.ErrUnknownContainer, "missing containerID", ""),
},
{
description: "invalid characters in containerID",
containerID: "1234%%%",
err: types.NewError(types.ErrInvalidEnvironmentVariables, "invalid characters in containerID", "1234%%%"),
},
{
description: "normal containerID",
containerID: "a51debf7e1eb",
err: nil,
},
}
for _, tt := range testData {
err := utils.ValidateContainerID(tt.containerID)
if !reflect.DeepEqual(tt.err, err) {
t.Errorf("Expected '%v' but got '%v'", tt.err, err)
}
}
}
func TestValidateNetworkName(t *testing.T) {
testData := []struct {
description string
networkName string
err *types.Error
}{
{
description: "empty networkName",
networkName: "",
err: types.NewError(types.ErrInvalidNetworkConfig, "missing network name:", ""),
},
{
description: "invalid characters in networkName",
networkName: "1234%%%",
err: types.NewError(types.ErrInvalidNetworkConfig, "invalid characters found in network name", "1234%%%"),
},
{
description: "normal networkName",
networkName: "eth0",
err: nil,
},
}
for _, tt := range testData {
err := utils.ValidateNetworkName(tt.networkName)
if !reflect.DeepEqual(tt.err, err) {
t.Errorf("Expected '%v' but got '%v'", tt.err, err)
}
}
}
func TestValidateInterfaceName(t *testing.T) {
testData := []struct {
description string
interfaceName string
err *types.Error
}{
{
description: "empty interfaceName",
interfaceName: "",
err: types.NewError(types.ErrInvalidEnvironmentVariables, "interface name is empty", ""),
},
{
description: "more than 16 characters in interfaceName",
interfaceName: "testnamemorethan16",
err: types.NewError(types.ErrInvalidEnvironmentVariables, "interface name is too long", "interface name should be less than 16 characters"),
},
{
description: "interfaceName is .",
interfaceName: ".",
err: types.NewError(types.ErrInvalidEnvironmentVariables, "interface name is . or ..", ""),
},
{
description: "interfaceName contains /",
interfaceName: "/testname",
err: types.NewError(types.ErrInvalidEnvironmentVariables, "interface name contains / or : or whitespace characters", ""),
},
{
description: "interfaceName contains whitespace characters",
interfaceName: "test name",
err: types.NewError(types.ErrInvalidEnvironmentVariables, "interface name contains / or : or whitespace characters", ""),
},
{
description: "normal interfaceName",
interfaceName: "testname",
err: nil,
},
}
for _, tt := range testData {
err := utils.ValidateInterfaceName(tt.interfaceName)
if !reflect.DeepEqual(tt.err, err) {
t.Errorf("Expected '%v' but got '%v'", tt.err, err)
}
}
}

View File

@ -15,12 +15,23 @@
package version package version
import ( import (
"github.com/containernetworking/cni/pkg/types/create" "encoding/json"
"fmt"
) )
// ConfigDecoder can decode the CNI version available in network config data // ConfigDecoder can decode the CNI version available in network config data
type ConfigDecoder struct{} type ConfigDecoder struct{}
func (*ConfigDecoder) Decode(jsonBytes []byte) (string, error) { func (*ConfigDecoder) Decode(jsonBytes []byte) (string, error) {
return create.DecodeVersion(jsonBytes) 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
} }

View File

@ -15,10 +15,10 @@
package version_test package version_test
import ( import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/containernetworking/cni/pkg/version" "github.com/containernetworking/cni/pkg/version"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
) )
var _ = Describe("Decoding the version of network config", func() { var _ = Describe("Decoding the version of network config", func() {

View File

@ -16,6 +16,7 @@ package legacy_examples
import ( import (
"fmt" "fmt"
"io/ioutil"
"os" "os"
noop_debug "github.com/containernetworking/cni/plugins/test/noop/debug" noop_debug "github.com/containernetworking/cni/plugins/test/noop/debug"
@ -107,9 +108,9 @@ func (e *ExampleRuntime) GenerateNetConf(name string) (*ExampleNetConf, error) {
return nil, fmt.Errorf("unknown example net config template %q", name) return nil, fmt.Errorf("unknown example net config template %q", name)
} }
debugFile, err := os.CreateTemp("", "cni_debug") debugFile, err := ioutil.TempFile("", "cni_debug")
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create noop plugin debug file: %w", err) return nil, fmt.Errorf("failed to create noop plugin debug file: %v", err)
} }
debugFilePath := debugFile.Name() debugFilePath := debugFile.Name()
@ -118,7 +119,7 @@ func (e *ExampleRuntime) GenerateNetConf(name string) (*ExampleNetConf, error) {
} }
if err := debug.WriteDebug(debugFilePath); err != nil { if err := debug.WriteDebug(debugFilePath); err != nil {
os.Remove(debugFilePath) os.Remove(debugFilePath)
return nil, fmt.Errorf("failed to write noop plugin debug file %q: %w", debugFilePath, err) return nil, fmt.Errorf("failed to write noop plugin debug file %q: %v", debugFilePath, err)
} }
conf := &ExampleNetConf{ conf := &ExampleNetConf{
Config: fmt.Sprintf(template.conf, debugFilePath), Config: fmt.Sprintf(template.conf, debugFilePath),
@ -143,12 +144,12 @@ var V010_Runtime = ExampleRuntime{
NetConfs: []string{"unversioned", "0.1.0"}, NetConfs: []string{"unversioned", "0.1.0"},
Example: Example{ Example: Example{
Name: "example_invoker_v010", Name: "example_invoker_v010",
CNIRepoGitRef: "c0d34c69", // version with ns.Do CNIRepoGitRef: "c0d34c69", //version with ns.Do
PluginSource: `package main PluginSource: `package main
import ( import (
"fmt" "fmt"
"io" "io/ioutil"
"os" "os"
"github.com/containernetworking/cni/libcni" "github.com/containernetworking/cni/libcni"
@ -160,7 +161,7 @@ func main(){
} }
func exec() int { func exec() int {
confBytes, err := io.ReadAll(os.Stdin) confBytes, err := ioutil.ReadAll(os.Stdin)
if err != nil { if err != nil {
fmt.Printf("could not read netconfig from stdin: %+v", err) fmt.Printf("could not read netconfig from stdin: %+v", err)
return 1 return 1

View File

@ -17,8 +17,8 @@
package legacy_examples package legacy_examples
import ( import (
"io/ioutil"
"net" "net"
"os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"sync" "sync"
@ -39,10 +39,8 @@ type Example struct {
PluginSource string PluginSource string
} }
var ( var buildDir = ""
buildDir = "" var buildDirLock sync.Mutex
buildDirLock sync.Mutex
)
func ensureBuildDirExists() error { func ensureBuildDirExists() error {
buildDirLock.Lock() buildDirLock.Lock()
@ -53,7 +51,7 @@ func ensureBuildDirExists() error {
} }
var err error var err error
buildDir, err = os.MkdirTemp("", "cni-example-plugins") buildDir, err = ioutil.TempDir("", "cni-example-plugins")
return err return err
} }
@ -122,7 +120,6 @@ func main() { skel.PluginMain(c, c) }
// As we change the CNI spec, the Result type and this value may change. // As we change the CNI spec, the Result type and this value may change.
// The text of the example plugins should not. // The text of the example plugins should not.
var ExpectedResult = &types020.Result{ var ExpectedResult = &types020.Result{
CNIVersion: "0.1.0",
IP4: &types020.IPConfig{ IP4: &types020.IPConfig{
IP: net.IPNet{ IP: net.IPNet{
IP: net.ParseIP("10.1.2.3"), IP: net.ParseIP("10.1.2.3"),
@ -130,7 +127,7 @@ var ExpectedResult = &types020.Result{
}, },
Gateway: net.ParseIP("10.1.2.1"), Gateway: net.ParseIP("10.1.2.1"),
Routes: []types.Route{ Routes: []types.Route{
{ types.Route{
Dst: net.IPNet{ Dst: net.IPNet{
IP: net.ParseIP("0.0.0.0"), IP: net.ParseIP("0.0.0.0"),
Mask: net.CIDRMask(0, 32), Mask: net.CIDRMask(0, 32),

View File

@ -15,10 +15,10 @@
package legacy_examples_test package legacy_examples_test
import ( import (
"testing" . "github.com/onsi/ginkgo"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"testing"
) )
func TestLegacyExamples(t *testing.T) { func TestLegacyExamples(t *testing.T) {

View File

@ -19,10 +19,9 @@ import (
"path/filepath" "path/filepath"
"runtime" "runtime"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/containernetworking/cni/pkg/version/legacy_examples" "github.com/containernetworking/cni/pkg/version/legacy_examples"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
) )
var _ = Describe("The v0.1.0 Example", func() { var _ = Describe("The v0.1.0 Example", func() {

View File

@ -68,7 +68,7 @@ func (*PluginDecoder) Decode(jsonBytes []byte) (PluginInfo, error) {
var info pluginInfo var info pluginInfo
err := json.Unmarshal(jsonBytes, &info) err := json.Unmarshal(jsonBytes, &info)
if err != nil { if err != nil {
return nil, fmt.Errorf("decoding version info: %w", err) return nil, fmt.Errorf("decoding version info: %s", err)
} }
if info.CNIVersion_ == "" { if info.CNIVersion_ == "" {
return nil, fmt.Errorf("decoding version info: missing field 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 // minor, and micro numbers or returns an error
func ParseVersion(version string) (int, int, int, error) { func ParseVersion(version string) (int, int, int, error) {
var major, minor, micro int var major, minor, micro int
if version == "" { // special case: no version declared == v0.1.0 if version == "" {
return 0, 1, 0, nil return -1, -1, -1, fmt.Errorf("invalid version %q: the version is empty", version)
} }
parts := strings.Split(version, ".") parts := strings.Split(version, ".")
@ -97,20 +97,20 @@ func ParseVersion(version string) (int, int, int, error) {
major, err := strconv.Atoi(parts[0]) major, err := strconv.Atoi(parts[0])
if err != nil { if err != nil {
return -1, -1, -1, fmt.Errorf("failed to convert major version part %q: %w", parts[0], err) return -1, -1, -1, fmt.Errorf("failed to convert major version part %q: %v", parts[0], err)
} }
if len(parts) >= 2 { if len(parts) >= 2 {
minor, err = strconv.Atoi(parts[1]) minor, err = strconv.Atoi(parts[1])
if err != nil { if err != nil {
return -1, -1, -1, fmt.Errorf("failed to convert minor version part %q: %w", parts[1], err) return -1, -1, -1, fmt.Errorf("failed to convert minor version part %q: %v", parts[1], err)
} }
} }
if len(parts) >= 3 { if len(parts) >= 3 {
micro, err = strconv.Atoi(parts[2]) micro, err = strconv.Atoi(parts[2])
if err != nil { if err != nil {
return -1, -1, -1, fmt.Errorf("failed to convert micro version part %q: %w", parts[2], err) return -1, -1, -1, fmt.Errorf("failed to convert micro version part %q: %v", parts[2], err)
} }
} }
@ -142,27 +142,3 @@ func GreaterThanOrEqualTo(version, otherVersion string) (bool, error) {
} }
return false, nil 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
}

View File

@ -15,10 +15,9 @@
package version_test package version_test
import ( import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/containernetworking/cni/pkg/version" "github.com/containernetworking/cni/pkg/version"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
) )
var _ = Describe("Decoding versions reported by a plugin", func() { var _ = Describe("Decoding versions reported by a plugin", func() {
@ -92,16 +91,8 @@ var _ = Describe("Decoding versions reported by a plugin", func() {
Expect(micro).To(Equal(3)) Expect(micro).To(Equal(3))
}) })
It("parses an empty string as v0.1.0", func() {
major, minor, micro, err := version.ParseVersion("")
Expect(err).NotTo(HaveOccurred())
Expect(major).To(Equal(0))
Expect(minor).To(Equal(1))
Expect(micro).To(Equal(0))
})
It("returns an error for malformed versions", func() { It("returns an error for malformed versions", func() {
badVersions := []string{"asdfasdf", "asdf.", ".asdfas", "asdf.adsf.", "0.", "..", "1.2.3.4.5"} badVersions := []string{"asdfasdf", "asdf.", ".asdfas", "asdf.adsf.", "0.", "..", "1.2.3.4.5", ""}
for _, v := range badVersions { for _, v := range badVersions {
_, _, _, err := version.ParseVersion(v) _, _, _, err := version.ParseVersion(v)
Expect(err).To(HaveOccurred()) Expect(err).To(HaveOccurred())
@ -121,19 +112,19 @@ var _ = Describe("Decoding versions reported by a plugin", func() {
// Make sure the first is greater than the second // Make sure the first is greater than the second
gt, err := version.GreaterThanOrEqualTo(v[0], v[1]) gt, err := version.GreaterThanOrEqualTo(v[0], v[1])
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(gt).To(BeTrue()) Expect(gt).To(Equal(true))
// And the opposite // And the opposite
gt, err = version.GreaterThanOrEqualTo(v[1], v[0]) gt, err = version.GreaterThanOrEqualTo(v[1], v[0])
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(gt).To(BeFalse()) Expect(gt).To(Equal(false))
} }
}) })
It("returns true when versions are the same", func() { It("returns true when versions are the same", func() {
gt, err := version.GreaterThanOrEqualTo("1.2.3", "1.2.3") gt, err := version.GreaterThanOrEqualTo("1.2.3", "1.2.3")
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(gt).To(BeTrue()) Expect(gt).To(Equal(true))
}) })
It("returns an error for malformed versions", func() { It("returns an error for malformed versions", func() {

View File

@ -15,10 +15,9 @@
package version_test package version_test
import ( import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/containernetworking/cni/pkg/version" "github.com/containernetworking/cni/pkg/version"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
) )
var _ = Describe("Reconcile versions of net config with versions supported by plugins", func() { var _ = Describe("Reconcile versions of net config with versions supported by plugins", func() {

View File

@ -22,12 +22,17 @@ package testhelpers
import ( import (
"fmt" "fmt"
"io/ioutil"
"math/rand"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
"time"
) )
const packageBaseName = "github.com/containernetworking/cni"
func run(cmd *exec.Cmd) error { func run(cmd *exec.Cmd) error {
out, err := cmd.CombinedOutput() out, err := cmd.CombinedOutput()
if err != nil { if err != nil {
@ -37,74 +42,114 @@ func run(cmd *exec.Cmd) error {
return nil return nil
} }
// unset GOPATH if it's set, so we use modules func goBuildEnviron(gopath string) []string {
func goBuildEnviron() []string { environ := os.Environ()
out := []string{} for i, kvp := range environ {
for _, kvp := range os.Environ() { if strings.HasPrefix(kvp, "GOPATH=") {
if !strings.HasPrefix(kvp, "GOPATH=") { environ[i] = "GOPATH=" + gopath
out = append(out, kvp) return environ
} }
} }
return out environ = append(environ, "GOPATH="+gopath)
return environ
} }
func buildGoProgram(modPath, outputFilePath string) error { func buildGoProgram(gopath, packageName, outputFilePath string) error {
cmd := exec.Command("go", "build", "-o", outputFilePath, ".") cmd := exec.Command("go", "build", "-o", outputFilePath, packageName)
cmd.Dir = modPath cmd.Env = goBuildEnviron(gopath)
cmd.Env = goBuildEnviron()
return run(cmd) return run(cmd)
} }
func modInit(path, name string) error { func createSingleFilePackage(gopath, packageName string, fileContents []byte) error {
cmd := exec.Command("go", "mod", "init", name) dirName := filepath.Join(gopath, "src", packageName)
cmd.Dir = path err := os.MkdirAll(dirName, 0700)
return run(cmd) if err != nil {
return err
}
return ioutil.WriteFile(filepath.Join(dirName, "main.go"), fileContents, 0600)
} }
// addLibcni will execute `go mod edit -replace` to fix libcni at a specified version func removePackage(gopath, packageName string) error {
func addLibcni(path, gitRef string) error { dirName := filepath.Join(gopath, "src", packageName)
cmd := exec.Command("go", "mod", "edit", "-replace=github.com/containernetworking/cni=github.com/containernetworking/cni@"+gitRef) return os.RemoveAll(dirName)
cmd.Dir = path
return run(cmd)
} }
// modTidy will execute `go mod tidy` to ensure all necessary dependencies func isRepoRoot(path string) bool {
func modTidy(path string) error { _, err := ioutil.ReadDir(filepath.Join(path, ".git"))
cmd := exec.Command("go", "mod", "tidy") return (err == nil) && (filepath.Base(path) == "cni")
cmd.Dir = path }
return run(cmd)
func LocateCurrentGitRepo() (string, error) {
dir, err := os.Getwd()
if err != nil {
return "", err
}
for i := 0; i < 5; i++ {
if isRepoRoot(dir) {
return dir, nil
}
dir, err = filepath.Abs(filepath.Dir(dir))
if err != nil {
return "", fmt.Errorf("abs(dir(%q)): %s", dir, err)
}
}
return "", fmt.Errorf("unable to find cni repo root, landed at %q", dir)
}
func gitCloneThisRepo(cloneDestination string) error {
err := os.MkdirAll(cloneDestination, 0700)
if err != nil {
return err
}
currentGitRepo, err := LocateCurrentGitRepo()
if err != nil {
return err
}
return run(exec.Command("git", "clone", currentGitRepo, cloneDestination))
}
func gitCheckout(localRepo string, gitRef string) error {
return run(exec.Command("git", "-C", localRepo, "checkout", gitRef))
} }
// BuildAt builds the go programSource using the version of the CNI library // BuildAt builds the go programSource using the version of the CNI library
// at gitRef, and saves the resulting binary file at outputFilePath // at gitRef, and saves the resulting binary file at outputFilePath
func BuildAt(programSource []byte, gitRef string, outputFilePath string) error { func BuildAt(programSource []byte, gitRef string, outputFilePath string) error {
tempDir, err := os.MkdirTemp(os.Getenv("GOTMPDIR"), "cni-test-") tempGoPath, err := ioutil.TempDir("", "cni-git-")
if err != nil { if err != nil {
return err return err
} }
defer os.RemoveAll(tempDir) defer os.RemoveAll(tempGoPath)
modName := filepath.Base(tempDir) cloneDestination := filepath.Join(tempGoPath, "src", packageBaseName)
err = gitCloneThisRepo(cloneDestination)
if err := modInit(tempDir, modName); err != nil {
return err
}
if err := addLibcni(tempDir, gitRef); err != nil {
return err
}
if err := os.WriteFile(filepath.Join(tempDir, "main.go"), programSource, 0o600); err != nil {
return err
}
if err := modTidy(tempDir); err != nil {
return err
}
err = buildGoProgram(tempDir, outputFilePath)
if err != nil { if err != nil {
return fmt.Errorf("failed to build: %w", err) return err
}
err = gitCheckout(cloneDestination, gitRef)
if err != nil {
return err
}
rand.Seed(time.Now().UnixNano())
testPackageName := fmt.Sprintf("test-package-%x", rand.Int31())
err = createSingleFilePackage(tempGoPath, testPackageName, programSource)
if err != nil {
return err
}
defer removePackage(tempGoPath, testPackageName)
err = buildGoProgram(tempGoPath, testPackageName, outputFilePath)
if err != nil {
return err
} }
return nil return nil

View File

@ -15,10 +15,10 @@
package testhelpers_test package testhelpers_test
import ( import (
"testing" . "github.com/onsi/ginkgo"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"testing"
) )
func TestTesthelpers(t *testing.T) { func TestTesthelpers(t *testing.T) {

View File

@ -4,22 +4,24 @@
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
// You may obtain a copy of the License at // You may obtain a copy of the License at
// //
// http://www.apache.org/licenses/LICENSE-2.0 // http://www.apache.org/licenses/LICENSE-2.0
// //
// Unless required by applicable law or agreed to in writing, software // Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, // distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package testhelpers package testhelpers_test
import ( import (
"io/ioutil"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"runtime" "runtime"
. "github.com/onsi/ginkgo/v2" "github.com/containernetworking/cni/pkg/version/testhelpers"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
) )
@ -42,7 +44,7 @@ func main() { skel.PluginMain(c, c) }
gitRef = "f4364185253" gitRef = "f4364185253"
var err error var err error
outputDir, err = os.MkdirTemp("", "bin") outputDir, err = ioutil.TempDir("", "bin")
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
outputFilePath = filepath.Join(outputDir, "some-binary") outputFilePath = filepath.Join(outputDir, "some-binary")
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
@ -57,7 +59,7 @@ func main() { skel.PluginMain(c, c) }
It("builds the provided source code using the CNI library at the given git ref", func() { It("builds the provided source code using the CNI library at the given git ref", func() {
Expect(outputFilePath).NotTo(BeAnExistingFile()) Expect(outputFilePath).NotTo(BeAnExistingFile())
err := BuildAt(programSource, gitRef, outputFilePath) err := testhelpers.BuildAt(programSource, gitRef, outputFilePath)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(outputFilePath).To(BeAnExistingFile()) Expect(outputFilePath).To(BeAnExistingFile())
@ -69,3 +71,40 @@ func main() { skel.PluginMain(c, c) }
Expect(output).To(ContainSubstring("unknown CNI_COMMAND: VERSION")) Expect(output).To(ContainSubstring("unknown CNI_COMMAND: VERSION"))
}) })
}) })
var _ = Describe("LocateCurrentGitRepo", func() {
It("returns the path to the root of the CNI git repo", func() {
path, err := testhelpers.LocateCurrentGitRepo()
Expect(err).NotTo(HaveOccurred())
AssertItIsTheCNIRepoRoot(path)
})
Context("when run from a different directory", func() {
BeforeEach(func() {
os.Chdir("..")
})
It("still finds the CNI repo root", func() {
path, err := testhelpers.LocateCurrentGitRepo()
Expect(err).NotTo(HaveOccurred())
AssertItIsTheCNIRepoRoot(path)
})
})
})
func AssertItIsTheCNIRepoRoot(path string) {
Expect(path).To(BeADirectory())
files, err := ioutil.ReadDir(path)
Expect(err).NotTo(HaveOccurred())
names := []string{}
for _, file := range files {
names = append(names, file.Name())
}
Expect(names).To(ContainElement("SPEC.md"))
Expect(names).To(ContainElement("libcni"))
Expect(names).To(ContainElement("cnitool"))
}

View File

@ -19,12 +19,13 @@ import (
"fmt" "fmt"
"github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/types/create" "github.com/containernetworking/cni/pkg/types/020"
"github.com/containernetworking/cni/pkg/types/current"
) )
// Current reports the version of the CNI spec implemented by this library // Current reports the version of the CNI spec implemented by this library
func Current() string { func Current() string {
return "1.1.0" return "0.4.0"
} }
// Legacy PluginInfo describes a plugin that is backwards compatible with the // Legacy PluginInfo describes a plugin that is backwards compatible with the
@ -34,56 +35,48 @@ func Current() string {
// //
// Any future CNI spec versions which meet this definition should be added to // Any future CNI spec versions which meet this definition should be added to
// this list. // this list.
var ( var Legacy = PluginSupports("0.1.0", "0.2.0")
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")
All = PluginSupports("0.1.0", "0.2.0", "0.3.0", "0.3.1", "0.4.0", "1.0.0", "1.1.0")
)
// VersionsFrom returns a list of versions starting from min, inclusive var resultFactories = []struct {
func VersionsStartingFrom(min string) PluginInfo { supportedVersions []string
out := []string{} newResult types.ResultFactoryFunc
// cheat, just assume ordered }{
ok := false {current.SupportedVersions, current.NewResult},
for _, v := range All.SupportedVersions() { {types020.SupportedVersions, types020.NewResult},
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 // 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. // that object to parse the plugin result, returning an error if parsing failed.
func NewResult(version string, resultBytes []byte) (types.Result, error) { func NewResult(version string, resultBytes []byte) (types.Result, error) {
return create.Create(version, resultBytes) 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)
} }
// ParsePrevResult parses a prevResult in a NetConf structure and sets // ParsePrevResult parses a prevResult in a NetConf structure and sets
// the NetConf's PrevResult member to the parsed Result object. // the NetConf's PrevResult member to the parsed Result object.
func ParsePrevResult(conf *types.PluginConf) error { func ParsePrevResult(conf *types.NetConf) error {
if conf.RawPrevResult == nil { if conf.RawPrevResult == nil {
return 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) resultBytes, err := json.Marshal(conf.RawPrevResult)
if err != nil { if err != nil {
return fmt.Errorf("could not serialize prevResult: %w", err) return fmt.Errorf("could not serialize prevResult: %v", err)
} }
conf.RawPrevResult = nil conf.RawPrevResult = nil
conf.PrevResult, err = create.Create(conf.CNIVersion, resultBytes) conf.PrevResult, err = NewResult(conf.CNIVersion, resultBytes)
if err != nil { if err != nil {
return fmt.Errorf("could not parse prevResult: %w", err) return fmt.Errorf("could not parse prevResult: %v", err)
} }
return nil return nil

View File

@ -15,10 +15,10 @@
package version_test package version_test
import ( import (
"testing" . "github.com/onsi/ginkgo"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"testing"
) )
func TestVersion(t *testing.T) { func TestVersion(t *testing.T) {

View File

@ -19,31 +19,24 @@ import (
"net" "net"
"reflect" "reflect"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types"
cniv1 "github.com/containernetworking/cni/pkg/types/100" "github.com/containernetworking/cni/pkg/types/current"
"github.com/containernetworking/cni/pkg/version" "github.com/containernetworking/cni/pkg/version"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
) )
var _ = Describe("Version operations", func() { var _ = Describe("Version operations", func() {
It("computes a list of versions correctly", func() {
actual := version.VersionsStartingFrom("0.3.1")
Expect(actual.SupportedVersions()).To(Equal([]string{"0.3.1", "0.4.0", "1.0.0", "1.1.0"}))
})
Context("when a prevResult is available", func() { Context("when a prevResult is available", func() {
It("parses the prevResult", func() { It("parses the prevResult", func() {
rawBytes := []byte(`{ rawBytes := []byte(`{
"cniVersion": "1.0.0", "cniVersion": "0.3.0",
"interfaces": [ "interfaces": [
{ {
"name": "eth0", "name": "eth0",
"mac": "00:11:22:33:44:55", "mac": "00:11:22:33:44:55",
"sandbox": "/proc/3553/ns/net", "sandbox": "/proc/3553/ns/net"
"pciID": "8086:9a01",
"socketPath": "/path/to/vhost/fd"
} }
], ],
"ips": [ "ips": [
@ -59,8 +52,8 @@ var _ = Describe("Version operations", func() {
err := json.Unmarshal(rawBytes, &raw) err := json.Unmarshal(rawBytes, &raw)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
conf := &types.PluginConf{ conf := &types.NetConf{
CNIVersion: "1.0.0", CNIVersion: "0.3.0",
Name: "foobar", Name: "foobar",
Type: "baz", Type: "baz",
RawPrevResult: raw, RawPrevResult: raw,
@ -68,20 +61,20 @@ var _ = Describe("Version operations", func() {
err = version.ParsePrevResult(conf) err = version.ParsePrevResult(conf)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
expectedResult := &cniv1.Result{
CNIVersion: "1.0.0", expectedResult := &current.Result{
Interfaces: []*cniv1.Interface{ CNIVersion: "0.3.0",
Interfaces: []*current.Interface{
{ {
Name: "eth0", Name: "eth0",
Mac: "00:11:22:33:44:55", Mac: "00:11:22:33:44:55",
Sandbox: "/proc/3553/ns/net", Sandbox: "/proc/3553/ns/net",
PciID: "8086:9a01",
SocketPath: "/path/to/vhost/fd",
}, },
}, },
IPs: []*cniv1.IPConfig{ IPs: []*current.IPConfig{
{ {
Interface: cniv1.Int(0), Version: "4",
Interface: current.Int(0),
Address: net.IPNet{ Address: net.IPNet{
IP: net.ParseIP("1.2.3.30"), IP: net.ParseIP("1.2.3.30"),
Mask: net.IPv4Mask(255, 255, 255, 0), Mask: net.IPv4Mask(255, 255, 255, 0),
@ -94,8 +87,8 @@ var _ = Describe("Version operations", func() {
}) })
It("fails if the prevResult version is unknown", func() { It("fails if the prevResult version is unknown", func() {
conf := &types.PluginConf{ conf := &types.NetConf{
CNIVersion: version.Current(), CNIVersion: "0.3.0",
Name: "foobar", Name: "foobar",
Type: "baz", Type: "baz",
RawPrevResult: map[string]interface{}{ RawPrevResult: map[string]interface{}{
@ -104,32 +97,28 @@ var _ = Describe("Version operations", func() {
} }
err := version.ParsePrevResult(conf) err := version.ParsePrevResult(conf)
Expect(err).To(MatchError(`could not parse prevResult: result type supports [1.0.0 1.1.0] but unmarshalled CNIVersion is "5678.456"`)) Expect(err).NotTo(HaveOccurred())
}) })
It("fails if the prevResult version does not match the prevResult version", func() { It("fails if the prevResult is invalid", func() {
conf := &types.PluginConf{ conf := &types.NetConf{
CNIVersion: version.Current(), CNIVersion: "0.3.0",
Name: "foobar", Name: "foobar",
Type: "baz", Type: "baz",
RawPrevResult: map[string]interface{}{ RawPrevResult: map[string]interface{}{
"cniVersion": "0.2.0", "adsfasdfasdfasdfasdfaf": nil,
"ip4": map[string]interface{}{
"ip": "1.2.3.30/24",
"gateway": "1.2.3.1",
},
}, },
} }
err := version.ParsePrevResult(conf) err := version.ParsePrevResult(conf)
Expect(err).To(MatchError("could not parse prevResult: result type supports [1.0.0 1.1.0] but unmarshalled CNIVersion is \"0.2.0\"")) Expect(err).NotTo(HaveOccurred())
}) })
}) })
Context("when a prevResult is not available", func() { Context("when a prevResult is not available", func() {
It("does not fail", func() { It("does not fail", func() {
conf := &types.PluginConf{ conf := &types.NetConf{
CNIVersion: version.Current(), CNIVersion: "0.3.0",
Name: "foobar", Name: "foobar",
Type: "baz", Type: "baz",
} }
@ -139,22 +128,4 @@ var _ = Describe("Version operations", func() {
Expect(conf.PrevResult).To(BeNil()) Expect(conf.PrevResult).To(BeNil())
}) })
}) })
Context("version parsing", func() {
It("parses versions correctly", func() {
v1 := "1.1.0"
v2 := "1.1.1"
check := func(a, b string, want bool) {
GinkgoHelper()
gt, err := version.GreaterThan(a, b)
Expect(err).NotTo(HaveOccurred())
Expect(gt).To(Equal(want))
}
check(v1, v2, false)
check(v2, v1, true)
check(v2, v2, false)
})
})
}) })

View File

@ -1,72 +0,0 @@
# debug plugin
## Overview
This plugin aims to help debugging or troubleshooting in CNI plugin development.
## Example Configuration
```
{
"cniVersion": "0.3.1",
"name": "mynet",
"plugins": [
{
"type": "ptp",
"ipMasq": true,
"ipam": {
"type": "host-local",
"subnet": "172.16.30.0/24",
"routes": [
{
"dst": "0.0.0.0/0"
}
]
}
},
{
"type": "debug",
"cniOutput": "/tmp/cni_output.txt",
"addHooks": [
[ "sh", "-c", "ip link set $CNI_IFNAME promisc on" ]
]
},
{
"type": "portmap",
"capabilities": {"portMappings": true},
"externalSetMarkChain": "KUBE-MARK-MASQ"
}
]
}
```
## Config Reference
* `cniOutput` (string, optional): output CNI request into file.
* `addHooks` (string array, optional): commands executed in container network namespace at interface add.
(note: but just execute it and does not catch command failure)
* `delHooks` (string array, optional): commands executed in container network namespace at interface delete.
(note: but just execute it and does not catch command failure)
* `checkHooks` (string array, optional): commands executed in container network namespace at interface check.
(note: but just execute it and does not catch command failure)
### Sample CNI Ouput
```
CmdAdd
ContainerID: cnitool-20c433bb2b1d6ede56d6
Netns: /var/run/netns/cnitest
IfName: eth0
Args:
Path: /opt/cni/bin
StdinData: {"cniOutput":"/tmp/cni_output.txt","cniVersion":"0.3.1","name":"test","prevResult":{"cniVersion":"0.3.1","interfaces":[{"name":"veth92e295cc","mac":"56:22:7f:b7:5b:75"},{"name":"eth0","mac":"46:b3:f3:77:bf:21","sandbox":"/var/run/netns/cnitest"}],"ips":[{"version":"4","interface":1,"address":"10.1.1.2/24","gateway":"10.1.1.1"}],"dns":{"nameservers":["10.64.255.25","8.8.8.8"]}},"type":"none"}
----------------------
CmdDel
ContainerID: cnitool-20c433bb2b1d6ede56d6
Netns: /var/run/netns/cnitest
IfName: eth0
Args:
Path: /opt/cni/bin
StdinData: {"cniOutput":"/tmp/cni_output.txt","cniVersion":"0.3.1","name":"test","type":"none"}
----------------------
```

View File

@ -1,15 +0,0 @@
module github.com/containernetworking/cni/plugins/debug
go 1.21
require (
github.com/containernetworking/cni v1.1.2
github.com/containernetworking/plugins v1.4.0
)
require (
github.com/vishvananda/netns v0.0.4 // indirect
golang.org/x/sys v0.15.0 // indirect
)
replace github.com/containernetworking/cni => ../..

View File

@ -1,26 +0,0 @@
github.com/containernetworking/plugins v1.4.0 h1:+w22VPYgk7nQHw7KT92lsRmuToHvb7wwSv9iTbXzzic=
github.com/containernetworking/plugins v1.4.0/go.mod h1:UYhcOyjefnrQvKvmmyEKsUA+M9Nfn7tqULPpH0Pkcj0=
github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
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/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20230323073829-e72429f035bd h1:r8yyd+DJDmsUhGrRBxH5Pj7KeFK5l+Y3FsgT8keqKtk=
github.com/google/pprof v0.0.0-20230323073829-e72429f035bd/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk=
github.com/onsi/ginkgo/v2 v2.13.2 h1:Bi2gGVkfn6gQcjNjZJVO8Gf0FHzMPf2phUei9tejVMs=
github.com/onsi/ginkgo/v2 v2.13.2/go.mod h1:XStQ8QcGwLyF4HdfcZB8SFOS/MWCgDuXMSBe6zrvLgM=
github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8=
github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc=
golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -1,148 +0,0 @@
// Copyright 2021 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 main
import (
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
"github.com/containernetworking/plugins/pkg/ns"
bv "github.com/containernetworking/plugins/pkg/utils/buildversion"
"github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/types"
type100 "github.com/containernetworking/cni/pkg/types/100"
"github.com/containernetworking/cni/pkg/version"
)
type NetConf struct {
types.NetConf
CNIOutput string `json:"cniOutput,omitempty"`
AddHooks [][]string `json:"addHooks,omitempty"`
DelHooks [][]string `json:"delHooks,omitempty"`
CheckHooks [][]string `json:"checkHooks,omitempty"`
}
func main() {
skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("none"))
}
func outputCmdArgs(fp io.Writer, args *skel.CmdArgs) {
fmt.Fprintf(fp, `ContainerID: %s
Netns: %s
IfName: %s
Args: %s
Path: %s
StdinData: %s
----------------------
`,
args.ContainerID,
args.Netns,
args.IfName,
args.Args,
args.Path,
string(args.StdinData))
}
func parseConf(data []byte) (*NetConf, error) {
conf := &NetConf{}
if err := json.Unmarshal(data, &conf); err != nil {
return nil, fmt.Errorf("failed to parse")
}
return conf, nil
}
func getResult(netConf *NetConf) *type100.Result {
if netConf.RawPrevResult == nil {
return &type100.Result{}
}
version.ParsePrevResult(&netConf.NetConf)
result, _ := type100.NewResultFromResult(netConf.PrevResult)
return result
}
func executeHooks(netnsName string, hooks [][]string) {
netns, err := ns.GetNS(netnsName)
if err != nil {
return
}
defer netns.Close()
netns.Do(func(_ ns.NetNS) error {
for _, hookStrs := range hooks {
hookCmd := hookStrs[0]
hookArgs := hookStrs[1:]
output, err := exec.Command(hookCmd, hookArgs...).Output()
if err != nil {
fmt.Fprintf(os.Stderr, "OUTPUT: %v", output)
fmt.Fprintf(os.Stderr, "ERR: %v", err)
}
}
return nil
})
}
func cmdAdd(args *skel.CmdArgs) error {
netConf, _ := parseConf(args.StdinData)
// Output CNI
if netConf.CNIOutput != "" {
fp, _ := os.OpenFile(netConf.CNIOutput, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o644)
defer fp.Close()
fmt.Fprintf(fp, "CmdAdd\n")
outputCmdArgs(fp, args)
}
// call hooks
if netConf.AddHooks != nil {
executeHooks(args.Netns, netConf.AddHooks)
}
return types.PrintResult(getResult(netConf), netConf.CNIVersion)
}
func cmdDel(args *skel.CmdArgs) error {
netConf, _ := parseConf(args.StdinData)
// Output CNI
if netConf.CNIOutput != "" {
fp, _ := os.OpenFile(netConf.CNIOutput, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o644)
defer fp.Close()
fmt.Fprintf(fp, "CmdDel\n")
outputCmdArgs(fp, args)
}
// call hooks
if netConf.DelHooks != nil {
executeHooks(args.Netns, netConf.DelHooks)
}
return types.PrintResult(&type100.Result{}, netConf.CNIVersion)
}
func cmdCheck(args *skel.CmdArgs) error {
netConf, _ := parseConf(args.StdinData)
// Output CNI
if netConf.CNIOutput != "" {
fp, _ := os.OpenFile(netConf.CNIOutput, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o644)
defer fp.Close()
fmt.Fprintf(fp, "CmdCheck\n")
outputCmdArgs(fp, args)
}
// call hooks
if netConf.CheckHooks != nil {
executeHooks(args.Netns, netConf.CheckHooks)
}
return types.PrintResult(&type100.Result{}, netConf.CNIVersion)
}

View File

@ -17,7 +17,7 @@ package debug
import ( import (
"encoding/json" "encoding/json"
"os" "io/ioutil"
"github.com/containernetworking/cni/pkg/skel" "github.com/containernetworking/cni/pkg/skel"
) )
@ -29,10 +29,8 @@ type Debug struct {
// Report* fields allow the test to control the behavior of the no-op plugin // Report* fields allow the test to control the behavior of the no-op plugin
ReportResult string ReportResult string
ReportError string ReportError string
ReportErrorCode uint
ReportStderr string ReportStderr string
ReportVersionSupport []string ReportVersionSupport []string
ExitWithCode int
// Command stores the CNI command that the plugin received // Command stores the CNI command that the plugin received
Command string Command string
@ -41,18 +39,9 @@ type Debug struct {
CmdArgs skel.CmdArgs CmdArgs skel.CmdArgs
} }
// CmdLogEntry records a single CNI command as well as its args
type CmdLogEntry struct {
Command string
CmdArgs skel.CmdArgs
}
// CmdLog records a list of CmdLogEntry received by the noop plugin
type CmdLog []CmdLogEntry
// ReadDebug will return a debug file recorded by the noop plugin // ReadDebug will return a debug file recorded by the noop plugin
func ReadDebug(debugFilePath string) (*Debug, error) { func ReadDebug(debugFilePath string) (*Debug, error) {
debugBytes, err := os.ReadFile(debugFilePath) debugBytes, err := ioutil.ReadFile(debugFilePath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -73,41 +62,10 @@ func (debug *Debug) WriteDebug(debugFilePath string) error {
return err return err
} }
err = os.WriteFile(debugFilePath, debugBytes, 0o600) err = ioutil.WriteFile(debugFilePath, debugBytes, 0600)
if err != nil { if err != nil {
return err return err
} }
return nil return nil
} }
// WriteCommandLog appends the executed cni command to the record file
func WriteCommandLog(path string, entry CmdLogEntry) error {
buf, err := os.ReadFile(path)
if err != nil {
return err
}
var cmds CmdLog
if len(buf) > 0 {
if err = json.Unmarshal(buf, &cmds); err != nil {
return err
}
}
cmds = append(cmds, entry)
if buf, err = json.Marshal(&cmds); err != nil {
return nil
}
return os.WriteFile(path, buf, 0o644)
}
func ReadCommandLog(path string) (CmdLog, error) {
buf, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var cmds CmdLog
if err = json.Unmarshal(buf, &cmds); err != nil {
return nil, err
}
return cmds, nil
}

View File

@ -23,31 +23,29 @@ package main
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io/ioutil"
"os" "os"
"strings" "strings"
"github.com/containernetworking/cni/pkg/skel" "github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types"
current "github.com/containernetworking/cni/pkg/types/100" "github.com/containernetworking/cni/pkg/types/current"
"github.com/containernetworking/cni/pkg/version" "github.com/containernetworking/cni/pkg/version"
noop_debug "github.com/containernetworking/cni/plugins/test/noop/debug" noop_debug "github.com/containernetworking/cni/plugins/test/noop/debug"
) )
type NetConf struct { type NetConf struct {
types.PluginConf types.NetConf
DebugFile string `json:"debugFile"` DebugFile string `json:"debugFile"`
CommandLog string `json:"commandLog"` PrevResult *current.Result `json:"prevResult,omitempty"`
} }
func loadConf(bytes []byte) (*NetConf, error) { func loadConf(bytes []byte) (*NetConf, error) {
n := &NetConf{} n := &NetConf{}
if err := json.Unmarshal(bytes, n); err != nil { if err := json.Unmarshal(bytes, n); err != nil {
return nil, fmt.Errorf("failed to load netconf: %w %q", err, string(bytes)) return nil, fmt.Errorf("failed to load netconf: %v %q", err, string(bytes))
}
if err := version.ParsePrevResult(&n.PluginConf); err != nil {
return nil, err
} }
return n, nil return n, nil
} }
@ -63,7 +61,7 @@ func parseExtraArgs(args string) (map[string]string, error) {
for _, item := range items { for _, item := range items {
kv := strings.Split(item, "=") kv := strings.Split(item, "=")
if len(kv) != 2 { if len(kv) != 2 {
return nil, fmt.Errorf("CNI_ARGS invalid key/value pair: %s", kv) return nil, fmt.Errorf("CNI_ARGS invalid key/value pair: %s\n", kv)
} }
m[kv[0]] = kv[1] m[kv[0]] = kv[1]
} }
@ -97,10 +95,7 @@ func debugBehavior(args *skel.CmdArgs, command string) error {
if debugFilePath == "" { if debugFilePath == "" {
fmt.Printf(`{}`) fmt.Printf(`{}`)
_, err = os.Stderr.WriteString("CNI_ARGS or config empty, no debug behavior\n") os.Stderr.WriteString("CNI_ARGS or config empty, no debug behavior\n")
if err != nil {
return err
}
return nil return nil
} }
@ -112,76 +107,48 @@ func debugBehavior(args *skel.CmdArgs, command string) error {
debug.CmdArgs = *args debug.CmdArgs = *args
debug.Command = command debug.Command = command
if debug.ReportResult == "" {
debug.ReportResult = fmt.Sprintf(` { "result": %q }`, noop_debug.EmptyReportResultMessage)
}
err = debug.WriteDebug(debugFilePath) err = debug.WriteDebug(debugFilePath)
if err != nil { if err != nil {
return err return err
} }
if netConf.CommandLog != "" { os.Stderr.WriteString(debug.ReportStderr)
if err = noop_debug.WriteCommandLog(
netConf.CommandLog,
noop_debug.CmdLogEntry{
Command: command,
CmdArgs: *args,
}); err != nil {
return err
}
}
if debug.ReportStderr != "" { if debug.ReportError != "" {
if _, err = os.Stderr.WriteString(debug.ReportStderr); err != nil { return errors.New(debug.ReportError)
return err } else if debug.ReportResult == "PASSTHROUGH" || debug.ReportResult == "INJECT-DNS" {
}
}
switch {
case debug.ReportError != "":
ec := debug.ReportErrorCode
if ec == 0 {
ec = types.ErrInternal
}
return &types.Error{
Msg: debug.ReportError,
Code: ec,
}
case debug.ReportResult == "PASSTHROUGH" || debug.ReportResult == "INJECT-DNS":
prevResult := netConf.PrevResult prevResult := netConf.PrevResult
if debug.ReportResult == "INJECT-DNS" { if debug.ReportResult == "INJECT-DNS" {
newResult, err := current.NewResultFromResult(netConf.PrevResult) prevResult, err = current.NewResultFromResult(netConf.PrevResult)
if err != nil { if err != nil {
return err return err
} }
newResult.DNS.Nameservers = []string{"1.2.3.4"} prevResult.DNS.Nameservers = []string{"1.2.3.4"}
prevResult = newResult
} }
// Must print the prevResult as the CNIVersion of the config // Must print the prevResult as the CNIVersion of the config
newResult, err := prevResult.GetAsVersion(netConf.CNIVersion) newResult, err := prevResult.GetAsVersion(netConf.CNIVersion)
if err != nil { if err != nil {
return fmt.Errorf("failed to convert result to config %q: %w", netConf.CNIVersion, err) return fmt.Errorf("failed to convert result to config %q: %v", netConf.CNIVersion, err)
} }
resultBytes, err := json.Marshal(newResult) resultBytes, err := json.Marshal(newResult)
if err != nil { if err != nil {
return fmt.Errorf("failed to marshal new result: %w", err) return fmt.Errorf("failed to marshal new result: %v", err)
}
_, err = os.Stdout.WriteString(string(resultBytes))
if err != nil {
return err
}
case debug.ReportResult != "":
_, err = os.Stdout.WriteString(debug.ReportResult)
if err != nil {
return err
} }
os.Stdout.WriteString(string(resultBytes))
} else {
os.Stdout.WriteString(debug.ReportResult)
} }
if debug.ExitWithCode > 0 {
os.Exit(debug.ExitWithCode)
}
return nil return nil
} }
func debugGetSupportedVersions(stdinData []byte) []string { func debugGetSupportedVersions(stdinData []byte) []string {
vers := []string{"0.-42.0", "0.1.0", "0.2.0", "0.3.0", "0.3.1", "0.4.0", "1.0.0", "1.1.0"} vers := []string{"0.-42.0", "0.1.0", "0.2.0", "0.3.0", "0.3.1", "0.4.0"}
cniArgs := os.Getenv("CNI_ARGS") cniArgs := os.Getenv("CNI_ARGS")
if cniArgs == "" { if cniArgs == "" {
return vers return vers
@ -214,17 +181,9 @@ func cmdDel(args *skel.CmdArgs) error {
return debugBehavior(args, "DEL") return debugBehavior(args, "DEL")
} }
func cmdGC(args *skel.CmdArgs) error {
return debugBehavior(args, "GC")
}
func cmdStatus(args *skel.CmdArgs) error {
return debugBehavior(args, "STATUS")
}
func saveStdin() ([]byte, error) { func saveStdin() ([]byte, error) {
// Read original stdin // Read original stdin
stdinData, err := io.ReadAll(os.Stdin) stdinData, err := ioutil.ReadAll(os.Stdin)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -253,11 +212,5 @@ func main() {
} }
supportedVersions := debugGetSupportedVersions(stdinData) supportedVersions := debugGetSupportedVersions(stdinData)
skel.PluginMainFuncs(skel.CNIFuncs{ skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.PluginSupports(supportedVersions...), "CNI noop plugin v0.7.0")
Add: cmdAdd,
Check: cmdCheck,
Del: cmdDel,
GC: cmdGC,
Status: cmdStatus,
}, version.PluginSupports(supportedVersions...), "CNI noop plugin v0.7.0")
} }

View File

@ -15,11 +15,11 @@
package main_test package main_test
import ( import (
"testing" . "github.com/onsi/ginkgo"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"github.com/onsi/gomega/gexec" "github.com/onsi/gomega/gexec"
"testing"
) )
func TestNoop(t *testing.T) { func TestNoop(t *testing.T) {

View File

@ -16,18 +16,17 @@ package main_test
import ( import (
"fmt" "fmt"
"io/ioutil"
"os" "os"
"os/exec" "os/exec"
"strings" "strings"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gexec"
"github.com/containernetworking/cni/pkg/skel" "github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/version" "github.com/containernetworking/cni/pkg/version"
noop_debug "github.com/containernetworking/cni/plugins/test/noop/debug" noop_debug "github.com/containernetworking/cni/plugins/test/noop/debug"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gexec"
) )
var _ = Describe("No-op plugin", func() { var _ = Describe("No-op plugin", func() {
@ -46,7 +45,7 @@ var _ = Describe("No-op plugin", func() {
ReportVersionSupport: []string{"0.1.0", "0.2.0", "0.3.0", "0.3.1", "0.4.0"}, ReportVersionSupport: []string{"0.1.0", "0.2.0", "0.3.0", "0.3.1", "0.4.0"},
} }
debugFile, err := os.CreateTemp("", "cni_debug") debugFile, err := ioutil.TempFile("", "cni_debug")
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(debugFile.Close()).To(Succeed()) Expect(debugFile.Close()).To(Succeed())
debugFileName = debugFile.Name() debugFileName = debugFile.Name()
@ -107,7 +106,6 @@ var _ = Describe("No-op plugin", func() {
"some":"stdin-json", "some":"stdin-json",
"cniVersion": "0.3.1", "cniVersion": "0.3.1",
"prevResult": { "prevResult": {
"cniVersion": "0.3.1",
"ips": [{"version": "4", "address": "10.1.2.15/24"}] "ips": [{"version": "4", "address": "10.1.2.15/24"}]
} }
}`) }`)
@ -126,7 +124,6 @@ var _ = Describe("No-op plugin", func() {
"some":"stdin-json", "some":"stdin-json",
"cniVersion": "0.4.0", "cniVersion": "0.4.0",
"prevResult": { "prevResult": {
"cniVersion": "0.4.0",
"ips": [{"version": "4", "address": "10.1.2.15/24"}] "ips": [{"version": "4", "address": "10.1.2.15/24"}]
} }
}`) }`)
@ -145,7 +142,7 @@ var _ = Describe("No-op plugin", func() {
"some":"stdin-json", "some":"stdin-json",
"cniVersion": "0.4.0", "cniVersion": "0.4.0",
"prevResult": { "prevResult": {
"cniVersion": "0.4.0", "cniVersion": "0.3.1",
"ips": [{"version": "4", "address": "10.1.2.3/24"}], "ips": [{"version": "4", "address": "10.1.2.3/24"}],
"dns": {} "dns": {}
} }
@ -155,7 +152,7 @@ var _ = Describe("No-op plugin", func() {
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Eventually(session).Should(gexec.Exit(0)) Eventually(session).Should(gexec.Exit(0))
Expect(session.Out.Contents()).To(MatchJSON(`{ Expect(session.Out.Contents()).To(MatchJSON(`{
"cniVersion": "0.4.0", "cniVersion": "0.4.0",
"ips": [{"version": "4", "address": "10.1.2.3/24"}], "ips": [{"version": "4", "address": "10.1.2.3/24"}],
"dns": {"nameservers": ["1.2.3.4"]} "dns": {"nameservers": ["1.2.3.4"]}
}`)) }`))
@ -198,46 +195,9 @@ var _ = Describe("No-op plugin", func() {
Expect(debug.WriteDebug(debugFileName)).To(Succeed()) Expect(debug.WriteDebug(debugFileName)).To(Succeed())
}) })
It("returns no result", func() {
session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter)
Expect(err).NotTo(HaveOccurred())
Eventually(session).Should(gexec.Exit(0))
Expect(session.Out.Contents()).To(Equal([]byte{}))
debug, err := noop_debug.ReadDebug(debugFileName)
Expect(err).NotTo(HaveOccurred())
Expect(debug.ReportResult).To(Equal(""))
})
})
Context("when the ExitWithCode debug field is set", func() {
BeforeEach(func() {
debug.ReportResult = ""
debug.ExitWithCode = 3
Expect(debug.WriteDebug(debugFileName)).To(Succeed())
})
It("returns no result and exits with the expected code", func() {
session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter)
Expect(err).NotTo(HaveOccurred())
Eventually(session).Should(gexec.Exit(3))
Expect(session.Out.Contents()).To(Equal([]byte{}))
debug, err := noop_debug.ReadDebug(debugFileName)
Expect(err).NotTo(HaveOccurred())
Expect(debug.ReportResult).To(Equal(""))
})
})
Context("when the ReportResult debug field is set", func() {
expectedResultString := fmt.Sprintf(` { "result": %q }`, noop_debug.EmptyReportResultMessage)
BeforeEach(func() {
debug.ReportResult = expectedResultString
Expect(debug.WriteDebug(debugFileName)).To(Succeed())
})
It("substitutes a helpful message for the test author", func() { It("substitutes a helpful message for the test author", func() {
expectedResultString := fmt.Sprintf(` { "result": %q }`, noop_debug.EmptyReportResultMessage)
session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter) session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Eventually(session).Should(gexec.Exit(0)) Eventually(session).Should(gexec.Exit(0))
@ -259,7 +219,7 @@ var _ = Describe("No-op plugin", func() {
session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter) session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Eventually(session).Should(gexec.Exit(1)) Eventually(session).Should(gexec.Exit(1))
Expect(session.Out.Contents()).To(MatchJSON(fmt.Sprintf(`{ "code": %d, "msg": "banana" }`, types.ErrInternal))) Expect(session.Out.Contents()).To(MatchJSON(`{ "code": 100, "msg": "banana" }`))
}) })
}) })

View File

@ -2,36 +2,7 @@
if [[ ${DEBUG} -gt 0 ]]; then set -x; fi if [[ ${DEBUG} -gt 0 ]]; then set -x; fi
NETCONFPATH="${NETCONFPATH-/etc/cni/net.d}" NETCONFPATH=${NETCONFPATH-/etc/cni/net.d}
function exec_list() {
plist="$1"
name="$2"
cniVersion="$3"
echo "$plist" | jq -c '.[]' | while read -r conf; do
plugin_bin="$(echo "$conf" | jq -r '.type')"
conf="$(echo "$conf" | jq -r ".name = \"$name\" | .cniVersion = \"$cniVersion\"")"
if [ -n "$res" ]; then
conf="$(echo "$conf" | jq -r ".prevResult=$res")"
fi
if ! res=$(echo "$conf" | $plugin_bin); then
error "$name" "$res"
elif [[ ${DEBUG} -gt 0 ]]; then
echo "${res}" | jq -r .
fi
done
}
function error () {
name="$1"
res="$2"
err_msg=$(echo "$res" | jq -r '.msg')
if [ -z "$errmsg" ]; then
err_msg=$res
fi
echo "${name} : error executing $CNI_COMMAND: $err_msg"
exit 1
}
function exec_plugins() { function exec_plugins() {
i=0 i=0
@ -42,24 +13,25 @@ function exec_plugins() {
export CNI_CONTAINERID=$contid export CNI_CONTAINERID=$contid
export CNI_NETNS=$netns export CNI_NETNS=$netns
for netconf in $(echo "$NETCONFPATH"/*.conf | sort); do for netconf in $(echo $NETCONFPATH/*.conf | sort); do
export CNI_IFNAME=$(printf eth%d $i) name=$(jq -r '.name' <$netconf)
name=$(jq -r '.name' <"$netconf") plugin=$(jq -r '.type' <$netconf)
cniVersion=$(jq -r '.cniVersion' <"$netconf") export CNI_IFNAME=$(printf eth%d $i)
plist=$(jq '.plugins | select(.!=null)' <"$netconf")
if [ -n "$plist" ]; then
exec_list "$plist" "$name" "$cniVersion"
else
plugin=$(jq -r '.type' <"$netconf")
if ! res=$($plugin <"$netconf"); then res=$($plugin <$netconf)
error "$name" "$res" if [ $? -ne 0 ]; then
elif [[ ${DEBUG} -gt 0 ]]; then errmsg=$(echo $res | jq -r '.msg')
echo "${res}" | jq -r . if [ -z "$errmsg" ]; then
fi errmsg=$res
fi fi
(( i++ )) || true echo "${name} : error executing $CNI_COMMAND: $errmsg"
exit 1
elif [[ ${DEBUG} -gt 0 ]]; then
echo ${res} | jq -r .
fi
let "i=i+1"
done done
} }

Some files were not shown because too many files have changed in this diff Show More