From cb4cd0e12ce960e860bebf9ac4a13b68908039e9 Mon Sep 17 00:00:00 2001 From: Dan Williams Date: Tue, 22 Nov 2016 10:02:39 -0600 Subject: [PATCH 1/6] testutils: pass netConf in for version operations; pass raw result out for tests --- pkg/testutils/cmd.go | 12 ++++++------ plugins/ipam/host-local/host_local_test.go | 4 ++-- plugins/main/bridge/bridge_test.go | 2 +- plugins/main/ipvlan/ipvlan_test.go | 2 +- plugins/main/macvlan/macvlan_test.go | 2 +- plugins/main/ptp/ptp_test.go | 2 +- plugins/meta/flannel/flannel_test.go | 2 +- test | 2 +- 8 files changed, 14 insertions(+), 14 deletions(-) diff --git a/pkg/testutils/cmd.go b/pkg/testutils/cmd.go index 201b935f..0118f61c 100644 --- a/pkg/testutils/cmd.go +++ b/pkg/testutils/cmd.go @@ -29,7 +29,7 @@ func envCleanup() { os.Unsetenv("CNI_IFNAME") } -func CmdAddWithResult(cniNetns, cniIfname string, f func() error) (*types.Result, error) { +func CmdAddWithResult(cniNetns, cniIfname string, conf []byte, f func() error) (*types.Result, []byte, error) { os.Setenv("CNI_COMMAND", "ADD") os.Setenv("CNI_PATH", os.Getenv("PATH")) os.Setenv("CNI_NETNS", cniNetns) @@ -40,30 +40,30 @@ func CmdAddWithResult(cniNetns, cniIfname string, f func() error) (*types.Result oldStdout := os.Stdout r, w, err := os.Pipe() if err != nil { - return nil, err + return nil, nil, err } os.Stdout = w err = f() w.Close() if err != nil { - return nil, err + return nil, nil, err } // parse the result out, err := ioutil.ReadAll(r) os.Stdout = oldStdout if err != nil { - return nil, err + return nil, nil, err } result := types.Result{} err = json.Unmarshal(out, &result) if err != nil { - return nil, err + return nil, nil, err } - return &result, nil + return &result, out, nil } func CmdDelWithResult(cniNetns, cniIfname string, f func() error) error { diff --git a/plugins/ipam/host-local/host_local_test.go b/plugins/ipam/host-local/host_local_test.go index 01906bb9..2aca1f23 100644 --- a/plugins/ipam/host-local/host_local_test.go +++ b/plugins/ipam/host-local/host_local_test.go @@ -62,7 +62,7 @@ var _ = Describe("host-local Operations", func() { } // Allocate the IP - result, err := testutils.CmdAddWithResult(nspath, ifname, func() error { + result, _, err := testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error { return cmdAdd(args) }) Expect(err).NotTo(HaveOccurred()) @@ -124,7 +124,7 @@ var _ = Describe("host-local Operations", func() { } // Allocate the IP - result, err := testutils.CmdAddWithResult(nspath, ifname, func() error { + result, _, err := testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error { return cmdAdd(args) }) Expect(err).NotTo(HaveOccurred()) diff --git a/plugins/main/bridge/bridge_test.go b/plugins/main/bridge/bridge_test.go index 3840aa91..870ad6b4 100644 --- a/plugins/main/bridge/bridge_test.go +++ b/plugins/main/bridge/bridge_test.go @@ -154,7 +154,7 @@ var _ = Describe("bridge Operations", func() { err = originalNS.Do(func(ns.NetNS) error { defer GinkgoRecover() - _, err := testutils.CmdAddWithResult(targetNs.Path(), IFNAME, func() error { + _, _, err := testutils.CmdAddWithResult(targetNs.Path(), IFNAME, []byte(conf), func() error { return cmdAdd(args) }) Expect(err).NotTo(HaveOccurred()) diff --git a/plugins/main/ipvlan/ipvlan_test.go b/plugins/main/ipvlan/ipvlan_test.go index 4e09ceba..d9c97644 100644 --- a/plugins/main/ipvlan/ipvlan_test.go +++ b/plugins/main/ipvlan/ipvlan_test.go @@ -126,7 +126,7 @@ var _ = Describe("ipvlan Operations", func() { err = originalNS.Do(func(ns.NetNS) error { defer GinkgoRecover() - _, err := testutils.CmdAddWithResult(targetNs.Path(), IFNAME, func() error { + _, _, err := testutils.CmdAddWithResult(targetNs.Path(), IFNAME, []byte(conf), func() error { return cmdAdd(args) }) Expect(err).NotTo(HaveOccurred()) diff --git a/plugins/main/macvlan/macvlan_test.go b/plugins/main/macvlan/macvlan_test.go index 9594a59b..ead07009 100644 --- a/plugins/main/macvlan/macvlan_test.go +++ b/plugins/main/macvlan/macvlan_test.go @@ -127,7 +127,7 @@ var _ = Describe("macvlan Operations", func() { err = originalNS.Do(func(ns.NetNS) error { defer GinkgoRecover() - _, err := testutils.CmdAddWithResult(targetNs.Path(), IFNAME, func() error { + _, _, err := testutils.CmdAddWithResult(targetNs.Path(), IFNAME, []byte(conf), func() error { return cmdAdd(args) }) Expect(err).NotTo(HaveOccurred()) diff --git a/plugins/main/ptp/ptp_test.go b/plugins/main/ptp/ptp_test.go index 5b85670d..4587d275 100644 --- a/plugins/main/ptp/ptp_test.go +++ b/plugins/main/ptp/ptp_test.go @@ -69,7 +69,7 @@ var _ = Describe("ptp Operations", func() { err = originalNS.Do(func(ns.NetNS) error { defer GinkgoRecover() - _, err := testutils.CmdAddWithResult(targetNs.Path(), IFNAME, func() error { + _, _, err := testutils.CmdAddWithResult(targetNs.Path(), IFNAME, []byte(conf), func() error { return cmdAdd(args) }) Expect(err).NotTo(HaveOccurred()) diff --git a/plugins/meta/flannel/flannel_test.go b/plugins/meta/flannel/flannel_test.go index 4434f913..2bda2168 100644 --- a/plugins/meta/flannel/flannel_test.go +++ b/plugins/meta/flannel/flannel_test.go @@ -102,7 +102,7 @@ FLANNEL_IPMASQ=true defer GinkgoRecover() By("calling ADD") - _, err := testutils.CmdAddWithResult(targetNs.Path(), IFNAME, func() error { + _, _, err := testutils.CmdAddWithResult(targetNs.Path(), IFNAME, []byte(input), func() error { return cmdAdd(args) }) Expect(err).NotTo(HaveOccurred()) diff --git a/test b/test index cb10c4db..673b08d0 100755 --- a/test +++ b/test @@ -11,7 +11,7 @@ set -e source ./build -TESTABLE="libcni plugins/ipam/dhcp plugins/ipam/dhcp plugins/ipam/host-local/backend/allocator plugins/ipam/host-local/backend plugins/main/loopback pkg/invoke pkg/ns pkg/skel pkg/types pkg/utils plugins/main/ipvlan plugins/main/macvlan plugins/main/bridge plugins/main/ptp plugins/test/noop pkg/utils/hwaddr pkg/ip pkg/version pkg/version/testhelpers plugins/meta/flannel" +TESTABLE="libcni plugins/ipam/dhcp plugins/ipam/host-local plugins/ipam/host-local/backend/allocator plugins/main/loopback pkg/invoke pkg/ns pkg/skel pkg/types pkg/utils plugins/main/ipvlan plugins/main/macvlan plugins/main/bridge plugins/main/ptp plugins/test/noop pkg/utils/hwaddr pkg/ip pkg/version pkg/version/testhelpers plugins/meta/flannel" FORMATTABLE="$TESTABLE pkg/testutils plugins/meta/flannel plugins/meta/tuning" # user has not provided PKG override From befb95977ce2eb4a75aaff01f1356ef56beb271e Mon Sep 17 00:00:00 2001 From: Dan Williams Date: Wed, 9 Nov 2016 15:11:18 -0600 Subject: [PATCH 2/6] types: make Result an interface and move existing Result to separate package --- libcni/api.go | 12 +- libcni/api_test.go | 19 ++- pkg/invoke/delegate.go | 2 +- pkg/invoke/exec.go | 16 +- pkg/invoke/exec_test.go | 6 +- pkg/ipam/ipam.go | 5 +- pkg/testutils/cmd.go | 16 +- pkg/types/current/types.go | 157 ++++++++++++++++++ pkg/types/current/types_suite_test.go | 27 +++ pkg/types/current/types_test.go | 128 ++++++++++++++ pkg/types/types.go | 85 +++------- pkg/version/legacy_examples/examples.go | 5 +- pkg/version/reconcile.go | 20 ++- pkg/version/reconcile_test.go | 4 +- pkg/version/version.go | 29 ++++ plugins/ipam/dhcp/daemon.go | 5 +- plugins/ipam/dhcp/main.go | 6 +- .../host-local/backend/allocator/allocator.go | 8 +- .../backend/allocator/allocator_test.go | 3 +- plugins/ipam/host-local/host_local_test.go | 11 +- plugins/ipam/host-local/main.go | 7 +- plugins/main/bridge/bridge.go | 8 +- plugins/main/ipvlan/ipvlan.go | 8 +- plugins/main/loopback/loopback.go | 4 +- plugins/main/macvlan/macvlan.go | 8 +- plugins/main/ptp/ptp.go | 15 +- plugins/meta/tuning/tuning.go | 3 +- plugins/test/noop/main.go | 12 +- test | 2 +- 29 files changed, 500 insertions(+), 131 deletions(-) create mode 100644 pkg/types/current/types.go create mode 100644 pkg/types/current/types_suite_test.go create mode 100644 pkg/types/current/types_test.go diff --git a/libcni/api.go b/libcni/api.go index 9a82dc34..3bbe46bd 100644 --- a/libcni/api.go +++ b/libcni/api.go @@ -42,10 +42,10 @@ type NetworkConfigList struct { } type CNI interface { - AddNetworkList(net *NetworkConfigList, rt *RuntimeConf) (*types.Result, error) + AddNetworkList(net *NetworkConfigList, rt *RuntimeConf) (types.Result, error) DelNetworkList(net *NetworkConfigList, rt *RuntimeConf) error - AddNetwork(net *NetworkConfig, rt *RuntimeConf) (*types.Result, error) + AddNetwork(net *NetworkConfig, rt *RuntimeConf) (types.Result, error) DelNetwork(net *NetworkConfig, rt *RuntimeConf) error } @@ -56,7 +56,7 @@ type CNIConfig struct { // CNIConfig implements the CNI interface var _ CNI = &CNIConfig{} -func buildOneConfig(list *NetworkConfigList, orig *NetworkConfig, prevResult *types.Result) (*NetworkConfig, error) { +func buildOneConfig(list *NetworkConfigList, orig *NetworkConfig, prevResult types.Result) (*NetworkConfig, error) { var err error // Ensure every config uses the same name and version @@ -81,8 +81,8 @@ func buildOneConfig(list *NetworkConfigList, orig *NetworkConfig, prevResult *ty } // AddNetworkList executes a sequence of plugins with the ADD command -func (c *CNIConfig) AddNetworkList(list *NetworkConfigList, rt *RuntimeConf) (*types.Result, error) { - var prevResult *types.Result +func (c *CNIConfig) AddNetworkList(list *NetworkConfigList, rt *RuntimeConf) (types.Result, error) { + var prevResult types.Result for _, net := range list.Plugins { pluginPath, err := invoke.FindInPath(net.Network.Type, c.Path) if err != nil { @@ -127,7 +127,7 @@ func (c *CNIConfig) DelNetworkList(list *NetworkConfigList, rt *RuntimeConf) err } // AddNetwork executes the plugin with the ADD command -func (c *CNIConfig) AddNetwork(net *NetworkConfig, rt *RuntimeConf) (*types.Result, error) { +func (c *CNIConfig) AddNetwork(net *NetworkConfig, rt *RuntimeConf) (types.Result, error) { pluginPath, err := invoke.FindInPath(net.Network.Type, c.Path) if err != nil { return nil, err diff --git a/libcni/api_test.go b/libcni/api_test.go index bc9f06d6..ce1070fe 100644 --- a/libcni/api_test.go +++ b/libcni/api_test.go @@ -24,6 +24,7 @@ import ( "github.com/containernetworking/cni/libcni" "github.com/containernetworking/cni/pkg/skel" "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/types/current" noop_debug "github.com/containernetworking/cni/plugins/test/noop/debug" . "github.com/onsi/ginkgo" @@ -116,11 +117,14 @@ var _ = Describe("Invoking plugins", func() { Describe("AddNetwork", func() { It("executes the plugin with command ADD", func() { - result, err := cniConfig.AddNetwork(netConfig, runtimeConfig) + r, err := cniConfig.AddNetwork(netConfig, runtimeConfig) Expect(err).NotTo(HaveOccurred()) - Expect(result).To(Equal(&types.Result{ - IP4: &types.IPConfig{ + result, err := current.GetResult(r) + Expect(err).NotTo(HaveOccurred()) + + Expect(result).To(Equal(¤t.Result{ + IP4: ¤t.IPConfig{ IP: net.IPNet{ IP: net.ParseIP("10.1.2.3"), Mask: net.IPv4Mask(255, 255, 255, 0), @@ -263,12 +267,15 @@ var _ = Describe("Invoking plugins", func() { Describe("AddNetworkList", func() { It("executes all plugins with command ADD and returns an intermediate result", func() { - result, err := cniConfig.AddNetworkList(netConfigList, runtimeConfig) + r, err := cniConfig.AddNetworkList(netConfigList, runtimeConfig) Expect(err).NotTo(HaveOccurred()) - Expect(result).To(Equal(&types.Result{ + result, err := current.GetResult(r) + Expect(err).NotTo(HaveOccurred()) + + Expect(result).To(Equal(¤t.Result{ // IP4 added by first plugin - IP4: &types.IPConfig{ + IP4: ¤t.IPConfig{ IP: net.IPNet{ IP: net.ParseIP("10.1.2.3"), Mask: net.IPv4Mask(255, 255, 255, 0), diff --git a/pkg/invoke/delegate.go b/pkg/invoke/delegate.go index ddf1d172..f25beddc 100644 --- a/pkg/invoke/delegate.go +++ b/pkg/invoke/delegate.go @@ -22,7 +22,7 @@ import ( "github.com/containernetworking/cni/pkg/types" ) -func DelegateAdd(delegatePlugin string, netconf []byte) (*types.Result, error) { +func DelegateAdd(delegatePlugin string, netconf []byte) (types.Result, error) { if os.Getenv("CNI_COMMAND") != "ADD" { return nil, fmt.Errorf("CNI_COMMAND is not ADD") } diff --git a/pkg/invoke/exec.go b/pkg/invoke/exec.go index 167d38f5..fc47e7c8 100644 --- a/pkg/invoke/exec.go +++ b/pkg/invoke/exec.go @@ -15,7 +15,6 @@ package invoke import ( - "encoding/json" "fmt" "os" @@ -23,7 +22,7 @@ import ( "github.com/containernetworking/cni/pkg/version" ) -func ExecPluginWithResult(pluginPath string, netconf []byte, args CNIArgs) (*types.Result, error) { +func ExecPluginWithResult(pluginPath string, netconf []byte, args CNIArgs) (types.Result, error) { return defaultPluginExec.WithResult(pluginPath, netconf, args) } @@ -49,15 +48,20 @@ type PluginExec struct { } } -func (e *PluginExec) WithResult(pluginPath string, netconf []byte, args CNIArgs) (*types.Result, error) { +func (e *PluginExec) WithResult(pluginPath string, netconf []byte, args CNIArgs) (types.Result, error) { stdoutBytes, err := e.RawExec.ExecPlugin(pluginPath, netconf, args.AsEnv()) if err != nil { return nil, err } - res := &types.Result{} - err = json.Unmarshal(stdoutBytes, res) - return res, err + // Plugin must return result in same version as specified in netconf + versionDecoder := &version.ConfigDecoder{} + confVersion, err := versionDecoder.Decode(netconf) + if err != nil { + return nil, err + } + + return version.NewResult(confVersion, stdoutBytes) } func (e *PluginExec) WithoutResult(pluginPath string, netconf []byte, args CNIArgs) error { diff --git a/pkg/invoke/exec_test.go b/pkg/invoke/exec_test.go index 2b9c9bf9..3e207c14 100644 --- a/pkg/invoke/exec_test.go +++ b/pkg/invoke/exec_test.go @@ -20,6 +20,7 @@ import ( "github.com/containernetworking/cni/pkg/invoke" "github.com/containernetworking/cni/pkg/invoke/fakes" + "github.com/containernetworking/cni/pkg/types/current" "github.com/containernetworking/cni/pkg/version" . "github.com/onsi/ginkgo" @@ -56,7 +57,10 @@ var _ = Describe("Executing a plugin, unit tests", func() { Describe("returning a result", func() { It("unmarshals the result bytes into the Result type", func() { - result, err := pluginExec.WithResult(pluginPath, netconf, cniargs) + r, err := pluginExec.WithResult(pluginPath, netconf, cniargs) + Expect(err).NotTo(HaveOccurred()) + + result, err := current.GetResult(r) Expect(err).NotTo(HaveOccurred()) Expect(result.IP4.IP.IP.String()).To(Equal("1.2.3.4")) }) diff --git a/pkg/ipam/ipam.go b/pkg/ipam/ipam.go index d9fbff74..8dd861a6 100644 --- a/pkg/ipam/ipam.go +++ b/pkg/ipam/ipam.go @@ -21,11 +21,12 @@ import ( "github.com/containernetworking/cni/pkg/invoke" "github.com/containernetworking/cni/pkg/ip" "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/types/current" "github.com/vishvananda/netlink" ) -func ExecAdd(plugin string, netconf []byte) (*types.Result, error) { +func ExecAdd(plugin string, netconf []byte) (types.Result, error) { return invoke.DelegateAdd(plugin, netconf) } @@ -35,7 +36,7 @@ func ExecDel(plugin string, netconf []byte) error { // ConfigureIface takes the result of IPAM plugin and // applies to the ifName interface -func ConfigureIface(ifName string, res *types.Result) error { +func ConfigureIface(ifName string, res *current.Result) error { link, err := netlink.LinkByName(ifName) if err != nil { return fmt.Errorf("failed to lookup %q: %v", ifName, err) diff --git a/pkg/testutils/cmd.go b/pkg/testutils/cmd.go index 0118f61c..5883c08f 100644 --- a/pkg/testutils/cmd.go +++ b/pkg/testutils/cmd.go @@ -15,11 +15,11 @@ package testutils import ( - "encoding/json" "io/ioutil" "os" "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/version" ) func envCleanup() { @@ -29,7 +29,7 @@ func envCleanup() { os.Unsetenv("CNI_IFNAME") } -func CmdAddWithResult(cniNetns, cniIfname string, conf []byte, f func() error) (*types.Result, []byte, error) { +func CmdAddWithResult(cniNetns, cniIfname string, conf []byte, f func() error) (types.Result, []byte, error) { os.Setenv("CNI_COMMAND", "ADD") os.Setenv("CNI_PATH", os.Getenv("PATH")) os.Setenv("CNI_NETNS", cniNetns) @@ -57,13 +57,19 @@ func CmdAddWithResult(cniNetns, cniIfname string, conf []byte, f func() error) ( return nil, nil, err } - result := types.Result{} - err = json.Unmarshal(out, &result) + // Plugin must return result in same version as specified in netconf + versionDecoder := &version.ConfigDecoder{} + confVersion, err := versionDecoder.Decode(conf) if err != nil { return nil, nil, err } - return &result, out, nil + result, err := version.NewResult(confVersion, out) + if err != nil { + return nil, nil, err + } + + return result, out, nil } func CmdDelWithResult(cniNetns, cniIfname string, f func() error) error { diff --git a/pkg/types/current/types.go b/pkg/types/current/types.go new file mode 100644 index 00000000..338b3fd2 --- /dev/null +++ b/pkg/types/current/types.go @@ -0,0 +1,157 @@ +// 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" +) + +const implementedSpecVersion string = "0.2.0" + +var SupportedVersions = []string{"", "0.1.0", 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) { + newResult, err := r.GetAsVersion(implementedSpecVersion) + if err != nil { + return nil, err + } + result, ok := newResult.(*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) +}{ + {SupportedVersions, convertFrom020}, +} + +func convertFrom020(result types.Result) (*Result, error) { + newResult, ok := result.(*Result) + if !ok { + return nil, fmt.Errorf("failed to convert result") + } + 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 result version %q", version) +} + +// 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"` + DNS types.DNS `json:"dns,omitempty"` +} + +func (r *Result) Version() string { + return implementedSpecVersion +} + +func (r *Result) GetAsVersion(version string) (types.Result, error) { + for _, supportedVersion := range SupportedVersions { + if version == supportedVersion { + return r, nil + } + } + return nil, fmt.Errorf("cannot convert version %q to %s", SupportedVersions, 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 "[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 + Gateway net.IP + Routes []types.Route +} + +// net.IPNet is not JSON (un)marshallable so this duality is needed +// for our custom IPNet type + +// JSON (un)marshallable types +type ipConfig struct { + IP types.IPNet `json:"ip"` + Gateway net.IP `json:"gateway,omitempty"` + Routes []types.Route `json:"routes,omitempty"` +} + +func (c *IPConfig) MarshalJSON() ([]byte, error) { + ipc := ipConfig{ + IP: types.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 +} diff --git a/pkg/types/current/types_suite_test.go b/pkg/types/current/types_suite_test.go new file mode 100644 index 00000000..42a47a25 --- /dev/null +++ b/pkg/types/current/types_suite_test.go @@ -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 current_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestTypes010(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "0.1.0 Types Suite") +} diff --git a/pkg/types/current/types_test.go b/pkg/types/current/types_test.go new file mode 100644 index 00000000..3810999d --- /dev/null +++ b/pkg/types/current/types_test.go @@ -0,0 +1,128 @@ +// 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_test + +import ( + "io/ioutil" + "net" + "os" + + "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/types/current" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Ensures compatibility with the 0.1.0 spec", func() { + It("correctly encodes a 0.1.0 Result", 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 := current.Result{ + IP4: ¤t.IPConfig{ + IP: *ipv4, + Gateway: net.ParseIP("1.2.3.1"), + Routes: []types.Route{ + {Dst: *routev4, GW: routegwv4}, + }, + }, + IP6: ¤t.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"}, + }, + } + + 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(`{ + "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" + ] + } +}`)) + }) +}) diff --git a/pkg/types/types.go b/pkg/types/types.go index c1fddcd5..2ceffebc 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -16,7 +16,6 @@ package types import ( "encoding/json" - "fmt" "net" "os" ) @@ -59,10 +58,9 @@ func (n *IPNet) UnmarshalJSON(data []byte) error { type NetConf struct { CNIVersion string `json:"cniVersion,omitempty"` - Name string `json:"name,omitempty"` - Type string `json:"type,omitempty"` - PrevResult *Result `json:"prevResult,omitempty"` - IPAM struct { + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` + IPAM struct { Type string `json:"type,omitempty"` } `json:"ipam,omitempty"` DNS DNS `json:"dns"` @@ -76,36 +74,31 @@ type NetConfList struct { Plugins []*NetConf `json:"plugins,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"` - DNS DNS `json:"dns,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 + // without having to convert + Version() string + + // Returns the result converted into the requested CNI specification + // result version, or an error if conversion failed + GetAsVersion(version string) (Result, error) + + // Prints the result in JSON format to stdout + Print() error + + // Returns a JSON string representation of the result + String() string } -func (r *Result) Print() error { - return prettyPrint(r) -} - -// 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) +func PrintResult(result Result, version string) error { + newResult, err := result.GetAsVersion(version) + if err != nil { + return err } - 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 - Gateway net.IP - Routes []Route + return newResult.Print() } // DNS contains values interesting for DNS resolvers @@ -147,39 +140,11 @@ func (e *Error) Print() error { // for our custom IPNet type // JSON (un)marshallable types -type ipConfig struct { - IP IPNet `json:"ip"` - Gateway net.IP `json:"gateway,omitempty"` - Routes []Route `json:"routes,omitempty"` -} - type route struct { Dst IPNet `json:"dst"` GW net.IP `json:"gw,omitempty"` } -func (c *IPConfig) MarshalJSON() ([]byte, error) { - ipc := ipConfig{ - 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 { diff --git a/pkg/version/legacy_examples/examples.go b/pkg/version/legacy_examples/examples.go index 57162312..8b079a3d 100644 --- a/pkg/version/legacy_examples/examples.go +++ b/pkg/version/legacy_examples/examples.go @@ -23,6 +23,7 @@ import ( "sync" "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/types/current" "github.com/containernetworking/cni/pkg/version/testhelpers" ) @@ -114,8 +115,8 @@ 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 = &types.Result{ - IP4: &types.IPConfig{ +var ExpectedResult = ¤t.Result{ + IP4: ¤t.IPConfig{ IP: net.IPNet{ IP: net.ParseIP("10.1.2.3"), Mask: net.CIDRMask(24, 32), diff --git a/pkg/version/reconcile.go b/pkg/version/reconcile.go index f61ef653..25c3810b 100644 --- a/pkg/version/reconcile.go +++ b/pkg/version/reconcile.go @@ -17,12 +17,12 @@ package version import "fmt" type ErrorIncompatible struct { - Config string - Plugin []string + Config string + Supported []string } func (e *ErrorIncompatible) Details() string { - return fmt.Sprintf("config is %q, plugin supports %q", e.Config, e.Plugin) + return fmt.Sprintf("config is %q, plugin supports %q", e.Config, e.Supported) } func (e *ErrorIncompatible) Error() string { @@ -31,17 +31,19 @@ func (e *ErrorIncompatible) Error() string { type Reconciler struct{} -func (*Reconciler) Check(configVersion string, pluginInfo PluginInfo) *ErrorIncompatible { - pluginVersions := pluginInfo.SupportedVersions() +func (r *Reconciler) Check(configVersion string, pluginInfo PluginInfo) *ErrorIncompatible { + return r.CheckRaw(configVersion, pluginInfo.SupportedVersions()) +} - for _, pluginVersion := range pluginVersions { - if configVersion == pluginVersion { +func (*Reconciler) CheckRaw(configVersion string, supportedVersions []string) *ErrorIncompatible { + for _, supportedVersion := range supportedVersions { + if configVersion == supportedVersion { return nil } } return &ErrorIncompatible{ - Config: configVersion, - Plugin: pluginVersions, + Config: configVersion, + Supported: supportedVersions, } } diff --git a/pkg/version/reconcile_test.go b/pkg/version/reconcile_test.go index 19a9e23f..0c964cea 100644 --- a/pkg/version/reconcile_test.go +++ b/pkg/version/reconcile_test.go @@ -41,8 +41,8 @@ var _ = Describe("Reconcile versions of net config with versions supported by pl err := reconciler.Check("0.1.0", pluginInfo) Expect(err).To(Equal(&version.ErrorIncompatible{ - Config: "0.1.0", - Plugin: []string{"1.2.3", "4.3.2"}, + Config: "0.1.0", + Supported: []string{"1.2.3", "4.3.2"}, })) Expect(err.Error()).To(Equal(`incompatible CNI versions: config is "0.1.0", plugin supports ["1.2.3" "4.3.2"]`)) diff --git a/pkg/version/version.go b/pkg/version/version.go index e39c3b55..e777e52c 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -14,6 +14,13 @@ package version +import ( + "fmt" + + "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/types/current" +) + // Current reports the version of the CNI spec implemented by this library func Current() string { return "0.2.0" @@ -27,3 +34,25 @@ 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 resultFactories = []struct { + supportedVersions []string + newResult types.ResultFactoryFunc +}{ + {current.SupportedVersions, current.NewResult}, +} + +// 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 nil, fmt.Errorf("unsupported CNI result version %q", version) +} diff --git a/plugins/ipam/dhcp/daemon.go b/plugins/ipam/dhcp/daemon.go index 2386f9a9..2910a41b 100644 --- a/plugins/ipam/dhcp/daemon.go +++ b/plugins/ipam/dhcp/daemon.go @@ -29,6 +29,7 @@ import ( "github.com/containernetworking/cni/pkg/skel" "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/types/current" "github.com/coreos/go-systemd/activation" ) @@ -50,7 +51,7 @@ func newDHCP() *DHCP { // Allocate acquires an IP from a DHCP server for a specified container. // The acquired lease will be maintained until Release() is called. -func (d *DHCP) Allocate(args *skel.CmdArgs, result *types.Result) error { +func (d *DHCP) Allocate(args *skel.CmdArgs, result *current.Result) error { conf := types.NetConf{} if err := json.Unmarshal(args.StdinData, &conf); err != nil { return fmt.Errorf("error parsing netconf: %v", err) @@ -70,7 +71,7 @@ func (d *DHCP) Allocate(args *skel.CmdArgs, result *types.Result) error { d.setLease(args.ContainerID, conf.Name, l) - result.IP4 = &types.IPConfig{ + result.IP4 = ¤t.IPConfig{ IP: *ipn, Gateway: l.Gateway(), Routes: l.Routes(), diff --git a/plugins/ipam/dhcp/main.go b/plugins/ipam/dhcp/main.go index 0e46af91..f43ad3ed 100644 --- a/plugins/ipam/dhcp/main.go +++ b/plugins/ipam/dhcp/main.go @@ -21,7 +21,7 @@ import ( "path/filepath" "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" ) @@ -36,8 +36,8 @@ func main() { } func cmdAdd(args *skel.CmdArgs) error { - result := types.Result{} - if err := rpcCall("DHCP.Allocate", args, &result); err != nil { + result := ¤t.Result{} + if err := rpcCall("DHCP.Allocate", args, result); err != nil { return err } return result.Print() diff --git a/plugins/ipam/host-local/backend/allocator/allocator.go b/plugins/ipam/host-local/backend/allocator/allocator.go index 6e18172b..a6f2c650 100644 --- a/plugins/ipam/host-local/backend/allocator/allocator.go +++ b/plugins/ipam/host-local/backend/allocator/allocator.go @@ -20,7 +20,7 @@ import ( "net" "github.com/containernetworking/cni/pkg/ip" - "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/types/current" "github.com/containernetworking/cni/plugins/ipam/host-local/backend" ) @@ -129,7 +129,7 @@ func validateRangeIP(ip net.IP, ipnet *net.IPNet, start net.IP, end net.IP) erro } // Returns newly allocated IP along with its config -func (a *IPAllocator) Get(id string) (*types.IPConfig, error) { +func (a *IPAllocator) Get(id string) (*current.IPConfig, error) { a.store.Lock() defer a.store.Unlock() @@ -163,7 +163,7 @@ func (a *IPAllocator) Get(id string) (*types.IPConfig, error) { } if reserved { - return &types.IPConfig{ + return ¤t.IPConfig{ IP: net.IPNet{IP: requestedIP, Mask: a.conf.Subnet.Mask}, Gateway: gw, Routes: a.conf.Routes, @@ -184,7 +184,7 @@ func (a *IPAllocator) Get(id string) (*types.IPConfig, error) { return nil, err } if reserved { - return &types.IPConfig{ + return ¤t.IPConfig{ IP: net.IPNet{IP: cur, Mask: a.conf.Subnet.Mask}, Gateway: gw, Routes: a.conf.Routes, diff --git a/plugins/ipam/host-local/backend/allocator/allocator_test.go b/plugins/ipam/host-local/backend/allocator/allocator_test.go index 44a60ba0..24fa2771 100644 --- a/plugins/ipam/host-local/backend/allocator/allocator_test.go +++ b/plugins/ipam/host-local/backend/allocator/allocator_test.go @@ -17,6 +17,7 @@ package allocator import ( "fmt" "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/types/current" fakestore "github.com/containernetworking/cni/plugins/ipam/host-local/backend/testing" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -30,7 +31,7 @@ type AllocatorTestCase struct { lastIP string } -func (t AllocatorTestCase) run() (*types.IPConfig, error) { +func (t AllocatorTestCase) run() (*current.IPConfig, error) { subnet, err := types.ParseCIDR(t.subnet) if err != nil { return nil, err diff --git a/plugins/ipam/host-local/host_local_test.go b/plugins/ipam/host-local/host_local_test.go index 2aca1f23..a8742fad 100644 --- a/plugins/ipam/host-local/host_local_test.go +++ b/plugins/ipam/host-local/host_local_test.go @@ -24,6 +24,7 @@ import ( "github.com/containernetworking/cni/pkg/skel" "github.com/containernetworking/cni/pkg/testutils" "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/types/current" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -62,11 +63,14 @@ var _ = Describe("host-local Operations", func() { } // Allocate the IP - result, _, err := testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error { + r, _, err := testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error { return cmdAdd(args) }) Expect(err).NotTo(HaveOccurred()) + result, err := current.GetResult(r) + Expect(err).NotTo(HaveOccurred()) + expectedAddress, err := types.ParseCIDR("10.1.2.2/24") Expect(err).NotTo(HaveOccurred()) expectedAddress.IP = expectedAddress.IP.To16() @@ -124,11 +128,14 @@ var _ = Describe("host-local Operations", func() { } // Allocate the IP - result, _, err := testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error { + r, _, err := testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error { return cmdAdd(args) }) Expect(err).NotTo(HaveOccurred()) + result, err := current.GetResult(r) + Expect(err).NotTo(HaveOccurred()) + ipFilePath := filepath.Join(tmpDir, "mynet", result.IP4.IP.IP.String()) contents, err := ioutil.ReadFile(ipFilePath) Expect(err).NotTo(HaveOccurred()) diff --git a/plugins/ipam/host-local/main.go b/plugins/ipam/host-local/main.go index 287c0a09..a5dd080a 100644 --- a/plugins/ipam/host-local/main.go +++ b/plugins/ipam/host-local/main.go @@ -19,7 +19,7 @@ import ( "github.com/containernetworking/cni/plugins/ipam/host-local/backend/disk" "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" ) @@ -33,7 +33,7 @@ func cmdAdd(args *skel.CmdArgs) error { return err } - r := types.Result{} + r := ¤t.Result{} if ipamConf.ResolvConf != "" { dns, err := parseResolvConf(ipamConf.ResolvConf) @@ -54,11 +54,10 @@ func cmdAdd(args *skel.CmdArgs) error { return err } - ipConf, err := allocator.Get(args.ContainerID) + r.IP4, err = allocator.Get(args.ContainerID) if err != nil { return err } - r.IP4 = ipConf return r.Print() } diff --git a/plugins/main/bridge/bridge.go b/plugins/main/bridge/bridge.go index bc173017..a2f8b17c 100644 --- a/plugins/main/bridge/bridge.go +++ b/plugins/main/bridge/bridge.go @@ -27,6 +27,7 @@ import ( "github.com/containernetworking/cni/pkg/ns" "github.com/containernetworking/cni/pkg/skel" "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/types/current" "github.com/containernetworking/cni/pkg/utils" "github.com/containernetworking/cni/pkg/version" "github.com/vishvananda/netlink" @@ -234,7 +235,12 @@ func cmdAdd(args *skel.CmdArgs) error { } // run the IPAM plugin and get back the config to apply - result, err := ipam.ExecAdd(n.IPAM.Type, args.StdinData) + r, err := ipam.ExecAdd(n.IPAM.Type, args.StdinData) + if err != nil { + return err + } + + result, err := current.GetResult(r) if err != nil { return err } diff --git a/plugins/main/ipvlan/ipvlan.go b/plugins/main/ipvlan/ipvlan.go index a86a781b..c7316bbe 100644 --- a/plugins/main/ipvlan/ipvlan.go +++ b/plugins/main/ipvlan/ipvlan.go @@ -25,6 +25,7 @@ import ( "github.com/containernetworking/cni/pkg/ns" "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/vishvananda/netlink" ) @@ -125,10 +126,15 @@ func cmdAdd(args *skel.CmdArgs) error { } // run the IPAM plugin and get back the config to apply - result, err := ipam.ExecAdd(n.IPAM.Type, args.StdinData) + r, err := ipam.ExecAdd(n.IPAM.Type, args.StdinData) if err != nil { return err } + result, err := current.GetResult(r) + if err != nil { + return err + } + if result.IP4 == nil { return errors.New("IPAM plugin returned missing IPv4 config") } diff --git a/plugins/main/loopback/loopback.go b/plugins/main/loopback/loopback.go index 1344c130..d2b69f99 100644 --- a/plugins/main/loopback/loopback.go +++ b/plugins/main/loopback/loopback.go @@ -17,7 +17,7 @@ package main import ( "github.com/containernetworking/cni/pkg/ns" "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/vishvananda/netlink" ) @@ -41,7 +41,7 @@ func cmdAdd(args *skel.CmdArgs) error { return err // not tested } - result := types.Result{} + result := current.Result{} return result.Print() } diff --git a/plugins/main/macvlan/macvlan.go b/plugins/main/macvlan/macvlan.go index ef012696..8e2adeb7 100644 --- a/plugins/main/macvlan/macvlan.go +++ b/plugins/main/macvlan/macvlan.go @@ -25,6 +25,7 @@ import ( "github.com/containernetworking/cni/pkg/ns" "github.com/containernetworking/cni/pkg/skel" "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/types/current" "github.com/containernetworking/cni/pkg/utils/sysctl" "github.com/containernetworking/cni/pkg/version" "github.com/vishvananda/netlink" @@ -141,10 +142,15 @@ func cmdAdd(args *skel.CmdArgs) error { } // run the IPAM plugin and get back the config to apply - result, err := ipam.ExecAdd(n.IPAM.Type, args.StdinData) + r, err := ipam.ExecAdd(n.IPAM.Type, args.StdinData) if err != nil { return err } + result, err := current.GetResult(r) + if err != nil { + return err + } + if result.IP4 == nil { return errors.New("IPAM plugin returned missing IPv4 config") } diff --git a/plugins/main/ptp/ptp.go b/plugins/main/ptp/ptp.go index a26b09ee..efda0bea 100644 --- a/plugins/main/ptp/ptp.go +++ b/plugins/main/ptp/ptp.go @@ -29,6 +29,7 @@ import ( "github.com/containernetworking/cni/pkg/ns" "github.com/containernetworking/cni/pkg/skel" "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/types/current" "github.com/containernetworking/cni/pkg/utils" "github.com/containernetworking/cni/pkg/version" ) @@ -46,7 +47,7 @@ type NetConf struct { MTU int `json:"mtu"` } -func setupContainerVeth(netns, ifName string, mtu int, pr *types.Result) (string, error) { +func setupContainerVeth(netns, ifName string, mtu int, pr *current.Result) (string, error) { // The IPAM result will be something like IP=192.168.3.5/24, GW=192.168.3.1. // What we want is really a point-to-point link but veth does not support IFF_POINTOPONT. // Next best thing would be to let it ARP but set interface to 192.168.3.5/32 and @@ -102,7 +103,7 @@ func setupContainerVeth(netns, ifName string, mtu int, pr *types.Result) (string } for _, r := range []netlink.Route{ - netlink.Route{ + { LinkIndex: contVeth.Attrs().Index, Dst: &net.IPNet{ IP: pr.IP4.Gateway, @@ -111,7 +112,7 @@ func setupContainerVeth(netns, ifName string, mtu int, pr *types.Result) (string Scope: netlink.SCOPE_LINK, Src: pr.IP4.IP.IP, }, - netlink.Route{ + { LinkIndex: contVeth.Attrs().Index, Dst: &net.IPNet{ IP: pr.IP4.IP.IP.Mask(pr.IP4.IP.Mask), @@ -132,7 +133,7 @@ func setupContainerVeth(netns, ifName string, mtu int, pr *types.Result) (string return hostVethName, err } -func setupHostVeth(vethName string, ipConf *types.IPConfig) error { +func setupHostVeth(vethName string, ipConf *current.IPConfig) error { // hostVeth moved namespaces and may have a new ifindex veth, err := netlink.LinkByName(vethName) if err != nil { @@ -172,7 +173,11 @@ func cmdAdd(args *skel.CmdArgs) error { } // run the IPAM plugin and get back the config to apply - result, err := ipam.ExecAdd(conf.IPAM.Type, args.StdinData) + r, err := ipam.ExecAdd(conf.IPAM.Type, args.StdinData) + if err != nil { + return err + } + result, err := current.GetResult(r) if err != nil { return err } diff --git a/plugins/meta/tuning/tuning.go b/plugins/meta/tuning/tuning.go index 98e92ec9..b1e7b0f8 100644 --- a/plugins/meta/tuning/tuning.go +++ b/plugins/meta/tuning/tuning.go @@ -27,6 +27,7 @@ import ( "github.com/containernetworking/cni/pkg/ns" "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" ) @@ -67,7 +68,7 @@ func cmdAdd(args *skel.CmdArgs) error { return err } - result := types.Result{} + result := current.Result{} return result.Print() } diff --git a/plugins/test/noop/main.go b/plugins/test/noop/main.go index 89370ef5..924bb9dc 100644 --- a/plugins/test/noop/main.go +++ b/plugins/test/noop/main.go @@ -31,14 +31,15 @@ import ( "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" noop_debug "github.com/containernetworking/cni/plugins/test/noop/debug" ) type NetConf struct { types.NetConf - DebugFile string `json:"debugFile"` - PrevResult *types.Result `json:"prevResult,omitempty"` + DebugFile string `json:"debugFile"` + PrevResult *current.Result `json:"prevResult,omitempty"` } func loadConf(bytes []byte) (*NetConf, error) { @@ -121,7 +122,12 @@ func debugBehavior(args *skel.CmdArgs, command string) error { return errors.New(debug.ReportError) } else if debug.ReportResult == "PASSTHROUGH" || debug.ReportResult == "INJECT-DNS" { if debug.ReportResult == "INJECT-DNS" { - netConf.PrevResult.DNS.Nameservers = []string{"1.2.3.4"} + newResult, err := current.NewResultFromResult(netConf.PrevResult) + if err != nil { + return err + } + newResult.DNS.Nameservers = []string{"1.2.3.4"} + netConf.PrevResult = newResult } newResult, err := json.Marshal(netConf.PrevResult) if err != nil { diff --git a/test b/test index 673b08d0..511997aa 100755 --- a/test +++ b/test @@ -11,7 +11,7 @@ set -e source ./build -TESTABLE="libcni plugins/ipam/dhcp plugins/ipam/host-local plugins/ipam/host-local/backend/allocator plugins/main/loopback pkg/invoke pkg/ns pkg/skel pkg/types pkg/utils plugins/main/ipvlan plugins/main/macvlan plugins/main/bridge plugins/main/ptp plugins/test/noop pkg/utils/hwaddr pkg/ip pkg/version pkg/version/testhelpers plugins/meta/flannel" +TESTABLE="libcni plugins/ipam/dhcp plugins/ipam/host-local plugins/ipam/host-local/backend/allocator plugins/main/loopback pkg/invoke pkg/ns pkg/skel pkg/types pkg/types/current pkg/utils plugins/main/ipvlan plugins/main/macvlan plugins/main/bridge plugins/main/ptp plugins/test/noop pkg/utils/hwaddr pkg/ip pkg/version pkg/version/testhelpers plugins/meta/flannel" FORMATTABLE="$TESTABLE pkg/testutils plugins/meta/flannel plugins/meta/tuning" # user has not provided PKG override From ad2a5ccb61703af63d45dd8578295bf7119c85fc Mon Sep 17 00:00:00 2001 From: Dan Williams Date: Mon, 28 Nov 2016 09:33:39 -0600 Subject: [PATCH 3/6] macvlan/ipvlan: use common RenameLink method --- pkg/ip/link.go | 8 ++++++++ plugins/main/ipvlan/ipvlan.go | 11 +---------- plugins/main/macvlan/macvlan.go | 11 +---------- 3 files changed, 10 insertions(+), 20 deletions(-) diff --git a/pkg/ip/link.go b/pkg/ip/link.go index 43b37390..6431bb41 100644 --- a/pkg/ip/link.go +++ b/pkg/ip/link.go @@ -90,6 +90,14 @@ func RandomVethName() (string, error) { return fmt.Sprintf("veth%x", entropy), nil } +func RenameLink(curName, newName string) error { + link, err := netlink.LinkByName(curName) + if err == nil { + err = netlink.LinkSetName(link, newName) + } + return err +} + // SetupVeth sets up a virtual ethernet link. // Should be in container netns, and will switch back to hostNS to set the host // veth end up. diff --git a/plugins/main/ipvlan/ipvlan.go b/plugins/main/ipvlan/ipvlan.go index c7316bbe..a0f47705 100644 --- a/plugins/main/ipvlan/ipvlan.go +++ b/plugins/main/ipvlan/ipvlan.go @@ -101,7 +101,7 @@ func createIpvlan(conf *NetConf, ifName string, netns ns.NetNS) error { } return netns.Do(func(_ ns.NetNS) error { - err := renameLink(tmpName, ifName) + err := ip.RenameLink(tmpName, ifName) if err != nil { return fmt.Errorf("failed to rename ipvlan to %q: %v", ifName, err) } @@ -170,15 +170,6 @@ func cmdDel(args *skel.CmdArgs) error { }) } -func renameLink(curName, newName string) error { - link, err := netlink.LinkByName(curName) - if err != nil { - return err - } - - return netlink.LinkSetName(link, newName) -} - func main() { skel.PluginMain(cmdAdd, cmdDel, version.Legacy) } diff --git a/plugins/main/macvlan/macvlan.go b/plugins/main/macvlan/macvlan.go index 8e2adeb7..52c48700 100644 --- a/plugins/main/macvlan/macvlan.go +++ b/plugins/main/macvlan/macvlan.go @@ -116,7 +116,7 @@ func createMacvlan(conf *NetConf, ifName string, netns ns.NetNS) error { return fmt.Errorf("failed to set proxy_arp on newly added interface %q: %v", tmpName, err) } - err := renameLink(tmpName, ifName) + err := ip.RenameLink(tmpName, ifName) if err != nil { _ = netlink.LinkDel(mv) return fmt.Errorf("failed to rename macvlan to %q: %v", ifName, err) @@ -190,15 +190,6 @@ func cmdDel(args *skel.CmdArgs) error { }) } -func renameLink(curName, newName string) error { - link, err := netlink.LinkByName(curName) - if err != nil { - return err - } - - return netlink.LinkSetName(link, newName) -} - func main() { skel.PluginMain(cmdAdd, cmdDel, version.Legacy) } From b0b896f79a7742a9e141cde584733d3a01d53725 Mon Sep 17 00:00:00 2001 From: Dan Williams Date: Wed, 18 Jan 2017 16:39:57 -0600 Subject: [PATCH 4/6] plugins/flannel: organize test JSON alphabetically Otherwise the test fails, since Go's JSON marshaller prints dict items alphabetically in its String() call. --- plugins/meta/flannel/flannel_test.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/plugins/meta/flannel/flannel_test.go b/plugins/meta/flannel/flannel_test.go index 2bda2168..51fdc6fb 100644 --- a/plugins/meta/flannel/flannel_test.go +++ b/plugins/meta/flannel/flannel_test.go @@ -114,20 +114,20 @@ FLANNEL_IPMASQ=true netConfBytes, err := ioutil.ReadFile(path) Expect(err).NotTo(HaveOccurred()) expected := `{ - "name" : "cni-flannel", - "type" : "bridge", + "ipMasq" : false, "ipam" : { - "type" : "host-local", - "subnet" : "10.1.17.0/24", "routes" : [ { "dst" : "10.1.0.0/16" } - ] + ], + "subnet" : "10.1.17.0/24", + "type" : "host-local" }, + "isGateway": true, "mtu" : 1472, - "ipMasq" : false, - "isGateway": true + "name" : "cni-flannel", + "type" : "bridge" } ` Expect(netConfBytes).Should(MatchJSON(expected)) From befad17174b3a796ccd7ea8ec5b06fec6962fb14 Mon Sep 17 00:00:00 2001 From: Dan Williams Date: Thu, 19 Jan 2017 08:44:35 -0600 Subject: [PATCH 5/6] pkg/ipam: add testcases --- pkg/ipam/ipam_test.go | 196 ++++++++++++++++++++++++++++++++++++++++++ test | 2 +- 2 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 pkg/ipam/ipam_test.go diff --git a/pkg/ipam/ipam_test.go b/pkg/ipam/ipam_test.go new file mode 100644 index 00000000..622e4c8a --- /dev/null +++ b/pkg/ipam/ipam_test.go @@ -0,0 +1,196 @@ +// Copyright 2015 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 ipam + +import ( + "net" + "syscall" + + "github.com/containernetworking/cni/pkg/ns" + "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/types/current" + + "github.com/vishvananda/netlink" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +const LINK_NAME = "eth0" + +func ipNetEqual(a, b *net.IPNet) bool { + aPrefix, aBits := a.Mask.Size() + bPrefix, bBits := b.Mask.Size() + if aPrefix != bPrefix || aBits != bBits { + return false + } + return a.IP.Equal(b.IP) +} + +var _ = Describe("IPAM Operations", func() { + var originalNS ns.NetNS + var ipv4, ipv6, routev4, routev6 *net.IPNet + var ipgw4, ipgw6, routegwv4, routegwv6 net.IP + var result *current.Result + + BeforeEach(func() { + // Create a new NetNS so we don't modify the host + var err error + originalNS, err = ns.NewNS() + Expect(err).NotTo(HaveOccurred()) + + err = originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + // Add master + err = netlink.LinkAdd(&netlink.Dummy{ + LinkAttrs: netlink.LinkAttrs{ + Name: LINK_NAME, + }, + }) + Expect(err).NotTo(HaveOccurred()) + _, err = netlink.LinkByName(LINK_NAME) + Expect(err).NotTo(HaveOccurred()) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + ipv4, err = types.ParseCIDR("1.2.3.30/24") + Expect(err).NotTo(HaveOccurred()) + Expect(ipv4).NotTo(BeNil()) + + _, routev4, err = net.ParseCIDR("15.5.6.8/24") + Expect(err).NotTo(HaveOccurred()) + Expect(routev4).NotTo(BeNil()) + routegwv4 = net.ParseIP("1.2.3.5") + Expect(routegwv4).NotTo(BeNil()) + + ipgw4 = net.ParseIP("1.2.3.1") + Expect(ipgw4).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()) + routegwv6 = net.ParseIP("abcd:1234:ffff::10") + Expect(routegwv6).NotTo(BeNil()) + + ipgw6 = net.ParseIP("abcd:1234:ffff::1") + Expect(ipgw6).NotTo(BeNil()) + + result = ¤t.Result{ + IP4: ¤t.IPConfig{ + IP: *ipv4, + Gateway: ipgw4, + Routes: []types.Route{ + {Dst: *routev4, GW: routegwv4}, + }, + }, + IP6: ¤t.IPConfig{ + IP: *ipv6, + Gateway: ipgw6, + Routes: []types.Route{ + {Dst: *routev6, GW: routegwv6}, + }, + }, + } + }) + + AfterEach(func() { + Expect(originalNS.Close()).To(Succeed()) + }) + + It("configures a link with addresses and routes", func() { + err := originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + err := ConfigureIface(LINK_NAME, result) + Expect(err).NotTo(HaveOccurred()) + + link, err := netlink.LinkByName(LINK_NAME) + Expect(err).NotTo(HaveOccurred()) + Expect(link.Attrs().Name).To(Equal(LINK_NAME)) + + v4addrs, err := netlink.AddrList(link, syscall.AF_INET) + Expect(err).NotTo(HaveOccurred()) + Expect(len(v4addrs)).To(Equal(1)) + Expect(ipNetEqual(v4addrs[0].IPNet, ipv4)).To(Equal(true)) + + // Doesn't support IPv6 yet so only link-local address expected + v6addrs, err := netlink.AddrList(link, syscall.AF_INET6) + Expect(err).NotTo(HaveOccurred()) + Expect(len(v6addrs)).To(Equal(1)) + + // Ensure the v4 route + routes, err := netlink.RouteList(link, 0) + Expect(err).NotTo(HaveOccurred()) + + var v4found bool + for _, route := range routes { + isv4 := route.Dst.IP.To4() != nil + if isv4 && ipNetEqual(route.Dst, routev4) && route.Gw.Equal(routegwv4) { + v4found = true + break + } + } + Expect(v4found).To(Equal(true)) + + return nil + }) + Expect(err).NotTo(HaveOccurred()) + }) + + It("configures a link with routes using address gateways", func() { + result.IP4.Routes[0].GW = nil + result.IP6.Routes[0].GW = nil + err := originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + err := ConfigureIface(LINK_NAME, result) + Expect(err).NotTo(HaveOccurred()) + + link, err := netlink.LinkByName(LINK_NAME) + Expect(err).NotTo(HaveOccurred()) + Expect(link.Attrs().Name).To(Equal(LINK_NAME)) + + // Ensure the v4 route + routes, err := netlink.RouteList(link, 0) + Expect(err).NotTo(HaveOccurred()) + + var v4found bool + for _, route := range routes { + isv4 := route.Dst.IP.To4() != nil + if isv4 && ipNetEqual(route.Dst, routev4) && route.Gw.Equal(ipgw4) { + v4found = true + break + } + } + Expect(v4found).To(Equal(true)) + + return nil + }) + Expect(err).NotTo(HaveOccurred()) + }) + + It("returns an error when configuring the wrong interface", func() { + err := originalNS.Do(func(ns.NetNS) error { + return ConfigureIface("asdfasdf", result) + }) + Expect(err).To(HaveOccurred()) + }) +}) diff --git a/test b/test index 511997aa..d8a1fe37 100755 --- a/test +++ b/test @@ -11,7 +11,7 @@ set -e source ./build -TESTABLE="libcni plugins/ipam/dhcp plugins/ipam/host-local plugins/ipam/host-local/backend/allocator plugins/main/loopback pkg/invoke pkg/ns pkg/skel pkg/types pkg/types/current pkg/utils plugins/main/ipvlan plugins/main/macvlan plugins/main/bridge plugins/main/ptp plugins/test/noop pkg/utils/hwaddr pkg/ip pkg/version pkg/version/testhelpers plugins/meta/flannel" +TESTABLE="libcni plugins/ipam/dhcp plugins/ipam/host-local plugins/ipam/host-local/backend/allocator plugins/main/loopback pkg/invoke pkg/ns pkg/skel pkg/types pkg/types/current pkg/utils plugins/main/ipvlan plugins/main/macvlan plugins/main/bridge plugins/main/ptp plugins/test/noop pkg/utils/hwaddr pkg/ip pkg/version pkg/version/testhelpers plugins/meta/flannel pkg/ipam" FORMATTABLE="$TESTABLE pkg/testutils plugins/meta/flannel plugins/meta/tuning" # user has not provided PKG override From d5acb127b8c137e62a657541ce37ec8a345315e6 Mon Sep 17 00:00:00 2001 From: Dan Williams Date: Tue, 22 Nov 2016 11:32:35 -0600 Subject: [PATCH 6/6] spec/plugins: return interface details and multiple IP addresses to runtime Updates the spec and plugins to return an array of interfaces and IP details to the runtime including: - interface names and MAC addresses configured by the plugin - whether the interfaces are sandboxed (container/VM) or host (bridge, veth, etc) - multiple IP addresses configured by IPAM and which interface they have been assigned to Returning interface details is useful for runtimes, as well as allowing more flexible chaining of CNI plugins themselves. For example, some meta plugins may need to know the host-side interface to be able to apply firewall or traffic shaping rules to the container. --- SPEC.md | 195 ++++++++++++----- libcni/api_test.go | 70 +++--- pkg/invoke/exec_test.go | 7 +- pkg/invoke/raw_exec_test.go | 2 +- pkg/ipam/ipam.go | 36 +++- pkg/ipam/ipam_test.go | 98 +++++++-- pkg/skel/skel_test.go | 4 +- pkg/types/020/types.go | 133 ++++++++++++ pkg/types/020/types_suite_test.go | 27 +++ pkg/types/020/types_test.go | 128 +++++++++++ pkg/types/current/types.go | 202 ++++++++++++++--- pkg/types/current/types_suite_test.go | 2 +- pkg/types/current/types_test.go | 160 ++++++++++---- pkg/types/types.go | 5 + pkg/version/legacy_examples/examples.go | 6 +- pkg/version/version.go | 5 +- plugins/ipam/dhcp/daemon.go | 9 +- plugins/ipam/dhcp/lease.go | 2 +- plugins/ipam/dhcp/main.go | 13 +- plugins/ipam/dhcp/options.go | 12 +- plugins/ipam/dhcp/options_test.go | 8 +- .../host-local/backend/allocator/allocator.go | 35 +-- .../backend/allocator/allocator_test.go | 46 ++-- .../host-local/backend/allocator/config.go | 26 ++- plugins/ipam/host-local/host_local_test.go | 74 ++++++- plugins/ipam/host-local/main.go | 17 +- plugins/main/bridge/bridge.go | 185 ++++++++++------ plugins/main/bridge/bridge_test.go | 204 +++++++++++++++--- plugins/main/ipvlan/ipvlan.go | 62 ++++-- plugins/main/ipvlan/ipvlan_test.go | 28 ++- plugins/main/loopback/loopback.go | 2 +- plugins/main/macvlan/macvlan.go | 89 ++++++-- plugins/main/macvlan/macvlan_test.go | 31 ++- plugins/main/ptp/ptp.go | 199 ++++++++++------- plugins/main/ptp/ptp_test.go | 2 +- plugins/meta/flannel/flannel.go | 2 +- plugins/meta/tuning/tuning.go | 2 +- plugins/test/noop/main.go | 2 +- plugins/test/noop/noop_test.go | 20 +- test | 2 +- 40 files changed, 1653 insertions(+), 499 deletions(-) create mode 100644 pkg/types/020/types.go create mode 100644 pkg/types/020/types_suite_test.go create mode 100644 pkg/types/020/types_test.go diff --git a/SPEC.md b/SPEC.md index 360b22c7..89af42f9 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,7 +1,7 @@ # Container Networking Interface Proposal ## Version -This is CNI **spec** version **0.2.0**. +This is CNI **spec** version **0.3.0**. Note that this is **independent from the version of the CNI library and plugins** in this repository (e.g. the versions of [releases](https://github.com/containernetworking/cni/releases)). @@ -57,7 +57,8 @@ The operations that the CNI plugin needs to support are: - **Extra arguments**. This provides an alternative mechanism to allow simple configuration of CNI plugins on a per-container basis. - **Name of the interface inside the container**. This is the name that should be assigned to the interface created inside the container (network namespace); consequently it must comply with the standard Linux restrictions on interface names. - Result: - - **IPs assigned to the interface**. This is either an IPv4 address, an IPv6 address, or both. + - **Interfaces list**. Depending on the plugin, this can include the sandbox (eg, container or hypervisor) interface name and/or the host interface name, the hardware addresses of each interface, and details about the sandbox (if any) the interface is in. + - **IP configuration assigned to each interface**. The IPv4 and/or IPv6 addresses, gateways, and routes assigned to sandbox and/or host interfaces. - **DNS information**. Dictionary that includes DNS information for nameservers, domain, search domains and options. - Delete container from network @@ -75,8 +76,8 @@ The operations that the CNI plugin needs to support are: ``` { - "cniVersion": "0.2.0", // the version of the CNI spec in use for this output - "supportedVersions": [ "0.1.0", "0.2.0" ] // the list of CNI spec versions that this plugin supports + "cniVersion": "0.3.0", // the version of the CNI spec in use for this output + "supportedVersions": [ "0.1.0", "0.2.0", "0.3.0" ] // the list of CNI spec versions that this plugin supports } ``` @@ -85,7 +86,7 @@ It will then look for this executable in a list of predefined directories. Once - `CNI_COMMAND`: indicates the desired operation; `ADD`, `DEL` or `VERSION`. - `CNI_CONTAINERID`: Container ID - `CNI_NETNS`: Path to network namespace file -- `CNI_IFNAME`: Interface name to set up +- `CNI_IFNAME`: Interface name to set up; plugin must honor this interface name or return an error - `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" - `CNI_PATH`: Colon-separated list of paths to search for CNI plugin executables @@ -94,33 +95,60 @@ Network configuration in JSON format is streamed to the plugin through stdin. Th ### Result -Success is indicated by a return code of zero and the following JSON printed to stdout in the case of the ADD command. This should be the same output as was returned by the IPAM plugin (see [IP Allocation](#ip-allocation) for details). +Note that IPAM plugins return an abbreviated `Result` structure as described in [IP Allocation](#ip-allocation). + +Success is indicated by a return code of zero and the following JSON printed to stdout in the case of the ADD command. The `ip` and `dns` items should be the same output as was returned by the IPAM plugin (see [IP Allocation](#ip-allocation) for details) except that the plugin should fill in the `interface` indexes appropriately, which are missing from IPAM plugin output since IPAM plugins should be unaware of interfaces. ``` { - "cniVersion": "0.2.0", - "ip4": { - "ip": , - "gateway": , (optional) - "routes": (optional) - }, - "ip6": { - "ip": , - "gateway": , (optional) - "routes": (optional) - }, + "cniVersion": "0.3.0", + "interfaces": [ (this key omitted by IPAM plugins) + { + "name": "", + "mac": "", (required if L2 addresses are meaningful) + "sandbox": "" (required for container/hypervisor interfaces, empty/omitted for host interfaces) + } + ], + "ip": [ + { + "version": "<4-or-6>", + "address": "", + "gateway": "", (optional) + "interface": + }, + ... + ], + "routes": [ (optional) + { + "dst": "", + "gw": "" (optional) + }, + ... + ] "dns": { - "nameservers": (optional) - "domain": (optional) - "search": (optional) - "options": (optional) + "nameservers": (optional) + "domain": (optional) + "search": (optional) + "options": (optional) } } ``` `cniVersion` specifies a [Semantic Version 2.0](http://semver.org) of CNI specification used by the plugin. -`dns` field contains a dictionary consisting of common DNS information that this network is aware of. -The result is returned in the same format as specified in the [configuration](#network-configuration). +`interfaces` describes specific network interfaces the plugin created. +If the `CNI_IFNAME` variable exists the plugin must use that name for the sandbox/hypervisor interface or return an error if it cannot. +- `mac` (string): the hardware address of the interface. + If L2 addresses are not meaningful for the plugin then this field is optional. +- `sandbox` (string): container/namespace-based environments should return the full filesystem path to the network namespace of that sandbox. + Hypervisor/VM-based plugins should return an ID unique to the virtualized sandbox the interface was created in. + This item must be provided for interfaces created or moved into a sandbox like a network namespace or a hypervisor/VM. + +The `ip` field is a list of IP configuration information. +See the [IP well-known structure](#ip) section for more information. + +The `dns` field contains a dictionary consisting of common DNS information. +See the [DNS well-known structure](#dns) section for more information. + The specification does not declare how this information must be processed by CNI consumers. Examples include generating an `/etc/resolv.conf` file to be injected into the container filesystem or running a DNS forwarder on the host. @@ -164,7 +192,7 @@ Plugins may define additional fields that they accept and may generate an error ```json { - "cniVersion": "0.2.0", + "cniVersion": "0.3.0", "name": "dbnet", "type": "bridge", // type (plugin) specific @@ -183,7 +211,7 @@ Plugins may define additional fields that they accept and may generate an error ```json { - "cniVersion": "0.2.0", + "cniVersion": "0.3.0", "name": "pci", "type": "ovs", // type (plugin) specific @@ -204,7 +232,7 @@ Plugins may define additional fields that they accept and may generate an error ```json { - "cniVersion": "0.1", + "cniVersion": "0.3.0", "name": "wan", "type": "macvlan", // ipam specific @@ -394,41 +422,40 @@ Success is indicated by a zero return code and the following JSON being printed ``` { - "cniVersion": "0.2.0", - "ip4": { - "ip": , - "gateway": , (optional) - "routes": (optional) - }, - "ip6": { - "ip": , - "gateway": , (optional) - "routes": (optional) - }, + "cniVersion": "0.3.0", + "ips": [ + { + "version": "<4-or-6>", + "address": "", + "gateway": "" (optional) + }, + ... + ], + "routes": [ (optional) + { + "dst": "", + "gw": "" (optional) + }, + ... + ] "dns": { - "nameservers": (optional) - "domain": (optional) - "search": (optional) - "options": (optional) + "nameservers": (optional) + "domain": (optional) + "search": (optional) + "options": (optional) } } ``` +Note that unlike regular CNI plugins, IPAM plugins return an abbreviated `Result` structure that does not include the `interfaces` key, since IPAM plugins should be unaware of interfaces configured by their parent plugin except those specifically required for IPAM (eg, like the `dhcp` IPAM plugin). + `cniVersion` specifies a [Semantic Version 2.0](http://semver.org) of CNI specification used by the plugin. -`gateway` is the default gateway for this subnet, if one exists. -It does not instruct the CNI plugin to add any routes with this gateway: routes to add are specified separately via the `routes` field. -An example use of this value is for the CNI plugin to add this IP address to the linux-bridge to make it a gateway. -Each route entry is a dictionary with the following fields: -- `dst` (string): Destination subnet specified in CIDR notation. -- `gw` (string): IP of the gateway. If omitted, a default gateway is assumed (as determined by the CNI plugin). +The `ips` field is a list of IP configuration information. +See the [IP well-known structure](#ip) section for more information. -The "dns" field contains a dictionary consisting of common DNS information. -- `nameservers` (list of strings): list of a priority-ordered list of DNS nameservers that this network is aware of. Each entry in the list is a string containing either an IPv4 or an IPv6 address. -- `domain` (string): the local domain used for short hostname lookups. -- `search` (list of strings): list of priority ordered search domains for short hostname lookups. Will be preferred over `domain` by most resolvers. -- `options` (list of strings): list of options that can be passed to the resolver -See [CNI Plugin Result](#result) section for more information. +The `dns` field contains a dictionary consisting of common DNS information. +See the [DNS well-known structure](#dns) section for more information. Errors and logs are communicated in the same way as the CNI plugin. See [CNI Plugin Result](#result) section for details. @@ -440,6 +467,68 @@ IPAM plugin examples: - Routes are expected to be added with a 0 metric. - A default route may be specified via "0.0.0.0/0". Since another network might have already configured the default route, the CNI plugin should be prepared to skip over its default route definition. +### Well-known Structures + +#### IP + +``` + "ips": [ + { + "version": "<4-or-6>", + "address": "", + "gateway": "", (optional) + "interface": (not required for IPAM plugins) + }, + ... + ] +``` + +The `ip` field is a list of IP configuration information determined by the plugin. Each item is a dictionary describing of IP configuration for a network interface. +IP configuration for multiple network interfaces and multiple IP configurations for a single interface may be returned as separate items in the `ip` list. +All properties known to the plugin should be provided, even if not strictly required. +- `version` (string): either "4" or "6" and corresponds to the IP version of the addresses in the entry. + All IP addresses and gateways provided must be valid for the given `version`. +- `address` (string): an IP address in CIDR notation (eg "192.168.1.3/24"). +- `gateway` (string): the default gateway for this subnet, if one exists. + It does not instruct the CNI plugin to add any routes with this gateway: routes to add are specified separately via the `routes` field. + An example use of this value is for the CNI `bridge` plugin to add this IP address to the Linux bridge to make it a gateway. +- `interface` (uint): the index into the `interfaces` list for a [CNI Plugin Result](#result) indicating which interface this IP configuration should be applied to. + IPAM plugins should not return this key since they have no information about network interfaces. + +#### Routes + +``` + "routes": [ + { + "dst": "", + "gw": "" (optional) + }, + ... + ] +``` + +- Each `routes` entry is a dictionary with the following fields. All IP addresses in the `routes` entry must be the same IP version, either 4 or 6. + - `dst` (string): destination subnet specified in CIDR notation. + - `gw` (string): IP of the gateway. If omitted, a default gateway is assumed (as determined by the CNI plugin). + +#### DNS + +``` + "dns": { + "nameservers": (optional) + "domain": (optional) + "search": (optional) + "options": (optional) + } +``` + +The `dns` field contains a dictionary consisting of common DNS information. +- `nameservers` (list of strings): list of a priority-ordered list of DNS nameservers that this network is aware of. Each entry in the list is a string containing either an IPv4 or an IPv6 address. +- `domain` (string): the local domain used for short hostname lookups. +- `search` (list of strings): list of priority ordered search domains for short hostname lookups. Will be preferred over `domain` by most resolvers. +- `options` (list of strings): list of options that can be passed to the resolver. + See [CNI Plugin Result](#result) section for more information. + ## Well-known Error Codes - `1` - Incompatible CNI version - `2` - Unsupported field in network configuration. The error message must contain the key and value of the unsupported field. diff --git a/libcni/api_test.go b/libcni/api_test.go index ce1070fe..e228c965 100644 --- a/libcni/api_test.go +++ b/libcni/api_test.go @@ -58,7 +58,7 @@ func newPluginInfo(configKey, configValue, prevResult string, injectDebugFilePat } Expect(debug.WriteDebug(debugFilePath)).To(Succeed()) - config := fmt.Sprintf(`{"type": "noop", "%s": "%s", "cniVersion": "0.2.0"`, configKey, configValue) + config := fmt.Sprintf(`{"type": "noop", "%s": "%s", "cniVersion": "0.3.0"`, configKey, configValue) if prevResult != "" { config += fmt.Sprintf(`, "prevResult": %s`, prevResult) } @@ -77,8 +77,10 @@ func newPluginInfo(configKey, configValue, prevResult string, injectDebugFilePat var _ = Describe("Invoking plugins", func() { Describe("Invoking a single plugin", func() { var ( - plugin pluginInfo + debugFilePath string + debug *noop_debug.Debug cniBinPath string + pluginConfig string cniConfig libcni.CNIConfig netConfig *libcni.NetworkConfig runtimeConfig *libcni.RuntimeConf @@ -87,31 +89,39 @@ var _ = Describe("Invoking plugins", func() { ) BeforeEach(func() { - pluginResult := `{ "ip4": { "ip": "10.1.2.3/24" }, "dns": {} }` - plugin = newPluginInfo("some-key", "some-value", "", false, pluginResult) + debugFile, err := ioutil.TempFile("", "cni_debug") + Expect(err).NotTo(HaveOccurred()) + Expect(debugFile.Close()).To(Succeed()) + debugFilePath = debugFile.Name() + + debug = &noop_debug.Debug{ + ReportResult: `{ "ips": [{ "version": "4", "address": "10.1.2.3/24" }], "dns": {} }`, + } + Expect(debug.WriteDebug(debugFilePath)).To(Succeed()) cniBinPath = filepath.Dir(pluginPaths["noop"]) + pluginConfig = `{ "type": "noop", "some-key": "some-value", "cniVersion": "0.3.0" }` cniConfig = libcni.CNIConfig{Path: []string{cniBinPath}} netConfig = &libcni.NetworkConfig{ Network: &types.NetConf{ Type: "noop", }, - Bytes: []byte(plugin.config), + Bytes: []byte(pluginConfig), } runtimeConfig = &libcni.RuntimeConf{ ContainerID: "some-container-id", NetNS: "/some/netns/path", IfName: "some-eth0", - Args: [][2]string{{"DEBUG", plugin.debugFilePath}}, + Args: [][2]string{[2]string{"DEBUG", debugFilePath}}, } expectedCmdArgs = skel.CmdArgs{ ContainerID: "some-container-id", Netns: "/some/netns/path", IfName: "some-eth0", - Args: "DEBUG=" + plugin.debugFilePath, + Args: "DEBUG=" + debugFilePath, Path: cniBinPath, - StdinData: []byte(plugin.config), + StdinData: []byte(pluginConfig), } }) @@ -124,15 +134,18 @@ var _ = Describe("Invoking plugins", func() { Expect(err).NotTo(HaveOccurred()) Expect(result).To(Equal(¤t.Result{ - IP4: ¤t.IPConfig{ - IP: net.IPNet{ - IP: net.ParseIP("10.1.2.3"), - Mask: net.IPv4Mask(255, 255, 255, 0), + IPs: []*current.IPConfig{ + { + Version: "4", + Address: net.IPNet{ + IP: net.ParseIP("10.1.2.3"), + Mask: net.IPv4Mask(255, 255, 255, 0), + }, }, }, })) - debug, err := noop_debug.ReadDebug(plugin.debugFilePath) + debug, err := noop_debug.ReadDebug(debugFilePath) Expect(err).NotTo(HaveOccurred()) Expect(debug.Command).To(Equal("ADD")) Expect(debug.CmdArgs).To(Equal(expectedCmdArgs)) @@ -151,8 +164,8 @@ var _ = Describe("Invoking plugins", func() { Context("when the plugin errors", func() { BeforeEach(func() { - plugin.debug.ReportError = "plugin error: banana" - Expect(plugin.debug.WriteDebug(plugin.debugFilePath)).To(Succeed()) + debug.ReportError = "plugin error: banana" + Expect(debug.WriteDebug(debugFilePath)).To(Succeed()) }) It("unmarshals and returns the error", func() { result, err := cniConfig.AddNetwork(netConfig, runtimeConfig) @@ -167,7 +180,7 @@ var _ = Describe("Invoking plugins", func() { err := cniConfig.DelNetwork(netConfig, runtimeConfig) Expect(err).NotTo(HaveOccurred()) - debug, err := noop_debug.ReadDebug(plugin.debugFilePath) + debug, err := noop_debug.ReadDebug(debugFilePath) Expect(err).NotTo(HaveOccurred()) Expect(debug.Command).To(Equal("DEL")) Expect(debug.CmdArgs).To(Equal(expectedCmdArgs)) @@ -186,8 +199,8 @@ var _ = Describe("Invoking plugins", func() { Context("when the plugin errors", func() { BeforeEach(func() { - plugin.debug.ReportError = "plugin error: banana" - Expect(plugin.debug.WriteDebug(plugin.debugFilePath)).To(Succeed()) + debug.ReportError = "plugin error: banana" + Expect(debug.WriteDebug(debugFilePath)).To(Succeed()) }) It("unmarshals and returns the error", func() { err := cniConfig.DelNetwork(netConfig, runtimeConfig) @@ -203,7 +216,7 @@ var _ = Describe("Invoking plugins", func() { Expect(versionInfo).NotTo(BeNil()) Expect(versionInfo.SupportedVersions()).To(Equal([]string{ - "0.-42.0", "0.1.0", "0.2.0", + "0.-42.0", "0.1.0", "0.2.0", "0.3.0", })) }) @@ -229,13 +242,13 @@ var _ = Describe("Invoking plugins", func() { BeforeEach(func() { plugins = make([]pluginInfo, 3, 3) - plugins[0] = newPluginInfo("some-key", "some-value", "", true, `{"dns":{},"ip4":{"ip": "10.1.2.3/24"}}`) - plugins[1] = newPluginInfo("some-key", "some-other-value", `{"dns":{},"ip4":{"ip": "10.1.2.3/24"}}`, true, "PASSTHROUGH") - plugins[2] = newPluginInfo("some-key", "yet-another-value", `{"dns":{},"ip4":{"ip": "10.1.2.3/24"}}`, true, "INJECT-DNS") + plugins[0] = newPluginInfo("some-key", "some-value", "", true, `{"dns":{},"ips":[{"version": "4", "address": "10.1.2.3/24"}]}`) + plugins[1] = newPluginInfo("some-key", "some-other-value", `{"dns":{},"ips":[{"version": "4", "address": "10.1.2.3/24"}]}`, true, "PASSTHROUGH") + plugins[2] = newPluginInfo("some-key", "yet-another-value", `{"dns":{},"ips":[{"version": "4", "address": "10.1.2.3/24"}]}`, true, "INJECT-DNS") configList := []byte(fmt.Sprintf(`{ "name": "some-list", - "cniVersion": "0.2.0", + "cniVersion": "0.3.0", "plugins": [ %s, %s, @@ -275,10 +288,13 @@ var _ = Describe("Invoking plugins", func() { Expect(result).To(Equal(¤t.Result{ // IP4 added by first plugin - IP4: ¤t.IPConfig{ - IP: net.IPNet{ - IP: net.ParseIP("10.1.2.3"), - Mask: net.IPv4Mask(255, 255, 255, 0), + IPs: []*current.IPConfig{ + { + Version: "4", + Address: net.IPNet{ + IP: net.ParseIP("10.1.2.3"), + Mask: net.IPv4Mask(255, 255, 255, 0), + }, }, }, // DNS injected by last plugin diff --git a/pkg/invoke/exec_test.go b/pkg/invoke/exec_test.go index 3e207c14..7e804ab7 100644 --- a/pkg/invoke/exec_test.go +++ b/pkg/invoke/exec_test.go @@ -40,7 +40,7 @@ var _ = Describe("Executing a plugin, unit tests", func() { BeforeEach(func() { rawExec = &fakes.RawExec{} - rawExec.ExecPluginCall.Returns.ResultBytes = []byte(`{ "ip4": { "ip": "1.2.3.4/24" } }`) + rawExec.ExecPluginCall.Returns.ResultBytes = []byte(`{ "ips": [ { "version": "4", "address": "1.2.3.4/24" } ] }`) versionDecoder = &fakes.VersionDecoder{} versionDecoder.DecodeCall.Returns.PluginInfo = version.PluginSupports("0.42.0") @@ -50,7 +50,7 @@ var _ = Describe("Executing a plugin, unit tests", func() { VersionDecoder: versionDecoder, } pluginPath = "/some/plugin/path" - netconf = []byte(`{ "some": "stdin", "cniVersion": "0.2.0" }`) + netconf = []byte(`{ "some": "stdin", "cniVersion": "0.3.0" }`) cniargs = &fakes.CNIArgs{} cniargs.AsEnvCall.Returns.Env = []string{"SOME=ENV"} }) @@ -62,7 +62,8 @@ var _ = Describe("Executing a plugin, unit tests", func() { result, err := current.GetResult(r) Expect(err).NotTo(HaveOccurred()) - Expect(result.IP4.IP.IP.String()).To(Equal("1.2.3.4")) + Expect(len(result.IPs)).To(Equal(1)) + Expect(result.IPs[0].Address.IP.String()).To(Equal("1.2.3.4")) }) It("passes its arguments through to the rawExec", func() { diff --git a/pkg/invoke/raw_exec_test.go b/pkg/invoke/raw_exec_test.go index b0ca9607..5ab23ae5 100644 --- a/pkg/invoke/raw_exec_test.go +++ b/pkg/invoke/raw_exec_test.go @@ -58,7 +58,7 @@ var _ = Describe("RawExec", func() { "CNI_PATH=/some/bin/path", "CNI_IFNAME=some-eth0", } - stdin = []byte(`{"some":"stdin-json", "cniVersion": "0.2.0"}`) + stdin = []byte(`{"some":"stdin-json", "cniVersion": "0.3.0"}`) execer = &invoke.RawExec{} }) diff --git a/pkg/ipam/ipam.go b/pkg/ipam/ipam.go index 8dd861a6..b76780f0 100644 --- a/pkg/ipam/ipam.go +++ b/pkg/ipam/ipam.go @@ -16,6 +16,7 @@ package ipam import ( "fmt" + "net" "os" "github.com/containernetworking/cni/pkg/invoke" @@ -37,6 +38,10 @@ func ExecDel(plugin string, netconf []byte) error { // ConfigureIface takes the result of IPAM plugin and // applies to the ifName interface func ConfigureIface(ifName string, res *current.Result) error { + if len(res.Interfaces) == 0 { + return fmt.Errorf("no interfaces to configure") + } + link, err := netlink.LinkByName(ifName) if err != nil { return fmt.Errorf("failed to lookup %q: %v", ifName, err) @@ -46,16 +51,35 @@ func ConfigureIface(ifName string, res *current.Result) error { 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) + var v4gw, v6gw net.IP + for _, ipc := range res.IPs { + if int(ipc.Interface) >= len(res.Interfaces) || res.Interfaces[ipc.Interface].Name != ifName { + // IP address is for a different interface + return fmt.Errorf("failed to add IP addr %v to %q: invalid interface index", ipc, ifName) + } + + addr := &netlink.Addr{IPNet: &ipc.Address, Label: ""} + if err = netlink.AddrAdd(link, addr); err != nil { + return fmt.Errorf("failed to add IP addr %v to %q: %v", ipc, ifName, err) + } + + gwIsV4 := ipc.Gateway.To4() != nil + if gwIsV4 && v4gw == nil { + v4gw = ipc.Gateway + } else if !gwIsV4 && v6gw == nil { + v6gw = ipc.Gateway + } } - for _, r := range res.IP4.Routes { + for _, r := range res.Routes { + routeIsV4 := r.Dst.IP.To4() != nil gw := r.GW if gw == nil { - gw = res.IP4.Gateway + if routeIsV4 && v4gw != nil { + gw = v4gw + } else if !routeIsV4 && v6gw != nil { + gw = v6gw + } } if err = ip.AddRoute(&r.Dst, gw, link); err != nil { // we skip over duplicate routes as we assume the first one wins diff --git a/pkg/ipam/ipam_test.go b/pkg/ipam/ipam_test.go index 622e4c8a..2d27825d 100644 --- a/pkg/ipam/ipam_test.go +++ b/pkg/ipam/ipam_test.go @@ -94,19 +94,35 @@ var _ = Describe("IPAM Operations", func() { Expect(ipgw6).NotTo(BeNil()) result = ¤t.Result{ - IP4: ¤t.IPConfig{ - IP: *ipv4, - Gateway: ipgw4, - Routes: []types.Route{ - {Dst: *routev4, GW: routegwv4}, + Interfaces: []*current.Interface{ + { + Name: "eth0", + Mac: "00:11:22:33:44:55", + Sandbox: "/proc/3553/ns/net", + }, + { + Name: "fake0", + Mac: "00:33:44:55:66:77", + Sandbox: "/proc/1234/ns/net", }, }, - IP6: ¤t.IPConfig{ - IP: *ipv6, - Gateway: ipgw6, - Routes: []types.Route{ - {Dst: *routev6, GW: routegwv6}, + IPs: []*current.IPConfig{ + { + Version: "4", + Interface: 0, + Address: *ipv4, + Gateway: ipgw4, }, + { + Version: "6", + Interface: 0, + Address: *ipv6, + Gateway: ipgw6, + }, + }, + Routes: []*types.Route{ + {Dst: *routev4, GW: routegwv4}, + {Dst: *routev6, GW: routegwv6}, }, } }) @@ -131,24 +147,39 @@ var _ = Describe("IPAM Operations", func() { Expect(len(v4addrs)).To(Equal(1)) Expect(ipNetEqual(v4addrs[0].IPNet, ipv4)).To(Equal(true)) - // Doesn't support IPv6 yet so only link-local address expected v6addrs, err := netlink.AddrList(link, syscall.AF_INET6) Expect(err).NotTo(HaveOccurred()) - Expect(len(v6addrs)).To(Equal(1)) + Expect(len(v6addrs)).To(Equal(2)) - // Ensure the v4 route + var found bool + for _, a := range v6addrs { + if ipNetEqual(a.IPNet, ipv6) { + found = true + break + } + } + Expect(found).To(Equal(true)) + + // Ensure the v4 route, v6 route, and subnet route routes, err := netlink.RouteList(link, 0) Expect(err).NotTo(HaveOccurred()) - var v4found bool + var v4found, v6found bool for _, route := range routes { isv4 := route.Dst.IP.To4() != nil if isv4 && ipNetEqual(route.Dst, routev4) && route.Gw.Equal(routegwv4) { v4found = true + } + if !isv4 && ipNetEqual(route.Dst, routev6) && route.Gw.Equal(routegwv6) { + v6found = true + } + + if v4found && v6found { break } } Expect(v4found).To(Equal(true)) + Expect(v6found).To(Equal(true)) return nil }) @@ -156,8 +187,8 @@ var _ = Describe("IPAM Operations", func() { }) It("configures a link with routes using address gateways", func() { - result.IP4.Routes[0].GW = nil - result.IP6.Routes[0].GW = nil + result.Routes[0].GW = nil + result.Routes[1].GW = nil err := originalNS.Do(func(ns.NetNS) error { defer GinkgoRecover() @@ -168,25 +199,56 @@ var _ = Describe("IPAM Operations", func() { Expect(err).NotTo(HaveOccurred()) Expect(link.Attrs().Name).To(Equal(LINK_NAME)) - // Ensure the v4 route + // Ensure the v4 route, v6 route, and subnet route routes, err := netlink.RouteList(link, 0) Expect(err).NotTo(HaveOccurred()) - var v4found bool + var v4found, v6found bool for _, route := range routes { isv4 := route.Dst.IP.To4() != nil if isv4 && ipNetEqual(route.Dst, routev4) && route.Gw.Equal(ipgw4) { v4found = true + } + if !isv4 && ipNetEqual(route.Dst, routev6) && route.Gw.Equal(ipgw6) { + v6found = true + } + + if v4found && v6found { break } } Expect(v4found).To(Equal(true)) + Expect(v6found).To(Equal(true)) return nil }) Expect(err).NotTo(HaveOccurred()) }) + It("returns an error when the interface index doesn't match the link name", func() { + result.IPs[0].Interface = 1 + err := originalNS.Do(func(ns.NetNS) error { + return ConfigureIface(LINK_NAME, result) + }) + Expect(err).To(HaveOccurred()) + }) + + It("returns an error when the interface index is too big", func() { + result.IPs[0].Interface = 2 + err := originalNS.Do(func(ns.NetNS) error { + return ConfigureIface(LINK_NAME, result) + }) + Expect(err).To(HaveOccurred()) + }) + + It("returns an error when there are no interfaces to configure", func() { + result.Interfaces = []*current.Interface{} + err := originalNS.Do(func(ns.NetNS) error { + return ConfigureIface(LINK_NAME, result) + }) + Expect(err).To(HaveOccurred()) + }) + It("returns an error when configuring the wrong interface", func() { err := originalNS.Do(func(ns.NetNS) error { return ConfigureIface("asdfasdf", result) diff --git a/pkg/skel/skel_test.go b/pkg/skel/skel_test.go index 6652fcdb..d7f729f0 100644 --- a/pkg/skel/skel_test.go +++ b/pkg/skel/skel_test.go @@ -226,7 +226,7 @@ var _ = Describe("dispatching to the correct callback", func() { Expect(err).NotTo(HaveOccurred()) Expect(stdout).To(MatchJSON(`{ - "cniVersion": "0.2.0", + "cniVersion": "0.3.0", "supportedVersions": ["9.8.7"] }`)) }) @@ -258,7 +258,7 @@ var _ = Describe("dispatching to the correct callback", func() { Expect(err).NotTo(HaveOccurred()) Expect(stdout).To(MatchJSON(`{ - "cniVersion": "0.2.0", + "cniVersion": "0.3.0", "supportedVersions": ["9.8.7"] }`)) }) diff --git a/pkg/types/020/types.go b/pkg/types/020/types.go new file mode 100644 index 00000000..666cfe93 --- /dev/null +++ b/pkg/types/020/types.go @@ -0,0 +1,133 @@ +// 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 types020 + +import ( + "encoding/json" + "fmt" + "net" + "os" + + "github.com/containernetworking/cni/pkg/types" +) + +const implementedSpecVersion string = "0.2.0" + +var SupportedVersions = []string{"", "0.1.0", implementedSpecVersion} + +// Compatibility types for CNI version 0.1.0 and 0.2.0 + +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) { + // We expect version 0.1.0/0.2.0 results + result020, err := r.GetAsVersion(implementedSpecVersion) + if err != nil { + return nil, err + } + result, ok := result020.(*Result) + if !ok { + return nil, fmt.Errorf("failed to convert result") + } + return result, nil +} + +// 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"` + DNS types.DNS `json:"dns,omitempty"` +} + +func (r *Result) Version() string { + return implementedSpecVersion +} + +func (r *Result) GetAsVersion(version string) (types.Result, error) { + for _, supportedVersion := range SupportedVersions { + if version == supportedVersion { + return r, nil + } + } + return nil, fmt.Errorf("cannot convert version %q to %s", SupportedVersions, 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 "[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 + Gateway net.IP + Routes []types.Route +} + +// net.IPNet is not JSON (un)marshallable so this duality is needed +// for our custom IPNet type + +// JSON (un)marshallable types +type ipConfig struct { + IP types.IPNet `json:"ip"` + Gateway net.IP `json:"gateway,omitempty"` + Routes []types.Route `json:"routes,omitempty"` +} + +func (c *IPConfig) MarshalJSON() ([]byte, error) { + ipc := ipConfig{ + IP: types.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 +} diff --git a/pkg/types/020/types_suite_test.go b/pkg/types/020/types_suite_test.go new file mode 100644 index 00000000..095d73e2 --- /dev/null +++ b/pkg/types/020/types_suite_test.go @@ -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 types020_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestTypes010(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "0.1.0/0.2.0 Types Suite") +} diff --git a/pkg/types/020/types_test.go b/pkg/types/020/types_test.go new file mode 100644 index 00000000..1bcdda73 --- /dev/null +++ b/pkg/types/020/types_test.go @@ -0,0 +1,128 @@ +// 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 types020_test + +import ( + "io/ioutil" + "net" + "os" + + "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/types/020" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +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() { + 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{ + 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"}, + }, + } + + 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(`{ + "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" + ] + } +}`)) + }) +}) diff --git a/pkg/types/current/types.go b/pkg/types/current/types.go index 338b3fd2..e686a9a7 100644 --- a/pkg/types/current/types.go +++ b/pkg/types/current/types.go @@ -21,11 +21,12 @@ import ( "os" "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/types/020" ) -const implementedSpecVersion string = "0.2.0" +const implementedSpecVersion string = "0.3.0" -var SupportedVersions = []string{"", "0.1.0", implementedSpecVersion} +var SupportedVersions = []string{implementedSpecVersion} func NewResult(data []byte) (types.Result, error) { result := &Result{} @@ -36,11 +37,11 @@ func NewResult(data []byte) (types.Result, error) { } func GetResult(r types.Result) (*Result, error) { - newResult, err := r.GetAsVersion(implementedSpecVersion) + resultCurrent, err := r.GetAsVersion(implementedSpecVersion) if err != nil { return nil, err } - result, ok := newResult.(*Result) + result, ok := resultCurrent.(*Result) if !ok { return nil, fmt.Errorf("failed to convert result") } @@ -51,10 +52,67 @@ var resultConverters = []struct { versions []string convert func(types.Result) (*Result, error) }{ - {SupportedVersions, convertFrom020}, + {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{ + DNS: oldResult.DNS, + Routes: []*types.Route{}, + } + + if oldResult.IP4 != nil { + newResult.IPs = append(newResult.IPs, &IPConfig{ + Version: "4", + Interface: -1, + 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", + Interface: -1, + 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") @@ -76,9 +134,58 @@ func NewResultFromResult(result types.Result) (*Result, error) { // 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"` - DNS types.DNS `json:"dns,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{ + 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 { @@ -86,12 +193,13 @@ func (r *Result) Version() string { } func (r *Result) GetAsVersion(version string) (types.Result, error) { - for _, supportedVersion := range SupportedVersions { - if version == supportedVersion { - return r, nil - } + switch version { + case implementedSpecVersion: + return r, nil + case types020.SupportedVersions[0], types020.SupportedVersions[1], types020.SupportedVersions[2]: + return r.convertTo020() } - return nil, fmt.Errorf("cannot convert version %q to %s", SupportedVersions, version) + return nil, fmt.Errorf("cannot convert version 0.3.0 to %q", version) } func (r *Result) Print() error { @@ -103,42 +211,67 @@ func (r *Result) Print() error { 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 +// 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 r.IP4 != nil { - str = fmt.Sprintf("IP4:%+v, ", *r.IP4) + if len(r.Interfaces) > 0 { + str += fmt.Sprintf("Interfaces:%+v, ", r.Interfaces) } - if r.IP6 != nil { - str += fmt.Sprintf("IP6:%+v, ", *r.IP6) + 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) } -// IPConfig contains values necessary to configure an interface -type IPConfig struct { - IP net.IPNet - Gateway net.IP - Routes []types.Route +// Convert this old version result to the current CNI version result +func (r *Result) Convert() (*Result, error) { + return r, nil } -// net.IPNet is not JSON (un)marshallable so this duality is needed -// for our custom IPNet type +// 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) +} + +// 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 { - IP types.IPNet `json:"ip"` - Gateway net.IP `json:"gateway,omitempty"` - Routes []types.Route `json:"routes,omitempty"` + 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{ - IP: types.IPNet(c.IP), - Gateway: c.Gateway, - Routes: c.Routes, + Version: c.Version, + Interface: c.Interface, + Address: types.IPNet(c.Address), + Gateway: c.Gateway, } return json.Marshal(ipc) @@ -150,8 +283,9 @@ func (c *IPConfig) UnmarshalJSON(data []byte) error { return err } - c.IP = net.IPNet(ipc.IP) + c.Version = ipc.Version + c.Interface = ipc.Interface + c.Address = net.IPNet(ipc.Address) c.Gateway = ipc.Gateway - c.Routes = ipc.Routes return nil } diff --git a/pkg/types/current/types_suite_test.go b/pkg/types/current/types_suite_test.go index 42a47a25..89cccecd 100644 --- a/pkg/types/current/types_suite_test.go +++ b/pkg/types/current/types_suite_test.go @@ -23,5 +23,5 @@ import ( func TestTypes010(t *testing.T) { RegisterFailHandler(Fail) - RunSpecs(t, "0.1.0 Types Suite") + RunSpecs(t, "0.3.0 Types Suite") } diff --git a/pkg/types/current/types_test.go b/pkg/types/current/types_test.go index 3810999d..f839cc90 100644 --- a/pkg/types/current/types_test.go +++ b/pkg/types/current/types_test.go @@ -26,51 +26,66 @@ import ( . "github.com/onsi/gomega" ) -var _ = Describe("Ensures compatibility with the 0.1.0 spec", func() { - It("correctly encodes a 0.1.0 Result", func() { - ipv4, err := types.ParseCIDR("1.2.3.30/24") - Expect(err).NotTo(HaveOccurred()) - Expect(ipv4).NotTo(BeNil()) +func testResult() *current.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()) + 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()) + 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()) + 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 := current.Result{ - IP4: ¤t.IPConfig{ - IP: *ipv4, - Gateway: net.ParseIP("1.2.3.1"), - Routes: []types.Route{ - {Dst: *routev4, GW: routegwv4}, - }, + // Set every field of the struct to ensure source compatibility + return ¤t.Result{ + Interfaces: []*current.Interface{ + { + Name: "eth0", + Mac: "00:11:22:33:44:55", + Sandbox: "/proc/3553/ns/net", }, - IP6: ¤t.IPConfig{ - IP: *ipv6, - Gateway: net.ParseIP("abcd:1234:ffff::1"), - Routes: []types.Route{ - {Dst: *routev6, GW: routegwv6}, - }, + }, + IPs: []*current.IPConfig{ + { + Version: "4", + Interface: 0, + Address: *ipv4, + Gateway: net.ParseIP("1.2.3.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"}, + { + Version: "6", + Interface: 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"}, + }, + } +} - 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]}")) +var _ = Describe("Ensures compatibility with the 0.3.0 spec", func() { + It("correctly encodes a 0.3.0 Result", func() { + res := testResult() + + Expect(res.String()).To(Equal("Interfaces:[{Name:eth0 Mac:00:11:22:33:44:55 Sandbox:/proc/3553/ns/net}], IP:[{Version:4 Interface:0 Address:{IP:1.2.3.30 Mask:ffffff00} Gateway:1.2.3.1} {Version:6 Interface:0 Address:{IP:abcd:1234:ffff::cdde Mask:ffffffffffffffff0000000000000000} Gateway:abcd:1234:ffff::1}], Routes:[{Dst:{IP:15.5.6.0 Mask:ffffff00} GW:15.5.6.8} {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 @@ -88,6 +103,76 @@ var _ = Describe("Ensures compatibility with the 0.1.0 spec", func() { Expect(err).NotTo(HaveOccurred()) Expect(string(out)).To(Equal(`{ + "interfaces": [ + { + "name": "eth0", + "mac": "00:11:22:33:44:55", + "sandbox": "/proc/3553/ns/net" + } + ], + "ips": [ + { + "version": "4", + "address": "1.2.3.30/24", + "gateway": "1.2.3.1" + }, + { + "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" + ] + } +}`)) + }) + + var _ = Describe("Ensures compatibility with the 0.1.0 spec", func() { + 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() + 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(`{ "ip4": { "ip": "1.2.3.30/24", "gateway": "1.2.3.1", @@ -124,5 +209,6 @@ var _ = Describe("Ensures compatibility with the 0.1.0 spec", func() { ] } }`)) + }) }) }) diff --git a/pkg/types/types.go b/pkg/types/types.go index 2ceffebc..a81ac702 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -16,6 +16,7 @@ package types import ( "encoding/json" + "fmt" "net" "os" ) @@ -114,6 +115,10 @@ type Route struct { GW net.IP } +func (r *Route) String() string { + return fmt.Sprintf("%+v", *r) +} + // Well known error codes // see https://github.com/containernetworking/cni/blob/master/SPEC.md#well-known-error-codes const ( diff --git a/pkg/version/legacy_examples/examples.go b/pkg/version/legacy_examples/examples.go index 8b079a3d..1bf406b3 100644 --- a/pkg/version/legacy_examples/examples.go +++ b/pkg/version/legacy_examples/examples.go @@ -23,7 +23,7 @@ import ( "sync" "github.com/containernetworking/cni/pkg/types" - "github.com/containernetworking/cni/pkg/types/current" + "github.com/containernetworking/cni/pkg/types/020" "github.com/containernetworking/cni/pkg/version/testhelpers" ) @@ -115,8 +115,8 @@ 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 = ¤t.Result{ - IP4: ¤t.IPConfig{ +var ExpectedResult = &types020.Result{ + IP4: &types020.IPConfig{ IP: net.IPNet{ IP: net.ParseIP("10.1.2.3"), Mask: net.CIDRMask(24, 32), diff --git a/pkg/version/version.go b/pkg/version/version.go index e777e52c..7c589633 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -18,12 +18,13 @@ import ( "fmt" "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/types/020" "github.com/containernetworking/cni/pkg/types/current" ) // Current reports the version of the CNI spec implemented by this library func Current() string { - return "0.2.0" + return "0.3.0" } // Legacy PluginInfo describes a plugin that is backwards compatible with the @@ -34,12 +35,14 @@ 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") var resultFactories = []struct { supportedVersions []string newResult types.ResultFactoryFunc }{ {current.SupportedVersions, current.NewResult}, + {types020.SupportedVersions, types020.NewResult}, } // Finds a Result object matching the requested version (if any) and asks diff --git a/plugins/ipam/dhcp/daemon.go b/plugins/ipam/dhcp/daemon.go index 2910a41b..c6660b7e 100644 --- a/plugins/ipam/dhcp/daemon.go +++ b/plugins/ipam/dhcp/daemon.go @@ -71,11 +71,12 @@ func (d *DHCP) Allocate(args *skel.CmdArgs, result *current.Result) error { d.setLease(args.ContainerID, conf.Name, l) - result.IP4 = ¤t.IPConfig{ - IP: *ipn, + result.IPs = []*current.IPConfig{{ + Version: "4", + Address: *ipn, Gateway: l.Gateway(), - Routes: l.Routes(), - } + }} + result.Routes = l.Routes() return nil } diff --git a/plugins/ipam/dhcp/lease.go b/plugins/ipam/dhcp/lease.go index eb2b4031..a202720d 100644 --- a/plugins/ipam/dhcp/lease.go +++ b/plugins/ipam/dhcp/lease.go @@ -291,7 +291,7 @@ func (l *DHCPLease) Gateway() net.IP { return parseRouter(l.opts) } -func (l *DHCPLease) Routes() []types.Route { +func (l *DHCPLease) Routes() []*types.Route { routes := parseRoutes(l.opts) return append(routes, parseCIDRRoutes(l.opts)...) } diff --git a/plugins/ipam/dhcp/main.go b/plugins/ipam/dhcp/main.go index f43ad3ed..9289957a 100644 --- a/plugins/ipam/dhcp/main.go +++ b/plugins/ipam/dhcp/main.go @@ -21,6 +21,7 @@ import ( "path/filepath" "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" ) @@ -31,16 +32,24 @@ func main() { if len(os.Args) > 1 && os.Args[1] == "daemon" { runDaemon() } else { - skel.PluginMain(cmdAdd, cmdDel, version.Legacy) + skel.PluginMain(cmdAdd, cmdDel, version.All) } } func cmdAdd(args *skel.CmdArgs) error { + // Plugin must return result in same version as specified in netconf + versionDecoder := &version.ConfigDecoder{} + confVersion, err := versionDecoder.Decode(args.StdinData) + if err != nil { + return err + } + result := ¤t.Result{} if err := rpcCall("DHCP.Allocate", args, result); err != nil { return err } - return result.Print() + + return types.PrintResult(result, confVersion) } func cmdDel(args *skel.CmdArgs) error { diff --git a/plugins/ipam/dhcp/options.go b/plugins/ipam/dhcp/options.go index b11ec21d..6e2e05c6 100644 --- a/plugins/ipam/dhcp/options.go +++ b/plugins/ipam/dhcp/options.go @@ -40,17 +40,17 @@ func classfulSubnet(sn net.IP) net.IPNet { } } -func parseRoutes(opts dhcp4.Options) []types.Route { +func parseRoutes(opts dhcp4.Options) []*types.Route { // StaticRoutes format: pairs of: // Dest = 4 bytes; Classful IP subnet // Router = 4 bytes; IP address of router - routes := []types.Route{} + routes := []*types.Route{} if opt, ok := opts[dhcp4.OptionStaticRoute]; ok { for len(opt) >= 8 { sn := opt[0:4] r := opt[4:8] - rt := types.Route{ + rt := &types.Route{ Dst: classfulSubnet(sn), GW: r, } @@ -62,10 +62,10 @@ func parseRoutes(opts dhcp4.Options) []types.Route { return routes } -func parseCIDRRoutes(opts dhcp4.Options) []types.Route { +func parseCIDRRoutes(opts dhcp4.Options) []*types.Route { // See RFC4332 for format (http://tools.ietf.org/html/rfc3442) - routes := []types.Route{} + routes := []*types.Route{} if opt, ok := opts[dhcp4.OptionClasslessRouteFormat]; ok { for len(opt) >= 5 { width := int(opt[0]) @@ -89,7 +89,7 @@ func parseCIDRRoutes(opts dhcp4.Options) []types.Route { gw := net.IP(opt[octets+1 : octets+5]) - rt := types.Route{ + rt := &types.Route{ Dst: net.IPNet{ IP: net.IP(sn), Mask: net.CIDRMask(width, 32), diff --git a/plugins/ipam/dhcp/options_test.go b/plugins/ipam/dhcp/options_test.go index f69ae7bc..9f2904bc 100644 --- a/plugins/ipam/dhcp/options_test.go +++ b/plugins/ipam/dhcp/options_test.go @@ -22,16 +22,16 @@ import ( "github.com/d2g/dhcp4" ) -func validateRoutes(t *testing.T, routes []types.Route) { - expected := []types.Route{ - types.Route{ +func validateRoutes(t *testing.T, routes []*types.Route) { + expected := []*types.Route{ + &types.Route{ Dst: net.IPNet{ IP: net.IPv4(10, 0, 0, 0), Mask: net.CIDRMask(8, 32), }, GW: net.IPv4(10, 1, 2, 3), }, - types.Route{ + &types.Route{ Dst: net.IPNet{ IP: net.IPv4(192, 168, 1, 0), Mask: net.CIDRMask(24, 32), diff --git a/plugins/ipam/host-local/backend/allocator/allocator.go b/plugins/ipam/host-local/backend/allocator/allocator.go index a6f2c650..5f934240 100644 --- a/plugins/ipam/host-local/backend/allocator/allocator.go +++ b/plugins/ipam/host-local/backend/allocator/allocator.go @@ -20,6 +20,7 @@ import ( "net" "github.com/containernetworking/cni/pkg/ip" + "github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types/current" "github.com/containernetworking/cni/plugins/ipam/host-local/backend" ) @@ -129,7 +130,7 @@ func validateRangeIP(ip net.IP, ipnet *net.IPNet, start net.IP, end net.IP) erro } // Returns newly allocated IP along with its config -func (a *IPAllocator) Get(id string) (*current.IPConfig, error) { +func (a *IPAllocator) Get(id string) (*current.IPConfig, []*types.Route, error) { a.store.Lock() defer a.store.Unlock() @@ -145,7 +146,7 @@ func (a *IPAllocator) Get(id string) (*current.IPConfig, error) { if requestedIP != nil { if gw != nil && gw.Equal(a.conf.Args.IP) { - return nil, fmt.Errorf("requested IP must differ gateway IP") + return nil, nil, fmt.Errorf("requested IP must differ gateway IP") } subnet := net.IPNet{ @@ -154,22 +155,24 @@ func (a *IPAllocator) Get(id string) (*current.IPConfig, error) { } err := validateRangeIP(requestedIP, &subnet, a.start, a.end) if err != nil { - return nil, err + return nil, nil, err } reserved, err := a.store.Reserve(id, requestedIP) if err != nil { - return nil, err + return nil, nil, err } if reserved { - return ¤t.IPConfig{ - IP: net.IPNet{IP: requestedIP, Mask: a.conf.Subnet.Mask}, + ipConfig := ¤t.IPConfig{ + Version: "4", + Address: net.IPNet{IP: requestedIP, Mask: a.conf.Subnet.Mask}, Gateway: gw, - Routes: a.conf.Routes, - }, nil + } + routes := convertRoutesToCurrent(a.conf.Routes) + return ipConfig, routes, nil } - return nil, fmt.Errorf("requested IP address %q is not available in network: %s", requestedIP, a.conf.Name) + return nil, nil, fmt.Errorf("requested IP address %q is not available in network: %s", requestedIP, a.conf.Name) } startIP, endIP := a.getSearchRange() @@ -181,21 +184,23 @@ func (a *IPAllocator) Get(id string) (*current.IPConfig, error) { reserved, err := a.store.Reserve(id, cur) if err != nil { - return nil, err + return nil, nil, err } if reserved { - return ¤t.IPConfig{ - IP: net.IPNet{IP: cur, Mask: a.conf.Subnet.Mask}, + ipConfig := ¤t.IPConfig{ + Version: "4", + Address: net.IPNet{IP: cur, Mask: a.conf.Subnet.Mask}, Gateway: gw, - Routes: a.conf.Routes, - }, nil + } + routes := convertRoutesToCurrent(a.conf.Routes) + return ipConfig, routes, nil } // break here to complete the loop if cur.Equal(endIP) { break } } - return nil, fmt.Errorf("no IP addresses available in network: %s", a.conf.Name) + return nil, nil, fmt.Errorf("no IP addresses available in network: %s", a.conf.Name) } // Releases all IPs allocated for the container with given ID diff --git a/plugins/ipam/host-local/backend/allocator/allocator_test.go b/plugins/ipam/host-local/backend/allocator/allocator_test.go index 24fa2771..147fe259 100644 --- a/plugins/ipam/host-local/backend/allocator/allocator_test.go +++ b/plugins/ipam/host-local/backend/allocator/allocator_test.go @@ -31,10 +31,10 @@ type AllocatorTestCase struct { lastIP string } -func (t AllocatorTestCase) run() (*current.IPConfig, error) { +func (t AllocatorTestCase) run() (*current.IPConfig, []*types.Route, error) { subnet, err := types.ParseCIDR(t.subnet) if err != nil { - return nil, err + return nil, nil, err } conf := IPAMConfig{ @@ -45,14 +45,14 @@ func (t AllocatorTestCase) run() (*current.IPConfig, error) { store := fakestore.NewFakeStore(t.ipmap, net.ParseIP(t.lastIP)) alloc, err := NewIPAllocator(&conf, store) if err != nil { - return nil, err + return nil, nil, err } - res, err := alloc.Get("ID") + res, routes, err := alloc.Get("ID") if err != nil { - return nil, err + return nil, nil, err } - return res, nil + return res, routes, nil } var _ = Describe("host-local ip allocator", func() { @@ -129,9 +129,9 @@ var _ = Describe("host-local ip allocator", func() { } for _, tc := range testCases { - res, err := tc.run() + res, _, err := tc.run() Expect(err).ToNot(HaveOccurred()) - Expect(res.IP.IP.String()).To(Equal(tc.expectResult)) + Expect(res.Address.IP.String()).To(Equal(tc.expectResult)) } }) @@ -149,14 +149,14 @@ var _ = Describe("host-local ip allocator", func() { Expect(err).ToNot(HaveOccurred()) for i := 1; i < 254; i++ { - res, err := alloc.Get("ID") + res, _, err := alloc.Get("ID") Expect(err).ToNot(HaveOccurred()) // i+1 because the gateway address is skipped s := fmt.Sprintf("192.168.1.%d/24", i+1) - Expect(s).To(Equal(res.IP.String())) + Expect(s).To(Equal(res.Address.String())) } - _, err = alloc.Get("ID") + _, _, err = alloc.Get("ID") Expect(err).To(HaveOccurred()) }) @@ -174,13 +174,13 @@ var _ = Describe("host-local ip allocator", func() { alloc, err := NewIPAllocator(&conf, store) Expect(err).ToNot(HaveOccurred()) - res, err := alloc.Get("ID") + res, _, err := alloc.Get("ID") Expect(err).ToNot(HaveOccurred()) - Expect(res.IP.String()).To(Equal("192.168.1.10/24")) + Expect(res.Address.String()).To(Equal("192.168.1.10/24")) - res, err = alloc.Get("ID") + res, _, err = alloc.Get("ID") Expect(err).ToNot(HaveOccurred()) - Expect(res.IP.String()).To(Equal("192.168.1.11/24")) + Expect(res.Address.String()).To(Equal("192.168.1.11/24")) }) It("should allocate RangeEnd but not past RangeEnd", func() { @@ -198,13 +198,13 @@ var _ = Describe("host-local ip allocator", func() { Expect(err).ToNot(HaveOccurred()) for i := 1; i < 5; i++ { - res, err := alloc.Get("ID") + res, _, err := alloc.Get("ID") Expect(err).ToNot(HaveOccurred()) // i+1 because the gateway address is skipped - Expect(res.IP.String()).To(Equal(fmt.Sprintf("192.168.1.%d/24", i+1))) + Expect(res.Address.String()).To(Equal(fmt.Sprintf("192.168.1.%d/24", i+1))) } - _, err = alloc.Get("ID") + _, _, err = alloc.Get("ID") Expect(err).To(HaveOccurred()) }) @@ -222,9 +222,9 @@ var _ = Describe("host-local ip allocator", func() { } store := fakestore.NewFakeStore(ipmap, nil) alloc, _ := NewIPAllocator(&conf, store) - res, err := alloc.Get("ID") + res, _, err := alloc.Get("ID") Expect(err).ToNot(HaveOccurred()) - Expect(res.IP.IP.String()).To(Equal(requestedIP.String())) + Expect(res.Address.IP.String()).To(Equal(requestedIP.String())) }) It("must return an error when the requested IP is after RangeEnd", func() { @@ -240,7 +240,7 @@ var _ = Describe("host-local ip allocator", func() { } store := fakestore.NewFakeStore(ipmap, nil) alloc, _ := NewIPAllocator(&conf, store) - _, err = alloc.Get("ID") + _, _, err = alloc.Get("ID") Expect(err).To(HaveOccurred()) }) @@ -257,7 +257,7 @@ var _ = Describe("host-local ip allocator", func() { } store := fakestore.NewFakeStore(ipmap, nil) alloc, _ := NewIPAllocator(&conf, store) - _, err = alloc.Get("ID") + _, _, err = alloc.Get("ID") Expect(err).To(HaveOccurred()) }) }) @@ -332,7 +332,7 @@ var _ = Describe("host-local ip allocator", func() { }, } for _, tc := range testCases { - _, err := tc.run() + _, _, err := tc.run() Expect(err).To(MatchError("no IP addresses available in network: test")) } }) diff --git a/plugins/ipam/host-local/backend/allocator/config.go b/plugins/ipam/host-local/backend/allocator/config.go index cf80ac25..d261d0f0 100644 --- a/plugins/ipam/host-local/backend/allocator/config.go +++ b/plugins/ipam/host-local/backend/allocator/config.go @@ -42,31 +42,43 @@ type IPAMArgs struct { } type Net struct { - Name string `json:"name"` - IPAM *IPAMConfig `json:"ipam"` + Name string `json:"name"` + CNIVersion string `json:"cniVersion"` + IPAM *IPAMConfig `json:"ipam"` } // NewIPAMConfig creates a NetworkConfig from the given network name. -func LoadIPAMConfig(bytes []byte, args string) (*IPAMConfig, error) { +func LoadIPAMConfig(bytes []byte, args string) (*IPAMConfig, string, error) { n := Net{} if err := json.Unmarshal(bytes, &n); err != nil { - return nil, err + return nil, "", err } if args != "" { n.IPAM.Args = &IPAMArgs{} err := types.LoadArgs(args, n.IPAM.Args) if err != nil { - return nil, err + return nil, "", err } } if n.IPAM == nil { - return nil, fmt.Errorf("IPAM config missing 'ipam' key") + return nil, "", fmt.Errorf("IPAM config missing 'ipam' key") } // Copy net name into IPAM so not to drag Net struct around n.IPAM.Name = n.Name - return n.IPAM, nil + return n.IPAM, n.CNIVersion, nil +} + +func convertRoutesToCurrent(routes []types.Route) []*types.Route { + var currentRoutes []*types.Route + for _, r := range routes { + currentRoutes = append(currentRoutes, &types.Route{ + Dst: r.Dst, + GW: r.GW, + }) + } + return currentRoutes } diff --git a/plugins/ipam/host-local/host_local_test.go b/plugins/ipam/host-local/host_local_test.go index a8742fad..52b1c793 100644 --- a/plugins/ipam/host-local/host_local_test.go +++ b/plugins/ipam/host-local/host_local_test.go @@ -20,10 +20,12 @@ import ( "net" "os" "path/filepath" + "strings" "github.com/containernetworking/cni/pkg/skel" "github.com/containernetworking/cni/pkg/testutils" "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/types/020" "github.com/containernetworking/cni/pkg/types/current" . "github.com/onsi/ginkgo" @@ -43,7 +45,7 @@ var _ = Describe("host-local Operations", func() { Expect(err).NotTo(HaveOccurred()) conf := fmt.Sprintf(`{ - "cniVersion": "0.2.0", + "cniVersion": "0.3.0", "name": "mynet", "type": "ipvlan", "master": "foo0", @@ -63,19 +65,83 @@ var _ = Describe("host-local Operations", func() { } // Allocate the IP - r, _, err := testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error { + r, raw, err := testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error { return cmdAdd(args) }) Expect(err).NotTo(HaveOccurred()) + Expect(strings.Index(string(raw), "\"version\":")).Should(BeNumerically(">", 0)) result, err := current.GetResult(r) Expect(err).NotTo(HaveOccurred()) + expectedAddress, err := types.ParseCIDR("10.1.2.2/24") + Expect(err).NotTo(HaveOccurred()) + Expect(len(result.IPs)).To(Equal(1)) + expectedAddress.IP = expectedAddress.IP.To16() + Expect(result.IPs[0].Address).To(Equal(*expectedAddress)) + Expect(result.IPs[0].Gateway).To(Equal(net.ParseIP("10.1.2.1"))) + + ipFilePath := filepath.Join(tmpDir, "mynet", "10.1.2.2") + contents, err := ioutil.ReadFile(ipFilePath) + Expect(err).NotTo(HaveOccurred()) + Expect(string(contents)).To(Equal("dummy")) + + lastFilePath := filepath.Join(tmpDir, "mynet", "last_reserved_ip") + contents, err = ioutil.ReadFile(lastFilePath) + Expect(err).NotTo(HaveOccurred()) + Expect(string(contents)).To(Equal("10.1.2.2")) + + // Release the IP + err = testutils.CmdDelWithResult(nspath, ifname, func() error { + return cmdDel(args) + }) + Expect(err).NotTo(HaveOccurred()) + + _, err = os.Stat(ipFilePath) + Expect(err).To(HaveOccurred()) + }) + + It("allocates and releases an address with ADD/DEL and 0.1.0 config", func() { + const ifname string = "eth0" + const nspath string = "/some/where" + + tmpDir, err := ioutil.TempDir("", "host_local_artifacts") + Expect(err).NotTo(HaveOccurred()) + defer os.RemoveAll(tmpDir) + + conf := fmt.Sprintf(`{ + "cniVersion": "0.1.0", + "name": "mynet", + "type": "ipvlan", + "master": "foo0", + "ipam": { + "type": "host-local", + "subnet": "10.1.2.0/24", + "dataDir": "%s" + } +}`, tmpDir) + + args := &skel.CmdArgs{ + ContainerID: "dummy", + Netns: nspath, + IfName: ifname, + StdinData: []byte(conf), + } + + // Allocate the IP + r, raw, err := testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error { + return cmdAdd(args) + }) + Expect(err).NotTo(HaveOccurred()) + Expect(strings.Index(string(raw), "\"ip4\":")).Should(BeNumerically(">", 0)) + + result, err := types020.GetResult(r) + Expect(err).NotTo(HaveOccurred()) + expectedAddress, err := types.ParseCIDR("10.1.2.2/24") Expect(err).NotTo(HaveOccurred()) expectedAddress.IP = expectedAddress.IP.To16() Expect(result.IP4.IP).To(Equal(*expectedAddress)) - Expect(result.IP4.Gateway).To(Equal(net.ParseIP("10.1.2.1"))) ipFilePath := filepath.Join(tmpDir, "mynet", "10.1.2.2") @@ -136,7 +202,7 @@ var _ = Describe("host-local Operations", func() { result, err := current.GetResult(r) Expect(err).NotTo(HaveOccurred()) - ipFilePath := filepath.Join(tmpDir, "mynet", result.IP4.IP.IP.String()) + ipFilePath := filepath.Join(tmpDir, "mynet", result.IPs[0].Address.IP.String()) contents, err := ioutil.ReadFile(ipFilePath) Expect(err).NotTo(HaveOccurred()) Expect(string(contents)).To(Equal("dummy")) diff --git a/plugins/ipam/host-local/main.go b/plugins/ipam/host-local/main.go index a5dd080a..2f77912b 100644 --- a/plugins/ipam/host-local/main.go +++ b/plugins/ipam/host-local/main.go @@ -19,28 +19,29 @@ import ( "github.com/containernetworking/cni/plugins/ipam/host-local/backend/disk" "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" ) func main() { - skel.PluginMain(cmdAdd, cmdDel, version.Legacy) + skel.PluginMain(cmdAdd, cmdDel, version.All) } func cmdAdd(args *skel.CmdArgs) error { - ipamConf, err := allocator.LoadIPAMConfig(args.StdinData, args.Args) + ipamConf, confVersion, err := allocator.LoadIPAMConfig(args.StdinData, args.Args) if err != nil { return err } - r := ¤t.Result{} + result := ¤t.Result{} if ipamConf.ResolvConf != "" { dns, err := parseResolvConf(ipamConf.ResolvConf) if err != nil { return err } - r.DNS = *dns + result.DNS = *dns } store, err := disk.New(ipamConf.Name, ipamConf.DataDir) @@ -54,16 +55,18 @@ func cmdAdd(args *skel.CmdArgs) error { return err } - r.IP4, err = allocator.Get(args.ContainerID) + ipConf, routes, err := allocator.Get(args.ContainerID) if err != nil { return err } + result.IPs = []*current.IPConfig{ipConf} + result.Routes = routes - return r.Print() + return types.PrintResult(result, confVersion) } func cmdDel(args *skel.CmdArgs) error { - ipamConf, err := allocator.LoadIPAMConfig(args.StdinData, args.Args) + ipamConf, _, err := allocator.LoadIPAMConfig(args.StdinData, args.Args) if err != nil { return err } diff --git a/plugins/main/bridge/bridge.go b/plugins/main/bridge/bridge.go index a2f8b17c..ac959791 100644 --- a/plugins/main/bridge/bridge.go +++ b/plugins/main/bridge/bridge.go @@ -53,14 +53,14 @@ func init() { runtime.LockOSThread() } -func loadNetConf(bytes []byte) (*NetConf, error) { +func loadNetConf(bytes []byte) (*NetConf, string, error) { n := &NetConf{ BrName: defaultBrName, } if err := json.Unmarshal(bytes, n); err != nil { - return nil, fmt.Errorf("failed to load netconf: %v", err) + return nil, "", fmt.Errorf("failed to load netconf: %v", err) } - return n, nil + return n, n.CNIVersion, nil } func ensureBridgeAddr(br *netlink.Bridge, ipn *net.IPNet, forceAddress bool) error { @@ -139,16 +139,16 @@ func ensureBridge(brName string, mtu int) (*netlink.Bridge, error) { }, } - if err := netlink.LinkAdd(br); err != nil { - if err != syscall.EEXIST { - return nil, fmt.Errorf("could not add %q: %v", brName, err) - } + err := netlink.LinkAdd(br) + if err != nil && err != syscall.EEXIST { + return nil, fmt.Errorf("could not add %q: %v", brName, err) + } - // it's ok if the device already exists as long as config is similar - br, err = bridgeByName(brName) - if err != nil { - return nil, err - } + // Re-fetch link to read all attributes and if it already existed, + // ensure it's really a bridge with similar configuration + br, err = bridgeByName(brName) + if err != nil { + return nil, err } if err := netlink.LinkSetUp(br); err != nil { @@ -158,40 +158,44 @@ func ensureBridge(brName string, mtu int) (*netlink.Bridge, error) { return br, nil } -func setupVeth(netns ns.NetNS, br *netlink.Bridge, ifName string, mtu int, hairpinMode bool) error { - var hostVethName string +func setupVeth(netns ns.NetNS, br *netlink.Bridge, ifName string, mtu int, hairpinMode bool) (*current.Interface, *current.Interface, error) { + contIface := ¤t.Interface{} + hostIface := ¤t.Interface{} err := netns.Do(func(hostNS ns.NetNS) error { // create the veth pair in the container and move host end into host netns - hostVeth, _, err := ip.SetupVeth(ifName, mtu, hostNS) + hostVeth, containerVeth, err := ip.SetupVeth(ifName, mtu, hostNS) if err != nil { return err } - - hostVethName = hostVeth.Attrs().Name + contIface.Name = containerVeth.Attrs().Name + contIface.Mac = containerVeth.Attrs().HardwareAddr.String() + contIface.Sandbox = netns.Path() + hostIface.Name = hostVeth.Attrs().Name return nil }) if err != nil { - return err + return nil, nil, err } // need to lookup hostVeth again as its index has changed during ns move - hostVeth, err := netlink.LinkByName(hostVethName) + hostVeth, err := netlink.LinkByName(hostIface.Name) if err != nil { - return fmt.Errorf("failed to lookup %q: %v", hostVethName, err) + return nil, nil, fmt.Errorf("failed to lookup %q: %v", hostIface.Name, err) } + hostIface.Mac = hostVeth.Attrs().HardwareAddr.String() // connect host veth end to the bridge - if err = netlink.LinkSetMaster(hostVeth, br); err != nil { - return fmt.Errorf("failed to connect %q to bridge %v: %v", hostVethName, br.Attrs().Name, err) + if err := netlink.LinkSetMaster(hostVeth, br); err != nil { + return nil, nil, fmt.Errorf("failed to connect %q to bridge %v: %v", hostVeth.Attrs().Name, br.Attrs().Name, err) } // set hairpin mode if err = netlink.LinkSetHairpin(hostVeth, hairpinMode); err != nil { - return fmt.Errorf("failed to setup hairpin mode for %v: %v", hostVethName, err) + return nil, nil, fmt.Errorf("failed to setup hairpin mode for %v: %v", hostVeth.Attrs().Name, err) } - return nil + return hostIface, contIface, nil } func calcGatewayIP(ipn *net.IPNet) net.IP { @@ -199,18 +203,21 @@ func calcGatewayIP(ipn *net.IPNet) net.IP { return ip.NextIP(nid) } -func setupBridge(n *NetConf) (*netlink.Bridge, error) { +func setupBridge(n *NetConf) (*netlink.Bridge, *current.Interface, error) { // create bridge if necessary br, err := ensureBridge(n.BrName, n.MTU) if err != nil { - return nil, fmt.Errorf("failed to create bridge %q: %v", n.BrName, err) + return nil, nil, fmt.Errorf("failed to create bridge %q: %v", n.BrName, err) } - return br, nil + return br, ¤t.Interface{ + Name: br.Attrs().Name, + Mac: br.Attrs().HardwareAddr.String(), + }, nil } func cmdAdd(args *skel.CmdArgs) error { - n, err := loadNetConf(args.StdinData) + n, cniVersion, err := loadNetConf(args.StdinData) if err != nil { return err } @@ -219,7 +226,7 @@ func cmdAdd(args *skel.CmdArgs) error { n.IsGW = true } - br, err := setupBridge(n) + br, brInterface, err := setupBridge(n) if err != nil { return err } @@ -230,7 +237,8 @@ func cmdAdd(args *skel.CmdArgs) error { } defer netns.Close() - if err = setupVeth(netns, br, args.IfName, n.MTU, n.HairpinMode); err != nil { + hostInterface, containerInterface, err := setupVeth(netns, br, args.IfName, n.MTU, n.HairpinMode) + if err != nil { return err } @@ -240,72 +248,100 @@ func cmdAdd(args *skel.CmdArgs) error { return err } - result, err := current.GetResult(r) + // Convert whatever the IPAM result was into the current Result type + result, err := current.NewResultFromResult(r) if err != nil { return err } - // TODO: make this optional when IPv6 is supported - if result.IP4 == nil { - return errors.New("IPAM plugin returned missing IPv4 config") + if len(result.IPs) == 0 { + return errors.New("IPAM plugin returned missing IP config") } - if result.IP4.Gateway == nil && n.IsGW { - result.IP4.Gateway = calcGatewayIP(&result.IP4.IP) + result.Interfaces = []*current.Interface{brInterface, hostInterface, containerInterface} + + for _, ipc := range result.IPs { + // All IPs currently refer to the container interface + ipc.Interface = 2 + if ipc.Gateway == nil && n.IsGW { + ipc.Gateway = calcGatewayIP(&ipc.Address) + } } if err := netns.Do(func(_ ns.NetNS) error { // set the default gateway if requested if n.IsDefaultGW { - _, defaultNet, err := net.ParseCIDR("0.0.0.0/0") - if err != nil { - return err - } + for _, ipc := range result.IPs { + defaultNet := &net.IPNet{} + switch { + case ipc.Address.IP.To4() != nil: + defaultNet.IP = net.IPv4zero + defaultNet.Mask = net.IPMask(net.IPv4zero) + case len(ipc.Address.IP) == net.IPv6len && ipc.Address.IP.To4() == nil: + defaultNet.IP = net.IPv6zero + defaultNet.Mask = net.IPMask(net.IPv6zero) + default: + return fmt.Errorf("Unknown IP object: %v", ipc) + } - for _, route := range result.IP4.Routes { - if defaultNet.String() == route.Dst.String() { - if route.GW != nil && !route.GW.Equal(result.IP4.Gateway) { - return fmt.Errorf( - "isDefaultGateway ineffective because IPAM sets default route via %q", - route.GW, - ) + for _, route := range result.Routes { + if defaultNet.String() == route.Dst.String() { + if route.GW != nil && !route.GW.Equal(ipc.Gateway) { + return fmt.Errorf( + "isDefaultGateway ineffective because IPAM sets default route via %q", + route.GW, + ) + } } } + + result.Routes = append( + result.Routes, + &types.Route{Dst: *defaultNet, GW: ipc.Gateway}, + ) } - - result.IP4.Routes = append( - result.IP4.Routes, - types.Route{Dst: *defaultNet, GW: result.IP4.Gateway}, - ) - - // TODO: IPV6 } if err := ipam.ConfigureIface(args.IfName, result); err != nil { return err } - if err := ip.SetHWAddrByIP(args.IfName, result.IP4.IP.IP, nil /* TODO IPv6 */); err != nil { + if err := ip.SetHWAddrByIP(args.IfName, result.IPs[0].Address.IP, nil /* TODO IPv6 */); err != nil { return err } + // Refetch the veth since its MAC address may changed + link, err := netlink.LinkByName(args.IfName) + if err != nil { + return fmt.Errorf("could not lookup %q: %v", args.IfName, err) + } + containerInterface.Mac = link.Attrs().HardwareAddr.String() + return nil }); err != nil { return err } if n.IsGW { - gwn := &net.IPNet{ - IP: result.IP4.Gateway, - Mask: result.IP4.IP.Mask, + var firstV4Addr net.IP + for _, ipc := range result.IPs { + gwn := &net.IPNet{ + IP: ipc.Gateway, + Mask: ipc.Address.Mask, + } + if ipc.Gateway.To4() != nil && firstV4Addr == nil { + firstV4Addr = ipc.Gateway + } + + if err = ensureBridgeAddr(br, gwn, n.ForceAddress); err != nil { + return err + } } - if err = ensureBridgeAddr(br, gwn, n.ForceAddress); err != nil { - return err - } - - if err := ip.SetHWAddrByIP(n.BrName, gwn.IP, nil /* TODO IPv6 */); err != nil { - return err + if firstV4Addr != nil { + if err := ip.SetHWAddrByIP(n.BrName, firstV4Addr, nil /* TODO IPv6 */); err != nil { + return err + } } if err := ip.EnableIP4Forward(); err != nil { @@ -316,17 +352,28 @@ func cmdAdd(args *skel.CmdArgs) error { if n.IPMasq { chain := utils.FormatChainName(n.Name, args.ContainerID) comment := utils.FormatComment(n.Name, args.ContainerID) - if err = ip.SetupIPMasq(ip.Network(&result.IP4.IP), chain, comment); err != nil { - return err + for _, ipc := range result.IPs { + if err = ip.SetupIPMasq(ip.Network(&ipc.Address), chain, comment); err != nil { + return err + } } } + // Refetch the bridge since its MAC address may change when the first + // veth is added or after its IP address is set + br, err = bridgeByName(n.BrName) + if err != nil { + return err + } + brInterface.Mac = br.Attrs().HardwareAddr.String() + result.DNS = n.DNS - return result.Print() + + return types.PrintResult(result, cniVersion) } func cmdDel(args *skel.CmdArgs) error { - n, err := loadNetConf(args.StdinData) + n, _, err := loadNetConf(args.StdinData) if err != nil { return err } @@ -361,5 +408,5 @@ func cmdDel(args *skel.CmdArgs) error { } func main() { - skel.PluginMain(cmdAdd, cmdDel, version.Legacy) + skel.PluginMain(cmdAdd, cmdDel, version.All) } diff --git a/plugins/main/bridge/bridge_test.go b/plugins/main/bridge/bridge_test.go index 870ad6b4..5a8fd5a0 100644 --- a/plugins/main/bridge/bridge_test.go +++ b/plugins/main/bridge/bridge_test.go @@ -17,12 +17,15 @@ package main import ( "fmt" "net" + "strings" "syscall" "github.com/containernetworking/cni/pkg/ns" "github.com/containernetworking/cni/pkg/skel" "github.com/containernetworking/cni/pkg/testutils" "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/utils/hwaddr" @@ -51,7 +54,7 @@ var _ = Describe("bridge Operations", func() { conf := &NetConf{ NetConf: types.NetConf{ - CNIVersion: "0.2.0", + CNIVersion: "0.3.0", Name: "testConfig", Type: "bridge", }, @@ -64,7 +67,7 @@ var _ = Describe("bridge Operations", func() { err := originalNS.Do(func(ns.NetNS) error { defer GinkgoRecover() - bridge, err := setupBridge(conf) + bridge, _, err := setupBridge(conf) Expect(err).NotTo(HaveOccurred()) Expect(bridge.Attrs().Name).To(Equal(IFNAME)) @@ -96,7 +99,7 @@ var _ = Describe("bridge Operations", func() { conf := &NetConf{ NetConf: types.NetConf{ - CNIVersion: "0.2.0", + CNIVersion: "0.3.0", Name: "testConfig", Type: "bridge", }, @@ -105,7 +108,7 @@ var _ = Describe("bridge Operations", func() { IPMasq: false, } - bridge, err := setupBridge(conf) + bridge, _, err := setupBridge(conf) Expect(err).NotTo(HaveOccurred()) Expect(bridge.Attrs().Name).To(Equal(IFNAME)) Expect(bridge.Attrs().Index).To(Equal(ifindex)) @@ -128,7 +131,7 @@ var _ = Describe("bridge Operations", func() { Expect(err).NotTo(HaveOccurred()) conf := fmt.Sprintf(`{ - "cniVersion": "0.2.0", + "cniVersion": "0.3.0", "name": "mynet", "type": "bridge", "bridge": "%s", @@ -151,18 +154,31 @@ var _ = Describe("bridge Operations", func() { StdinData: []byte(conf), } + var result *current.Result err = originalNS.Do(func(ns.NetNS) error { defer GinkgoRecover() - _, _, err := testutils.CmdAddWithResult(targetNs.Path(), IFNAME, []byte(conf), func() error { + r, raw, err := testutils.CmdAddWithResult(targetNs.Path(), IFNAME, []byte(conf), func() error { return cmdAdd(args) }) Expect(err).NotTo(HaveOccurred()) + Expect(strings.Index(string(raw), "\"interfaces\":")).Should(BeNumerically(">", 0)) + + result, err = current.GetResult(r) + Expect(err).NotTo(HaveOccurred()) + + Expect(len(result.Interfaces)).To(Equal(3)) + Expect(result.Interfaces[0].Name).To(Equal(BRNAME)) + Expect(result.Interfaces[2].Name).To(Equal(IFNAME)) // Make sure bridge link exists - link, err := netlink.LinkByName(BRNAME) + link, err := netlink.LinkByName(result.Interfaces[0].Name) Expect(err).NotTo(HaveOccurred()) Expect(link.Attrs().Name).To(Equal(BRNAME)) + Expect(link).To(BeAssignableToTypeOf(&netlink.Bridge{})) + Expect(link.Attrs().HardwareAddr.String()).To(Equal(result.Interfaces[0].Mac)) + hwAddr := fmt.Sprintf("%s", link.Attrs().HardwareAddr) + Expect(hwAddr).To(HavePrefix(hwaddr.PrivateMACPrefixString)) // Ensure bridge has gateway address addrs, err := netlink.AddrList(link, syscall.AF_INET) @@ -183,23 +199,10 @@ var _ = Describe("bridge Operations", func() { links, err := netlink.LinkList() Expect(err).NotTo(HaveOccurred()) Expect(len(links)).To(Equal(3)) // Bridge, veth, and loopback - for _, l := range links { - switch { - case l.Attrs().Name == BRNAME: - { - _, isBridge := l.(*netlink.Bridge) - Expect(isBridge).To(Equal(true)) - hwAddr := fmt.Sprintf("%s", l.Attrs().HardwareAddr) - Expect(hwAddr).To(HavePrefix(hwaddr.PrivateMACPrefixString)) - } - case l.Attrs().Name != BRNAME && l.Attrs().Name != "lo": - { - _, isVeth := l.(*netlink.Veth) - Expect(isVeth).To(Equal(true)) - } - } - } + + link, err = netlink.LinkByName(result.Interfaces[1].Name) Expect(err).NotTo(HaveOccurred()) + Expect(link).To(BeAssignableToTypeOf(&netlink.Veth{})) return nil }) Expect(err).NotTo(HaveOccurred()) @@ -211,6 +214,11 @@ var _ = Describe("bridge Operations", func() { link, err := netlink.LinkByName(IFNAME) Expect(err).NotTo(HaveOccurred()) Expect(link.Attrs().Name).To(Equal(IFNAME)) + Expect(link).To(BeAssignableToTypeOf(&netlink.Veth{})) + + addrs, err := netlink.AddrList(link, syscall.AF_INET) + Expect(err).NotTo(HaveOccurred()) + Expect(len(addrs)).To(Equal(1)) hwAddr := fmt.Sprintf("%s", link.Attrs().HardwareAddr) Expect(hwAddr).To(HavePrefix(hwaddr.PrivateMACPrefixString)) @@ -243,7 +251,7 @@ var _ = Describe("bridge Operations", func() { }) Expect(err).NotTo(HaveOccurred()) - // Make sure macvlan link has been deleted + // Make sure the host veth has been deleted err = targetNs.Do(func(ns.NetNS) error { defer GinkgoRecover() @@ -253,6 +261,150 @@ var _ = Describe("bridge Operations", func() { return nil }) Expect(err).NotTo(HaveOccurred()) + + // Make sure the container veth has been deleted + err = originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + link, err := netlink.LinkByName(result.Interfaces[1].Name) + Expect(err).To(HaveOccurred()) + Expect(link).To(BeNil()) + return nil + }) + }) + + It("configures and deconfigures a bridge and veth with default route with ADD/DEL for 0.1.0 config", func() { + const BRNAME = "cni0" + const IFNAME = "eth0" + + gwaddr, subnet, err := net.ParseCIDR("10.1.2.1/24") + Expect(err).NotTo(HaveOccurred()) + + conf := fmt.Sprintf(`{ + "cniVersion": "0.1.0", + "name": "mynet", + "type": "bridge", + "bridge": "%s", + "isDefaultGateway": true, + "ipMasq": false, + "ipam": { + "type": "host-local", + "subnet": "%s" + } +}`, BRNAME, subnet.String()) + + targetNs, err := ns.NewNS() + Expect(err).NotTo(HaveOccurred()) + defer targetNs.Close() + + args := &skel.CmdArgs{ + ContainerID: "dummy", + Netns: targetNs.Path(), + IfName: IFNAME, + StdinData: []byte(conf), + } + + var result *types020.Result + err = originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + r, raw, err := testutils.CmdAddWithResult(targetNs.Path(), IFNAME, []byte(conf), func() error { + return cmdAdd(args) + }) + Expect(err).NotTo(HaveOccurred()) + Expect(strings.Index(string(raw), "\"ip4\":")).Should(BeNumerically(">", 0)) + + // We expect a version 0.1.0 result + result, err = types020.GetResult(r) + Expect(err).NotTo(HaveOccurred()) + + // Make sure bridge link exists + link, err := netlink.LinkByName(BRNAME) + Expect(err).NotTo(HaveOccurred()) + Expect(link.Attrs().Name).To(Equal(BRNAME)) + Expect(link).To(BeAssignableToTypeOf(&netlink.Bridge{})) + hwAddr := fmt.Sprintf("%s", link.Attrs().HardwareAddr) + Expect(hwAddr).To(HavePrefix(hwaddr.PrivateMACPrefixString)) + + // Ensure bridge has gateway address + addrs, err := netlink.AddrList(link, syscall.AF_INET) + Expect(err).NotTo(HaveOccurred()) + Expect(len(addrs)).To(BeNumerically(">", 0)) + found := false + subnetPrefix, subnetBits := subnet.Mask.Size() + for _, a := range addrs { + aPrefix, aBits := a.IPNet.Mask.Size() + if a.IPNet.IP.Equal(gwaddr) && aPrefix == subnetPrefix && aBits == subnetBits { + found = true + break + } + } + Expect(found).To(Equal(true)) + + // Check for the veth link in the main namespace; can't + // check the for the specific link since version 0.1.0 + // doesn't report interfaces + links, err := netlink.LinkList() + Expect(err).NotTo(HaveOccurred()) + Expect(len(links)).To(Equal(3)) // Bridge, veth, and loopback + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + // Find the veth peer in the container namespace and the default route + err = targetNs.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + link, err := netlink.LinkByName(IFNAME) + Expect(err).NotTo(HaveOccurred()) + Expect(link.Attrs().Name).To(Equal(IFNAME)) + Expect(link).To(BeAssignableToTypeOf(&netlink.Veth{})) + + addrs, err := netlink.AddrList(link, syscall.AF_INET) + Expect(err).NotTo(HaveOccurred()) + Expect(len(addrs)).To(Equal(1)) + + hwAddr := fmt.Sprintf("%s", link.Attrs().HardwareAddr) + Expect(hwAddr).To(HavePrefix(hwaddr.PrivateMACPrefixString)) + + // Ensure the default route + routes, err := netlink.RouteList(link, 0) + Expect(err).NotTo(HaveOccurred()) + + var defaultRouteFound bool + for _, route := range routes { + defaultRouteFound = (route.Dst == nil && route.Src == nil && route.Gw.Equal(gwaddr)) + if defaultRouteFound { + break + } + } + Expect(defaultRouteFound).To(Equal(true)) + + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + err = originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + err := testutils.CmdDelWithResult(targetNs.Path(), IFNAME, func() error { + return cmdDel(args) + }) + Expect(err).NotTo(HaveOccurred()) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + // Make sure the container veth has been deleted; cannot check + // host veth as version 0.1.0 can't report its name + err = originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + link, err := netlink.LinkByName(IFNAME) + Expect(err).To(HaveOccurred()) + Expect(link).To(BeNil()) + return nil + }) }) It("ensure bridge address", func() { @@ -262,7 +414,7 @@ var _ = Describe("bridge Operations", func() { conf := &NetConf{ NetConf: types.NetConf{ - CNIVersion: "0.2.0", + CNIVersion: "0.3.0", Name: "testConfig", Type: "bridge", }, @@ -285,7 +437,7 @@ var _ = Describe("bridge Operations", func() { err := originalNS.Do(func(ns.NetNS) error { defer GinkgoRecover() - bridge, err := setupBridge(conf) + bridge, _, err := setupBridge(conf) Expect(err).NotTo(HaveOccurred()) // Check if ForceAddress has default value Expect(conf.ForceAddress).To(Equal(false)) diff --git a/plugins/main/ipvlan/ipvlan.go b/plugins/main/ipvlan/ipvlan.go index a0f47705..3afced35 100644 --- a/plugins/main/ipvlan/ipvlan.go +++ b/plugins/main/ipvlan/ipvlan.go @@ -44,15 +44,15 @@ func init() { runtime.LockOSThread() } -func loadConf(bytes []byte) (*NetConf, error) { +func loadConf(bytes []byte) (*NetConf, string, error) { n := &NetConf{} if err := json.Unmarshal(bytes, n); err != nil { - return nil, fmt.Errorf("failed to load netconf: %v", err) + return nil, "", fmt.Errorf("failed to load netconf: %v", err) } if n.Master == "" { - return nil, fmt.Errorf(`"master" field is required. It specifies the host interface name to virtualize`) + return nil, "", fmt.Errorf(`"master" field is required. It specifies the host interface name to virtualize`) } - return n, nil + return n, n.CNIVersion, nil } func modeFromString(s string) (netlink.IPVlanMode, error) { @@ -68,22 +68,24 @@ func modeFromString(s string) (netlink.IPVlanMode, error) { } } -func createIpvlan(conf *NetConf, ifName string, netns ns.NetNS) error { +func createIpvlan(conf *NetConf, ifName string, netns ns.NetNS) (*current.Interface, error) { + ipvlan := ¤t.Interface{} + mode, err := modeFromString(conf.Mode) if err != nil { - return err + return nil, err } m, err := netlink.LinkByName(conf.Master) if err != nil { - return fmt.Errorf("failed to lookup master %q: %v", conf.Master, err) + return nil, fmt.Errorf("failed to lookup master %q: %v", conf.Master, err) } // due to kernel bug we have to create with tmpname or it might // collide with the name on the host and error out tmpName, err := ip.RandomVethName() if err != nil { - return err + return nil, err } mv := &netlink.IPVlan{ @@ -97,20 +99,35 @@ func createIpvlan(conf *NetConf, ifName string, netns ns.NetNS) error { } if err := netlink.LinkAdd(mv); err != nil { - return fmt.Errorf("failed to create ipvlan: %v", err) + return nil, fmt.Errorf("failed to create ipvlan: %v", err) } - return netns.Do(func(_ ns.NetNS) error { + err = netns.Do(func(_ ns.NetNS) error { err := ip.RenameLink(tmpName, ifName) if err != nil { return fmt.Errorf("failed to rename ipvlan to %q: %v", ifName, err) } + ipvlan.Name = ifName + + // Re-fetch ipvlan to get all properties/attributes + contIpvlan, err := netlink.LinkByName(ipvlan.Name) + if err != nil { + return fmt.Errorf("failed to refetch ipvlan %q: %v", ipvlan.Name, err) + } + ipvlan.Mac = contIpvlan.Attrs().HardwareAddr.String() + ipvlan.Sandbox = netns.Path() + return nil }) + if err != nil { + return nil, err + } + + return ipvlan, nil } func cmdAdd(args *skel.CmdArgs) error { - n, err := loadConf(args.StdinData) + n, cniVersion, err := loadConf(args.StdinData) if err != nil { return err } @@ -121,7 +138,8 @@ func cmdAdd(args *skel.CmdArgs) error { } defer netns.Close() - if err = createIpvlan(n, args.IfName, netns); err != nil { + ipvlanInterface, err := createIpvlan(n, args.IfName, netns) + if err != nil { return err } @@ -130,14 +148,21 @@ func cmdAdd(args *skel.CmdArgs) error { if err != nil { return err } - result, err := current.GetResult(r) + // Convert whatever the IPAM result was into the current Result type + result, err := current.NewResultFromResult(r) if err != nil { return err } - if result.IP4 == nil { - return errors.New("IPAM plugin returned missing IPv4 config") + if len(result.IPs) == 0 { + return errors.New("IPAM plugin returned missing IP config") } + for _, ipc := range result.IPs { + // All addresses belong to the ipvlan interface + ipc.Interface = 0 + } + + result.Interfaces = []*current.Interface{ipvlanInterface} err = netns.Do(func(_ ns.NetNS) error { return ipam.ConfigureIface(args.IfName, result) @@ -147,11 +172,12 @@ func cmdAdd(args *skel.CmdArgs) error { } result.DNS = n.DNS - return result.Print() + + return types.PrintResult(result, cniVersion) } func cmdDel(args *skel.CmdArgs) error { - n, err := loadConf(args.StdinData) + n, _, err := loadConf(args.StdinData) if err != nil { return err } @@ -171,5 +197,5 @@ func cmdDel(args *skel.CmdArgs) error { } func main() { - skel.PluginMain(cmdAdd, cmdDel, version.Legacy) + skel.PluginMain(cmdAdd, cmdDel, version.All) } diff --git a/plugins/main/ipvlan/ipvlan_test.go b/plugins/main/ipvlan/ipvlan_test.go index d9c97644..7f2d64ed 100644 --- a/plugins/main/ipvlan/ipvlan_test.go +++ b/plugins/main/ipvlan/ipvlan_test.go @@ -16,11 +16,14 @@ package main import ( "fmt" + "net" + "syscall" "github.com/containernetworking/cni/pkg/ns" "github.com/containernetworking/cni/pkg/skel" "github.com/containernetworking/cni/pkg/testutils" "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/types/current" "github.com/vishvananda/netlink" @@ -63,7 +66,7 @@ var _ = Describe("ipvlan Operations", func() { It("creates an ipvlan link in a non-default namespace", func() { conf := &NetConf{ NetConf: types.NetConf{ - CNIVersion: "0.2.0", + CNIVersion: "0.3.0", Name: "testConfig", Type: "ipvlan", }, @@ -80,10 +83,11 @@ var _ = Describe("ipvlan Operations", func() { err = originalNS.Do(func(ns.NetNS) error { defer GinkgoRecover() - err := createIpvlan(conf, "foobar0", targetNs) + _, err := createIpvlan(conf, "foobar0", targetNs) Expect(err).NotTo(HaveOccurred()) return nil }) + Expect(err).NotTo(HaveOccurred()) // Make sure ipvlan link exists in the target namespace @@ -102,7 +106,7 @@ var _ = Describe("ipvlan Operations", func() { const IFNAME = "ipvl0" conf := fmt.Sprintf(`{ - "cniVersion": "0.2.0", + "cniVersion": "0.3.0", "name": "mynet", "type": "ipvlan", "master": "%s", @@ -123,13 +127,21 @@ var _ = Describe("ipvlan Operations", func() { StdinData: []byte(conf), } + var result *current.Result err = originalNS.Do(func(ns.NetNS) error { defer GinkgoRecover() - _, _, err := testutils.CmdAddWithResult(targetNs.Path(), IFNAME, []byte(conf), func() error { + r, _, err := testutils.CmdAddWithResult(targetNs.Path(), IFNAME, []byte(conf), func() error { return cmdAdd(args) }) Expect(err).NotTo(HaveOccurred()) + + result, err = current.GetResult(r) + Expect(err).NotTo(HaveOccurred()) + + Expect(len(result.Interfaces)).To(Equal(1)) + Expect(result.Interfaces[0].Name).To(Equal(IFNAME)) + Expect(len(result.IPs)).To(Equal(1)) return nil }) Expect(err).NotTo(HaveOccurred()) @@ -141,6 +153,14 @@ var _ = Describe("ipvlan Operations", func() { link, err := netlink.LinkByName(IFNAME) Expect(err).NotTo(HaveOccurred()) Expect(link.Attrs().Name).To(Equal(IFNAME)) + + hwaddr, err := net.ParseMAC(result.Interfaces[0].Mac) + Expect(err).NotTo(HaveOccurred()) + Expect(link.Attrs().HardwareAddr).To(Equal(hwaddr)) + + addrs, err := netlink.AddrList(link, syscall.AF_INET) + Expect(err).NotTo(HaveOccurred()) + Expect(len(addrs)).To(Equal(1)) return nil }) Expect(err).NotTo(HaveOccurred()) diff --git a/plugins/main/loopback/loopback.go b/plugins/main/loopback/loopback.go index d2b69f99..6441ba5c 100644 --- a/plugins/main/loopback/loopback.go +++ b/plugins/main/loopback/loopback.go @@ -68,5 +68,5 @@ func cmdDel(args *skel.CmdArgs) error { } func main() { - skel.PluginMain(cmdAdd, cmdDel, version.Legacy) + skel.PluginMain(cmdAdd, cmdDel, version.All) } diff --git a/plugins/main/macvlan/macvlan.go b/plugins/main/macvlan/macvlan.go index 52c48700..c4502f1f 100644 --- a/plugins/main/macvlan/macvlan.go +++ b/plugins/main/macvlan/macvlan.go @@ -18,6 +18,7 @@ import ( "encoding/json" "errors" "fmt" + "net" "runtime" "github.com/containernetworking/cni/pkg/ip" @@ -49,15 +50,15 @@ func init() { runtime.LockOSThread() } -func loadConf(bytes []byte) (*NetConf, error) { +func loadConf(bytes []byte) (*NetConf, string, error) { n := &NetConf{} if err := json.Unmarshal(bytes, n); err != nil { - return nil, fmt.Errorf("failed to load netconf: %v", err) + return nil, "", fmt.Errorf("failed to load netconf: %v", err) } if n.Master == "" { - return nil, fmt.Errorf(`"master" field is required. It specifies the host interface name to virtualize`) + return nil, "", fmt.Errorf(`"master" field is required. It specifies the host interface name to virtualize`) } - return n, nil + return n, n.CNIVersion, nil } func modeFromString(s string) (netlink.MacvlanMode, error) { @@ -75,22 +76,24 @@ func modeFromString(s string) (netlink.MacvlanMode, error) { } } -func createMacvlan(conf *NetConf, ifName string, netns ns.NetNS) error { +func createMacvlan(conf *NetConf, ifName string, netns ns.NetNS) (*current.Interface, error) { + macvlan := ¤t.Interface{} + mode, err := modeFromString(conf.Mode) if err != nil { - return err + return nil, err } m, err := netlink.LinkByName(conf.Master) if err != nil { - return fmt.Errorf("failed to lookup master %q: %v", conf.Master, err) + return nil, fmt.Errorf("failed to lookup master %q: %v", conf.Master, err) } // due to kernel bug we have to create with tmpName or it might // collide with the name on the host and error out tmpName, err := ip.RandomVethName() if err != nil { - return err + return nil, err } mv := &netlink.Macvlan{ @@ -104,10 +107,10 @@ func createMacvlan(conf *NetConf, ifName string, netns ns.NetNS) error { } if err := netlink.LinkAdd(mv); err != nil { - return fmt.Errorf("failed to create macvlan: %v", err) + return nil, fmt.Errorf("failed to create macvlan: %v", err) } - return netns.Do(func(_ ns.NetNS) error { + err = netns.Do(func(_ ns.NetNS) error { // TODO: duplicate following lines for ipv6 support, when it will be added in other places ipv4SysctlValueName := fmt.Sprintf(IPv4InterfaceArpProxySysctlTemplate, tmpName) if _, err := sysctl.Sysctl(ipv4SysctlValueName, "1"); err != nil { @@ -121,12 +124,27 @@ func createMacvlan(conf *NetConf, ifName string, netns ns.NetNS) error { _ = netlink.LinkDel(mv) return fmt.Errorf("failed to rename macvlan to %q: %v", ifName, err) } + macvlan.Name = ifName + + // Re-fetch macvlan to get all properties/attributes + contMacvlan, err := netlink.LinkByName(ifName) + if err != nil { + return fmt.Errorf("failed to refetch macvlan %q: %v", ifName, err) + } + macvlan.Mac = contMacvlan.Attrs().HardwareAddr.String() + macvlan.Sandbox = netns.Path() + return nil }) + if err != nil { + return nil, err + } + + return macvlan, nil } func cmdAdd(args *skel.CmdArgs) error { - n, err := loadConf(args.StdinData) + n, cniVersion, err := loadConf(args.StdinData) if err != nil { return err } @@ -137,7 +155,8 @@ func cmdAdd(args *skel.CmdArgs) error { } defer netns.Close() - if err = createMacvlan(n, args.IfName, netns); err != nil { + macvlanInterface, err := createMacvlan(n, args.IfName, netns) + if err != nil { return err } @@ -146,32 +165,60 @@ func cmdAdd(args *skel.CmdArgs) error { if err != nil { return err } - result, err := current.GetResult(r) + // Convert whatever the IPAM result was into the current Result type + result, err := current.NewResultFromResult(r) if err != nil { return err } - if result.IP4 == nil { - return errors.New("IPAM plugin returned missing IPv4 config") + if len(result.IPs) == 0 { + return errors.New("IPAM plugin returned missing IP config") + } + result.Interfaces = []*current.Interface{macvlanInterface} + + var firstV4Addr net.IP + for _, ipc := range result.IPs { + // All addresses apply to the container macvlan interface + ipc.Interface = 0 + + if ipc.Address.IP.To4() != nil && firstV4Addr == nil { + firstV4Addr = ipc.Address.IP + } } - err = netns.Do(func(_ ns.NetNS) error { - if err := ip.SetHWAddrByIP(args.IfName, result.IP4.IP.IP, nil /* TODO IPv6 */); err != nil { + if firstV4Addr != nil { + err = netns.Do(func(_ ns.NetNS) error { + if err := ip.SetHWAddrByIP(args.IfName, firstV4Addr, nil /* TODO IPv6 */); err != nil { + return err + } + + return ipam.ConfigureIface(args.IfName, result) + }) + if err != nil { return err } + } - return ipam.ConfigureIface(args.IfName, result) + // Re-fetch macvlan interface as its MAC address may have changed + err = netns.Do(func(_ ns.NetNS) error { + link, err := netlink.LinkByName(args.IfName) + if err != nil { + return fmt.Errorf("failed to re-fetch macvlan interface: %v", err) + } + macvlanInterface.Mac = link.Attrs().HardwareAddr.String() + return nil }) if err != nil { return err } result.DNS = n.DNS - return result.Print() + + return types.PrintResult(result, cniVersion) } func cmdDel(args *skel.CmdArgs) error { - n, err := loadConf(args.StdinData) + n, _, err := loadConf(args.StdinData) if err != nil { return err } @@ -191,5 +238,5 @@ func cmdDel(args *skel.CmdArgs) error { } func main() { - skel.PluginMain(cmdAdd, cmdDel, version.Legacy) + skel.PluginMain(cmdAdd, cmdDel, version.All) } diff --git a/plugins/main/macvlan/macvlan_test.go b/plugins/main/macvlan/macvlan_test.go index ead07009..dfb4e8c6 100644 --- a/plugins/main/macvlan/macvlan_test.go +++ b/plugins/main/macvlan/macvlan_test.go @@ -16,11 +16,14 @@ package main import ( "fmt" + "net" + "syscall" "github.com/containernetworking/cni/pkg/ns" "github.com/containernetworking/cni/pkg/skel" "github.com/containernetworking/cni/pkg/testutils" "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/types/current" "github.com/containernetworking/cni/pkg/utils/hwaddr" "github.com/vishvananda/netlink" @@ -64,7 +67,7 @@ var _ = Describe("macvlan Operations", func() { It("creates an macvlan link in a non-default namespace", func() { conf := &NetConf{ NetConf: types.NetConf{ - CNIVersion: "0.2.0", + CNIVersion: "0.3.0", Name: "testConfig", Type: "macvlan", }, @@ -80,7 +83,7 @@ var _ = Describe("macvlan Operations", func() { err = originalNS.Do(func(ns.NetNS) error { defer GinkgoRecover() - err = createMacvlan(conf, "foobar0", targetNs) + _, err = createMacvlan(conf, "foobar0", targetNs) Expect(err).NotTo(HaveOccurred()) return nil }) @@ -102,7 +105,7 @@ var _ = Describe("macvlan Operations", func() { const IFNAME = "macvl0" conf := fmt.Sprintf(`{ - "cniVersion": "0.2.0", + "cniVersion": "0.3.0", "name": "mynet", "type": "macvlan", "master": "%s", @@ -123,14 +126,21 @@ var _ = Describe("macvlan Operations", func() { StdinData: []byte(conf), } - // Make sure macvlan link exists in the target namespace + var result *current.Result err = originalNS.Do(func(ns.NetNS) error { defer GinkgoRecover() - _, _, err := testutils.CmdAddWithResult(targetNs.Path(), IFNAME, []byte(conf), func() error { + r, _, err := testutils.CmdAddWithResult(targetNs.Path(), IFNAME, []byte(conf), func() error { return cmdAdd(args) }) Expect(err).NotTo(HaveOccurred()) + + result, err = current.GetResult(r) + Expect(err).NotTo(HaveOccurred()) + + Expect(len(result.Interfaces)).To(Equal(1)) + Expect(result.Interfaces[0].Name).To(Equal(IFNAME)) + Expect(len(result.IPs)).To(Equal(1)) return nil }) Expect(err).NotTo(HaveOccurred()) @@ -143,9 +153,16 @@ var _ = Describe("macvlan Operations", func() { Expect(err).NotTo(HaveOccurred()) Expect(link.Attrs().Name).To(Equal(IFNAME)) - hwAddr := fmt.Sprintf("%s", link.Attrs().HardwareAddr) - Expect(hwAddr).To(HavePrefix(hwaddr.PrivateMACPrefixString)) + hwaddrString := fmt.Sprintf("%s", link.Attrs().HardwareAddr) + Expect(hwaddrString).To(HavePrefix(hwaddr.PrivateMACPrefixString)) + hwaddr, err := net.ParseMAC(result.Interfaces[0].Mac) + Expect(err).NotTo(HaveOccurred()) + Expect(link.Attrs().HardwareAddr).To(Equal(hwaddr)) + + addrs, err := netlink.AddrList(link, syscall.AF_INET) + Expect(err).NotTo(HaveOccurred()) + Expect(len(addrs)).To(Equal(1)) return nil }) Expect(err).NotTo(HaveOccurred()) diff --git a/plugins/main/ptp/ptp.go b/plugins/main/ptp/ptp.go index efda0bea..796c4588 100644 --- a/plugins/main/ptp/ptp.go +++ b/plugins/main/ptp/ptp.go @@ -47,7 +47,7 @@ type NetConf struct { MTU int `json:"mtu"` } -func setupContainerVeth(netns, ifName string, mtu int, pr *current.Result) (string, error) { +func setupContainerVeth(netns, ifName string, mtu int, pr *current.Result) (*current.Interface, *current.Interface, error) { // The IPAM result will be something like IP=192.168.3.5/24, GW=192.168.3.1. // What we want is really a point-to-point link but veth does not support IFF_POINTOPONT. // Next best thing would be to let it ARP but set interface to 192.168.3.5/32 and @@ -59,104 +59,139 @@ func setupContainerVeth(netns, ifName string, mtu int, pr *current.Result) (stri // "192.168.3.1/32 dev $ifName" and "192.168.3.0/24 via 192.168.3.1 dev $ifName". // In other words we force all traffic to ARP via the gateway except for GW itself. - var hostVethName string + hostInterface := ¤t.Interface{} + containerInterface := ¤t.Interface{} + err := ns.WithNetNSPath(netns, func(hostNS ns.NetNS) error { - hostVeth, _, err := ip.SetupVeth(ifName, mtu, hostNS) + hostVeth, contVeth, err := ip.SetupVeth(ifName, mtu, hostNS) if err != nil { return err } + hostInterface.Name = hostVeth.Attrs().Name + hostInterface.Mac = hostVeth.Attrs().HardwareAddr.String() + containerInterface.Name = contVeth.Attrs().Name + containerInterface.Mac = contVeth.Attrs().HardwareAddr.String() - hostNS.Do(func(_ ns.NetNS) error { - hostVethName = hostVeth.Attrs().Name - if err := ip.SetHWAddrByIP(hostVethName, pr.IP4.IP.IP, nil /* TODO IPv6 */); err != nil { - return fmt.Errorf("failed to set hardware addr by IP: %v", err) + var firstV4Addr net.IP + for _, ipc := range pr.IPs { + // All addresses apply to the container veth interface + ipc.Interface = 1 + + if ipc.Address.IP.To4() != nil && firstV4Addr == nil { + firstV4Addr = ipc.Address.IP } + } - return nil - }) + pr.Interfaces = []*current.Interface{hostInterface, containerInterface} + + if firstV4Addr != nil { + err = hostNS.Do(func(_ ns.NetNS) error { + hostVethName := hostVeth.Attrs().Name + if err := ip.SetHWAddrByIP(hostVethName, firstV4Addr, nil /* TODO IPv6 */); err != nil { + return fmt.Errorf("failed to set hardware addr by IP: %v", err) + } + + return nil + }) + if err != nil { + return err + } + } if err = ipam.ConfigureIface(ifName, pr); err != nil { return err } - contVeth, err := netlink.LinkByName(ifName) + if err := ip.SetHWAddrByIP(contVeth.Attrs().Name, firstV4Addr, nil /* TODO IPv6 */); err != nil { + return fmt.Errorf("failed to set hardware addr by IP: %v", err) + } + + // Re-fetch container veth to update attributes + contVeth, err = netlink.LinkByName(ifName) if err != nil { return fmt.Errorf("failed to look up %q: %v", ifName, err) } - if err := ip.SetHWAddrByIP(contVeth.Attrs().Name, pr.IP4.IP.IP, nil /* TODO IPv6 */); err != nil { - return fmt.Errorf("failed to set hardware addr by IP: %v", err) - } - - // Delete the route that was automatically added - route := netlink.Route{ - LinkIndex: contVeth.Attrs().Index, - Dst: &net.IPNet{ - IP: pr.IP4.IP.IP.Mask(pr.IP4.IP.Mask), - Mask: pr.IP4.IP.Mask, - }, - Scope: netlink.SCOPE_NOWHERE, - } - - if err := netlink.RouteDel(&route); err != nil { - return fmt.Errorf("failed to delete route %v: %v", route, err) - } - - for _, r := range []netlink.Route{ - { + for _, ipc := range pr.IPs { + // Delete the route that was automatically added + route := netlink.Route{ LinkIndex: contVeth.Attrs().Index, Dst: &net.IPNet{ - IP: pr.IP4.Gateway, - Mask: net.CIDRMask(32, 32), + IP: ipc.Address.IP.Mask(ipc.Address.Mask), + Mask: ipc.Address.Mask, }, - Scope: netlink.SCOPE_LINK, - Src: pr.IP4.IP.IP, - }, - { - LinkIndex: contVeth.Attrs().Index, - Dst: &net.IPNet{ - IP: pr.IP4.IP.IP.Mask(pr.IP4.IP.Mask), - Mask: pr.IP4.IP.Mask, + Scope: netlink.SCOPE_NOWHERE, + } + + if err := netlink.RouteDel(&route); err != nil { + return fmt.Errorf("failed to delete route %v: %v", route, err) + } + + for _, r := range []netlink.Route{ + netlink.Route{ + LinkIndex: contVeth.Attrs().Index, + Dst: &net.IPNet{ + IP: ipc.Gateway, + Mask: net.CIDRMask(32, 32), + }, + Scope: netlink.SCOPE_LINK, + Src: ipc.Address.IP, }, - Scope: netlink.SCOPE_UNIVERSE, - Gw: pr.IP4.Gateway, - Src: pr.IP4.IP.IP, - }, - } { - if err := netlink.RouteAdd(&r); err != nil { - return fmt.Errorf("failed to add route %v: %v", r, err) + netlink.Route{ + LinkIndex: contVeth.Attrs().Index, + Dst: &net.IPNet{ + IP: ipc.Address.IP.Mask(ipc.Address.Mask), + Mask: ipc.Address.Mask, + }, + Scope: netlink.SCOPE_UNIVERSE, + Gw: ipc.Gateway, + Src: ipc.Address.IP, + }, + } { + if err := netlink.RouteAdd(&r); err != nil { + return fmt.Errorf("failed to add route %v: %v", r, err) + } } } return nil }) - return hostVethName, err + if err != nil { + return nil, nil, err + } + return hostInterface, containerInterface, nil } -func setupHostVeth(vethName string, ipConf *current.IPConfig) error { +func setupHostVeth(vethName string, result *current.Result) error { // hostVeth moved namespaces and may have a new ifindex veth, err := netlink.LinkByName(vethName) if err != nil { return fmt.Errorf("failed to lookup %q: %v", vethName, err) } - // TODO(eyakubovich): IPv6 - ipn := &net.IPNet{ - IP: ipConf.Gateway, - Mask: net.CIDRMask(32, 32), - } - addr := &netlink.Addr{IPNet: ipn, Label: ""} - if err = netlink.AddrAdd(veth, addr); err != nil { - return fmt.Errorf("failed to add IP addr (%#v) to veth: %v", ipn, err) - } + for _, ipc := range result.IPs { + maskLen := 128 + if ipc.Address.IP.To4() != nil { + maskLen = 32 + } - ipn = &net.IPNet{ - IP: ipConf.IP.IP, - Mask: net.CIDRMask(32, 32), - } - // dst happens to be the same as IP/net of host veth - if err = ip.AddHostRoute(ipn, nil, veth); err != nil && !os.IsExist(err) { - return fmt.Errorf("failed to add route on host: %v", err) + ipn := &net.IPNet{ + IP: ipc.Gateway, + Mask: net.CIDRMask(maskLen, maskLen), + } + addr := &netlink.Addr{IPNet: ipn, Label: ""} + if err = netlink.AddrAdd(veth, addr); err != nil { + return fmt.Errorf("failed to add IP addr (%#v) to veth: %v", ipn, err) + } + + ipn = &net.IPNet{ + IP: ipc.Address.IP, + Mask: net.CIDRMask(maskLen, maskLen), + } + // dst happens to be the same as IP/net of host veth + if err = ip.AddHostRoute(ipn, nil, veth); err != nil && !os.IsExist(err) { + return fmt.Errorf("failed to add route on host: %v", err) + } } return nil @@ -177,33 +212,39 @@ func cmdAdd(args *skel.CmdArgs) error { if err != nil { return err } - result, err := current.GetResult(r) - if err != nil { - return err - } - if result.IP4 == nil { - return errors.New("IPAM plugin returned missing IPv4 config") - } - - hostVethName, err := setupContainerVeth(args.Netns, args.IfName, conf.MTU, result) + // Convert whatever the IPAM result was into the current Result type + result, err := current.NewResultFromResult(r) if err != nil { return err } - if err = setupHostVeth(hostVethName, result.IP4); err != nil { + if len(result.IPs) == 0 { + return errors.New("IPAM plugin returned missing IP config") + } + + hostInterface, containerInterface, err := setupContainerVeth(args.Netns, args.IfName, conf.MTU, result) + if err != nil { + return err + } + + if err = setupHostVeth(hostInterface.Name, result); err != nil { return err } if conf.IPMasq { chain := utils.FormatChainName(conf.Name, args.ContainerID) comment := utils.FormatComment(conf.Name, args.ContainerID) - if err = ip.SetupIPMasq(&result.IP4.IP, chain, comment); err != nil { - return err + for _, ipc := range result.IPs { + if err = ip.SetupIPMasq(&ipc.Address, chain, comment); err != nil { + return err + } } } result.DNS = conf.DNS - return result.Print() + result.Interfaces = []*current.Interface{hostInterface, containerInterface} + + return types.PrintResult(result, conf.CNIVersion) } func cmdDel(args *skel.CmdArgs) error { @@ -242,5 +283,5 @@ func cmdDel(args *skel.CmdArgs) error { } func main() { - skel.PluginMain(cmdAdd, cmdDel, version.Legacy) + skel.PluginMain(cmdAdd, cmdDel, version.All) } diff --git a/plugins/main/ptp/ptp_test.go b/plugins/main/ptp/ptp_test.go index 4587d275..0330a2c6 100644 --- a/plugins/main/ptp/ptp_test.go +++ b/plugins/main/ptp/ptp_test.go @@ -43,7 +43,7 @@ var _ = Describe("ptp Operations", func() { const IFNAME = "ptp0" conf := `{ - "cniVersion": "0.2.0", + "cniVersion": "0.3.0", "name": "mynet", "type": "ptp", "ipMasq": true, diff --git a/plugins/meta/flannel/flannel.go b/plugins/meta/flannel/flannel.go index 911ec947..e1eb2786 100644 --- a/plugins/meta/flannel/flannel.go +++ b/plugins/meta/flannel/flannel.go @@ -257,5 +257,5 @@ func cmdDel(args *skel.CmdArgs) error { } func main() { - skel.PluginMain(cmdAdd, cmdDel, version.Legacy) + skel.PluginMain(cmdAdd, cmdDel, version.All) } diff --git a/plugins/meta/tuning/tuning.go b/plugins/meta/tuning/tuning.go index b1e7b0f8..9d8e9002 100644 --- a/plugins/meta/tuning/tuning.go +++ b/plugins/meta/tuning/tuning.go @@ -80,5 +80,5 @@ func cmdDel(args *skel.CmdArgs) error { } func main() { - skel.PluginMain(cmdAdd, cmdDel, version.Legacy) + skel.PluginMain(cmdAdd, cmdDel, version.All) } diff --git a/plugins/test/noop/main.go b/plugins/test/noop/main.go index 924bb9dc..a00f7e95 100644 --- a/plugins/test/noop/main.go +++ b/plugins/test/noop/main.go @@ -142,7 +142,7 @@ func debugBehavior(args *skel.CmdArgs, command string) error { } func debugGetSupportedVersions(stdinData []byte) []string { - vers := []string{"0.-42.0", "0.1.0", "0.2.0"} + vers := []string{"0.-42.0", "0.1.0", "0.2.0", "0.3.0"} cniArgs := os.Getenv("CNI_ARGS") if cniArgs == "" { return vers diff --git a/plugins/test/noop/noop_test.go b/plugins/test/noop/noop_test.go index 3573985a..946803c5 100644 --- a/plugins/test/noop/noop_test.go +++ b/plugins/test/noop/noop_test.go @@ -37,7 +37,7 @@ var _ = Describe("No-op plugin", func() { expectedCmdArgs skel.CmdArgs ) - const reportResult = `{ "ip4": { "ip": "10.1.2.3/24" }, "dns": {} }` + const reportResult = `{ "ips": [{ "version": "4", "address": "10.1.2.3/24" }], "dns": {} }` BeforeEach(func() { debug = &noop_debug.Debug{ @@ -64,14 +64,14 @@ var _ = Describe("No-op plugin", func() { // Keep this last "CNI_ARGS=" + args, } - cmd.Stdin = strings.NewReader(`{"some":"stdin-json", "cniVersion": "0.2.0"}`) + cmd.Stdin = strings.NewReader(`{"some":"stdin-json", "cniVersion": "0.3.0"}`) expectedCmdArgs = skel.CmdArgs{ ContainerID: "some-container-id", Netns: "/some/netns/path", IfName: "some-eth0", Args: args, Path: "/some/bin/path", - StdinData: []byte(`{"some":"stdin-json", "cniVersion": "0.2.0"}`), + StdinData: []byte(`{"some":"stdin-json", "cniVersion": "0.3.0"}`), } }) @@ -102,15 +102,15 @@ var _ = Describe("No-op plugin", func() { cmd.Stdin = strings.NewReader(`{ "some":"stdin-json", - "cniVersion": "0.2.0", + "cniVersion": "0.3.0", "prevResult": { - "ip4": {"ip": "10.1.2.15/24"} + "ips": [{"version": "4", "address": "10.1.2.15/24"}] } }`) session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter) Expect(err).NotTo(HaveOccurred()) Eventually(session).Should(gexec.Exit(0)) - Expect(session.Out.Contents()).To(MatchJSON(`{"ip4": {"ip": "10.1.2.15/24"}, "dns": {}}`)) + Expect(session.Out.Contents()).To(MatchJSON(`{"ips": [{"version": "4", "address": "10.1.2.15/24"}], "dns": {}}`)) }) It("injects DNS into previous result when ReportResult is INJECT-DNS", func() { @@ -119,9 +119,9 @@ var _ = Describe("No-op plugin", func() { cmd.Stdin = strings.NewReader(`{ "some":"stdin-json", - "cniVersion": "0.2.0", + "cniVersion": "0.3.0", "prevResult": { - "ip4": {"ip": "10.1.2.3/24"}, + "ips": [{"version": "4", "address": "10.1.2.3/24"}], "dns": {} } }`) @@ -130,7 +130,7 @@ var _ = Describe("No-op plugin", func() { Expect(err).NotTo(HaveOccurred()) Eventually(session).Should(gexec.Exit(0)) Expect(session.Out.Contents()).To(MatchJSON(`{ - "ip4": {"ip": "10.1.2.3/24"}, + "ips": [{"version": "4", "address": "10.1.2.3/24"}], "dns": {"nameservers": ["1.2.3.4"]} }`)) }) @@ -139,7 +139,7 @@ var _ = Describe("No-op plugin", func() { // Remove the DEBUG option from CNI_ARGS and regular args newArgs := "FOO=BAR" cmd.Env[len(cmd.Env)-1] = "CNI_ARGS=" + newArgs - newStdin := fmt.Sprintf(`{"some":"stdin-json", "cniVersion": "0.2.0", "debugFile": "%s"}`, debugFileName) + newStdin := fmt.Sprintf(`{"some":"stdin-json", "cniVersion": "0.3.0", "debugFile": "%s"}`, debugFileName) cmd.Stdin = strings.NewReader(newStdin) expectedCmdArgs.Args = newArgs expectedCmdArgs.StdinData = []byte(newStdin) diff --git a/test b/test index d8a1fe37..6629f224 100755 --- a/test +++ b/test @@ -11,7 +11,7 @@ set -e source ./build -TESTABLE="libcni plugins/ipam/dhcp plugins/ipam/host-local plugins/ipam/host-local/backend/allocator plugins/main/loopback pkg/invoke pkg/ns pkg/skel pkg/types pkg/types/current pkg/utils plugins/main/ipvlan plugins/main/macvlan plugins/main/bridge plugins/main/ptp plugins/test/noop pkg/utils/hwaddr pkg/ip pkg/version pkg/version/testhelpers plugins/meta/flannel pkg/ipam" +TESTABLE="libcni plugins/ipam/dhcp plugins/ipam/host-local plugins/ipam/host-local/backend/allocator plugins/main/loopback pkg/invoke pkg/ns pkg/skel pkg/types pkg/types/current pkg/types/020 pkg/utils plugins/main/ipvlan plugins/main/macvlan plugins/main/bridge plugins/main/ptp plugins/test/noop pkg/utils/hwaddr pkg/ip pkg/version pkg/version/testhelpers plugins/meta/flannel pkg/ipam" FORMATTABLE="$TESTABLE pkg/testutils plugins/meta/flannel plugins/meta/tuning" # user has not provided PKG override