From 06b397912bf4d3ad97119930de313339be02cecd Mon Sep 17 00:00:00 2001 From: Dan Williams Date: Tue, 22 Nov 2016 11:32:35 -0600 Subject: [PATCH] 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. --- invoke/exec_test.go | 7 +- invoke/raw_exec_test.go | 2 +- ipam/ipam.go | 36 ++++- ipam/ipam_test.go | 98 +++++++++++--- skel/skel_test.go | 4 +- types/020/types.go | 133 ++++++++++++++++++ types/020/types_suite_test.go | 27 ++++ types/020/types_test.go | 128 ++++++++++++++++++ types/current/types.go | 202 +++++++++++++++++++++++----- types/current/types_suite_test.go | 2 +- types/current/types_test.go | 160 +++++++++++++++++----- types/types.go | 5 + version/legacy_examples/examples.go | 6 +- version/version.go | 5 +- 14 files changed, 709 insertions(+), 106 deletions(-) create mode 100644 types/020/types.go create mode 100644 types/020/types_suite_test.go create mode 100644 types/020/types_test.go diff --git a/invoke/exec_test.go b/invoke/exec_test.go index 3e207c14..7e804ab7 100644 --- a/invoke/exec_test.go +++ b/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/invoke/raw_exec_test.go b/invoke/raw_exec_test.go index b0ca9607..5ab23ae5 100644 --- a/invoke/raw_exec_test.go +++ b/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/ipam/ipam.go b/ipam/ipam.go index 8dd861a6..b76780f0 100644 --- a/ipam/ipam.go +++ b/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/ipam/ipam_test.go b/ipam/ipam_test.go index 622e4c8a..2d27825d 100644 --- a/ipam/ipam_test.go +++ b/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/skel/skel_test.go b/skel/skel_test.go index 6652fcdb..d7f729f0 100644 --- a/skel/skel_test.go +++ b/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/types/020/types.go b/types/020/types.go new file mode 100644 index 00000000..666cfe93 --- /dev/null +++ b/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/types/020/types_suite_test.go b/types/020/types_suite_test.go new file mode 100644 index 00000000..095d73e2 --- /dev/null +++ b/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/types/020/types_test.go b/types/020/types_test.go new file mode 100644 index 00000000..1bcdda73 --- /dev/null +++ b/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/types/current/types.go b/types/current/types.go index 338b3fd2..e686a9a7 100644 --- a/types/current/types.go +++ b/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/types/current/types_suite_test.go b/types/current/types_suite_test.go index 42a47a25..89cccecd 100644 --- a/types/current/types_suite_test.go +++ b/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/types/current/types_test.go b/types/current/types_test.go index 3810999d..f839cc90 100644 --- a/types/current/types_test.go +++ b/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/types/types.go b/types/types.go index 2ceffebc..a81ac702 100644 --- a/types/types.go +++ b/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/version/legacy_examples/examples.go b/version/legacy_examples/examples.go index 8b079a3d..1bf406b3 100644 --- a/version/legacy_examples/examples.go +++ b/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/version/version.go b/version/version.go index e777e52c..7c589633 100644 --- a/version/version.go +++ b/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