diff --git a/plugins/ipam/static/README.md b/plugins/ipam/static/README.md index 7b3c01fe9..d9ca28c5b 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 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 CIDR notation IP addresses, comma separated +* `GATEWAY`: request a specific gateway address + + (example: CNI_ARGS="IP=10.10.0.1/24;GATEWAY=10.10.0.254") diff --git a/plugins/ipam/static/main.go b/plugins/ipam/static/main.go index d578f3d46..1b4842ff7 100644 --- a/plugins/ipam/static/main.go +++ b/plugins/ipam/static/main.go @@ -18,6 +18,7 @@ import ( "encoding/json" "fmt" "net" + "strings" "github.com/containernetworking/cni/pkg/skel" "github.com/containernetworking/cni/pkg/types" @@ -39,10 +40,16 @@ 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 types.UnmarshallableString `json:"ip,omitempty"` + GATEWAY types.UnmarshallableString `json:"gateway,omitempty"` +} + type Address struct { AddressStr string `json:"address"` Gateway net.IP `json:"gateway,omitempty"` @@ -88,6 +95,7 @@ func LoadIPAMConfig(bytes []byte, envArgs string) (*IPAMConfig, string, error) { // 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 { @@ -109,6 +117,50 @@ 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 != "" { + for _, item := range strings.Split(string(e.IP), ",") { + ipstr := strings.TrimSpace(item) + + ip, subnet, err := net.ParseCIDR(ipstr) + if err != nil { + return nil, "", fmt.Errorf("invalid CIDR %s: %s", ipstr, err) + } + + addr := Address{Address: net.IPNet{IP: ip, Mask: subnet.Mask}} + if addr.Address.IP.To4() != nil { + addr.Version = "4" + numV4++ + } else { + addr.Version = "6" + numV6++ + } + n.IPAM.Addresses = append(n.IPAM.Addresses, addr) + } + } + + if e.GATEWAY != "" { + for _, item := range strings.Split(string(e.GATEWAY), ",") { + gwip := net.ParseIP(strings.TrimSpace(item)) + if gwip == nil { + return nil, "", fmt.Errorf("invalid gateway address: %s", item) + } + + for i := range n.IPAM.Addresses { + if n.IPAM.Addresses[i].Address.Contains(gwip) { + n.IPAM.Addresses[i].Gateway = gwip + } + } + } + } + } + // 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_suite_test.go b/plugins/ipam/static/static_suite_test.go index aae2fee43..c729ecf19 100644 --- a/plugins/ipam/static/static_suite_test.go +++ b/plugins/ipam/static/static_suite_test.go @@ -23,5 +23,5 @@ import ( func TestStatic(t *testing.T) { RegisterFailHandler(Fail) - RunSpecs(t, "Static Suite") + RunSpecs(t, "plugins/ipam/static") } diff --git a/plugins/ipam/static/static_test.go b/plugins/ipam/static/static_test.go index 31ac3c283..c6d9aea65 100644 --- a/plugins/ipam/static/static_test.go +++ b/plugins/ipam/static/static_test.go @@ -148,6 +148,123 @@ 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/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()) + }) + + It("allocates and releases multiple 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" + } + }` + + args := &skel.CmdArgs{ + ContainerID: "dummy", + Netns: nspath, + IfName: ifname, + StdinData: []byte(conf), + Args: "IP=10.10.0.1/24,11.11.0.1/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(*result.IPs[1]).To(Equal( + current.IPConfig{ + Version: "4", + Address: mustCIDR("11.11.0.1/24"), + Gateway: nil, + })) + + Expect(len(result.IPs)).To(Equal(2)) + + // Release the IP + err = testutils.CmdDelWithArgs(args, func() error { + return cmdDel(args) + }) + Expect(err).NotTo(HaveOccurred()) + }) }) func mustCIDR(s string) net.IPNet {