Add plugin code
authorEugene Yakubovich <eugene.yakubovich@coreos.com>
Wed, 15 Apr 2015 22:35:02 +0000 (15:35 -0700)
committerEugene Yakubovich <eugene.yakubovich@coreos.com>
Mon, 27 Apr 2015 21:14:29 +0000 (14:14 -0700)
This adds basic plugins.
"main" types: veth, bridge, macvlan
"ipam" type: host-local

The code has been ported over from github.com/coreos/rkt project
and adapted to fit the CNI spec.

ip/cidr.go [new file with mode: 0644]
ip/ipmasq.go [new file with mode: 0644]
ip/link.go [new file with mode: 0644]
ip/route.go [new file with mode: 0644]
ns/ns.go [new file with mode: 0644]
plugin/ipam.go [new file with mode: 0644]
plugin/types.go [new file with mode: 0644]
skel/skel.go [new file with mode: 0644]

diff --git a/ip/cidr.go b/ip/cidr.go
new file mode 100644 (file)
index 0000000..c963398
--- /dev/null
@@ -0,0 +1,86 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// 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 ip
+
+import (
+       "encoding/json"
+       "math/big"
+       "net"
+)
+
+// ParseCIDR takes a string like "10.2.3.1/24" and
+// return IPNet with "10.2.3.1" and /24 mask
+func ParseCIDR(s string) (*net.IPNet, error) {
+       ip, ipn, err := net.ParseCIDR(s)
+       if err != nil {
+               return nil, err
+       }
+
+       ipn.IP = ip
+       return ipn, nil
+}
+
+// NextIP returns IP incremented by 1
+func NextIP(ip net.IP) net.IP {
+       i := ipToInt(ip)
+       return intToIP(i.Add(i, big.NewInt(1)))
+}
+
+// PrevIP returns IP decremented by 1
+func PrevIP(ip net.IP) net.IP {
+       i := ipToInt(ip)
+       return intToIP(i.Sub(i, big.NewInt(1)))
+}
+
+func ipToInt(ip net.IP) *big.Int {
+       if v := ip.To4(); v != nil {
+               return big.NewInt(0).SetBytes(v)
+       }
+       return big.NewInt(0).SetBytes(ip.To16())
+}
+
+func intToIP(i *big.Int) net.IP {
+       return net.IP(i.Bytes())
+}
+
+// Network masks off the host portion of the IP
+func Network(ipn *net.IPNet) *net.IPNet {
+       return &net.IPNet{
+               IP:   ipn.IP.Mask(ipn.Mask),
+               Mask: ipn.Mask,
+       }
+}
+
+// like net.IPNet but adds JSON marshalling and unmarshalling
+type IPNet net.IPNet
+
+func (n IPNet) MarshalJSON() ([]byte, error) {
+       return json.Marshal((*net.IPNet)(&n).String())
+}
+
+func (n *IPNet) UnmarshalJSON(data []byte) error {
+       var s string
+       if err := json.Unmarshal(data, &s); err != nil {
+               return err
+       }
+
+       tmp, err := ParseCIDR(s)
+       if err != nil {
+               return err
+       }
+
+       *n = IPNet(*tmp)
+       return nil
+}
diff --git a/ip/ipmasq.go b/ip/ipmasq.go
new file mode 100644 (file)
index 0000000..665189b
--- /dev/null
@@ -0,0 +1,66 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// 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 ip
+
+import (
+       "fmt"
+       "net"
+
+       "github.com/appc/cni/Godeps/_workspace/src/github.com/coreos/go-iptables/iptables"
+)
+
+// SetupIPMasq installs iptables rules to masquerade traffic
+// coming from ipn and going outside of it
+func SetupIPMasq(ipn *net.IPNet, chain string) error {
+       ipt, err := iptables.New()
+       if err != nil {
+               return fmt.Errorf("failed to locate iptabes: %v", err)
+       }
+
+       if err = ipt.NewChain("nat", chain); err != nil {
+               if err.(*iptables.Error).ExitStatus() != 1 {
+                       // TODO(eyakubovich): assumes exit status 1 implies chain exists
+                       return err
+               }
+       }
+
+       if err = ipt.AppendUnique("nat", chain, "-d", ipn.String(), "-j", "ACCEPT"); err != nil {
+               return err
+       }
+
+       if err = ipt.AppendUnique("nat", chain, "!", "-d", "224.0.0.0/4", "-j", "MASQUERADE"); err != nil {
+               return err
+       }
+
+       return ipt.AppendUnique("nat", "POSTROUTING", "-s", ipn.String(), "-j", chain)
+}
+
+// TeardownIPMasq undoes the effects of SetupIPMasq
+func TeardownIPMasq(ipn *net.IPNet, chain string) error {
+       ipt, err := iptables.New()
+       if err != nil {
+               return fmt.Errorf("failed to locate iptabes: %v", err)
+       }
+
+       if err = ipt.Delete("nat", "POSTROUTING", "-s", ipn.String(), "-j", chain); err != nil {
+               return err
+       }
+
+       if err = ipt.ClearChain("nat", chain); err != nil {
+               return err
+       }
+
+       return ipt.DeleteChain("nat", chain)
+}
diff --git a/ip/link.go b/ip/link.go
new file mode 100644 (file)
index 0000000..59865cf
--- /dev/null
@@ -0,0 +1,117 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// 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 ip
+
+import (
+       "crypto/sha512"
+       "fmt"
+       "net"
+       "os"
+
+       "github.com/appc/cni/Godeps/_workspace/src/github.com/vishvananda/netlink"
+)
+
+func makeVeth(name, peer string, mtu int) (netlink.Link, error) {
+       veth := &netlink.Veth{
+               LinkAttrs: netlink.LinkAttrs{
+                       Name:  name,
+                       Flags: net.FlagUp,
+                       MTU:   mtu,
+               },
+               PeerName: peer,
+       }
+       if err := netlink.LinkAdd(veth); err != nil {
+               return nil, err
+       }
+
+       return veth, nil
+}
+
+// RandomVethName returns string "veth" with random prefix (hashed from entropy)
+func RandomVethName(entropy string) string {
+       h := sha512.New()
+       h.Write([]byte(entropy))
+       return fmt.Sprintf("veth%x", h.Sum(nil)[:5])
+}
+
+// SetupVeth sets up a virtual ethernet link.
+// Should be in container netns.
+// TODO(eyakubovich): get rid of entropy and ask kernel to pick name via pattern
+func SetupVeth(entropy, contVethName string, mtu int, hostNS *os.File) (hostVeth, contVeth netlink.Link, err error) {
+       // NetworkManager (recent versions) will ignore veth devices that start with "veth"
+       hostVethName := RandomVethName(entropy)
+       hostVeth, err = makeVeth(hostVethName, contVethName, mtu)
+       if err != nil {
+               err = fmt.Errorf("failed to make veth pair: %v", err)
+               return
+       }
+
+       if err = netlink.LinkSetUp(hostVeth); err != nil {
+               err = fmt.Errorf("failed to set %q up: %v", hostVethName, err)
+               return
+       }
+
+       contVeth, err = netlink.LinkByName(contVethName)
+       if err != nil {
+               err = fmt.Errorf("failed to lookup %q: %v", contVethName, err)
+               return
+       }
+
+       if err = netlink.LinkSetUp(contVeth); err != nil {
+               err = fmt.Errorf("failed to set %q up: %v", contVethName, err)
+               return
+       }
+
+       if err = netlink.LinkSetNsFd(hostVeth, int(hostNS.Fd())); err != nil {
+               err = fmt.Errorf("failed to move veth to host netns: %v", err)
+               return
+       }
+
+       return
+}
+
+// DelLinkByName removes an interface link.
+func DelLinkByName(ifName string) error {
+       iface, err := netlink.LinkByName(ifName)
+       if err != nil {
+               return fmt.Errorf("failed to lookup %q: %v", ifName, err)
+       }
+
+       if err = netlink.LinkDel(iface); err != nil {
+               return fmt.Errorf("failed to delete %q: %v", ifName, err)
+       }
+
+       return nil
+}
+
+// DelLinkByNameAddr remove an interface returns its IP address
+// of the specified family
+func DelLinkByNameAddr(ifName string, family int) (*net.IPNet, error) {
+       iface, err := netlink.LinkByName(ifName)
+       if err != nil {
+               return nil, fmt.Errorf("failed to lookup %q: %v", ifName, err)
+       }
+
+       addrs, err := netlink.AddrList(iface, family)
+       if err != nil || len(addrs) == 0 {
+               return nil, fmt.Errorf("failed to get IP addresses for %q: %v", ifName, err)
+       }
+
+       if err = netlink.LinkDel(iface); err != nil {
+               return nil, fmt.Errorf("failed to delete %q: %v", ifName, err)
+       }
+
+       return addrs[0].IPNet, nil
+}
diff --git a/ip/route.go b/ip/route.go
new file mode 100644 (file)
index 0000000..f310f1e
--- /dev/null
@@ -0,0 +1,47 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// 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 ip
+
+import (
+       "net"
+
+       "github.com/appc/cni/Godeps/_workspace/src/github.com/vishvananda/netlink"
+)
+
+// AddDefaultRoute sets the default route on the given gateway.
+func AddDefaultRoute(gw net.IP, dev netlink.Link) error {
+       _, defNet, _ := net.ParseCIDR("0.0.0.0/0")
+       return AddRoute(defNet, gw, dev)
+}
+
+// AddRoute adds a universally-scoped route to a device.
+func AddRoute(ipn *net.IPNet, gw net.IP, dev netlink.Link) error {
+       return netlink.RouteAdd(&netlink.Route{
+               LinkIndex: dev.Attrs().Index,
+               Scope:     netlink.SCOPE_UNIVERSE,
+               Dst:       ipn,
+               Gw:        gw,
+       })
+}
+
+// AddHostRoute adds a host-scoped route to a device.
+func AddHostRoute(ipn *net.IPNet, gw net.IP, dev netlink.Link) error {
+       return netlink.RouteAdd(&netlink.Route{
+               LinkIndex: dev.Attrs().Index,
+               Scope:     netlink.SCOPE_HOST,
+               Dst:       ipn,
+               Gw:        gw,
+       })
+}
diff --git a/ns/ns.go b/ns/ns.go
new file mode 100644 (file)
index 0000000..82291f9
--- /dev/null
+++ b/ns/ns.go
@@ -0,0 +1,81 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// 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 (
+       "fmt"
+       "os"
+       "runtime"
+       "syscall"
+)
+
+var setNsMap = map[string]uintptr{
+       "386":   346,
+       "amd64": 308,
+       "arm":   374,
+}
+
+// SetNS sets the network namespace on a target file.
+func SetNS(f *os.File, flags uintptr) error {
+       if runtime.GOOS != "linux" {
+               return fmt.Errorf("unsupported OS: %s", runtime.GOOS)
+       }
+
+       trap, ok := setNsMap[runtime.GOARCH]
+       if !ok {
+               return fmt.Errorf("unsupported arch: %s", runtime.GOARCH)
+       }
+
+       _, _, err := syscall.RawSyscall(trap, f.Fd(), flags, 0)
+       if err != 0 {
+               return err
+       }
+
+       return nil
+}
+
+// WithNetNSPath executes the passed closure under the given network
+// namespace, restoring the original namespace afterwards.
+func WithNetNSPath(nspath string, f func(*os.File) error) error {
+       ns, err := os.Open(nspath)
+       if err != nil {
+               return fmt.Errorf("Failed to open %v: %v", nspath, err)
+       }
+       defer ns.Close()
+
+       return WithNetNS(ns, f)
+}
+
+// WithNetNS executes the passed closure under the given network
+// namespace, restoring the original namespace afterwards.
+func WithNetNS(ns *os.File, f func(*os.File) error) error {
+       // save a handle to current (host) network namespace
+       thisNS, err := os.Open("/proc/self/ns/net")
+       if err != nil {
+               return fmt.Errorf("Failed to open /proc/self/ns/net: %v", err)
+       }
+       defer thisNS.Close()
+
+       if err = SetNS(ns, syscall.CLONE_NEWNET); err != nil {
+               return fmt.Errorf("Error switching to ns %v: %v", ns.Name(), err)
+       }
+
+       if err = f(thisNS); err != nil {
+               return err
+       }
+
+       // switch back
+       return SetNS(thisNS, syscall.CLONE_NEWNET)
+}
diff --git a/plugin/ipam.go b/plugin/ipam.go
new file mode 100644 (file)
index 0000000..8b59cab
--- /dev/null
@@ -0,0 +1,136 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// 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 plugin
+
+import (
+       "bytes"
+       "encoding/json"
+       "fmt"
+       "os"
+       "os/exec"
+       "path/filepath"
+       "strings"
+
+       "github.com/appc/cni/Godeps/_workspace/src/github.com/vishvananda/netlink"
+       "github.com/appc/cni/pkg/ip"
+)
+
+// Find returns the full path of the plugin by searching in CNI_PATH
+func Find(plugin string) string {
+       paths := strings.Split(os.Getenv("CNI_PATH"), ":")
+
+       for _, p := range paths {
+               fullname := filepath.Join(p, plugin)
+               if fi, err := os.Stat(fullname); err == nil && fi.Mode().IsRegular() {
+                       return fullname
+               }
+       }
+
+       return ""
+}
+
+// ExecAdd executes IPAM plugin, assuming CNI_COMMAND == ADD.
+// Parses and returns resulting IPConfig
+func ExecAdd(plugin string, netconf []byte) (*Result, error) {
+       if os.Getenv("CNI_COMMAND") != "ADD" {
+               return nil, fmt.Errorf("CNI_COMMAND is not ADD")
+       }
+
+       pluginPath := Find(plugin)
+       if pluginPath == "" {
+               return nil, fmt.Errorf("could not find %q plugin", plugin)
+       }
+
+       stdout := &bytes.Buffer{}
+
+       c := exec.Cmd{
+               Path:   pluginPath,
+               Args:   []string{pluginPath},
+               Stdin:  bytes.NewBuffer(netconf),
+               Stdout: stdout,
+               Stderr: os.Stderr,
+       }
+       if err := c.Run(); err != nil {
+               return nil, err
+       }
+
+       res := &Result{}
+       err := json.Unmarshal(stdout.Bytes(), res)
+       return res, err
+}
+
+// ExecDel executes IPAM plugin, assuming CNI_COMMAND == DEL.
+func ExecDel(plugin string, netconf []byte) error {
+       if os.Getenv("CNI_COMMAND") != "DEL" {
+               return fmt.Errorf("CNI_COMMAND is not DEL")
+       }
+
+       pluginPath := Find(plugin)
+       if pluginPath == "" {
+               return fmt.Errorf("could not find %q plugin", plugin)
+       }
+
+       c := exec.Cmd{
+               Path:   pluginPath,
+               Args:   []string{pluginPath},
+               Stdin:  bytes.NewBuffer(netconf),
+               Stderr: os.Stderr,
+       }
+       return c.Run()
+}
+
+// ConfigureIface takes the result of IPAM plugin and
+// applies to the ifName interface
+func ConfigureIface(ifName string, res *Result) error {
+       link, err := netlink.LinkByName(ifName)
+       if err != nil {
+               return fmt.Errorf("failed to lookup %q: %v", ifName, err)
+       }
+
+       if err := netlink.LinkSetUp(link); err != nil {
+               return fmt.Errorf("failed too set %q UP: %v", ifName, err)
+       }
+
+       // TODO(eyakubovich): IPv6
+       addr := &netlink.Addr{IPNet: &res.IP4.IP, Label: ""}
+       if err = netlink.AddrAdd(link, addr); err != nil {
+               return fmt.Errorf("failed to add IP addr to %q: %v", ifName, err)
+       }
+
+       for _, r := range res.IP4.Routes {
+               gw := r.GW
+               if gw == nil {
+                       gw = res.IP4.Gateway
+               }
+               if err = ip.AddRoute(&r.Dst, gw, link); err != nil {
+                       // we skip over duplicate routes as we assume the first one wins
+                       if !os.IsExist(err) {
+                               return fmt.Errorf("failed to add route '%v via %v dev %v': %v", r.Dst, gw, ifName, err)
+                       }
+               }
+       }
+
+       return nil
+}
+
+// PrintResult writes out prettified Result to stdout
+func PrintResult(res *Result) error {
+       data, err := json.MarshalIndent(res, "", "    ")
+       if err != nil {
+               return err
+       }
+       _, err = os.Stdout.Write(data)
+       return err
+}
diff --git a/plugin/types.go b/plugin/types.go
new file mode 100644 (file)
index 0000000..6eb6ac2
--- /dev/null
@@ -0,0 +1,106 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// 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 plugin
+
+import (
+       "encoding/json"
+       "net"
+
+       "github.com/appc/cni/pkg/ip"
+)
+
+// NetConf describes a network.
+type NetConf struct {
+       Name string `json:"name,omitempty"`
+       Type string `json:"type,omitempty"`
+       IPAM struct {
+               Type string `json:"type,omitempty"`
+       } `json:"ipam,omitempty"`
+}
+
+// Result is what gets returned from the plugin (via stdout) to the caller
+type Result struct {
+       IP4 *IPConfig `json:"ip4,omitempty"`
+       IP6 *IPConfig `json:"ip6,omitempty"`
+}
+
+// IPConfig contains values necessary to configure an interface
+type IPConfig struct {
+       IP      net.IPNet
+       Gateway net.IP
+       Routes  []Route
+}
+
+type Route struct {
+       Dst net.IPNet
+       GW  net.IP
+}
+
+// net.IPNet is not JSON (un)marshallable so this duality is needed
+// for our custom ip.IPNet type
+
+// JSON (un)marshallable types
+type ipConfig struct {
+       IP      ip.IPNet `json:"ip"`
+       Gateway net.IP   `json:"gateway,omitempty"`
+       Routes  []Route  `json:"routes,omitempty"`
+}
+
+type route struct {
+       Dst ip.IPNet `json:"dst"`
+       GW  net.IP   `json:"gw,omitempty"`
+}
+
+func (c *IPConfig) MarshalJSON() ([]byte, error) {
+       ipc := ipConfig{
+               IP:      ip.IPNet(c.IP),
+               Gateway: c.Gateway,
+               Routes:  c.Routes,
+       }
+
+       return json.Marshal(ipc)
+}
+
+func (c *IPConfig) UnmarshalJSON(data []byte) error {
+       ipc := ipConfig{}
+       if err := json.Unmarshal(data, &ipc); err != nil {
+               return err
+       }
+
+       c.IP = net.IPNet(ipc.IP)
+       c.Gateway = ipc.Gateway
+       c.Routes = ipc.Routes
+       return nil
+}
+
+func (r *Route) UnmarshalJSON(data []byte) error {
+       rt := route{}
+       if err := json.Unmarshal(data, &rt); err != nil {
+               return err
+       }
+
+       r.Dst = net.IPNet(rt.Dst)
+       r.GW = rt.GW
+       return nil
+}
+
+func (r *Route) MarshalJSON() ([]byte, error) {
+       rt := route{
+               Dst: ip.IPNet(r.Dst),
+               GW:  r.GW,
+       }
+
+       return json.Marshal(rt)
+}
diff --git a/skel/skel.go b/skel/skel.go
new file mode 100644 (file)
index 0000000..9f03335
--- /dev/null
@@ -0,0 +1,98 @@
+// Copyright 2014 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package skel provides skeleton code for a CNI plugin.
+// In particular, it implements argument parsing and validation.
+package skel
+
+import (
+       "io/ioutil"
+       "log"
+       "os"
+)
+
+// CmdArgs captures all the arguments passed in to the plugin
+// via both env vars and stdin
+type CmdArgs struct {
+       ContainerID string
+       Netns       string
+       IfName      string
+       Args        string
+       Path        string
+       StdinData   []byte
+}
+
+// PluginMain is the "main" for a plugin. It accepts
+// two callback functions for add and del commands.
+func PluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error) {
+       var cmd, contID, netns, ifName, args, path string
+
+       vars := []struct {
+               name string
+               val  *string
+               req  bool
+       }{
+               {"CNI_COMMAND", &cmd, true},
+               {"CNI_CONTAINERID", &contID, false},
+               {"CNI_NETNS", &netns, true},
+               {"CNI_IFNAME", &ifName, true},
+               {"CNI_ARGS", &args, false},
+               {"CNI_PATH", &path, true},
+       }
+
+       argsMissing := false
+       for _, v := range vars {
+               *v.val = os.Getenv(v.name)
+               if v.req && *v.val == "" {
+                       log.Printf("%v env variable missing", v.name)
+                       argsMissing = true
+               }
+       }
+
+       if argsMissing {
+               os.Exit(1)
+       }
+
+       stdinData, err := ioutil.ReadAll(os.Stdin)
+       if err != nil {
+               log.Printf("Error reading from stdin: %v", err)
+               os.Exit(1)
+       }
+
+       cmdArgs := &CmdArgs{
+               ContainerID: contID,
+               Netns:       netns,
+               IfName:      ifName,
+               Args:        args,
+               Path:        path,
+               StdinData:   stdinData,
+       }
+
+       switch cmd {
+       case "ADD":
+               err = cmdAdd(cmdArgs)
+
+       case "DEL":
+               err = cmdDel(cmdArgs)
+
+       default:
+               log.Printf("Unknown CNI_COMMAND: %v", cmd)
+               os.Exit(1)
+       }
+
+       if err != nil {
+               log.Printf("%v: %v", cmd, err)
+               os.Exit(1)
+       }
+}