Compare commits
560 Commits
v0.7.0-alp
...
main
Author | SHA1 | Date |
---|---|---|
|
ee7c96f201 | |
|
d0bdb01ee9 | |
|
8338cebec2 | |
|
44a77d77e9 | |
|
277757c333 | |
|
0b7ae2f1b1 | |
|
40b0a08c95 | |
|
7f701df6ab | |
|
a28faab926 | |
|
3f7369aeb9 | |
|
097592d34b | |
|
d2f3f46deb | |
|
3578abe586 | |
|
5f84615a5d | |
|
3b4dfc5dff | |
|
a845cc88d2 | |
|
4c9ae43c0e | |
|
be3f5a9b2d | |
|
f4f2dc7430 | |
|
81ed2d06d6 | |
|
00f4094a12 | |
|
f0289faa47 | |
|
81d15e90c3 | |
|
309b6bbc17 | |
|
692efbd2a7 | |
|
6a68851f26 | |
|
d5c71ad528 | |
|
587837eeda | |
|
425425837e | |
|
c0cc785dbc | |
|
9f73d4da82 | |
|
e82d9969cf | |
|
2c926b5f17 | |
|
ed014fe9dc | |
|
db70b6d343 | |
|
fac588ba52 | |
|
6e57c2ee5d | |
|
499596af1b | |
|
73debca5ce | |
|
9d422db72c | |
|
c28331408d | |
|
1125b83816 | |
|
529bccb346 | |
|
267db7092b | |
|
cebd7dff3a | |
|
6ef571535d | |
|
02ffd013fe | |
|
1e7858f987 | |
|
c04330ee9e | |
|
52babb0dd3 | |
|
9f32fa36f7 | |
|
f5e23f52c0 | |
|
85241009c7 | |
|
e34763a7ec | |
|
f85b8fe31f | |
|
3d7be1bae9 | |
|
df52e9460d | |
|
047255eeed | |
|
0137b32398 | |
|
dd65e2d527 | |
|
bbae2202a6 | |
|
0036b628e1 | |
|
83287db830 | |
|
a67dabb3d0 | |
|
951d8d0e05 | |
|
f846cb72b0 | |
|
e4b261e1a2 | |
|
829ae61c58 | |
|
86c37cb0a2 | |
|
56f11c6c0c | |
|
8eb1091d78 | |
|
a448e71e98 | |
|
2e9688f3b4 | |
|
1c8da137b2 | |
|
c45adb66fe | |
|
563cf56c27 | |
|
2d04079cfe | |
|
845a73789a | |
|
00576a2e55 | |
|
a4d4a0d1ff | |
|
06d9dec402 | |
|
d9f3a7bb2b | |
|
c822f1331b | |
|
163db33480 | |
|
94fc2b1545 | |
|
3c4079b3db | |
|
b62753aa2b | |
|
1557aeb57c | |
|
dc24f70105 | |
|
8ddb290852 | |
|
d1276f1ed4 | |
|
66c292a7c5 | |
|
2317778d4d | |
|
a6a989119a | |
|
1362169a1f | |
|
f51f8eafe2 | |
|
f8d7b81f43 | |
|
6bac4ee028 | |
|
91db252b9d | |
|
d2bbac8e63 | |
|
0e4c0e225b | |
|
2cec989c82 | |
|
4caff13753 | |
|
9c19f13738 | |
|
d654dde532 | |
|
b9a9324609 | |
|
4a0bccb249 | |
|
efffbeeef4 | |
|
93878e6530 | |
|
26cfdbe95b | |
|
cf9ca9e003 | |
|
baaee10d93 | |
|
cd1b855eae | |
|
c18965d032 | |
|
911855c357 | |
|
8640c3ff16 | |
|
58eb836866 | |
|
6ed65ac99d | |
|
f67e207467 | |
|
17331add54 | |
|
cfdc39b16b | |
|
509c4908cf | |
|
5968d61a1e | |
|
94fd332ecd | |
|
0c68fe12dc | |
|
a6eaa01146 | |
|
58553d6cdb | |
|
c9fdf812d2 | |
|
82b44c5282 | |
|
29ba516d90 | |
|
b7d75ff4da | |
|
4683716a59 | |
|
2cfe79728d | |
|
91629d5751 | |
|
b15c73737e | |
|
e86c9ca58f | |
|
02155e0c8c | |
|
d9f1e675b7 | |
|
8fdefe9412 | |
|
743a44d404 | |
|
004b09b6a6 | |
|
902534de61 | |
|
11077bcc03 | |
|
0021b93858 | |
|
f6506e215f | |
|
3072cfe946 | |
|
3bbe370e68 | |
|
44a6eef2b2 | |
|
837171b012 | |
|
215259b17d | |
|
4a096f6be2 | |
|
ca96f4ca96 | |
|
a899051e44 | |
|
c768dcb129 | |
|
622537633d | |
|
e34f9d2bff | |
|
e255525e4d | |
|
2161bf8c24 | |
|
a8fdbf9c40 | |
|
dc0779e8ce | |
|
b6608f8c87 | |
|
15612fccaa | |
|
10ec024835 | |
|
cfb0b54407 | |
|
c9cfcaa2c7 | |
|
db718fc6d5 | |
|
4093160909 | |
|
9302e5ff67 | |
|
9a71f523e2 | |
|
05f80e4fd9 | |
|
f8aa587bba | |
|
aef15f60b3 | |
|
6626820a52 | |
|
435fcb1414 | |
|
52b1cb11a5 | |
|
d4c7848c2a | |
|
d42556829e | |
|
1ffb655895 | |
|
cb0d26535b | |
|
420e5949b0 | |
|
024c57f43f | |
|
0821ff55fc | |
|
121798a08d | |
|
f89a8c6a3e | |
|
da8672c859 | |
|
c5603949c1 | |
|
8ac5c8a7f1 | |
|
ed461a9b0e | |
|
5fa3464d59 | |
|
6a43fb5511 | |
|
45761d9522 | |
|
aeb1d8ebf8 | |
|
dd2d40c4f6 | |
|
97954935c5 | |
|
0a26996875 | |
|
cc036171cc | |
|
55be4ccd0d | |
|
f024754da8 | |
|
e18f6322ff | |
|
6e5ac36b42 | |
|
58488a6214 | |
|
e629d106b4 | |
|
62709e0a59 | |
|
76aaefb2cb | |
|
04dce8c7d3 | |
|
307ed01652 | |
|
be9139d588 | |
|
30fd41546b | |
|
dc22d0462d | |
|
69967699d7 | |
|
08fb4605fc | |
|
dbf33e2c59 | |
|
8dba3824fb | |
|
40fa4795fb | |
|
a710a7b4c1 | |
|
3363d14368 | |
|
1c7c696779 | |
|
812a8cfa8a | |
|
58b77bdf71 | |
|
3ec19197ef | |
|
55fe94e885 | |
|
940e662699 | |
|
99eac24911 | |
|
08f8596ff3 | |
|
1054f8ead7 | |
|
f32e3df6fb | |
|
c9114c284a | |
|
54f1587d67 | |
|
bb46e27465 | |
|
e6dea17746 | |
|
aba8f8b1fb | |
|
4b46fe6d73 | |
|
2f6d8b1930 | |
|
35efaabf93 | |
|
e4a9f965d0 | |
|
96a1883818 | |
|
3e49ce16b5 | |
|
96425dafeb | |
|
b92c836f3a | |
|
269bf6103d | |
|
48fac6a491 | |
|
798e63d83f | |
|
4756a3616c | |
|
cff680034b | |
|
10f777cb7d | |
|
9070cb3eec | |
|
1d9f32dc0e | |
|
21cd5f0c1f | |
|
c3625975b3 | |
|
6a92df875a | |
|
42f24747b7 | |
|
5725f27869 | |
|
2e4887bf8a | |
|
1694fd7b57 | |
|
c7f5f70554 | |
|
5608690f77 | |
|
699ad28878 | |
|
30e06a8b76 | |
|
0dfe02421e | |
|
a9562466f1 | |
|
63a3bca188 | |
|
b277ec1bc2 | |
|
34a8a464e1 | |
|
76bf3de7f8 | |
|
b4e078af7a | |
|
4feedb9dde | |
|
57cf1cee7d | |
|
4fdc5f62c9 | |
|
3a8fc073b5 | |
|
f30a8249fd | |
|
c63d850d60 | |
|
37ce8d6fbe | |
|
9546b704c8 | |
|
7ba00c594c | |
|
3a13f6854d | |
|
05ea7269e1 | |
|
16f664ce57 | |
|
c92642bda0 | |
|
67ec90448e | |
|
e33f449ec9 | |
|
0555966685 | |
|
8b25a7bf9e | |
|
8ad568b543 | |
|
ea59a90103 | |
|
00169178ed | |
|
c2f6afa41f | |
|
9c075a3e40 | |
|
23b841a269 | |
|
e00ed00197 | |
|
ad59be0959 | |
|
b5ab16f010 | |
|
e32b586636 | |
|
296290a913 | |
|
a199e6aa75 | |
|
a5c35b1d17 | |
|
ada67263b1 | |
|
15391a8c4a | |
|
eec3755640 | |
|
216a7099cb | |
|
867451c7a0 | |
|
77cd8fe3d6 | |
|
75e7788841 | |
|
679ed9d310 | |
|
6d8228e1d3 | |
|
c7357803b1 | |
|
b678c260c4 | |
|
62e54113f4 | |
|
e781c94eea | |
|
d1e1ae3c34 | |
|
7555ca3250 | |
|
6823eba0ec | |
|
3805b13a3b | |
|
0050bfa528 | |
|
6fc72a49e9 | |
|
90311ea2b0 | |
|
e91547363e | |
|
f9b5c9b162 | |
|
17b2a04fa4 | |
|
e5c65a5bf3 | |
|
6b482fdc76 | |
|
65bf6884d1 | |
|
a7cceb916d | |
|
93a7425495 | |
|
11db36c11b | |
|
9ec274acd6 | |
|
0016c95d2e | |
|
c78d465e9f | |
|
fc1de42c01 | |
|
c815aca145 | |
|
f92762a8bf | |
|
17a6379deb | |
|
956c943a7e | |
|
77436456f2 | |
|
acc3dadce9 | |
|
e2a736653a | |
|
76b18ea294 | |
|
bf84331c94 | |
|
7d9ec90bff | |
|
44dabed6ce | |
|
279bc6c21b | |
|
fdcc7b15d7 | |
|
cf734d46be | |
|
ca0082f790 | |
|
4fae32b849 | |
|
38353fabbb | |
|
56ace59a9e | |
|
134f603c67 | |
|
c252795066 | |
|
1435c6bddb | |
|
f69d1f2cab | |
|
6b46a03d2b | |
|
0adeb0ed54 | |
|
075e303ba8 | |
|
d0fd3ff4e9 | |
|
c2b68cd595 | |
|
5c2bade00b | |
|
0b1c6497ed | |
|
704c56d291 | |
|
f208f19fd3 | |
|
cc6e8afa5a | |
|
a09ec7a992 | |
|
6680f870ab | |
|
025e32f89e | |
|
6f29b01658 | |
|
4c077d2fec | |
|
d65360849f | |
|
340972aad4 | |
|
777584fb17 | |
|
7aed59f421 | |
|
dce771e2a0 | |
|
e00fa533de | |
|
7815be7f0a | |
|
dcb71ae8c9 | |
|
f2fa4a3fa0 | |
|
b36de6e00b | |
|
ade6ad342d | |
|
c35ac21f14 | |
|
3eb88acd2a | |
|
46b9d6fa20 | |
|
6c6a3156c3 | |
|
c2508f1804 | |
|
b89eff5321 | |
|
2d4bc3d57a | |
|
4eec64802d | |
|
63202159f1 | |
|
eefc06974c | |
|
904ce461b0 | |
|
8f2d48e6e1 | |
|
b5188cf64f | |
|
c94fcd742c | |
|
02a25a7635 | |
|
15d37883e6 | |
|
7be1ac932d | |
|
9f4a623222 | |
|
2a82881b00 | |
|
bd4e822851 | |
|
c0f784d16d | |
|
94399d569c | |
|
83439463f7 | |
|
bd64c46461 | |
|
9d4429ded4 | |
|
d8dfb56fd8 | |
|
e4a11bae33 | |
|
a83f3cb0f4 | |
|
894863c200 | |
|
13b6ad6447 | |
|
f3654f3d2d | |
|
8c6c47d1c7 | |
|
3e79703c66 | |
|
d19c2358a5 | |
|
d34f7f4ead | |
|
1318d7c94c | |
|
df124b22e4 | |
|
9af40ed73a | |
|
461cf2b48f | |
|
b2c510b758 | |
|
722a488070 | |
|
227c438835 | |
|
ba034ef5da | |
|
4b29940c46 | |
|
50192c0c4c | |
|
0af0477c17 | |
|
b92d83cb59 | |
|
220a58f522 | |
|
a48337ac26 | |
|
dc71cd2ba6 | |
|
5077b14734 | |
|
ef03d0458c | |
|
80ad241eb6 | |
|
5dbeae87ff | |
|
a03dc28d25 | |
|
dc953e2fd9 | |
|
4cfb7b5689 | |
|
33bb6067c0 | |
|
e3a51e54fd | |
|
d11ef1fb4e | |
|
53e9245e7b | |
|
1475a6e3c0 | |
|
e67d6cba0f | |
|
5bab2c0253 | |
|
cbca75246a | |
|
a94ff7bdaf | |
|
0469a05ab5 | |
|
e40cce2373 | |
|
ea274f7629 | |
|
f726df9103 | |
|
ffc61ed2d3 | |
|
7d76556571 | |
|
c47f15614b | |
|
ec5ec68fa4 | |
|
530eed4d27 | |
|
121ab0058f | |
|
de8cf72507 | |
|
59d8d02f67 | |
|
8361cffee2 | |
|
4461d54929 | |
|
207be7e31e | |
|
0471e018e5 | |
|
c9c5031a1b | |
|
6bddfcd3bb | |
|
fbb95fff8a | |
|
f7e07eec38 | |
|
c976b421df | |
|
139f460f90 | |
|
fe0a28d82a | |
|
2c4013efcd | |
|
0dba47b1f9 | |
|
4554856998 | |
|
f4fdaa2304 | |
|
f55f75bf0e | |
|
83ed2459f7 | |
|
16d174d296 | |
|
f00ca5f42b | |
|
6896907ab4 | |
|
3d19d747b6 | |
|
203f70b6bb | |
|
2e00c58a3b | |
|
5761cff2ef | |
|
b3342a6e36 | |
|
667a69ea9d | |
|
b69a8d52b6 | |
|
8574263c4e | |
|
b1838c7f90 | |
|
f36276e1bc | |
|
8fa373b4fd | |
|
405805692c | |
|
c96f9d1d95 | |
|
5ba9bb8fa0 | |
|
ab0ee6fa98 | |
|
7aacc67f18 | |
|
bfcd424c0e | |
|
51885a9b69 | |
|
c9bc5b79ec | |
|
8e8abe455b | |
|
4425e7e337 | |
|
93bfd62fb1 | |
|
06b37e1ff5 | |
|
0cf415d434 | |
|
0126b26304 | |
|
afdc49d0a6 | |
|
d6ffc8d74f | |
|
762bb8ae5d | |
|
1c35013cf4 | |
|
ce8938617e | |
|
ce46ad0130 | |
|
b74b969c00 | |
|
e2bdb13345 | |
|
94bf13891a | |
|
d6ed2fd5a3 | |
|
213b3fdea2 | |
|
b7ae50cbb4 | |
|
3de1875e27 | |
|
3a3a701be0 | |
|
34a6112b3b | |
|
3d6117d63e | |
|
66dafdf3e9 | |
|
fdcc5db51b | |
|
297457de48 | |
|
dba13c4822 | |
|
c744c26d2e | |
|
951c0fb806 | |
|
b271ce351d | |
|
47f042a96e | |
|
a8a44a721a | |
|
cc562d1b44 | |
|
24aa1f4030 | |
|
1dba71945f | |
|
f301e8fe68 | |
|
777324ca6b | |
|
df75bcfdbe | |
|
89d8d97746 | |
|
68c963649f | |
|
42e857f0a2 | |
|
f145359def | |
|
c5ab1c3fc6 | |
|
7418eded11 | |
|
626458fb67 | |
|
01eda4e0cf | |
|
93b8b11111 | |
|
d59bfbf33e | |
|
47cf2da00b | |
|
57da9a82b0 | |
|
97b1f58534 | |
|
5ef0e9aa4c | |
|
779dc12496 | |
|
d2836a7b59 | |
|
cbd2718984 | |
|
9c9cc5365d | |
|
0750650021 | |
|
9fc6eb434c | |
|
478f7b417e | |
|
0f6a509e4b | |
|
2ce2c24cc2 | |
|
e67bb289cc | |
|
9bd516a786 | |
|
9a34081fef | |
|
d974c1dd54 | |
|
96343561e0 | |
|
aed7a2f2a1 | |
|
b34795b3ce |
|
@ -1,17 +0,0 @@
|
|||
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 ./...
|
|
@ -0,0 +1,2 @@
|
|||
# Don't rewrite line endings
|
||||
*.go -text
|
|
@ -0,0 +1,7 @@
|
|||
FROM alpine:3.20
|
||||
|
||||
RUN apk add --no-cache curl jq
|
||||
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
|
@ -0,0 +1,11 @@
|
|||
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 }}
|
|
@ -0,0 +1,45 @@
|
|||
#!/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" }'
|
|
@ -0,0 +1,27 @@
|
|||
# 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"
|
|
@ -0,0 +1,17 @@
|
|||
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 }}
|
|
@ -0,0 +1,40 @@
|
|||
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
|
|
@ -0,0 +1,96 @@
|
|||
---
|
||||
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
|
|
@ -1,5 +1,5 @@
|
|||
bin/
|
||||
gopath/
|
||||
.idea/
|
||||
*.sw[ponm]
|
||||
.vagrant
|
||||
release-*
|
||||
cnitool/cnitool
|
|
@ -0,0 +1,30 @@
|
|||
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
37
.travis.yml
|
@ -1,37 +0,0 @@
|
|||
language: go
|
||||
dist: trusty
|
||||
|
||||
go:
|
||||
- 1.9.x
|
||||
- 1.10.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
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
extends: default
|
||||
|
||||
rules:
|
||||
document-start: disable
|
||||
line-length: disable
|
||||
truthy:
|
||||
ignore: |
|
||||
.github/workflows/*.yml
|
||||
.github/workflows/*.yaml
|
|
@ -1,4 +1,3 @@
|
|||
## Community Code of Conduct
|
||||
|
||||
CNI follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md).
|
||||
# Community Code of Conduct
|
||||
|
||||
CNI follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md).
|
||||
|
|
|
@ -7,18 +7,19 @@ it easier to get your contribution accepted.
|
|||
|
||||
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
|
||||
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
|
||||
contribution. See the [DCO](DCO) file for details.
|
||||
|
||||
# Email and Chat
|
||||
## Email and Chat
|
||||
|
||||
The project uses the the cni-dev email list and IRC chat:
|
||||
The project uses the cni-dev email list, IRC chat, and Slack:
|
||||
- Email: [cni-dev](https://groups.google.com/forum/#!forum/cni-dev)
|
||||
- IRC: #[containernetworking](irc://irc.freenode.org:6667/#containernetworking) channel on freenode.org
|
||||
- IRC: #[containernetworking](irc://irc.freenode.net:6667/#containernetworking) channel on [freenode.net](https://freenode.net/)
|
||||
- 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
|
||||
are very busy and read the mailing lists.
|
||||
|
@ -33,12 +34,12 @@ are very busy and read the mailing lists.
|
|||
|
||||
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 master).
|
||||
- Create a topic branch from where you want to base your work (usually branched from main).
|
||||
- Make commits of logical units.
|
||||
- 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.
|
||||
- If you changed code:
|
||||
- add automated tests to cover your changes, using the [Ginkgo](http://onsi.github.io/ginkgo/) & [Gomega](http://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
|
||||
of `TESTABLE` packages in the `test.sh` script.
|
||||
- run the full test script and ensure it passes
|
||||
|
@ -46,6 +47,7 @@ This is a rough outline of how to prepare a contribution:
|
|||
- Submit a pull request to the original repository.
|
||||
|
||||
## How to run the test suite
|
||||
|
||||
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
|
||||
|
@ -62,19 +64,19 @@ cd /go/src/github.com/containernetworking/cni
|
|||
./test.sh
|
||||
|
||||
# to focus on a particular test suite
|
||||
cd plugins/main/loopback
|
||||
cd libcni
|
||||
go test
|
||||
```
|
||||
|
||||
# Acceptance policy
|
||||
## Acceptance policy
|
||||
|
||||
These things will make a PR more likely to be accepted:
|
||||
|
||||
* a well-described requirement
|
||||
* tests for new code
|
||||
* tests for old code!
|
||||
* new code and tests follow the conventions in old code and tests
|
||||
* a good commit message (see below)
|
||||
- a well-described requirement
|
||||
- tests for new code
|
||||
- tests for old code!
|
||||
- new code and tests follow the conventions in old code and tests
|
||||
- a good commit message (see below)
|
||||
|
||||
In general, we will merge a PR once two maintainers have endorsed it.
|
||||
Trivial changes (e.g., corrections to spelling) may get waved through.
|
||||
|
@ -86,7 +88,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
|
||||
the body of the commit should describe the why.
|
||||
|
||||
```
|
||||
```md
|
||||
scripts: add the test-cluster command
|
||||
|
||||
this uses tmux to setup a test cluster that you can easily kill and
|
||||
|
@ -97,7 +99,7 @@ Fixes #38
|
|||
|
||||
The format can be described more formally as follows:
|
||||
|
||||
```
|
||||
```md
|
||||
<subsystem>: <what changed>
|
||||
<BLANK LINE>
|
||||
<why this change was made>
|
||||
|
@ -111,6 +113,7 @@ This allows the message to be easier to read on GitHub as well as in various
|
|||
git tools.
|
||||
|
||||
## 3rd party plugins
|
||||
|
||||
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).
|
||||
|
|
|
@ -10,7 +10,7 @@ Establishing these conventions allows plugins to work across multiple runtimes.
|
|||
|
||||
## 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.
|
||||
* 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.
|
||||
* 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.
|
||||
|
||||
## 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.
|
||||
|
@ -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.
|
||||
|
||||
## Dynamic Plugin specific fields (Capabilities / Runtime Configuration)
|
||||
[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.
|
||||
[Plugin specific fields](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.
|
||||
|
||||
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
|
||||
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](https://github.com/containernetworking/cni/blob/master/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](SPEC.md#network-configuration-lists).
|
||||
```json
|
||||
{
|
||||
"name" : "ExamplePlugin",
|
||||
|
@ -58,12 +58,18 @@ But the runtime would fill in the mappings so the plugin itself would receive so
|
|||
| Area | Purpose | Capability | Spec and Example | Runtime implementations | Plugin Implementations |
|
||||
| ----- | ------- | -----------| ---------------- | ----------------------- | --------------------- |
|
||||
| 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 |
|
||||
| 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 |
|
||||
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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](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` in [network config](SPEC.md#network-configuration) were reserved as a field in the `0.2.0` release of the CNI spec.
|
||||
> 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.
|
||||
|
@ -72,10 +78,10 @@ But the runtime would fill in the mappings so the plugin itself would receive so
|
|||
|
||||
This method of passing information to a plugin is recommended when the information is optional and the plugin can choose to ignore it. It's often that case that such information is passed to all plugins by the runtime without regard for whether the plugin can understand it.
|
||||
|
||||
The conventions documented here are all namepaced 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:
|
||||
```json
|
||||
```jsonc
|
||||
{
|
||||
"cniVersion":"0.2.0",
|
||||
"name":"net",
|
||||
|
@ -84,9 +90,9 @@ For example:
|
|||
"labels": [{"key": "app", "value": "myapp"}]
|
||||
}
|
||||
},
|
||||
<REST OF CNI CONFIG HERE>
|
||||
// <REST OF CNI CONFIG HERE>
|
||||
"ipam":{
|
||||
<IPAM CONFIG HERE>
|
||||
// <IPAM CONFIG HERE>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
@ -94,17 +100,17 @@ For example:
|
|||
| 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 |
|
||||
| ips | Request static IPs | <pre>"ips": ["10.2.2.42", "2001:db8::5"]</pre> | none | host-local |
|
||||
| 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 |
|
||||
|
||||
## CNI_ARGS
|
||||
CNI_ARGS formed part of the original CNI spec and have been present since the initial release.
|
||||
> `CNI_ARGS`: Extra arguments passed in by the user at invocation time. Alphanumeric key-value pairs separated by semicolons; for example, "FOO=BAR;ABC=123"
|
||||
|
||||
The use of `CNI_ARGS` is deprecated and "args" should be used instead.
|
||||
The use of `CNI_ARGS` is deprecated and "args" should be used instead. If a runtime passes an equivalent key via `args` (eg the `ips` `args` Area and the `CNI_ARGS` `IP` Field) and the plugin understands `args`, the plugin must ignore the CNI_ARGS Field.
|
||||
|
||||
| Field | Purpose| Spec and Example | Runtime implementations | Plugin Implementations |
|
||||
| ------ | ------ | ---------------- | ----------------------- | ---------------------- |
|
||||
| IP | Request a specific IP from IPAM plugins | IP=192.168.10.4 | *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/cni/blob/master/Documentation/host-local.md#supported-arguments).|
|
||||
| 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 |
|
||||
|
||||
## 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.
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
# Overview
|
||||
|
||||
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
|
||||
`network name` and a `network namespace` and a command to `ADD` or
|
||||
`DEL`,.i.e, attach or detach containers from a network. The `cnitool`
|
||||
relies on the following environment variables to operate properly:
|
||||
|
||||
* `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 with the extension
|
||||
|
@ -11,6 +13,7 @@ relies on the following environment variables to operate properly:
|
|||
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.
|
||||
|
||||
For the full documentation of `cnitool` see the [cnitool docs](../cnitool/README.md)
|
||||
|
|
|
@ -1,12 +1,45 @@
|
|||
# How to upgrade to CNI Specification v0.3.1
|
||||
# How to Upgrade to CNI Specification v1.0
|
||||
|
||||
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.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.
|
||||
|
||||
# How to upgrade to CNI Specification v0.3.0
|
||||
|
||||
Version 0.3.0 of the [CNI Specification](../SPEC.md) provides rich information
|
||||
Version 0.3.0 of the [CNI Specification](https://github.com/containernetworking/cni/blob/spec-v0.3.0/SPEC.md) provides rich information
|
||||
about container network configuration, including details of network interfaces
|
||||
and support for multiple IP addresses.
|
||||
|
||||
|
@ -31,12 +64,12 @@ ensure that the configuration files specify a `cniVersion` field and that the
|
|||
version there is supported by your container runtime and CNI plugins.
|
||||
Configuration files without a version field should be given version 0.2.0.
|
||||
The CNI spec includes example configuration files for
|
||||
[single plugins](https://github.com/containernetworking/cni/blob/master/SPEC.md#example-configurations)
|
||||
and for [lists of chained plugins](https://github.com/containernetworking/cni/blob/master/SPEC.md#example-configurations).
|
||||
[single plugins](SPEC.md#example-configurations)
|
||||
and for [lists of chained plugins](SPEC.md#example-configurations).
|
||||
|
||||
Consult the documentation for your runtime and plugins to determine what
|
||||
CNI spec versions they support. Test any plugin upgrades before deploying to
|
||||
production. You may find [cnitool](https://github.com/containernetworking/cni/tree/master/cnitool)
|
||||
production. You may find [cnitool](https://github.com/containernetworking/cni/tree/main/cnitool)
|
||||
useful. Specifically, your configuration version should be the lowest common
|
||||
version supported by your plugins.
|
||||
|
||||
|
@ -46,7 +79,7 @@ This section provides guidance for upgrading plugins to CNI Spec Version 0.3.0.
|
|||
### General guidance for all plugins (language agnostic)
|
||||
To provide the smoothest upgrade path, **existing plugins should support
|
||||
multiple versions of the CNI spec**. In particular, plugins with existing
|
||||
installed bases should add support for CNI spec version 0.3.0 while maintaining
|
||||
installed bases should add support for CNI spec version 1.0.0 while maintaining
|
||||
compatibility with older versions.
|
||||
|
||||
To do this, two changes are required. First, a plugin should advertise which
|
||||
|
@ -55,13 +88,13 @@ command with the following JSON data:
|
|||
|
||||
```json
|
||||
{
|
||||
"cniVersion": "0.3.0",
|
||||
"supportedVersions": [ "0.1.0", "0.2.0", "0.3.0" ]
|
||||
"cniVersion": "1.0.0",
|
||||
"supportedVersions": [ "0.1.0", "0.2.0", "0.3.0", "0.3.1", "0.4.0", "1.0.0" ]
|
||||
}
|
||||
```
|
||||
|
||||
Second, for the `ADD` command, a plugin must respect the `cniVersion` field
|
||||
provided in the [network configuration JSON](https://github.com/containernetworking/cni/blob/master/SPEC.md#network-configuration).
|
||||
provided in the [network configuration JSON](SPEC.md#network-configuration).
|
||||
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
|
||||
|
@ -69,11 +102,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.
|
||||
|
||||
- Otherwise, the plugin must return a [CNI Result](https://github.com/containernetworking/cni/blob/master/SPEC.md#result)
|
||||
- Otherwise, the plugin must return a [CNI Result](SPEC.md#result)
|
||||
in the format requested.
|
||||
|
||||
Result formats for older CNI spec versions are available in the
|
||||
[git history for SPEC.md](https://github.com/containernetworking/cni/commits/master/SPEC.md).
|
||||
[git history for SPEC.md](https://github.com/containernetworking/cni/commits/main/SPEC.md).
|
||||
|
||||
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`,
|
||||
|
@ -82,21 +115,21 @@ the plugin must return result JSON conforming to CNI spec version 0.2.0.
|
|||
### Specific guidance for plugins written in Go
|
||||
Plugins written in Go may leverage the Go language packages in this repository
|
||||
to ease the process of upgrading and supporting multiple versions. CNI
|
||||
[Library and Plugins Release v0.5.0](https://github.com/containernetworking/cni/releases)
|
||||
[Library and Plugins Release v0.5.0](https://github.com/containernetworking/cni/releases/tag/v0.5.0)
|
||||
includes important changes to the Golang APIs. Plugins using these APIs will
|
||||
require some changes now, but should more-easily handle spec changes and
|
||||
new features going forward.
|
||||
|
||||
For plugin authors, the biggest change is that `types.Result` is now an
|
||||
interface implemented by concrete struct types in the `types/current` and
|
||||
`types/020` subpackages.
|
||||
interface implemented by concrete struct types in the `types/100`,
|
||||
`types/040`, and `types/020` subpackages.
|
||||
|
||||
Internally, plugins should use the `types/current` structs, and convert
|
||||
to or from specific versions when required. A typical plugin will only need
|
||||
to do a single conversion. That is when it is about to complete and needs to
|
||||
print the result JSON in the correct format to stdout. The library
|
||||
function `types.PrintResult()` simplifies this by converting and printing in
|
||||
a single call.
|
||||
Internally, plugins should use the latest spec version (eg `types/100`) structs,
|
||||
and convert to or from specific versions when required. A typical plugin will
|
||||
only need to do a single conversion when it is about to complete and
|
||||
needs to print the result JSON in the requested `cniVersion` format to stdout.
|
||||
The library function `types.PrintResult()` simplifies this by converting and
|
||||
printing in a single call.
|
||||
|
||||
Additionally, the plugin should advertise which CNI Spec versions it supports
|
||||
via the 3rd argument to `skel.PluginMain()`.
|
||||
|
@ -107,7 +140,7 @@ Here is some example code
|
|||
import (
|
||||
"github.com/containernetworking/cni/pkg/skel"
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/pkg/types/current"
|
||||
current "github.com/containernetworking/cni/pkg/types/100"
|
||||
"github.com/containernetworking/cni/pkg/version"
|
||||
)
|
||||
|
||||
|
@ -136,7 +169,7 @@ func cmdAdd(args *skel.CmdArgs) error {
|
|||
}
|
||||
|
||||
func main() {
|
||||
skel.PluginMain(cmdAdd, cmdDel, version.PluginSupports("0.1.0", "0.2.0", "0.3.0"))
|
||||
skel.PluginMain(cmdAdd, cmdDel, version.All)
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -149,13 +182,13 @@ result, err := current.NewResultFromResult(ipamResult)
|
|||
```
|
||||
|
||||
Other examples of spec v0.3.0-compatible plugins are the
|
||||
[main plugins in this repo](https://github.com/containernetworking/cni/tree/master/plugins/main)
|
||||
[main plugins in this repo](https://github.com/containernetworking/plugins/)
|
||||
|
||||
|
||||
## For Runtime Authors
|
||||
|
||||
This section provides guidance for upgrading container runtimes to support
|
||||
CNI Spec Version 0.3.0.
|
||||
CNI Spec Version 0.3.0 and later.
|
||||
|
||||
### General guidance for all runtimes (language agnostic)
|
||||
|
||||
|
@ -163,22 +196,22 @@ CNI Spec Version 0.3.0.
|
|||
To provide the smoothest upgrade path and support the broadest range of CNI
|
||||
plugins, **container runtimes should support multiple versions of the CNI spec**.
|
||||
In particular, runtimes with existing installed bases should add support for CNI
|
||||
spec version 0.3.0 while maintaining compatibility with older versions.
|
||||
spec version 0.3.0 and later while maintaining compatibility with older versions.
|
||||
|
||||
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.
|
||||
|
||||
When calling a plugin, the runtime must request that the plugin respond in a
|
||||
particular format by specifying the `cniVersion` field in the
|
||||
[Network Configuration](https://github.com/containernetworking/cni/blob/master/SPEC.md#network-configuration)
|
||||
[Network Configuration](SPEC.md#network-configuration)
|
||||
JSON block. The plugin will then respond with
|
||||
a [Result](https://github.com/containernetworking/cni/blob/master/SPEC.md#result)
|
||||
a [Result](SPEC.md#result)
|
||||
in the format defined by that CNI spec version, and the runtime must parse
|
||||
and handle this result.
|
||||
|
||||
#### Handle errors due to version incompatibility
|
||||
Plugins may respond with error indicating that they don't support the requested
|
||||
CNI version (see [Well-known Error Codes](https://github.com/containernetworking/cni/blob/master/SPEC.md#well-known-error-codes)),
|
||||
CNI version (see [Well-known Error Codes](SPEC.md#well-known-error-codes)),
|
||||
e.g.
|
||||
```json
|
||||
{
|
||||
|
@ -197,16 +230,17 @@ 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
|
||||
CNI spec v0.1.0.
|
||||
|
||||
#### Handle missing data in v0.3.0 results
|
||||
The Result for the `ADD` command in CNI spec version 0.3.0 includes a new field
|
||||
`interfaces`. An IP address in the `ip` field may describe which interface
|
||||
it is assigned to, by placing a numeric index in the `interface` subfield.
|
||||
#### Handle missing data in v0.3.0 and later results
|
||||
The Result for the `ADD` command in CNI spec version 0.3.0 and later includes
|
||||
a new field `interfaces`. An IP address in the `ip` field may describe which
|
||||
interface it is assigned to, by placing a numeric index in the `interface`
|
||||
subfield.
|
||||
|
||||
However, some plugins which are v0.3.0 compatible may nonetheless omit the
|
||||
`interfaces` field and/or set the `interface` index value to `-1`. Runtimes
|
||||
should gracefully handle this situation, unless they have good reason to rely
|
||||
on the existence of the interface data. In that case, provide the user an
|
||||
error message that helps diagnose the issue.
|
||||
However, some plugins which are v0.3.0 and later compatible may nonetheless
|
||||
omit the `interfaces` field and/or set the `interface` index value to `-1`.
|
||||
Runtimes should gracefully handle this situation, unless they have good reason
|
||||
to rely on the existence of the interface data. In that case, provide the user
|
||||
an error message that helps diagnose the issue.
|
||||
|
||||
### Specific guidance for container runtimes written in Go
|
||||
Container runtimes written in Go may leverage the Go language packages in this
|
||||
|
@ -223,17 +257,18 @@ 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`
|
||||
subpackage contains the latest (spec v0.3.0) types.
|
||||
|
||||
When up-converting older result types to spec v0.3.0, fields new in
|
||||
spec v0.3.0 (like `interfaces`) may be empty. Conversely, when
|
||||
down-converting v0.3.0 results to an older version, any data in those fields
|
||||
will be lost.
|
||||
|
||||
| From | 0.1 | 0.2 | 0.3 |
|
||||
|--------|-----|-----|-----|
|
||||
| To 0.1 | ✔ | ✔ | x |
|
||||
| To 0.2 | ✔ | ✔ | x |
|
||||
| To 0.3 | ✴ | ✴ | ✔ |
|
||||
When up-converting older result types to spec v0.3.0 and later, fields new in
|
||||
spec v0.3.0 and later (like `interfaces`) may be empty. Conversely, when
|
||||
down-converting v0.3.0 and later results to an older version, any data in
|
||||
those fields will be lost.
|
||||
|
||||
| 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:
|
||||
> ✔ : lossless conversion <br>
|
||||
|
@ -249,7 +284,7 @@ work with the fields exposed by that struct:
|
|||
```go
|
||||
// runtime invokes the plugin to get the opaque types.Result
|
||||
// this may conform to any CNI spec version
|
||||
resultInterface, err := libcni.AddNetwork(netConf, runtimeConf)
|
||||
resultInterface, err := libcni.AddNetwork(ctx, netConf, runtimeConf)
|
||||
|
||||
// upconvert result to the current 0.3.0 spec
|
||||
result, err := current.NewResultFromResult(resultInterface)
|
||||
|
|
16
MAINTAINERS
16
MAINTAINERS
|
@ -1,6 +1,14 @@
|
|||
Bryan Boreham <bryan@weave.works> (@bboreham)
|
||||
Casey Callendrello <casey.callendrello@coreos.com> (@squeed)
|
||||
Bruce Ma <brucema19901024@gmail.com> (@mars1024)
|
||||
Casey Callendrello <cdc@isovalent.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)
|
||||
Gabe Rosenhouse <grosenhouse@pivotal.io> (@rosenhouse)
|
||||
Matt Dupre <matt@tigera.io> (@matthewdupre)
|
||||
Stefan Junker <stefan.junker@coreos.com> (@steveeJ)
|
||||
Piotr Skamruk <piotr.skamruk@gmail.com> (@jellonek)
|
88
README.md
88
README.md
|
@ -1,22 +1,12 @@
|
|||
[](https://travis-ci.org/containernetworking/cni)
|
||||
[](https://ci.appveyor.com/project/cni-bot/cni/branch/master)
|
||||
[](https://coveralls.io/github/containernetworking/cni?branch=master)
|
||||
[](https://cryptic-tundra-43194.herokuapp.com/)
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
# Community Sync Meeting
|
||||
|
||||
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).
|
||||
|
||||
The next meeting will be held on *Wednesday, October 4th* at *3: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=2017-10-04&sln=15-16).
|
||||
|
||||
---
|
||||
|
||||
# CNI - the Container Network Interface
|
||||
|
||||
[](https://bestpractices.coreinfrastructure.org/projects/2446)
|
||||
[](https://securityscorecards.dev/viewer/?uri=github.com/containernetworking/cni)
|
||||
|
||||
## What is CNI?
|
||||
|
||||
CNI (_Container Network Interface_), a [Cloud Native Computing Foundation](https://cncf.io) project, consists of a specification and libraries for writing plugins to configure network interfaces in Linux containers, along with a number of supported plugins.
|
||||
|
@ -28,6 +18,19 @@ 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.
|
||||
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?
|
||||
|
||||
Application containers on Linux are a rapidly evolving area, and within this area networking is not well addressed as it is highly environment-specific.
|
||||
|
@ -37,44 +40,49 @@ To avoid duplication, we think it is prudent to define a common interface betwee
|
|||
|
||||
## Who is using CNI?
|
||||
### Container runtimes
|
||||
- [rkt - container engine](https://coreos.com/blog/rkt-cni-networking.html)
|
||||
- [Kubernetes - a system to simplify container operations](http://kubernetes.io/docs/admin/network-plugins/)
|
||||
- [Kubernetes - a system to simplify container operations](https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/network-plugins/)
|
||||
- [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)
|
||||
- [Apache Mesos - a distributed systems kernel](https://github.com/apache/mesos/blob/master/docs/cni.md)
|
||||
- [Amazon ECS - a highly scalable, high performance container management service](https://aws.amazon.com/ecs/)
|
||||
- [Singularity - container platform optimized for HPC, EPC, and AI](https://github.com/sylabs/singularity)
|
||||
- [OpenSVC - orchestrator for legacy and containerized application stacks](https://docs.opensvc.com/latest/fr/agent.configure.cni.html)
|
||||
|
||||
### 3rd party plugins
|
||||
- [Project Calico - a layer 3 virtual network](https://github.com/projectcalico/calico-cni)
|
||||
- [Weave - a multi-host Docker network](https://github.com/weaveworks/weave)
|
||||
- [Project Calico - a layer 3 virtual network](https://github.com/projectcalico/calico)
|
||||
- [Contiv Networking - policy networking for various use cases](https://github.com/contiv/netplugin)
|
||||
- [SR-IOV](https://github.com/hustcat/sriov-cni)
|
||||
- [Cilium - BPF & XDP for containers](https://github.com/cilium/cilium)
|
||||
- [Infoblox - enterprise IP address management for containers](https://github.com/infobloxopen/cni-infoblox)
|
||||
- [Multus - a Multi plugin](https://github.com/Intel-Corp/multus-cni)
|
||||
- [Cilium - eBPF & XDP for containers](https://github.com/cilium/cilium)
|
||||
- [Multus - a Multi plugin](https://github.com/k8snetworkplumbingwg/multus-cni)
|
||||
- [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)
|
||||
- [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)
|
||||
- [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)
|
||||
- [Bonding CNI - a Link aggregating plugin to address failover and high availability network](https://github.com/Intel-Corp/bond-cni)
|
||||
- [ovn-kubernetes - an container network plugin built on Open vSwitch (OVS) and Open Virtual Networking (OVN) with support for both Linux and Windows](https://github.com/openvswitch/ovn-kubernetes)
|
||||
- [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)
|
||||
- [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)
|
||||
- [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).
|
||||
|
||||
|
||||
## 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?
|
||||
|
||||
### Requirements
|
||||
|
||||
The CNI spec is language agnostic. To use the Go language libraries in this repository, you'll need a recent version of Go. Our [automated tests](https://travis-ci.org/containernetworking/cni/builds) cover Go versions 1.7 and 1.8.
|
||||
The CNI spec is language agnostic. To use the Go language libraries in this repository, you'll need a recent version of Go. You can find the Go versions covered by our [automated tests](https://travis-ci.org/containernetworking/cni/builds) in [.travis.yaml](.travis.yml).
|
||||
|
||||
### Reference Plugins
|
||||
|
||||
|
@ -111,6 +119,7 @@ EOF
|
|||
$ cat >/etc/cni/net.d/99-loopback.conf <<EOF
|
||||
{
|
||||
"cniVersion": "0.2.0",
|
||||
"name": "lo",
|
||||
"type": "loopback"
|
||||
}
|
||||
EOF
|
||||
|
@ -122,7 +131,7 @@ Next, build the plugins:
|
|||
|
||||
```bash
|
||||
$ cd $GOPATH/src/github.com/containernetworking/plugins
|
||||
$ ./build.sh
|
||||
$ ./build_linux.sh # or build_windows.sh
|
||||
```
|
||||
|
||||
Finally, execute a command (`ifconfig` in this example) in a private network namespace that has joined the `mynet` network:
|
||||
|
@ -182,17 +191,30 @@ lo Link encap:Local Loopback
|
|||
|
||||
## What might CNI do in the future?
|
||||
|
||||
CNI currently covers a wide range of needs for network configuration due to it simple model and API.
|
||||
CNI currently covers a wide range of needs for network configuration due to its simple model and API.
|
||||
However, in the future CNI might want to branch out into other directions:
|
||||
|
||||
- Dynamic updates to existing network configuration
|
||||
- Dynamic policies for network bandwidth and firewall rules
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## 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
|
||||
|
||||
For any questions about CNI, please reach out on the mailing list:
|
||||
For any questions about CNI, please reach out via:
|
||||
- Email: [cni-dev](https://groups.google.com/forum/#!forum/cni-dev)
|
||||
- IRC: #[containernetworking](irc://irc.freenode.org:6667/#containernetworking) channel on freenode.org
|
||||
- Slack: [containernetworking.slack.com](https://cryptic-tundra-43194.herokuapp.com)
|
||||
- IRC: #[containernetworking](irc://irc.freenode.net:6667/#containernetworking) channel on [freenode.net](https://freenode.net/)
|
||||
- Slack: #cni on the [CNCF slack](https://slack.cncf.io/). NOTE: the previous CNI Slack (containernetworking.slack.com) has been sunsetted.
|
||||
|
||||
## Security
|
||||
|
||||
If you have a _security_ issue to report, please do so privately to the email addresses listed in the [MAINTAINERS](MAINTAINERS) file.
|
||||
|
|
35
RELEASING.md
35
RELEASING.md
|
@ -1,34 +1,19 @@
|
|||
# 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
|
||||
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.
|
||||
|
||||
## Creating the release artifacts
|
||||
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. 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.
|
||||
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.
|
||||
|
||||
## Publishing the release
|
||||
|
||||
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. Push the tag to git `git push origin <TAG>`
|
||||
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. Announce the release on at least the CNI mailing, IRC and Slack.
|
||||
|
||||
|
|
15
ROADMAP.md
15
ROADMAP.md
|
@ -5,19 +5,14 @@ The list below is not complete, and we advise to get the current project state f
|
|||
|
||||
## 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)
|
||||
|
||||
- Targeted for April 2020
|
||||
- More precise specification language
|
||||
- GET action
|
||||
- Conformance test suite for CNI plugins (both reference and 3rd party)
|
||||
- Stable SPEC
|
||||
- Complete test coverage
|
||||
- Signed release binaries
|
||||
|
||||
### Beyond v1.0.0
|
||||
|
||||
- Conformance test suite for CNI plugins (both reference and 3rd party)
|
||||
- Signed release binaries
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
# -*- 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.10.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
|
|
@ -3,45 +3,75 @@
|
|||
`cnitool` is a simple program that executes a CNI configuration. It will
|
||||
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
|
||||
|
||||
First, install cnitool:
|
||||
|
||||
```
|
||||
```bash
|
||||
go get github.com/containernetworking/cni
|
||||
go install github.com/containernetworking/cni/cnitool
|
||||
```
|
||||
|
||||
Then, check out and build the plugins. All commands should be run from this directory.
|
||||
```
|
||||
|
||||
```bash
|
||||
git clone https://github.com/containernetworking/plugins.git
|
||||
cd plugins
|
||||
./build.sh
|
||||
./build_linux.sh
|
||||
# or
|
||||
./build_windows.sh
|
||||
```
|
||||
|
||||
Create a network configuration
|
||||
```
|
||||
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
|
||||
|
||||
```bash
|
||||
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`:
|
||||
|
||||
```
|
||||
```bash
|
||||
sudo ip netns add testing
|
||||
```
|
||||
|
||||
Add the container to the network:
|
||||
```
|
||||
|
||||
```bash
|
||||
sudo CNI_PATH=./bin cnitool add myptp /var/run/netns/testing
|
||||
```
|
||||
|
||||
Test that it works:
|
||||
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:
|
||||
|
||||
```bash
|
||||
sudo ip -n testing addr
|
||||
sudo ip netns exec testing ping -c 1 4.2.2.2
|
||||
```
|
||||
|
||||
And clean up:
|
||||
```
|
||||
|
||||
```bash
|
||||
sudo CNI_PATH=./bin cnitool del myptp /var/run/netns/testing
|
||||
sudo ip netns del testing
|
||||
```
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha512"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
@ -25,16 +26,21 @@ import (
|
|||
"github.com/containernetworking/cni/libcni"
|
||||
)
|
||||
|
||||
// Protocol parameters are passed to the plugins via OS environment variables.
|
||||
const (
|
||||
EnvCNIPath = "CNI_PATH"
|
||||
EnvNetDir = "NETCONFPATH"
|
||||
EnvCapabilityArgs = "CAP_ARGS"
|
||||
EnvCNIArgs = "CNI_ARGS"
|
||||
EnvCNIIfname = "CNI_IFNAME"
|
||||
|
||||
DefaultNetDir = "/etc/cni/net.d"
|
||||
|
||||
CmdAdd = "add"
|
||||
CmdCheck = "check"
|
||||
CmdDel = "del"
|
||||
CmdGC = "gc"
|
||||
CmdStatus = "status"
|
||||
)
|
||||
|
||||
func parseArgs(args string) ([][2]string, error) {
|
||||
|
@ -54,16 +60,15 @@ func parseArgs(args string) ([][2]string, error) {
|
|||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 3 {
|
||||
if len(os.Args) < 4 {
|
||||
usage()
|
||||
return
|
||||
}
|
||||
|
||||
netdir := os.Getenv(EnvNetDir)
|
||||
if netdir == "" {
|
||||
netdir = DefaultNetDir
|
||||
}
|
||||
netconf, err := libcni.LoadConfList(netdir, os.Args[2])
|
||||
netconf, err := libcni.LoadNetworkConf(netdir, os.Args[2])
|
||||
if err != nil {
|
||||
exit(err)
|
||||
}
|
||||
|
@ -85,6 +90,11 @@ func main() {
|
|||
}
|
||||
}
|
||||
|
||||
ifName, ok := os.LookupEnv(EnvCNIIfname)
|
||||
if !ok {
|
||||
ifName = "eth0"
|
||||
}
|
||||
|
||||
netns := os.Args[3]
|
||||
netns, err = filepath.Abs(netns)
|
||||
if err != nil {
|
||||
|
@ -100,29 +110,40 @@ func main() {
|
|||
rt := &libcni.RuntimeConf{
|
||||
ContainerID: containerID,
|
||||
NetNS: netns,
|
||||
IfName: "eth0",
|
||||
IfName: ifName,
|
||||
Args: cniArgs,
|
||||
CapabilityArgs: capabilityArgs,
|
||||
}
|
||||
|
||||
switch os.Args[1] {
|
||||
case CmdAdd:
|
||||
result, err := cninet.AddNetworkList(netconf, rt)
|
||||
result, err := cninet.AddNetworkList(context.TODO(), netconf, rt)
|
||||
if result != nil {
|
||||
_ = result.Print()
|
||||
}
|
||||
exit(err)
|
||||
case CmdCheck:
|
||||
err := cninet.CheckNetworkList(context.TODO(), netconf, rt)
|
||||
exit(err)
|
||||
case CmdDel:
|
||||
exit(cninet.DelNetworkList(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() {
|
||||
exe := filepath.Base(os.Args[0])
|
||||
|
||||
fmt.Fprintf(os.Stderr, "%s: Add or remove network interfaces from a network namespace\n", exe)
|
||||
fmt.Fprintf(os.Stderr, " %s %s <net> <netns>\n", exe, CmdAdd)
|
||||
fmt.Fprintf(os.Stderr, " %s %s <net> <netns>\n", exe, CmdDel)
|
||||
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 <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 gc <net> <netns>\n", exe)
|
||||
fmt.Fprintf(os.Stderr, " %s status <net> <netns>\n", exe)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
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
|
||||
)
|
|
@ -0,0 +1,36 @@
|
|||
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=
|
737
libcni/api.go
737
libcni/api.go
|
@ -14,21 +14,37 @@
|
|||
|
||||
package libcni
|
||||
|
||||
// Note this is the actual implementation of the CNI specification, which
|
||||
// is reflected in the SPEC.md file.
|
||||
// it is typically bundled into runtime providers (i.e. containerd or cri-o would use this
|
||||
// before calling runc or hcsshim). It is also bundled into CNI providers as well, for example,
|
||||
// to add an IP to a container, to parse the configuration of the CNI and so on.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/invoke"
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/pkg/types/create"
|
||||
"github.com/containernetworking/cni/pkg/utils"
|
||||
"github.com/containernetworking/cni/pkg/version"
|
||||
)
|
||||
|
||||
var (
|
||||
CacheDir = "/var/lib/cni"
|
||||
// slightly awkward wording to preserve anyone matching on error strings
|
||||
ErrorCheckNotSupp = fmt.Errorf("does not support the CHECK command")
|
||||
)
|
||||
|
||||
const (
|
||||
CNICacheV1 = "cniCacheV1"
|
||||
)
|
||||
|
||||
// A RuntimeConf holds the arguments to one invocation of a CNI plugin
|
||||
|
@ -47,35 +63,71 @@ type RuntimeConf struct {
|
|||
// to the plugin
|
||||
CapabilityArgs map[string]interface{}
|
||||
|
||||
// A cache directory in which to library data. Defaults to CacheDir
|
||||
// DEPRECATED. Will be removed in a future release.
|
||||
CacheDir string
|
||||
}
|
||||
|
||||
type NetworkConfig struct {
|
||||
Network *types.NetConf
|
||||
// Use PluginConfig instead of NetworkConfig, the NetworkConfig
|
||||
// backwards-compat alias will be removed in a future release.
|
||||
type NetworkConfig = PluginConfig
|
||||
|
||||
type PluginConfig struct {
|
||||
Network *types.PluginConf
|
||||
Bytes []byte
|
||||
}
|
||||
|
||||
type NetworkConfigList struct {
|
||||
Name string
|
||||
CNIVersion string
|
||||
Plugins []*NetworkConfig
|
||||
DisableCheck bool
|
||||
DisableGC bool
|
||||
LoadOnlyInlinedPlugins bool
|
||||
Plugins []*PluginConfig
|
||||
Bytes []byte
|
||||
}
|
||||
|
||||
type CNI interface {
|
||||
AddNetworkList(net *NetworkConfigList, rt *RuntimeConf) (types.Result, error)
|
||||
GetNetworkList(net *NetworkConfigList, rt *RuntimeConf) (types.Result, error)
|
||||
DelNetworkList(net *NetworkConfigList, rt *RuntimeConf) error
|
||||
type NetworkAttachment struct {
|
||||
ContainerID string
|
||||
Network string
|
||||
IfName string
|
||||
Config []byte
|
||||
NetNS string
|
||||
CniArgs [][2]string
|
||||
CapabilityArgs map[string]interface{}
|
||||
}
|
||||
|
||||
AddNetwork(net *NetworkConfig, rt *RuntimeConf) (types.Result, error)
|
||||
GetNetwork(net *NetworkConfig, rt *RuntimeConf) (types.Result, error)
|
||||
DelNetwork(net *NetworkConfig, rt *RuntimeConf) error
|
||||
type GCArgs struct {
|
||||
ValidAttachments []types.GCAttachment
|
||||
}
|
||||
|
||||
type CNI interface {
|
||||
AddNetworkList(ctx context.Context, net *NetworkConfigList, rt *RuntimeConf) (types.Result, error)
|
||||
CheckNetworkList(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)
|
||||
CheckNetwork(ctx context.Context, net *PluginConfig, rt *RuntimeConf) error
|
||||
DelNetwork(ctx context.Context, net *PluginConfig, rt *RuntimeConf) error
|
||||
GetNetworkCachedResult(net *PluginConfig, rt *RuntimeConf) (types.Result, error)
|
||||
GetNetworkCachedConfig(net *PluginConfig, rt *RuntimeConf) ([]byte, *RuntimeConf, error)
|
||||
|
||||
ValidateNetworkList(ctx context.Context, net *NetworkConfigList) ([]string, error)
|
||||
ValidateNetwork(ctx context.Context, net *PluginConfig) ([]string, error)
|
||||
|
||||
GCNetworkList(ctx context.Context, net *NetworkConfigList, args *GCArgs) error
|
||||
GetStatusNetworkList(ctx context.Context, net *NetworkConfigList) error
|
||||
|
||||
GetCachedAttachments(containerID string) ([]*NetworkAttachment, error)
|
||||
|
||||
GetVersionInfo(ctx context.Context, pluginType string) (version.PluginInfo, error)
|
||||
}
|
||||
|
||||
type CNIConfig struct {
|
||||
Path []string
|
||||
exec invoke.Exec
|
||||
cacheDir string
|
||||
}
|
||||
|
||||
// CNIConfig implements the CNI interface
|
||||
|
@ -85,13 +137,22 @@ var _ CNI = &CNIConfig{}
|
|||
// 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.
|
||||
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{
|
||||
Path: path,
|
||||
cacheDir: cacheDir,
|
||||
exec: exec,
|
||||
}
|
||||
}
|
||||
|
||||
func buildOneConfig(name, cniVersion string, orig *NetworkConfig, prevResult types.Result, rt *RuntimeConf) (*NetworkConfig, error) {
|
||||
func buildOneConfig(name, cniVersion string, orig *PluginConfig, prevResult types.Result, rt *RuntimeConf) (*PluginConfig, error) {
|
||||
var err error
|
||||
|
||||
inject := map[string]interface{}{
|
||||
|
@ -108,8 +169,11 @@ func buildOneConfig(name, cniVersion string, orig *NetworkConfig, prevResult typ
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if rt != nil {
|
||||
return injectRuntimeConfig(orig, rt)
|
||||
}
|
||||
|
||||
return orig, nil
|
||||
}
|
||||
|
||||
// This function takes a libcni RuntimeConf structure and injects values into
|
||||
|
@ -120,11 +184,11 @@ func buildOneConfig(name, cniVersion string, orig *NetworkConfig, prevResult typ
|
|||
// These capabilities arguments are filtered through the plugin's advertised
|
||||
// capabilities from its config JSON, and any keys in the CapabilityArgs
|
||||
// matching plugin capabilities are added to the "runtimeConfig" dictionary
|
||||
// sent to the plugin via JSON on stdin. For exmaple, if the plugin's
|
||||
// sent to the plugin via JSON on stdin. For example, if the plugin's
|
||||
// capabilities include "portMappings", and the CapabilityArgs map includes a
|
||||
// "portMappings" key, that key and its value are added to the "runtimeConfig"
|
||||
// dictionary to be passed to the plugin's stdin.
|
||||
func injectRuntimeConfig(orig *NetworkConfig, rt *RuntimeConf) (*NetworkConfig, error) {
|
||||
func injectRuntimeConfig(orig *PluginConfig, rt *RuntimeConf) (*PluginConfig, error) {
|
||||
var err error
|
||||
|
||||
rc := make(map[string]interface{})
|
||||
|
@ -158,76 +222,132 @@ func (c *CNIConfig) ensureExec() invoke.Exec {
|
|||
return c.exec
|
||||
}
|
||||
|
||||
func (c *CNIConfig) addOrGetNetwork(command, name, cniVersion string, net *NetworkConfig, prevResult types.Result, rt *RuntimeConf) (types.Result, error) {
|
||||
c.ensureExec()
|
||||
pluginPath, err := c.exec.FindInPath(net.Network.Type, c.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newConf, err := buildOneConfig(name, cniVersion, net, prevResult, rt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return invoke.ExecPluginWithResult(pluginPath, newConf.Bytes, c.args(command, rt), c.exec)
|
||||
type cachedInfo struct {
|
||||
Kind string `json:"kind"`
|
||||
ContainerID string `json:"containerId"`
|
||||
Config []byte `json:"config"`
|
||||
IfName string `json:"ifName"`
|
||||
NetworkName string `json:"networkName"`
|
||||
NetNS string `json:"netns,omitempty"`
|
||||
CniArgs [][2]string `json:"cniArgs,omitempty"`
|
||||
CapabilityArgs map[string]interface{} `json:"capabilityArgs,omitempty"`
|
||||
RawResult map[string]interface{} `json:"result,omitempty"`
|
||||
Result types.Result `json:"-"`
|
||||
}
|
||||
|
||||
// Note that only GET requests should pass an initial prevResult
|
||||
func (c *CNIConfig) addOrGetNetworkList(command string, prevResult types.Result, list *NetworkConfigList, rt *RuntimeConf) (types.Result, error) {
|
||||
var err error
|
||||
for _, net := range list.Plugins {
|
||||
prevResult, err = c.addOrGetNetwork(command, list.Name, list.CNIVersion, net, prevResult, rt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// getCacheDir returns the cache directory in this order:
|
||||
// 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 prevResult, nil
|
||||
return CacheDir
|
||||
}
|
||||
|
||||
func getResultCacheFilePath(netName string, rt *RuntimeConf) string {
|
||||
cacheDir := rt.CacheDir
|
||||
if cacheDir == "" {
|
||||
cacheDir = 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(cacheDir, "results", fmt.Sprintf("%s-%s", netName, rt.ContainerID))
|
||||
return filepath.Join(c.getCacheDir(rt), "results", fmt.Sprintf("%s-%s-%s", netName, rt.ContainerID, rt.IfName)), nil
|
||||
}
|
||||
|
||||
func setCachedResult(result types.Result, netName string, rt *RuntimeConf) error {
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fname := getResultCacheFilePath(netName, rt)
|
||||
if err := os.MkdirAll(filepath.Dir(fname), 0700); err != nil {
|
||||
|
||||
err = json.Unmarshal(data, &cached.RawResult)
|
||||
if err != nil {
|
||||
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 delCachedResult(netName string, rt *RuntimeConf) error {
|
||||
fname := getResultCacheFilePath(netName, rt)
|
||||
func (c *CNIConfig) cacheDel(netName string, rt *RuntimeConf) error {
|
||||
fname, err := c.getCacheFilePath(netName, rt)
|
||||
if err != nil {
|
||||
// Ignore error
|
||||
return nil
|
||||
}
|
||||
return os.Remove(fname)
|
||||
}
|
||||
|
||||
func getCachedResult(netName, cniVersion string, rt *RuntimeConf) (types.Result, error) {
|
||||
fname := getResultCacheFilePath(netName, rt)
|
||||
data, err := ioutil.ReadFile(fname)
|
||||
func (c *CNIConfig) getCachedConfig(netName string, rt *RuntimeConf) ([]byte, *RuntimeConf, error) {
|
||||
var bytes []byte
|
||||
|
||||
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 {
|
||||
// Ignore read errors; the cached result may not exist on-disk
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Read the version of the cached result
|
||||
decoder := version.ConfigDecoder{}
|
||||
resultCniVersion, err := decoder.Decode(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Ensure we can understand the result
|
||||
result, err := version.NewResult(resultCniVersion, data)
|
||||
// Load the cached result
|
||||
result, err := create.CreateFromBytes(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -237,43 +357,179 @@ func getCachedResult(netName, cniVersion string, rt *RuntimeConf) (types.Result,
|
|||
// should match the config version unless the config was changed
|
||||
// while the container was running.
|
||||
result, err = result.GetAsVersion(cniVersion)
|
||||
if err != nil && resultCniVersion != cniVersion {
|
||||
return nil, fmt.Errorf("failed to convert cached result version %q to config version %q: %v", resultCniVersion, cniVersion, err)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert cached result to config version %q: %w", cniVersion, err)
|
||||
}
|
||||
return result, err
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// AddNetworkList executes a sequence of plugins with the ADD command
|
||||
func (c *CNIConfig) AddNetworkList(list *NetworkConfigList, rt *RuntimeConf) (types.Result, error) {
|
||||
result, err := c.addOrGetNetworkList("ADD", nil, list, rt)
|
||||
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
|
||||
}
|
||||
|
||||
if err = setCachedResult(result, list.Name, rt); err != nil {
|
||||
return nil, fmt.Errorf("failed to set network '%s' cached result: %v", list.Name, 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
|
||||
// AddNetworkList() operation for a network list, or an error.
|
||||
func (c *CNIConfig) GetNetworkListCachedResult(list *NetworkConfigList, rt *RuntimeConf) (types.Result, error) {
|
||||
return c.getCachedResult(list.Name, list.CNIVersion, rt)
|
||||
}
|
||||
|
||||
// GetNetworkCachedResult returns the cached Result of the previous
|
||||
// AddNetwork() operation for a network, or an error.
|
||||
func (c *CNIConfig) GetNetworkCachedResult(net *PluginConfig, rt *RuntimeConf) (types.Result, error) {
|
||||
return c.getCachedResult(net.Network.Name, net.Network.CNIVersion, rt)
|
||||
}
|
||||
|
||||
// GetNetworkListCachedConfig copies the input RuntimeConf to output
|
||||
// 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()
|
||||
pluginPath, err := c.exec.FindInPath(net.Network.Type, c.Path)
|
||||
if err != nil {
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return invoke.ExecPluginWithResult(ctx, pluginPath, newConf.Bytes, c.args("ADD", rt), c.exec)
|
||||
}
|
||||
|
||||
// AddNetworkList executes a sequence of plugins with the ADD command
|
||||
func (c *CNIConfig) AddNetworkList(ctx context.Context, list *NetworkConfigList, rt *RuntimeConf) (types.Result, error) {
|
||||
var err error
|
||||
var result types.Result
|
||||
for _, net := range list.Plugins {
|
||||
result, err = c.addNetwork(ctx, list.Name, list.CNIVersion, net, result, rt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("plugin %s failed (add): %w", pluginDescription(net.Network), err)
|
||||
}
|
||||
}
|
||||
|
||||
if err = c.cacheAdd(result, list.Bytes, list.Name, rt); err != nil {
|
||||
return nil, fmt.Errorf("failed to set network %q cached result: %w", list.Name, err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetNetworkList executes a sequence of plugins with the GET command
|
||||
func (c *CNIConfig) GetNetworkList(list *NetworkConfigList, rt *RuntimeConf) (types.Result, error) {
|
||||
// GET was added in CNI spec version 0.4.0 and higher
|
||||
if gtet, err := version.GreaterThanOrEqualTo(list.CNIVersion, "0.4.0"); err != nil {
|
||||
return nil, err
|
||||
} else if !gtet {
|
||||
return nil, fmt.Errorf("configuration version %q does not support the GET command", list.CNIVersion)
|
||||
}
|
||||
|
||||
cachedResult, err := getCachedResult(list.Name, list.CNIVersion, rt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get network '%s' cached result: %v", list.Name, err)
|
||||
}
|
||||
return c.addOrGetNetworkList("GET", cachedResult, list, rt)
|
||||
}
|
||||
|
||||
func (c *CNIConfig) delNetwork(name, cniVersion string, net *NetworkConfig, prevResult types.Result, rt *RuntimeConf) error {
|
||||
func (c *CNIConfig) checkNetwork(ctx context.Context, name, cniVersion string, net *PluginConfig, prevResult types.Result, rt *RuntimeConf) error {
|
||||
c.ensureExec()
|
||||
pluginPath, err := c.exec.FindInPath(net.Network.Type, c.Path)
|
||||
if err != nil {
|
||||
|
@ -285,95 +541,350 @@ func (c *CNIConfig) delNetwork(name, cniVersion string, net *NetworkConfig, prev
|
|||
return err
|
||||
}
|
||||
|
||||
return invoke.ExecPluginWithoutResult(pluginPath, newConf.Bytes, c.args("DEL", rt), c.exec)
|
||||
return invoke.ExecPluginWithoutResult(ctx, pluginPath, newConf.Bytes, c.args("CHECK", rt), c.exec)
|
||||
}
|
||||
|
||||
// CheckNetworkList executes a sequence of plugins with the CHECK command
|
||||
func (c *CNIConfig) CheckNetworkList(ctx context.Context, list *NetworkConfigList, rt *RuntimeConf) error {
|
||||
// CHECK was added in CNI spec version 0.4.0 and higher
|
||||
if gtet, err := version.GreaterThanOrEqualTo(list.CNIVersion, "0.4.0"); err != nil {
|
||||
return err
|
||||
} else if !gtet {
|
||||
return fmt.Errorf("configuration version %q %w", list.CNIVersion, ErrorCheckNotSupp)
|
||||
}
|
||||
|
||||
if list.DisableCheck {
|
||||
return nil
|
||||
}
|
||||
|
||||
cachedResult, err := c.getCachedResult(list.Name, list.CNIVersion, rt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get network %q cached result: %w", list.Name, err)
|
||||
}
|
||||
|
||||
for _, net := range list.Plugins {
|
||||
if err := c.checkNetwork(ctx, list.Name, list.CNIVersion, net, cachedResult, rt); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CNIConfig) delNetwork(ctx context.Context, name, cniVersion string, net *PluginConfig, prevResult types.Result, rt *RuntimeConf) error {
|
||||
c.ensureExec()
|
||||
pluginPath, err := c.exec.FindInPath(net.Network.Type, c.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newConf, err := buildOneConfig(name, cniVersion, net, prevResult, rt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return invoke.ExecPluginWithoutResult(ctx, pluginPath, newConf.Bytes, c.args("DEL", rt), c.exec)
|
||||
}
|
||||
|
||||
// DelNetworkList executes a sequence of plugins with the DEL command
|
||||
func (c *CNIConfig) DelNetworkList(list *NetworkConfigList, rt *RuntimeConf) error {
|
||||
func (c *CNIConfig) DelNetworkList(ctx context.Context, list *NetworkConfigList, rt *RuntimeConf) error {
|
||||
var cachedResult types.Result
|
||||
|
||||
// Cached result on DEL was added in CNI spec version 0.4.0 and higher
|
||||
if gtet, err := version.GreaterThanOrEqualTo(list.CNIVersion, "0.4.0"); err != nil {
|
||||
return err
|
||||
} else if gtet {
|
||||
cachedResult, err = getCachedResult(list.Name, list.CNIVersion, rt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get network '%s' cached result: %v", list.Name, err)
|
||||
if cachedResult, err = c.getCachedResult(list.Name, list.CNIVersion, rt); err != nil {
|
||||
_ = c.cacheDel(list.Name, rt)
|
||||
cachedResult = nil
|
||||
}
|
||||
}
|
||||
|
||||
for i := len(list.Plugins) - 1; i >= 0; i-- {
|
||||
net := list.Plugins[i]
|
||||
if err := c.delNetwork(list.Name, list.CNIVersion, net, cachedResult, rt); err != nil {
|
||||
return err
|
||||
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)
|
||||
}
|
||||
}
|
||||
_ = delCachedResult(list.Name, rt)
|
||||
|
||||
_ = c.cacheDel(list.Name, rt)
|
||||
|
||||
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
|
||||
func (c *CNIConfig) AddNetwork(net *NetworkConfig, rt *RuntimeConf) (types.Result, error) {
|
||||
result, err := c.addOrGetNetwork("ADD", net.Network.Name, net.Network.CNIVersion, net, nil, rt)
|
||||
func (c *CNIConfig) AddNetwork(ctx context.Context, net *PluginConfig, rt *RuntimeConf) (types.Result, error) {
|
||||
result, err := c.addNetwork(ctx, net.Network.Name, net.Network.CNIVersion, net, nil, rt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = setCachedResult(result, net.Network.Name, rt); err != nil {
|
||||
return nil, fmt.Errorf("failed to set network '%s' cached result: %v", net.Network.Name, err)
|
||||
if err = c.cacheAdd(result, net.Bytes, net.Network.Name, rt); err != nil {
|
||||
return nil, fmt.Errorf("failed to set network %q cached result: %w", net.Network.Name, err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetNetwork executes the plugin with the GET command
|
||||
func (c *CNIConfig) GetNetwork(net *NetworkConfig, rt *RuntimeConf) (types.Result, error) {
|
||||
// GET was added in CNI spec version 0.4.0 and higher
|
||||
// CheckNetwork executes the plugin with the CHECK command
|
||||
func (c *CNIConfig) CheckNetwork(ctx context.Context, net *PluginConfig, rt *RuntimeConf) error {
|
||||
// CHECK was added in CNI spec version 0.4.0 and higher
|
||||
if gtet, err := version.GreaterThanOrEqualTo(net.Network.CNIVersion, "0.4.0"); err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
} else if !gtet {
|
||||
return nil, fmt.Errorf("configuration version %q does not support the GET command", net.Network.CNIVersion)
|
||||
return fmt.Errorf("configuration version %q %w", net.Network.CNIVersion, ErrorCheckNotSupp)
|
||||
}
|
||||
|
||||
cachedResult, err := getCachedResult(net.Network.Name, net.Network.CNIVersion, rt)
|
||||
cachedResult, err := c.getCachedResult(net.Network.Name, net.Network.CNIVersion, rt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get network '%s' cached result: %v", net.Network.Name, err)
|
||||
return fmt.Errorf("failed to get network %q cached result: %w", net.Network.Name, err)
|
||||
}
|
||||
return c.addOrGetNetwork("GET", 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
|
||||
func (c *CNIConfig) DelNetwork(net *NetworkConfig, rt *RuntimeConf) error {
|
||||
func (c *CNIConfig) DelNetwork(ctx context.Context, net *PluginConfig, rt *RuntimeConf) error {
|
||||
var cachedResult types.Result
|
||||
|
||||
// Cached result on DEL was added in CNI spec version 0.4.0 and higher
|
||||
if gtet, err := version.GreaterThanOrEqualTo(net.Network.CNIVersion, "0.4.0"); err != nil {
|
||||
return err
|
||||
} else if gtet {
|
||||
cachedResult, err = getCachedResult(net.Network.Name, net.Network.CNIVersion, rt)
|
||||
cachedResult, err = c.getCachedResult(net.Network.Name, net.Network.CNIVersion, rt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get network '%s' cached result: %v", net.Network.Name, err)
|
||||
return fmt.Errorf("failed to get network %q cached result: %w", net.Network.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.delNetwork(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
|
||||
}
|
||||
_ = delCachedResult(net.Network.Name, rt)
|
||||
_ = c.cacheDel(net.Network.Name, rt)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateNetworkList checks that a configuration is reasonably valid.
|
||||
// - all the specified plugins exist on disk
|
||||
// - every plugin supports the desired version.
|
||||
//
|
||||
// Returns a list of all capabilities supported by the configuration, or error
|
||||
func (c *CNIConfig) ValidateNetworkList(ctx context.Context, list *NetworkConfigList) ([]string, error) {
|
||||
version := list.CNIVersion
|
||||
|
||||
// holding map for seen caps (in case of duplicates)
|
||||
caps := map[string]interface{}{}
|
||||
|
||||
errs := []error{}
|
||||
for _, net := range list.Plugins {
|
||||
if err := c.validatePlugin(ctx, net.Network.Type, version); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
for c, enabled := range net.Network.Capabilities {
|
||||
if !enabled {
|
||||
continue
|
||||
}
|
||||
caps[c] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return nil, fmt.Errorf("%v", errs)
|
||||
}
|
||||
|
||||
// make caps list
|
||||
cc := make([]string, 0, len(caps))
|
||||
for c := range caps {
|
||||
cc = append(cc, c)
|
||||
}
|
||||
|
||||
return cc, nil
|
||||
}
|
||||
|
||||
// ValidateNetwork checks that a configuration is reasonably valid.
|
||||
// It uses the same logic as ValidateNetworkList)
|
||||
// Returns a list of capabilities
|
||||
func (c *CNIConfig) ValidateNetwork(ctx context.Context, net *PluginConfig) ([]string, error) {
|
||||
caps := []string{}
|
||||
for c, ok := range net.Network.Capabilities {
|
||||
if ok {
|
||||
caps = append(caps, c)
|
||||
}
|
||||
}
|
||||
if err := c.validatePlugin(ctx, net.Network.Type, net.Network.CNIVersion); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return caps, nil
|
||||
}
|
||||
|
||||
// validatePlugin checks that an individual plugin's configuration is sane
|
||||
func (c *CNIConfig) validatePlugin(ctx context.Context, pluginName, expectedVersion string) error {
|
||||
c.ensureExec()
|
||||
pluginPath, err := c.exec.FindInPath(pluginName, c.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if expectedVersion == "" {
|
||||
expectedVersion = "0.1.0"
|
||||
}
|
||||
|
||||
vi, err := invoke.GetVersionInfo(ctx, pluginPath, c.exec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, vers := range vi.SupportedVersions() {
|
||||
if vers == expectedVersion {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("plugin %s does not support config version %q", pluginName, expectedVersion)
|
||||
}
|
||||
|
||||
// GetVersionInfo reports which versions of the CNI spec are supported by
|
||||
// the given plugin.
|
||||
func (c *CNIConfig) GetVersionInfo(pluginType string) (version.PluginInfo, error) {
|
||||
func (c *CNIConfig) GetVersionInfo(ctx context.Context, pluginType string) (version.PluginInfo, error) {
|
||||
c.ensureExec()
|
||||
pluginPath, err := c.exec.FindInPath(pluginType, c.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return invoke.GetVersionInfo(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)
|
||||
}
|
||||
|
||||
// =====
|
||||
|
|
1342
libcni/api_test.go
1342
libcni/api_test.go
File diff suppressed because it is too large
Load Diff
|
@ -15,19 +15,20 @@
|
|||
package libcni_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/containernetworking/cni/libcni"
|
||||
"github.com/containernetworking/cni/pkg/version/legacy_examples"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gexec"
|
||||
|
||||
"github.com/containernetworking/cni/libcni"
|
||||
"github.com/containernetworking/cni/pkg/version/legacy_examples"
|
||||
)
|
||||
|
||||
var _ = Describe("Backwards compatibility", func() {
|
||||
|
@ -35,7 +36,7 @@ var _ = Describe("Backwards compatibility", func() {
|
|||
|
||||
BeforeEach(func() {
|
||||
var err error
|
||||
cacheDirPath, err = ioutil.TempDir("", "cni_cachedir")
|
||||
cacheDirPath, err = os.MkdirTemp("", "cni_cachedir")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
|
@ -56,16 +57,18 @@ var _ = Describe("Backwards compatibility", func() {
|
|||
ContainerID: "some-container-id",
|
||||
NetNS: "/some/netns/path",
|
||||
IfName: "eth0",
|
||||
CacheDir: cacheDirPath,
|
||||
}
|
||||
|
||||
cniConfig := libcni.NewCNIConfig([]string{filepath.Dir(pluginPath)}, nil)
|
||||
cniConfig := libcni.NewCNIConfigWithCacheDir([]string{filepath.Dir(pluginPath)}, cacheDirPath, nil)
|
||||
|
||||
result, err := cniConfig.AddNetwork(netConf, runtimeConf)
|
||||
result, err := cniConfig.AddNetwork(context.TODO(), netConf, runtimeConf)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(result).To(Equal(legacy_examples.ExpectedResult))
|
||||
|
||||
err = cniConfig.DelNetwork(context.TODO(), netConf, runtimeConf)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(os.RemoveAll(pluginPath)).To(Succeed())
|
||||
})
|
||||
|
||||
|
|
246
libcni/conf.go
246
libcni/conf.go
|
@ -16,11 +16,16 @@ package libcni
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/pkg/version"
|
||||
)
|
||||
|
||||
type NotFoundError struct {
|
||||
|
@ -40,10 +45,17 @@ func (e NoConfigsFoundError) Error() string {
|
|||
return fmt.Sprintf(`no net configurations found in %s`, e.Dir)
|
||||
}
|
||||
|
||||
func ConfFromBytes(bytes []byte) (*NetworkConfig, error) {
|
||||
conf := &NetworkConfig{Bytes: bytes}
|
||||
if err := json.Unmarshal(bytes, &conf.Network); err != nil {
|
||||
return nil, fmt.Errorf("error parsing configuration: %s", err)
|
||||
// This will not validate that the plugins actually belong to the netconfig by ensuring
|
||||
// that they are loaded from a directory named after the networkName, relative to the network config.
|
||||
//
|
||||
// Since here we are just accepting raw bytes, the caller is responsible for ensuring that the plugin
|
||||
// config provided here actually "belongs" to the networkconfig in question.
|
||||
func NetworkPluginConfFromBytes(pluginConfBytes []byte) (*PluginConfig, error) {
|
||||
// TODO why are we creating a struct that holds both the byte representation and the deserialized
|
||||
// representation, and returning that, instead of just returning the deserialized representation?
|
||||
conf := &PluginConfig{Bytes: pluginConfBytes, Network: &types.PluginConf{}}
|
||||
if err := json.Unmarshal(pluginConfBytes, conf.Network); err != nil {
|
||||
return nil, fmt.Errorf("error parsing configuration: %w", err)
|
||||
}
|
||||
if conf.Network.Type == "" {
|
||||
return nil, fmt.Errorf("error parsing configuration: missing 'type'")
|
||||
|
@ -51,18 +63,36 @@ func ConfFromBytes(bytes []byte) (*NetworkConfig, error) {
|
|||
return conf, nil
|
||||
}
|
||||
|
||||
func ConfFromFile(filename string) (*NetworkConfig, error) {
|
||||
bytes, err := ioutil.ReadFile(filename)
|
||||
// Given a path to a directory containing a network configuration, and the name of a network,
|
||||
// loads all plugin definitions found at path `networkConfPath/networkName/*.conf`
|
||||
func NetworkPluginConfsFromFiles(networkConfPath, networkName string) ([]*PluginConfig, error) {
|
||||
var pConfs []*PluginConfig
|
||||
|
||||
pluginConfPath := filepath.Join(networkConfPath, networkName)
|
||||
|
||||
pluginConfFiles, err := ConfFiles(pluginConfPath, []string{".conf"})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading %s: %s", filename, err)
|
||||
return nil, fmt.Errorf("failed to read plugin config files in %s: %w", pluginConfPath, err)
|
||||
}
|
||||
return ConfFromBytes(bytes)
|
||||
|
||||
for _, pluginConfFile := range pluginConfFiles {
|
||||
pluginConfBytes, err := os.ReadFile(pluginConfFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading %s: %w", pluginConfFile, err)
|
||||
}
|
||||
pluginConf, err := NetworkPluginConfFromBytes(pluginConfBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pConfs = append(pConfs, pluginConf)
|
||||
}
|
||||
return pConfs, nil
|
||||
}
|
||||
|
||||
func ConfListFromBytes(bytes []byte) (*NetworkConfigList, error) {
|
||||
func NetworkConfFromBytes(confBytes []byte) (*NetworkConfigList, error) {
|
||||
rawList := make(map[string]interface{})
|
||||
if err := json.Unmarshal(bytes, &rawList); err != nil {
|
||||
return nil, fmt.Errorf("error parsing configuration list: %s", err)
|
||||
if err := json.Unmarshal(confBytes, &rawList); err != nil {
|
||||
return nil, fmt.Errorf("error parsing configuration list: %w", err)
|
||||
}
|
||||
|
||||
rawName, ok := rawList["name"]
|
||||
|
@ -83,17 +113,115 @@ func ConfListFromBytes(bytes []byte) (*NetworkConfigList, error) {
|
|||
}
|
||||
}
|
||||
|
||||
rawVersions, ok := rawList["cniVersions"]
|
||||
if ok {
|
||||
// Parse the current package CNI version
|
||||
rvs, ok := rawVersions.([]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("error parsing configuration list: invalid type for cniVersions: %T", rvs)
|
||||
}
|
||||
vs := make([]string, 0, len(rvs))
|
||||
for i, rv := range rvs {
|
||||
v, ok := rv.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("error parsing configuration list: invalid type for cniVersions index %d: %T", i, rv)
|
||||
}
|
||||
gt, err := version.GreaterThan(v, version.Current())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing configuration list: invalid cniVersions entry %s at index %d: %w", v, i, err)
|
||||
} else if !gt {
|
||||
// Skip versions "greater" than this implementation of the spec
|
||||
vs = append(vs, v)
|
||||
}
|
||||
}
|
||||
|
||||
// if cniVersion was already set, append it to the list for sorting.
|
||||
if cniVersion != "" {
|
||||
gt, err := version.GreaterThan(cniVersion, version.Current())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing configuration list: invalid cniVersion %s: %w", cniVersion, err)
|
||||
} else if !gt {
|
||||
// ignore any versions higher than the current implemented spec version
|
||||
vs = append(vs, cniVersion)
|
||||
}
|
||||
}
|
||||
slices.SortFunc[[]string](vs, func(v1, v2 string) int {
|
||||
if v1 == v2 {
|
||||
return 0
|
||||
}
|
||||
if gt, _ := version.GreaterThan(v1, v2); gt {
|
||||
return 1
|
||||
}
|
||||
return -1
|
||||
})
|
||||
if len(vs) > 0 {
|
||||
cniVersion = vs[len(vs)-1]
|
||||
}
|
||||
}
|
||||
|
||||
readBool := func(key string) (bool, error) {
|
||||
rawVal, ok := rawList[key]
|
||||
if !ok {
|
||||
return false, nil
|
||||
}
|
||||
if b, ok := rawVal.(bool); ok {
|
||||
return b, nil
|
||||
}
|
||||
|
||||
s, ok := rawVal.(string)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("error parsing configuration list: invalid type %T for %s", rawVal, key)
|
||||
}
|
||||
s = strings.ToLower(s)
|
||||
switch s {
|
||||
case "false":
|
||||
return false, nil
|
||||
case "true":
|
||||
return true, nil
|
||||
}
|
||||
return false, fmt.Errorf("error parsing configuration list: invalid value %q for %s", s, key)
|
||||
}
|
||||
|
||||
disableCheck, err := readBool("disableCheck")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
disableGC, err := readBool("disableGC")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
loadOnlyInlinedPlugins, err := readBool("loadOnlyInlinedPlugins")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
list := &NetworkConfigList{
|
||||
Name: name,
|
||||
DisableCheck: disableCheck,
|
||||
DisableGC: disableGC,
|
||||
LoadOnlyInlinedPlugins: loadOnlyInlinedPlugins,
|
||||
CNIVersion: cniVersion,
|
||||
Bytes: bytes,
|
||||
Bytes: confBytes,
|
||||
}
|
||||
|
||||
var plugins []interface{}
|
||||
plug, ok := rawList["plugins"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("error parsing configuration list: no 'plugins' key")
|
||||
// We can have a `plugins` list key in the main conf,
|
||||
// We can also have `loadOnlyInlinedPlugins == true`
|
||||
//
|
||||
// If `plugins` is there, then `loadOnlyInlinedPlugins` can be true
|
||||
//
|
||||
// If plugins is NOT there, then `loadOnlyInlinedPlugins` cannot be true
|
||||
//
|
||||
// We have to have at least some plugins.
|
||||
if !ok && loadOnlyInlinedPlugins {
|
||||
return nil, fmt.Errorf("error parsing configuration list: `loadOnlyInlinedPlugins` is true, and no 'plugins' key")
|
||||
} else if !ok && !loadOnlyInlinedPlugins {
|
||||
return list, nil
|
||||
}
|
||||
|
||||
plugins, ok = plug.([]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("error parsing configuration list: invalid 'plugins' type %T", plug)
|
||||
|
@ -105,32 +233,76 @@ func ConfListFromBytes(bytes []byte) (*NetworkConfigList, error) {
|
|||
for i, conf := range plugins {
|
||||
newBytes, err := json.Marshal(conf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to marshal plugin config %d: %v", i, err)
|
||||
return nil, fmt.Errorf("failed to marshal plugin config %d: %w", i, err)
|
||||
}
|
||||
netConf, err := ConfFromBytes(newBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to parse plugin config %d: %v", i, err)
|
||||
return nil, fmt.Errorf("failed to parse plugin config %d: %w", i, err)
|
||||
}
|
||||
list.Plugins = append(list.Plugins, netConf)
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func ConfListFromFile(filename string) (*NetworkConfigList, error) {
|
||||
bytes, err := ioutil.ReadFile(filename)
|
||||
func NetworkConfFromFile(filename string) (*NetworkConfigList, error) {
|
||||
bytes, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading %s: %s", filename, err)
|
||||
return nil, fmt.Errorf("error reading %s: %w", filename, err)
|
||||
}
|
||||
return ConfListFromBytes(bytes)
|
||||
|
||||
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) {
|
||||
return NetworkConfFromFile(filename)
|
||||
}
|
||||
|
||||
// ConfFiles simply returns a slice of all files in the provided directory
|
||||
// with extensions matching the provided set.
|
||||
func ConfFiles(dir string, extensions []string) ([]string, error) {
|
||||
// In part, adapted from rkt/networking/podenv.go#listFiles
|
||||
files, err := ioutil.ReadDir(dir)
|
||||
files, err := os.ReadDir(dir)
|
||||
switch {
|
||||
case err == nil: // break
|
||||
case os.IsNotExist(err):
|
||||
// If folder not there, return no error - only return an
|
||||
// error if we cannot read contents or there are no contents.
|
||||
return nil, nil
|
||||
default:
|
||||
return nil, err
|
||||
|
@ -151,6 +323,7 @@ func ConfFiles(dir string, extensions []string) ([]string, error) {
|
|||
return confFiles, nil
|
||||
}
|
||||
|
||||
// Deprecated: This file format is no longer supported, use NetworkConfXXX and NetworkPluginXXX functions
|
||||
func LoadConf(dir, name string) (*NetworkConfig, error) {
|
||||
files, err := ConfFiles(dir, []string{".conf", ".json"})
|
||||
switch {
|
||||
|
@ -174,6 +347,15 @@ func LoadConf(dir, name string) (*NetworkConfig, error) {
|
|||
}
|
||||
|
||||
func LoadConfList(dir, name string) (*NetworkConfigList, error) {
|
||||
return LoadNetworkConf(dir, name)
|
||||
}
|
||||
|
||||
// LoadNetworkConf looks at all the network configs in a given dir,
|
||||
// loads and parses them all, and returns the first one with an extension of `.conf`
|
||||
// that matches the provided network name predicate.
|
||||
func LoadNetworkConf(dir, name string) (*NetworkConfigList, error) {
|
||||
// TODO this .conflist/.conf extension thing is confusing and inexact
|
||||
// for implementors. We should pick one extension for everything and stick with it.
|
||||
files, err := ConfFiles(dir, []string{".conflist"})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -181,7 +363,7 @@ func LoadConfList(dir, name string) (*NetworkConfigList, error) {
|
|||
sort.Strings(files)
|
||||
|
||||
for _, confFile := range files {
|
||||
conf, err := ConfListFromFile(confFile)
|
||||
conf, err := NetworkConfFromFile(confFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -190,12 +372,13 @@ func LoadConfList(dir, name string) (*NetworkConfigList, error) {
|
|||
}
|
||||
}
|
||||
|
||||
// Try and load a network configuration file (instead of list)
|
||||
// Deprecated: Try and load a network configuration file (instead of list)
|
||||
// from the same name, then upconvert.
|
||||
singleConf, err := LoadConf(dir, name)
|
||||
if err != nil {
|
||||
// A little extra logic so the error makes sense
|
||||
if _, ok := err.(NoConfigsFoundError); len(files) != 0 && ok {
|
||||
var ncfErr NoConfigsFoundError
|
||||
if len(files) != 0 && errors.As(err, &ncfErr) {
|
||||
// Config lists found but no config files found
|
||||
return nil, NotFoundError{dir, name}
|
||||
}
|
||||
|
@ -205,11 +388,12 @@ func LoadConfList(dir, name string) (*NetworkConfigList, error) {
|
|||
return ConfListFromConf(singleConf)
|
||||
}
|
||||
|
||||
func InjectConf(original *NetworkConfig, newValues map[string]interface{}) (*NetworkConfig, error) {
|
||||
// InjectConf takes a PluginConfig and inserts additional values into it, ensuring the result is serializable.
|
||||
func InjectConf(original *PluginConfig, newValues map[string]interface{}) (*PluginConfig, error) {
|
||||
config := make(map[string]interface{})
|
||||
err := json.Unmarshal(original.Bytes, &config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unmarshal existing network bytes: %s", err)
|
||||
return nil, fmt.Errorf("unmarshal existing network bytes: %w", err)
|
||||
}
|
||||
|
||||
for key, value := range newValues {
|
||||
|
@ -229,12 +413,14 @@ func InjectConf(original *NetworkConfig, newValues map[string]interface{}) (*Net
|
|||
return nil, err
|
||||
}
|
||||
|
||||
return ConfFromBytes(newBytes)
|
||||
return NetworkPluginConfFromBytes(newBytes)
|
||||
}
|
||||
|
||||
// ConfListFromConf "upconverts" a network config in to a NetworkConfigList,
|
||||
// with the single network as the only entry in the list.
|
||||
func ConfListFromConf(original *NetworkConfig) (*NetworkConfigList, error) {
|
||||
//
|
||||
// Deprecated: Non-conflist file formats are unsupported, use NetworkConfXXX and NetworkPluginXXX functions
|
||||
func ConfListFromConf(original *PluginConfig) (*NetworkConfigList, error) {
|
||||
// Re-deserialize the config's json, then make a raw map configlist.
|
||||
// This may seem a bit strange, but it's to make the Bytes fields
|
||||
// actually make sense. Otherwise, the generated json is littered with
|
||||
|
|
|
@ -15,14 +15,16 @@
|
|||
package libcni_test
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/containernetworking/cni/libcni"
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Loading configuration from disk", func() {
|
||||
|
@ -34,11 +36,11 @@ var _ = Describe("Loading configuration from disk", func() {
|
|||
|
||||
BeforeEach(func() {
|
||||
var err error
|
||||
configDir, err = ioutil.TempDir("", "plugin-conf")
|
||||
configDir, err = os.MkdirTemp("", "plugin-conf")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
pluginConfig = []byte(`{ "name": "some-plugin", "type": "foobar", "some-key": "some-value" }`)
|
||||
Expect(ioutil.WriteFile(filepath.Join(configDir, "50-whatever.conf"), pluginConfig, 0600)).To(Succeed())
|
||||
Expect(os.WriteFile(filepath.Join(configDir, "50-whatever.conf"), pluginConfig, 0o600)).To(Succeed())
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
|
@ -48,8 +50,8 @@ var _ = Describe("Loading configuration from disk", func() {
|
|||
It("finds the network config file for the plugin of the given type", func() {
|
||||
netConfig, err := libcni.LoadConf(configDir, "some-plugin")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(netConfig).To(Equal(&libcni.NetworkConfig{
|
||||
Network: &types.NetConf{
|
||||
Expect(netConfig).To(Equal(&libcni.PluginConfig{
|
||||
Network: &types.PluginConf{
|
||||
Name: "some-plugin",
|
||||
Type: "foobar",
|
||||
},
|
||||
|
@ -72,13 +74,13 @@ var _ = Describe("Loading configuration from disk", func() {
|
|||
BeforeEach(func() {
|
||||
Expect(os.Remove(configDir + "/50-whatever.conf")).To(Succeed())
|
||||
pluginConfig = []byte(`{ "name": "some-plugin", "some-key": "some-value", "type": "foobar" }`)
|
||||
Expect(ioutil.WriteFile(filepath.Join(configDir, "50-whatever.json"), pluginConfig, 0600)).To(Succeed())
|
||||
Expect(os.WriteFile(filepath.Join(configDir, "50-whatever.json"), pluginConfig, 0o600)).To(Succeed())
|
||||
})
|
||||
It("finds the network config file for the plugin of the given type", func() {
|
||||
netConfig, err := libcni.LoadConf(configDir, "some-plugin")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(netConfig).To(Equal(&libcni.NetworkConfig{
|
||||
Network: &types.NetConf{
|
||||
Expect(netConfig).To(Equal(&libcni.PluginConfig{
|
||||
Network: &types.PluginConf{
|
||||
Name: "some-plugin",
|
||||
Type: "foobar",
|
||||
},
|
||||
|
@ -96,7 +98,7 @@ var _ = Describe("Loading configuration from disk", func() {
|
|||
|
||||
Context("when a config file is malformed", func() {
|
||||
BeforeEach(func() {
|
||||
Expect(ioutil.WriteFile(filepath.Join(configDir, "00-bad.conf"), []byte(`{`), 0600)).To(Succeed())
|
||||
Expect(os.WriteFile(filepath.Join(configDir, "00-bad.conf"), []byte(`{`), 0o600)).To(Succeed())
|
||||
})
|
||||
|
||||
It("returns a useful error", func() {
|
||||
|
@ -108,10 +110,10 @@ var _ = Describe("Loading configuration from disk", func() {
|
|||
Context("when the config is in a nested subdir", func() {
|
||||
BeforeEach(func() {
|
||||
subdir := filepath.Join(configDir, "subdir1", "subdir2")
|
||||
Expect(os.MkdirAll(subdir, 0700)).To(Succeed())
|
||||
Expect(os.MkdirAll(subdir, 0o700)).To(Succeed())
|
||||
|
||||
pluginConfig = []byte(`{ "name": "deep", "some-key": "some-value" }`)
|
||||
Expect(ioutil.WriteFile(filepath.Join(subdir, "90-deep.conf"), pluginConfig, 0600)).To(Succeed())
|
||||
Expect(os.WriteFile(filepath.Join(subdir, "90-deep.conf"), pluginConfig, 0o600)).To(Succeed())
|
||||
})
|
||||
|
||||
It("will not find the config", func() {
|
||||
|
@ -126,11 +128,11 @@ var _ = Describe("Loading configuration from disk", func() {
|
|||
|
||||
BeforeEach(func() {
|
||||
var err error
|
||||
configDir, err = ioutil.TempDir("", "plugin-conf")
|
||||
configDir, err = os.MkdirTemp("", "plugin-conf")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
pluginConfig := []byte(`{ "name": "some-plugin", "type": "noop", "cniVersion": "0.3.1", "capabilities": { "portMappings": true, "somethingElse": true, "noCapability": false } }`)
|
||||
Expect(ioutil.WriteFile(filepath.Join(configDir, "50-whatever.conf"), pluginConfig, 0600)).To(Succeed())
|
||||
Expect(os.WriteFile(filepath.Join(configDir, "50-whatever.conf"), pluginConfig, 0o600)).To(Succeed())
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
|
@ -160,12 +162,12 @@ var _ = Describe("Loading configuration from disk", func() {
|
|||
var fileName, configDir string
|
||||
BeforeEach(func() {
|
||||
var err error
|
||||
configDir, err = ioutil.TempDir("", "plugin-conf")
|
||||
configDir, err = os.MkdirTemp("", "plugin-conf")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
fileName = filepath.Join(configDir, "50-whatever.conf")
|
||||
pluginConfig := []byte(`{ "name": "some-plugin", "some-key": "some-value" }`)
|
||||
Expect(ioutil.WriteFile(fileName, pluginConfig, 0600)).To(Succeed())
|
||||
Expect(os.WriteFile(fileName, pluginConfig, 0o600)).To(Succeed())
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
|
@ -179,7 +181,7 @@ var _ = Describe("Loading configuration from disk", func() {
|
|||
})
|
||||
})
|
||||
|
||||
Describe("ConfFromBytes", func() {
|
||||
Describe("NetworkPluginConfFromBytes", func() {
|
||||
Context("when the config is missing 'type'", func() {
|
||||
It("returns a useful error", func() {
|
||||
_, err := libcni.ConfFromBytes([]byte(`{ "name": "some-plugin", "some-key": "some-value" }`))
|
||||
|
@ -188,7 +190,7 @@ var _ = Describe("Loading configuration from disk", func() {
|
|||
})
|
||||
})
|
||||
|
||||
Describe("LoadConfList", func() {
|
||||
Describe("LoadNetworkConf", func() {
|
||||
var (
|
||||
configDir string
|
||||
configList []byte
|
||||
|
@ -196,12 +198,13 @@ var _ = Describe("Loading configuration from disk", func() {
|
|||
|
||||
BeforeEach(func() {
|
||||
var err error
|
||||
configDir, err = ioutil.TempDir("", "plugin-conf")
|
||||
configDir, err = os.MkdirTemp("", "plugin-conf")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
configList = []byte(`{
|
||||
"name": "some-list",
|
||||
"name": "some-network",
|
||||
"cniVersion": "0.2.0",
|
||||
"disableCheck": true,
|
||||
"plugins": [
|
||||
{
|
||||
"type": "host-local",
|
||||
|
@ -217,7 +220,7 @@ var _ = Describe("Loading configuration from disk", func() {
|
|||
}
|
||||
]
|
||||
}`)
|
||||
Expect(ioutil.WriteFile(filepath.Join(configDir, "50-whatever.conflist"), configList, 0600)).To(Succeed())
|
||||
Expect(os.WriteFile(filepath.Join(configDir, "50-whatever.conflist"), configList, 0o600)).To(Succeed())
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
|
@ -225,22 +228,23 @@ var _ = Describe("Loading configuration from disk", func() {
|
|||
})
|
||||
|
||||
It("finds the network config file for the plugin of the given type", func() {
|
||||
netConfigList, err := libcni.LoadConfList(configDir, "some-list")
|
||||
netConfigList, err := libcni.LoadNetworkConf(configDir, "some-network")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(netConfigList).To(Equal(&libcni.NetworkConfigList{
|
||||
Name: "some-list",
|
||||
Name: "some-network",
|
||||
CNIVersion: "0.2.0",
|
||||
Plugins: []*libcni.NetworkConfig{
|
||||
DisableCheck: true,
|
||||
Plugins: []*libcni.PluginConfig{
|
||||
{
|
||||
Network: &types.NetConf{Type: "host-local"},
|
||||
Network: &types.PluginConf{Type: "host-local"},
|
||||
Bytes: []byte(`{"subnet":"10.0.0.1/24","type":"host-local"}`),
|
||||
},
|
||||
{
|
||||
Network: &types.NetConf{Type: "bridge"},
|
||||
Network: &types.PluginConf{Type: "bridge"},
|
||||
Bytes: []byte(`{"mtu":1400,"type":"bridge"}`),
|
||||
},
|
||||
{
|
||||
Network: &types.NetConf{Type: "port-forwarding"},
|
||||
Network: &types.PluginConf{Type: "port-forwarding"},
|
||||
Bytes: []byte(`{"ports":{"20.0.0.1:8080":"80"},"type":"port-forwarding"}`),
|
||||
},
|
||||
},
|
||||
|
@ -251,25 +255,25 @@ var _ = Describe("Loading configuration from disk", func() {
|
|||
Context("when there is a config file with the same name as the list", func() {
|
||||
BeforeEach(func() {
|
||||
configFile := []byte(`{
|
||||
"name": "some-list",
|
||||
"name": "some-network",
|
||||
"cniVersion": "0.2.0",
|
||||
"type": "bridge"
|
||||
}`)
|
||||
Expect(ioutil.WriteFile(filepath.Join(configDir, "49-whatever.conf"), configFile, 0600)).To(Succeed())
|
||||
Expect(os.WriteFile(filepath.Join(configDir, "49-whatever.conf"), configFile, 0o600)).To(Succeed())
|
||||
})
|
||||
|
||||
It("Loads the config list first", func() {
|
||||
netConfigList, err := libcni.LoadConfList(configDir, "some-list")
|
||||
netConfigList, err := libcni.LoadNetworkConf(configDir, "some-network")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(len(netConfigList.Plugins)).To(Equal(3))
|
||||
Expect(netConfigList.Plugins).To(HaveLen(3))
|
||||
})
|
||||
|
||||
It("falls back to the config file", func() {
|
||||
Expect(os.Remove(filepath.Join(configDir, "50-whatever.conflist"))).To(Succeed())
|
||||
|
||||
netConfigList, err := libcni.LoadConfList(configDir, "some-list")
|
||||
netConfigList, err := libcni.LoadNetworkConf(configDir, "some-network")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(len(netConfigList.Plugins)).To(Equal(1))
|
||||
Expect(netConfigList.Plugins).To(HaveLen(1))
|
||||
Expect(netConfigList.Plugins[0].Network.Type).To(Equal("bridge"))
|
||||
})
|
||||
})
|
||||
|
@ -280,25 +284,25 @@ var _ = Describe("Loading configuration from disk", func() {
|
|||
})
|
||||
|
||||
It("returns a useful error", func() {
|
||||
_, err := libcni.LoadConfList(configDir, "some-plugin")
|
||||
_, err := libcni.LoadNetworkConf(configDir, "some-network")
|
||||
Expect(err).To(MatchError(libcni.NoConfigsFoundError{Dir: configDir}))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when there is no config for the desired plugin list", func() {
|
||||
Context("when there is no config for the desired network name", func() {
|
||||
It("returns a useful error", func() {
|
||||
_, err := libcni.LoadConfList(configDir, "some-other-plugin")
|
||||
Expect(err).To(MatchError(libcni.NotFoundError{Dir: configDir, Name: "some-other-plugin"}))
|
||||
_, err := libcni.LoadNetworkConf(configDir, "some-other-network")
|
||||
Expect(err).To(MatchError(libcni.NotFoundError{Dir: configDir, Name: "some-other-network"}))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when a config file is malformed", func() {
|
||||
BeforeEach(func() {
|
||||
Expect(ioutil.WriteFile(filepath.Join(configDir, "00-bad.conflist"), []byte(`{`), 0600)).To(Succeed())
|
||||
Expect(os.WriteFile(filepath.Join(configDir, "00-bad.conflist"), []byte(`{`), 0o600)).To(Succeed())
|
||||
})
|
||||
|
||||
It("returns a useful error", func() {
|
||||
_, err := libcni.LoadConfList(configDir, "some-plugin")
|
||||
_, err := libcni.LoadNetworkConf(configDir, "some-plugin")
|
||||
Expect(err).To(MatchError(`error parsing configuration list: unexpected end of JSON input`))
|
||||
})
|
||||
})
|
||||
|
@ -306,7 +310,7 @@ var _ = Describe("Loading configuration from disk", func() {
|
|||
Context("when the config is in a nested subdir", func() {
|
||||
BeforeEach(func() {
|
||||
subdir := filepath.Join(configDir, "subdir1", "subdir2")
|
||||
Expect(os.MkdirAll(subdir, 0700)).To(Succeed())
|
||||
Expect(os.MkdirAll(subdir, 0o700)).To(Succeed())
|
||||
|
||||
configList = []byte(`{
|
||||
"name": "deep",
|
||||
|
@ -318,37 +322,309 @@ var _ = Describe("Loading configuration from disk", func() {
|
|||
},
|
||||
]
|
||||
}`)
|
||||
Expect(ioutil.WriteFile(filepath.Join(subdir, "90-deep.conflist"), configList, 0600)).To(Succeed())
|
||||
Expect(os.WriteFile(filepath.Join(subdir, "90-deep.conflist"), configList, 0o600)).To(Succeed())
|
||||
})
|
||||
|
||||
It("will not find the config", func() {
|
||||
_, err := libcni.LoadConfList(configDir, "deep")
|
||||
_, err := libcni.LoadNetworkConf(configDir, "deep")
|
||||
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("ConfListFromFile", func() {
|
||||
Describe("NetworkConfFromFile", func() {
|
||||
Context("when the file cannot be opened", func() {
|
||||
It("returns a useful error", func() {
|
||||
_, err := libcni.ConfListFromFile("/tmp/nope/not-here")
|
||||
_, err := libcni.NetworkConfFromFile("/tmp/nope/not-here")
|
||||
Expect(err).To(MatchError(HavePrefix(`error reading /tmp/nope/not-here: open /tmp/nope/not-here`)))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("InjectConf", func() {
|
||||
var testNetConfig *libcni.NetworkConfig
|
||||
var testNetConfig *libcni.PluginConfig
|
||||
|
||||
BeforeEach(func() {
|
||||
testNetConfig = &libcni.NetworkConfig{Network: &types.NetConf{Name: "some-plugin", Type: "foobar"},
|
||||
Bytes: []byte(`{ "name": "some-plugin", "type": "foobar" }`)}
|
||||
testNetConfig = &libcni.PluginConfig{
|
||||
Network: &types.PluginConf{Name: "some-plugin", Type: "foobar"},
|
||||
Bytes: []byte(`{ "name": "some-plugin", "type": "foobar" }`),
|
||||
}
|
||||
})
|
||||
|
||||
Context("when function parameters are incorrect", func() {
|
||||
It("returns unmarshal error", func() {
|
||||
conf := &libcni.NetworkConfig{Network: &types.NetConf{Name: "some-plugin"},
|
||||
Bytes: []byte(`{ cc cc cc}`)}
|
||||
conf := &libcni.PluginConfig{
|
||||
Network: &types.PluginConf{Name: "some-plugin"},
|
||||
Bytes: []byte(`{ cc cc cc}`),
|
||||
}
|
||||
|
||||
_, err := libcni.InjectConf(conf, map[string]interface{}{"": nil})
|
||||
Expect(err).To(MatchError(HavePrefix(`unmarshal existing network bytes`)))
|
||||
|
@ -371,8 +647,8 @@ var _ = Describe("Loading configuration from disk", func() {
|
|||
|
||||
resultConfig, err := libcni.InjectConf(testNetConfig, map[string]interface{}{"test": "test"})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(resultConfig).To(Equal(&libcni.NetworkConfig{
|
||||
Network: &types.NetConf{
|
||||
Expect(resultConfig).To(Equal(&libcni.PluginConfig{
|
||||
Network: &types.PluginConf{
|
||||
Name: "some-plugin",
|
||||
Type: "foobar",
|
||||
},
|
||||
|
@ -389,8 +665,8 @@ var _ = Describe("Loading configuration from disk", func() {
|
|||
resultConfig, err = libcni.InjectConf(resultConfig, map[string]interface{}{"test": "changedValue"})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(resultConfig).To(Equal(&libcni.NetworkConfig{
|
||||
Network: &types.NetConf{
|
||||
Expect(resultConfig).To(Equal(&libcni.PluginConfig{
|
||||
Network: &types.PluginConf{
|
||||
Name: "some-plugin",
|
||||
Type: "foobar",
|
||||
},
|
||||
|
@ -407,8 +683,8 @@ var _ = Describe("Loading configuration from disk", func() {
|
|||
resultConfig, err = libcni.InjectConf(resultConfig, map[string]interface{}{"test": "test"})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(resultConfig).To(Equal(&libcni.NetworkConfig{
|
||||
Network: &types.NetConf{
|
||||
Expect(resultConfig).To(Equal(&libcni.PluginConfig{
|
||||
Network: &types.PluginConf{
|
||||
Name: "some-plugin",
|
||||
Type: "foobar",
|
||||
},
|
||||
|
@ -417,7 +693,6 @@ var _ = Describe("Loading configuration from disk", 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"}`)
|
||||
servers := []string{"server1", "server2"}
|
||||
newDNS := &types.DNS{Nameservers: servers, Domain: "local"}
|
||||
|
@ -430,8 +705,8 @@ var _ = Describe("Loading configuration from disk", func() {
|
|||
resultConfig, err = libcni.InjectConf(resultConfig, map[string]interface{}{"type": "bridge"})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(resultConfig).To(Equal(&libcni.NetworkConfig{
|
||||
Network: &types.NetConf{Name: "some-plugin", Type: "bridge", DNS: types.DNS{Nameservers: servers, Domain: "local"}},
|
||||
Expect(resultConfig).To(Equal(&libcni.PluginConfig{
|
||||
Network: &types.PluginConf{Name: "some-plugin", Type: "bridge", DNS: types.DNS{Nameservers: servers, Domain: "local"}},
|
||||
Bytes: expectedPluginConfig,
|
||||
}))
|
||||
})
|
||||
|
@ -439,8 +714,55 @@ 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 testNetConfig *libcni.NetworkConfig
|
||||
var testNetConfig *libcni.PluginConfig
|
||||
|
||||
BeforeEach(func() {
|
||||
pb := []byte(`{"name":"some-plugin","cniVersion":"0.3.1", "type":"foobar"}`)
|
||||
|
@ -462,16 +784,15 @@ var _ = Describe("ConfListFromConf", func() {
|
|||
Expect(ncl).To(Equal(&libcni.NetworkConfigList{
|
||||
Name: "some-plugin",
|
||||
CNIVersion: "0.3.1",
|
||||
Plugins: []*libcni.NetworkConfig{testNetConfig},
|
||||
Plugins: []*libcni.PluginConfig{testNetConfig},
|
||||
}))
|
||||
|
||||
//Test that the json unmarshals to the same data
|
||||
ncl2, err := libcni.ConfListFromBytes(bytes)
|
||||
// Test that the json unmarshals to the same data
|
||||
ncl2, err := libcni.NetworkConfFromBytes(bytes)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
ncl2.Bytes = nil
|
||||
ncl2.Plugins[0].Bytes = nil
|
||||
|
||||
Expect(ncl2).To(Equal(ncl))
|
||||
})
|
||||
|
||||
})
|
||||
|
|
|
@ -17,12 +17,11 @@ package libcni_test
|
|||
import (
|
||||
"encoding/json"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gexec"
|
||||
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLibcni(t *testing.T) {
|
||||
|
@ -32,13 +31,15 @@ func TestLibcni(t *testing.T) {
|
|||
|
||||
var pluginPackages = map[string]string{
|
||||
"noop": "github.com/containernetworking/cni/plugins/test/noop",
|
||||
"sleep": "github.com/containernetworking/cni/plugins/test/sleep",
|
||||
}
|
||||
|
||||
var pluginPaths map[string]string
|
||||
var pluginDirs []string // array of plugin dirs
|
||||
var (
|
||||
pluginPaths map[string]string
|
||||
pluginDirs []string // array of plugin dirs
|
||||
)
|
||||
|
||||
var _ = SynchronizedBeforeSuite(func() []byte {
|
||||
|
||||
paths := map[string]string{}
|
||||
for name, packagePath := range pluginPackages {
|
||||
execPath, err := gexec.Build(packagePath)
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
GOLANGCI_LINT_VERSION="v1.57.1"
|
||||
|
||||
go install "github.com/golangci/golangci-lint/cmd/golangci-lint@${GOLANGCI_LINT_VERSION}"
|
|
@ -0,0 +1,14 @@
|
|||
.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
|
|
@ -15,6 +15,7 @@
|
|||
package invoke
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
@ -22,6 +23,8 @@ import (
|
|||
type CNIArgs interface {
|
||||
// For use with os/exec; i.e., return nil to inherit the
|
||||
// environment from this process
|
||||
// For use in delegation; inherit the environment from this
|
||||
// process and allow overrides
|
||||
AsEnv() []string
|
||||
}
|
||||
|
||||
|
@ -29,7 +32,7 @@ type inherited struct{}
|
|||
|
||||
var inheritArgsFromEnv inherited
|
||||
|
||||
func (_ *inherited) AsEnv() []string {
|
||||
func (*inherited) AsEnv() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -57,17 +60,17 @@ func (args *Args) AsEnv() []string {
|
|||
pluginArgsStr = stringify(args.PluginArgs)
|
||||
}
|
||||
|
||||
// Ensure that the custom values are first, so any value present in
|
||||
// the process environment won't override them.
|
||||
env = append([]string{
|
||||
"CNI_COMMAND=" + args.Command,
|
||||
"CNI_CONTAINERID=" + args.ContainerID,
|
||||
"CNI_NETNS=" + args.NetNS,
|
||||
"CNI_ARGS=" + pluginArgsStr,
|
||||
"CNI_IFNAME=" + args.IfName,
|
||||
"CNI_PATH=" + args.Path,
|
||||
}, env...)
|
||||
return env
|
||||
// Duplicated values which come first will be overridden, so we must put the
|
||||
// custom values in the end to avoid being overridden by the process environments.
|
||||
env = append(env,
|
||||
"CNI_COMMAND="+args.Command,
|
||||
"CNI_CONTAINERID="+args.ContainerID,
|
||||
"CNI_NETNS="+args.NetNS,
|
||||
"CNI_ARGS="+pluginArgsStr,
|
||||
"CNI_IFNAME="+args.IfName,
|
||||
"CNI_PATH="+args.Path,
|
||||
)
|
||||
return dedupEnv(env)
|
||||
}
|
||||
|
||||
// taken from rkt/networking/net_plugin.go
|
||||
|
@ -80,3 +83,46 @@ func stringify(pluginArgs [][2]string) string {
|
|||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -17,15 +17,24 @@ package invoke_test
|
|||
import (
|
||||
"os"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/invoke"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/invoke"
|
||||
)
|
||||
|
||||
var _ = Describe("Args", func() {
|
||||
Describe("AsEnv", func() {
|
||||
It("places the CNI_ environment variables ahead of any ambient variables", func() {
|
||||
var _ = Describe("CNIArgs AsEnv", func() {
|
||||
Describe("Args AsEnv", func() {
|
||||
BeforeEach(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{
|
||||
Command: "ADD",
|
||||
ContainerID: "some-container-id",
|
||||
|
@ -37,24 +46,94 @@ var _ = Describe("Args", func() {
|
|||
IfName: "eth7",
|
||||
Path: "/some/cni/path",
|
||||
}
|
||||
const numCNIEnvVars = 6
|
||||
|
||||
latentVars := os.Environ()
|
||||
latentEnvs := os.Environ()
|
||||
numLatentEnvs := len(latentEnvs)
|
||||
|
||||
cniEnv := args.AsEnv()
|
||||
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",
|
||||
}))
|
||||
cniEnvs := args.AsEnv()
|
||||
Expect(cniEnvs).To(HaveLen(numLatentEnvs))
|
||||
|
||||
for i := range latentVars {
|
||||
Expect(cniEnv[numCNIEnvVars+i]).To(Equal(latentVars[i]))
|
||||
Expect(inStringSlice("CNI_COMMAND=ADD", cniEnvs)).To(BeTrue())
|
||||
Expect(inStringSlice("CNI_IFNAME=eth7", cniEnvs)).To(BeTrue())
|
||||
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
|
||||
}
|
||||
|
|
|
@ -15,14 +15,14 @@
|
|||
package invoke
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
)
|
||||
|
||||
func delegateAddOrGet(command, delegatePlugin string, netconf []byte, exec Exec) (types.Result, error) {
|
||||
func delegateCommon(delegatePlugin string, exec Exec) (string, Exec, error) {
|
||||
if exec == nil {
|
||||
exec = defaultExec
|
||||
}
|
||||
|
@ -30,46 +30,60 @@ func delegateAddOrGet(command, delegatePlugin string, netconf []byte, exec Exec)
|
|||
paths := filepath.SplitList(os.Getenv("CNI_PATH"))
|
||||
pluginPath, err := exec.FindInPath(delegatePlugin, paths)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
return ExecPluginWithResult(pluginPath, netconf, ArgsFromEnv(), exec)
|
||||
return pluginPath, exec, nil
|
||||
}
|
||||
|
||||
// DelegateAdd calls the given delegate plugin with the CNI ADD action and
|
||||
// JSON configuration
|
||||
func DelegateAdd(delegatePlugin string, netconf []byte, exec Exec) (types.Result, error) {
|
||||
if os.Getenv("CNI_COMMAND") != "ADD" {
|
||||
return nil, fmt.Errorf("CNI_COMMAND is not ADD")
|
||||
func DelegateAdd(ctx context.Context, delegatePlugin string, netconf []byte, exec Exec) (types.Result, error) {
|
||||
pluginPath, realExec, err := delegateCommon(delegatePlugin, exec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return delegateAddOrGet("ADD", delegatePlugin, netconf, exec)
|
||||
|
||||
// DelegateAdd will override the original "CNI_COMMAND" env from process with ADD
|
||||
return ExecPluginWithResult(ctx, pluginPath, netconf, delegateArgs("ADD"), realExec)
|
||||
}
|
||||
|
||||
// DelegateGet calls the given delegate plugin with the CNI GET action and
|
||||
// DelegateCheck calls the given delegate plugin with the CNI CHECK action and
|
||||
// JSON configuration
|
||||
func DelegateGet(delegatePlugin string, netconf []byte, exec Exec) (types.Result, error) {
|
||||
if os.Getenv("CNI_COMMAND") != "GET" {
|
||||
return nil, fmt.Errorf("CNI_COMMAND is not GET")
|
||||
}
|
||||
return delegateAddOrGet("GET", delegatePlugin, netconf, exec)
|
||||
func DelegateCheck(ctx context.Context, delegatePlugin string, netconf []byte, exec Exec) error {
|
||||
return delegateNoResult(ctx, delegatePlugin, netconf, exec, "CHECK")
|
||||
}
|
||||
|
||||
// DelegateDel calls the given delegate plugin with the CNI DEL action and
|
||||
// JSON configuration
|
||||
func DelegateDel(delegatePlugin string, netconf []byte, exec Exec) error {
|
||||
if exec == nil {
|
||||
exec = defaultExec
|
||||
}
|
||||
|
||||
if os.Getenv("CNI_COMMAND") != "DEL" {
|
||||
return fmt.Errorf("CNI_COMMAND is not DEL")
|
||||
}
|
||||
|
||||
paths := filepath.SplitList(os.Getenv("CNI_PATH"))
|
||||
pluginPath, err := exec.FindInPath(delegatePlugin, paths)
|
||||
func delegateNoResult(ctx context.Context, delegatePlugin string, netconf []byte, exec Exec, verb string) error {
|
||||
pluginPath, realExec, err := delegateCommon(delegatePlugin, exec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ExecPluginWithoutResult(pluginPath, netconf, ArgsFromEnv(), exec)
|
||||
return ExecPluginWithoutResult(ctx, pluginPath, netconf, delegateArgs(verb), realExec)
|
||||
}
|
||||
|
||||
// DelegateDel calls the given delegate plugin with the CNI DEL action and
|
||||
// JSON configuration
|
||||
func DelegateDel(ctx context.Context, delegatePlugin string, netconf []byte, exec Exec) error {
|
||||
return delegateNoResult(ctx, delegatePlugin, netconf, exec, "DEL")
|
||||
}
|
||||
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,18 +15,19 @@
|
|||
package invoke_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/invoke"
|
||||
"github.com/containernetworking/cni/pkg/types/current"
|
||||
"github.com/containernetworking/cni/plugins/test/noop/debug"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/invoke"
|
||||
current "github.com/containernetworking/cni/pkg/types/100"
|
||||
"github.com/containernetworking/cni/pkg/version"
|
||||
"github.com/containernetworking/cni/plugins/test/noop/debug"
|
||||
)
|
||||
|
||||
var _ = Describe("Delegate", func() {
|
||||
|
@ -36,19 +37,19 @@ var _ = Describe("Delegate", func() {
|
|||
debugFileName string
|
||||
debugBehavior *debug.Debug
|
||||
expectedResult *current.Result
|
||||
ctx context.Context
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
netConf, _ = json.Marshal(map[string]string{
|
||||
"name": "delegate-test",
|
||||
"cniVersion": "0.4.0",
|
||||
"cniVersion": version.Current(),
|
||||
})
|
||||
|
||||
expectedResult = ¤t.Result{
|
||||
CNIVersion: "0.4.0",
|
||||
CNIVersion: current.ImplementedSpecVersion,
|
||||
IPs: []*current.IPConfig{
|
||||
{
|
||||
Version: "4",
|
||||
Address: net.IPNet{
|
||||
IP: net.ParseIP("10.1.2.3"),
|
||||
Mask: net.CIDRMask(24, 32),
|
||||
|
@ -58,7 +59,7 @@ var _ = Describe("Delegate", func() {
|
|||
}
|
||||
expectedResultBytes, _ := json.Marshal(expectedResult)
|
||||
|
||||
debugFile, err := ioutil.TempFile("", "cni_debug")
|
||||
debugFile, err := os.CreateTemp("", "cni_debug")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(debugFile.Close()).To(Succeed())
|
||||
debugFileName = debugFile.Name()
|
||||
|
@ -67,7 +68,7 @@ var _ = Describe("Delegate", func() {
|
|||
}
|
||||
Expect(debugBehavior.WriteDebug(debugFileName)).To(Succeed())
|
||||
pluginName = "noop"
|
||||
|
||||
ctx = context.TODO()
|
||||
os.Setenv("CNI_ARGS", "DEBUG="+debugFileName)
|
||||
os.Setenv("CNI_PATH", filepath.Dir(pathToPlugin))
|
||||
os.Setenv("CNI_NETNS", "/tmp/some/netns/path")
|
||||
|
@ -89,7 +90,7 @@ var _ = Describe("Delegate", func() {
|
|||
})
|
||||
|
||||
It("finds and execs the named plugin", func() {
|
||||
result, err := invoke.DelegateAdd(pluginName, netConf, nil)
|
||||
result, err := invoke.DelegateAdd(ctx, pluginName, netConf, nil)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(result).To(Equal(expectedResult))
|
||||
|
||||
|
@ -99,53 +100,23 @@ var _ = Describe("Delegate", func() {
|
|||
Expect(pluginInvocation.CmdArgs.IfName).To(Equal("eth7"))
|
||||
})
|
||||
|
||||
Context("if the delegation isn't part of an existing ADD command", func() {
|
||||
Context("if the ADD delegation runs on an existing non-ADD command, ", func() {
|
||||
BeforeEach(func() {
|
||||
os.Setenv("CNI_COMMAND", "NOPE")
|
||||
})
|
||||
|
||||
It("aborts and returns a useful error", func() {
|
||||
_, err := invoke.DelegateAdd(pluginName, netConf, nil)
|
||||
Expect(err).To(MatchError("CNI_COMMAND is not ADD"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when the plugin cannot be found", func() {
|
||||
BeforeEach(func() {
|
||||
pluginName = "non-existent-plugin"
|
||||
})
|
||||
|
||||
It("returns a useful error", func() {
|
||||
_, err := invoke.DelegateAdd(pluginName, netConf, nil)
|
||||
Expect(err).To(MatchError(HavePrefix("failed to find plugin")))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("DelegateGet", func() {
|
||||
BeforeEach(func() {
|
||||
os.Setenv("CNI_COMMAND", "GET")
|
||||
})
|
||||
|
||||
It("finds and execs the named plugin", func() {
|
||||
result, err := invoke.DelegateGet(pluginName, netConf, nil)
|
||||
result, err := invoke.DelegateAdd(ctx, pluginName, netConf, nil)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(result).To(Equal(expectedResult))
|
||||
|
||||
pluginInvocation, err := debug.ReadDebug(debugFileName)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(pluginInvocation.Command).To(Equal("GET"))
|
||||
Expect(pluginInvocation.Command).To(Equal("ADD"))
|
||||
Expect(pluginInvocation.CmdArgs.IfName).To(Equal("eth7"))
|
||||
})
|
||||
|
||||
Context("if the delegation isn't part of an existing GET command", func() {
|
||||
BeforeEach(func() {
|
||||
os.Setenv("CNI_COMMAND", "NOPE")
|
||||
})
|
||||
|
||||
It("aborts and returns a useful error", func() {
|
||||
_, err := invoke.DelegateGet(pluginName, netConf, nil)
|
||||
Expect(err).To(MatchError("CNI_COMMAND is not GET"))
|
||||
// check the original env
|
||||
Expect(os.Getenv("CNI_COMMAND")).To(Equal("NOPE"))
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -155,7 +126,53 @@ var _ = Describe("Delegate", func() {
|
|||
})
|
||||
|
||||
It("returns a useful error", func() {
|
||||
_, err := invoke.DelegateGet(pluginName, netConf, nil)
|
||||
_, err := invoke.DelegateAdd(ctx, pluginName, netConf, nil)
|
||||
Expect(err).To(MatchError(HavePrefix("failed to find plugin")))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("DelegateCheck", func() {
|
||||
BeforeEach(func() {
|
||||
os.Setenv("CNI_COMMAND", "CHECK")
|
||||
})
|
||||
|
||||
It("finds and execs the named plugin", func() {
|
||||
err := invoke.DelegateCheck(ctx, pluginName, netConf, nil)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
pluginInvocation, err := debug.ReadDebug(debugFileName)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(pluginInvocation.Command).To(Equal("CHECK"))
|
||||
Expect(pluginInvocation.CmdArgs.IfName).To(Equal("eth7"))
|
||||
})
|
||||
|
||||
Context("if the CHECK delegation runs on an existing non-CHECK command", func() {
|
||||
BeforeEach(func() {
|
||||
os.Setenv("CNI_COMMAND", "NOPE")
|
||||
})
|
||||
|
||||
It("aborts and returns a useful error", func() {
|
||||
err := invoke.DelegateCheck(ctx, pluginName, netConf, nil)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
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"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when the plugin cannot be found", func() {
|
||||
BeforeEach(func() {
|
||||
pluginName = "non-existent-plugin"
|
||||
})
|
||||
|
||||
It("returns a useful error", func() {
|
||||
err := invoke.DelegateCheck(ctx, pluginName, netConf, nil)
|
||||
Expect(err).To(MatchError(HavePrefix("failed to find plugin")))
|
||||
})
|
||||
})
|
||||
|
@ -167,7 +184,7 @@ var _ = Describe("Delegate", func() {
|
|||
})
|
||||
|
||||
It("finds and execs the named plugin", func() {
|
||||
err := invoke.DelegateDel(pluginName, netConf, nil)
|
||||
err := invoke.DelegateDel(ctx, pluginName, netConf, nil)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
pluginInvocation, err := debug.ReadDebug(debugFileName)
|
||||
|
@ -176,14 +193,22 @@ var _ = Describe("Delegate", func() {
|
|||
Expect(pluginInvocation.CmdArgs.IfName).To(Equal("eth7"))
|
||||
})
|
||||
|
||||
Context("if the delegation isn't part of an existing DEL command", func() {
|
||||
Context("if the DEL delegation runs on an existing non-DEL command", func() {
|
||||
BeforeEach(func() {
|
||||
os.Setenv("CNI_COMMAND", "NOPE")
|
||||
})
|
||||
|
||||
It("aborts and returns a useful error", func() {
|
||||
err := invoke.DelegateDel(pluginName, netConf, nil)
|
||||
Expect(err).To(MatchError("CNI_COMMAND is not DEL"))
|
||||
err := invoke.DelegateDel(ctx, pluginName, netConf, nil)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
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"))
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -193,7 +218,51 @@ var _ = Describe("Delegate", func() {
|
|||
})
|
||||
|
||||
It("returns a useful error", func() {
|
||||
err := invoke.DelegateDel(pluginName, netConf, nil)
|
||||
err := invoke.DelegateDel(ctx, pluginName, netConf, nil)
|
||||
Expect(err).To(MatchError(HavePrefix("failed to find plugin")))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
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")))
|
||||
})
|
||||
})
|
||||
|
|
|
@ -15,10 +15,13 @@
|
|||
package invoke
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/pkg/types/create"
|
||||
"github.com/containernetworking/cni/pkg/version"
|
||||
)
|
||||
|
||||
|
@ -26,26 +29,69 @@ import (
|
|||
// and executing a CNI plugin. Tests may provide a fake implementation
|
||||
// to avoid writing fake plugins to temporary directories during the test.
|
||||
type Exec interface {
|
||||
ExecPlugin(pluginPath string, stdinData []byte, environ []string) ([]byte, error)
|
||||
ExecPlugin(ctx context.Context, pluginPath string, stdinData []byte, environ []string) ([]byte, error)
|
||||
FindInPath(plugin string, paths []string) (string, 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
|
||||
// object to ExecPluginWithResult() to verify the incoming stdin and environment
|
||||
// and provide a tailored response:
|
||||
//
|
||||
//import (
|
||||
// import (
|
||||
// "encoding/json"
|
||||
// "path"
|
||||
// "strings"
|
||||
//)
|
||||
// )
|
||||
//
|
||||
//type fakeExec struct {
|
||||
// type fakeExec struct {
|
||||
// version.PluginDecoder
|
||||
//}
|
||||
// }
|
||||
//
|
||||
//func (f *fakeExec) ExecPlugin(pluginPath string, stdinData []byte, environ []string) ([]byte, error) {
|
||||
// func (f *fakeExec) ExecPlugin(pluginPath string, stdinData []byte, environ []string) ([]byte, error) {
|
||||
// net := &types.NetConf{}
|
||||
// err := json.Unmarshal(stdinData, net)
|
||||
// if err != nil {
|
||||
|
@ -63,40 +109,38 @@ type Exec interface {
|
|||
// }
|
||||
// }
|
||||
// return []byte("{\"CNIVersion\":\"0.4.0\"}"), nil
|
||||
//}
|
||||
// }
|
||||
//
|
||||
//func (f *fakeExec) FindInPath(plugin string, paths []string) (string, error) {
|
||||
// func (f *fakeExec) FindInPath(plugin string, paths []string) (string, error) {
|
||||
// if len(paths) > 0 {
|
||||
// return path.Join(paths[0], plugin), nil
|
||||
// }
|
||||
// return "", fmt.Errorf("failed to find plugin %s in paths %v", plugin, paths)
|
||||
//}
|
||||
// }
|
||||
|
||||
func ExecPluginWithResult(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 {
|
||||
exec = defaultExec
|
||||
}
|
||||
|
||||
stdoutBytes, err := exec.ExecPlugin(pluginPath, netconf, args.AsEnv())
|
||||
stdoutBytes, err := exec.ExecPlugin(ctx, pluginPath, netconf, args.AsEnv())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Plugin must return result in same version as specified in netconf
|
||||
versionDecoder := &version.ConfigDecoder{}
|
||||
confVersion, err := versionDecoder.Decode(netconf)
|
||||
resultVersion, fixedBytes, err := fixupResultVersion(netconf, stdoutBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return version.NewResult(confVersion, stdoutBytes)
|
||||
return create.Create(resultVersion, fixedBytes)
|
||||
}
|
||||
|
||||
func ExecPluginWithoutResult(pluginPath string, netconf []byte, args CNIArgs, exec Exec) error {
|
||||
func ExecPluginWithoutResult(ctx context.Context, pluginPath string, netconf []byte, args CNIArgs, exec Exec) error {
|
||||
if exec == nil {
|
||||
exec = defaultExec
|
||||
}
|
||||
_, err := exec.ExecPlugin(pluginPath, netconf, args.AsEnv())
|
||||
_, err := exec.ExecPlugin(ctx, pluginPath, netconf, args.AsEnv())
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -104,7 +148,7 @@ func ExecPluginWithoutResult(pluginPath string, netconf []byte, args CNIArgs, ex
|
|||
// For recent-enough plugins, it uses the information returned by the VERSION
|
||||
// command. For older plugins which do not recognize that command, it reports
|
||||
// version 0.1.0
|
||||
func GetVersionInfo(pluginPath string, exec Exec) (version.PluginInfo, error) {
|
||||
func GetVersionInfo(ctx context.Context, pluginPath string, exec Exec) (version.PluginInfo, error) {
|
||||
if exec == nil {
|
||||
exec = defaultExec
|
||||
}
|
||||
|
@ -117,7 +161,7 @@ func GetVersionInfo(pluginPath string, exec Exec) (version.PluginInfo, error) {
|
|||
Path: "dummy",
|
||||
}
|
||||
stdin := []byte(fmt.Sprintf(`{"cniVersion":%q}`, version.Current()))
|
||||
stdoutBytes, err := exec.ExecPlugin(pluginPath, stdin, args.AsEnv())
|
||||
stdoutBytes, err := exec.ExecPlugin(ctx, pluginPath, stdin, args.AsEnv())
|
||||
if err != nil {
|
||||
if err.Error() == "unknown CNI_COMMAND: VERSION" {
|
||||
return version.PluginSupports("0.1.0"), nil
|
||||
|
|
|
@ -15,16 +15,17 @@
|
|||
package invoke_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/invoke"
|
||||
"github.com/containernetworking/cni/pkg/invoke/fakes"
|
||||
"github.com/containernetworking/cni/pkg/types/current"
|
||||
current "github.com/containernetworking/cni/pkg/types/100"
|
||||
"github.com/containernetworking/cni/pkg/version"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Executing a plugin, unit tests", func() {
|
||||
|
@ -36,11 +37,12 @@ var _ = Describe("Executing a plugin, unit tests", func() {
|
|||
pluginPath string
|
||||
netconf []byte
|
||||
cniargs *fakes.CNIArgs
|
||||
ctx context.Context
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
rawExec = &fakes.RawExec{}
|
||||
rawExec.ExecPluginCall.Returns.ResultBytes = []byte(`{ "ips": [ { "version": "4", "address": "1.2.3.4/24" } ] }`)
|
||||
rawExec.ExecPluginCall.Returns.ResultBytes = []byte(`{ "cniVersion": "0.3.1", "ips": [ { "version": "4", "address": "1.2.3.4/24" } ] }`)
|
||||
|
||||
versionDecoder = &fakes.VersionDecoder{}
|
||||
versionDecoder.DecodeCall.Returns.PluginInfo = version.PluginSupports("0.42.0")
|
||||
|
@ -56,21 +58,23 @@ var _ = Describe("Executing a plugin, unit tests", func() {
|
|||
netconf = []byte(`{ "some": "stdin", "cniVersion": "0.3.1" }`)
|
||||
cniargs = &fakes.CNIArgs{}
|
||||
cniargs.AsEnvCall.Returns.Env = []string{"SOME=ENV"}
|
||||
ctx = context.TODO()
|
||||
})
|
||||
|
||||
Describe("returning a result", func() {
|
||||
It("unmarshals the result bytes into the Result type", func() {
|
||||
r, err := invoke.ExecPluginWithResult(pluginPath, netconf, cniargs, pluginExec)
|
||||
r, err := invoke.ExecPluginWithResult(ctx, pluginPath, netconf, cniargs, pluginExec)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
result, err := current.GetResult(r)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(len(result.IPs)).To(Equal(1))
|
||||
Expect(result.IPs).To(HaveLen(1))
|
||||
Expect(result.IPs[0].Address.IP.String()).To(Equal("1.2.3.4"))
|
||||
})
|
||||
|
||||
It("passes its arguments through to the rawExec", func() {
|
||||
invoke.ExecPluginWithResult(pluginPath, netconf, cniargs, pluginExec)
|
||||
_, err := invoke.ExecPluginWithResult(ctx, pluginPath, netconf, cniargs, pluginExec)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(rawExec.ExecPluginCall.Received.PluginPath).To(Equal(pluginPath))
|
||||
Expect(rawExec.ExecPluginCall.Received.StdinData).To(Equal(netconf))
|
||||
Expect(rawExec.ExecPluginCall.Received.Environ).To(Equal([]string{"SOME=ENV"}))
|
||||
|
@ -81,7 +85,7 @@ var _ = Describe("Executing a plugin, unit tests", func() {
|
|||
rawExec.ExecPluginCall.Returns.Error = errors.New("banana")
|
||||
})
|
||||
It("returns the error", func() {
|
||||
_, err := invoke.ExecPluginWithResult(pluginPath, netconf, cniargs, pluginExec)
|
||||
_, err := invoke.ExecPluginWithResult(ctx, pluginPath, netconf, cniargs, pluginExec)
|
||||
Expect(err).To(MatchError("banana"))
|
||||
})
|
||||
})
|
||||
|
@ -90,14 +94,47 @@ var _ = Describe("Executing a plugin, unit tests", func() {
|
|||
// pluginPath should not exist on-disk so we expect an error.
|
||||
// This test simply tests that the default exec handler
|
||||
// is run when the exec interface is nil.
|
||||
_, err := invoke.ExecPluginWithResult(pluginPath, netconf, cniargs, nil)
|
||||
_, err := invoke.ExecPluginWithResult(ctx, pluginPath, netconf, cniargs, nil)
|
||||
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() {
|
||||
It("passes its arguments through to the rawExec", func() {
|
||||
invoke.ExecPluginWithoutResult(pluginPath, netconf, cniargs, pluginExec)
|
||||
err := invoke.ExecPluginWithoutResult(ctx, pluginPath, netconf, cniargs, pluginExec)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(rawExec.ExecPluginCall.Received.PluginPath).To(Equal(pluginPath))
|
||||
Expect(rawExec.ExecPluginCall.Received.StdinData).To(Equal(netconf))
|
||||
Expect(rawExec.ExecPluginCall.Received.Environ).To(Equal([]string{"SOME=ENV"}))
|
||||
|
@ -108,7 +145,7 @@ var _ = Describe("Executing a plugin, unit tests", func() {
|
|||
rawExec.ExecPluginCall.Returns.Error = errors.New("banana")
|
||||
})
|
||||
It("returns the error", func() {
|
||||
err := invoke.ExecPluginWithoutResult(pluginPath, netconf, cniargs, pluginExec)
|
||||
err := invoke.ExecPluginWithoutResult(ctx, pluginPath, netconf, cniargs, pluginExec)
|
||||
Expect(err).To(MatchError("banana"))
|
||||
})
|
||||
})
|
||||
|
@ -117,7 +154,7 @@ var _ = Describe("Executing a plugin, unit tests", func() {
|
|||
// pluginPath should not exist on-disk so we expect an error.
|
||||
// This test simply tests that the default exec handler
|
||||
// is run when the exec interface is nil.
|
||||
err := invoke.ExecPluginWithoutResult(pluginPath, netconf, cniargs, nil)
|
||||
err := invoke.ExecPluginWithoutResult(ctx, pluginPath, netconf, cniargs, nil)
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
@ -128,7 +165,8 @@ var _ = Describe("Executing a plugin, unit tests", func() {
|
|||
})
|
||||
|
||||
It("execs the plugin with the command VERSION", func() {
|
||||
invoke.GetVersionInfo(pluginPath, pluginExec)
|
||||
_, err := invoke.GetVersionInfo(ctx, pluginPath, pluginExec)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(rawExec.ExecPluginCall.Received.PluginPath).To(Equal(pluginPath))
|
||||
Expect(rawExec.ExecPluginCall.Received.Environ).To(ContainElement("CNI_COMMAND=VERSION"))
|
||||
expectedStdin, _ := json.Marshal(map[string]string{"cniVersion": version.Current()})
|
||||
|
@ -136,7 +174,7 @@ var _ = Describe("Executing a plugin, unit tests", func() {
|
|||
})
|
||||
|
||||
It("decodes and returns the version info", func() {
|
||||
versionInfo, err := invoke.GetVersionInfo(pluginPath, pluginExec)
|
||||
versionInfo, err := invoke.GetVersionInfo(ctx, pluginPath, pluginExec)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(versionInfo.SupportedVersions()).To(Equal([]string{"0.42.0"}))
|
||||
Expect(versionDecoder.DecodeCall.Received.JSONBytes).To(MatchJSON(`{ "some": "version-info" }`))
|
||||
|
@ -147,7 +185,7 @@ var _ = Describe("Executing a plugin, unit tests", func() {
|
|||
rawExec.ExecPluginCall.Returns.Error = errors.New("banana")
|
||||
})
|
||||
It("returns the error", func() {
|
||||
_, err := invoke.GetVersionInfo(pluginPath, pluginExec)
|
||||
_, err := invoke.GetVersionInfo(ctx, pluginPath, pluginExec)
|
||||
Expect(err).To(MatchError("banana"))
|
||||
})
|
||||
})
|
||||
|
@ -158,13 +196,14 @@ var _ = Describe("Executing a plugin, unit tests", func() {
|
|||
})
|
||||
|
||||
It("interprets the error as a 0.1.0 version", func() {
|
||||
versionInfo, err := invoke.GetVersionInfo(pluginPath, pluginExec)
|
||||
versionInfo, err := invoke.GetVersionInfo(ctx, pluginPath, pluginExec)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(versionInfo.SupportedVersions()).To(ConsistOf("0.1.0"))
|
||||
})
|
||||
|
||||
It("sets dummy values for env vars required by very old plugins", func() {
|
||||
invoke.GetVersionInfo(pluginPath, pluginExec)
|
||||
_, err := invoke.GetVersionInfo(ctx, pluginPath, pluginExec)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
env := rawExec.ExecPluginCall.Received.Environ
|
||||
Expect(env).To(ContainElement("CNI_NETNS=dummy"))
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
|
||||
package fakes
|
||||
|
||||
import "context"
|
||||
|
||||
type RawExec struct {
|
||||
ExecPluginCall struct {
|
||||
Received struct {
|
||||
|
@ -38,7 +40,7 @@ type RawExec struct {
|
|||
}
|
||||
}
|
||||
|
||||
func (e *RawExec) ExecPlugin(pluginPath string, stdinData []byte, environ []string) ([]byte, error) {
|
||||
func (e *RawExec) ExecPlugin(ctx context.Context, pluginPath string, stdinData []byte, environ []string) ([]byte, error) {
|
||||
e.ExecPluginCall.Received.PluginPath = pluginPath
|
||||
e.ExecPluginCall.Received.StdinData = stdinData
|
||||
e.ExecPluginCall.Received.Environ = environ
|
||||
|
|
|
@ -18,6 +18,7 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FindInPath returns the full path of the plugin by searching in the provided path
|
||||
|
@ -26,6 +27,10 @@ func FindInPath(plugin string, paths []string) (string, error) {
|
|||
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 {
|
||||
return "", fmt.Errorf("no paths provided")
|
||||
}
|
||||
|
|
|
@ -16,14 +16,14 @@ package invoke_test
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/invoke"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/invoke"
|
||||
)
|
||||
|
||||
var _ = Describe("FindInPath", func() {
|
||||
|
@ -37,17 +37,17 @@ var _ = Describe("FindInPath", func() {
|
|||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
tempDir, err := ioutil.TempDir("", "cni-find")
|
||||
tempDir, err := os.MkdirTemp("", "cni-find")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
plugin, err := ioutil.TempFile(tempDir, "a-cni-plugin")
|
||||
plugin, err := os.CreateTemp(tempDir, "a-cni-plugin")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
plugin2Name := "a-plugin-with-extension" + invoke.ExecutableFileExtensions[0]
|
||||
plugin2, err := os.Create(filepath.Join(tempDir, plugin2Name))
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
anotherTempDir, err = ioutil.TempDir("", "nothing-here")
|
||||
anotherTempDir, err = os.MkdirTemp("", "nothing-here")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
multiplePaths = []string{anotherTempDir, tempDir}
|
||||
|
@ -99,5 +99,13 @@ var _ = Describe("FindInPath", func() {
|
|||
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))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -15,18 +15,17 @@
|
|||
package invoke_test
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/invoke"
|
||||
"github.com/containernetworking/cni/pkg/version"
|
||||
"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() {
|
||||
|
@ -36,7 +35,7 @@ var _ = Describe("GetVersion, integration tests", func() {
|
|||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
pluginDir, err := ioutil.TempDir("", "plugins")
|
||||
pluginDir, err := os.MkdirTemp("", "plugins")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
pluginPath = filepath.Join(pluginDir, "test-plugin")
|
||||
if runtime.GOOS == "windows" {
|
||||
|
@ -51,7 +50,7 @@ var _ = Describe("GetVersion, integration tests", func() {
|
|||
DescribeTable("correctly reporting plugin versions",
|
||||
func(gitRef string, pluginSource string, expectedVersions version.PluginInfo) {
|
||||
Expect(testhelpers.BuildAt([]byte(pluginSource), gitRef, pluginPath)).To(Succeed())
|
||||
versionInfo, err := invoke.GetVersionInfo(pluginPath, nil)
|
||||
versionInfo, err := invoke.GetVersionInfo(context.TODO(), pluginPath, nil)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(versionInfo.SupportedVersions()).To(ConsistOf(expectedVersions.SupportedVersions()))
|
||||
|
@ -72,7 +71,7 @@ var _ = Describe("GetVersion, integration tests", func() {
|
|||
version.PluginSupports("0.2.0", "0.999.0"),
|
||||
),
|
||||
|
||||
Entry("historical: before GET was introduced",
|
||||
Entry("historical: before CHECK was introduced",
|
||||
git_ref_v031, plugin_source_v020_custom_versions,
|
||||
version.PluginSupports("0.2.0", "0.999.0"),
|
||||
),
|
||||
|
@ -80,14 +79,14 @@ var _ = Describe("GetVersion, integration tests", func() {
|
|||
// this entry tracks the current behavior. Before you change it, ensure
|
||||
// that its previous behavior is captured in the most recent "historical" entry
|
||||
Entry("current",
|
||||
"HEAD", plugin_source_v040_get,
|
||||
"HEAD", plugin_source_v040_check,
|
||||
version.PluginSupports("0.2.0", "0.4.0", "0.999.0"),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
// A 0.4.0 plugin that supports GET
|
||||
const plugin_source_v040_get = `package main
|
||||
// A 0.4.0 plugin that supports CHECK
|
||||
const plugin_source_v040_check = `package main
|
||||
|
||||
import (
|
||||
"github.com/containernetworking/cni/pkg/skel"
|
||||
|
@ -128,5 +127,7 @@ func c(_ *skel.CmdArgs) error { fmt.Println("{}"); return nil }
|
|||
func main() { skel.PluginMain(c, c) }
|
||||
`
|
||||
|
||||
const git_ref_v010 = "2c482f4"
|
||||
const git_ref_v020_no_custom_versions = "349d66d"
|
||||
const (
|
||||
git_ref_v010 = "2c482f4"
|
||||
git_ref_v020_no_custom_versions = "349d66d"
|
||||
)
|
||||
|
|
|
@ -15,11 +15,11 @@
|
|||
package invoke_test
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gexec"
|
||||
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestInvoke(t *testing.T) {
|
||||
|
|
|
@ -12,7 +12,8 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// +build darwin dragonfly freebsd linux netbsd opensbd solaris
|
||||
//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
|
||||
// +build darwin dragonfly freebsd linux netbsd openbsd solaris
|
||||
|
||||
package invoke
|
||||
|
||||
|
|
|
@ -16,10 +16,13 @@ package invoke
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
)
|
||||
|
@ -28,34 +31,56 @@ type RawExec struct {
|
|||
Stderr io.Writer
|
||||
}
|
||||
|
||||
func (e *RawExec) ExecPlugin(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{}
|
||||
stderr := &bytes.Buffer{}
|
||||
c := exec.CommandContext(ctx, pluginPath)
|
||||
c.Env = environ
|
||||
c.Stdin = bytes.NewBuffer(stdinData)
|
||||
c.Stdout = stdout
|
||||
c.Stderr = stderr
|
||||
|
||||
c := exec.Cmd{
|
||||
Env: environ,
|
||||
Path: pluginPath,
|
||||
Args: []string{pluginPath},
|
||||
Stdin: bytes.NewBuffer(stdinData),
|
||||
Stdout: stdout,
|
||||
Stderr: e.Stderr,
|
||||
}
|
||||
if err := c.Run(); err != nil {
|
||||
return nil, pluginErr(err, stdout.Bytes())
|
||||
// Retry the command on "text file busy" errors
|
||||
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
|
||||
}
|
||||
|
||||
func pluginErr(err error, output []byte) error {
|
||||
if _, ok := err.(*exec.ExitError); ok {
|
||||
func (e *RawExec) pluginErr(err error, stdout, stderr []byte) error {
|
||||
emsg := types.Error{}
|
||||
if perr := json.Unmarshal(output, &emsg); perr != nil {
|
||||
emsg.Msg = fmt.Sprintf("netplugin failed but error parsing its diagnostic message %q: %v", string(output), perr)
|
||||
if len(stdout) == 0 {
|
||||
if len(stderr) == 0 {
|
||||
emsg.Msg = fmt.Sprintf("netplugin failed with no error message: %v", err)
|
||||
} else {
|
||||
emsg.Msg = fmt.Sprintf("netplugin failed: %q", string(stderr))
|
||||
}
|
||||
} else if perr := json.Unmarshal(stdout, &emsg); perr != nil {
|
||||
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) {
|
||||
|
|
|
@ -16,15 +16,14 @@ package invoke_test
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/invoke"
|
||||
|
||||
noop_debug "github.com/containernetworking/cni/plugins/test/noop/debug"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/invoke"
|
||||
noop_debug "github.com/containernetworking/cni/plugins/test/noop/debug"
|
||||
)
|
||||
|
||||
var _ = Describe("RawExec", func() {
|
||||
|
@ -34,12 +33,13 @@ var _ = Describe("RawExec", func() {
|
|||
environ []string
|
||||
stdin []byte
|
||||
execer *invoke.RawExec
|
||||
ctx context.Context
|
||||
)
|
||||
|
||||
const reportResult = `{ "some": "result" }`
|
||||
|
||||
BeforeEach(func() {
|
||||
debugFile, err := ioutil.TempFile("", "cni_debug")
|
||||
debugFile, err := os.CreateTemp("", "cni_debug")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(debugFile.Close()).To(Succeed())
|
||||
debugFileName = debugFile.Name()
|
||||
|
@ -60,6 +60,7 @@ var _ = Describe("RawExec", func() {
|
|||
}
|
||||
stdin = []byte(`{"name": "raw-exec-test", "some":"stdin-json", "cniVersion": "0.3.1"}`)
|
||||
execer = &invoke.RawExec{}
|
||||
ctx = context.TODO()
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
|
@ -67,7 +68,7 @@ var _ = Describe("RawExec", func() {
|
|||
})
|
||||
|
||||
It("runs the plugin with the given stdin and environment", func() {
|
||||
_, err := execer.ExecPlugin(pathToPlugin, stdin, environ)
|
||||
_, err := execer.ExecPlugin(ctx, pathToPlugin, stdin, environ)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
debug, err := noop_debug.ReadDebug(debugFileName)
|
||||
|
@ -78,7 +79,7 @@ var _ = Describe("RawExec", func() {
|
|||
})
|
||||
|
||||
It("returns the resulting stdout as bytes", func() {
|
||||
resultBytes, err := execer.ExecPlugin(pathToPlugin, stdin, environ)
|
||||
resultBytes, err := execer.ExecPlugin(ctx, pathToPlugin, stdin, environ)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(resultBytes).To(BeEquivalentTo(reportResult))
|
||||
|
@ -93,7 +94,7 @@ var _ = Describe("RawExec", func() {
|
|||
})
|
||||
|
||||
It("forwards any stderr bytes to the Stderr writer", func() {
|
||||
_, err := execer.ExecPlugin(pathToPlugin, stdin, environ)
|
||||
_, err := execer.ExecPlugin(ctx, pathToPlugin, stdin, environ)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(stderrBuffer.String()).To(Equal("some stderr message"))
|
||||
|
@ -102,20 +103,45 @@ var _ = Describe("RawExec", func() {
|
|||
|
||||
Context("when the plugin errors", func() {
|
||||
BeforeEach(func() {
|
||||
debug.ReportError = "banana"
|
||||
Expect(debug.WriteDebug(debugFileName)).To(Succeed())
|
||||
debug.ReportResult = ""
|
||||
})
|
||||
|
||||
Context("and writes valid error JSON to stdout", func() {
|
||||
It("wraps and returns the error", func() {
|
||||
_, err := execer.ExecPlugin(pathToPlugin, stdin, environ)
|
||||
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())
|
||||
_, err := execer.ExecPlugin(ctx, pathToPlugin, stdin, environ)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err).To(MatchError("netplugin failed with no error message: exit status 1"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when the system is unable to execute the plugin", func() {
|
||||
It("returns the error", func() {
|
||||
_, err := execer.ExecPlugin("/tmp/some/invalid/plugin/path", stdin, environ)
|
||||
_, err := execer.ExecPlugin(ctx, "/tmp/some/invalid/plugin/path", stdin, environ)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err).To(MatchError(ContainSubstring("/tmp/some/invalid/plugin/path")))
|
||||
})
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
// Copyright 2022 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ns
|
||||
|
||||
import "github.com/containernetworking/cni/pkg/types"
|
||||
|
||||
func CheckNetNS(nsPath string) (bool, *types.Error) {
|
||||
return false, nil
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
// Copyright 2022 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ns
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
"github.com/vishvananda/netns"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
)
|
||||
|
||||
// Returns an object representing the current OS thread's network namespace
|
||||
func getCurrentNS() (netns.NsHandle, error) {
|
||||
// Lock the thread in case other goroutine executes in it and changes its
|
||||
// network namespace after getCurrentThreadNetNSPath(), otherwise it might
|
||||
// return an unexpected network namespace.
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
return netns.Get()
|
||||
}
|
||||
|
||||
func CheckNetNS(nsPath string) (bool, *types.Error) {
|
||||
ns, err := netns.GetFromPath(nsPath)
|
||||
// Let plugins check whether nsPath from args is valid. Also support CNI DEL for empty nsPath as already-deleted nsPath.
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
defer ns.Close()
|
||||
|
||||
pluginNS, err := getCurrentNS()
|
||||
if err != nil {
|
||||
return false, types.NewError(types.ErrInvalidNetNS, "get plugin's netns failed", "")
|
||||
}
|
||||
defer pluginNS.Close()
|
||||
|
||||
return pluginNS.Equal(ns), nil
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
// Copyright 2022 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ns
|
||||
|
||||
import "github.com/containernetworking/cni/pkg/types"
|
||||
|
||||
func CheckNetNS(nsPath string) (bool, *types.Error) {
|
||||
return false, nil
|
||||
}
|
299
pkg/skel/skel.go
299
pkg/skel/skel.go
|
@ -19,13 +19,16 @@ package skel
|
|||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/ns"
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/pkg/utils"
|
||||
"github.com/containernetworking/cni/pkg/version"
|
||||
)
|
||||
|
||||
|
@ -37,6 +40,7 @@ type CmdArgs struct {
|
|||
IfName string
|
||||
Args string
|
||||
Path string
|
||||
NetnsOverride string
|
||||
StdinData []byte
|
||||
}
|
||||
|
||||
|
@ -52,92 +56,123 @@ type dispatcher struct {
|
|||
|
||||
type reqForCmdEntry map[string]bool
|
||||
|
||||
func (t *dispatcher) getCmdArgsFromEnv() (string, *CmdArgs, error) {
|
||||
var cmd, contID, netns, ifName, args, path string
|
||||
func (t *dispatcher) getCmdArgsFromEnv() (string, *CmdArgs, *types.Error) {
|
||||
var cmd, contID, netns, ifName, args, path, netnsOverride string
|
||||
|
||||
vars := []struct {
|
||||
name string
|
||||
val *string
|
||||
reqForCmd reqForCmdEntry
|
||||
validateFn func(string) *types.Error
|
||||
}{
|
||||
{
|
||||
"CNI_COMMAND",
|
||||
&cmd,
|
||||
reqForCmdEntry{
|
||||
"ADD": true,
|
||||
"GET": true,
|
||||
"CHECK": true,
|
||||
"DEL": true,
|
||||
"GC": true,
|
||||
"STATUS": true,
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"CNI_CONTAINERID",
|
||||
&contID,
|
||||
reqForCmdEntry{
|
||||
"ADD": true,
|
||||
"GET": true,
|
||||
"CHECK": true,
|
||||
"DEL": true,
|
||||
},
|
||||
utils.ValidateContainerID,
|
||||
},
|
||||
{
|
||||
"CNI_NETNS",
|
||||
&netns,
|
||||
reqForCmdEntry{
|
||||
"ADD": true,
|
||||
"GET": true,
|
||||
"CHECK": true,
|
||||
"DEL": false,
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"CNI_IFNAME",
|
||||
&ifName,
|
||||
reqForCmdEntry{
|
||||
"ADD": true,
|
||||
"GET": true,
|
||||
"CHECK": true,
|
||||
"DEL": true,
|
||||
},
|
||||
utils.ValidateInterfaceName,
|
||||
},
|
||||
{
|
||||
"CNI_ARGS",
|
||||
&args,
|
||||
reqForCmdEntry{
|
||||
"ADD": false,
|
||||
"GET": false,
|
||||
"CHECK": false,
|
||||
"DEL": false,
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"CNI_PATH",
|
||||
&path,
|
||||
reqForCmdEntry{
|
||||
"ADD": true,
|
||||
"GET": true,
|
||||
"CHECK": true,
|
||||
"DEL": true,
|
||||
"GC": true,
|
||||
"STATUS": true,
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"CNI_NETNS_OVERRIDE",
|
||||
&netnsOverride,
|
||||
reqForCmdEntry{
|
||||
"ADD": false,
|
||||
"CHECK": false,
|
||||
"DEL": false,
|
||||
},
|
||||
nil,
|
||||
},
|
||||
}
|
||||
|
||||
argsMissing := false
|
||||
argsMissing := make([]string, 0)
|
||||
for _, v := range vars {
|
||||
*v.val = t.Getenv(v.name)
|
||||
if *v.val == "" {
|
||||
if v.reqForCmd[cmd] || v.name == "CNI_COMMAND" {
|
||||
fmt.Fprintf(t.Stderr, "%v env variable missing\n", v.name)
|
||||
argsMissing = true
|
||||
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 argsMissing {
|
||||
return "", nil, fmt.Errorf("required env variables missing")
|
||||
if len(argsMissing) > 0 {
|
||||
joined := strings.Join(argsMissing, ",")
|
||||
return "", nil, types.NewError(types.ErrInvalidEnvironmentVariables, fmt.Sprintf("required env variables [%s] missing", joined), "")
|
||||
}
|
||||
|
||||
if cmd == "VERSION" {
|
||||
t.Stdin = bytes.NewReader(nil)
|
||||
}
|
||||
|
||||
stdinData, err := ioutil.ReadAll(t.Stdin)
|
||||
stdinData, err := io.ReadAll(t.Stdin)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("error reading from stdin: %v", err)
|
||||
return "", nil, types.NewError(types.ErrIOFailure, fmt.Sprintf("error reading from stdin: %v", err), "")
|
||||
}
|
||||
|
||||
if cmd != "VERSION" {
|
||||
if err := validateConfig(stdinData); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
}
|
||||
|
||||
cmdArgs := &CmdArgs{
|
||||
|
@ -147,115 +182,171 @@ func (t *dispatcher) getCmdArgsFromEnv() (string, *CmdArgs, error) {
|
|||
Args: args,
|
||||
Path: path,
|
||||
StdinData: stdinData,
|
||||
NetnsOverride: netnsOverride,
|
||||
}
|
||||
return cmd, cmdArgs, nil
|
||||
}
|
||||
|
||||
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 {
|
||||
func (t *dispatcher) checkVersionAndCall(cmdArgs *CmdArgs, pluginVersionInfo version.PluginInfo, toCall func(*CmdArgs) error) *types.Error {
|
||||
configVersion, err := t.ConfVersionDecoder.Decode(cmdArgs.StdinData)
|
||||
if err != nil {
|
||||
return err
|
||||
return types.NewError(types.ErrDecodingFailure, err.Error(), "")
|
||||
}
|
||||
verErr := t.VersionReconciler.Check(configVersion, pluginVersionInfo)
|
||||
if verErr != nil {
|
||||
return &types.Error{
|
||||
Code: types.ErrIncompatibleCNIVersion,
|
||||
Msg: "incompatible CNI versions",
|
||||
Details: verErr.Details(),
|
||||
}
|
||||
return types.NewError(types.ErrIncompatibleCNIVersion, "incompatible CNI versions", verErr.Details())
|
||||
}
|
||||
|
||||
return toCall(cmdArgs)
|
||||
if toCall == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err = toCall(cmdArgs); err != nil {
|
||||
var e *types.Error
|
||||
if errors.As(err, &e) {
|
||||
// don't wrap Error in Error
|
||||
return e
|
||||
}
|
||||
return types.NewError(types.ErrInternal, err.Error(), "")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateConfig(jsonBytes []byte) error {
|
||||
func validateConfig(jsonBytes []byte) *types.Error {
|
||||
var conf struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
if err := json.Unmarshal(jsonBytes, &conf); err != nil {
|
||||
return fmt.Errorf("error reading network config: %s", err)
|
||||
return types.NewError(types.ErrDecodingFailure, fmt.Sprintf("error unmarshall network config: %v", err), "")
|
||||
}
|
||||
if conf.Name == "" {
|
||||
return fmt.Errorf("missing network name")
|
||||
return types.NewError(types.ErrInvalidNetworkConfig, "missing network name", "")
|
||||
}
|
||||
if err := utils.ValidateNetworkName(conf.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *dispatcher) pluginMain(cmdAdd, cmdGet, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo, about string) *types.Error {
|
||||
func (t *dispatcher) pluginMain(funcs CNIFuncs, versionInfo version.PluginInfo, about string) *types.Error {
|
||||
cmd, cmdArgs, err := t.getCmdArgsFromEnv()
|
||||
if err != nil {
|
||||
// Print the about string to stderr when no command is set
|
||||
if t.Getenv("CNI_COMMAND") == "" && about != "" {
|
||||
fmt.Fprintln(t.Stderr, about)
|
||||
}
|
||||
return createTypedError(err.Error())
|
||||
}
|
||||
|
||||
if cmd != "VERSION" {
|
||||
err = validateConfig(cmdArgs.StdinData)
|
||||
if err != nil {
|
||||
return createTypedError(err.Error())
|
||||
if err.Code == types.ErrInvalidEnvironmentVariables && t.Getenv("CNI_COMMAND") == "" && about != "" {
|
||||
_, _ = fmt.Fprintln(t.Stderr, about)
|
||||
_, _ = fmt.Fprintf(t.Stderr, "CNI protocol versions supported: %s\n", strings.Join(versionInfo.SupportedVersions(), ", "))
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
switch cmd {
|
||||
case "ADD":
|
||||
err = t.checkVersionAndCall(cmdArgs, versionInfo, cmdAdd)
|
||||
case "GET":
|
||||
err = t.checkVersionAndCall(cmdArgs, versionInfo, funcs.Add)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.ToUpper(cmdArgs.NetnsOverride) != "TRUE" && cmdArgs.NetnsOverride != "1" {
|
||||
isPluginNetNS, checkErr := ns.CheckNetNS(cmdArgs.Netns)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
} else if isPluginNetNS {
|
||||
return types.NewError(types.ErrInvalidNetNS, "plugin's netns and netns from CNI_NETNS should not be the same", "")
|
||||
}
|
||||
}
|
||||
case "CHECK":
|
||||
configVersion, err := t.ConfVersionDecoder.Decode(cmdArgs.StdinData)
|
||||
if err != nil {
|
||||
return createTypedError(err.Error())
|
||||
return types.NewError(types.ErrDecodingFailure, err.Error(), "")
|
||||
}
|
||||
if gtet, err := version.GreaterThanOrEqualTo(configVersion, "0.4.0"); err != nil {
|
||||
return createTypedError(err.Error())
|
||||
return types.NewError(types.ErrDecodingFailure, err.Error(), "")
|
||||
} else if !gtet {
|
||||
return &types.Error{
|
||||
Code: types.ErrIncompatibleCNIVersion,
|
||||
Msg: "config version does not allow GET",
|
||||
}
|
||||
return types.NewError(types.ErrIncompatibleCNIVersion, "config version does not allow CHECK", "")
|
||||
}
|
||||
for _, pluginVersion := range versionInfo.SupportedVersions() {
|
||||
gtet, err := version.GreaterThanOrEqualTo(pluginVersion, configVersion)
|
||||
if err != nil {
|
||||
return createTypedError(err.Error())
|
||||
return types.NewError(types.ErrDecodingFailure, err.Error(), "")
|
||||
} else if gtet {
|
||||
if err := t.checkVersionAndCall(cmdArgs, versionInfo, cmdGet); err != nil {
|
||||
return createTypedError(err.Error())
|
||||
if err := t.checkVersionAndCall(cmdArgs, versionInfo, funcs.Check); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return &types.Error{
|
||||
Code: types.ErrIncompatibleCNIVersion,
|
||||
Msg: "plugin version does not allow GET",
|
||||
}
|
||||
return types.NewError(types.ErrIncompatibleCNIVersion, "plugin version does not allow CHECK", "")
|
||||
case "DEL":
|
||||
err = t.checkVersionAndCall(cmdArgs, versionInfo, cmdDel)
|
||||
err = t.checkVersionAndCall(cmdArgs, versionInfo, funcs.Del)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.ToUpper(cmdArgs.NetnsOverride) != "TRUE" && cmdArgs.NetnsOverride != "1" {
|
||||
isPluginNetNS, checkErr := ns.CheckNetNS(cmdArgs.Netns)
|
||||
if checkErr != nil {
|
||||
return checkErr
|
||||
} else if isPluginNetNS {
|
||||
return types.NewError(types.ErrInvalidNetNS, "plugin's netns and netns from CNI_NETNS should not be the same", "")
|
||||
}
|
||||
}
|
||||
case "GC":
|
||||
configVersion, err := t.ConfVersionDecoder.Decode(cmdArgs.StdinData)
|
||||
if err != nil {
|
||||
return types.NewError(types.ErrDecodingFailure, err.Error(), "")
|
||||
}
|
||||
if gtet, err := version.GreaterThanOrEqualTo(configVersion, "1.1.0"); err != nil {
|
||||
return types.NewError(types.ErrDecodingFailure, err.Error(), "")
|
||||
} else if !gtet {
|
||||
return types.NewError(types.ErrIncompatibleCNIVersion, "config version does not allow GC", "")
|
||||
}
|
||||
for _, pluginVersion := range versionInfo.SupportedVersions() {
|
||||
gtet, err := version.GreaterThanOrEqualTo(pluginVersion, configVersion)
|
||||
if err != nil {
|
||||
return types.NewError(types.ErrDecodingFailure, err.Error(), "")
|
||||
} else if gtet {
|
||||
if err := t.checkVersionAndCall(cmdArgs, versionInfo, funcs.GC); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return types.NewError(types.ErrIncompatibleCNIVersion, "plugin version does not allow GC", "")
|
||||
case "STATUS":
|
||||
configVersion, err := t.ConfVersionDecoder.Decode(cmdArgs.StdinData)
|
||||
if err != nil {
|
||||
return types.NewError(types.ErrDecodingFailure, err.Error(), "")
|
||||
}
|
||||
if gtet, err := version.GreaterThanOrEqualTo(configVersion, "1.1.0"); err != nil {
|
||||
return types.NewError(types.ErrDecodingFailure, err.Error(), "")
|
||||
} else if !gtet {
|
||||
return types.NewError(types.ErrIncompatibleCNIVersion, "config version does not allow STATUS", "")
|
||||
}
|
||||
for _, pluginVersion := range versionInfo.SupportedVersions() {
|
||||
gtet, err := version.GreaterThanOrEqualTo(pluginVersion, configVersion)
|
||||
if err != nil {
|
||||
return types.NewError(types.ErrDecodingFailure, err.Error(), "")
|
||||
} else if gtet {
|
||||
if err := t.checkVersionAndCall(cmdArgs, versionInfo, funcs.Status); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return types.NewError(types.ErrIncompatibleCNIVersion, "plugin version does not allow STATUS", "")
|
||||
case "VERSION":
|
||||
err = versionInfo.Encode(t.Stdout)
|
||||
if err := versionInfo.Encode(t.Stdout); err != nil {
|
||||
return types.NewError(types.ErrIOFailure, err.Error(), "")
|
||||
}
|
||||
default:
|
||||
return createTypedError("unknown CNI_COMMAND: %v", cmd)
|
||||
return types.NewError(types.ErrInvalidEnvironmentVariables, fmt.Sprintf("unknown CNI_COMMAND: %v", cmd), "")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if e, ok := err.(*types.Error); ok {
|
||||
// don't wrap Error in Error
|
||||
return e
|
||||
}
|
||||
return createTypedError(err.Error())
|
||||
}
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
// PluginMainWithError is the core "main" for a plugin. It accepts
|
||||
// callback functions for add, get, and del CNI commands and returns an error.
|
||||
// callback functions for add, check, and del CNI commands and returns an error.
|
||||
//
|
||||
// The caller must also specify what CNI spec versions the plugin supports.
|
||||
//
|
||||
|
@ -266,13 +357,63 @@ func (t *dispatcher) pluginMain(cmdAdd, cmdGet, cmdDel func(_ *CmdArgs) error, v
|
|||
//
|
||||
// To let this package automatically handle errors and call os.Exit(1) for you,
|
||||
// use PluginMain() instead.
|
||||
func PluginMainWithError(cmdAdd, cmdGet, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo, about string) *types.Error {
|
||||
//
|
||||
// Deprecated: Use github.com/containernetworking/cni/pkg/skel.PluginMainFuncsWithError instead.
|
||||
func PluginMainWithError(cmdAdd, cmdCheck, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo, about string) *types.Error {
|
||||
return PluginMainFuncsWithError(CNIFuncs{Add: cmdAdd, Check: cmdCheck, Del: cmdDel}, versionInfo, about)
|
||||
}
|
||||
|
||||
// CNIFuncs contains a group of callback command funcs to be passed in as
|
||||
// parameters to the core "main" for a plugin.
|
||||
type CNIFuncs struct {
|
||||
Add func(_ *CmdArgs) error
|
||||
Del func(_ *CmdArgs) error
|
||||
Check func(_ *CmdArgs) error
|
||||
GC func(_ *CmdArgs) error
|
||||
Status func(_ *CmdArgs) error
|
||||
}
|
||||
|
||||
// PluginMainFuncsWithError is the core "main" for a plugin. It accepts
|
||||
// callback functions defined within CNIFuncs and returns an error.
|
||||
//
|
||||
// The caller must also specify what CNI spec versions the plugin supports.
|
||||
//
|
||||
// It is the responsibility of the caller to check for non-nil error return.
|
||||
//
|
||||
// For a plugin to comply with the CNI spec, it must print any error to stdout
|
||||
// as JSON and then exit with nonzero status code.
|
||||
//
|
||||
// To let this package automatically handle errors and call os.Exit(1) for you,
|
||||
// use PluginMainFuncs() instead.
|
||||
func PluginMainFuncsWithError(funcs CNIFuncs, versionInfo version.PluginInfo, about string) *types.Error {
|
||||
return (&dispatcher{
|
||||
Getenv: os.Getenv,
|
||||
Stdin: os.Stdin,
|
||||
Stdout: os.Stdout,
|
||||
Stderr: os.Stderr,
|
||||
}).pluginMain(cmdAdd, cmdGet, cmdDel, versionInfo, about)
|
||||
}).pluginMain(funcs, versionInfo, about)
|
||||
}
|
||||
|
||||
// PluginMainFuncs is the core "main" for a plugin which includes automatic error handling.
|
||||
// This is a newer alternative func to PluginMain which abstracts CNI commands within a
|
||||
// CNIFuncs interface.
|
||||
//
|
||||
// The caller must also specify what CNI spec versions the plugin supports.
|
||||
//
|
||||
// The caller can specify an "about" string, which is printed on stderr
|
||||
// when no CNI_COMMAND is specified. The recommended output is "CNI plugin <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.
|
||||
|
@ -280,14 +421,16 @@ func PluginMainWithError(cmdAdd, cmdGet, cmdDel func(_ *CmdArgs) error, versionI
|
|||
// 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 reccomended output is "CNI plugin <foo> v<version>"
|
||||
// when no CNI_COMMAND is specified. The recommended output is "CNI plugin <foo> v<version>"
|
||||
//
|
||||
// When an error occurs in either cmdAdd, cmdGet, or cmdDel, PluginMain will print the error
|
||||
// When an error occurs in either cmdAdd, cmdCheck, or cmdDel, PluginMain will print the error
|
||||
// as JSON to stdout and call os.Exit(1).
|
||||
//
|
||||
// To have more control over error handling, use PluginMainWithError() instead.
|
||||
func PluginMain(cmdAdd, cmdGet, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo, about string) {
|
||||
if e := PluginMainWithError(cmdAdd, cmdGet, cmdDel, versionInfo, about); e != nil {
|
||||
//
|
||||
// Deprecated: Use github.com/containernetworking/cni/pkg/skel.PluginMainFuncs instead.
|
||||
func PluginMain(cmdAdd, cmdCheck, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo, about string) {
|
||||
if e := PluginMainWithError(cmdAdd, cmdCheck, cmdDel, versionInfo, about); e != nil {
|
||||
if err := e.Print(); err != nil {
|
||||
log.Print("Error writing error JSON to stdout: ", err)
|
||||
}
|
||||
|
|
|
@ -15,10 +15,10 @@
|
|||
package skel
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestSkel(t *testing.T) {
|
||||
|
|
|
@ -20,13 +20,11 @@ import (
|
|||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/pkg/types/current"
|
||||
"github.com/containernetworking/cni/pkg/version"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/ginkgo/extensions/table"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/pkg/version"
|
||||
)
|
||||
|
||||
type fakeCmd struct {
|
||||
|
@ -50,10 +48,11 @@ var _ = Describe("dispatching to the correct callback", func() {
|
|||
environment map[string]string
|
||||
stdinData string
|
||||
stdout, stderr *bytes.Buffer
|
||||
cmdAdd, cmdGet, cmdDel *fakeCmd
|
||||
cmdAdd, cmdCheck, cmdDel, cmdGC *fakeCmd
|
||||
dispatch *dispatcher
|
||||
expectedCmdArgs *CmdArgs
|
||||
versionInfo version.PluginInfo
|
||||
funcs CNIFuncs
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
|
@ -69,7 +68,7 @@ var _ = Describe("dispatching to the correct callback", func() {
|
|||
stdinData = `{ "name":"skel-test", "some": "config", "cniVersion": "9.8.7" }`
|
||||
stdout = &bytes.Buffer{}
|
||||
stderr = &bytes.Buffer{}
|
||||
versionInfo = version.PluginSupports("9.8.7")
|
||||
versionInfo = version.PluginSupports("9.8.7", "10.0.0")
|
||||
dispatch = &dispatcher{
|
||||
Getenv: func(key string) string { return environment[key] },
|
||||
Stdin: strings.NewReader(stdinData),
|
||||
|
@ -77,8 +76,16 @@ var _ = Describe("dispatching to the correct callback", func() {
|
|||
Stderr: stderr,
|
||||
}
|
||||
cmdAdd = &fakeCmd{}
|
||||
cmdGet = &fakeCmd{}
|
||||
cmdCheck = &fakeCmd{}
|
||||
cmdDel = &fakeCmd{}
|
||||
cmdGC = &fakeCmd{}
|
||||
funcs = CNIFuncs{
|
||||
Add: cmdAdd.Func,
|
||||
Del: cmdDel.Func,
|
||||
Check: cmdCheck.Func,
|
||||
GC: cmdGC.Func,
|
||||
}
|
||||
|
||||
expectedCmdArgs = &CmdArgs{
|
||||
ContainerID: "some-container-id",
|
||||
Netns: "/some/netns/path",
|
||||
|
@ -89,37 +96,130 @@ var _ = Describe("dispatching to the correct callback", func() {
|
|||
}
|
||||
})
|
||||
|
||||
var envVarChecker = func(envVar string, isRequired bool) {
|
||||
envVarChecker := func(envVar string, isRequired bool) {
|
||||
delete(environment, envVar)
|
||||
|
||||
err := dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")
|
||||
err := dispatch.pluginMain(funcs, versionInfo, "")
|
||||
if isRequired {
|
||||
Expect(err).To(Equal(&types.Error{
|
||||
Code: 100,
|
||||
Msg: "required env variables missing",
|
||||
Code: types.ErrInvalidEnvironmentVariables,
|
||||
Msg: "required env variables [" + envVar + "] missing",
|
||||
}))
|
||||
Expect(stderr.String()).To(ContainSubstring(envVar + " env variable missing\n"))
|
||||
} else {
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
err := dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")
|
||||
err := dispatch.pluginMain(funcs, versionInfo, "")
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(cmdAdd.CallCount).To(Equal(1))
|
||||
Expect(cmdGet.CallCount).To(Equal(0))
|
||||
Expect(cmdCheck.CallCount).To(Equal(0))
|
||||
Expect(cmdDel.CallCount).To(Equal(0))
|
||||
Expect(cmdAdd.Received.CmdArgs).To(Equal(expectedCmdArgs))
|
||||
})
|
||||
|
||||
It("does not call cmdGet or cmdDel", func() {
|
||||
err := dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")
|
||||
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() {
|
||||
err := dispatch.pluginMain(funcs, versionInfo, "")
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(cmdGet.CallCount).To(Equal(0))
|
||||
Expect(cmdCheck.CallCount).To(Equal(0))
|
||||
Expect(cmdDel.CallCount).To(Equal(0))
|
||||
})
|
||||
|
||||
|
@ -140,13 +240,13 @@ var _ = Describe("dispatching to the correct callback", func() {
|
|||
})
|
||||
|
||||
It("reports that all of them are missing, not just the first", func() {
|
||||
Expect(dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")).NotTo(Succeed())
|
||||
|
||||
log := stderr.String()
|
||||
Expect(log).To(ContainSubstring("CNI_NETNS env variable missing\n"))
|
||||
Expect(log).To(ContainSubstring("CNI_IFNAME env variable missing\n"))
|
||||
Expect(log).To(ContainSubstring("CNI_PATH env variable missing\n"))
|
||||
err := dispatch.pluginMain(funcs, versionInfo, "")
|
||||
Expect(err).To(HaveOccurred())
|
||||
|
||||
Expect(err).To(Equal(&types.Error{
|
||||
Code: types.ErrInvalidEnvironmentVariables,
|
||||
Msg: "required env variables [CNI_NETNS,CNI_IFNAME,CNI_PATH] missing",
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -162,8 +262,7 @@ var _ = Describe("dispatching to the correct callback", func() {
|
|||
})
|
||||
|
||||
It("infers the config is 0.1.0 and calls the cmdAdd callback", func() {
|
||||
|
||||
err := dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")
|
||||
err := dispatch.pluginMain(funcs, versionInfo, "")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(cmdAdd.CallCount).To(Equal(1))
|
||||
|
@ -177,39 +276,40 @@ var _ = Describe("dispatching to the correct callback", func() {
|
|||
})
|
||||
|
||||
It("immediately returns a useful error", func() {
|
||||
err := dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")
|
||||
Expect(err.Code).To(Equal(types.ErrIncompatibleCNIVersion)) // see https://github.com/containernetworking/cni/blob/master/SPEC.md#well-known-error-codes
|
||||
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("incompatible CNI versions"))
|
||||
Expect(err.Details).To(Equal(`config is "0.1.0", plugin supports ["4.3.2"]`))
|
||||
})
|
||||
|
||||
It("does not call either callback", func() {
|
||||
dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")
|
||||
err := dispatch.pluginMain(funcs, versionInfo, "")
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(cmdAdd.CallCount).To(Equal(0))
|
||||
Expect(cmdGet.CallCount).To(Equal(0))
|
||||
Expect(cmdCheck.CallCount).To(Equal(0))
|
||||
Expect(cmdDel.CallCount).To(Equal(0))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Context("when the CNI_COMMAND is GET", func() {
|
||||
Context("when the CNI_COMMAND is CHECK", func() {
|
||||
BeforeEach(func() {
|
||||
environment["CNI_COMMAND"] = "GET"
|
||||
environment["CNI_COMMAND"] = "CHECK"
|
||||
})
|
||||
|
||||
It("extracts env vars and stdin data and calls cmdGet", func() {
|
||||
err := dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")
|
||||
It("extracts env vars and stdin data and calls cmdCheck", func() {
|
||||
err := dispatch.pluginMain(funcs, versionInfo, "")
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(cmdAdd.CallCount).To(Equal(0))
|
||||
Expect(cmdGet.CallCount).To(Equal(1))
|
||||
Expect(cmdCheck.CallCount).To(Equal(1))
|
||||
Expect(cmdDel.CallCount).To(Equal(0))
|
||||
Expect(cmdGet.Received.CmdArgs).To(Equal(expectedCmdArgs))
|
||||
Expect(cmdCheck.Received.CmdArgs).To(Equal(expectedCmdArgs))
|
||||
})
|
||||
|
||||
It("does not call cmdAdd or cmdDel", func() {
|
||||
err := dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")
|
||||
err := dispatch.pluginMain(funcs, versionInfo, "")
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(cmdAdd.CallCount).To(Equal(0))
|
||||
|
@ -233,23 +333,24 @@ var _ = Describe("dispatching to the correct callback", func() {
|
|||
})
|
||||
|
||||
It("reports that all of them are missing, not just the first", func() {
|
||||
Expect(dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")).NotTo(Succeed())
|
||||
log := stderr.String()
|
||||
Expect(log).To(ContainSubstring("CNI_NETNS env variable missing\n"))
|
||||
Expect(log).To(ContainSubstring("CNI_IFNAME env variable missing\n"))
|
||||
Expect(log).To(ContainSubstring("CNI_PATH env variable missing\n"))
|
||||
err := dispatch.pluginMain(funcs, versionInfo, "")
|
||||
Expect(err).To(HaveOccurred())
|
||||
|
||||
Expect(err).To(Equal(&types.Error{
|
||||
Code: types.ErrInvalidEnvironmentVariables,
|
||||
Msg: "required env variables [CNI_NETNS,CNI_IFNAME,CNI_PATH] missing",
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when cniVersion is less than 0.4.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(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")
|
||||
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 GET"))
|
||||
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 CHECK"))
|
||||
Expect(cmdAdd.CallCount).To(Equal(0))
|
||||
Expect(cmdGet.CallCount).To(Equal(0))
|
||||
Expect(cmdCheck.CallCount).To(Equal(0))
|
||||
Expect(cmdDel.CallCount).To(Equal(0))
|
||||
})
|
||||
})
|
||||
|
@ -258,47 +359,178 @@ var _ = Describe("dispatching to the correct callback", func() {
|
|||
It("immediately returns a useful error", func() {
|
||||
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")
|
||||
err := dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")
|
||||
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 GET"))
|
||||
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 CHECK"))
|
||||
Expect(cmdAdd.CallCount).To(Equal(0))
|
||||
Expect(cmdGet.CallCount).To(Equal(0))
|
||||
Expect(cmdCheck.CallCount).To(Equal(0))
|
||||
Expect(cmdDel.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" }`)
|
||||
dispatch.Stdin = strings.NewReader(`{ "cniVersion": "adsfsadf", "some": "config", "name": "test" }`)
|
||||
versionInfo = version.PluginSupports("0.1.0", "0.2.0", "0.3.0")
|
||||
err := dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")
|
||||
Expect(err.Code).To(Equal(uint(100)))
|
||||
err := dispatch.pluginMain(funcs, versionInfo, "")
|
||||
Expect(err.Code).To(Equal(types.ErrDecodingFailure))
|
||||
Expect(cmdAdd.CallCount).To(Equal(0))
|
||||
Expect(cmdGet.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(cmdCheck.CallCount).To(Equal(0))
|
||||
Expect(cmdDel.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": "0.4.0", "some": "config" }`)
|
||||
dispatch.Stdin = strings.NewReader(`{ "cniVersion": "0.4.0", "some": "config", "name": "test" }`)
|
||||
versionInfo = version.PluginSupports("0.1.0", "0.2.0", "adsfasdf")
|
||||
err := dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")
|
||||
Expect(err.Code).To(Equal(uint(100)))
|
||||
err := dispatch.pluginMain(funcs, versionInfo, "")
|
||||
Expect(err.Code).To(Equal(types.ErrDecodingFailure))
|
||||
Expect(cmdAdd.CallCount).To(Equal(0))
|
||||
Expect(cmdGet.CallCount).To(Equal(0))
|
||||
Expect(cmdCheck.CallCount).To(Equal(0))
|
||||
Expect(cmdDel.CallCount).To(Equal(0))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
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() {
|
||||
BeforeEach(func() {
|
||||
environment["CNI_COMMAND"] = "DEL"
|
||||
})
|
||||
|
||||
It("calls cmdDel with the env vars and stdin data", func() {
|
||||
err := dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")
|
||||
err := dispatch.pluginMain(funcs, versionInfo, "")
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(cmdDel.CallCount).To(Equal(1))
|
||||
|
@ -306,7 +538,7 @@ var _ = Describe("dispatching to the correct callback", func() {
|
|||
})
|
||||
|
||||
It("does not call cmdAdd", func() {
|
||||
err := dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")
|
||||
err := dispatch.pluginMain(funcs, versionInfo, "")
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(cmdAdd.CallCount).To(Equal(0))
|
||||
|
@ -328,17 +560,17 @@ var _ = Describe("dispatching to the correct callback", func() {
|
|||
})
|
||||
|
||||
It("prints the version to stdout", func() {
|
||||
err := dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")
|
||||
err := dispatch.pluginMain(funcs, versionInfo, "")
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(stdout).To(MatchJSON(fmt.Sprintf(`{
|
||||
"cniVersion": "%s",
|
||||
"supportedVersions": ["9.8.7"]
|
||||
}`, current.ImplementedSpecVersion)))
|
||||
"supportedVersions": ["9.8.7", "10.0.0"]
|
||||
}`, version.Current())))
|
||||
})
|
||||
|
||||
It("does not call cmdAdd or cmdDel", func() {
|
||||
err := dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")
|
||||
err := dispatch.pluginMain(funcs, versionInfo, "")
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(cmdAdd.CallCount).To(Equal(0))
|
||||
|
@ -358,14 +590,14 @@ var _ = Describe("dispatching to the correct callback", func() {
|
|||
r := &BadReader{}
|
||||
dispatch.Stdin = r
|
||||
|
||||
err := dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")
|
||||
err := dispatch.pluginMain(funcs, versionInfo, "")
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(r.ReadCount).To(Equal(0))
|
||||
Expect(stdout).To(MatchJSON(fmt.Sprintf(`{
|
||||
"cniVersion": "%s",
|
||||
"supportedVersions": ["9.8.7"]
|
||||
}`, current.ImplementedSpecVersion)))
|
||||
"supportedVersions": ["9.8.7", "10.0.0"]
|
||||
}`, version.Current())))
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -375,45 +607,72 @@ var _ = Describe("dispatching to the correct callback", func() {
|
|||
})
|
||||
|
||||
It("does not call any cmd callback", func() {
|
||||
dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")
|
||||
|
||||
err := dispatch.pluginMain(funcs, versionInfo, "")
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(cmdAdd.CallCount).To(Equal(0))
|
||||
Expect(cmdDel.CallCount).To(Equal(0))
|
||||
})
|
||||
|
||||
It("returns an error", func() {
|
||||
err := dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")
|
||||
err := dispatch.pluginMain(funcs, versionInfo, "")
|
||||
|
||||
Expect(err).To(Equal(&types.Error{
|
||||
Code: 100,
|
||||
Code: types.ErrInvalidEnvironmentVariables,
|
||||
Msg: "unknown CNI_COMMAND: NOPE",
|
||||
}))
|
||||
})
|
||||
|
||||
It("prints the about string when the command is blank", func() {
|
||||
environment["CNI_COMMAND"] = ""
|
||||
dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "test framework v42")
|
||||
err := dispatch.pluginMain(funcs, versionInfo, "test framework v42")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(stderr.String()).To(ContainSubstring("test framework v42"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when the CNI_COMMAND is missing", func() {
|
||||
It("prints the about string to stderr", func() {
|
||||
environment = map[string]string{}
|
||||
err := dispatch.pluginMain(funcs, versionInfo, "AWESOME PLUGIN")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(cmdAdd.CallCount).To(Equal(0))
|
||||
Expect(cmdDel.CallCount).To(Equal(0))
|
||||
log := stderr.String()
|
||||
Expect(log).To(Equal("AWESOME PLUGIN\nCNI protocol versions supported: 9.8.7, 10.0.0\n"))
|
||||
})
|
||||
|
||||
It("fails if there is no about string", func() {
|
||||
environment = map[string]string{}
|
||||
err := dispatch.pluginMain(funcs, versionInfo, "")
|
||||
Expect(err).To(HaveOccurred())
|
||||
|
||||
Expect(cmdAdd.CallCount).To(Equal(0))
|
||||
Expect(cmdDel.CallCount).To(Equal(0))
|
||||
Expect(err).To(Equal(&types.Error{
|
||||
Code: types.ErrInvalidEnvironmentVariables,
|
||||
Msg: "required env variables [CNI_COMMAND] missing",
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when stdin cannot be read", func() {
|
||||
BeforeEach(func() {
|
||||
dispatch.Stdin = &BadReader{}
|
||||
})
|
||||
|
||||
It("does not call any cmd callback", func() {
|
||||
dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")
|
||||
|
||||
err := dispatch.pluginMain(funcs, versionInfo, "")
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(cmdAdd.CallCount).To(Equal(0))
|
||||
Expect(cmdDel.CallCount).To(Equal(0))
|
||||
})
|
||||
|
||||
It("wraps and returns the error", func() {
|
||||
err := dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")
|
||||
err := dispatch.pluginMain(funcs, versionInfo, "")
|
||||
|
||||
Expect(err).To(Equal(&types.Error{
|
||||
Code: 100,
|
||||
Code: types.ErrIOFailure,
|
||||
Msg: "error reading from stdin: banana",
|
||||
}))
|
||||
})
|
||||
|
@ -429,7 +688,7 @@ var _ = Describe("dispatching to the correct callback", func() {
|
|||
})
|
||||
|
||||
It("returns the error as-is", func() {
|
||||
err := dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")
|
||||
err := dispatch.pluginMain(funcs, versionInfo, "")
|
||||
|
||||
Expect(err).To(Equal(&types.Error{
|
||||
Code: 1234,
|
||||
|
@ -444,10 +703,10 @@ var _ = Describe("dispatching to the correct callback", func() {
|
|||
})
|
||||
|
||||
It("wraps and returns the error", func() {
|
||||
err := dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")
|
||||
err := dispatch.pluginMain(funcs, versionInfo, "")
|
||||
|
||||
Expect(err).To(Equal(&types.Error{
|
||||
Code: 100,
|
||||
Code: types.ErrInternal,
|
||||
Msg: "potato",
|
||||
}))
|
||||
})
|
||||
|
|
|
@ -17,29 +17,52 @@ package types020
|
|||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
convert "github.com/containernetworking/cni/pkg/types/internal"
|
||||
)
|
||||
|
||||
const ImplementedSpecVersion string = "0.2.0"
|
||||
|
||||
var SupportedVersions = []string{"", "0.1.0", ImplementedSpecVersion}
|
||||
var supportedVersions = []string{"", "0.1.0", ImplementedSpecVersion}
|
||||
|
||||
// Register converters for all versions less than the implemented spec version
|
||||
func init() {
|
||||
convert.RegisterConverter("0.1.0", []string{ImplementedSpecVersion}, convertFrom010)
|
||||
convert.RegisterConverter(ImplementedSpecVersion, []string{"0.1.0"}, convertTo010)
|
||||
|
||||
// Creator
|
||||
convert.RegisterCreator(supportedVersions, NewResult)
|
||||
}
|
||||
|
||||
// Compatibility types for CNI version 0.1.0 and 0.2.0
|
||||
|
||||
// NewResult creates a new Result object from JSON data. The JSON data
|
||||
// must be compatible with the CNI versions implemented by this type.
|
||||
func NewResult(data []byte) (types.Result, error) {
|
||||
result := &Result{}
|
||||
if err := json.Unmarshal(data, result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, v := range supportedVersions {
|
||||
if result.CNIVersion == v {
|
||||
if result.CNIVersion == "" {
|
||||
result.CNIVersion = "0.1.0"
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("result type supports %v but unmarshalled CNIVersion is %q",
|
||||
supportedVersions, result.CNIVersion)
|
||||
}
|
||||
|
||||
// GetResult converts the given Result object to the ImplementedSpecVersion
|
||||
// and returns the concrete type or an error
|
||||
func GetResult(r types.Result) (*Result, error) {
|
||||
// We expect version 0.1.0/0.2.0 results
|
||||
result020, err := r.GetAsVersion(ImplementedSpecVersion)
|
||||
result020, err := convert.Convert(r, ImplementedSpecVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -50,6 +73,32 @@ func GetResult(r types.Result) (*Result, error) {
|
|||
return result, nil
|
||||
}
|
||||
|
||||
func convertFrom010(from types.Result, toVersion string) (types.Result, error) {
|
||||
if toVersion != "0.2.0" {
|
||||
panic("only converts to version 0.2.0")
|
||||
}
|
||||
fromResult := from.(*Result)
|
||||
return &Result{
|
||||
CNIVersion: ImplementedSpecVersion,
|
||||
IP4: fromResult.IP4.Copy(),
|
||||
IP6: fromResult.IP6.Copy(),
|
||||
DNS: *fromResult.DNS.Copy(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func convertTo010(from types.Result, toVersion string) (types.Result, error) {
|
||||
if toVersion != "0.1.0" {
|
||||
panic("only converts to version 0.1.0")
|
||||
}
|
||||
fromResult := from.(*Result)
|
||||
return &Result{
|
||||
CNIVersion: "0.1.0",
|
||||
IP4: fromResult.IP4.Copy(),
|
||||
IP6: fromResult.IP6.Copy(),
|
||||
DNS: *fromResult.DNS.Copy(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Result is what gets returned from the plugin (via stdout) to the caller
|
||||
type Result struct {
|
||||
CNIVersion string `json:"cniVersion,omitempty"`
|
||||
|
@ -59,42 +108,31 @@ type Result struct {
|
|||
}
|
||||
|
||||
func (r *Result) Version() string {
|
||||
return ImplementedSpecVersion
|
||||
return r.CNIVersion
|
||||
}
|
||||
|
||||
func (r *Result) GetAsVersion(version string) (types.Result, error) {
|
||||
for _, supportedVersion := range SupportedVersions {
|
||||
if version == supportedVersion {
|
||||
r.CNIVersion = version
|
||||
return r, nil
|
||||
// If the creator of the result did not set the CNIVersion, assume it
|
||||
// should be the highest spec version implemented by this Result
|
||||
if r.CNIVersion == "" {
|
||||
r.CNIVersion = ImplementedSpecVersion
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("cannot convert version %q to %s", SupportedVersions, version)
|
||||
return convert.Convert(r, version)
|
||||
}
|
||||
|
||||
func (r *Result) Print() error {
|
||||
return r.PrintTo(os.Stdout)
|
||||
}
|
||||
|
||||
func (r *Result) PrintTo(writer io.Writer) error {
|
||||
data, err := json.MarshalIndent(r, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = os.Stdout.Write(data)
|
||||
_, err = writer.Write(data)
|
||||
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
|
||||
type IPConfig struct {
|
||||
IP net.IPNet
|
||||
|
@ -102,6 +140,22 @@ type IPConfig struct {
|
|||
Routes []types.Route
|
||||
}
|
||||
|
||||
func (i *IPConfig) Copy() *IPConfig {
|
||||
if i == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var routes []types.Route
|
||||
for _, fromRoute := range i.Routes {
|
||||
routes = append(routes, *fromRoute.Copy())
|
||||
}
|
||||
return &IPConfig{
|
||||
IP: i.IP,
|
||||
Gateway: i.Gateway,
|
||||
Routes: routes,
|
||||
}
|
||||
}
|
||||
|
||||
// net.IPNet is not JSON (un)marshallable so this duality is needed
|
||||
// for our custom IPNet type
|
||||
|
||||
|
|
|
@ -15,10 +15,10 @@
|
|||
package types020_test
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestTypes010(t *testing.T) {
|
||||
|
|
|
@ -15,19 +15,19 @@
|
|||
package types020_test
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/pkg/types/020"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/containernetworking/cni/pkg/types/create"
|
||||
)
|
||||
|
||||
var _ = Describe("Ensures compatibility with the 0.1.0/0.2.0 spec", func() {
|
||||
It("correctly encodes a 0.1.0/0.2.0 Result", func() {
|
||||
func testResult(resultCNIVersion, jsonCNIVersion string) (*types020.Result, string) {
|
||||
ipv4, err := types.ParseCIDR("1.2.3.30/24")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(ipv4).NotTo(BeNil())
|
||||
|
@ -47,8 +47,8 @@ var _ = Describe("Ensures compatibility with the 0.1.0/0.2.0 spec", func() {
|
|||
Expect(routegwv6).NotTo(BeNil())
|
||||
|
||||
// Set every field of the struct to ensure source compatibility
|
||||
res := types020.Result{
|
||||
CNIVersion: types020.ImplementedSpecVersion,
|
||||
res := &types020.Result{
|
||||
CNIVersion: resultCNIVersion,
|
||||
IP4: &types020.IPConfig{
|
||||
IP: *ipv4,
|
||||
Gateway: net.ParseIP("1.2.3.1"),
|
||||
|
@ -71,25 +71,8 @@ var _ = Describe("Ensures compatibility with the 0.1.0/0.2.0 spec", func() {
|
|||
},
|
||||
}
|
||||
|
||||
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
|
||||
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",
|
||||
json := fmt.Sprintf(`{
|
||||
"cniVersion": "%s",
|
||||
"ip4": {
|
||||
"ip": "1.2.3.30/24",
|
||||
"gateway": "1.2.3.1",
|
||||
|
@ -125,6 +108,50 @@ var _ = Describe("Ensures compatibility with the 0.1.0/0.2.0 spec", func() {
|
|||
"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"))
|
||||
})
|
||||
})
|
||||
|
|
|
@ -0,0 +1,306 @@
|
|||
// Copyright 2016 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package types040
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
types020 "github.com/containernetworking/cni/pkg/types/020"
|
||||
convert "github.com/containernetworking/cni/pkg/types/internal"
|
||||
)
|
||||
|
||||
const ImplementedSpecVersion string = "0.4.0"
|
||||
|
||||
var supportedVersions = []string{"0.3.0", "0.3.1", ImplementedSpecVersion}
|
||||
|
||||
// Register converters for all versions less than the implemented spec version
|
||||
func init() {
|
||||
// Up-converters
|
||||
convert.RegisterConverter("0.1.0", supportedVersions, convertFrom02x)
|
||||
convert.RegisterConverter("0.2.0", supportedVersions, convertFrom02x)
|
||||
convert.RegisterConverter("0.3.0", supportedVersions, convertInternal)
|
||||
convert.RegisterConverter("0.3.1", supportedVersions, convertInternal)
|
||||
|
||||
// Down-converters
|
||||
convert.RegisterConverter("0.4.0", []string{"0.3.0", "0.3.1"}, convertInternal)
|
||||
convert.RegisterConverter("0.4.0", []string{"0.1.0", "0.2.0"}, convertTo02x)
|
||||
convert.RegisterConverter("0.3.1", []string{"0.1.0", "0.2.0"}, convertTo02x)
|
||||
convert.RegisterConverter("0.3.0", []string{"0.1.0", "0.2.0"}, convertTo02x)
|
||||
|
||||
// Creator
|
||||
convert.RegisterCreator(supportedVersions, NewResult)
|
||||
}
|
||||
|
||||
func NewResult(data []byte) (types.Result, error) {
|
||||
result := &Result{}
|
||||
if err := json.Unmarshal(data, result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, v := range supportedVersions {
|
||||
if result.CNIVersion == v {
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("result type supports %v but unmarshalled CNIVersion is %q",
|
||||
supportedVersions, result.CNIVersion)
|
||||
}
|
||||
|
||||
func GetResult(r types.Result) (*Result, error) {
|
||||
resultCurrent, err := r.GetAsVersion(ImplementedSpecVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result, ok := resultCurrent.(*Result)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to convert result")
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func NewResultFromResult(result types.Result) (*Result, error) {
|
||||
newResult, err := convert.Convert(result, ImplementedSpecVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newResult.(*Result), nil
|
||||
}
|
||||
|
||||
// Result is what gets returned from the plugin (via stdout) to the caller
|
||||
type Result struct {
|
||||
CNIVersion string `json:"cniVersion,omitempty"`
|
||||
Interfaces []*Interface `json:"interfaces,omitempty"`
|
||||
IPs []*IPConfig `json:"ips,omitempty"`
|
||||
Routes []*types.Route `json:"routes,omitempty"`
|
||||
DNS types.DNS `json:"dns,omitempty"`
|
||||
}
|
||||
|
||||
func convert020IPConfig(from *types020.IPConfig, ipVersion string) *IPConfig {
|
||||
return &IPConfig{
|
||||
Version: ipVersion,
|
||||
Address: from.IP,
|
||||
Gateway: from.Gateway,
|
||||
}
|
||||
}
|
||||
|
||||
func convertFrom02x(from types.Result, toVersion string) (types.Result, error) {
|
||||
fromResult := from.(*types020.Result)
|
||||
toResult := &Result{
|
||||
CNIVersion: toVersion,
|
||||
DNS: *fromResult.DNS.Copy(),
|
||||
Routes: []*types.Route{},
|
||||
}
|
||||
if fromResult.IP4 != nil {
|
||||
toResult.IPs = append(toResult.IPs, convert020IPConfig(fromResult.IP4, "4"))
|
||||
for _, fromRoute := range fromResult.IP4.Routes {
|
||||
toResult.Routes = append(toResult.Routes, fromRoute.Copy())
|
||||
}
|
||||
}
|
||||
|
||||
if fromResult.IP6 != nil {
|
||||
toResult.IPs = append(toResult.IPs, convert020IPConfig(fromResult.IP6, "6"))
|
||||
for _, fromRoute := range fromResult.IP6.Routes {
|
||||
toResult.Routes = append(toResult.Routes, fromRoute.Copy())
|
||||
}
|
||||
}
|
||||
|
||||
return toResult, nil
|
||||
}
|
||||
|
||||
func convertInternal(from types.Result, toVersion string) (types.Result, error) {
|
||||
fromResult := from.(*Result)
|
||||
toResult := &Result{
|
||||
CNIVersion: toVersion,
|
||||
DNS: *fromResult.DNS.Copy(),
|
||||
Routes: []*types.Route{},
|
||||
}
|
||||
for _, fromIntf := range fromResult.Interfaces {
|
||||
toResult.Interfaces = append(toResult.Interfaces, fromIntf.Copy())
|
||||
}
|
||||
for _, fromIPC := range fromResult.IPs {
|
||||
toResult.IPs = append(toResult.IPs, fromIPC.Copy())
|
||||
}
|
||||
for _, fromRoute := range fromResult.Routes {
|
||||
toResult.Routes = append(toResult.Routes, fromRoute.Copy())
|
||||
}
|
||||
return toResult, nil
|
||||
}
|
||||
|
||||
func convertTo02x(from types.Result, toVersion string) (types.Result, error) {
|
||||
fromResult := from.(*Result)
|
||||
toResult := &types020.Result{
|
||||
CNIVersion: toVersion,
|
||||
DNS: *fromResult.DNS.Copy(),
|
||||
}
|
||||
|
||||
for _, fromIP := range fromResult.IPs {
|
||||
// Only convert the first IP address of each version as 0.2.0
|
||||
// and earlier cannot handle multiple IP addresses
|
||||
if fromIP.Version == "4" && toResult.IP4 == nil {
|
||||
toResult.IP4 = &types020.IPConfig{
|
||||
IP: fromIP.Address,
|
||||
Gateway: fromIP.Gateway,
|
||||
}
|
||||
} else if fromIP.Version == "6" && toResult.IP6 == nil {
|
||||
toResult.IP6 = &types020.IPConfig{
|
||||
IP: fromIP.Address,
|
||||
Gateway: fromIP.Gateway,
|
||||
}
|
||||
}
|
||||
if toResult.IP4 != nil && toResult.IP6 != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for _, fromRoute := range fromResult.Routes {
|
||||
is4 := fromRoute.Dst.IP.To4() != nil
|
||||
if is4 && toResult.IP4 != nil {
|
||||
toResult.IP4.Routes = append(toResult.IP4.Routes, types.Route{
|
||||
Dst: fromRoute.Dst,
|
||||
GW: fromRoute.GW,
|
||||
})
|
||||
} else if !is4 && toResult.IP6 != nil {
|
||||
toResult.IP6.Routes = append(toResult.IP6.Routes, types.Route{
|
||||
Dst: fromRoute.Dst,
|
||||
GW: fromRoute.GW,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 0.2.0 and earlier require at least one IP address in the Result
|
||||
if toResult.IP4 == nil && toResult.IP6 == nil {
|
||||
return nil, fmt.Errorf("cannot convert: no valid IP addresses")
|
||||
}
|
||||
|
||||
return toResult, nil
|
||||
}
|
||||
|
||||
func (r *Result) Version() string {
|
||||
return r.CNIVersion
|
||||
}
|
||||
|
||||
func (r *Result) GetAsVersion(version string) (types.Result, error) {
|
||||
// If the creator of the result did not set the CNIVersion, assume it
|
||||
// should be the highest spec version implemented by this Result
|
||||
if r.CNIVersion == "" {
|
||||
r.CNIVersion = ImplementedSpecVersion
|
||||
}
|
||||
return convert.Convert(r, version)
|
||||
}
|
||||
|
||||
func (r *Result) Print() error {
|
||||
return r.PrintTo(os.Stdout)
|
||||
}
|
||||
|
||||
func (r *Result) PrintTo(writer io.Writer) error {
|
||||
data, err := json.MarshalIndent(r, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = writer.Write(data)
|
||||
return err
|
||||
}
|
||||
|
||||
// Interface contains values about the created interfaces
|
||||
type Interface struct {
|
||||
Name string `json:"name"`
|
||||
Mac string `json:"mac,omitempty"`
|
||||
Sandbox string `json:"sandbox,omitempty"`
|
||||
}
|
||||
|
||||
func (i *Interface) String() string {
|
||||
return fmt.Sprintf("%+v", *i)
|
||||
}
|
||||
|
||||
func (i *Interface) Copy() *Interface {
|
||||
if i == nil {
|
||||
return nil
|
||||
}
|
||||
newIntf := *i
|
||||
return &newIntf
|
||||
}
|
||||
|
||||
// Int returns a pointer to the int value passed in. Used to
|
||||
// set the IPConfig.Interface field.
|
||||
func Int(v int) *int {
|
||||
return &v
|
||||
}
|
||||
|
||||
// IPConfig contains values necessary to configure an IP address on an interface
|
||||
type IPConfig struct {
|
||||
// IP version, either "4" or "6"
|
||||
Version string
|
||||
// Index into Result structs Interfaces list
|
||||
Interface *int
|
||||
Address net.IPNet
|
||||
Gateway net.IP
|
||||
}
|
||||
|
||||
func (i *IPConfig) String() string {
|
||||
return fmt.Sprintf("%+v", *i)
|
||||
}
|
||||
|
||||
func (i *IPConfig) Copy() *IPConfig {
|
||||
if i == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ipc := &IPConfig{
|
||||
Version: i.Version,
|
||||
Address: i.Address,
|
||||
Gateway: i.Gateway,
|
||||
}
|
||||
if i.Interface != nil {
|
||||
intf := *i.Interface
|
||||
ipc.Interface = &intf
|
||||
}
|
||||
return ipc
|
||||
}
|
||||
|
||||
// JSON (un)marshallable types
|
||||
type ipConfig struct {
|
||||
Version string `json:"version"`
|
||||
Interface *int `json:"interface,omitempty"`
|
||||
Address types.IPNet `json:"address"`
|
||||
Gateway net.IP `json:"gateway,omitempty"`
|
||||
}
|
||||
|
||||
func (c *IPConfig) MarshalJSON() ([]byte, error) {
|
||||
ipc := ipConfig{
|
||||
Version: c.Version,
|
||||
Interface: c.Interface,
|
||||
Address: types.IPNet(c.Address),
|
||||
Gateway: c.Gateway,
|
||||
}
|
||||
|
||||
return json.Marshal(ipc)
|
||||
}
|
||||
|
||||
func (c *IPConfig) UnmarshalJSON(data []byte) error {
|
||||
ipc := ipConfig{}
|
||||
if err := json.Unmarshal(data, &ipc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Version = ipc.Version
|
||||
c.Interface = ipc.Interface
|
||||
c.Address = net.IPNet(ipc.Address)
|
||||
c.Gateway = ipc.Gateway
|
||||
return nil
|
||||
}
|
|
@ -12,13 +12,13 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package current_test
|
||||
package types040_test
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestTypesCurrent(t *testing.T) {
|
|
@ -0,0 +1,387 @@
|
|||
// 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_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
types020 "github.com/containernetworking/cni/pkg/types/020"
|
||||
types040 "github.com/containernetworking/cni/pkg/types/040"
|
||||
)
|
||||
|
||||
func testResult() *types040.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 &types040.Result{
|
||||
CNIVersion: "0.3.1",
|
||||
Interfaces: []*types040.Interface{
|
||||
{
|
||||
Name: "eth0",
|
||||
Mac: "00:11:22:33:44:55",
|
||||
Sandbox: "/proc/3553/ns/net",
|
||||
},
|
||||
},
|
||||
IPs: []*types040.IPConfig{
|
||||
{
|
||||
Version: "4",
|
||||
Interface: types040.Int(0),
|
||||
Address: *ipv4,
|
||||
Gateway: net.ParseIP("1.2.3.1"),
|
||||
},
|
||||
{
|
||||
Version: "6",
|
||||
Interface: types040.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("040 types operations", func() {
|
||||
It("correctly encodes a 0.3.x 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": "0.3.1",
|
||||
"interfaces": [
|
||||
{
|
||||
"name": "eth0",
|
||||
"mac": "00:11:22:33:44:55",
|
||||
"sandbox": "/proc/3553/ns/net"
|
||||
}
|
||||
],
|
||||
"ips": [
|
||||
{
|
||||
"version": "4",
|
||||
"interface": 0,
|
||||
"address": "1.2.3.30/24",
|
||||
"gateway": "1.2.3.1"
|
||||
},
|
||||
{
|
||||
"version": "6",
|
||||
"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 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 round-trips a 0.2.0 Result with route gateways", func() {
|
||||
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
|
||||
res := &types020.Result{
|
||||
CNIVersion: types020.ImplementedSpecVersion,
|
||||
IP4: &types020.IPConfig{
|
||||
IP: *ipv4,
|
||||
Gateway: net.ParseIP("1.2.3.1"),
|
||||
Routes: []types.Route{
|
||||
{Dst: *routev4, GW: routegwv4},
|
||||
},
|
||||
},
|
||||
IP6: &types020.IPConfig{
|
||||
IP: *ipv6,
|
||||
Gateway: net.ParseIP("abcd:1234:ffff::1"),
|
||||
Routes: []types.Route{
|
||||
{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"},
|
||||
},
|
||||
}
|
||||
|
||||
// Convert to 040
|
||||
newRes, err := types040.NewResultFromResult(res)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
// Convert back to 0.2.0
|
||||
oldRes, err := newRes.GetAsVersion("0.2.0")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Match JSON so we can figure out what's wrong if something fails the test
|
||||
origJson, err := json.Marshal(res)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
oldJson, err := json.Marshal(oldRes)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(oldJson).To(MatchJSON(origJson))
|
||||
})
|
||||
|
||||
It("correctly round-trips a 0.2.0 Result without route gateways", func() {
|
||||
ipv4, err := types.ParseCIDR("1.2.3.30/24")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(ipv4).NotTo(BeNil())
|
||||
|
||||
_, routev4, err := net.ParseCIDR("15.5.6.0/24")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(routev4).NotTo(BeNil())
|
||||
|
||||
ipv6, err := types.ParseCIDR("abcd:1234:ffff::cdde/64")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(ipv6).NotTo(BeNil())
|
||||
|
||||
_, routev6, err := net.ParseCIDR("1111:dddd::aaaa/80")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(routev6).NotTo(BeNil())
|
||||
|
||||
// Set every field of the struct to ensure source compatibility
|
||||
res := &types020.Result{
|
||||
CNIVersion: types020.ImplementedSpecVersion,
|
||||
IP4: &types020.IPConfig{
|
||||
IP: *ipv4,
|
||||
Gateway: net.ParseIP("1.2.3.1"),
|
||||
Routes: []types.Route{
|
||||
{Dst: *routev4},
|
||||
},
|
||||
},
|
||||
IP6: &types020.IPConfig{
|
||||
IP: *ipv6,
|
||||
Gateway: net.ParseIP("abcd:1234:ffff::1"),
|
||||
Routes: []types.Route{
|
||||
{Dst: *routev6},
|
||||
},
|
||||
},
|
||||
DNS: types.DNS{
|
||||
Nameservers: []string{"1.2.3.4", "1::cafe"},
|
||||
Domain: "acompany.com",
|
||||
Search: []string{"somedomain.com", "otherdomain.net"},
|
||||
Options: []string{"foo", "bar"},
|
||||
},
|
||||
}
|
||||
|
||||
// Convert to 040
|
||||
newRes, err := types040.NewResultFromResult(res)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
// Convert back to 0.2.0
|
||||
oldRes, err := newRes.GetAsVersion("0.2.0")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Match JSON so we can figure out what's wrong if something fails the test
|
||||
origJson, err := json.Marshal(res)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
oldJson, err := json.Marshal(oldRes)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(oldJson).To(MatchJSON(origJson))
|
||||
})
|
||||
|
||||
It("correctly marshals and unmarshals interface index 0", func() {
|
||||
ipc := &types040.IPConfig{
|
||||
Version: "4",
|
||||
Interface: types040.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(`{
|
||||
"version": "4",
|
||||
"interface": 0,
|
||||
"address": "10.1.2.3/24"
|
||||
}`))
|
||||
|
||||
recovered := &types040.IPConfig{}
|
||||
Expect(json.Unmarshal(jsonBytes, recovered)).To(Succeed())
|
||||
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() {
|
||||
It("returns an error", func() {
|
||||
recovered := &types040.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 := &types040.IPConfig{
|
||||
Version: "4",
|
||||
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(`{
|
||||
"version": "4",
|
||||
"address": "10.1.2.3/24"
|
||||
}`))
|
||||
})
|
||||
})
|
|
@ -0,0 +1,352 @@
|
|||
// Copyright 2016 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package types100
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
types040 "github.com/containernetworking/cni/pkg/types/040"
|
||||
convert "github.com/containernetworking/cni/pkg/types/internal"
|
||||
)
|
||||
|
||||
// The types did not change between v1.0 and v1.1
|
||||
const ImplementedSpecVersion string = "1.1.0"
|
||||
|
||||
var supportedVersions = []string{"1.0.0", "1.1.0"}
|
||||
|
||||
// Register converters for all versions less than the implemented spec version
|
||||
func init() {
|
||||
// Up-converters
|
||||
convert.RegisterConverter("0.1.0", supportedVersions, convertFrom02x)
|
||||
convert.RegisterConverter("0.2.0", supportedVersions, convertFrom02x)
|
||||
convert.RegisterConverter("0.3.0", supportedVersions, convertFrom04x)
|
||||
convert.RegisterConverter("0.3.1", supportedVersions, convertFrom04x)
|
||||
convert.RegisterConverter("0.4.0", supportedVersions, convertFrom04x)
|
||||
convert.RegisterConverter("1.0.0", []string{"1.1.0"}, convertFrom100)
|
||||
|
||||
// Down-converters
|
||||
convert.RegisterConverter("1.0.0", []string{"0.3.0", "0.3.1", "0.4.0"}, convertTo04x)
|
||||
convert.RegisterConverter("1.0.0", []string{"0.1.0", "0.2.0"}, convertTo02x)
|
||||
convert.RegisterConverter("1.1.0", []string{"0.3.0", "0.3.1", "0.4.0"}, convertTo04x)
|
||||
convert.RegisterConverter("1.1.0", []string{"0.1.0", "0.2.0"}, convertTo02x)
|
||||
convert.RegisterConverter("1.1.0", []string{"1.0.0"}, convertFrom100)
|
||||
|
||||
// Creator
|
||||
convert.RegisterCreator(supportedVersions, NewResult)
|
||||
}
|
||||
|
||||
func NewResult(data []byte) (types.Result, error) {
|
||||
result := &Result{}
|
||||
if err := json.Unmarshal(data, result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, v := range supportedVersions {
|
||||
if result.CNIVersion == v {
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("result type supports %v but unmarshalled CNIVersion is %q",
|
||||
supportedVersions, result.CNIVersion)
|
||||
}
|
||||
|
||||
func GetResult(r types.Result) (*Result, error) {
|
||||
resultCurrent, err := r.GetAsVersion(ImplementedSpecVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result, ok := resultCurrent.(*Result)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to convert result")
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func NewResultFromResult(result types.Result) (*Result, error) {
|
||||
newResult, err := convert.Convert(result, ImplementedSpecVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newResult.(*Result), nil
|
||||
}
|
||||
|
||||
// Result is what gets returned from the plugin (via stdout) to the caller
|
||||
type Result struct {
|
||||
CNIVersion string `json:"cniVersion,omitempty"`
|
||||
Interfaces []*Interface `json:"interfaces,omitempty"`
|
||||
IPs []*IPConfig `json:"ips,omitempty"`
|
||||
Routes []*types.Route `json:"routes,omitempty"`
|
||||
DNS types.DNS `json:"dns,omitempty"`
|
||||
}
|
||||
|
||||
// Note: DNS should be omit if DNS is empty but default Marshal function
|
||||
// will output empty structure hence need to write a Marshal function
|
||||
func (r *Result) MarshalJSON() ([]byte, error) {
|
||||
// use type alias to escape recursion for json.Marshal() to MarshalJSON()
|
||||
type fixObjType = Result
|
||||
|
||||
bytes, err := json.Marshal(fixObjType(*r)) //nolint:all
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fixupObj := make(map[string]interface{})
|
||||
if err := json.Unmarshal(bytes, &fixupObj); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if r.DNS.IsEmpty() {
|
||||
delete(fixupObj, "dns")
|
||||
}
|
||||
|
||||
return json.Marshal(fixupObj)
|
||||
}
|
||||
|
||||
// convertFrom100 does nothing except set the version; the types are the same
|
||||
func convertFrom100(from types.Result, toVersion string) (types.Result, error) {
|
||||
fromResult := from.(*Result)
|
||||
|
||||
result := &Result{
|
||||
CNIVersion: toVersion,
|
||||
Interfaces: fromResult.Interfaces,
|
||||
IPs: fromResult.IPs,
|
||||
Routes: fromResult.Routes,
|
||||
DNS: fromResult.DNS,
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func convertFrom02x(from types.Result, toVersion string) (types.Result, error) {
|
||||
result040, err := convert.Convert(from, "0.4.0")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result100, err := convertFrom04x(result040, toVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result100, nil
|
||||
}
|
||||
|
||||
func convertIPConfigFrom040(from *types040.IPConfig) *IPConfig {
|
||||
to := &IPConfig{
|
||||
Address: from.Address,
|
||||
Gateway: from.Gateway,
|
||||
}
|
||||
if from.Interface != nil {
|
||||
intf := *from.Interface
|
||||
to.Interface = &intf
|
||||
}
|
||||
return to
|
||||
}
|
||||
|
||||
func convertInterfaceFrom040(from *types040.Interface) *Interface {
|
||||
return &Interface{
|
||||
Name: from.Name,
|
||||
Mac: from.Mac,
|
||||
Sandbox: from.Sandbox,
|
||||
}
|
||||
}
|
||||
|
||||
func convertFrom04x(from types.Result, toVersion string) (types.Result, error) {
|
||||
fromResult := from.(*types040.Result)
|
||||
toResult := &Result{
|
||||
CNIVersion: toVersion,
|
||||
DNS: *fromResult.DNS.Copy(),
|
||||
Routes: []*types.Route{},
|
||||
}
|
||||
for _, fromIntf := range fromResult.Interfaces {
|
||||
toResult.Interfaces = append(toResult.Interfaces, convertInterfaceFrom040(fromIntf))
|
||||
}
|
||||
for _, fromIPC := range fromResult.IPs {
|
||||
toResult.IPs = append(toResult.IPs, convertIPConfigFrom040(fromIPC))
|
||||
}
|
||||
for _, fromRoute := range fromResult.Routes {
|
||||
toResult.Routes = append(toResult.Routes, fromRoute.Copy())
|
||||
}
|
||||
return toResult, nil
|
||||
}
|
||||
|
||||
func convertIPConfigTo040(from *IPConfig) *types040.IPConfig {
|
||||
version := "6"
|
||||
if from.Address.IP.To4() != nil {
|
||||
version = "4"
|
||||
}
|
||||
to := &types040.IPConfig{
|
||||
Version: version,
|
||||
Address: from.Address,
|
||||
Gateway: from.Gateway,
|
||||
}
|
||||
if from.Interface != nil {
|
||||
intf := *from.Interface
|
||||
to.Interface = &intf
|
||||
}
|
||||
return to
|
||||
}
|
||||
|
||||
func convertInterfaceTo040(from *Interface) *types040.Interface {
|
||||
return &types040.Interface{
|
||||
Name: from.Name,
|
||||
Mac: from.Mac,
|
||||
Sandbox: from.Sandbox,
|
||||
}
|
||||
}
|
||||
|
||||
func convertTo04x(from types.Result, toVersion string) (types.Result, error) {
|
||||
fromResult := from.(*Result)
|
||||
toResult := &types040.Result{
|
||||
CNIVersion: toVersion,
|
||||
DNS: *fromResult.DNS.Copy(),
|
||||
Routes: []*types.Route{},
|
||||
}
|
||||
for _, fromIntf := range fromResult.Interfaces {
|
||||
toResult.Interfaces = append(toResult.Interfaces, convertInterfaceTo040(fromIntf))
|
||||
}
|
||||
for _, fromIPC := range fromResult.IPs {
|
||||
toResult.IPs = append(toResult.IPs, convertIPConfigTo040(fromIPC))
|
||||
}
|
||||
for _, fromRoute := range fromResult.Routes {
|
||||
toResult.Routes = append(toResult.Routes, fromRoute.Copy())
|
||||
}
|
||||
return toResult, nil
|
||||
}
|
||||
|
||||
func convertTo02x(from types.Result, toVersion string) (types.Result, error) {
|
||||
// First convert to 0.4.0
|
||||
result040, err := convertTo04x(from, "0.4.0")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result02x, err := convert.Convert(result040, toVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result02x, nil
|
||||
}
|
||||
|
||||
func (r *Result) Version() string {
|
||||
return r.CNIVersion
|
||||
}
|
||||
|
||||
func (r *Result) GetAsVersion(version string) (types.Result, error) {
|
||||
// If the creator of the result did not set the CNIVersion, assume it
|
||||
// should be the highest spec version implemented by this Result
|
||||
if r.CNIVersion == "" {
|
||||
r.CNIVersion = ImplementedSpecVersion
|
||||
}
|
||||
return convert.Convert(r, version)
|
||||
}
|
||||
|
||||
func (r *Result) Print() error {
|
||||
return r.PrintTo(os.Stdout)
|
||||
}
|
||||
|
||||
func (r *Result) PrintTo(writer io.Writer) error {
|
||||
data, err := json.MarshalIndent(r, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = writer.Write(data)
|
||||
return err
|
||||
}
|
||||
|
||||
// Interface contains values about the created interfaces
|
||||
type Interface struct {
|
||||
Name string `json:"name"`
|
||||
Mac string `json:"mac,omitempty"`
|
||||
Mtu int `json:"mtu,omitempty"`
|
||||
Sandbox string `json:"sandbox,omitempty"`
|
||||
SocketPath string `json:"socketPath,omitempty"`
|
||||
PciID string `json:"pciID,omitempty"`
|
||||
}
|
||||
|
||||
func (i *Interface) String() string {
|
||||
return fmt.Sprintf("%+v", *i)
|
||||
}
|
||||
|
||||
func (i *Interface) Copy() *Interface {
|
||||
if i == nil {
|
||||
return nil
|
||||
}
|
||||
newIntf := *i
|
||||
return &newIntf
|
||||
}
|
||||
|
||||
// Int returns a pointer to the int value passed in. Used to
|
||||
// set the IPConfig.Interface field.
|
||||
func Int(v int) *int {
|
||||
return &v
|
||||
}
|
||||
|
||||
// IPConfig contains values necessary to configure an IP address on an interface
|
||||
type IPConfig struct {
|
||||
// Index into Result structs Interfaces list
|
||||
Interface *int
|
||||
Address net.IPNet
|
||||
Gateway net.IP
|
||||
}
|
||||
|
||||
func (i *IPConfig) String() string {
|
||||
return fmt.Sprintf("%+v", *i)
|
||||
}
|
||||
|
||||
func (i *IPConfig) Copy() *IPConfig {
|
||||
if i == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ipc := &IPConfig{
|
||||
Address: i.Address,
|
||||
Gateway: i.Gateway,
|
||||
}
|
||||
if i.Interface != nil {
|
||||
intf := *i.Interface
|
||||
ipc.Interface = &intf
|
||||
}
|
||||
return ipc
|
||||
}
|
||||
|
||||
// JSON (un)marshallable types
|
||||
type ipConfig struct {
|
||||
Interface *int `json:"interface,omitempty"`
|
||||
Address types.IPNet `json:"address"`
|
||||
Gateway net.IP `json:"gateway,omitempty"`
|
||||
}
|
||||
|
||||
func (c *IPConfig) MarshalJSON() ([]byte, error) {
|
||||
ipc := ipConfig{
|
||||
Interface: c.Interface,
|
||||
Address: types.IPNet(c.Address),
|
||||
Gateway: c.Gateway,
|
||||
}
|
||||
|
||||
return json.Marshal(ipc)
|
||||
}
|
||||
|
||||
func (c *IPConfig) UnmarshalJSON(data []byte) error {
|
||||
ipc := ipConfig{}
|
||||
if err := json.Unmarshal(data, &ipc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Interface = ipc.Interface
|
||||
c.Address = net.IPNet(ipc.Address)
|
||||
c.Gateway = ipc.Gateway
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
// 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")
|
||||
}
|
|
@ -12,19 +12,19 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package current_test
|
||||
package types100_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/pkg/types/current"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "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 {
|
||||
|
@ -45,26 +45,26 @@ func testResult() *current.Result {
|
|||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(routev6).NotTo(BeNil())
|
||||
Expect(routegwv6).NotTo(BeNil())
|
||||
|
||||
// Set every field of the struct to ensure source compatibility
|
||||
return ¤t.Result{
|
||||
CNIVersion: "0.3.1",
|
||||
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{
|
||||
{
|
||||
Version: "4",
|
||||
Interface: current.Int(0),
|
||||
Address: *ipv4,
|
||||
Gateway: net.ParseIP("1.2.3.1"),
|
||||
},
|
||||
{
|
||||
Version: "6",
|
||||
Interface: current.Int(0),
|
||||
Address: *ipv6,
|
||||
Gateway: net.ParseIP("abcd:1234:ffff::1"),
|
||||
|
@ -84,7 +84,7 @@ func testResult() *current.Result {
|
|||
}
|
||||
|
||||
var _ = Describe("Current types operations", func() {
|
||||
It("correctly encodes a 0.3.x Result", func() {
|
||||
It("correctly encodes a 1.1.0 Result", func() {
|
||||
res := testResult()
|
||||
|
||||
// Redirect stdout to capture JSON result
|
||||
|
@ -98,28 +98,29 @@ var _ = Describe("Current types operations", func() {
|
|||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// parse the result
|
||||
out, err := ioutil.ReadAll(r)
|
||||
out, err := io.ReadAll(r)
|
||||
os.Stdout = oldStdout
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(string(out)).To(MatchJSON(`{
|
||||
"cniVersion": "0.3.1",
|
||||
"cniVersion": "` + current.ImplementedSpecVersion + `",
|
||||
"interfaces": [
|
||||
{
|
||||
"name": "eth0",
|
||||
"mac": "00:11:22:33:44:55",
|
||||
"sandbox": "/proc/3553/ns/net"
|
||||
"mtu": 1500,
|
||||
"sandbox": "/proc/3553/ns/net",
|
||||
"pciID": "8086:9a01",
|
||||
"socketPath": "/path/to/vhost/fd"
|
||||
}
|
||||
],
|
||||
"ips": [
|
||||
{
|
||||
"version": "4",
|
||||
"interface": 0,
|
||||
"address": "1.2.3.30/24",
|
||||
"gateway": "1.2.3.1"
|
||||
},
|
||||
{
|
||||
"version": "6",
|
||||
"interface": 0,
|
||||
"address": "abcd:1234:ffff::cdde/64",
|
||||
"gateway": "abcd:1234:ffff::1"
|
||||
|
@ -153,12 +154,26 @@ var _ = Describe("Current types operations", func() {
|
|||
}`))
|
||||
})
|
||||
|
||||
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())
|
||||
|
||||
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
|
||||
oldStdout := os.Stdout
|
||||
r, w, err := os.Pipe()
|
||||
|
@ -170,12 +185,12 @@ var _ = Describe("Current types operations", func() {
|
|||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// parse the result
|
||||
out, err := ioutil.ReadAll(r)
|
||||
out, err := io.ReadAll(r)
|
||||
os.Stdout = oldStdout
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(string(out)).To(MatchJSON(`{
|
||||
"cniVersion": "0.2.0",
|
||||
"cniVersion": "0.1.0",
|
||||
"ip4": {
|
||||
"ip": "1.2.3.30/24",
|
||||
"gateway": "1.2.3.1",
|
||||
|
@ -214,9 +229,78 @@ var _ = Describe("Current types operations", func() {
|
|||
}`))
|
||||
})
|
||||
|
||||
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 := ¤t.IPConfig{
|
||||
Version: "4",
|
||||
Interface: current.Int(0),
|
||||
Address: net.IPNet{
|
||||
IP: net.ParseIP("10.1.2.3"),
|
||||
|
@ -227,27 +311,25 @@ var _ = Describe("Current types operations", func() {
|
|||
jsonBytes, err := json.Marshal(ipc)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(jsonBytes).To(MatchJSON(`{
|
||||
"version": "4",
|
||||
"interface": 0,
|
||||
"address": "10.1.2.3/24"
|
||||
}`))
|
||||
|
||||
recovered := ¤t.IPConfig{}
|
||||
Expect(json.Unmarshal(jsonBytes, &recovered)).To(Succeed())
|
||||
Expect(json.Unmarshal(jsonBytes, recovered)).To(Succeed())
|
||||
Expect(recovered).To(Equal(ipc))
|
||||
})
|
||||
|
||||
Context("when unmarshaling json fails", func() {
|
||||
Context("when unmarshalling json fails", func() {
|
||||
It("returns an error", func() {
|
||||
recovered := ¤t.IPConfig{}
|
||||
err := json.Unmarshal([]byte(`{"address": 5}`), &recovered)
|
||||
err := json.Unmarshal([]byte(`{"address": 5}`), recovered)
|
||||
Expect(err).To(MatchError(HavePrefix("json: cannot unmarshal")))
|
||||
})
|
||||
})
|
||||
|
||||
It("correctly marshals a missing interface index", func() {
|
||||
ipc := ¤t.IPConfig{
|
||||
Version: "4",
|
||||
Address: net.IPNet{
|
||||
IP: net.ParseIP("10.1.2.3"),
|
||||
Mask: net.IPv4Mask(255, 255, 255, 0),
|
||||
|
@ -257,7 +339,6 @@ var _ = Describe("Current types operations", func() {
|
|||
json, err := json.Marshal(ipc)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(json).To(MatchJSON(`{
|
||||
"version": "4",
|
||||
"address": "10.1.2.3/24"
|
||||
}`))
|
||||
})
|
|
@ -26,8 +26,8 @@ import (
|
|||
type UnmarshallableBool bool
|
||||
|
||||
// UnmarshalText implements the encoding.TextUnmarshaler interface.
|
||||
// Returns boolean true if the string is "1" or "[Tt]rue"
|
||||
// Returns boolean false if the string is "0" or "[Ff]alse"
|
||||
// Returns boolean true if the string is "1" or "true" or "True"
|
||||
// Returns boolean false if the string is "0" or "false" or "False”
|
||||
func (b *UnmarshallableBool) UnmarshalText(data []byte) error {
|
||||
s := strings.ToLower(string(data))
|
||||
switch s {
|
||||
|
@ -36,7 +36,7 @@ func (b *UnmarshallableBool) UnmarshalText(data []byte) error {
|
|||
case "0", "false":
|
||||
*b = false
|
||||
default:
|
||||
return fmt.Errorf("Boolean unmarshal error: invalid input %s", s)
|
||||
return fmt.Errorf("boolean unmarshal error: invalid input %s", s)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -91,16 +91,26 @@ func LoadArgs(args string, container interface{}) error {
|
|||
unknownArgs = append(unknownArgs, pair)
|
||||
continue
|
||||
}
|
||||
keyFieldIface := keyField.Addr().Interface()
|
||||
u, ok := keyFieldIface.(encoding.TextUnmarshaler)
|
||||
|
||||
var keyFieldInterface interface{}
|
||||
switch {
|
||||
case keyField.Kind() == reflect.Ptr:
|
||||
keyField.Set(reflect.New(keyField.Type().Elem()))
|
||||
keyFieldInterface = keyField.Interface()
|
||||
case keyField.CanAddr() && keyField.Addr().CanInterface():
|
||||
keyFieldInterface = keyField.Addr().Interface()
|
||||
default:
|
||||
return UnmarshalableArgsError{fmt.Errorf("field '%s' has no valid interface", keyString)}
|
||||
}
|
||||
u, ok := keyFieldInterface.(encoding.TextUnmarshaler)
|
||||
if !ok {
|
||||
return UnmarshalableArgsError{fmt.Errorf(
|
||||
"ARGS: cannot unmarshal into field '%s' - type '%s' does not implement encoding.TextUnmarshaler",
|
||||
keyString, reflect.TypeOf(keyFieldIface))}
|
||||
keyString, reflect.TypeOf(keyFieldInterface))}
|
||||
}
|
||||
err := u.UnmarshalText([]byte(valueString))
|
||||
if err != nil {
|
||||
return fmt.Errorf("ARGS: error parsing value of pair %q: %v)", pair, err)
|
||||
return fmt.Errorf("ARGS: error parsing value of pair %q: %w", pair, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -15,13 +15,13 @@
|
|||
package types_test
|
||||
|
||||
import (
|
||||
"net"
|
||||
"reflect"
|
||||
|
||||
. "github.com/containernetworking/cni/pkg/types"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/ginkgo/extensions/table"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
. "github.com/containernetworking/cni/pkg/types"
|
||||
)
|
||||
|
||||
var _ = Describe("UnmarshallableBool UnmarshalText", func() {
|
||||
|
@ -126,7 +126,37 @@ var _ = Describe("LoadArgs", func() {
|
|||
}{}
|
||||
err := LoadArgs("IP=10.0.0.0/24", &conf)
|
||||
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())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
// Copyright 2016 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package create
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
_ "github.com/containernetworking/cni/pkg/types/020"
|
||||
_ "github.com/containernetworking/cni/pkg/types/040"
|
||||
_ "github.com/containernetworking/cni/pkg/types/100"
|
||||
convert "github.com/containernetworking/cni/pkg/types/internal"
|
||||
)
|
||||
|
||||
// DecodeVersion returns the CNI version from CNI configuration or result JSON,
|
||||
// or an error if the operation could not be performed.
|
||||
func DecodeVersion(jsonBytes []byte) (string, error) {
|
||||
var conf struct {
|
||||
CNIVersion string `json:"cniVersion"`
|
||||
}
|
||||
err := json.Unmarshal(jsonBytes, &conf)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("decoding version from network config: %w", err)
|
||||
}
|
||||
if conf.CNIVersion == "" {
|
||||
return "0.1.0", nil
|
||||
}
|
||||
return conf.CNIVersion, nil
|
||||
}
|
||||
|
||||
// Create creates a CNI Result using the given JSON with the expected
|
||||
// version, or an error if the creation could not be performed
|
||||
func Create(version string, bytes []byte) (types.Result, error) {
|
||||
return convert.Create(version, bytes)
|
||||
}
|
||||
|
||||
// CreateFromBytes creates a CNI Result from the given JSON, automatically
|
||||
// detecting the CNI spec version of the result. An error is returned if the
|
||||
// operation could not be performed.
|
||||
func CreateFromBytes(bytes []byte) (types.Result, error) {
|
||||
version, err := DecodeVersion(bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convert.Create(version, bytes)
|
||||
}
|
|
@ -1,300 +0,0 @@
|
|||
// Copyright 2016 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package current
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"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 {
|
||||
gw := route.GW
|
||||
if gw == nil {
|
||||
gw = oldResult.IP4.Gateway
|
||||
}
|
||||
newResult.Routes = append(newResult.Routes, &types.Route{
|
||||
Dst: route.Dst,
|
||||
GW: 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 {
|
||||
gw := route.GW
|
||||
if gw == nil {
|
||||
gw = oldResult.IP6.Gateway
|
||||
}
|
||||
newResult.Routes = append(newResult.Routes, &types.Route{
|
||||
Dst: route.Dst,
|
||||
GW: gw,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(newResult.IPs) == 0 {
|
||||
return nil, fmt.Errorf("cannot convert: no valid IP addresses")
|
||||
}
|
||||
|
||||
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 {
|
||||
data, err := json.MarshalIndent(r, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = os.Stdout.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
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
// Copyright 2016 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package convert
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
)
|
||||
|
||||
// ConvertFn should convert from the given arbitrary Result type into a
|
||||
// Result implementing CNI specification version passed in toVersion.
|
||||
// The function is guaranteed to be passed a Result type matching the
|
||||
// fromVersion it was registered with, and is guaranteed to be
|
||||
// passed a toVersion matching one of the toVersions it was registered with.
|
||||
type ConvertFn func(from types.Result, toVersion string) (types.Result, error)
|
||||
|
||||
type converter struct {
|
||||
// fromVersion is the CNI Result spec version that convertFn accepts
|
||||
fromVersion string
|
||||
// toVersions is a list of versions that convertFn can convert to
|
||||
toVersions []string
|
||||
convertFn ConvertFn
|
||||
}
|
||||
|
||||
var converters []*converter
|
||||
|
||||
func findConverter(fromVersion, toVersion string) *converter {
|
||||
for _, c := range converters {
|
||||
if c.fromVersion == fromVersion {
|
||||
for _, v := range c.toVersions {
|
||||
if v == toVersion {
|
||||
return c
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert converts a CNI Result to the requested CNI specification version,
|
||||
// or returns an error if the conversion could not be performed or failed
|
||||
func Convert(from types.Result, toVersion string) (types.Result, error) {
|
||||
if toVersion == "" {
|
||||
toVersion = "0.1.0"
|
||||
}
|
||||
|
||||
fromVersion := from.Version()
|
||||
|
||||
// Shortcut for same version
|
||||
if fromVersion == toVersion {
|
||||
return from, nil
|
||||
}
|
||||
|
||||
// Otherwise find the right converter
|
||||
c := findConverter(fromVersion, toVersion)
|
||||
if c == nil {
|
||||
return nil, fmt.Errorf("no converter for CNI result version %s to %s",
|
||||
fromVersion, toVersion)
|
||||
}
|
||||
return c.convertFn(from, toVersion)
|
||||
}
|
||||
|
||||
// RegisterConverter registers a CNI Result converter. SHOULD NOT BE CALLED
|
||||
// EXCEPT FROM CNI ITSELF.
|
||||
func RegisterConverter(fromVersion string, toVersions []string, convertFn ConvertFn) {
|
||||
// Make sure there is no converter already registered for these
|
||||
// from and to versions
|
||||
for _, v := range toVersions {
|
||||
if findConverter(fromVersion, v) != nil {
|
||||
panic(fmt.Sprintf("converter already registered for %s to %s",
|
||||
fromVersion, v))
|
||||
}
|
||||
}
|
||||
converters = append(converters, &converter{
|
||||
fromVersion: fromVersion,
|
||||
toVersions: toVersions,
|
||||
convertFn: convertFn,
|
||||
})
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
// Copyright 2016 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package convert
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
)
|
||||
|
||||
type ResultFactoryFunc func([]byte) (types.Result, error)
|
||||
|
||||
type creator struct {
|
||||
// CNI Result spec versions that createFn can create a Result for
|
||||
versions []string
|
||||
createFn ResultFactoryFunc
|
||||
}
|
||||
|
||||
var creators []*creator
|
||||
|
||||
func findCreator(version string) *creator {
|
||||
for _, c := range creators {
|
||||
for _, v := range c.versions {
|
||||
if v == version {
|
||||
return c
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create creates a CNI Result using the given JSON, or an error if the creation
|
||||
// could not be performed
|
||||
func Create(version string, bytes []byte) (types.Result, error) {
|
||||
if c := findCreator(version); c != nil {
|
||||
return c.createFn(bytes)
|
||||
}
|
||||
return nil, fmt.Errorf("unsupported CNI result version %q", version)
|
||||
}
|
||||
|
||||
// RegisterCreator registers a CNI Result creator. SHOULD NOT BE CALLED
|
||||
// EXCEPT FROM CNI ITSELF.
|
||||
func RegisterCreator(versions []string, createFn ResultFactoryFunc) {
|
||||
// Make sure there is no creator already registered for these versions
|
||||
for _, v := range versions {
|
||||
if findCreator(v) != nil {
|
||||
panic(fmt.Sprintf("creator already registered for %s", v))
|
||||
}
|
||||
}
|
||||
creators = append(creators, &creator{
|
||||
versions: versions,
|
||||
createFn: createFn,
|
||||
})
|
||||
}
|
|
@ -16,8 +16,8 @@ package types
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
)
|
||||
|
@ -56,34 +56,77 @@ func (n *IPNet) UnmarshalJSON(data []byte) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// NetConf describes a network.
|
||||
type NetConf struct {
|
||||
// Use PluginConf instead of NetConf, the NetConf
|
||||
// backwards-compat alias will be removed in a future release.
|
||||
type NetConf = PluginConf
|
||||
|
||||
// PluginConf describes a plugin configuration for a specific network.
|
||||
type PluginConf struct {
|
||||
CNIVersion string `json:"cniVersion,omitempty"`
|
||||
|
||||
Name string `json:"name,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Capabilities map[string]bool `json:"capabilities,omitempty"`
|
||||
IPAM IPAM `json:"ipam,omitempty"`
|
||||
DNS DNS `json:"dns"`
|
||||
DNS DNS `json:"dns,omitempty"`
|
||||
|
||||
RawPrevResult map[string]interface{} `json:"prevResult,omitempty"`
|
||||
PrevResult Result `json:"-"`
|
||||
|
||||
// ValidAttachments is only supplied when executing a GC operation
|
||||
ValidAttachments []GCAttachment `json:"cni.dev/valid-attachments,omitempty"`
|
||||
}
|
||||
|
||||
// GCAttachment is the parameters to a GC call -- namely,
|
||||
// the container ID and ifname pair that represents a
|
||||
// still-valid attachment.
|
||||
type GCAttachment struct {
|
||||
ContainerID string `json:"containerID"`
|
||||
IfName string `json:"ifname"`
|
||||
}
|
||||
|
||||
// Note: DNS should be omit if DNS is empty but default Marshal function
|
||||
// will output empty structure hence need to write a Marshal function
|
||||
func (n *PluginConf) MarshalJSON() ([]byte, error) {
|
||||
bytes, err := json.Marshal(*n)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fixupObj := make(map[string]interface{})
|
||||
if err := json.Unmarshal(bytes, &fixupObj); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if n.DNS.IsEmpty() {
|
||||
delete(fixupObj, "dns")
|
||||
}
|
||||
|
||||
return json.Marshal(fixupObj)
|
||||
}
|
||||
|
||||
type IPAM struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
// IsEmpty returns true if IPAM structure has no value, otherwise return false
|
||||
func (i *IPAM) IsEmpty() bool {
|
||||
return i.Type == ""
|
||||
}
|
||||
|
||||
// NetConfList describes an ordered list of networks.
|
||||
type NetConfList struct {
|
||||
CNIVersion string `json:"cniVersion,omitempty"`
|
||||
|
||||
Name string `json:"name,omitempty"`
|
||||
Plugins []*NetConf `json:"plugins,omitempty"`
|
||||
DisableCheck bool `json:"disableCheck,omitempty"`
|
||||
DisableGC bool `json:"disableGC,omitempty"`
|
||||
Plugins []*PluginConf `json:"plugins,omitempty"`
|
||||
}
|
||||
|
||||
type ResultFactoryFunc func([]byte) (Result, error)
|
||||
|
||||
// Result is an interface that provides the result of plugin execution
|
||||
type Result interface {
|
||||
// The highest CNI specification result verison the result supports
|
||||
// The highest CNI specification result version the result supports
|
||||
// without having to convert
|
||||
Version() string
|
||||
|
||||
|
@ -94,8 +137,8 @@ type Result interface {
|
|||
// Prints the result in JSON format to stdout
|
||||
Print() error
|
||||
|
||||
// Returns a JSON string representation of the result
|
||||
String() string
|
||||
// Prints the result in JSON format to provided writer
|
||||
PrintTo(writer io.Writer) error
|
||||
}
|
||||
|
||||
func PrintResult(result Result, version string) error {
|
||||
|
@ -114,21 +157,93 @@ type DNS struct {
|
|||
Options []string `json:"options,omitempty"`
|
||||
}
|
||||
|
||||
// IsEmpty returns true if DNS structure has no value, otherwise return false
|
||||
func (d *DNS) IsEmpty() bool {
|
||||
if len(d.Nameservers) == 0 && d.Domain == "" && len(d.Search) == 0 && len(d.Options) == 0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (d *DNS) Copy() *DNS {
|
||||
if d == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
to := &DNS{Domain: d.Domain}
|
||||
to.Nameservers = append(to.Nameservers, d.Nameservers...)
|
||||
to.Search = append(to.Search, d.Search...)
|
||||
to.Options = append(to.Options, d.Options...)
|
||||
return to
|
||||
}
|
||||
|
||||
type Route struct {
|
||||
Dst net.IPNet
|
||||
GW net.IP
|
||||
MTU int
|
||||
AdvMSS int
|
||||
Priority int
|
||||
Table *int
|
||||
Scope *int
|
||||
}
|
||||
|
||||
func (r *Route) String() string {
|
||||
return fmt.Sprintf("%+v", *r)
|
||||
table := "<nil>"
|
||||
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
|
||||
// see https://github.com/containernetworking/cni/blob/master/SPEC.md#well-known-error-codes
|
||||
// see https://github.com/containernetworking/cni/blob/main/SPEC.md#error
|
||||
const (
|
||||
ErrUnknown uint = iota // 0
|
||||
ErrIncompatibleCNIVersion // 1
|
||||
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 {
|
||||
|
@ -137,6 +252,14 @@ type Error struct {
|
|||
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 {
|
||||
details := ""
|
||||
if e.Details != "" {
|
||||
|
@ -156,6 +279,11 @@ func (e *Error) Print() error {
|
|||
type route struct {
|
||||
Dst IPNet `json:"dst"`
|
||||
GW net.IP `json:"gw,omitempty"`
|
||||
MTU int `json:"mtu,omitempty"`
|
||||
AdvMSS int `json:"advmss,omitempty"`
|
||||
Priority int `json:"priority,omitempty"`
|
||||
Table *int `json:"table,omitempty"`
|
||||
Scope *int `json:"scope,omitempty"`
|
||||
}
|
||||
|
||||
func (r *Route) UnmarshalJSON(data []byte) error {
|
||||
|
@ -166,6 +294,12 @@ func (r *Route) UnmarshalJSON(data []byte) error {
|
|||
|
||||
r.Dst = net.IPNet(rt.Dst)
|
||||
r.GW = rt.GW
|
||||
r.MTU = rt.MTU
|
||||
r.AdvMSS = rt.AdvMSS
|
||||
r.Priority = rt.Priority
|
||||
r.Table = rt.Table
|
||||
r.Scope = rt.Scope
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -173,6 +307,11 @@ func (r Route) MarshalJSON() ([]byte, error) {
|
|||
rt := route{
|
||||
Dst: IPNet(r.Dst),
|
||||
GW: r.GW,
|
||||
MTU: r.MTU,
|
||||
AdvMSS: r.AdvMSS,
|
||||
Priority: r.Priority,
|
||||
Table: r.Table,
|
||||
Scope: r.Scope,
|
||||
}
|
||||
|
||||
return json.Marshal(rt)
|
||||
|
@ -186,6 +325,3 @@ func prettyPrint(obj interface{}) error {
|
|||
_, err = os.Stdout.Write(data)
|
||||
return err
|
||||
}
|
||||
|
||||
// NotImplementedError is used to indicate that a method is not implemented for the given platform
|
||||
var NotImplementedError = errors.New("Not Implemented")
|
||||
|
|
|
@ -15,10 +15,10 @@
|
|||
package types_test
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestTypes(t *testing.T) {
|
||||
|
|
|
@ -18,14 +18,15 @@ import (
|
|||
"encoding/json"
|
||||
"net"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/ginkgo/extensions/table"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
types040 "github.com/containernetworking/cni/pkg/types/040"
|
||||
current "github.com/containernetworking/cni/pkg/types/100"
|
||||
)
|
||||
|
||||
var _ = Describe("Types", func() {
|
||||
|
||||
Describe("ParseCIDR", func() {
|
||||
DescribeTable("Parse and stringify",
|
||||
func(input, expectedIP string, expectedMask int) {
|
||||
|
@ -88,13 +89,18 @@ var _ = Describe("Types", func() {
|
|||
Mask: net.CIDRMask(24, 32),
|
||||
},
|
||||
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() {
|
||||
jsonBytes, err := json.Marshal(example)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(jsonBytes).To(MatchJSON(`{ "dst": "1.2.3.0/24", "gw": "1.2.3.1" }`))
|
||||
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 }`))
|
||||
|
||||
var unmarshaled types.Route
|
||||
Expect(json.Unmarshal(jsonBytes, &unmarshaled)).To(Succeed())
|
||||
|
@ -110,7 +116,7 @@ var _ = Describe("Types", 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}`))
|
||||
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}`))
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -137,5 +143,59 @@ 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 = ¤t.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"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
// 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
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,23 +15,12 @@
|
|||
package version
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/containernetworking/cni/pkg/types/create"
|
||||
)
|
||||
|
||||
// ConfigDecoder can decode the CNI version available in network config data
|
||||
type ConfigDecoder struct{}
|
||||
|
||||
func (*ConfigDecoder) Decode(jsonBytes []byte) (string, error) {
|
||||
var conf struct {
|
||||
CNIVersion string `json:"cniVersion"`
|
||||
}
|
||||
err := json.Unmarshal(jsonBytes, &conf)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("decoding version from network config: %s", err)
|
||||
}
|
||||
if conf.CNIVersion == "" {
|
||||
return "0.1.0", nil
|
||||
}
|
||||
return conf.CNIVersion, nil
|
||||
return create.DecodeVersion(jsonBytes)
|
||||
}
|
||||
|
|
|
@ -15,10 +15,10 @@
|
|||
package version_test
|
||||
|
||||
import (
|
||||
"github.com/containernetworking/cni/pkg/version"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/version"
|
||||
)
|
||||
|
||||
var _ = Describe("Decoding the version of network config", func() {
|
||||
|
@ -32,7 +32,7 @@ var _ = Describe("Decoding the version of network config", func() {
|
|||
configBytes = []byte(`{ "cniVersion": "4.3.2" }`)
|
||||
})
|
||||
|
||||
Context("when the version is explict", func() {
|
||||
Context("when the version is explicit", func() {
|
||||
It("returns the version", func() {
|
||||
version, err := decoder.Decode(configBytes)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
|
|
@ -16,7 +16,6 @@ package legacy_examples
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
noop_debug "github.com/containernetworking/cni/plugins/test/noop/debug"
|
||||
|
@ -108,9 +107,9 @@ func (e *ExampleRuntime) GenerateNetConf(name string) (*ExampleNetConf, error) {
|
|||
return nil, fmt.Errorf("unknown example net config template %q", name)
|
||||
}
|
||||
|
||||
debugFile, err := ioutil.TempFile("", "cni_debug")
|
||||
debugFile, err := os.CreateTemp("", "cni_debug")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create noop plugin debug file: %v", err)
|
||||
return nil, fmt.Errorf("failed to create noop plugin debug file: %w", err)
|
||||
}
|
||||
debugFilePath := debugFile.Name()
|
||||
|
||||
|
@ -119,7 +118,7 @@ func (e *ExampleRuntime) GenerateNetConf(name string) (*ExampleNetConf, error) {
|
|||
}
|
||||
if err := debug.WriteDebug(debugFilePath); err != nil {
|
||||
os.Remove(debugFilePath)
|
||||
return nil, fmt.Errorf("failed to write noop plugin debug file %q: %v", debugFilePath, err)
|
||||
return nil, fmt.Errorf("failed to write noop plugin debug file %q: %w", debugFilePath, err)
|
||||
}
|
||||
conf := &ExampleNetConf{
|
||||
Config: fmt.Sprintf(template.conf, debugFilePath),
|
||||
|
@ -139,17 +138,17 @@ func (c *ExampleNetConf) Cleanup() {
|
|||
}
|
||||
|
||||
// V010_Runtime creates a simple noop network configuration, then
|
||||
// executes libcni against the the noop test plugin.
|
||||
// executes libcni against the noop test plugin.
|
||||
var V010_Runtime = ExampleRuntime{
|
||||
NetConfs: []string{"unversioned", "0.1.0"},
|
||||
Example: Example{
|
||||
Name: "example_invoker_v010",
|
||||
CNIRepoGitRef: "c0d34c69", //version with ns.Do
|
||||
CNIRepoGitRef: "c0d34c69", // version with ns.Do
|
||||
PluginSource: `package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/containernetworking/cni/libcni"
|
||||
|
@ -161,7 +160,7 @@ func main(){
|
|||
}
|
||||
|
||||
func exec() int {
|
||||
confBytes, err := ioutil.ReadAll(os.Stdin)
|
||||
confBytes, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
fmt.Printf("could not read netconfig from stdin: %+v", err)
|
||||
return 1
|
||||
|
|
|
@ -17,8 +17,8 @@
|
|||
package legacy_examples
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
@ -39,8 +39,10 @@ type Example struct {
|
|||
PluginSource string
|
||||
}
|
||||
|
||||
var buildDir = ""
|
||||
var buildDirLock sync.Mutex
|
||||
var (
|
||||
buildDir = ""
|
||||
buildDirLock sync.Mutex
|
||||
)
|
||||
|
||||
func ensureBuildDirExists() error {
|
||||
buildDirLock.Lock()
|
||||
|
@ -51,7 +53,7 @@ func ensureBuildDirExists() error {
|
|||
}
|
||||
|
||||
var err error
|
||||
buildDir, err = ioutil.TempDir("", "cni-example-plugins")
|
||||
buildDir, err = os.MkdirTemp("", "cni-example-plugins")
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -120,6 +122,7 @@ func main() { skel.PluginMain(c, c) }
|
|||
// As we change the CNI spec, the Result type and this value may change.
|
||||
// The text of the example plugins should not.
|
||||
var ExpectedResult = &types020.Result{
|
||||
CNIVersion: "0.1.0",
|
||||
IP4: &types020.IPConfig{
|
||||
IP: net.IPNet{
|
||||
IP: net.ParseIP("10.1.2.3"),
|
||||
|
@ -127,7 +130,7 @@ var ExpectedResult = &types020.Result{
|
|||
},
|
||||
Gateway: net.ParseIP("10.1.2.1"),
|
||||
Routes: []types.Route{
|
||||
types.Route{
|
||||
{
|
||||
Dst: net.IPNet{
|
||||
IP: net.ParseIP("0.0.0.0"),
|
||||
Mask: net.CIDRMask(0, 32),
|
||||
|
|
|
@ -15,10 +15,10 @@
|
|||
package legacy_examples_test
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestLegacyExamples(t *testing.T) {
|
||||
|
|
|
@ -19,9 +19,10 @@ import (
|
|||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/version/legacy_examples"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/version/legacy_examples"
|
||||
)
|
||||
|
||||
var _ = Describe("The v0.1.0 Example", func() {
|
||||
|
|
|
@ -68,7 +68,7 @@ func (*PluginDecoder) Decode(jsonBytes []byte) (PluginInfo, error) {
|
|||
var info pluginInfo
|
||||
err := json.Unmarshal(jsonBytes, &info)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decoding version info: %s", err)
|
||||
return nil, fmt.Errorf("decoding version info: %w", err)
|
||||
}
|
||||
if info.CNIVersion_ == "" {
|
||||
return nil, fmt.Errorf("decoding version info: missing field cniVersion")
|
||||
|
@ -86,27 +86,31 @@ func (*PluginDecoder) Decode(jsonBytes []byte) (PluginInfo, error) {
|
|||
// minor, and micro numbers or returns an error
|
||||
func ParseVersion(version string) (int, int, int, error) {
|
||||
var major, minor, micro int
|
||||
if version == "" { // special case: no version declared == v0.1.0
|
||||
return 0, 1, 0, nil
|
||||
}
|
||||
|
||||
parts := strings.Split(version, ".")
|
||||
if len(parts) == 0 || len(parts) >= 4 {
|
||||
return -1, -1, -1, fmt.Errorf("invalid version %q: too many or too few parts", version)
|
||||
if len(parts) >= 4 {
|
||||
return -1, -1, -1, fmt.Errorf("invalid version %q: too many parts", version)
|
||||
}
|
||||
|
||||
major, err := strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
return -1, -1, -1, fmt.Errorf("failed to convert major version part %q: %v", parts[0], err)
|
||||
return -1, -1, -1, fmt.Errorf("failed to convert major version part %q: %w", parts[0], err)
|
||||
}
|
||||
|
||||
if len(parts) >= 2 {
|
||||
minor, err = strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return -1, -1, -1, fmt.Errorf("failed to convert minor version part %q: %v", parts[1], err)
|
||||
return -1, -1, -1, fmt.Errorf("failed to convert minor version part %q: %w", parts[1], err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(parts) >= 3 {
|
||||
micro, err = strconv.Atoi(parts[2])
|
||||
if err != nil {
|
||||
return -1, -1, -1, fmt.Errorf("failed to convert micro version part %q: %v", parts[2], err)
|
||||
return -1, -1, -1, fmt.Errorf("failed to convert micro version part %q: %w", parts[2], err)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -114,7 +118,7 @@ func ParseVersion(version string) (int, int, int, error) {
|
|||
}
|
||||
|
||||
// GreaterThanOrEqualTo takes two string versions, parses them into major/minor/micro
|
||||
// nubmers, and compares them to determine whether the first version is greater
|
||||
// numbers, and compares them to determine whether the first version is greater
|
||||
// than or equal to the second
|
||||
func GreaterThanOrEqualTo(version, otherVersion string) (bool, error) {
|
||||
firstMajor, firstMinor, firstMicro, err := ParseVersion(version)
|
||||
|
@ -138,3 +142,27 @@ func GreaterThanOrEqualTo(version, otherVersion string) (bool, error) {
|
|||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// GreaterThan returns true if the first version is greater than the second
|
||||
func GreaterThan(version, otherVersion string) (bool, error) {
|
||||
firstMajor, firstMinor, firstMicro, err := ParseVersion(version)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
secondMajor, secondMinor, secondMicro, err := ParseVersion(otherVersion)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if firstMajor > secondMajor {
|
||||
return true, nil
|
||||
} else if firstMajor == secondMajor {
|
||||
if firstMinor > secondMinor {
|
||||
return true, nil
|
||||
} else if firstMinor == secondMinor && firstMicro > secondMicro {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
|
|
@ -15,9 +15,10 @@
|
|||
package version_test
|
||||
|
||||
import (
|
||||
"github.com/containernetworking/cni/pkg/version"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/version"
|
||||
)
|
||||
|
||||
var _ = Describe("Decoding versions reported by a plugin", func() {
|
||||
|
@ -91,6 +92,14 @@ var _ = Describe("Decoding versions reported by a plugin", func() {
|
|||
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() {
|
||||
badVersions := []string{"asdfasdf", "asdf.", ".asdfas", "asdf.adsf.", "0.", "..", "1.2.3.4.5"}
|
||||
for _, v := range badVersions {
|
||||
|
@ -112,19 +121,19 @@ var _ = Describe("Decoding versions reported by a plugin", func() {
|
|||
// Make sure the first is greater than the second
|
||||
gt, err := version.GreaterThanOrEqualTo(v[0], v[1])
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(gt).To(Equal(true))
|
||||
Expect(gt).To(BeTrue())
|
||||
|
||||
// And the opposite
|
||||
gt, err = version.GreaterThanOrEqualTo(v[1], v[0])
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(gt).To(Equal(false))
|
||||
Expect(gt).To(BeFalse())
|
||||
}
|
||||
})
|
||||
|
||||
It("returns true when versions are the same", func() {
|
||||
gt, err := version.GreaterThanOrEqualTo("1.2.3", "1.2.3")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(gt).To(Equal(true))
|
||||
Expect(gt).To(BeTrue())
|
||||
})
|
||||
|
||||
It("returns an error for malformed versions", func() {
|
||||
|
|
|
@ -15,9 +15,10 @@
|
|||
package version_test
|
||||
|
||||
import (
|
||||
"github.com/containernetworking/cni/pkg/version"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/version"
|
||||
)
|
||||
|
||||
var _ = Describe("Reconcile versions of net config with versions supported by plugins", func() {
|
||||
|
|
|
@ -22,17 +22,12 @@ package testhelpers
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const packageBaseName = "github.com/containernetworking/cni"
|
||||
|
||||
func run(cmd *exec.Cmd) error {
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
|
@ -42,115 +37,75 @@ func run(cmd *exec.Cmd) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func goBuildEnviron(gopath string) []string {
|
||||
environ := os.Environ()
|
||||
for i, kvp := range environ {
|
||||
if strings.HasPrefix(kvp, "GOPATH=") {
|
||||
environ[i] = "GOPATH=" + gopath
|
||||
return environ
|
||||
// unset GOPATH if it's set, so we use modules
|
||||
func goBuildEnviron() []string {
|
||||
out := []string{}
|
||||
for _, kvp := range os.Environ() {
|
||||
if !strings.HasPrefix(kvp, "GOPATH=") {
|
||||
out = append(out, kvp)
|
||||
}
|
||||
}
|
||||
environ = append(environ, "GOPATH="+gopath)
|
||||
return environ
|
||||
return out
|
||||
}
|
||||
|
||||
func buildGoProgram(gopath, packageName, outputFilePath string) error {
|
||||
cmd := exec.Command("go", "build", "-o", outputFilePath, packageName)
|
||||
cmd.Env = goBuildEnviron(gopath)
|
||||
func buildGoProgram(modPath, outputFilePath string) error {
|
||||
cmd := exec.Command("go", "build", "-o", outputFilePath, ".")
|
||||
cmd.Dir = modPath
|
||||
cmd.Env = goBuildEnviron()
|
||||
return run(cmd)
|
||||
}
|
||||
|
||||
func createSingleFilePackage(gopath, packageName string, fileContents []byte) error {
|
||||
dirName := filepath.Join(gopath, "src", packageName)
|
||||
err := os.MkdirAll(dirName, 0700)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ioutil.WriteFile(filepath.Join(dirName, "main.go"), fileContents, 0600)
|
||||
func modInit(path, name string) error {
|
||||
cmd := exec.Command("go", "mod", "init", name)
|
||||
cmd.Dir = path
|
||||
return run(cmd)
|
||||
}
|
||||
|
||||
func removePackage(gopath, packageName string) error {
|
||||
dirName := filepath.Join(gopath, "src", packageName)
|
||||
return os.RemoveAll(dirName)
|
||||
// addLibcni will execute `go mod edit -replace` to fix libcni at a specified version
|
||||
func addLibcni(path, gitRef string) error {
|
||||
cmd := exec.Command("go", "mod", "edit", "-replace=github.com/containernetworking/cni=github.com/containernetworking/cni@"+gitRef)
|
||||
cmd.Dir = path
|
||||
return run(cmd)
|
||||
}
|
||||
|
||||
func isRepoRoot(path string) bool {
|
||||
_, err := ioutil.ReadDir(filepath.Join(path, ".git"))
|
||||
return (err == nil) && (filepath.Base(path) == "cni")
|
||||
}
|
||||
|
||||
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))
|
||||
// modTidy will execute `go mod tidy` to ensure all necessary dependencies
|
||||
func modTidy(path string) error {
|
||||
cmd := exec.Command("go", "mod", "tidy")
|
||||
cmd.Dir = path
|
||||
return run(cmd)
|
||||
}
|
||||
|
||||
// BuildAt builds the go programSource using the version of the CNI library
|
||||
// at gitRef, and saves the resulting binary file at outputFilePath
|
||||
func BuildAt(programSource []byte, gitRef string, outputFilePath string) error {
|
||||
tempGoPath, err := ioutil.TempDir("", "cni-git-")
|
||||
tempDir, err := os.MkdirTemp(os.Getenv("GOTMPDIR"), "cni-test-")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(tempGoPath)
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
cloneDestination := filepath.Join(tempGoPath, "src", packageBaseName)
|
||||
err = gitCloneThisRepo(cloneDestination)
|
||||
if err != nil {
|
||||
modName := filepath.Base(tempDir)
|
||||
|
||||
if err := modInit(tempDir, modName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = gitCheckout(cloneDestination, gitRef)
|
||||
if err != nil {
|
||||
if err := addLibcni(tempDir, gitRef); 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 {
|
||||
if err := os.WriteFile(filepath.Join(tempDir, "main.go"), programSource, 0o600); err != nil {
|
||||
return err
|
||||
}
|
||||
defer removePackage(tempGoPath, testPackageName)
|
||||
|
||||
err = buildGoProgram(tempGoPath, testPackageName, outputFilePath)
|
||||
if err != nil {
|
||||
if err := modTidy(tempDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = buildGoProgram(tempDir, outputFilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to build: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -15,10 +15,10 @@
|
|||
package testhelpers_test
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestTesthelpers(t *testing.T) {
|
||||
|
|
|
@ -11,17 +11,15 @@
|
|||
// 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 testhelpers_test
|
||||
package testhelpers
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/version/testhelpers"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
|
@ -44,7 +42,7 @@ func main() { skel.PluginMain(c, c) }
|
|||
gitRef = "f4364185253"
|
||||
|
||||
var err error
|
||||
outputDir, err = ioutil.TempDir("", "bin")
|
||||
outputDir, err = os.MkdirTemp("", "bin")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
outputFilePath = filepath.Join(outputDir, "some-binary")
|
||||
if runtime.GOOS == "windows" {
|
||||
|
@ -59,7 +57,7 @@ func main() { skel.PluginMain(c, c) }
|
|||
It("builds the provided source code using the CNI library at the given git ref", func() {
|
||||
Expect(outputFilePath).NotTo(BeAnExistingFile())
|
||||
|
||||
err := testhelpers.BuildAt(programSource, gitRef, outputFilePath)
|
||||
err := BuildAt(programSource, gitRef, outputFilePath)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(outputFilePath).To(BeAnExistingFile())
|
||||
|
@ -71,40 +69,3 @@ func main() { skel.PluginMain(c, c) }
|
|||
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"))
|
||||
}
|
||||
|
|
|
@ -15,16 +15,16 @@
|
|||
package version
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/pkg/types/020"
|
||||
"github.com/containernetworking/cni/pkg/types/current"
|
||||
"github.com/containernetworking/cni/pkg/types/create"
|
||||
)
|
||||
|
||||
// Current reports the version of the CNI spec implemented by this library
|
||||
func Current() string {
|
||||
return "0.4.0"
|
||||
return "1.1.0"
|
||||
}
|
||||
|
||||
// Legacy PluginInfo describes a plugin that is backwards compatible with the
|
||||
|
@ -34,28 +34,57 @@ func Current() string {
|
|||
//
|
||||
// Any future CNI spec versions which meet this definition should be added to
|
||||
// this list.
|
||||
var Legacy = PluginSupports("0.1.0", "0.2.0")
|
||||
var All = PluginSupports("0.1.0", "0.2.0", "0.3.0", "0.3.1", "0.4.0")
|
||||
var (
|
||||
Legacy = PluginSupports("0.1.0", "0.2.0")
|
||||
All = PluginSupports("0.1.0", "0.2.0", "0.3.0", "0.3.1", "0.4.0", "1.0.0", "1.1.0")
|
||||
)
|
||||
|
||||
var resultFactories = []struct {
|
||||
supportedVersions []string
|
||||
newResult types.ResultFactoryFunc
|
||||
}{
|
||||
{current.SupportedVersions, current.NewResult},
|
||||
{types020.SupportedVersions, types020.NewResult},
|
||||
// VersionsFrom returns a list of versions starting from min, inclusive
|
||||
func VersionsStartingFrom(min string) PluginInfo {
|
||||
out := []string{}
|
||||
// cheat, just assume ordered
|
||||
ok := false
|
||||
for _, v := range All.SupportedVersions() {
|
||||
if !ok && v == min {
|
||||
ok = true
|
||||
}
|
||||
if ok {
|
||||
out = append(out, v)
|
||||
}
|
||||
}
|
||||
return PluginSupports(out...)
|
||||
}
|
||||
|
||||
// Finds a Result object matching the requested version (if any) and asks
|
||||
// that object to parse the plugin result, returning an error if parsing failed.
|
||||
func NewResult(version string, resultBytes []byte) (types.Result, error) {
|
||||
reconciler := &Reconciler{}
|
||||
for _, resultFactory := range resultFactories {
|
||||
err := reconciler.CheckRaw(version, resultFactory.supportedVersions)
|
||||
if err == nil {
|
||||
// Result supports this version
|
||||
return resultFactory.newResult(resultBytes)
|
||||
}
|
||||
return create.Create(version, resultBytes)
|
||||
}
|
||||
|
||||
// ParsePrevResult parses a prevResult in a NetConf structure and sets
|
||||
// the NetConf's PrevResult member to the parsed Result object.
|
||||
func ParsePrevResult(conf *types.PluginConf) error {
|
||||
if conf.RawPrevResult == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unsupported CNI result version %q", version)
|
||||
// Prior to 1.0.0, Result types may not marshal a CNIVersion. Since the
|
||||
// result version must match the config version, if the Result's version
|
||||
// is empty, inject the config version.
|
||||
if ver, ok := conf.RawPrevResult["CNIVersion"]; !ok || ver == "" {
|
||||
conf.RawPrevResult["CNIVersion"] = conf.CNIVersion
|
||||
}
|
||||
|
||||
resultBytes, err := json.Marshal(conf.RawPrevResult)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not serialize prevResult: %w", err)
|
||||
}
|
||||
|
||||
conf.RawPrevResult = nil
|
||||
conf.PrevResult, err = create.Create(conf.CNIVersion, resultBytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse prevResult: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -15,10 +15,10 @@
|
|||
package version_test
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestVersion(t *testing.T) {
|
||||
|
|
|
@ -0,0 +1,160 @@
|
|||
// Copyright 2018 CNI authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package version_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net"
|
||||
"reflect"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
cniv1 "github.com/containernetworking/cni/pkg/types/100"
|
||||
"github.com/containernetworking/cni/pkg/version"
|
||||
)
|
||||
|
||||
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() {
|
||||
It("parses the prevResult", func() {
|
||||
rawBytes := []byte(`{
|
||||
"cniVersion": "1.0.0",
|
||||
"interfaces": [
|
||||
{
|
||||
"name": "eth0",
|
||||
"mac": "00:11:22:33:44:55",
|
||||
"sandbox": "/proc/3553/ns/net",
|
||||
"pciID": "8086:9a01",
|
||||
"socketPath": "/path/to/vhost/fd"
|
||||
}
|
||||
],
|
||||
"ips": [
|
||||
{
|
||||
"version": "4",
|
||||
"interface": 0,
|
||||
"address": "1.2.3.30/24",
|
||||
"gateway": "1.2.3.1"
|
||||
}
|
||||
]
|
||||
}`)
|
||||
var raw map[string]interface{}
|
||||
err := json.Unmarshal(rawBytes, &raw)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
conf := &types.PluginConf{
|
||||
CNIVersion: "1.0.0",
|
||||
Name: "foobar",
|
||||
Type: "baz",
|
||||
RawPrevResult: raw,
|
||||
}
|
||||
|
||||
err = version.ParsePrevResult(conf)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
expectedResult := &cniv1.Result{
|
||||
CNIVersion: "1.0.0",
|
||||
Interfaces: []*cniv1.Interface{
|
||||
{
|
||||
Name: "eth0",
|
||||
Mac: "00:11:22:33:44:55",
|
||||
Sandbox: "/proc/3553/ns/net",
|
||||
PciID: "8086:9a01",
|
||||
SocketPath: "/path/to/vhost/fd",
|
||||
},
|
||||
},
|
||||
IPs: []*cniv1.IPConfig{
|
||||
{
|
||||
Interface: cniv1.Int(0),
|
||||
Address: net.IPNet{
|
||||
IP: net.ParseIP("1.2.3.30"),
|
||||
Mask: net.IPv4Mask(255, 255, 255, 0),
|
||||
},
|
||||
Gateway: net.ParseIP("1.2.3.1"),
|
||||
},
|
||||
},
|
||||
}
|
||||
Expect(reflect.DeepEqual(conf.PrevResult, expectedResult)).To(BeTrue())
|
||||
})
|
||||
|
||||
It("fails if the prevResult version is unknown", func() {
|
||||
conf := &types.PluginConf{
|
||||
CNIVersion: version.Current(),
|
||||
Name: "foobar",
|
||||
Type: "baz",
|
||||
RawPrevResult: map[string]interface{}{
|
||||
"cniVersion": "5678.456",
|
||||
},
|
||||
}
|
||||
|
||||
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"`))
|
||||
})
|
||||
|
||||
It("fails if the prevResult version does not match the prevResult version", func() {
|
||||
conf := &types.PluginConf{
|
||||
CNIVersion: version.Current(),
|
||||
Name: "foobar",
|
||||
Type: "baz",
|
||||
RawPrevResult: map[string]interface{}{
|
||||
"cniVersion": "0.2.0",
|
||||
"ip4": map[string]interface{}{
|
||||
"ip": "1.2.3.30/24",
|
||||
"gateway": "1.2.3.1",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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\""))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when a prevResult is not available", func() {
|
||||
It("does not fail", func() {
|
||||
conf := &types.PluginConf{
|
||||
CNIVersion: version.Current(),
|
||||
Name: "foobar",
|
||||
Type: "baz",
|
||||
}
|
||||
|
||||
err := version.ParsePrevResult(conf)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
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)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,72 @@
|
|||
# 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"}
|
||||
----------------------
|
||||
```
|
|
@ -0,0 +1,15 @@
|
|||
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 => ../..
|
|
@ -0,0 +1,26 @@
|
|||
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=
|
|
@ -0,0 +1,148 @@
|
|||
// 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)
|
||||
}
|
|
@ -17,7 +17,7 @@ package debug
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/skel"
|
||||
)
|
||||
|
@ -29,19 +29,30 @@ type Debug struct {
|
|||
// Report* fields allow the test to control the behavior of the no-op plugin
|
||||
ReportResult string
|
||||
ReportError string
|
||||
ReportErrorCode uint
|
||||
ReportStderr string
|
||||
ReportVersionSupport []string
|
||||
ExitWithCode int
|
||||
|
||||
// Command stores the CNI command that the plugin received
|
||||
Command string
|
||||
|
||||
// CmdArgs stores the CNI Args and Env Vars that the plugin recieved
|
||||
// CmdArgs stores the CNI Args and Env Vars that the plugin received
|
||||
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
|
||||
func ReadDebug(debugFilePath string) (*Debug, error) {
|
||||
debugBytes, err := ioutil.ReadFile(debugFilePath)
|
||||
debugBytes, err := os.ReadFile(debugFilePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -62,10 +73,41 @@ func (debug *Debug) WriteDebug(debugFilePath string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(debugFilePath, debugBytes, 0600)
|
||||
err = os.WriteFile(debugFilePath, debugBytes, 0o600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -23,29 +23,31 @@ package main
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/skel"
|
||||
"github.com/containernetworking/cni/pkg/types"
|
||||
"github.com/containernetworking/cni/pkg/types/current"
|
||||
current "github.com/containernetworking/cni/pkg/types/100"
|
||||
"github.com/containernetworking/cni/pkg/version"
|
||||
noop_debug "github.com/containernetworking/cni/plugins/test/noop/debug"
|
||||
)
|
||||
|
||||
type NetConf struct {
|
||||
types.NetConf
|
||||
types.PluginConf
|
||||
DebugFile string `json:"debugFile"`
|
||||
PrevResult *current.Result `json:"prevResult,omitempty"`
|
||||
CommandLog string `json:"commandLog"`
|
||||
}
|
||||
|
||||
func loadConf(bytes []byte) (*NetConf, error) {
|
||||
n := &NetConf{}
|
||||
if err := json.Unmarshal(bytes, n); err != nil {
|
||||
return nil, fmt.Errorf("failed to load netconf: %v %q", err, string(bytes))
|
||||
return nil, fmt.Errorf("failed to load netconf: %w %q", err, string(bytes))
|
||||
}
|
||||
if err := version.ParsePrevResult(&n.PluginConf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
@ -61,7 +63,7 @@ func parseExtraArgs(args string) (map[string]string, error) {
|
|||
for _, item := range items {
|
||||
kv := strings.Split(item, "=")
|
||||
if len(kv) != 2 {
|
||||
return nil, fmt.Errorf("CNI_ARGS invalid key/value pair: %s\n", kv)
|
||||
return nil, fmt.Errorf("CNI_ARGS invalid key/value pair: %s", kv)
|
||||
}
|
||||
m[kv[0]] = kv[1]
|
||||
}
|
||||
|
@ -95,7 +97,10 @@ func debugBehavior(args *skel.CmdArgs, command string) error {
|
|||
|
||||
if debugFilePath == "" {
|
||||
fmt.Printf(`{}`)
|
||||
os.Stderr.WriteString("CNI_ARGS or config empty, no debug behavior\n")
|
||||
_, err = os.Stderr.WriteString("CNI_ARGS or config empty, no debug behavior\n")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -107,48 +112,76 @@ func debugBehavior(args *skel.CmdArgs, command string) error {
|
|||
debug.CmdArgs = *args
|
||||
debug.Command = command
|
||||
|
||||
if debug.ReportResult == "" {
|
||||
debug.ReportResult = fmt.Sprintf(` { "result": %q }`, noop_debug.EmptyReportResultMessage)
|
||||
}
|
||||
|
||||
err = debug.WriteDebug(debugFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
os.Stderr.WriteString(debug.ReportStderr)
|
||||
if netConf.CommandLog != "" {
|
||||
if err = noop_debug.WriteCommandLog(
|
||||
netConf.CommandLog,
|
||||
noop_debug.CmdLogEntry{
|
||||
Command: command,
|
||||
CmdArgs: *args,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if debug.ReportError != "" {
|
||||
return errors.New(debug.ReportError)
|
||||
} else if debug.ReportResult == "PASSTHROUGH" || debug.ReportResult == "INJECT-DNS" {
|
||||
if debug.ReportStderr != "" {
|
||||
if _, err = os.Stderr.WriteString(debug.ReportStderr); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
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
|
||||
if debug.ReportResult == "INJECT-DNS" {
|
||||
prevResult, err = current.NewResultFromResult(netConf.PrevResult)
|
||||
newResult, err := current.NewResultFromResult(netConf.PrevResult)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
prevResult.DNS.Nameservers = []string{"1.2.3.4"}
|
||||
newResult.DNS.Nameservers = []string{"1.2.3.4"}
|
||||
prevResult = newResult
|
||||
}
|
||||
|
||||
// Must print the prevResult as the CNIVersion of the config
|
||||
newResult, err := prevResult.GetAsVersion(netConf.CNIVersion)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert result to config %q: %v", netConf.CNIVersion, err)
|
||||
return fmt.Errorf("failed to convert result to config %q: %w", netConf.CNIVersion, err)
|
||||
}
|
||||
resultBytes, err := json.Marshal(newResult)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal new result: %v", err)
|
||||
return fmt.Errorf("failed to marshal new result: %w", 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
|
||||
}
|
||||
|
||||
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"}
|
||||
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"}
|
||||
cniArgs := os.Getenv("CNI_ARGS")
|
||||
if cniArgs == "" {
|
||||
return vers
|
||||
|
@ -173,17 +206,25 @@ func cmdAdd(args *skel.CmdArgs) error {
|
|||
return debugBehavior(args, "ADD")
|
||||
}
|
||||
|
||||
func cmdGet(args *skel.CmdArgs) error {
|
||||
return debugBehavior(args, "GET")
|
||||
func cmdCheck(args *skel.CmdArgs) error {
|
||||
return debugBehavior(args, "CHECK")
|
||||
}
|
||||
|
||||
func cmdDel(args *skel.CmdArgs) error {
|
||||
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) {
|
||||
// Read original stdin
|
||||
stdinData, err := ioutil.ReadAll(os.Stdin)
|
||||
stdinData, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -212,5 +253,11 @@ func main() {
|
|||
}
|
||||
|
||||
supportedVersions := debugGetSupportedVersions(stdinData)
|
||||
skel.PluginMain(cmdAdd, cmdGet, cmdDel, version.PluginSupports(supportedVersions...), "CNI nnop plugin v0.7.0")
|
||||
skel.PluginMainFuncs(skel.CNIFuncs{
|
||||
Add: cmdAdd,
|
||||
Check: cmdCheck,
|
||||
Del: cmdDel,
|
||||
GC: cmdGC,
|
||||
Status: cmdStatus,
|
||||
}, version.PluginSupports(supportedVersions...), "CNI noop plugin v0.7.0")
|
||||
}
|
||||
|
|
|
@ -15,11 +15,11 @@
|
|||
package main_test
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gexec"
|
||||
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNoop(t *testing.T) {
|
||||
|
|
|
@ -16,17 +16,18 @@ package main_test
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/containernetworking/cni/pkg/skel"
|
||||
"github.com/containernetworking/cni/pkg/version"
|
||||
noop_debug "github.com/containernetworking/cni/plugins/test/noop/debug"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "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/types"
|
||||
"github.com/containernetworking/cni/pkg/version"
|
||||
noop_debug "github.com/containernetworking/cni/plugins/test/noop/debug"
|
||||
)
|
||||
|
||||
var _ = Describe("No-op plugin", func() {
|
||||
|
@ -45,7 +46,7 @@ var _ = Describe("No-op plugin", func() {
|
|||
ReportVersionSupport: []string{"0.1.0", "0.2.0", "0.3.0", "0.3.1", "0.4.0"},
|
||||
}
|
||||
|
||||
debugFile, err := ioutil.TempFile("", "cni_debug")
|
||||
debugFile, err := os.CreateTemp("", "cni_debug")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(debugFile.Close()).To(Succeed())
|
||||
debugFileName = debugFile.Name()
|
||||
|
@ -106,6 +107,7 @@ var _ = Describe("No-op plugin", func() {
|
|||
"some":"stdin-json",
|
||||
"cniVersion": "0.3.1",
|
||||
"prevResult": {
|
||||
"cniVersion": "0.3.1",
|
||||
"ips": [{"version": "4", "address": "10.1.2.15/24"}]
|
||||
}
|
||||
}`)
|
||||
|
@ -124,6 +126,7 @@ var _ = Describe("No-op plugin", func() {
|
|||
"some":"stdin-json",
|
||||
"cniVersion": "0.4.0",
|
||||
"prevResult": {
|
||||
"cniVersion": "0.4.0",
|
||||
"ips": [{"version": "4", "address": "10.1.2.15/24"}]
|
||||
}
|
||||
}`)
|
||||
|
@ -142,7 +145,7 @@ var _ = Describe("No-op plugin", func() {
|
|||
"some":"stdin-json",
|
||||
"cniVersion": "0.4.0",
|
||||
"prevResult": {
|
||||
"cniVersion": "0.3.1",
|
||||
"cniVersion": "0.4.0",
|
||||
"ips": [{"version": "4", "address": "10.1.2.3/24"}],
|
||||
"dns": {}
|
||||
}
|
||||
|
@ -195,9 +198,46 @@ var _ = Describe("No-op plugin", func() {
|
|||
Expect(debug.WriteDebug(debugFileName)).To(Succeed())
|
||||
})
|
||||
|
||||
It("substitutes a helpful message for the test author", func() {
|
||||
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() {
|
||||
session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Eventually(session).Should(gexec.Exit(0))
|
||||
|
@ -219,7 +259,7 @@ var _ = Describe("No-op plugin", func() {
|
|||
session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Eventually(session).Should(gexec.Exit(1))
|
||||
Expect(session.Out.Contents()).To(MatchJSON(`{ "code": 100, "msg": "banana" }`))
|
||||
Expect(session.Out.Contents()).To(MatchJSON(fmt.Sprintf(`{ "code": %d, "msg": "banana" }`, types.ErrInternal)))
|
||||
})
|
||||
})
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue