mkdir -p "${PWD}/bin"
echo "Building plugins"
-PLUGINS="plugins/meta/* plugins/main/* plugins/ipam/* plugins/sample"
+PLUGINS="plugins/meta/* plugins/main/* plugins/ipam/* plugins/anycast plugins/sample"
for d in $PLUGINS; do
if [ -d "$d" ]; then
plugin="$(basename "$d")"
--- /dev/null
+# Sample CNI plugin
+
+This is an example of a sample chained plugin. It includes solutions for some
+of the more subtle cases that can be experienced with multi-version chained
+plugins.
+
+To use it, just add your code to the cmdAdd and cmdDel plugins.
--- /dev/null
+// Copyright 2017 CNI authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// This is a sample chained plugin that supports multiple CNI versions. It
+// parses prevResult according to the cniVersion
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "net"
+
+ "github.com/containernetworking/cni/pkg/skel"
+ "github.com/containernetworking/cni/pkg/types"
+ "github.com/containernetworking/cni/pkg/types/current"
+ "github.com/containernetworking/cni/pkg/version"
+
+ "github.com/containernetworking/cni/pkg/ns"
+ "github.com/vishvananda/netlink"
+ "log"
+ "os"
+ "syscall"
+)
+
+// PluginConf is whatever you expect your configuration json to be. This is whatever
+// is passed in on stdin. Your plugin may wish to expose its functionality via
+// runtime args, see CONVENTIONS.md in the CNI spec.
+type PluginConf struct {
+ types.NetConf // You may wish to not nest this type
+ RuntimeConfig *struct {
+ SampleConfig map[string]interface{} `json:"sample"`
+ } `json:"runtimeConfig"`
+
+ // This is the previous result, when called in the context of a chained
+ // plugin. Because this plugin supports multiple versions, we'll have to
+ // parse this in two passes. If your plugin is not chained, this can be
+ // removed (though you may wish to error if a non-chainable plugin is
+ // chained.
+ // If you need to modify the result before returning it, you will need
+ // to actually convert it to a concrete versioned struct.
+ RawPrevResult *map[string]interface{} `json:"prevResult"`
+ PrevResult *current.Result `json:"-"`
+
+ // Add plugin-specifc flags here
+ MyAwesomeFlag bool `json:"myAwesomeFlag"`
+ AnotherAwesomeArg string `json:"anotherAwesomeArg"`
+ STATICIP net.IP `json:"StaticIP,omitempty"`
+ VIAIP net.IP `json:"viaIP,omitempty"`
+ UPLINK types.UnmarshallableString `json:"uplink,omitempty"`
+}
+
+// parseConfig parses the supplied configuration (and prevResult) from stdin.
+func parseConfig(stdin []byte) (*PluginConf, error) {
+ conf := PluginConf{}
+
+ if err := json.Unmarshal(stdin, &conf); err != nil {
+ return nil, fmt.Errorf("failed to parse network configuration: %v", err)
+ }
+
+ // Parse previous result. Remove this if your plugin is not chained.
+ if conf.RawPrevResult != nil {
+ resultBytes, err := json.Marshal(conf.RawPrevResult)
+ if err != nil {
+ return nil, fmt.Errorf("could not serialize prevResult: %v", err)
+ }
+ res, err := version.NewResult(conf.CNIVersion, resultBytes)
+ if err != nil {
+ return nil, fmt.Errorf("could not parse prevResult: %v", err)
+ }
+ conf.RawPrevResult = nil
+ conf.PrevResult, err = current.NewResultFromResult(res)
+ if err != nil {
+ return nil, fmt.Errorf("could not convert result to current version: %v", err)
+ }
+ }
+ // End previous result parsing
+
+ // Do any validation here
+ if conf.AnotherAwesomeArg == "" {
+ return nil, fmt.Errorf("anotherAwesomeArg must be specified")
+ }
+
+ return &conf, nil
+}
+
+// cmdAdd is called for ADD requests
+func cmdAdd(args *skel.CmdArgs) error {
+ conf, err := parseConfig(args.StdinData)
+ if err != nil {
+ return err
+ }
+
+ if conf.PrevResult == nil {
+ return fmt.Errorf("must be called as chained plugin")
+ }
+
+ // This is some sample code to generate the list of container-side IPs.
+ // We're casting the prevResult to a 0.3.1 response, which can also include
+ // host-side IPs (but doesn't when converted from a 0.2.0 response).
+ containerIPs := make([]net.IP, 0, len(conf.PrevResult.IPs))
+ if conf.CNIVersion != "0.3.1" {
+ for _, ip := range conf.PrevResult.IPs {
+ containerIPs = append(containerIPs, ip.Address.IP)
+ log.Println("xIP 1 is: ", ip.Address.IP)
+ log.Println("xIP 2 is: ", ip)
+ }
+ } else {
+ for _, ip := range conf.PrevResult.IPs {
+ intIdx := ip.Interface
+ // Every IP is indexed in to the interfaces array, with "-1" standing
+ // for an unknown interface (which we'll assume to be Container-side
+ // Skip all IPs we know belong to an interface with the wrong name.
+ if intIdx >= 0 && intIdx < len(conf.PrevResult.Interfaces) && conf.PrevResult.Interfaces[intIdx].Name != args.IfName {
+ log.Println("skip: 1 ", ip.Address.IP)
+ log.Println("skip: 2", ip)
+ continue
+ }
+ containerIPs = append(containerIPs, ip.Address.IP)
+ log.Println("no skip IP 1 is: ", ip.Address.IP)
+ log.Println("no skip IP 2 is: ", ip)
+}
+ }
+ if len(containerIPs) == 0 {
+ return fmt.Errorf("got no container IPs")
+ }
+
+ containerNs, err := ns.GetNS(args.Netns)
+ if err != nil {
+ return fmt.Errorf("failed to open netns %q: %v", args.Netns, err)
+ }
+ defer containerNs.Close()
+
+ log.Printf("Slice IPs: len=%d, cap=%d values= %v\n",
+ len(containerIPs),
+ cap(containerIPs),
+ containerIPs)
+
+ viaIP := conf.VIAIP
+ log.Println("viaIP = ", viaIP)
+
+ uplink := conf.UPLINK
+ log.Println("uplink = ", uplink)
+
+ staticIP := conf.STATICIP
+ log.Println("staticIP = ", staticIP)
+
+ log.Println("AdvRoute: BGP Advertises /32", staticIP, viaIP, uplink)
+
+ dst := &net.IPNet{
+ IP: staticIP,
+ Mask: net.CIDRMask(32, 32),
+ }
+
+ link, err := netlink.LinkByName(string(uplink))
+ if err != nil {
+ log.Println("Can't obtain link index for: ", uplink)
+ return err
+ }
+
+ route := netlink.Route{
+ Dst: dst,
+ LinkIndex: link.Attrs().Index,
+ Gw: viaIP,
+ }
+
+ if err := netlink.RouteAdd(&route); err != nil {
+ fmt.Fprintln(os.Stderr, "There was an error adding netlink route: ", err)
+ if (err == syscall.EAGAIN) {
+ log.Println("ERRNO: eagain")
+ } else if (err == syscall.EEXIST) {
+ log.Println("ERRNO: route already exists")
+ } else {
+ log.Println("ERRNO: value is: ", (int(err.(syscall.Errno))))
+ }
+ return err
+ }
+
+ // Pass trough the result for the next plugin
+ return types.PrintResult(conf.PrevResult, conf.CNIVersion)
+}
+
+// cmdDel is called for DELETE requests
+func cmdDel(args *skel.CmdArgs) error {
+ conf, err := parseConfig(args.StdinData)
+ if err != nil {
+ return err
+ }
+ _ = conf
+
+ containerNs, err := ns.GetNS(args.Netns)
+ if err != nil {
+ return fmt.Errorf("failed to open netns %q: %v", args.Netns, err)
+ }
+ defer containerNs.Close()
+
+ viaIP := conf.VIAIP
+ log.Println("viaIP = ", viaIP)
+
+ uplink := conf.UPLINK
+ log.Println("uplink = ", uplink)
+
+ staticIP := conf.STATICIP
+ log.Println("staticIP = ", staticIP)
+
+ log.Println("AdvRoute: BGP Advertises /32", staticIP, viaIP, uplink)
+
+ dst := &net.IPNet{
+ IP: staticIP,
+ Mask: net.CIDRMask(32, 32),
+ }
+
+ link, err := netlink.LinkByName(string(uplink))
+ if err != nil {
+ log.Println("Can't obtain link index for: ", uplink)
+ return err
+ }
+
+ route := netlink.Route{
+ Dst: dst,
+ LinkIndex: link.Attrs().Index,
+ Gw: viaIP,
+ }
+
+ if err := netlink.RouteDel(&route); err != nil {
+ fmt.Fprintln(os.Stderr, "There was an error adding netlink route: ", err)
+ if (err == syscall.EAGAIN) {
+ log.Println("ERRNO: eagain")
+ } else if (err == syscall.EEXIST) {
+ log.Println("ERRNO: route already exists")
+ } else {
+ log.Println("ERRNO: value is: ", (int(err.(syscall.Errno))))
+ }
+ return err
+ }
+
+ return nil
+}
+
+func main() {
+ skel.PluginMain(cmdAdd, cmdDel, version.PluginSupports("", "0.1.0", "0.2.0", version.Current()))
+}
--- /dev/null
+// Copyright 2017 CNI authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// This is a sample chained plugin that supports multiple CNI versions. It
+// parses prevResult according to the cniVersion
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "net"
+
+ "github.com/containernetworking/cni/pkg/skel"
+ "github.com/containernetworking/cni/pkg/types"
+ "github.com/containernetworking/cni/pkg/types/current"
+ "github.com/containernetworking/cni/pkg/version"
+
+ "github.com/containernetworking/cni/pkg/ns"
+ "github.com/vishvananda/netlink"
+ "log"
+ "os"
+ "syscall"
+)
+
+// PluginConf is whatever you expect your configuration json to be. This is whatever
+// is passed in on stdin. Your plugin may wish to expose its functionality via
+// runtime args, see CONVENTIONS.md in the CNI spec.
+type PluginConf struct {
+ types.NetConf // You may wish to not nest this type
+ RuntimeConfig *struct {
+ SampleConfig map[string]interface{} `json:"sample"`
+ } `json:"runtimeConfig"`
+
+ // This is the previous result, when called in the context of a chained
+ // plugin. Because this plugin supports multiple versions, we'll have to
+ // parse this in two passes. If your plugin is not chained, this can be
+ // removed (though you may wish to error if a non-chainable plugin is
+ // chained.
+ // If you need to modify the result before returning it, you will need
+ // to actually convert it to a concrete versioned struct.
+ RawPrevResult *map[string]interface{} `json:"prevResult"`
+ PrevResult *current.Result `json:"-"`
+
+ // Add plugin-specifc flags here
+ MyAwesomeFlag bool `json:"myAwesomeFlag"`
+ AnotherAwesomeArg string `json:"anotherAwesomeArg"`
+ STATICIP net.IP `json:"StaticIP,omitempty"`
+ VIAIP net.IP `json:"viaIP,omitempty"`
+ UPLINK types.UnmarshallableString `json:"uplink,omitempty"`
+}
+
+// parseConfig parses the supplied configuration (and prevResult) from stdin.
+func parseConfig(stdin []byte) (*PluginConf, error) {
+ conf := PluginConf{}
+
+ if err := json.Unmarshal(stdin, &conf); err != nil {
+ return nil, fmt.Errorf("failed to parse network configuration: %v", err)
+ }
+
+ // Parse previous result. Remove this if your plugin is not chained.
+ if conf.RawPrevResult != nil {
+ resultBytes, err := json.Marshal(conf.RawPrevResult)
+ if err != nil {
+ return nil, fmt.Errorf("could not serialize prevResult: %v", err)
+ }
+ res, err := version.NewResult(conf.CNIVersion, resultBytes)
+ if err != nil {
+ return nil, fmt.Errorf("could not parse prevResult: %v", err)
+ }
+ conf.RawPrevResult = nil
+ conf.PrevResult, err = current.NewResultFromResult(res)
+ if err != nil {
+ return nil, fmt.Errorf("could not convert result to current version: %v", err)
+ }
+ }
+ // End previous result parsing
+
+ // Do any validation here
+ if conf.AnotherAwesomeArg == "" {
+ return nil, fmt.Errorf("anotherAwesomeArg must be specified")
+ }
+
+ return &conf, nil
+}
+
+// cmdAdd is called for ADD requests
+func cmdAdd(args *skel.CmdArgs) error {
+ conf, err := parseConfig(args.StdinData)
+ if err != nil {
+ return err
+ }
+
+ if conf.PrevResult == nil {
+ return fmt.Errorf("must be called as chained plugin")
+ }
+
+ // This is some sample code to generate the list of container-side IPs.
+ // We're casting the prevResult to a 0.3.1 response, which can also include
+ // host-side IPs (but doesn't when converted from a 0.2.0 response).
+ containerIPs := make([]net.IP, 0, len(conf.PrevResult.IPs))
+ if conf.CNIVersion != "0.3.1" {
+ for _, ip := range conf.PrevResult.IPs {
+ containerIPs = append(containerIPs, ip.Address.IP)
+ log.Println("xIP 1 is: ", ip.Address.IP)
+ log.Println("xIP 2 is: ", ip)
+ }
+ } else {
+ for _, ip := range conf.PrevResult.IPs {
+ intIdx := ip.Interface
+ // Every IP is indexed in to the interfaces array, with "-1" standing
+ // for an unknown interface (which we'll assume to be Container-side
+ // Skip all IPs we know belong to an interface with the wrong name.
+ if intIdx >= 0 && intIdx < len(conf.PrevResult.Interfaces) && conf.PrevResult.Interfaces[intIdx].Name != args.IfName {
+ log.Println("skip: 1 ", ip.Address.IP)
+ log.Println("skip: 2", ip)
+ continue
+ }
+ containerIPs = append(containerIPs, ip.Address.IP)
+ log.Println("no skip IP 1 is: ", ip.Address.IP)
+ log.Println("no skip IP 2 is: ", ip)
+}
+ }
+ if len(containerIPs) == 0 {
+ return fmt.Errorf("got no container IPs")
+ }
+
+ containerNs, err := ns.GetNS(args.Netns)
+ if err != nil {
+ return fmt.Errorf("failed to open netns %q: %v", args.Netns, err)
+ }
+ defer containerNs.Close()
+
+ fmt.Printf("Slice IPs: len=%d, cap=%d values= %v\n",
+ len(containerIPs),
+ cap(containerIPs),
+ containerIPs)
+
+ viaIP := conf.VIAIP
+ log.Println("viaIP = ", viaIP)
+
+ uplink := conf.UPLINK
+ log.Println("uplink = ", uplink)
+
+ staticIP := conf.STATICIP
+ log.Println("staticIP = ", staticIP)
+
+ log.Println("AdvRoute: BGP Advertises /32", staticIP, viaIP, uplink)
+
+ dst := &net.IPNet{
+ IP: staticIP,
+ Mask: net.CIDRMask(32, 32),
+ }
+
+ link, err := netlink.LinkByName(string(uplink))
+ if err != nil {
+ log.Println("Can't obtain link index for: ", uplink)
+ return err
+ }
+
+ route := netlink.Route{
+ Dst: dst,
+ LinkIndex: link.Attrs().Index,
+ Gw: viaIP,
+ }
+
+ if err := netlink.RouteAdd(&route); err != nil {
+ fmt.Fprintln(os.Stderr, "There was an error adding netlink route: ", err)
+ if (err == syscall.EAGAIN) {
+ log.Println("ERRNO: eagain")
+ } else if (err == syscall.EEXIST) {
+ log.Println("ERRNO: route already exists")
+ } else {
+ log.Println("ERRNO: value is: ", (int(err.(syscall.Errno))))
+ }
+ return err
+ }
+
+ // Pass trough the result for the next plugin
+ return types.PrintResult(conf.PrevResult, conf.CNIVersion)
+}
+
+// cmdDel is called for DELETE requests
+func cmdDel(args *skel.CmdArgs) error {
+ conf, err := parseConfig(args.StdinData)
+ if err != nil {
+ return err
+ }
+ _ = conf
+
+ containerNs, err := ns.GetNS(args.Netns)
+ if err != nil {
+ return fmt.Errorf("failed to open netns %q: %v", args.Netns, err)
+ }
+ defer containerNs.Close()
+
+ viaIP := conf.VIAIP
+ log.Println("viaIP = ", viaIP)
+
+ uplink := conf.UPLINK
+ log.Println("uplink = ", uplink)
+
+ staticIP := conf.STATICIP
+ log.Println("staticIP = ", staticIP)
+
+ log.Println("AdvRoute: BGP Advertises /32", staticIP, viaIP, uplink)
+
+ dst := &net.IPNet{
+ IP: staticIP,
+ Mask: net.CIDRMask(32, 32),
+ }
+
+ link, err := netlink.LinkByName(string(uplink))
+ if err != nil {
+ log.Println("Can't obtain link index for: ", uplink)
+ return err
+ }
+
+ route := netlink.Route{
+ Dst: dst,
+ LinkIndex: link.Attrs().Index,
+ Gw: viaIP,
+ }
+
+ if err := netlink.RouteDel(&route); err != nil {
+ fmt.Fprintln(os.Stderr, "There was an error adding netlink route: ", err)
+ if (err == syscall.EAGAIN) {
+ log.Println("ERRNO: eagain")
+ } else if (err == syscall.EEXIST) {
+ log.Println("ERRNO: route already exists")
+ } else {
+ log.Println("ERRNO: value is: ", (int(err.(syscall.Errno))))
+ }
+ return err
+ }
+
+ return nil
+}
+
+func main() {
+ skel.PluginMain(cmdAdd, cmdDel, version.PluginSupports("", "0.1.0", "0.2.0", version.Current()))
+}
--- /dev/null
+// The boilerplate needed for Ginkgo
+
+package main
+
+import (
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+
+ "testing"
+)
+
+func TestSample(t *testing.T) {
+ RegisterFailHandler(Fail)
+ RunSpecs(t, "sample suite")
+}
--- /dev/null
+// Copyright 2017 CNI authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+ "fmt"
+
+ "github.com/containernetworking/cni/pkg/ns"
+ "github.com/containernetworking/cni/pkg/skel"
+ "github.com/containernetworking/cni/pkg/testutils"
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+)
+
+var _ = Describe("sample test", func() {
+ var targetNs ns.NetNS
+
+ BeforeEach(func() {
+ var err error
+ targetNs, err = ns.NewNS()
+ Expect(err).NotTo(HaveOccurred())
+ })
+
+ AfterEach(func() {
+ targetNs.Close()
+ })
+
+ It("Works with a 0.3.0 config", func() {
+ ifname := "eth0"
+ conf := `{
+ "cniVersion": "0.3.0",
+ "name": "cni-plugin-sample-test",
+ "type": "sample",
+ "anotherAwesomeArg": "awesome",
+ "prevResult": {
+ "interfaces": [
+ {
+ "name": "%s",
+ "sandbox": "%s"
+ }
+ ],
+ "ips": [
+ {
+ "version": "4",
+ "address": "10.0.0.2/24",
+ "gateway": "10.0.0.1",
+ "interface": 0
+ }
+ ],
+ "routes": []
+ }
+}`
+ conf = fmt.Sprintf(conf, ifname, targetNs.Path())
+ args := &skel.CmdArgs{
+ ContainerID: "dummy",
+ Netns: targetNs.Path(),
+ IfName: ifname,
+ StdinData: []byte(conf),
+ }
+ _, _, err := testutils.CmdAddWithResult(targetNs.Path(), "eth0", []byte(conf), func() error { return cmdAdd(args) })
+ Expect(err).NotTo(HaveOccurred())
+
+ })
+
+ It("fails an invalid config", func() {
+ conf := `{
+ "cniVersion": "0.3.0",
+ "name": "cni-plugin-sample-test",
+ "type": "sample",
+ "prevResult": {
+ "interfaces": [
+ {
+ "name": "eth0",
+ "sandbox": "/var/run/netns/test"
+ }
+ ],
+ "ips": [
+ {
+ "version": "4",
+ "address": "10.0.0.2/24",
+ "gateway": "10.0.0.1",
+ "interface": 0
+ }
+ ],
+ "routes": []
+ }
+}`
+
+ args := &skel.CmdArgs{
+ ContainerID: "dummy",
+ Netns: targetNs.Path(),
+ IfName: "eth0",
+ StdinData: []byte(conf),
+ }
+ _, _, err := testutils.CmdAddWithResult(targetNs.Path(), "eth0", []byte(conf), func() error { return cmdAdd(args) })
+ Expect(err).To(MatchError("anotherAwesomeArg must be specified"))
+
+ })
+
+ It("works with a 0.2.0 config", func() {
+ conf := `{
+ "cniVersion": "0.2.0",
+ "name": "cni-plugin-sample-test",
+ "type": "sample",
+ "anotherAwesomeArg": "foo",
+ "prevResult": {
+ "ip4": {
+ "ip": "10.0.0.2/24",
+ "gateway": "10.0.0.1",
+ "routes": []
+ }
+ }
+}`
+
+ args := &skel.CmdArgs{
+ ContainerID: "dummy",
+ Netns: targetNs.Path(),
+ IfName: "eth0",
+ StdinData: []byte(conf),
+ }
+ _, _, err := testutils.CmdAddWithResult(targetNs.Path(), "eth0", []byte(conf), func() error { return cmdAdd(args) })
+ Expect(err).NotTo(HaveOccurred())
+
+ })
+
+})