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.
This commit is contained in:
Dan Williams 2016-11-22 11:32:35 -06:00
parent befad17174
commit d5acb127b8
40 changed files with 1653 additions and 499 deletions

171
SPEC.md
View File

@ -1,7 +1,7 @@
# Container Networking Interface Proposal # Container Networking Interface Proposal
## Version ## Version
This is CNI **spec** version **0.2.0**. This is CNI **spec** version **0.3.0**.
Note that this is **independent from the version of the CNI library and plugins** in this repository (e.g. the versions of [releases](https://github.com/containernetworking/cni/releases)). Note that this is **independent from the version of the CNI library and plugins** in this repository (e.g. the versions of [releases](https://github.com/containernetworking/cni/releases)).
@ -57,7 +57,8 @@ The operations that the CNI plugin needs to support are:
- **Extra arguments**. This provides an alternative mechanism to allow simple configuration of CNI plugins on a per-container basis. - **Extra arguments**. This provides an alternative mechanism to allow simple configuration of CNI plugins on a per-container basis.
- **Name of the interface inside the container**. This is the name that should be assigned to the interface created inside the container (network namespace); consequently it must comply with the standard Linux restrictions on interface names. - **Name of the interface inside the container**. This is the name that should be assigned to the interface created inside the container (network namespace); consequently it must comply with the standard Linux restrictions on interface names.
- Result: - Result:
- **IPs assigned to the interface**. This is either an IPv4 address, an IPv6 address, or both. - **Interfaces list**. Depending on the plugin, this can include the sandbox (eg, container or hypervisor) interface name and/or the host interface name, the hardware addresses of each interface, and details about the sandbox (if any) the interface is in.
- **IP configuration assigned to each interface**. The IPv4 and/or IPv6 addresses, gateways, and routes assigned to sandbox and/or host interfaces.
- **DNS information**. Dictionary that includes DNS information for nameservers, domain, search domains and options. - **DNS information**. Dictionary that includes DNS information for nameservers, domain, search domains and options.
- Delete container from network - Delete container from network
@ -75,8 +76,8 @@ The operations that the CNI plugin needs to support are:
``` ```
{ {
"cniVersion": "0.2.0", // the version of the CNI spec in use for this output "cniVersion": "0.3.0", // the version of the CNI spec in use for this output
"supportedVersions": [ "0.1.0", "0.2.0" ] // the list of CNI spec versions that this plugin supports "supportedVersions": [ "0.1.0", "0.2.0", "0.3.0" ] // the list of CNI spec versions that this plugin supports
} }
``` ```
@ -85,7 +86,7 @@ It will then look for this executable in a list of predefined directories. Once
- `CNI_COMMAND`: indicates the desired operation; `ADD`, `DEL` or `VERSION`. - `CNI_COMMAND`: indicates the desired operation; `ADD`, `DEL` or `VERSION`.
- `CNI_CONTAINERID`: Container ID - `CNI_CONTAINERID`: Container ID
- `CNI_NETNS`: Path to network namespace file - `CNI_NETNS`: Path to network namespace file
- `CNI_IFNAME`: Interface name to set up - `CNI_IFNAME`: Interface name to set up; plugin must honor this interface name or return an error
- `CNI_ARGS`: Extra arguments passed in by the user at invocation time. Alphanumeric key-value pairs separated by semicolons; for example, "FOO=BAR;ABC=123" - `CNI_ARGS`: Extra arguments passed in by the user at invocation time. Alphanumeric key-value pairs separated by semicolons; for example, "FOO=BAR;ABC=123"
- `CNI_PATH`: Colon-separated list of paths to search for CNI plugin executables - `CNI_PATH`: Colon-separated list of paths to search for CNI plugin executables
@ -94,21 +95,36 @@ Network configuration in JSON format is streamed to the plugin through stdin. Th
### Result ### Result
Success is indicated by a return code of zero and the following JSON printed to stdout in the case of the ADD command. This should be the same output as was returned by the IPAM plugin (see [IP Allocation](#ip-allocation) for details). Note that IPAM plugins return an abbreviated `Result` structure as described in [IP Allocation](#ip-allocation).
Success is indicated by a return code of zero and the following JSON printed to stdout in the case of the ADD command. The `ip` and `dns` items should be the same output as was returned by the IPAM plugin (see [IP Allocation](#ip-allocation) for details) except that the plugin should fill in the `interface` indexes appropriately, which are missing from IPAM plugin output since IPAM plugins should be unaware of interfaces.
``` ```
{ {
"cniVersion": "0.2.0", "cniVersion": "0.3.0",
"ip4": { "interfaces": [ (this key omitted by IPAM plugins)
"ip": <ipv4-and-subnet-in-CIDR>, {
"gateway": <ipv4-of-the-gateway>, (optional) "name": "<name>",
"routes": <list-of-ipv4-routes> (optional) "mac": "<MAC address>", (required if L2 addresses are meaningful)
"sandbox": "<netns path or hypervisor identifier>" (required for container/hypervisor interfaces, empty/omitted for host interfaces)
}
],
"ip": [
{
"version": "<4-or-6>",
"address": "<ip-and-prefix-in-CIDR>",
"gateway": "<ip-address-of-the-gateway>", (optional)
"interface": <numeric index into 'interfaces' list>
}, },
"ip6": { ...
"ip": <ipv6-and-subnet-in-CIDR>, ],
"gateway": <ipv6-of-the-gateway>, (optional) "routes": [ (optional)
"routes": <list-of-ipv6-routes> (optional) {
"dst": "<ip-and-prefix-in-cidr>",
"gw": "<ip-of-next-hop>" (optional)
}, },
...
]
"dns": { "dns": {
"nameservers": <list-of-nameservers> (optional) "nameservers": <list-of-nameservers> (optional)
"domain": <name-of-local-domain> (optional) "domain": <name-of-local-domain> (optional)
@ -119,8 +135,20 @@ Success is indicated by a return code of zero and the following JSON printed to
``` ```
`cniVersion` specifies a [Semantic Version 2.0](http://semver.org) of CNI specification used by the plugin. `cniVersion` specifies a [Semantic Version 2.0](http://semver.org) of CNI specification used by the plugin.
`dns` field contains a dictionary consisting of common DNS information that this network is aware of. `interfaces` describes specific network interfaces the plugin created.
The result is returned in the same format as specified in the [configuration](#network-configuration). If the `CNI_IFNAME` variable exists the plugin must use that name for the sandbox/hypervisor interface or return an error if it cannot.
- `mac` (string): the hardware address of the interface.
If L2 addresses are not meaningful for the plugin then this field is optional.
- `sandbox` (string): container/namespace-based environments should return the full filesystem path to the network namespace of that sandbox.
Hypervisor/VM-based plugins should return an ID unique to the virtualized sandbox the interface was created in.
This item must be provided for interfaces created or moved into a sandbox like a network namespace or a hypervisor/VM.
The `ip` field is a list of IP configuration information.
See the [IP well-known structure](#ip) section for more information.
The `dns` field contains a dictionary consisting of common DNS information.
See the [DNS well-known structure](#dns) section for more information.
The specification does not declare how this information must be processed by CNI consumers. The specification does not declare how this information must be processed by CNI consumers.
Examples include generating an `/etc/resolv.conf` file to be injected into the container filesystem or running a DNS forwarder on the host. Examples include generating an `/etc/resolv.conf` file to be injected into the container filesystem or running a DNS forwarder on the host.
@ -164,7 +192,7 @@ Plugins may define additional fields that they accept and may generate an error
```json ```json
{ {
"cniVersion": "0.2.0", "cniVersion": "0.3.0",
"name": "dbnet", "name": "dbnet",
"type": "bridge", "type": "bridge",
// type (plugin) specific // type (plugin) specific
@ -183,7 +211,7 @@ Plugins may define additional fields that they accept and may generate an error
```json ```json
{ {
"cniVersion": "0.2.0", "cniVersion": "0.3.0",
"name": "pci", "name": "pci",
"type": "ovs", "type": "ovs",
// type (plugin) specific // type (plugin) specific
@ -204,7 +232,7 @@ Plugins may define additional fields that they accept and may generate an error
```json ```json
{ {
"cniVersion": "0.1", "cniVersion": "0.3.0",
"name": "wan", "name": "wan",
"type": "macvlan", "type": "macvlan",
// ipam specific // ipam specific
@ -394,17 +422,22 @@ Success is indicated by a zero return code and the following JSON being printed
``` ```
{ {
"cniVersion": "0.2.0", "cniVersion": "0.3.0",
"ip4": { "ips": [
"ip": <ipv4-and-subnet-in-CIDR>, {
"gateway": <ipv4-of-the-gateway>, (optional) "version": "<4-or-6>",
"routes": <list-of-ipv4-routes> (optional) "address": "<ip-and-prefix-in-CIDR>",
"gateway": "<ip-address-of-the-gateway>" (optional)
}, },
"ip6": { ...
"ip": <ipv6-and-subnet-in-CIDR>, ],
"gateway": <ipv6-of-the-gateway>, (optional) "routes": [ (optional)
"routes": <list-of-ipv6-routes> (optional) {
"dst": "<ip-and-prefix-in-cidr>",
"gw": "<ip-of-next-hop>" (optional)
}, },
...
]
"dns": { "dns": {
"nameservers": <list-of-nameservers> (optional) "nameservers": <list-of-nameservers> (optional)
"domain": <name-of-local-domain> (optional) "domain": <name-of-local-domain> (optional)
@ -414,21 +447,15 @@ Success is indicated by a zero return code and the following JSON being printed
} }
``` ```
Note that unlike regular CNI plugins, IPAM plugins return an abbreviated `Result` structure that does not include the `interfaces` key, since IPAM plugins should be unaware of interfaces configured by their parent plugin except those specifically required for IPAM (eg, like the `dhcp` IPAM plugin).
`cniVersion` specifies a [Semantic Version 2.0](http://semver.org) of CNI specification used by the plugin. `cniVersion` specifies a [Semantic Version 2.0](http://semver.org) of CNI specification used by the plugin.
`gateway` is the default gateway for this subnet, if one exists.
It does not instruct the CNI plugin to add any routes with this gateway: routes to add are specified separately via the `routes` field.
An example use of this value is for the CNI plugin to add this IP address to the linux-bridge to make it a gateway.
Each route entry is a dictionary with the following fields: The `ips` field is a list of IP configuration information.
- `dst` (string): Destination subnet specified in CIDR notation. See the [IP well-known structure](#ip) section for more information.
- `gw` (string): IP of the gateway. If omitted, a default gateway is assumed (as determined by the CNI plugin).
The "dns" field contains a dictionary consisting of common DNS information. The `dns` field contains a dictionary consisting of common DNS information.
- `nameservers` (list of strings): list of a priority-ordered list of DNS nameservers that this network is aware of. Each entry in the list is a string containing either an IPv4 or an IPv6 address. See the [DNS well-known structure](#dns) section for more information.
- `domain` (string): the local domain used for short hostname lookups.
- `search` (list of strings): list of priority ordered search domains for short hostname lookups. Will be preferred over `domain` by most resolvers.
- `options` (list of strings): list of options that can be passed to the resolver
See [CNI Plugin Result](#result) section for more information.
Errors and logs are communicated in the same way as the CNI plugin. See [CNI Plugin Result](#result) section for details. Errors and logs are communicated in the same way as the CNI plugin. See [CNI Plugin Result](#result) section for details.
@ -440,6 +467,68 @@ IPAM plugin examples:
- Routes are expected to be added with a 0 metric. - Routes are expected to be added with a 0 metric.
- A default route may be specified via "0.0.0.0/0". Since another network might have already configured the default route, the CNI plugin should be prepared to skip over its default route definition. - A default route may be specified via "0.0.0.0/0". Since another network might have already configured the default route, the CNI plugin should be prepared to skip over its default route definition.
### Well-known Structures
#### IP
```
"ips": [
{
"version": "<4-or-6>",
"address": "<ip-and-prefix-in-CIDR>",
"gateway": "<ip-address-of-the-gateway>", (optional)
"interface": <numeric index into 'interfaces' list> (not required for IPAM plugins)
},
...
]
```
The `ip` field is a list of IP configuration information determined by the plugin. Each item is a dictionary describing of IP configuration for a network interface.
IP configuration for multiple network interfaces and multiple IP configurations for a single interface may be returned as separate items in the `ip` list.
All properties known to the plugin should be provided, even if not strictly required.
- `version` (string): either "4" or "6" and corresponds to the IP version of the addresses in the entry.
All IP addresses and gateways provided must be valid for the given `version`.
- `address` (string): an IP address in CIDR notation (eg "192.168.1.3/24").
- `gateway` (string): the default gateway for this subnet, if one exists.
It does not instruct the CNI plugin to add any routes with this gateway: routes to add are specified separately via the `routes` field.
An example use of this value is for the CNI `bridge` plugin to add this IP address to the Linux bridge to make it a gateway.
- `interface` (uint): the index into the `interfaces` list for a [CNI Plugin Result](#result) indicating which interface this IP configuration should be applied to.
IPAM plugins should not return this key since they have no information about network interfaces.
#### Routes
```
"routes": [
{
"dst": "<ip-and-prefix-in-cidr>",
"gw": "<ip-of-next-hop>" (optional)
},
...
]
```
- Each `routes` entry is a dictionary with the following fields. All IP addresses in the `routes` entry must be the same IP version, either 4 or 6.
- `dst` (string): destination subnet specified in CIDR notation.
- `gw` (string): IP of the gateway. If omitted, a default gateway is assumed (as determined by the CNI plugin).
#### DNS
```
"dns": {
"nameservers": <list-of-nameservers> (optional)
"domain": <name-of-local-domain> (optional)
"search": <list-of-additional-search-domains> (optional)
"options": <list-of-options> (optional)
}
```
The `dns` field contains a dictionary consisting of common DNS information.
- `nameservers` (list of strings): list of a priority-ordered list of DNS nameservers that this network is aware of. Each entry in the list is a string containing either an IPv4 or an IPv6 address.
- `domain` (string): the local domain used for short hostname lookups.
- `search` (list of strings): list of priority ordered search domains for short hostname lookups. Will be preferred over `domain` by most resolvers.
- `options` (list of strings): list of options that can be passed to the resolver.
See [CNI Plugin Result](#result) section for more information.
## Well-known Error Codes ## Well-known Error Codes
- `1` - Incompatible CNI version - `1` - Incompatible CNI version
- `2` - Unsupported field in network configuration. The error message must contain the key and value of the unsupported field. - `2` - Unsupported field in network configuration. The error message must contain the key and value of the unsupported field.

View File

@ -58,7 +58,7 @@ func newPluginInfo(configKey, configValue, prevResult string, injectDebugFilePat
} }
Expect(debug.WriteDebug(debugFilePath)).To(Succeed()) Expect(debug.WriteDebug(debugFilePath)).To(Succeed())
config := fmt.Sprintf(`{"type": "noop", "%s": "%s", "cniVersion": "0.2.0"`, configKey, configValue) config := fmt.Sprintf(`{"type": "noop", "%s": "%s", "cniVersion": "0.3.0"`, configKey, configValue)
if prevResult != "" { if prevResult != "" {
config += fmt.Sprintf(`, "prevResult": %s`, prevResult) config += fmt.Sprintf(`, "prevResult": %s`, prevResult)
} }
@ -77,8 +77,10 @@ func newPluginInfo(configKey, configValue, prevResult string, injectDebugFilePat
var _ = Describe("Invoking plugins", func() { var _ = Describe("Invoking plugins", func() {
Describe("Invoking a single plugin", func() { Describe("Invoking a single plugin", func() {
var ( var (
plugin pluginInfo debugFilePath string
debug *noop_debug.Debug
cniBinPath string cniBinPath string
pluginConfig string
cniConfig libcni.CNIConfig cniConfig libcni.CNIConfig
netConfig *libcni.NetworkConfig netConfig *libcni.NetworkConfig
runtimeConfig *libcni.RuntimeConf runtimeConfig *libcni.RuntimeConf
@ -87,31 +89,39 @@ var _ = Describe("Invoking plugins", func() {
) )
BeforeEach(func() { BeforeEach(func() {
pluginResult := `{ "ip4": { "ip": "10.1.2.3/24" }, "dns": {} }` debugFile, err := ioutil.TempFile("", "cni_debug")
plugin = newPluginInfo("some-key", "some-value", "", false, pluginResult) Expect(err).NotTo(HaveOccurred())
Expect(debugFile.Close()).To(Succeed())
debugFilePath = debugFile.Name()
debug = &noop_debug.Debug{
ReportResult: `{ "ips": [{ "version": "4", "address": "10.1.2.3/24" }], "dns": {} }`,
}
Expect(debug.WriteDebug(debugFilePath)).To(Succeed())
cniBinPath = filepath.Dir(pluginPaths["noop"]) cniBinPath = filepath.Dir(pluginPaths["noop"])
pluginConfig = `{ "type": "noop", "some-key": "some-value", "cniVersion": "0.3.0" }`
cniConfig = libcni.CNIConfig{Path: []string{cniBinPath}} cniConfig = libcni.CNIConfig{Path: []string{cniBinPath}}
netConfig = &libcni.NetworkConfig{ netConfig = &libcni.NetworkConfig{
Network: &types.NetConf{ Network: &types.NetConf{
Type: "noop", Type: "noop",
}, },
Bytes: []byte(plugin.config), Bytes: []byte(pluginConfig),
} }
runtimeConfig = &libcni.RuntimeConf{ runtimeConfig = &libcni.RuntimeConf{
ContainerID: "some-container-id", ContainerID: "some-container-id",
NetNS: "/some/netns/path", NetNS: "/some/netns/path",
IfName: "some-eth0", IfName: "some-eth0",
Args: [][2]string{{"DEBUG", plugin.debugFilePath}}, Args: [][2]string{[2]string{"DEBUG", debugFilePath}},
} }
expectedCmdArgs = skel.CmdArgs{ expectedCmdArgs = skel.CmdArgs{
ContainerID: "some-container-id", ContainerID: "some-container-id",
Netns: "/some/netns/path", Netns: "/some/netns/path",
IfName: "some-eth0", IfName: "some-eth0",
Args: "DEBUG=" + plugin.debugFilePath, Args: "DEBUG=" + debugFilePath,
Path: cniBinPath, Path: cniBinPath,
StdinData: []byte(plugin.config), StdinData: []byte(pluginConfig),
} }
}) })
@ -124,15 +134,18 @@ var _ = Describe("Invoking plugins", func() {
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(result).To(Equal(&current.Result{ Expect(result).To(Equal(&current.Result{
IP4: &current.IPConfig{ IPs: []*current.IPConfig{
IP: net.IPNet{ {
Version: "4",
Address: net.IPNet{
IP: net.ParseIP("10.1.2.3"), IP: net.ParseIP("10.1.2.3"),
Mask: net.IPv4Mask(255, 255, 255, 0), Mask: net.IPv4Mask(255, 255, 255, 0),
}, },
}, },
},
})) }))
debug, err := noop_debug.ReadDebug(plugin.debugFilePath) debug, err := noop_debug.ReadDebug(debugFilePath)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(debug.Command).To(Equal("ADD")) Expect(debug.Command).To(Equal("ADD"))
Expect(debug.CmdArgs).To(Equal(expectedCmdArgs)) Expect(debug.CmdArgs).To(Equal(expectedCmdArgs))
@ -151,8 +164,8 @@ var _ = Describe("Invoking plugins", func() {
Context("when the plugin errors", func() { Context("when the plugin errors", func() {
BeforeEach(func() { BeforeEach(func() {
plugin.debug.ReportError = "plugin error: banana" debug.ReportError = "plugin error: banana"
Expect(plugin.debug.WriteDebug(plugin.debugFilePath)).To(Succeed()) Expect(debug.WriteDebug(debugFilePath)).To(Succeed())
}) })
It("unmarshals and returns the error", func() { It("unmarshals and returns the error", func() {
result, err := cniConfig.AddNetwork(netConfig, runtimeConfig) result, err := cniConfig.AddNetwork(netConfig, runtimeConfig)
@ -167,7 +180,7 @@ var _ = Describe("Invoking plugins", func() {
err := cniConfig.DelNetwork(netConfig, runtimeConfig) err := cniConfig.DelNetwork(netConfig, runtimeConfig)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
debug, err := noop_debug.ReadDebug(plugin.debugFilePath) debug, err := noop_debug.ReadDebug(debugFilePath)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(debug.Command).To(Equal("DEL")) Expect(debug.Command).To(Equal("DEL"))
Expect(debug.CmdArgs).To(Equal(expectedCmdArgs)) Expect(debug.CmdArgs).To(Equal(expectedCmdArgs))
@ -186,8 +199,8 @@ var _ = Describe("Invoking plugins", func() {
Context("when the plugin errors", func() { Context("when the plugin errors", func() {
BeforeEach(func() { BeforeEach(func() {
plugin.debug.ReportError = "plugin error: banana" debug.ReportError = "plugin error: banana"
Expect(plugin.debug.WriteDebug(plugin.debugFilePath)).To(Succeed()) Expect(debug.WriteDebug(debugFilePath)).To(Succeed())
}) })
It("unmarshals and returns the error", func() { It("unmarshals and returns the error", func() {
err := cniConfig.DelNetwork(netConfig, runtimeConfig) err := cniConfig.DelNetwork(netConfig, runtimeConfig)
@ -203,7 +216,7 @@ var _ = Describe("Invoking plugins", func() {
Expect(versionInfo).NotTo(BeNil()) Expect(versionInfo).NotTo(BeNil())
Expect(versionInfo.SupportedVersions()).To(Equal([]string{ Expect(versionInfo.SupportedVersions()).To(Equal([]string{
"0.-42.0", "0.1.0", "0.2.0", "0.-42.0", "0.1.0", "0.2.0", "0.3.0",
})) }))
}) })
@ -229,13 +242,13 @@ var _ = Describe("Invoking plugins", func() {
BeforeEach(func() { BeforeEach(func() {
plugins = make([]pluginInfo, 3, 3) plugins = make([]pluginInfo, 3, 3)
plugins[0] = newPluginInfo("some-key", "some-value", "", true, `{"dns":{},"ip4":{"ip": "10.1.2.3/24"}}`) plugins[0] = newPluginInfo("some-key", "some-value", "", true, `{"dns":{},"ips":[{"version": "4", "address": "10.1.2.3/24"}]}`)
plugins[1] = newPluginInfo("some-key", "some-other-value", `{"dns":{},"ip4":{"ip": "10.1.2.3/24"}}`, true, "PASSTHROUGH") plugins[1] = newPluginInfo("some-key", "some-other-value", `{"dns":{},"ips":[{"version": "4", "address": "10.1.2.3/24"}]}`, true, "PASSTHROUGH")
plugins[2] = newPluginInfo("some-key", "yet-another-value", `{"dns":{},"ip4":{"ip": "10.1.2.3/24"}}`, true, "INJECT-DNS") plugins[2] = newPluginInfo("some-key", "yet-another-value", `{"dns":{},"ips":[{"version": "4", "address": "10.1.2.3/24"}]}`, true, "INJECT-DNS")
configList := []byte(fmt.Sprintf(`{ configList := []byte(fmt.Sprintf(`{
"name": "some-list", "name": "some-list",
"cniVersion": "0.2.0", "cniVersion": "0.3.0",
"plugins": [ "plugins": [
%s, %s,
%s, %s,
@ -275,12 +288,15 @@ var _ = Describe("Invoking plugins", func() {
Expect(result).To(Equal(&current.Result{ Expect(result).To(Equal(&current.Result{
// IP4 added by first plugin // IP4 added by first plugin
IP4: &current.IPConfig{ IPs: []*current.IPConfig{
IP: net.IPNet{ {
Version: "4",
Address: net.IPNet{
IP: net.ParseIP("10.1.2.3"), IP: net.ParseIP("10.1.2.3"),
Mask: net.IPv4Mask(255, 255, 255, 0), Mask: net.IPv4Mask(255, 255, 255, 0),
}, },
}, },
},
// DNS injected by last plugin // DNS injected by last plugin
DNS: types.DNS{ DNS: types.DNS{
Nameservers: []string{"1.2.3.4"}, Nameservers: []string{"1.2.3.4"},

View File

@ -40,7 +40,7 @@ var _ = Describe("Executing a plugin, unit tests", func() {
BeforeEach(func() { BeforeEach(func() {
rawExec = &fakes.RawExec{} 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 = &fakes.VersionDecoder{}
versionDecoder.DecodeCall.Returns.PluginInfo = version.PluginSupports("0.42.0") versionDecoder.DecodeCall.Returns.PluginInfo = version.PluginSupports("0.42.0")
@ -50,7 +50,7 @@ var _ = Describe("Executing a plugin, unit tests", func() {
VersionDecoder: versionDecoder, VersionDecoder: versionDecoder,
} }
pluginPath = "/some/plugin/path" pluginPath = "/some/plugin/path"
netconf = []byte(`{ "some": "stdin", "cniVersion": "0.2.0" }`) netconf = []byte(`{ "some": "stdin", "cniVersion": "0.3.0" }`)
cniargs = &fakes.CNIArgs{} cniargs = &fakes.CNIArgs{}
cniargs.AsEnvCall.Returns.Env = []string{"SOME=ENV"} cniargs.AsEnvCall.Returns.Env = []string{"SOME=ENV"}
}) })
@ -62,7 +62,8 @@ var _ = Describe("Executing a plugin, unit tests", func() {
result, err := current.GetResult(r) result, err := current.GetResult(r)
Expect(err).NotTo(HaveOccurred()) 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() { It("passes its arguments through to the rawExec", func() {

View File

@ -58,7 +58,7 @@ var _ = Describe("RawExec", func() {
"CNI_PATH=/some/bin/path", "CNI_PATH=/some/bin/path",
"CNI_IFNAME=some-eth0", "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{} execer = &invoke.RawExec{}
}) })

View File

@ -16,6 +16,7 @@ package ipam
import ( import (
"fmt" "fmt"
"net"
"os" "os"
"github.com/containernetworking/cni/pkg/invoke" "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 // ConfigureIface takes the result of IPAM plugin and
// applies to the ifName interface // applies to the ifName interface
func ConfigureIface(ifName string, res *current.Result) error { 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) link, err := netlink.LinkByName(ifName)
if err != nil { if err != nil {
return fmt.Errorf("failed to lookup %q: %v", ifName, err) 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) return fmt.Errorf("failed to set %q UP: %v", ifName, err)
} }
// TODO(eyakubovich): IPv6 var v4gw, v6gw net.IP
addr := &netlink.Addr{IPNet: &res.IP4.IP, Label: ""} for _, ipc := range res.IPs {
if err = netlink.AddrAdd(link, addr); err != nil { if int(ipc.Interface) >= len(res.Interfaces) || res.Interfaces[ipc.Interface].Name != ifName {
return fmt.Errorf("failed to add IP addr to %q: %v", ifName, err) // IP address is for a different interface
return fmt.Errorf("failed to add IP addr %v to %q: invalid interface index", ipc, ifName)
} }
for _, r := range res.IP4.Routes { 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.Routes {
routeIsV4 := r.Dst.IP.To4() != nil
gw := r.GW gw := r.GW
if gw == nil { 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 { if err = ip.AddRoute(&r.Dst, gw, link); err != nil {
// we skip over duplicate routes as we assume the first one wins // we skip over duplicate routes as we assume the first one wins

View File

@ -94,20 +94,36 @@ var _ = Describe("IPAM Operations", func() {
Expect(ipgw6).NotTo(BeNil()) Expect(ipgw6).NotTo(BeNil())
result = &current.Result{ result = &current.Result{
IP4: &current.IPConfig{ Interfaces: []*current.Interface{
IP: *ipv4, {
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",
},
},
IPs: []*current.IPConfig{
{
Version: "4",
Interface: 0,
Address: *ipv4,
Gateway: ipgw4, Gateway: ipgw4,
Routes: []types.Route{
{Dst: *routev4, GW: routegwv4},
}, },
}, {
IP6: &current.IPConfig{ Version: "6",
IP: *ipv6, Interface: 0,
Address: *ipv6,
Gateway: ipgw6, Gateway: ipgw6,
Routes: []types.Route{
{Dst: *routev6, GW: routegwv6},
}, },
}, },
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(len(v4addrs)).To(Equal(1))
Expect(ipNetEqual(v4addrs[0].IPNet, ipv4)).To(Equal(true)) 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) v6addrs, err := netlink.AddrList(link, syscall.AF_INET6)
Expect(err).NotTo(HaveOccurred()) 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) routes, err := netlink.RouteList(link, 0)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
var v4found bool var v4found, v6found bool
for _, route := range routes { for _, route := range routes {
isv4 := route.Dst.IP.To4() != nil isv4 := route.Dst.IP.To4() != nil
if isv4 && ipNetEqual(route.Dst, routev4) && route.Gw.Equal(routegwv4) { if isv4 && ipNetEqual(route.Dst, routev4) && route.Gw.Equal(routegwv4) {
v4found = true v4found = true
}
if !isv4 && ipNetEqual(route.Dst, routev6) && route.Gw.Equal(routegwv6) {
v6found = true
}
if v4found && v6found {
break break
} }
} }
Expect(v4found).To(Equal(true)) Expect(v4found).To(Equal(true))
Expect(v6found).To(Equal(true))
return nil return nil
}) })
@ -156,8 +187,8 @@ var _ = Describe("IPAM Operations", func() {
}) })
It("configures a link with routes using address gateways", func() { It("configures a link with routes using address gateways", func() {
result.IP4.Routes[0].GW = nil result.Routes[0].GW = nil
result.IP6.Routes[0].GW = nil result.Routes[1].GW = nil
err := originalNS.Do(func(ns.NetNS) error { err := originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover() defer GinkgoRecover()
@ -168,25 +199,56 @@ var _ = Describe("IPAM Operations", func() {
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(link.Attrs().Name).To(Equal(LINK_NAME)) 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) routes, err := netlink.RouteList(link, 0)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
var v4found bool var v4found, v6found bool
for _, route := range routes { for _, route := range routes {
isv4 := route.Dst.IP.To4() != nil isv4 := route.Dst.IP.To4() != nil
if isv4 && ipNetEqual(route.Dst, routev4) && route.Gw.Equal(ipgw4) { if isv4 && ipNetEqual(route.Dst, routev4) && route.Gw.Equal(ipgw4) {
v4found = true v4found = true
}
if !isv4 && ipNetEqual(route.Dst, routev6) && route.Gw.Equal(ipgw6) {
v6found = true
}
if v4found && v6found {
break break
} }
} }
Expect(v4found).To(Equal(true)) Expect(v4found).To(Equal(true))
Expect(v6found).To(Equal(true))
return nil return nil
}) })
Expect(err).NotTo(HaveOccurred()) 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() { It("returns an error when configuring the wrong interface", func() {
err := originalNS.Do(func(ns.NetNS) error { err := originalNS.Do(func(ns.NetNS) error {
return ConfigureIface("asdfasdf", result) return ConfigureIface("asdfasdf", result)

View File

@ -226,7 +226,7 @@ var _ = Describe("dispatching to the correct callback", func() {
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(stdout).To(MatchJSON(`{ Expect(stdout).To(MatchJSON(`{
"cniVersion": "0.2.0", "cniVersion": "0.3.0",
"supportedVersions": ["9.8.7"] "supportedVersions": ["9.8.7"]
}`)) }`))
}) })
@ -258,7 +258,7 @@ var _ = Describe("dispatching to the correct callback", func() {
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(stdout).To(MatchJSON(`{ Expect(stdout).To(MatchJSON(`{
"cniVersion": "0.2.0", "cniVersion": "0.3.0",
"supportedVersions": ["9.8.7"] "supportedVersions": ["9.8.7"]
}`)) }`))
}) })

133
pkg/types/020/types.go Normal file
View File

@ -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
}

View File

@ -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")
}

128
pkg/types/020/types_test.go Normal file
View File

@ -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"
]
}
}`))
})
})

View File

@ -21,11 +21,12 @@ import (
"os" "os"
"github.com/containernetworking/cni/pkg/types" "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) { func NewResult(data []byte) (types.Result, error) {
result := &Result{} result := &Result{}
@ -36,11 +37,11 @@ func NewResult(data []byte) (types.Result, error) {
} }
func GetResult(r types.Result) (*Result, error) { func GetResult(r types.Result) (*Result, error) {
newResult, err := r.GetAsVersion(implementedSpecVersion) resultCurrent, err := r.GetAsVersion(implementedSpecVersion)
if err != nil { if err != nil {
return nil, err return nil, err
} }
result, ok := newResult.(*Result) result, ok := resultCurrent.(*Result)
if !ok { if !ok {
return nil, fmt.Errorf("failed to convert result") return nil, fmt.Errorf("failed to convert result")
} }
@ -51,10 +52,67 @@ var resultConverters = []struct {
versions []string versions []string
convert func(types.Result) (*Result, error) convert func(types.Result) (*Result, error)
}{ }{
{SupportedVersions, convertFrom020}, {types020.SupportedVersions, convertFrom020},
{SupportedVersions, convertFrom030},
} }
func convertFrom020(result types.Result) (*Result, error) { 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) newResult, ok := result.(*Result)
if !ok { if !ok {
return nil, fmt.Errorf("failed to convert result") return nil, fmt.Errorf("failed to convert result")
@ -76,22 +134,72 @@ func NewResultFromResult(result types.Result) (*Result, error) {
// Result is what gets returned from the plugin (via stdout) to the caller // Result is what gets returned from the plugin (via stdout) to the caller
type Result struct { type Result struct {
IP4 *IPConfig `json:"ip4,omitempty"` Interfaces []*Interface `json:"interfaces,omitempty"`
IP6 *IPConfig `json:"ip6,omitempty"` IPs []*IPConfig `json:"ips,omitempty"`
Routes []*types.Route `json:"routes,omitempty"`
DNS types.DNS `json:"dns,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 { func (r *Result) Version() string {
return implementedSpecVersion return implementedSpecVersion
} }
func (r *Result) GetAsVersion(version string) (types.Result, error) { func (r *Result) GetAsVersion(version string) (types.Result, error) {
for _, supportedVersion := range SupportedVersions { switch version {
if version == supportedVersion { case implementedSpecVersion:
return r, nil return r, nil
case types020.SupportedVersions[0], types020.SupportedVersions[1], types020.SupportedVersions[2]:
return r.convertTo020()
} }
} return nil, fmt.Errorf("cannot convert version 0.3.0 to %q", version)
return nil, fmt.Errorf("cannot convert version %q to %s", SupportedVersions, version)
} }
func (r *Result) Print() error { func (r *Result) Print() error {
@ -103,42 +211,67 @@ func (r *Result) Print() error {
return err return err
} }
// String returns a formatted string in the form of "[IP4: $1,][ IP6: $2,] DNS: $3" where // String returns a formatted string in the form of "[Interfaces: $1,][ IP: $2,] DNS: $3" where
// $1 represents the receiver's IPv4, $2 represents the receiver's IPv6 and $3 the // $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. // receiver's DNS. If $1 or $2 are nil, they won't be present in the returned string.
func (r *Result) String() string { func (r *Result) String() string {
var str string var str string
if r.IP4 != nil { if len(r.Interfaces) > 0 {
str = fmt.Sprintf("IP4:%+v, ", *r.IP4) str += fmt.Sprintf("Interfaces:%+v, ", r.Interfaces)
} }
if r.IP6 != nil { if len(r.IPs) > 0 {
str += fmt.Sprintf("IP6:%+v, ", *r.IP6) 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) return fmt.Sprintf("%sDNS:%+v", str, r.DNS)
} }
// IPConfig contains values necessary to configure an interface // Convert this old version result to the current CNI version result
type IPConfig struct { func (r *Result) Convert() (*Result, error) {
IP net.IPNet return r, nil
Gateway net.IP
Routes []types.Route
} }
// net.IPNet is not JSON (un)marshallable so this duality is needed // Interface contains values about the created interfaces
// for our custom IPNet type 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 // JSON (un)marshallable types
type ipConfig struct { type ipConfig struct {
IP types.IPNet `json:"ip"` Version string `json:"version"`
Interface int `json:"interface,omitempty"`
Address types.IPNet `json:"address"`
Gateway net.IP `json:"gateway,omitempty"` Gateway net.IP `json:"gateway,omitempty"`
Routes []types.Route `json:"routes,omitempty"`
} }
func (c *IPConfig) MarshalJSON() ([]byte, error) { func (c *IPConfig) MarshalJSON() ([]byte, error) {
ipc := ipConfig{ ipc := ipConfig{
IP: types.IPNet(c.IP), Version: c.Version,
Interface: c.Interface,
Address: types.IPNet(c.Address),
Gateway: c.Gateway, Gateway: c.Gateway,
Routes: c.Routes,
} }
return json.Marshal(ipc) return json.Marshal(ipc)
@ -150,8 +283,9 @@ func (c *IPConfig) UnmarshalJSON(data []byte) error {
return err 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.Gateway = ipc.Gateway
c.Routes = ipc.Routes
return nil return nil
} }

View File

@ -23,5 +23,5 @@ import (
func TestTypes010(t *testing.T) { func TestTypes010(t *testing.T) {
RegisterFailHandler(Fail) RegisterFailHandler(Fail)
RunSpecs(t, "0.1.0 Types Suite") RunSpecs(t, "0.3.0 Types Suite")
} }

View File

@ -26,8 +26,7 @@ import (
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
) )
var _ = Describe("Ensures compatibility with the 0.1.0 spec", func() { func testResult() *current.Result {
It("correctly encodes a 0.1.0 Result", func() {
ipv4, err := types.ParseCIDR("1.2.3.30/24") ipv4, err := types.ParseCIDR("1.2.3.30/24")
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(ipv4).NotTo(BeNil()) Expect(ipv4).NotTo(BeNil())
@ -47,21 +46,32 @@ var _ = Describe("Ensures compatibility with the 0.1.0 spec", func() {
Expect(routegwv6).NotTo(BeNil()) Expect(routegwv6).NotTo(BeNil())
// Set every field of the struct to ensure source compatibility // Set every field of the struct to ensure source compatibility
res := current.Result{ return &current.Result{
IP4: &current.IPConfig{ Interfaces: []*current.Interface{
IP: *ipv4, {
Name: "eth0",
Mac: "00:11:22:33:44:55",
Sandbox: "/proc/3553/ns/net",
},
},
IPs: []*current.IPConfig{
{
Version: "4",
Interface: 0,
Address: *ipv4,
Gateway: net.ParseIP("1.2.3.1"), Gateway: net.ParseIP("1.2.3.1"),
Routes: []types.Route{
{Dst: *routev4, GW: routegwv4},
}, },
}, {
IP6: &current.IPConfig{ Version: "6",
IP: *ipv6, Interface: 0,
Address: *ipv6,
Gateway: net.ParseIP("abcd:1234:ffff::1"), Gateway: net.ParseIP("abcd:1234:ffff::1"),
Routes: []types.Route{
{Dst: *routev6, GW: routegwv6},
}, },
}, },
Routes: []*types.Route{
{Dst: *routev4, GW: routegwv4},
{Dst: *routev6, GW: routegwv6},
},
DNS: types.DNS{ DNS: types.DNS{
Nameservers: []string{"1.2.3.4", "1::cafe"}, Nameservers: []string{"1.2.3.4", "1::cafe"},
Domain: "acompany.com", Domain: "acompany.com",
@ -69,6 +79,81 @@ var _ = Describe("Ensures compatibility with the 0.1.0 spec", func() {
Options: []string{"foo", "bar"}, Options: []string{"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
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(`{
"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]}")) 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]}"))
@ -125,4 +210,5 @@ var _ = Describe("Ensures compatibility with the 0.1.0 spec", func() {
} }
}`)) }`))
}) })
})
}) })

View File

@ -16,6 +16,7 @@ package types
import ( import (
"encoding/json" "encoding/json"
"fmt"
"net" "net"
"os" "os"
) )
@ -114,6 +115,10 @@ type Route struct {
GW net.IP GW net.IP
} }
func (r *Route) String() string {
return fmt.Sprintf("%+v", *r)
}
// Well known error codes // Well known error codes
// see https://github.com/containernetworking/cni/blob/master/SPEC.md#well-known-error-codes // see https://github.com/containernetworking/cni/blob/master/SPEC.md#well-known-error-codes
const ( const (

View File

@ -23,7 +23,7 @@ import (
"sync" "sync"
"github.com/containernetworking/cni/pkg/types" "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" "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. // As we change the CNI spec, the Result type and this value may change.
// The text of the example plugins should not. // The text of the example plugins should not.
var ExpectedResult = &current.Result{ var ExpectedResult = &types020.Result{
IP4: &current.IPConfig{ IP4: &types020.IPConfig{
IP: net.IPNet{ IP: net.IPNet{
IP: net.ParseIP("10.1.2.3"), IP: net.ParseIP("10.1.2.3"),
Mask: net.CIDRMask(24, 32), Mask: net.CIDRMask(24, 32),

View File

@ -18,12 +18,13 @@ import (
"fmt" "fmt"
"github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/types/020"
"github.com/containernetworking/cni/pkg/types/current" "github.com/containernetworking/cni/pkg/types/current"
) )
// Current reports the version of the CNI spec implemented by this library // Current reports the version of the CNI spec implemented by this library
func Current() string { func Current() string {
return "0.2.0" return "0.3.0"
} }
// Legacy PluginInfo describes a plugin that is backwards compatible with the // 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 // Any future CNI spec versions which meet this definition should be added to
// this list. // this list.
var Legacy = PluginSupports("0.1.0", "0.2.0") var Legacy = PluginSupports("0.1.0", "0.2.0")
var All = PluginSupports("0.1.0", "0.2.0", "0.3.0")
var resultFactories = []struct { var resultFactories = []struct {
supportedVersions []string supportedVersions []string
newResult types.ResultFactoryFunc newResult types.ResultFactoryFunc
}{ }{
{current.SupportedVersions, current.NewResult}, {current.SupportedVersions, current.NewResult},
{types020.SupportedVersions, types020.NewResult},
} }
// Finds a Result object matching the requested version (if any) and asks // Finds a Result object matching the requested version (if any) and asks

View File

@ -71,11 +71,12 @@ func (d *DHCP) Allocate(args *skel.CmdArgs, result *current.Result) error {
d.setLease(args.ContainerID, conf.Name, l) d.setLease(args.ContainerID, conf.Name, l)
result.IP4 = &current.IPConfig{ result.IPs = []*current.IPConfig{{
IP: *ipn, Version: "4",
Address: *ipn,
Gateway: l.Gateway(), Gateway: l.Gateway(),
Routes: l.Routes(), }}
} result.Routes = l.Routes()
return nil return nil
} }

View File

@ -291,7 +291,7 @@ func (l *DHCPLease) Gateway() net.IP {
return parseRouter(l.opts) return parseRouter(l.opts)
} }
func (l *DHCPLease) Routes() []types.Route { func (l *DHCPLease) Routes() []*types.Route {
routes := parseRoutes(l.opts) routes := parseRoutes(l.opts)
return append(routes, parseCIDRRoutes(l.opts)...) return append(routes, parseCIDRRoutes(l.opts)...)
} }

View File

@ -21,6 +21,7 @@ import (
"path/filepath" "path/filepath"
"github.com/containernetworking/cni/pkg/skel" "github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/types/current" "github.com/containernetworking/cni/pkg/types/current"
"github.com/containernetworking/cni/pkg/version" "github.com/containernetworking/cni/pkg/version"
) )
@ -31,16 +32,24 @@ func main() {
if len(os.Args) > 1 && os.Args[1] == "daemon" { if len(os.Args) > 1 && os.Args[1] == "daemon" {
runDaemon() runDaemon()
} else { } else {
skel.PluginMain(cmdAdd, cmdDel, version.Legacy) skel.PluginMain(cmdAdd, cmdDel, version.All)
} }
} }
func cmdAdd(args *skel.CmdArgs) error { func cmdAdd(args *skel.CmdArgs) error {
// Plugin must return result in same version as specified in netconf
versionDecoder := &version.ConfigDecoder{}
confVersion, err := versionDecoder.Decode(args.StdinData)
if err != nil {
return err
}
result := &current.Result{} result := &current.Result{}
if err := rpcCall("DHCP.Allocate", args, result); err != nil { if err := rpcCall("DHCP.Allocate", args, result); err != nil {
return err return err
} }
return result.Print()
return types.PrintResult(result, confVersion)
} }
func cmdDel(args *skel.CmdArgs) error { func cmdDel(args *skel.CmdArgs) error {

View File

@ -40,17 +40,17 @@ func classfulSubnet(sn net.IP) net.IPNet {
} }
} }
func parseRoutes(opts dhcp4.Options) []types.Route { func parseRoutes(opts dhcp4.Options) []*types.Route {
// StaticRoutes format: pairs of: // StaticRoutes format: pairs of:
// Dest = 4 bytes; Classful IP subnet // Dest = 4 bytes; Classful IP subnet
// Router = 4 bytes; IP address of router // Router = 4 bytes; IP address of router
routes := []types.Route{} routes := []*types.Route{}
if opt, ok := opts[dhcp4.OptionStaticRoute]; ok { if opt, ok := opts[dhcp4.OptionStaticRoute]; ok {
for len(opt) >= 8 { for len(opt) >= 8 {
sn := opt[0:4] sn := opt[0:4]
r := opt[4:8] r := opt[4:8]
rt := types.Route{ rt := &types.Route{
Dst: classfulSubnet(sn), Dst: classfulSubnet(sn),
GW: r, GW: r,
} }
@ -62,10 +62,10 @@ func parseRoutes(opts dhcp4.Options) []types.Route {
return routes return routes
} }
func parseCIDRRoutes(opts dhcp4.Options) []types.Route { func parseCIDRRoutes(opts dhcp4.Options) []*types.Route {
// See RFC4332 for format (http://tools.ietf.org/html/rfc3442) // See RFC4332 for format (http://tools.ietf.org/html/rfc3442)
routes := []types.Route{} routes := []*types.Route{}
if opt, ok := opts[dhcp4.OptionClasslessRouteFormat]; ok { if opt, ok := opts[dhcp4.OptionClasslessRouteFormat]; ok {
for len(opt) >= 5 { for len(opt) >= 5 {
width := int(opt[0]) width := int(opt[0])
@ -89,7 +89,7 @@ func parseCIDRRoutes(opts dhcp4.Options) []types.Route {
gw := net.IP(opt[octets+1 : octets+5]) gw := net.IP(opt[octets+1 : octets+5])
rt := types.Route{ rt := &types.Route{
Dst: net.IPNet{ Dst: net.IPNet{
IP: net.IP(sn), IP: net.IP(sn),
Mask: net.CIDRMask(width, 32), Mask: net.CIDRMask(width, 32),

View File

@ -22,16 +22,16 @@ import (
"github.com/d2g/dhcp4" "github.com/d2g/dhcp4"
) )
func validateRoutes(t *testing.T, routes []types.Route) { func validateRoutes(t *testing.T, routes []*types.Route) {
expected := []types.Route{ expected := []*types.Route{
types.Route{ &types.Route{
Dst: net.IPNet{ Dst: net.IPNet{
IP: net.IPv4(10, 0, 0, 0), IP: net.IPv4(10, 0, 0, 0),
Mask: net.CIDRMask(8, 32), Mask: net.CIDRMask(8, 32),
}, },
GW: net.IPv4(10, 1, 2, 3), GW: net.IPv4(10, 1, 2, 3),
}, },
types.Route{ &types.Route{
Dst: net.IPNet{ Dst: net.IPNet{
IP: net.IPv4(192, 168, 1, 0), IP: net.IPv4(192, 168, 1, 0),
Mask: net.CIDRMask(24, 32), Mask: net.CIDRMask(24, 32),

View File

@ -20,6 +20,7 @@ import (
"net" "net"
"github.com/containernetworking/cni/pkg/ip" "github.com/containernetworking/cni/pkg/ip"
"github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/types/current" "github.com/containernetworking/cni/pkg/types/current"
"github.com/containernetworking/cni/plugins/ipam/host-local/backend" "github.com/containernetworking/cni/plugins/ipam/host-local/backend"
) )
@ -129,7 +130,7 @@ func validateRangeIP(ip net.IP, ipnet *net.IPNet, start net.IP, end net.IP) erro
} }
// Returns newly allocated IP along with its config // Returns newly allocated IP along with its config
func (a *IPAllocator) Get(id string) (*current.IPConfig, error) { func (a *IPAllocator) Get(id string) (*current.IPConfig, []*types.Route, error) {
a.store.Lock() a.store.Lock()
defer a.store.Unlock() defer a.store.Unlock()
@ -145,7 +146,7 @@ func (a *IPAllocator) Get(id string) (*current.IPConfig, error) {
if requestedIP != nil { if requestedIP != nil {
if gw != nil && gw.Equal(a.conf.Args.IP) { if gw != nil && gw.Equal(a.conf.Args.IP) {
return nil, fmt.Errorf("requested IP must differ gateway IP") return nil, nil, fmt.Errorf("requested IP must differ gateway IP")
} }
subnet := net.IPNet{ subnet := net.IPNet{
@ -154,22 +155,24 @@ func (a *IPAllocator) Get(id string) (*current.IPConfig, error) {
} }
err := validateRangeIP(requestedIP, &subnet, a.start, a.end) err := validateRangeIP(requestedIP, &subnet, a.start, a.end)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
reserved, err := a.store.Reserve(id, requestedIP) reserved, err := a.store.Reserve(id, requestedIP)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
if reserved { if reserved {
return &current.IPConfig{ ipConfig := &current.IPConfig{
IP: net.IPNet{IP: requestedIP, Mask: a.conf.Subnet.Mask}, Version: "4",
Address: net.IPNet{IP: requestedIP, Mask: a.conf.Subnet.Mask},
Gateway: gw, Gateway: gw,
Routes: a.conf.Routes,
}, nil
} }
return nil, fmt.Errorf("requested IP address %q is not available in network: %s", requestedIP, a.conf.Name) routes := convertRoutesToCurrent(a.conf.Routes)
return ipConfig, routes, nil
}
return nil, nil, fmt.Errorf("requested IP address %q is not available in network: %s", requestedIP, a.conf.Name)
} }
startIP, endIP := a.getSearchRange() startIP, endIP := a.getSearchRange()
@ -181,21 +184,23 @@ func (a *IPAllocator) Get(id string) (*current.IPConfig, error) {
reserved, err := a.store.Reserve(id, cur) reserved, err := a.store.Reserve(id, cur)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
if reserved { if reserved {
return &current.IPConfig{ ipConfig := &current.IPConfig{
IP: net.IPNet{IP: cur, Mask: a.conf.Subnet.Mask}, Version: "4",
Address: net.IPNet{IP: cur, Mask: a.conf.Subnet.Mask},
Gateway: gw, Gateway: gw,
Routes: a.conf.Routes, }
}, nil routes := convertRoutesToCurrent(a.conf.Routes)
return ipConfig, routes, nil
} }
// break here to complete the loop // break here to complete the loop
if cur.Equal(endIP) { if cur.Equal(endIP) {
break break
} }
} }
return nil, fmt.Errorf("no IP addresses available in network: %s", a.conf.Name) return nil, nil, fmt.Errorf("no IP addresses available in network: %s", a.conf.Name)
} }
// Releases all IPs allocated for the container with given ID // Releases all IPs allocated for the container with given ID

View File

@ -31,10 +31,10 @@ type AllocatorTestCase struct {
lastIP string lastIP string
} }
func (t AllocatorTestCase) run() (*current.IPConfig, error) { func (t AllocatorTestCase) run() (*current.IPConfig, []*types.Route, error) {
subnet, err := types.ParseCIDR(t.subnet) subnet, err := types.ParseCIDR(t.subnet)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
conf := IPAMConfig{ conf := IPAMConfig{
@ -45,14 +45,14 @@ func (t AllocatorTestCase) run() (*current.IPConfig, error) {
store := fakestore.NewFakeStore(t.ipmap, net.ParseIP(t.lastIP)) store := fakestore.NewFakeStore(t.ipmap, net.ParseIP(t.lastIP))
alloc, err := NewIPAllocator(&conf, store) alloc, err := NewIPAllocator(&conf, store)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
res, err := alloc.Get("ID") res, routes, err := alloc.Get("ID")
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
return res, nil return res, routes, nil
} }
var _ = Describe("host-local ip allocator", func() { var _ = Describe("host-local ip allocator", func() {
@ -129,9 +129,9 @@ var _ = Describe("host-local ip allocator", func() {
} }
for _, tc := range testCases { for _, tc := range testCases {
res, err := tc.run() res, _, err := tc.run()
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
Expect(res.IP.IP.String()).To(Equal(tc.expectResult)) Expect(res.Address.IP.String()).To(Equal(tc.expectResult))
} }
}) })
@ -149,14 +149,14 @@ var _ = Describe("host-local ip allocator", func() {
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
for i := 1; i < 254; i++ { for i := 1; i < 254; i++ {
res, err := alloc.Get("ID") res, _, err := alloc.Get("ID")
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
// i+1 because the gateway address is skipped // i+1 because the gateway address is skipped
s := fmt.Sprintf("192.168.1.%d/24", i+1) s := fmt.Sprintf("192.168.1.%d/24", i+1)
Expect(s).To(Equal(res.IP.String())) Expect(s).To(Equal(res.Address.String()))
} }
_, err = alloc.Get("ID") _, _, err = alloc.Get("ID")
Expect(err).To(HaveOccurred()) Expect(err).To(HaveOccurred())
}) })
@ -174,13 +174,13 @@ var _ = Describe("host-local ip allocator", func() {
alloc, err := NewIPAllocator(&conf, store) alloc, err := NewIPAllocator(&conf, store)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
res, err := alloc.Get("ID") res, _, err := alloc.Get("ID")
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
Expect(res.IP.String()).To(Equal("192.168.1.10/24")) Expect(res.Address.String()).To(Equal("192.168.1.10/24"))
res, err = alloc.Get("ID") res, _, err = alloc.Get("ID")
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
Expect(res.IP.String()).To(Equal("192.168.1.11/24")) Expect(res.Address.String()).To(Equal("192.168.1.11/24"))
}) })
It("should allocate RangeEnd but not past RangeEnd", func() { It("should allocate RangeEnd but not past RangeEnd", func() {
@ -198,13 +198,13 @@ var _ = Describe("host-local ip allocator", func() {
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
for i := 1; i < 5; i++ { for i := 1; i < 5; i++ {
res, err := alloc.Get("ID") res, _, err := alloc.Get("ID")
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
// i+1 because the gateway address is skipped // i+1 because the gateway address is skipped
Expect(res.IP.String()).To(Equal(fmt.Sprintf("192.168.1.%d/24", i+1))) Expect(res.Address.String()).To(Equal(fmt.Sprintf("192.168.1.%d/24", i+1)))
} }
_, err = alloc.Get("ID") _, _, err = alloc.Get("ID")
Expect(err).To(HaveOccurred()) Expect(err).To(HaveOccurred())
}) })
@ -222,9 +222,9 @@ var _ = Describe("host-local ip allocator", func() {
} }
store := fakestore.NewFakeStore(ipmap, nil) store := fakestore.NewFakeStore(ipmap, nil)
alloc, _ := NewIPAllocator(&conf, store) alloc, _ := NewIPAllocator(&conf, store)
res, err := alloc.Get("ID") res, _, err := alloc.Get("ID")
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
Expect(res.IP.IP.String()).To(Equal(requestedIP.String())) Expect(res.Address.IP.String()).To(Equal(requestedIP.String()))
}) })
It("must return an error when the requested IP is after RangeEnd", func() { It("must return an error when the requested IP is after RangeEnd", func() {
@ -240,7 +240,7 @@ var _ = Describe("host-local ip allocator", func() {
} }
store := fakestore.NewFakeStore(ipmap, nil) store := fakestore.NewFakeStore(ipmap, nil)
alloc, _ := NewIPAllocator(&conf, store) alloc, _ := NewIPAllocator(&conf, store)
_, err = alloc.Get("ID") _, _, err = alloc.Get("ID")
Expect(err).To(HaveOccurred()) Expect(err).To(HaveOccurred())
}) })
@ -257,7 +257,7 @@ var _ = Describe("host-local ip allocator", func() {
} }
store := fakestore.NewFakeStore(ipmap, nil) store := fakestore.NewFakeStore(ipmap, nil)
alloc, _ := NewIPAllocator(&conf, store) alloc, _ := NewIPAllocator(&conf, store)
_, err = alloc.Get("ID") _, _, err = alloc.Get("ID")
Expect(err).To(HaveOccurred()) Expect(err).To(HaveOccurred())
}) })
}) })
@ -332,7 +332,7 @@ var _ = Describe("host-local ip allocator", func() {
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
_, err := tc.run() _, _, err := tc.run()
Expect(err).To(MatchError("no IP addresses available in network: test")) Expect(err).To(MatchError("no IP addresses available in network: test"))
} }
}) })

View File

@ -43,30 +43,42 @@ type IPAMArgs struct {
type Net struct { type Net struct {
Name string `json:"name"` Name string `json:"name"`
CNIVersion string `json:"cniVersion"`
IPAM *IPAMConfig `json:"ipam"` IPAM *IPAMConfig `json:"ipam"`
} }
// NewIPAMConfig creates a NetworkConfig from the given network name. // NewIPAMConfig creates a NetworkConfig from the given network name.
func LoadIPAMConfig(bytes []byte, args string) (*IPAMConfig, error) { func LoadIPAMConfig(bytes []byte, args string) (*IPAMConfig, string, error) {
n := Net{} n := Net{}
if err := json.Unmarshal(bytes, &n); err != nil { if err := json.Unmarshal(bytes, &n); err != nil {
return nil, err return nil, "", err
} }
if args != "" { if args != "" {
n.IPAM.Args = &IPAMArgs{} n.IPAM.Args = &IPAMArgs{}
err := types.LoadArgs(args, n.IPAM.Args) err := types.LoadArgs(args, n.IPAM.Args)
if err != nil { if err != nil {
return nil, err return nil, "", err
} }
} }
if n.IPAM == nil { if n.IPAM == nil {
return nil, fmt.Errorf("IPAM config missing 'ipam' key") return nil, "", fmt.Errorf("IPAM config missing 'ipam' key")
} }
// Copy net name into IPAM so not to drag Net struct around // Copy net name into IPAM so not to drag Net struct around
n.IPAM.Name = n.Name n.IPAM.Name = n.Name
return n.IPAM, nil return n.IPAM, n.CNIVersion, nil
}
func convertRoutesToCurrent(routes []types.Route) []*types.Route {
var currentRoutes []*types.Route
for _, r := range routes {
currentRoutes = append(currentRoutes, &types.Route{
Dst: r.Dst,
GW: r.GW,
})
}
return currentRoutes
} }

View File

@ -20,10 +20,12 @@ import (
"net" "net"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"github.com/containernetworking/cni/pkg/skel" "github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/testutils" "github.com/containernetworking/cni/pkg/testutils"
"github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/types/020"
"github.com/containernetworking/cni/pkg/types/current" "github.com/containernetworking/cni/pkg/types/current"
. "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo"
@ -43,7 +45,7 @@ var _ = Describe("host-local Operations", func() {
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
conf := fmt.Sprintf(`{ conf := fmt.Sprintf(`{
"cniVersion": "0.2.0", "cniVersion": "0.3.0",
"name": "mynet", "name": "mynet",
"type": "ipvlan", "type": "ipvlan",
"master": "foo0", "master": "foo0",
@ -63,19 +65,83 @@ var _ = Describe("host-local Operations", func() {
} }
// Allocate the IP // Allocate the IP
r, _, err := testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error { r, raw, err := testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error {
return cmdAdd(args) return cmdAdd(args)
}) })
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(strings.Index(string(raw), "\"version\":")).Should(BeNumerically(">", 0))
result, err := current.GetResult(r) result, err := current.GetResult(r)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
expectedAddress, err := types.ParseCIDR("10.1.2.2/24")
Expect(err).NotTo(HaveOccurred())
Expect(len(result.IPs)).To(Equal(1))
expectedAddress.IP = expectedAddress.IP.To16()
Expect(result.IPs[0].Address).To(Equal(*expectedAddress))
Expect(result.IPs[0].Gateway).To(Equal(net.ParseIP("10.1.2.1")))
ipFilePath := filepath.Join(tmpDir, "mynet", "10.1.2.2")
contents, err := ioutil.ReadFile(ipFilePath)
Expect(err).NotTo(HaveOccurred())
Expect(string(contents)).To(Equal("dummy"))
lastFilePath := filepath.Join(tmpDir, "mynet", "last_reserved_ip")
contents, err = ioutil.ReadFile(lastFilePath)
Expect(err).NotTo(HaveOccurred())
Expect(string(contents)).To(Equal("10.1.2.2"))
// Release the IP
err = testutils.CmdDelWithResult(nspath, ifname, func() error {
return cmdDel(args)
})
Expect(err).NotTo(HaveOccurred())
_, err = os.Stat(ipFilePath)
Expect(err).To(HaveOccurred())
})
It("allocates and releases an address with ADD/DEL and 0.1.0 config", func() {
const ifname string = "eth0"
const nspath string = "/some/where"
tmpDir, err := ioutil.TempDir("", "host_local_artifacts")
Expect(err).NotTo(HaveOccurred())
defer os.RemoveAll(tmpDir)
conf := fmt.Sprintf(`{
"cniVersion": "0.1.0",
"name": "mynet",
"type": "ipvlan",
"master": "foo0",
"ipam": {
"type": "host-local",
"subnet": "10.1.2.0/24",
"dataDir": "%s"
}
}`, tmpDir)
args := &skel.CmdArgs{
ContainerID: "dummy",
Netns: nspath,
IfName: ifname,
StdinData: []byte(conf),
}
// Allocate the IP
r, raw, err := testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error {
return cmdAdd(args)
})
Expect(err).NotTo(HaveOccurred())
Expect(strings.Index(string(raw), "\"ip4\":")).Should(BeNumerically(">", 0))
result, err := types020.GetResult(r)
Expect(err).NotTo(HaveOccurred())
expectedAddress, err := types.ParseCIDR("10.1.2.2/24") expectedAddress, err := types.ParseCIDR("10.1.2.2/24")
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
expectedAddress.IP = expectedAddress.IP.To16() expectedAddress.IP = expectedAddress.IP.To16()
Expect(result.IP4.IP).To(Equal(*expectedAddress)) Expect(result.IP4.IP).To(Equal(*expectedAddress))
Expect(result.IP4.Gateway).To(Equal(net.ParseIP("10.1.2.1"))) Expect(result.IP4.Gateway).To(Equal(net.ParseIP("10.1.2.1")))
ipFilePath := filepath.Join(tmpDir, "mynet", "10.1.2.2") ipFilePath := filepath.Join(tmpDir, "mynet", "10.1.2.2")
@ -136,7 +202,7 @@ var _ = Describe("host-local Operations", func() {
result, err := current.GetResult(r) result, err := current.GetResult(r)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
ipFilePath := filepath.Join(tmpDir, "mynet", result.IP4.IP.IP.String()) ipFilePath := filepath.Join(tmpDir, "mynet", result.IPs[0].Address.IP.String())
contents, err := ioutil.ReadFile(ipFilePath) contents, err := ioutil.ReadFile(ipFilePath)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(string(contents)).To(Equal("dummy")) Expect(string(contents)).To(Equal("dummy"))

View File

@ -19,28 +19,29 @@ import (
"github.com/containernetworking/cni/plugins/ipam/host-local/backend/disk" "github.com/containernetworking/cni/plugins/ipam/host-local/backend/disk"
"github.com/containernetworking/cni/pkg/skel" "github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/types/current" "github.com/containernetworking/cni/pkg/types/current"
"github.com/containernetworking/cni/pkg/version" "github.com/containernetworking/cni/pkg/version"
) )
func main() { func main() {
skel.PluginMain(cmdAdd, cmdDel, version.Legacy) skel.PluginMain(cmdAdd, cmdDel, version.All)
} }
func cmdAdd(args *skel.CmdArgs) error { func cmdAdd(args *skel.CmdArgs) error {
ipamConf, err := allocator.LoadIPAMConfig(args.StdinData, args.Args) ipamConf, confVersion, err := allocator.LoadIPAMConfig(args.StdinData, args.Args)
if err != nil { if err != nil {
return err return err
} }
r := &current.Result{} result := &current.Result{}
if ipamConf.ResolvConf != "" { if ipamConf.ResolvConf != "" {
dns, err := parseResolvConf(ipamConf.ResolvConf) dns, err := parseResolvConf(ipamConf.ResolvConf)
if err != nil { if err != nil {
return err return err
} }
r.DNS = *dns result.DNS = *dns
} }
store, err := disk.New(ipamConf.Name, ipamConf.DataDir) store, err := disk.New(ipamConf.Name, ipamConf.DataDir)
@ -54,16 +55,18 @@ func cmdAdd(args *skel.CmdArgs) error {
return err return err
} }
r.IP4, err = allocator.Get(args.ContainerID) ipConf, routes, err := allocator.Get(args.ContainerID)
if err != nil { if err != nil {
return err return err
} }
result.IPs = []*current.IPConfig{ipConf}
result.Routes = routes
return r.Print() return types.PrintResult(result, confVersion)
} }
func cmdDel(args *skel.CmdArgs) error { func cmdDel(args *skel.CmdArgs) error {
ipamConf, err := allocator.LoadIPAMConfig(args.StdinData, args.Args) ipamConf, _, err := allocator.LoadIPAMConfig(args.StdinData, args.Args)
if err != nil { if err != nil {
return err return err
} }

View File

@ -53,14 +53,14 @@ func init() {
runtime.LockOSThread() runtime.LockOSThread()
} }
func loadNetConf(bytes []byte) (*NetConf, error) { func loadNetConf(bytes []byte) (*NetConf, string, error) {
n := &NetConf{ n := &NetConf{
BrName: defaultBrName, BrName: defaultBrName,
} }
if err := json.Unmarshal(bytes, n); err != nil { if err := json.Unmarshal(bytes, n); err != nil {
return nil, fmt.Errorf("failed to load netconf: %v", err) return nil, "", fmt.Errorf("failed to load netconf: %v", err)
} }
return n, nil return n, n.CNIVersion, nil
} }
func ensureBridgeAddr(br *netlink.Bridge, ipn *net.IPNet, forceAddress bool) error { func ensureBridgeAddr(br *netlink.Bridge, ipn *net.IPNet, forceAddress bool) error {
@ -139,17 +139,17 @@ func ensureBridge(brName string, mtu int) (*netlink.Bridge, error) {
}, },
} }
if err := netlink.LinkAdd(br); err != nil { err := netlink.LinkAdd(br)
if err != syscall.EEXIST { if err != nil && err != syscall.EEXIST {
return nil, fmt.Errorf("could not add %q: %v", brName, err) return nil, fmt.Errorf("could not add %q: %v", brName, err)
} }
// it's ok if the device already exists as long as config is similar // Re-fetch link to read all attributes and if it already existed,
// ensure it's really a bridge with similar configuration
br, err = bridgeByName(brName) br, err = bridgeByName(brName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
}
if err := netlink.LinkSetUp(br); err != nil { if err := netlink.LinkSetUp(br); err != nil {
return nil, err return nil, err
@ -158,40 +158,44 @@ func ensureBridge(brName string, mtu int) (*netlink.Bridge, error) {
return br, nil return br, nil
} }
func setupVeth(netns ns.NetNS, br *netlink.Bridge, ifName string, mtu int, hairpinMode bool) error { func setupVeth(netns ns.NetNS, br *netlink.Bridge, ifName string, mtu int, hairpinMode bool) (*current.Interface, *current.Interface, error) {
var hostVethName string contIface := &current.Interface{}
hostIface := &current.Interface{}
err := netns.Do(func(hostNS ns.NetNS) error { err := netns.Do(func(hostNS ns.NetNS) error {
// create the veth pair in the container and move host end into host netns // create the veth pair in the container and move host end into host netns
hostVeth, _, err := ip.SetupVeth(ifName, mtu, hostNS) hostVeth, containerVeth, err := ip.SetupVeth(ifName, mtu, hostNS)
if err != nil { if err != nil {
return err return err
} }
contIface.Name = containerVeth.Attrs().Name
hostVethName = hostVeth.Attrs().Name contIface.Mac = containerVeth.Attrs().HardwareAddr.String()
contIface.Sandbox = netns.Path()
hostIface.Name = hostVeth.Attrs().Name
return nil return nil
}) })
if err != nil { if err != nil {
return err return nil, nil, err
} }
// need to lookup hostVeth again as its index has changed during ns move // need to lookup hostVeth again as its index has changed during ns move
hostVeth, err := netlink.LinkByName(hostVethName) hostVeth, err := netlink.LinkByName(hostIface.Name)
if err != nil { if err != nil {
return fmt.Errorf("failed to lookup %q: %v", hostVethName, err) return nil, nil, fmt.Errorf("failed to lookup %q: %v", hostIface.Name, err)
} }
hostIface.Mac = hostVeth.Attrs().HardwareAddr.String()
// connect host veth end to the bridge // connect host veth end to the bridge
if err = netlink.LinkSetMaster(hostVeth, br); err != nil { if err := netlink.LinkSetMaster(hostVeth, br); err != nil {
return fmt.Errorf("failed to connect %q to bridge %v: %v", hostVethName, br.Attrs().Name, err) return nil, nil, fmt.Errorf("failed to connect %q to bridge %v: %v", hostVeth.Attrs().Name, br.Attrs().Name, err)
} }
// set hairpin mode // set hairpin mode
if err = netlink.LinkSetHairpin(hostVeth, hairpinMode); err != nil { if err = netlink.LinkSetHairpin(hostVeth, hairpinMode); err != nil {
return fmt.Errorf("failed to setup hairpin mode for %v: %v", hostVethName, err) return nil, nil, fmt.Errorf("failed to setup hairpin mode for %v: %v", hostVeth.Attrs().Name, err)
} }
return nil return hostIface, contIface, nil
} }
func calcGatewayIP(ipn *net.IPNet) net.IP { func calcGatewayIP(ipn *net.IPNet) net.IP {
@ -199,18 +203,21 @@ func calcGatewayIP(ipn *net.IPNet) net.IP {
return ip.NextIP(nid) return ip.NextIP(nid)
} }
func setupBridge(n *NetConf) (*netlink.Bridge, error) { func setupBridge(n *NetConf) (*netlink.Bridge, *current.Interface, error) {
// create bridge if necessary // create bridge if necessary
br, err := ensureBridge(n.BrName, n.MTU) br, err := ensureBridge(n.BrName, n.MTU)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create bridge %q: %v", n.BrName, err) return nil, nil, fmt.Errorf("failed to create bridge %q: %v", n.BrName, err)
} }
return br, nil return br, &current.Interface{
Name: br.Attrs().Name,
Mac: br.Attrs().HardwareAddr.String(),
}, nil
} }
func cmdAdd(args *skel.CmdArgs) error { func cmdAdd(args *skel.CmdArgs) error {
n, err := loadNetConf(args.StdinData) n, cniVersion, err := loadNetConf(args.StdinData)
if err != nil { if err != nil {
return err return err
} }
@ -219,7 +226,7 @@ func cmdAdd(args *skel.CmdArgs) error {
n.IsGW = true n.IsGW = true
} }
br, err := setupBridge(n) br, brInterface, err := setupBridge(n)
if err != nil { if err != nil {
return err return err
} }
@ -230,7 +237,8 @@ func cmdAdd(args *skel.CmdArgs) error {
} }
defer netns.Close() defer netns.Close()
if err = setupVeth(netns, br, args.IfName, n.MTU, n.HairpinMode); err != nil { hostInterface, containerInterface, err := setupVeth(netns, br, args.IfName, n.MTU, n.HairpinMode)
if err != nil {
return err return err
} }
@ -240,31 +248,45 @@ func cmdAdd(args *skel.CmdArgs) error {
return err return err
} }
result, err := current.GetResult(r) // Convert whatever the IPAM result was into the current Result type
result, err := current.NewResultFromResult(r)
if err != nil { if err != nil {
return err return err
} }
// TODO: make this optional when IPv6 is supported if len(result.IPs) == 0 {
if result.IP4 == nil { return errors.New("IPAM plugin returned missing IP config")
return errors.New("IPAM plugin returned missing IPv4 config")
} }
if result.IP4.Gateway == nil && n.IsGW { result.Interfaces = []*current.Interface{brInterface, hostInterface, containerInterface}
result.IP4.Gateway = calcGatewayIP(&result.IP4.IP)
for _, ipc := range result.IPs {
// All IPs currently refer to the container interface
ipc.Interface = 2
if ipc.Gateway == nil && n.IsGW {
ipc.Gateway = calcGatewayIP(&ipc.Address)
}
} }
if err := netns.Do(func(_ ns.NetNS) error { if err := netns.Do(func(_ ns.NetNS) error {
// set the default gateway if requested // set the default gateway if requested
if n.IsDefaultGW { if n.IsDefaultGW {
_, defaultNet, err := net.ParseCIDR("0.0.0.0/0") for _, ipc := range result.IPs {
if err != nil { defaultNet := &net.IPNet{}
return err switch {
case ipc.Address.IP.To4() != nil:
defaultNet.IP = net.IPv4zero
defaultNet.Mask = net.IPMask(net.IPv4zero)
case len(ipc.Address.IP) == net.IPv6len && ipc.Address.IP.To4() == nil:
defaultNet.IP = net.IPv6zero
defaultNet.Mask = net.IPMask(net.IPv6zero)
default:
return fmt.Errorf("Unknown IP object: %v", ipc)
} }
for _, route := range result.IP4.Routes { for _, route := range result.Routes {
if defaultNet.String() == route.Dst.String() { if defaultNet.String() == route.Dst.String() {
if route.GW != nil && !route.GW.Equal(result.IP4.Gateway) { if route.GW != nil && !route.GW.Equal(ipc.Gateway) {
return fmt.Errorf( return fmt.Errorf(
"isDefaultGateway ineffective because IPAM sets default route via %q", "isDefaultGateway ineffective because IPAM sets default route via %q",
route.GW, route.GW,
@ -273,40 +295,54 @@ func cmdAdd(args *skel.CmdArgs) error {
} }
} }
result.IP4.Routes = append( result.Routes = append(
result.IP4.Routes, result.Routes,
types.Route{Dst: *defaultNet, GW: result.IP4.Gateway}, &types.Route{Dst: *defaultNet, GW: ipc.Gateway},
) )
}
// TODO: IPV6
} }
if err := ipam.ConfigureIface(args.IfName, result); err != nil { if err := ipam.ConfigureIface(args.IfName, result); err != nil {
return err return err
} }
if err := ip.SetHWAddrByIP(args.IfName, result.IP4.IP.IP, nil /* TODO IPv6 */); err != nil { if err := ip.SetHWAddrByIP(args.IfName, result.IPs[0].Address.IP, nil /* TODO IPv6 */); err != nil {
return err return err
} }
// Refetch the veth since its MAC address may changed
link, err := netlink.LinkByName(args.IfName)
if err != nil {
return fmt.Errorf("could not lookup %q: %v", args.IfName, err)
}
containerInterface.Mac = link.Attrs().HardwareAddr.String()
return nil return nil
}); err != nil { }); err != nil {
return err return err
} }
if n.IsGW { if n.IsGW {
var firstV4Addr net.IP
for _, ipc := range result.IPs {
gwn := &net.IPNet{ gwn := &net.IPNet{
IP: result.IP4.Gateway, IP: ipc.Gateway,
Mask: result.IP4.IP.Mask, Mask: ipc.Address.Mask,
}
if ipc.Gateway.To4() != nil && firstV4Addr == nil {
firstV4Addr = ipc.Gateway
} }
if err = ensureBridgeAddr(br, gwn, n.ForceAddress); err != nil { if err = ensureBridgeAddr(br, gwn, n.ForceAddress); err != nil {
return err return err
} }
}
if err := ip.SetHWAddrByIP(n.BrName, gwn.IP, nil /* TODO IPv6 */); err != nil { if firstV4Addr != nil {
if err := ip.SetHWAddrByIP(n.BrName, firstV4Addr, nil /* TODO IPv6 */); err != nil {
return err return err
} }
}
if err := ip.EnableIP4Forward(); err != nil { if err := ip.EnableIP4Forward(); err != nil {
return fmt.Errorf("failed to enable forwarding: %v", err) return fmt.Errorf("failed to enable forwarding: %v", err)
@ -316,17 +352,28 @@ func cmdAdd(args *skel.CmdArgs) error {
if n.IPMasq { if n.IPMasq {
chain := utils.FormatChainName(n.Name, args.ContainerID) chain := utils.FormatChainName(n.Name, args.ContainerID)
comment := utils.FormatComment(n.Name, args.ContainerID) comment := utils.FormatComment(n.Name, args.ContainerID)
if err = ip.SetupIPMasq(ip.Network(&result.IP4.IP), chain, comment); err != nil { for _, ipc := range result.IPs {
if err = ip.SetupIPMasq(ip.Network(&ipc.Address), chain, comment); err != nil {
return err return err
} }
} }
}
// Refetch the bridge since its MAC address may change when the first
// veth is added or after its IP address is set
br, err = bridgeByName(n.BrName)
if err != nil {
return err
}
brInterface.Mac = br.Attrs().HardwareAddr.String()
result.DNS = n.DNS result.DNS = n.DNS
return result.Print()
return types.PrintResult(result, cniVersion)
} }
func cmdDel(args *skel.CmdArgs) error { func cmdDel(args *skel.CmdArgs) error {
n, err := loadNetConf(args.StdinData) n, _, err := loadNetConf(args.StdinData)
if err != nil { if err != nil {
return err return err
} }
@ -361,5 +408,5 @@ func cmdDel(args *skel.CmdArgs) error {
} }
func main() { func main() {
skel.PluginMain(cmdAdd, cmdDel, version.Legacy) skel.PluginMain(cmdAdd, cmdDel, version.All)
} }

View File

@ -17,12 +17,15 @@ package main
import ( import (
"fmt" "fmt"
"net" "net"
"strings"
"syscall" "syscall"
"github.com/containernetworking/cni/pkg/ns" "github.com/containernetworking/cni/pkg/ns"
"github.com/containernetworking/cni/pkg/skel" "github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/testutils" "github.com/containernetworking/cni/pkg/testutils"
"github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/types/020"
"github.com/containernetworking/cni/pkg/types/current"
"github.com/containernetworking/cni/pkg/utils/hwaddr" "github.com/containernetworking/cni/pkg/utils/hwaddr"
@ -51,7 +54,7 @@ var _ = Describe("bridge Operations", func() {
conf := &NetConf{ conf := &NetConf{
NetConf: types.NetConf{ NetConf: types.NetConf{
CNIVersion: "0.2.0", CNIVersion: "0.3.0",
Name: "testConfig", Name: "testConfig",
Type: "bridge", Type: "bridge",
}, },
@ -64,7 +67,7 @@ var _ = Describe("bridge Operations", func() {
err := originalNS.Do(func(ns.NetNS) error { err := originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover() defer GinkgoRecover()
bridge, err := setupBridge(conf) bridge, _, err := setupBridge(conf)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(bridge.Attrs().Name).To(Equal(IFNAME)) Expect(bridge.Attrs().Name).To(Equal(IFNAME))
@ -96,7 +99,7 @@ var _ = Describe("bridge Operations", func() {
conf := &NetConf{ conf := &NetConf{
NetConf: types.NetConf{ NetConf: types.NetConf{
CNIVersion: "0.2.0", CNIVersion: "0.3.0",
Name: "testConfig", Name: "testConfig",
Type: "bridge", Type: "bridge",
}, },
@ -105,7 +108,7 @@ var _ = Describe("bridge Operations", func() {
IPMasq: false, IPMasq: false,
} }
bridge, err := setupBridge(conf) bridge, _, err := setupBridge(conf)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(bridge.Attrs().Name).To(Equal(IFNAME)) Expect(bridge.Attrs().Name).To(Equal(IFNAME))
Expect(bridge.Attrs().Index).To(Equal(ifindex)) Expect(bridge.Attrs().Index).To(Equal(ifindex))
@ -128,7 +131,7 @@ var _ = Describe("bridge Operations", func() {
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
conf := fmt.Sprintf(`{ conf := fmt.Sprintf(`{
"cniVersion": "0.2.0", "cniVersion": "0.3.0",
"name": "mynet", "name": "mynet",
"type": "bridge", "type": "bridge",
"bridge": "%s", "bridge": "%s",
@ -151,18 +154,31 @@ var _ = Describe("bridge Operations", func() {
StdinData: []byte(conf), StdinData: []byte(conf),
} }
var result *current.Result
err = originalNS.Do(func(ns.NetNS) error { err = originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover() defer GinkgoRecover()
_, _, err := testutils.CmdAddWithResult(targetNs.Path(), IFNAME, []byte(conf), func() error { r, raw, err := testutils.CmdAddWithResult(targetNs.Path(), IFNAME, []byte(conf), func() error {
return cmdAdd(args) return cmdAdd(args)
}) })
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(strings.Index(string(raw), "\"interfaces\":")).Should(BeNumerically(">", 0))
result, err = current.GetResult(r)
Expect(err).NotTo(HaveOccurred())
Expect(len(result.Interfaces)).To(Equal(3))
Expect(result.Interfaces[0].Name).To(Equal(BRNAME))
Expect(result.Interfaces[2].Name).To(Equal(IFNAME))
// Make sure bridge link exists // Make sure bridge link exists
link, err := netlink.LinkByName(BRNAME) link, err := netlink.LinkByName(result.Interfaces[0].Name)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(link.Attrs().Name).To(Equal(BRNAME)) Expect(link.Attrs().Name).To(Equal(BRNAME))
Expect(link).To(BeAssignableToTypeOf(&netlink.Bridge{}))
Expect(link.Attrs().HardwareAddr.String()).To(Equal(result.Interfaces[0].Mac))
hwAddr := fmt.Sprintf("%s", link.Attrs().HardwareAddr)
Expect(hwAddr).To(HavePrefix(hwaddr.PrivateMACPrefixString))
// Ensure bridge has gateway address // Ensure bridge has gateway address
addrs, err := netlink.AddrList(link, syscall.AF_INET) addrs, err := netlink.AddrList(link, syscall.AF_INET)
@ -183,23 +199,10 @@ var _ = Describe("bridge Operations", func() {
links, err := netlink.LinkList() links, err := netlink.LinkList()
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(len(links)).To(Equal(3)) // Bridge, veth, and loopback Expect(len(links)).To(Equal(3)) // Bridge, veth, and loopback
for _, l := range links {
switch { link, err = netlink.LinkByName(result.Interfaces[1].Name)
case l.Attrs().Name == BRNAME:
{
_, isBridge := l.(*netlink.Bridge)
Expect(isBridge).To(Equal(true))
hwAddr := fmt.Sprintf("%s", l.Attrs().HardwareAddr)
Expect(hwAddr).To(HavePrefix(hwaddr.PrivateMACPrefixString))
}
case l.Attrs().Name != BRNAME && l.Attrs().Name != "lo":
{
_, isVeth := l.(*netlink.Veth)
Expect(isVeth).To(Equal(true))
}
}
}
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(link).To(BeAssignableToTypeOf(&netlink.Veth{}))
return nil return nil
}) })
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
@ -211,6 +214,11 @@ var _ = Describe("bridge Operations", func() {
link, err := netlink.LinkByName(IFNAME) link, err := netlink.LinkByName(IFNAME)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(link.Attrs().Name).To(Equal(IFNAME)) Expect(link.Attrs().Name).To(Equal(IFNAME))
Expect(link).To(BeAssignableToTypeOf(&netlink.Veth{}))
addrs, err := netlink.AddrList(link, syscall.AF_INET)
Expect(err).NotTo(HaveOccurred())
Expect(len(addrs)).To(Equal(1))
hwAddr := fmt.Sprintf("%s", link.Attrs().HardwareAddr) hwAddr := fmt.Sprintf("%s", link.Attrs().HardwareAddr)
Expect(hwAddr).To(HavePrefix(hwaddr.PrivateMACPrefixString)) Expect(hwAddr).To(HavePrefix(hwaddr.PrivateMACPrefixString))
@ -243,7 +251,7 @@ var _ = Describe("bridge Operations", func() {
}) })
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
// Make sure macvlan link has been deleted // Make sure the host veth has been deleted
err = targetNs.Do(func(ns.NetNS) error { err = targetNs.Do(func(ns.NetNS) error {
defer GinkgoRecover() defer GinkgoRecover()
@ -253,6 +261,150 @@ var _ = Describe("bridge Operations", func() {
return nil return nil
}) })
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
// Make sure the container veth has been deleted
err = originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
link, err := netlink.LinkByName(result.Interfaces[1].Name)
Expect(err).To(HaveOccurred())
Expect(link).To(BeNil())
return nil
})
})
It("configures and deconfigures a bridge and veth with default route with ADD/DEL for 0.1.0 config", func() {
const BRNAME = "cni0"
const IFNAME = "eth0"
gwaddr, subnet, err := net.ParseCIDR("10.1.2.1/24")
Expect(err).NotTo(HaveOccurred())
conf := fmt.Sprintf(`{
"cniVersion": "0.1.0",
"name": "mynet",
"type": "bridge",
"bridge": "%s",
"isDefaultGateway": true,
"ipMasq": false,
"ipam": {
"type": "host-local",
"subnet": "%s"
}
}`, BRNAME, subnet.String())
targetNs, err := ns.NewNS()
Expect(err).NotTo(HaveOccurred())
defer targetNs.Close()
args := &skel.CmdArgs{
ContainerID: "dummy",
Netns: targetNs.Path(),
IfName: IFNAME,
StdinData: []byte(conf),
}
var result *types020.Result
err = originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
r, raw, err := testutils.CmdAddWithResult(targetNs.Path(), IFNAME, []byte(conf), func() error {
return cmdAdd(args)
})
Expect(err).NotTo(HaveOccurred())
Expect(strings.Index(string(raw), "\"ip4\":")).Should(BeNumerically(">", 0))
// We expect a version 0.1.0 result
result, err = types020.GetResult(r)
Expect(err).NotTo(HaveOccurred())
// Make sure bridge link exists
link, err := netlink.LinkByName(BRNAME)
Expect(err).NotTo(HaveOccurred())
Expect(link.Attrs().Name).To(Equal(BRNAME))
Expect(link).To(BeAssignableToTypeOf(&netlink.Bridge{}))
hwAddr := fmt.Sprintf("%s", link.Attrs().HardwareAddr)
Expect(hwAddr).To(HavePrefix(hwaddr.PrivateMACPrefixString))
// Ensure bridge has gateway address
addrs, err := netlink.AddrList(link, syscall.AF_INET)
Expect(err).NotTo(HaveOccurred())
Expect(len(addrs)).To(BeNumerically(">", 0))
found := false
subnetPrefix, subnetBits := subnet.Mask.Size()
for _, a := range addrs {
aPrefix, aBits := a.IPNet.Mask.Size()
if a.IPNet.IP.Equal(gwaddr) && aPrefix == subnetPrefix && aBits == subnetBits {
found = true
break
}
}
Expect(found).To(Equal(true))
// Check for the veth link in the main namespace; can't
// check the for the specific link since version 0.1.0
// doesn't report interfaces
links, err := netlink.LinkList()
Expect(err).NotTo(HaveOccurred())
Expect(len(links)).To(Equal(3)) // Bridge, veth, and loopback
return nil
})
Expect(err).NotTo(HaveOccurred())
// Find the veth peer in the container namespace and the default route
err = targetNs.Do(func(ns.NetNS) error {
defer GinkgoRecover()
link, err := netlink.LinkByName(IFNAME)
Expect(err).NotTo(HaveOccurred())
Expect(link.Attrs().Name).To(Equal(IFNAME))
Expect(link).To(BeAssignableToTypeOf(&netlink.Veth{}))
addrs, err := netlink.AddrList(link, syscall.AF_INET)
Expect(err).NotTo(HaveOccurred())
Expect(len(addrs)).To(Equal(1))
hwAddr := fmt.Sprintf("%s", link.Attrs().HardwareAddr)
Expect(hwAddr).To(HavePrefix(hwaddr.PrivateMACPrefixString))
// Ensure the default route
routes, err := netlink.RouteList(link, 0)
Expect(err).NotTo(HaveOccurred())
var defaultRouteFound bool
for _, route := range routes {
defaultRouteFound = (route.Dst == nil && route.Src == nil && route.Gw.Equal(gwaddr))
if defaultRouteFound {
break
}
}
Expect(defaultRouteFound).To(Equal(true))
return nil
})
Expect(err).NotTo(HaveOccurred())
err = originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
err := testutils.CmdDelWithResult(targetNs.Path(), IFNAME, func() error {
return cmdDel(args)
})
Expect(err).NotTo(HaveOccurred())
return nil
})
Expect(err).NotTo(HaveOccurred())
// Make sure the container veth has been deleted; cannot check
// host veth as version 0.1.0 can't report its name
err = originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
link, err := netlink.LinkByName(IFNAME)
Expect(err).To(HaveOccurred())
Expect(link).To(BeNil())
return nil
})
}) })
It("ensure bridge address", func() { It("ensure bridge address", func() {
@ -262,7 +414,7 @@ var _ = Describe("bridge Operations", func() {
conf := &NetConf{ conf := &NetConf{
NetConf: types.NetConf{ NetConf: types.NetConf{
CNIVersion: "0.2.0", CNIVersion: "0.3.0",
Name: "testConfig", Name: "testConfig",
Type: "bridge", Type: "bridge",
}, },
@ -285,7 +437,7 @@ var _ = Describe("bridge Operations", func() {
err := originalNS.Do(func(ns.NetNS) error { err := originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover() defer GinkgoRecover()
bridge, err := setupBridge(conf) bridge, _, err := setupBridge(conf)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
// Check if ForceAddress has default value // Check if ForceAddress has default value
Expect(conf.ForceAddress).To(Equal(false)) Expect(conf.ForceAddress).To(Equal(false))

View File

@ -44,15 +44,15 @@ func init() {
runtime.LockOSThread() runtime.LockOSThread()
} }
func loadConf(bytes []byte) (*NetConf, error) { func loadConf(bytes []byte) (*NetConf, string, error) {
n := &NetConf{} n := &NetConf{}
if err := json.Unmarshal(bytes, n); err != nil { if err := json.Unmarshal(bytes, n); err != nil {
return nil, fmt.Errorf("failed to load netconf: %v", err) return nil, "", fmt.Errorf("failed to load netconf: %v", err)
} }
if n.Master == "" { if n.Master == "" {
return nil, fmt.Errorf(`"master" field is required. It specifies the host interface name to virtualize`) return nil, "", fmt.Errorf(`"master" field is required. It specifies the host interface name to virtualize`)
} }
return n, nil return n, n.CNIVersion, nil
} }
func modeFromString(s string) (netlink.IPVlanMode, error) { func modeFromString(s string) (netlink.IPVlanMode, error) {
@ -68,22 +68,24 @@ func modeFromString(s string) (netlink.IPVlanMode, error) {
} }
} }
func createIpvlan(conf *NetConf, ifName string, netns ns.NetNS) error { func createIpvlan(conf *NetConf, ifName string, netns ns.NetNS) (*current.Interface, error) {
ipvlan := &current.Interface{}
mode, err := modeFromString(conf.Mode) mode, err := modeFromString(conf.Mode)
if err != nil { if err != nil {
return err return nil, err
} }
m, err := netlink.LinkByName(conf.Master) m, err := netlink.LinkByName(conf.Master)
if err != nil { if err != nil {
return fmt.Errorf("failed to lookup master %q: %v", conf.Master, err) return nil, fmt.Errorf("failed to lookup master %q: %v", conf.Master, err)
} }
// due to kernel bug we have to create with tmpname or it might // due to kernel bug we have to create with tmpname or it might
// collide with the name on the host and error out // collide with the name on the host and error out
tmpName, err := ip.RandomVethName() tmpName, err := ip.RandomVethName()
if err != nil { if err != nil {
return err return nil, err
} }
mv := &netlink.IPVlan{ mv := &netlink.IPVlan{
@ -97,20 +99,35 @@ func createIpvlan(conf *NetConf, ifName string, netns ns.NetNS) error {
} }
if err := netlink.LinkAdd(mv); err != nil { if err := netlink.LinkAdd(mv); err != nil {
return fmt.Errorf("failed to create ipvlan: %v", err) return nil, fmt.Errorf("failed to create ipvlan: %v", err)
} }
return netns.Do(func(_ ns.NetNS) error { err = netns.Do(func(_ ns.NetNS) error {
err := ip.RenameLink(tmpName, ifName) err := ip.RenameLink(tmpName, ifName)
if err != nil { if err != nil {
return fmt.Errorf("failed to rename ipvlan to %q: %v", ifName, err) return fmt.Errorf("failed to rename ipvlan to %q: %v", ifName, err)
} }
ipvlan.Name = ifName
// Re-fetch ipvlan to get all properties/attributes
contIpvlan, err := netlink.LinkByName(ipvlan.Name)
if err != nil {
return fmt.Errorf("failed to refetch ipvlan %q: %v", ipvlan.Name, err)
}
ipvlan.Mac = contIpvlan.Attrs().HardwareAddr.String()
ipvlan.Sandbox = netns.Path()
return nil return nil
}) })
if err != nil {
return nil, err
}
return ipvlan, nil
} }
func cmdAdd(args *skel.CmdArgs) error { func cmdAdd(args *skel.CmdArgs) error {
n, err := loadConf(args.StdinData) n, cniVersion, err := loadConf(args.StdinData)
if err != nil { if err != nil {
return err return err
} }
@ -121,7 +138,8 @@ func cmdAdd(args *skel.CmdArgs) error {
} }
defer netns.Close() defer netns.Close()
if err = createIpvlan(n, args.IfName, netns); err != nil { ipvlanInterface, err := createIpvlan(n, args.IfName, netns)
if err != nil {
return err return err
} }
@ -130,14 +148,21 @@ func cmdAdd(args *skel.CmdArgs) error {
if err != nil { if err != nil {
return err return err
} }
result, err := current.GetResult(r) // Convert whatever the IPAM result was into the current Result type
result, err := current.NewResultFromResult(r)
if err != nil { if err != nil {
return err return err
} }
if result.IP4 == nil { if len(result.IPs) == 0 {
return errors.New("IPAM plugin returned missing IPv4 config") return errors.New("IPAM plugin returned missing IP config")
} }
for _, ipc := range result.IPs {
// All addresses belong to the ipvlan interface
ipc.Interface = 0
}
result.Interfaces = []*current.Interface{ipvlanInterface}
err = netns.Do(func(_ ns.NetNS) error { err = netns.Do(func(_ ns.NetNS) error {
return ipam.ConfigureIface(args.IfName, result) return ipam.ConfigureIface(args.IfName, result)
@ -147,11 +172,12 @@ func cmdAdd(args *skel.CmdArgs) error {
} }
result.DNS = n.DNS result.DNS = n.DNS
return result.Print()
return types.PrintResult(result, cniVersion)
} }
func cmdDel(args *skel.CmdArgs) error { func cmdDel(args *skel.CmdArgs) error {
n, err := loadConf(args.StdinData) n, _, err := loadConf(args.StdinData)
if err != nil { if err != nil {
return err return err
} }
@ -171,5 +197,5 @@ func cmdDel(args *skel.CmdArgs) error {
} }
func main() { func main() {
skel.PluginMain(cmdAdd, cmdDel, version.Legacy) skel.PluginMain(cmdAdd, cmdDel, version.All)
} }

View File

@ -16,11 +16,14 @@ package main
import ( import (
"fmt" "fmt"
"net"
"syscall"
"github.com/containernetworking/cni/pkg/ns" "github.com/containernetworking/cni/pkg/ns"
"github.com/containernetworking/cni/pkg/skel" "github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/testutils" "github.com/containernetworking/cni/pkg/testutils"
"github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/types/current"
"github.com/vishvananda/netlink" "github.com/vishvananda/netlink"
@ -63,7 +66,7 @@ var _ = Describe("ipvlan Operations", func() {
It("creates an ipvlan link in a non-default namespace", func() { It("creates an ipvlan link in a non-default namespace", func() {
conf := &NetConf{ conf := &NetConf{
NetConf: types.NetConf{ NetConf: types.NetConf{
CNIVersion: "0.2.0", CNIVersion: "0.3.0",
Name: "testConfig", Name: "testConfig",
Type: "ipvlan", Type: "ipvlan",
}, },
@ -80,10 +83,11 @@ var _ = Describe("ipvlan Operations", func() {
err = originalNS.Do(func(ns.NetNS) error { err = originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover() defer GinkgoRecover()
err := createIpvlan(conf, "foobar0", targetNs) _, err := createIpvlan(conf, "foobar0", targetNs)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
return nil return nil
}) })
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
// Make sure ipvlan link exists in the target namespace // Make sure ipvlan link exists in the target namespace
@ -102,7 +106,7 @@ var _ = Describe("ipvlan Operations", func() {
const IFNAME = "ipvl0" const IFNAME = "ipvl0"
conf := fmt.Sprintf(`{ conf := fmt.Sprintf(`{
"cniVersion": "0.2.0", "cniVersion": "0.3.0",
"name": "mynet", "name": "mynet",
"type": "ipvlan", "type": "ipvlan",
"master": "%s", "master": "%s",
@ -123,13 +127,21 @@ var _ = Describe("ipvlan Operations", func() {
StdinData: []byte(conf), StdinData: []byte(conf),
} }
var result *current.Result
err = originalNS.Do(func(ns.NetNS) error { err = originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover() defer GinkgoRecover()
_, _, err := testutils.CmdAddWithResult(targetNs.Path(), IFNAME, []byte(conf), func() error { r, _, err := testutils.CmdAddWithResult(targetNs.Path(), IFNAME, []byte(conf), func() error {
return cmdAdd(args) return cmdAdd(args)
}) })
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
result, err = current.GetResult(r)
Expect(err).NotTo(HaveOccurred())
Expect(len(result.Interfaces)).To(Equal(1))
Expect(result.Interfaces[0].Name).To(Equal(IFNAME))
Expect(len(result.IPs)).To(Equal(1))
return nil return nil
}) })
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
@ -141,6 +153,14 @@ var _ = Describe("ipvlan Operations", func() {
link, err := netlink.LinkByName(IFNAME) link, err := netlink.LinkByName(IFNAME)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(link.Attrs().Name).To(Equal(IFNAME)) Expect(link.Attrs().Name).To(Equal(IFNAME))
hwaddr, err := net.ParseMAC(result.Interfaces[0].Mac)
Expect(err).NotTo(HaveOccurred())
Expect(link.Attrs().HardwareAddr).To(Equal(hwaddr))
addrs, err := netlink.AddrList(link, syscall.AF_INET)
Expect(err).NotTo(HaveOccurred())
Expect(len(addrs)).To(Equal(1))
return nil return nil
}) })
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())

View File

@ -68,5 +68,5 @@ func cmdDel(args *skel.CmdArgs) error {
} }
func main() { func main() {
skel.PluginMain(cmdAdd, cmdDel, version.Legacy) skel.PluginMain(cmdAdd, cmdDel, version.All)
} }

View File

@ -18,6 +18,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"net"
"runtime" "runtime"
"github.com/containernetworking/cni/pkg/ip" "github.com/containernetworking/cni/pkg/ip"
@ -49,15 +50,15 @@ func init() {
runtime.LockOSThread() runtime.LockOSThread()
} }
func loadConf(bytes []byte) (*NetConf, error) { func loadConf(bytes []byte) (*NetConf, string, error) {
n := &NetConf{} n := &NetConf{}
if err := json.Unmarshal(bytes, n); err != nil { if err := json.Unmarshal(bytes, n); err != nil {
return nil, fmt.Errorf("failed to load netconf: %v", err) return nil, "", fmt.Errorf("failed to load netconf: %v", err)
} }
if n.Master == "" { if n.Master == "" {
return nil, fmt.Errorf(`"master" field is required. It specifies the host interface name to virtualize`) return nil, "", fmt.Errorf(`"master" field is required. It specifies the host interface name to virtualize`)
} }
return n, nil return n, n.CNIVersion, nil
} }
func modeFromString(s string) (netlink.MacvlanMode, error) { func modeFromString(s string) (netlink.MacvlanMode, error) {
@ -75,22 +76,24 @@ func modeFromString(s string) (netlink.MacvlanMode, error) {
} }
} }
func createMacvlan(conf *NetConf, ifName string, netns ns.NetNS) error { func createMacvlan(conf *NetConf, ifName string, netns ns.NetNS) (*current.Interface, error) {
macvlan := &current.Interface{}
mode, err := modeFromString(conf.Mode) mode, err := modeFromString(conf.Mode)
if err != nil { if err != nil {
return err return nil, err
} }
m, err := netlink.LinkByName(conf.Master) m, err := netlink.LinkByName(conf.Master)
if err != nil { if err != nil {
return fmt.Errorf("failed to lookup master %q: %v", conf.Master, err) return nil, fmt.Errorf("failed to lookup master %q: %v", conf.Master, err)
} }
// due to kernel bug we have to create with tmpName or it might // due to kernel bug we have to create with tmpName or it might
// collide with the name on the host and error out // collide with the name on the host and error out
tmpName, err := ip.RandomVethName() tmpName, err := ip.RandomVethName()
if err != nil { if err != nil {
return err return nil, err
} }
mv := &netlink.Macvlan{ mv := &netlink.Macvlan{
@ -104,10 +107,10 @@ func createMacvlan(conf *NetConf, ifName string, netns ns.NetNS) error {
} }
if err := netlink.LinkAdd(mv); err != nil { if err := netlink.LinkAdd(mv); err != nil {
return fmt.Errorf("failed to create macvlan: %v", err) return nil, fmt.Errorf("failed to create macvlan: %v", err)
} }
return netns.Do(func(_ ns.NetNS) error { err = netns.Do(func(_ ns.NetNS) error {
// TODO: duplicate following lines for ipv6 support, when it will be added in other places // TODO: duplicate following lines for ipv6 support, when it will be added in other places
ipv4SysctlValueName := fmt.Sprintf(IPv4InterfaceArpProxySysctlTemplate, tmpName) ipv4SysctlValueName := fmt.Sprintf(IPv4InterfaceArpProxySysctlTemplate, tmpName)
if _, err := sysctl.Sysctl(ipv4SysctlValueName, "1"); err != nil { if _, err := sysctl.Sysctl(ipv4SysctlValueName, "1"); err != nil {
@ -121,12 +124,27 @@ func createMacvlan(conf *NetConf, ifName string, netns ns.NetNS) error {
_ = netlink.LinkDel(mv) _ = netlink.LinkDel(mv)
return fmt.Errorf("failed to rename macvlan to %q: %v", ifName, err) return fmt.Errorf("failed to rename macvlan to %q: %v", ifName, err)
} }
macvlan.Name = ifName
// Re-fetch macvlan to get all properties/attributes
contMacvlan, err := netlink.LinkByName(ifName)
if err != nil {
return fmt.Errorf("failed to refetch macvlan %q: %v", ifName, err)
}
macvlan.Mac = contMacvlan.Attrs().HardwareAddr.String()
macvlan.Sandbox = netns.Path()
return nil return nil
}) })
if err != nil {
return nil, err
}
return macvlan, nil
} }
func cmdAdd(args *skel.CmdArgs) error { func cmdAdd(args *skel.CmdArgs) error {
n, err := loadConf(args.StdinData) n, cniVersion, err := loadConf(args.StdinData)
if err != nil { if err != nil {
return err return err
} }
@ -137,7 +155,8 @@ func cmdAdd(args *skel.CmdArgs) error {
} }
defer netns.Close() defer netns.Close()
if err = createMacvlan(n, args.IfName, netns); err != nil { macvlanInterface, err := createMacvlan(n, args.IfName, netns)
if err != nil {
return err return err
} }
@ -146,17 +165,30 @@ func cmdAdd(args *skel.CmdArgs) error {
if err != nil { if err != nil {
return err return err
} }
result, err := current.GetResult(r) // Convert whatever the IPAM result was into the current Result type
result, err := current.NewResultFromResult(r)
if err != nil { if err != nil {
return err return err
} }
if result.IP4 == nil { if len(result.IPs) == 0 {
return errors.New("IPAM plugin returned missing IPv4 config") return errors.New("IPAM plugin returned missing IP config")
}
result.Interfaces = []*current.Interface{macvlanInterface}
var firstV4Addr net.IP
for _, ipc := range result.IPs {
// All addresses apply to the container macvlan interface
ipc.Interface = 0
if ipc.Address.IP.To4() != nil && firstV4Addr == nil {
firstV4Addr = ipc.Address.IP
}
} }
if firstV4Addr != nil {
err = netns.Do(func(_ ns.NetNS) error { err = netns.Do(func(_ ns.NetNS) error {
if err := ip.SetHWAddrByIP(args.IfName, result.IP4.IP.IP, nil /* TODO IPv6 */); err != nil { if err := ip.SetHWAddrByIP(args.IfName, firstV4Addr, nil /* TODO IPv6 */); err != nil {
return err return err
} }
@ -165,13 +197,28 @@ func cmdAdd(args *skel.CmdArgs) error {
if err != nil { if err != nil {
return err return err
} }
}
// Re-fetch macvlan interface as its MAC address may have changed
err = netns.Do(func(_ ns.NetNS) error {
link, err := netlink.LinkByName(args.IfName)
if err != nil {
return fmt.Errorf("failed to re-fetch macvlan interface: %v", err)
}
macvlanInterface.Mac = link.Attrs().HardwareAddr.String()
return nil
})
if err != nil {
return err
}
result.DNS = n.DNS result.DNS = n.DNS
return result.Print()
return types.PrintResult(result, cniVersion)
} }
func cmdDel(args *skel.CmdArgs) error { func cmdDel(args *skel.CmdArgs) error {
n, err := loadConf(args.StdinData) n, _, err := loadConf(args.StdinData)
if err != nil { if err != nil {
return err return err
} }
@ -191,5 +238,5 @@ func cmdDel(args *skel.CmdArgs) error {
} }
func main() { func main() {
skel.PluginMain(cmdAdd, cmdDel, version.Legacy) skel.PluginMain(cmdAdd, cmdDel, version.All)
} }

View File

@ -16,11 +16,14 @@ package main
import ( import (
"fmt" "fmt"
"net"
"syscall"
"github.com/containernetworking/cni/pkg/ns" "github.com/containernetworking/cni/pkg/ns"
"github.com/containernetworking/cni/pkg/skel" "github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/testutils" "github.com/containernetworking/cni/pkg/testutils"
"github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/types/current"
"github.com/containernetworking/cni/pkg/utils/hwaddr" "github.com/containernetworking/cni/pkg/utils/hwaddr"
"github.com/vishvananda/netlink" "github.com/vishvananda/netlink"
@ -64,7 +67,7 @@ var _ = Describe("macvlan Operations", func() {
It("creates an macvlan link in a non-default namespace", func() { It("creates an macvlan link in a non-default namespace", func() {
conf := &NetConf{ conf := &NetConf{
NetConf: types.NetConf{ NetConf: types.NetConf{
CNIVersion: "0.2.0", CNIVersion: "0.3.0",
Name: "testConfig", Name: "testConfig",
Type: "macvlan", Type: "macvlan",
}, },
@ -80,7 +83,7 @@ var _ = Describe("macvlan Operations", func() {
err = originalNS.Do(func(ns.NetNS) error { err = originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover() defer GinkgoRecover()
err = createMacvlan(conf, "foobar0", targetNs) _, err = createMacvlan(conf, "foobar0", targetNs)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
return nil return nil
}) })
@ -102,7 +105,7 @@ var _ = Describe("macvlan Operations", func() {
const IFNAME = "macvl0" const IFNAME = "macvl0"
conf := fmt.Sprintf(`{ conf := fmt.Sprintf(`{
"cniVersion": "0.2.0", "cniVersion": "0.3.0",
"name": "mynet", "name": "mynet",
"type": "macvlan", "type": "macvlan",
"master": "%s", "master": "%s",
@ -123,14 +126,21 @@ var _ = Describe("macvlan Operations", func() {
StdinData: []byte(conf), StdinData: []byte(conf),
} }
// Make sure macvlan link exists in the target namespace var result *current.Result
err = originalNS.Do(func(ns.NetNS) error { err = originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover() defer GinkgoRecover()
_, _, err := testutils.CmdAddWithResult(targetNs.Path(), IFNAME, []byte(conf), func() error { r, _, err := testutils.CmdAddWithResult(targetNs.Path(), IFNAME, []byte(conf), func() error {
return cmdAdd(args) return cmdAdd(args)
}) })
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
result, err = current.GetResult(r)
Expect(err).NotTo(HaveOccurred())
Expect(len(result.Interfaces)).To(Equal(1))
Expect(result.Interfaces[0].Name).To(Equal(IFNAME))
Expect(len(result.IPs)).To(Equal(1))
return nil return nil
}) })
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
@ -143,9 +153,16 @@ var _ = Describe("macvlan Operations", func() {
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(link.Attrs().Name).To(Equal(IFNAME)) Expect(link.Attrs().Name).To(Equal(IFNAME))
hwAddr := fmt.Sprintf("%s", link.Attrs().HardwareAddr) hwaddrString := fmt.Sprintf("%s", link.Attrs().HardwareAddr)
Expect(hwAddr).To(HavePrefix(hwaddr.PrivateMACPrefixString)) Expect(hwaddrString).To(HavePrefix(hwaddr.PrivateMACPrefixString))
hwaddr, err := net.ParseMAC(result.Interfaces[0].Mac)
Expect(err).NotTo(HaveOccurred())
Expect(link.Attrs().HardwareAddr).To(Equal(hwaddr))
addrs, err := netlink.AddrList(link, syscall.AF_INET)
Expect(err).NotTo(HaveOccurred())
Expect(len(addrs)).To(Equal(1))
return nil return nil
}) })
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())

View File

@ -47,7 +47,7 @@ type NetConf struct {
MTU int `json:"mtu"` MTU int `json:"mtu"`
} }
func setupContainerVeth(netns, ifName string, mtu int, pr *current.Result) (string, error) { func setupContainerVeth(netns, ifName string, mtu int, pr *current.Result) (*current.Interface, *current.Interface, error) {
// The IPAM result will be something like IP=192.168.3.5/24, GW=192.168.3.1. // 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. // 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 // Next best thing would be to let it ARP but set interface to 192.168.3.5/32 and
@ -59,41 +59,66 @@ func setupContainerVeth(netns, ifName string, mtu int, pr *current.Result) (stri
// "192.168.3.1/32 dev $ifName" and "192.168.3.0/24 via 192.168.3.1 dev $ifName". // "192.168.3.1/32 dev $ifName" and "192.168.3.0/24 via 192.168.3.1 dev $ifName".
// In other words we force all traffic to ARP via the gateway except for GW itself. // In other words we force all traffic to ARP via the gateway except for GW itself.
var hostVethName string hostInterface := &current.Interface{}
containerInterface := &current.Interface{}
err := ns.WithNetNSPath(netns, func(hostNS ns.NetNS) error { err := ns.WithNetNSPath(netns, func(hostNS ns.NetNS) error {
hostVeth, _, err := ip.SetupVeth(ifName, mtu, hostNS) hostVeth, contVeth, err := ip.SetupVeth(ifName, mtu, hostNS)
if err != nil { if err != nil {
return err return err
} }
hostInterface.Name = hostVeth.Attrs().Name
hostInterface.Mac = hostVeth.Attrs().HardwareAddr.String()
containerInterface.Name = contVeth.Attrs().Name
containerInterface.Mac = contVeth.Attrs().HardwareAddr.String()
hostNS.Do(func(_ ns.NetNS) error { var firstV4Addr net.IP
hostVethName = hostVeth.Attrs().Name for _, ipc := range pr.IPs {
if err := ip.SetHWAddrByIP(hostVethName, pr.IP4.IP.IP, nil /* TODO IPv6 */); err != nil { // All addresses apply to the container veth interface
ipc.Interface = 1
if ipc.Address.IP.To4() != nil && firstV4Addr == nil {
firstV4Addr = ipc.Address.IP
}
}
pr.Interfaces = []*current.Interface{hostInterface, containerInterface}
if firstV4Addr != nil {
err = hostNS.Do(func(_ ns.NetNS) error {
hostVethName := hostVeth.Attrs().Name
if err := ip.SetHWAddrByIP(hostVethName, firstV4Addr, nil /* TODO IPv6 */); err != nil {
return fmt.Errorf("failed to set hardware addr by IP: %v", err) return fmt.Errorf("failed to set hardware addr by IP: %v", err)
} }
return nil return nil
}) })
if err != nil {
return err
}
}
if err = ipam.ConfigureIface(ifName, pr); err != nil { if err = ipam.ConfigureIface(ifName, pr); err != nil {
return err return err
} }
contVeth, err := netlink.LinkByName(ifName) if err := ip.SetHWAddrByIP(contVeth.Attrs().Name, firstV4Addr, nil /* TODO IPv6 */); err != nil {
return fmt.Errorf("failed to set hardware addr by IP: %v", err)
}
// Re-fetch container veth to update attributes
contVeth, err = netlink.LinkByName(ifName)
if err != nil { if err != nil {
return fmt.Errorf("failed to look up %q: %v", ifName, err) return fmt.Errorf("failed to look up %q: %v", ifName, err)
} }
if err := ip.SetHWAddrByIP(contVeth.Attrs().Name, pr.IP4.IP.IP, nil /* TODO IPv6 */); err != nil { for _, ipc := range pr.IPs {
return fmt.Errorf("failed to set hardware addr by IP: %v", err)
}
// Delete the route that was automatically added // Delete the route that was automatically added
route := netlink.Route{ route := netlink.Route{
LinkIndex: contVeth.Attrs().Index, LinkIndex: contVeth.Attrs().Index,
Dst: &net.IPNet{ Dst: &net.IPNet{
IP: pr.IP4.IP.IP.Mask(pr.IP4.IP.Mask), IP: ipc.Address.IP.Mask(ipc.Address.Mask),
Mask: pr.IP4.IP.Mask, Mask: ipc.Address.Mask,
}, },
Scope: netlink.SCOPE_NOWHERE, Scope: netlink.SCOPE_NOWHERE,
} }
@ -103,47 +128,56 @@ func setupContainerVeth(netns, ifName string, mtu int, pr *current.Result) (stri
} }
for _, r := range []netlink.Route{ for _, r := range []netlink.Route{
{ netlink.Route{
LinkIndex: contVeth.Attrs().Index, LinkIndex: contVeth.Attrs().Index,
Dst: &net.IPNet{ Dst: &net.IPNet{
IP: pr.IP4.Gateway, IP: ipc.Gateway,
Mask: net.CIDRMask(32, 32), Mask: net.CIDRMask(32, 32),
}, },
Scope: netlink.SCOPE_LINK, Scope: netlink.SCOPE_LINK,
Src: pr.IP4.IP.IP, Src: ipc.Address.IP,
}, },
{ netlink.Route{
LinkIndex: contVeth.Attrs().Index, LinkIndex: contVeth.Attrs().Index,
Dst: &net.IPNet{ Dst: &net.IPNet{
IP: pr.IP4.IP.IP.Mask(pr.IP4.IP.Mask), IP: ipc.Address.IP.Mask(ipc.Address.Mask),
Mask: pr.IP4.IP.Mask, Mask: ipc.Address.Mask,
}, },
Scope: netlink.SCOPE_UNIVERSE, Scope: netlink.SCOPE_UNIVERSE,
Gw: pr.IP4.Gateway, Gw: ipc.Gateway,
Src: pr.IP4.IP.IP, Src: ipc.Address.IP,
}, },
} { } {
if err := netlink.RouteAdd(&r); err != nil { if err := netlink.RouteAdd(&r); err != nil {
return fmt.Errorf("failed to add route %v: %v", r, err) return fmt.Errorf("failed to add route %v: %v", r, err)
} }
} }
}
return nil return nil
}) })
return hostVethName, err if err != nil {
return nil, nil, err
}
return hostInterface, containerInterface, nil
} }
func setupHostVeth(vethName string, ipConf *current.IPConfig) error { func setupHostVeth(vethName string, result *current.Result) error {
// hostVeth moved namespaces and may have a new ifindex // hostVeth moved namespaces and may have a new ifindex
veth, err := netlink.LinkByName(vethName) veth, err := netlink.LinkByName(vethName)
if err != nil { if err != nil {
return fmt.Errorf("failed to lookup %q: %v", vethName, err) return fmt.Errorf("failed to lookup %q: %v", vethName, err)
} }
// TODO(eyakubovich): IPv6 for _, ipc := range result.IPs {
maskLen := 128
if ipc.Address.IP.To4() != nil {
maskLen = 32
}
ipn := &net.IPNet{ ipn := &net.IPNet{
IP: ipConf.Gateway, IP: ipc.Gateway,
Mask: net.CIDRMask(32, 32), Mask: net.CIDRMask(maskLen, maskLen),
} }
addr := &netlink.Addr{IPNet: ipn, Label: ""} addr := &netlink.Addr{IPNet: ipn, Label: ""}
if err = netlink.AddrAdd(veth, addr); err != nil { if err = netlink.AddrAdd(veth, addr); err != nil {
@ -151,13 +185,14 @@ func setupHostVeth(vethName string, ipConf *current.IPConfig) error {
} }
ipn = &net.IPNet{ ipn = &net.IPNet{
IP: ipConf.IP.IP, IP: ipc.Address.IP,
Mask: net.CIDRMask(32, 32), Mask: net.CIDRMask(maskLen, maskLen),
} }
// dst happens to be the same as IP/net of host veth // dst happens to be the same as IP/net of host veth
if err = ip.AddHostRoute(ipn, nil, veth); err != nil && !os.IsExist(err) { if err = ip.AddHostRoute(ipn, nil, veth); err != nil && !os.IsExist(err) {
return fmt.Errorf("failed to add route on host: %v", err) return fmt.Errorf("failed to add route on host: %v", err)
} }
}
return nil return nil
} }
@ -177,33 +212,39 @@ func cmdAdd(args *skel.CmdArgs) error {
if err != nil { if err != nil {
return err return err
} }
result, err := current.GetResult(r) // Convert whatever the IPAM result was into the current Result type
if err != nil { result, err := current.NewResultFromResult(r)
return err
}
if result.IP4 == nil {
return errors.New("IPAM plugin returned missing IPv4 config")
}
hostVethName, err := setupContainerVeth(args.Netns, args.IfName, conf.MTU, result)
if err != nil { if err != nil {
return err return err
} }
if err = setupHostVeth(hostVethName, result.IP4); err != nil { if len(result.IPs) == 0 {
return errors.New("IPAM plugin returned missing IP config")
}
hostInterface, containerInterface, err := setupContainerVeth(args.Netns, args.IfName, conf.MTU, result)
if err != nil {
return err
}
if err = setupHostVeth(hostInterface.Name, result); err != nil {
return err return err
} }
if conf.IPMasq { if conf.IPMasq {
chain := utils.FormatChainName(conf.Name, args.ContainerID) chain := utils.FormatChainName(conf.Name, args.ContainerID)
comment := utils.FormatComment(conf.Name, args.ContainerID) comment := utils.FormatComment(conf.Name, args.ContainerID)
if err = ip.SetupIPMasq(&result.IP4.IP, chain, comment); err != nil { for _, ipc := range result.IPs {
if err = ip.SetupIPMasq(&ipc.Address, chain, comment); err != nil {
return err return err
} }
} }
}
result.DNS = conf.DNS result.DNS = conf.DNS
return result.Print() result.Interfaces = []*current.Interface{hostInterface, containerInterface}
return types.PrintResult(result, conf.CNIVersion)
} }
func cmdDel(args *skel.CmdArgs) error { func cmdDel(args *skel.CmdArgs) error {
@ -242,5 +283,5 @@ func cmdDel(args *skel.CmdArgs) error {
} }
func main() { func main() {
skel.PluginMain(cmdAdd, cmdDel, version.Legacy) skel.PluginMain(cmdAdd, cmdDel, version.All)
} }

View File

@ -43,7 +43,7 @@ var _ = Describe("ptp Operations", func() {
const IFNAME = "ptp0" const IFNAME = "ptp0"
conf := `{ conf := `{
"cniVersion": "0.2.0", "cniVersion": "0.3.0",
"name": "mynet", "name": "mynet",
"type": "ptp", "type": "ptp",
"ipMasq": true, "ipMasq": true,

View File

@ -257,5 +257,5 @@ func cmdDel(args *skel.CmdArgs) error {
} }
func main() { func main() {
skel.PluginMain(cmdAdd, cmdDel, version.Legacy) skel.PluginMain(cmdAdd, cmdDel, version.All)
} }

View File

@ -80,5 +80,5 @@ func cmdDel(args *skel.CmdArgs) error {
} }
func main() { func main() {
skel.PluginMain(cmdAdd, cmdDel, version.Legacy) skel.PluginMain(cmdAdd, cmdDel, version.All)
} }

View File

@ -142,7 +142,7 @@ func debugBehavior(args *skel.CmdArgs, command string) error {
} }
func debugGetSupportedVersions(stdinData []byte) []string { func debugGetSupportedVersions(stdinData []byte) []string {
vers := []string{"0.-42.0", "0.1.0", "0.2.0"} vers := []string{"0.-42.0", "0.1.0", "0.2.0", "0.3.0"}
cniArgs := os.Getenv("CNI_ARGS") cniArgs := os.Getenv("CNI_ARGS")
if cniArgs == "" { if cniArgs == "" {
return vers return vers

View File

@ -37,7 +37,7 @@ var _ = Describe("No-op plugin", func() {
expectedCmdArgs skel.CmdArgs expectedCmdArgs skel.CmdArgs
) )
const reportResult = `{ "ip4": { "ip": "10.1.2.3/24" }, "dns": {} }` const reportResult = `{ "ips": [{ "version": "4", "address": "10.1.2.3/24" }], "dns": {} }`
BeforeEach(func() { BeforeEach(func() {
debug = &noop_debug.Debug{ debug = &noop_debug.Debug{
@ -64,14 +64,14 @@ var _ = Describe("No-op plugin", func() {
// Keep this last // Keep this last
"CNI_ARGS=" + args, "CNI_ARGS=" + args,
} }
cmd.Stdin = strings.NewReader(`{"some":"stdin-json", "cniVersion": "0.2.0"}`) cmd.Stdin = strings.NewReader(`{"some":"stdin-json", "cniVersion": "0.3.0"}`)
expectedCmdArgs = skel.CmdArgs{ expectedCmdArgs = skel.CmdArgs{
ContainerID: "some-container-id", ContainerID: "some-container-id",
Netns: "/some/netns/path", Netns: "/some/netns/path",
IfName: "some-eth0", IfName: "some-eth0",
Args: args, Args: args,
Path: "/some/bin/path", Path: "/some/bin/path",
StdinData: []byte(`{"some":"stdin-json", "cniVersion": "0.2.0"}`), StdinData: []byte(`{"some":"stdin-json", "cniVersion": "0.3.0"}`),
} }
}) })
@ -102,15 +102,15 @@ var _ = Describe("No-op plugin", func() {
cmd.Stdin = strings.NewReader(`{ cmd.Stdin = strings.NewReader(`{
"some":"stdin-json", "some":"stdin-json",
"cniVersion": "0.2.0", "cniVersion": "0.3.0",
"prevResult": { "prevResult": {
"ip4": {"ip": "10.1.2.15/24"} "ips": [{"version": "4", "address": "10.1.2.15/24"}]
} }
}`) }`)
session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter) session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Eventually(session).Should(gexec.Exit(0)) Eventually(session).Should(gexec.Exit(0))
Expect(session.Out.Contents()).To(MatchJSON(`{"ip4": {"ip": "10.1.2.15/24"}, "dns": {}}`)) Expect(session.Out.Contents()).To(MatchJSON(`{"ips": [{"version": "4", "address": "10.1.2.15/24"}], "dns": {}}`))
}) })
It("injects DNS into previous result when ReportResult is INJECT-DNS", func() { It("injects DNS into previous result when ReportResult is INJECT-DNS", func() {
@ -119,9 +119,9 @@ var _ = Describe("No-op plugin", func() {
cmd.Stdin = strings.NewReader(`{ cmd.Stdin = strings.NewReader(`{
"some":"stdin-json", "some":"stdin-json",
"cniVersion": "0.2.0", "cniVersion": "0.3.0",
"prevResult": { "prevResult": {
"ip4": {"ip": "10.1.2.3/24"}, "ips": [{"version": "4", "address": "10.1.2.3/24"}],
"dns": {} "dns": {}
} }
}`) }`)
@ -130,7 +130,7 @@ var _ = Describe("No-op plugin", func() {
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Eventually(session).Should(gexec.Exit(0)) Eventually(session).Should(gexec.Exit(0))
Expect(session.Out.Contents()).To(MatchJSON(`{ Expect(session.Out.Contents()).To(MatchJSON(`{
"ip4": {"ip": "10.1.2.3/24"}, "ips": [{"version": "4", "address": "10.1.2.3/24"}],
"dns": {"nameservers": ["1.2.3.4"]} "dns": {"nameservers": ["1.2.3.4"]}
}`)) }`))
}) })
@ -139,7 +139,7 @@ var _ = Describe("No-op plugin", func() {
// Remove the DEBUG option from CNI_ARGS and regular args // Remove the DEBUG option from CNI_ARGS and regular args
newArgs := "FOO=BAR" newArgs := "FOO=BAR"
cmd.Env[len(cmd.Env)-1] = "CNI_ARGS=" + newArgs cmd.Env[len(cmd.Env)-1] = "CNI_ARGS=" + newArgs
newStdin := fmt.Sprintf(`{"some":"stdin-json", "cniVersion": "0.2.0", "debugFile": "%s"}`, debugFileName) newStdin := fmt.Sprintf(`{"some":"stdin-json", "cniVersion": "0.3.0", "debugFile": "%s"}`, debugFileName)
cmd.Stdin = strings.NewReader(newStdin) cmd.Stdin = strings.NewReader(newStdin)
expectedCmdArgs.Args = newArgs expectedCmdArgs.Args = newArgs
expectedCmdArgs.StdinData = []byte(newStdin) expectedCmdArgs.StdinData = []byte(newStdin)

2
test
View File

@ -11,7 +11,7 @@ set -e
source ./build source ./build
TESTABLE="libcni plugins/ipam/dhcp plugins/ipam/host-local plugins/ipam/host-local/backend/allocator plugins/main/loopback pkg/invoke pkg/ns pkg/skel pkg/types pkg/types/current pkg/utils plugins/main/ipvlan plugins/main/macvlan plugins/main/bridge plugins/main/ptp plugins/test/noop pkg/utils/hwaddr pkg/ip pkg/version pkg/version/testhelpers plugins/meta/flannel pkg/ipam" TESTABLE="libcni plugins/ipam/dhcp plugins/ipam/host-local plugins/ipam/host-local/backend/allocator plugins/main/loopback pkg/invoke pkg/ns pkg/skel pkg/types pkg/types/current pkg/types/020 pkg/utils plugins/main/ipvlan plugins/main/macvlan plugins/main/bridge plugins/main/ptp plugins/test/noop pkg/utils/hwaddr pkg/ip pkg/version pkg/version/testhelpers plugins/meta/flannel pkg/ipam"
FORMATTABLE="$TESTABLE pkg/testutils plugins/meta/flannel plugins/meta/tuning" FORMATTABLE="$TESTABLE pkg/testutils plugins/meta/flannel plugins/meta/tuning"
# user has not provided PKG override # user has not provided PKG override