Factor an API out into a module
authorMichael Bridgen <mikeb@squaremobius.net>
Fri, 7 Aug 2015 15:27:52 +0000 (16:27 +0100)
committerMichael Bridgen <mikeb@squaremobius.net>
Wed, 16 Sep 2015 09:14:39 +0000 (10:14 +0100)
This takes some of the machinery from CNI and from the rkt networking
code, and turns it into a library that can be linked into go apps.

Included is an example command-line application that uses the library,
called `cnitool`.

Other headline changes:

 * Plugin exec'ing is factored out

The motivation here is to factor out the protocol for invoking
plugins. To that end, a generalisation of the code from api.go and
pkg/plugin/ipam.go goes into pkg/invoke/exec.go.

 * Move argument-handling and conf-loading into public API

The fact that the arguments get turned into an environment for the
plugin is incidental to the API; so, provide a way of supplying them
as a struct or saying "just use the same arguments as I got" (the
latter is for IPAM plugins).

invoke/args.go [new file with mode: 0644]
invoke/exec.go [new file with mode: 0644]
invoke/find.go [new file with mode: 0644]
ip/cidr.go
ipam/ipam.go [new file with mode: 0644]
plugin/ipam.go [deleted file]
skel/skel.go
types/args.go [moved from plugin/args.go with 56% similarity]
types/types.go [moved from plugin/types.go with 73% similarity]

diff --git a/invoke/args.go b/invoke/args.go
new file mode 100644 (file)
index 0000000..6f0a813
--- /dev/null
@@ -0,0 +1,76 @@
+// 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 invoke
+
+import (
+       "os"
+       "strings"
+)
+
+type CNIArgs interface {
+       // For use with os/exec; i.e., return nil to inherit the
+       // environment from this process
+       AsEnv() []string
+}
+
+type inherited struct{}
+
+var inheritArgsFromEnv inherited
+
+func (_ *inherited) AsEnv() []string {
+       return nil
+}
+
+func ArgsFromEnv() CNIArgs {
+       return &inheritArgsFromEnv
+}
+
+type Args struct {
+       Command       string
+       ContainerID   string
+       NetNS         string
+       PluginArgs    [][2]string
+       PluginArgsStr string
+       IfName        string
+       Path          string
+}
+
+func (args *Args) AsEnv() []string {
+       env := os.Environ()
+       pluginArgsStr := args.PluginArgsStr
+       if pluginArgsStr == "" {
+               pluginArgsStr = stringify(args.PluginArgs)
+       }
+
+       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 env
+}
+
+// taken from rkt/networking/net_plugin.go
+func stringify(pluginArgs [][2]string) string {
+       entries := make([]string, len(pluginArgs))
+
+       for i, kv := range pluginArgs {
+               entries[i] = strings.Join(kv[:], "=")
+       }
+
+       return strings.Join(entries, ";")
+}
diff --git a/invoke/exec.go b/invoke/exec.go
new file mode 100644 (file)
index 0000000..d7c5b7a
--- /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 invoke
+
+import (
+       "bytes"
+       "encoding/json"
+       "fmt"
+       "os"
+       "os/exec"
+       "path/filepath"
+
+       "github.com/appc/cni/pkg/types"
+)
+
+func pluginErr(err error, output []byte) error {
+       if _, ok := err.(*exec.ExitError); ok {
+               emsg := types.Error{}
+               if perr := json.Unmarshal(output, &emsg); perr != nil {
+                       return fmt.Errorf("netplugin failed but error parsing its diagnostic message %q: %v", string(output), perr)
+               }
+               details := ""
+               if emsg.Details != "" {
+                       details = fmt.Sprintf("; %v", emsg.Details)
+               }
+               return fmt.Errorf("%v%v", emsg.Msg, details)
+       }
+
+       return err
+}
+
+func ExecPlugin(pluginPath string, netconf []byte, args CNIArgs) (*types.Result, error) {
+       if pluginPath == "" {
+               return nil, fmt.Errorf("could not find %q plugin", filepath.Base(pluginPath))
+       }
+
+       stdout := &bytes.Buffer{}
+
+       c := exec.Cmd{
+               Env:    args.AsEnv(),
+               Path:   pluginPath,
+               Args:   []string{pluginPath},
+               Stdin:  bytes.NewBuffer(netconf),
+               Stdout: stdout,
+               Stderr: os.Stderr,
+       }
+       if err := c.Run(); err != nil {
+               return nil, pluginErr(err, stdout.Bytes())
+       }
+
+       res := &types.Result{}
+       err := json.Unmarshal(stdout.Bytes(), res)
+       return res, err
+}
diff --git a/invoke/find.go b/invoke/find.go
new file mode 100644 (file)
index 0000000..dfad12b
--- /dev/null
@@ -0,0 +1,37 @@
+// 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 invoke
+
+import (
+       "os"
+       "path/filepath"
+       "strings"
+)
+
+func FindInPath(plugin string, path []string) string {
+       for _, p := range path {
+               fullname := filepath.Join(p, plugin)
+               if fi, err := os.Stat(fullname); err == nil && fi.Mode().IsRegular() {
+                       return fullname
+               }
+       }
+       return ""
+}
+
+// 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"), ":")
+       return FindInPath(plugin, paths)
+}
index c963398..723a1f7 100644 (file)
 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)
@@ -62,25 +49,3 @@ func Network(ipn *net.IPNet) *net.IPNet {
                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/ipam/ipam.go b/ipam/ipam.go
new file mode 100644 (file)
index 0000000..a76299d
--- /dev/null
@@ -0,0 +1,75 @@
+// 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 ipam
+
+import (
+       "fmt"
+       "os"
+
+       "github.com/appc/cni/pkg/invoke"
+       "github.com/appc/cni/pkg/ip"
+       "github.com/appc/cni/pkg/types"
+
+       "github.com/vishvananda/netlink"
+)
+
+func ExecAdd(plugin string, netconf []byte) (*types.Result, error) {
+       if os.Getenv("CNI_COMMAND") != "ADD" {
+               return nil, fmt.Errorf("CNI_COMMAND is not ADD")
+       }
+       return invoke.ExecPlugin(invoke.Find(plugin), netconf, invoke.ArgsFromEnv())
+}
+
+func ExecDel(plugin string, netconf []byte) error {
+       if os.Getenv("CNI_COMMAND") != "DEL" {
+               return fmt.Errorf("CNI_COMMAND is not DEL")
+       }
+       _, err := invoke.ExecPlugin(invoke.Find(plugin), netconf, invoke.ArgsFromEnv())
+       return err
+}
+
+// ConfigureIface takes the result of IPAM plugin and
+// applies to the ifName interface
+func ConfigureIface(ifName string, res *types.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 to 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
+}
diff --git a/plugin/ipam.go b/plugin/ipam.go
deleted file mode 100644 (file)
index f304301..0000000
+++ /dev/null
@@ -1,151 +0,0 @@
-// 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/pkg/ip"
-       "github.com/vishvananda/netlink"
-)
-
-// 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 ""
-}
-
-func pluginErr(err error, output []byte) error {
-       if _, ok := err.(*exec.ExitError); ok {
-               emsg := Error{}
-               if perr := json.Unmarshal(output, &emsg); perr != nil {
-                       return fmt.Errorf("netplugin failed but error parsing its diagnostic message %q: %v", string(output), perr)
-               }
-               details := ""
-               if emsg.Details != "" {
-                       details = fmt.Sprintf("; %v", emsg.Details)
-               }
-               return fmt.Errorf("%v%v", emsg.Msg, details)
-       }
-
-       return err
-}
-
-// 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")
-       }
-       if plugin == "" {
-               return nil, fmt.Errorf(`name of IPAM plugin is missing. Please specify a "type" field in the "ipam" section`)
-       }
-
-       pluginPath := Find(plugin)
-       if pluginPath == "" {
-               return nil, fmt.Errorf("could not find %q IPAM 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, pluginErr(err, stdout.Bytes())
-       }
-
-       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)
-       }
-
-       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 pluginErr(err, stdout.Bytes())
-       }
-       return nil
-}
-
-// 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 to 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
-}
index bf79b91..d6204dd 100644 (file)
@@ -22,7 +22,7 @@ import (
        "log"
        "os"
 
-       "github.com/appc/cni/pkg/plugin"
+       "github.com/appc/cni/pkg/types"
 )
 
 // CmdArgs captures all the arguments passed in to the plugin
@@ -93,7 +93,7 @@ func PluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error) {
        }
 
        if err != nil {
-               if e, ok := err.(*plugin.Error); ok {
+               if e, ok := err.(*types.Error); ok {
                        // don't wrap Error in Error
                        dieErr(e)
                }
@@ -102,14 +102,14 @@ func PluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error) {
 }
 
 func dieMsg(f string, args ...interface{}) {
-       e := &plugin.Error{
+       e := &types.Error{
                Code: 100,
                Msg:  fmt.Sprintf(f, args...),
        }
        dieErr(e)
 }
 
-func dieErr(e *plugin.Error) {
+func dieErr(e *types.Error) {
        if err := e.Print(); err != nil {
                log.Print("Error writing error JSON to stdout: ", err)
        }
similarity index 56%
rename from plugin/args.go
rename to types/args.go
index 274ec66..6816243 100644 (file)
@@ -1,4 +1,18 @@
-package plugin
+// 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 types
 
 import (
        "encoding"
similarity index 73%
rename from plugin/types.go
rename to types/types.go
index d5952dd..21ba32d 100644 (file)
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package plugin
+package types
 
 import (
        "encoding/json"
        "net"
        "os"
-
-       "github.com/appc/cni/pkg/ip"
 )
 
+// like net.IPNet but adds JSON marshalling and unmarshalling
+type IPNet net.IPNet
+
+// 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
+}
+
+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
+}
+
 // NetConf describes a network.
 type NetConf struct {
        Name string `json:"name,omitempty"`
@@ -68,23 +100,23 @@ func (e *Error) Print() error {
 }
 
 // net.IPNet is not JSON (un)marshallable so this duality is needed
-// for our custom ip.IPNet type
+// for our custom 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"`
+       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"`
+       Dst IPNet  `json:"dst"`
+       GW  net.IP `json:"gw,omitempty"`
 }
 
 func (c *IPConfig) MarshalJSON() ([]byte, error) {
        ipc := ipConfig{
-               IP:      ip.IPNet(c.IP),
+               IP:      IPNet(c.IP),
                Gateway: c.Gateway,
                Routes:  c.Routes,
        }
@@ -117,7 +149,7 @@ func (r *Route) UnmarshalJSON(data []byte) error {
 
 func (r *Route) MarshalJSON() ([]byte, error) {
        rt := route{
-               Dst: ip.IPNet(r.Dst),
+               Dst: IPNet(r.Dst),
                GW:  r.GW,
        }