diff --git a/plugins/ipam/static/README.md b/plugins/ipam/static/README.md index 7b3c01fe..37540603 100644 --- a/plugins/ipam/static/README.md +++ b/plugins/ipam/static/README.md @@ -38,8 +38,17 @@ static IPAM is very simple IPAM plugin that assigns IPv4 and IPv6 addresses stat ## Network configuration reference * `type` (string, required): "static" -* `addresses` (array, required): an array of arrays of ip address objects: +* `addresses` (array, optional): an array of arrays of ip address objects: * `address` (string, required): CIDR notation IP address. * `gateway` (string, optional): IP inside of "subnet" to designate as the gateway. * `routes` (string, optional): list of routes add to the container namespace. Each route is a dictionary with "dst" and optional "gw" fields. If "gw" is omitted, value of "gateway" will be used. * `dns` (string, optional): the dictionary with "nameservers", "domain" and "search". + +## Supported arguments +The following [CNI_ARGS](https://github.com/containernetworking/cni/blob/master/SPEC.md#parameters) are supported: + +* `IP`: request a specific IP address from a subnet +* `SUBNET`: request a specific subnet +* `GATEWAY`: request a specific gateway address + + (example: CNI_ARGS="IP=10.10.0.1;SUBNET=10.10.0.0/24;GATEWAY=10.10.0.254") diff --git a/plugins/ipam/static/main.go b/plugins/ipam/static/main.go index d236c0b6..74505644 100644 --- a/plugins/ipam/static/main.go +++ b/plugins/ipam/static/main.go @@ -30,19 +30,29 @@ import ( // The top-level network config - IPAM plugins are passed the full configuration // of the calling plugin, not just the IPAM section. type Net struct { - Name string `json:"name"` - CNIVersion string `json:"cniVersion"` - IPAM *IPAMConfig `json:"ipam"` + Name string `json:"name"` + CNIVersion string `json:"cniVersion"` + IPAM *IPAMConfig `json:"ipam"` + RuntimeConfig struct { + Addresses []Address `json:"addresses,omitempty"` + } `json:"runtimeConfig,omitempty"` } type IPAMConfig struct { Name string Type string `json:"type"` Routes []*types.Route `json:"routes"` - Addresses []Address `json:"addresses"` + Addresses []Address `json:"addresses,omitempty"` DNS types.DNS `json:"dns"` } +type IPAMEnvArgs struct { + types.CommonArgs + IP net.IP `json:"ip,omitempty"` + SUBNET types.UnmarshallableString `json:"subnet,omitempty"` + GATEWAY net.IP `json:"gateway,omitempty"` +} + type Address struct { AddressStr string `json:"address"` Gateway net.IP `json:"gateway,omitempty"` @@ -77,9 +87,14 @@ func LoadIPAMConfig(bytes []byte, envArgs string) (*IPAMConfig, string, error) { return nil, "", fmt.Errorf("IPAM config missing 'ipam' key") } + if len(n.RuntimeConfig.Addresses) != 0 { + n.IPAM.Addresses = append(n.RuntimeConfig.Addresses, n.IPAM.Addresses...) + } + // Validate all ranges numV4 := 0 numV6 := 0 + for i := range n.IPAM.Addresses { ip, addr, err := net.ParseCIDR(n.IPAM.Addresses[i].AddressStr) if err != nil { @@ -101,6 +116,34 @@ func LoadIPAMConfig(bytes []byte, envArgs string) (*IPAMConfig, string, error) { } } + if envArgs != "" { + e := IPAMEnvArgs{} + err := types.LoadArgs(envArgs, &e) + if err != nil { + return nil, "", err + } + + if e.IP != nil && e.SUBNET != "" { + _, subnet, err := net.ParseCIDR(string(e.SUBNET)) + if err != nil { + return nil, "", fmt.Errorf("invalid CIDR %s: %s", e.SUBNET, err) + } + + addr := Address{Address: net.IPNet{IP: e.IP, Mask: subnet.Mask}} + if e.GATEWAY != nil { + addr.Gateway = e.GATEWAY + } + if addr.Address.IP.To4() != nil { + addr.Version = "4" + numV4++ + } else { + addr.Version = "6" + numV6++ + } + n.IPAM.Addresses = append(n.IPAM.Addresses, addr) + } + } + // CNI spec 0.2.0 and below supported only one v4 and v6 address if numV4 > 1 || numV6 > 1 { for _, v := range types020.SupportedVersions { diff --git a/plugins/ipam/static/static_test.go b/plugins/ipam/static/static_test.go index 31ac3c28..77d0ee6a 100644 --- a/plugins/ipam/static/static_test.go +++ b/plugins/ipam/static/static_test.go @@ -148,6 +148,68 @@ var _ = Describe("static Operations", func() { }) Expect(err).NotTo(HaveOccurred()) }) + + It("allocates and releases addresses with ADD/DEL, with ENV variables", func() { + const ifname string = "eth0" + const nspath string = "/some/where" + + conf := `{ + "cniVersion": "0.3.1", + "name": "mynet", + "type": "ipvlan", + "master": "foo0", + "ipam": { + "type": "static", + "routes": [ + { "dst": "0.0.0.0/0" }, + { "dst": "192.168.0.0/16", "gw": "10.10.5.1" }], + "dns": { + "nameservers" : ["8.8.8.8"], + "domain": "example.com", + "search": [ "example.com" ] + } + } + }` + + args := &skel.CmdArgs{ + ContainerID: "dummy", + Netns: nspath, + IfName: ifname, + StdinData: []byte(conf), + Args: "IP=10.10.0.1;SUBNET=10.10.0.0/24;GATEWAY=10.10.0.254", + } + + // Allocate the IP + r, raw, err := testutils.CmdAddWithArgs(args, func() error { + return cmdAdd(args) + }) + Expect(err).NotTo(HaveOccurred()) + Expect(strings.Index(string(raw), "\"version\":")).Should(BeNumerically(">", 0)) + + result, err := current.GetResult(r) + Expect(err).NotTo(HaveOccurred()) + + // Gomega is cranky about slices with different caps + Expect(*result.IPs[0]).To(Equal( + current.IPConfig{ + Version: "4", + Address: mustCIDR("10.10.0.1/24"), + Gateway: net.ParseIP("10.10.0.254"), + })) + + Expect(len(result.IPs)).To(Equal(1)) + + Expect(result.Routes).To(Equal([]*types.Route{ + {Dst: mustCIDR("0.0.0.0/0")}, + {Dst: mustCIDR("192.168.0.0/16"), GW: net.ParseIP("10.10.5.1")}, + })) + + // Release the IP + err = testutils.CmdDelWithArgs(args, func() error { + return cmdDel(args) + }) + Expect(err).NotTo(HaveOccurred()) + }) }) func mustCIDR(s string) net.IPNet {