diff --git a/Documentation/host-local.md b/Documentation/host-local.md index ff6ee775..35aba355 100644 --- a/Documentation/host-local.md +++ b/Documentation/host-local.md @@ -31,6 +31,11 @@ It stores the state locally on the host filesystem, therefore ensuring uniquenes * `gateway` (string, optional): IP inside of "subnet" to designate as the gateway. Defaults to ".1" IP inside of the "subnet" block. * `routes` (string, optional): list of routes to 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. +## Supported arguments +The following [CNI_ARGS](https://github.com/appc/cni/blob/master/SPEC.md#parameters) are supported: + +* `ip`: request a specific IP address from the subnet. If it's not available, the plugin will exit with an error + ## Files Allocated IP addresses are stored as files in /var/lib/cni/networks/$NETWORK_NAME. diff --git a/pkg/plugin/args.go b/pkg/plugin/args.go new file mode 100644 index 00000000..84895793 --- /dev/null +++ b/pkg/plugin/args.go @@ -0,0 +1,36 @@ +package plugin + +import ( + "encoding" + "fmt" + "reflect" + "strings" +) + +func LoadArgs(args string, container interface{}) error { + if args == "" { + return nil + } + + containerValue := reflect.ValueOf(container) + + pairs := strings.Split(args, ",") + for _, pair := range pairs { + kv := strings.Split(pair, "=") + if len(kv) != 2 { + return fmt.Errorf("ARGS: invalid pair %q", pair) + } + keyString := kv[0] + valueString := kv[1] + keyField := containerValue.Elem().FieldByName(keyString) + if !keyField.IsValid() { + return fmt.Errorf("ARGS: invalid key %q", keyString) + } + u := keyField.Addr().Interface().(encoding.TextUnmarshaler) + err := u.UnmarshalText([]byte(valueString)) + if err != nil { + return fmt.Errorf("ARGS: error parsing value of pair %q: %v)", pair, err) + } + } + return nil +} diff --git a/plugins/ipam/host-local/allocator.go b/plugins/ipam/host-local/allocator.go index 0f3699d8..8d20344a 100644 --- a/plugins/ipam/host-local/allocator.go +++ b/plugins/ipam/host-local/allocator.go @@ -78,6 +78,40 @@ func (a *IPAllocator) Get(id string) (*plugin.IPConfig, error) { gw = ip.NextIP(a.conf.Subnet.IP) } + var requestedIP net.IP + if a.conf.Args != nil { + requestedIP = a.conf.Args.IP + } + + if requestedIP != nil { + if gw != nil && gw.Equal(a.conf.Args.IP) { + return nil, fmt.Errorf("requested IP must differ gateway IP") + } + + subnet := net.IPNet{ + IP: a.conf.Subnet.IP, + Mask: a.conf.Subnet.Mask, + } + err := validateRangeIP(requestedIP, &subnet) + if err != nil { + return nil, err + } + + reserved, err := a.store.Reserve(id, requestedIP) + if err != nil { + return nil, err + } + + if reserved { + return &plugin.IPConfig{ + IP: net.IPNet{requestedIP, a.conf.Subnet.Mask}, + 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) + } + for cur := a.start; !cur.Equal(a.end); cur = ip.NextIP(cur) { // don't allocate gateway IP if gw != nil && cur.Equal(gw) { @@ -96,7 +130,6 @@ func (a *IPAllocator) Get(id string) (*plugin.IPConfig, error) { }, nil } } - return nil, fmt.Errorf("no IP addresses available in network: %s", a.conf.Name) } diff --git a/plugins/ipam/host-local/config.go b/plugins/ipam/host-local/config.go index 1dd017ce..828b123d 100644 --- a/plugins/ipam/host-local/config.go +++ b/plugins/ipam/host-local/config.go @@ -32,6 +32,11 @@ type IPAMConfig struct { Subnet ip.IPNet `json:"subnet"` Gateway net.IP `json:"gateway"` Routes []plugin.Route `json:"routes"` + Args *IPAMArgs `json:"-"` +} + +type IPAMArgs struct { + IP net.IP `json:"ip",omitempty` } type Net struct { @@ -40,12 +45,20 @@ type Net struct { } // NewIPAMConfig creates a NetworkConfig from the given network name. -func LoadIPAMConfig(bytes []byte) (*IPAMConfig, error) { +func LoadIPAMConfig(bytes []byte, args string) (*IPAMConfig, error) { n := Net{} if err := json.Unmarshal(bytes, &n); err != nil { return nil, err } + if args != "" { + ipamArgs := IPAMArgs{} + err := plugin.LoadArgs(args, &ipamArgs) + if err != nil { + return nil, err + } + } + if n.IPAM == nil { return nil, fmt.Errorf("%q missing 'ipam' key") } diff --git a/plugins/ipam/host-local/main.go b/plugins/ipam/host-local/main.go index 799b114b..76b9e847 100644 --- a/plugins/ipam/host-local/main.go +++ b/plugins/ipam/host-local/main.go @@ -28,7 +28,7 @@ func main() { } func cmdAdd(args *skel.CmdArgs) error { - ipamConf, err := LoadIPAMConfig(args.StdinData) + ipamConf, err := LoadIPAMConfig(args.StdinData, args.Args) if err != nil { return err } @@ -39,6 +39,13 @@ func cmdAdd(args *skel.CmdArgs) error { } defer store.Close() + ipamArgs := IPAMArgs{} + err = plugin.LoadArgs(args.Args, &ipamArgs) + if err != nil { + return err + } + ipamConf.Args = &ipamArgs + allocator, err := NewIPAllocator(ipamConf, store) if err != nil { return err @@ -66,7 +73,7 @@ func cmdAdd(args *skel.CmdArgs) error { } func cmdDel(args *skel.CmdArgs) error { - ipamConf, err := LoadIPAMConfig(args.StdinData) + ipamConf, err := LoadIPAMConfig(args.StdinData, args.Args) if err != nil { return err }