Initial anycast plugin
authorMichael C Cambria <mcc@node.halfball.org>
Thu, 18 May 2017 19:36:05 +0000 (19:36 +0000)
committerMichael C Cambria <mcc@node.halfball.org>
Thu, 18 May 2017 19:36:05 +0000 (19:36 +0000)
build.sh
plugins/anycast/README.md [new file with mode: 0644]
plugins/anycast/main.go [new file with mode: 0644]
plugins/anycast/main.go~ [new file with mode: 0644]
plugins/anycast/sample_suite_test.go [new file with mode: 0644]
plugins/anycast/sample_test.go [new file with mode: 0644]

index b1b82a6..8ba8b41 100755 (executable)
--- a/build.sh
+++ b/build.sh
@@ -15,7 +15,7 @@ export GOPATH=${PWD}/gopath
 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")"
diff --git a/plugins/anycast/README.md b/plugins/anycast/README.md
new file mode 100644 (file)
index 0000000..9bbd6e9
--- /dev/null
@@ -0,0 +1,7 @@
+# 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.
diff --git a/plugins/anycast/main.go b/plugins/anycast/main.go
new file mode 100644 (file)
index 0000000..01f8f8d
--- /dev/null
@@ -0,0 +1,252 @@
+// 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()))
+}
diff --git a/plugins/anycast/main.go~ b/plugins/anycast/main.go~
new file mode 100644 (file)
index 0000000..39ff639
--- /dev/null
@@ -0,0 +1,252 @@
+// 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()))
+}
diff --git a/plugins/anycast/sample_suite_test.go b/plugins/anycast/sample_suite_test.go
new file mode 100644 (file)
index 0000000..f792c1e
--- /dev/null
@@ -0,0 +1,15 @@
+// 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")
+}
diff --git a/plugins/anycast/sample_test.go b/plugins/anycast/sample_test.go
new file mode 100644 (file)
index 0000000..9d2688c
--- /dev/null
@@ -0,0 +1,138 @@
+// 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())
+
+       })
+
+})