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