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