Merge pull request #599 from mars1024/feat/hostlocal-ips
host-local: support custom IPs allocation through runtime configuraton
This commit is contained in:
commit
d917ab6b0c
105
pkg/ip/ip.go
Normal file
105
pkg/ip/ip.go
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
// Copyright 2021 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 ip
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IP is a CNI maintained type inherited from net.IPNet which can
|
||||||
|
// represent a single IP address with or without prefix.
|
||||||
|
type IP struct {
|
||||||
|
net.IPNet
|
||||||
|
}
|
||||||
|
|
||||||
|
// newIP will create an IP with net.IP and net.IPMask
|
||||||
|
func newIP(ip net.IP, mask net.IPMask) *IP {
|
||||||
|
return &IP{
|
||||||
|
IPNet: net.IPNet{
|
||||||
|
IP: ip,
|
||||||
|
Mask: mask,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseIP will parse string s as an IP, and return it.
|
||||||
|
// The string s must be formed like <ip>[/<prefix>].
|
||||||
|
// If s is not a valid textual representation of an IP,
|
||||||
|
// will return nil.
|
||||||
|
func ParseIP(s string) *IP {
|
||||||
|
if strings.ContainsAny(s, "/") {
|
||||||
|
ip, ipNet, err := net.ParseCIDR(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return newIP(ip, ipNet.Mask)
|
||||||
|
} else {
|
||||||
|
ip := net.ParseIP(s)
|
||||||
|
if ip == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return newIP(ip, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToIP will return a net.IP in standard form from this IP.
|
||||||
|
// If this IP can not be converted to a valid net.IP, will return nil.
|
||||||
|
func (i *IP) ToIP() net.IP {
|
||||||
|
switch {
|
||||||
|
case i.IP.To4() != nil:
|
||||||
|
return i.IP.To4()
|
||||||
|
case i.IP.To16() != nil:
|
||||||
|
return i.IP.To16()
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the string form of this IP.
|
||||||
|
func (i *IP) String() string {
|
||||||
|
if len(i.Mask) > 0 {
|
||||||
|
return i.IPNet.String()
|
||||||
|
}
|
||||||
|
return i.IP.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalText implements the encoding.TextMarshaler interface.
|
||||||
|
// The encoding is the same as returned by String,
|
||||||
|
// But when len(ip) is zero, will return an empty slice.
|
||||||
|
func (i *IP) MarshalText() ([]byte, error) {
|
||||||
|
if len(i.IP) == 0 {
|
||||||
|
return []byte{}, nil
|
||||||
|
}
|
||||||
|
return []byte(i.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalText implements the encoding.TextUnmarshaler interface.
|
||||||
|
// The textual bytes are expected in a form accepted by Parse,
|
||||||
|
// But when len(b) is zero, will return an empty IP.
|
||||||
|
func (i *IP) UnmarshalText(b []byte) error {
|
||||||
|
if len(b) == 0 {
|
||||||
|
*i = IP{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ip := ParseIP(string(b))
|
||||||
|
if ip == nil {
|
||||||
|
return fmt.Errorf("invalid IP address %s", string(b))
|
||||||
|
}
|
||||||
|
*i = *ip
|
||||||
|
return nil
|
||||||
|
}
|
272
pkg/ip/ip_test.go
Normal file
272
pkg/ip/ip_test.go
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
// Copyright 2021 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 ip
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
. "github.com/onsi/ginkgo"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = Describe("IP Operations", func() {
|
||||||
|
It("Parse", func() {
|
||||||
|
testCases := []struct {
|
||||||
|
ipStr string
|
||||||
|
expected *IP
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"192.168.0.10",
|
||||||
|
newIP(net.IPv4(192, 168, 0, 10), nil),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"2001:db8::1",
|
||||||
|
newIP(net.ParseIP("2001:db8::1"), nil),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"192.168.0.10/24",
|
||||||
|
newIP(net.IPv4(192, 168, 0, 10), net.IPv4Mask(255, 255, 255, 0)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"2001:db8::1/64",
|
||||||
|
newIP(net.ParseIP("2001:db8::1"), net.CIDRMask(64, 128)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"invalid",
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
ip := ParseIP(test.ipStr)
|
||||||
|
|
||||||
|
Expect(ip).To(Equal(test.expected))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
It("String", func() {
|
||||||
|
testCases := []struct {
|
||||||
|
ip *IP
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
newIP(net.IPv4(192, 168, 0, 1), net.IPv4Mask(255, 255, 255, 0)),
|
||||||
|
"192.168.0.1/24",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
newIP(net.IPv4(192, 168, 0, 2), nil),
|
||||||
|
"192.168.0.2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
newIP(net.ParseIP("2001:db8::1"), nil),
|
||||||
|
"2001:db8::1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
newIP(net.ParseIP("2001:db8::1"), net.CIDRMask(64, 128)),
|
||||||
|
"2001:db8::1/64",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
newIP(nil, nil),
|
||||||
|
"<nil>",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
Expect(test.ip.String()).To(Equal(test.expected))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
It("ToIP", func() {
|
||||||
|
testCases := []struct {
|
||||||
|
ip *IP
|
||||||
|
expectedLen int
|
||||||
|
expectedIP net.IP
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
newIP(net.IPv4(192, 168, 0, 1), net.IPv4Mask(255, 255, 255, 0)),
|
||||||
|
net.IPv4len,
|
||||||
|
net.IP{192, 168, 0, 1},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
newIP(net.IPv4(192, 168, 0, 2), nil),
|
||||||
|
net.IPv4len,
|
||||||
|
net.IP{192, 168, 0, 2},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
newIP(net.ParseIP("2001:db8::1"), nil),
|
||||||
|
net.IPv6len,
|
||||||
|
net.IP{32, 1, 13, 184, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
newIP(net.ParseIP("2001:db8::1"), net.CIDRMask(64, 128)),
|
||||||
|
net.IPv6len,
|
||||||
|
net.IP{32, 1, 13, 184, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
newIP(nil, nil),
|
||||||
|
0,
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
Expect(len(test.ip.ToIP())).To(Equal(test.expectedLen))
|
||||||
|
Expect(test.ip.ToIP()).To(Equal(test.expectedIP))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
It("Encode", func() {
|
||||||
|
testCases := []struct {
|
||||||
|
object interface{}
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
newIP(net.IPv4(192, 168, 0, 1), net.IPv4Mask(255, 255, 255, 0)),
|
||||||
|
`"192.168.0.1/24"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
newIP(net.IPv4(192, 168, 0, 2), nil),
|
||||||
|
`"192.168.0.2"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
newIP(net.ParseIP("2001:db8::1"), nil),
|
||||||
|
`"2001:db8::1"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
newIP(net.ParseIP("2001:db8::1"), net.CIDRMask(64, 128)),
|
||||||
|
`"2001:db8::1/64"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
newIP(nil, nil),
|
||||||
|
`""`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]*IP{
|
||||||
|
newIP(net.IPv4(192, 168, 0, 1), net.IPv4Mask(255, 255, 255, 0)),
|
||||||
|
newIP(net.IPv4(192, 168, 0, 2), nil),
|
||||||
|
newIP(net.ParseIP("2001:db8::1"), nil),
|
||||||
|
newIP(net.ParseIP("2001:db8::1"), net.CIDRMask(64, 128)),
|
||||||
|
newIP(nil, nil),
|
||||||
|
},
|
||||||
|
`["192.168.0.1/24","192.168.0.2","2001:db8::1","2001:db8::1/64",""]`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
bytes, err := json.Marshal(test.object)
|
||||||
|
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(string(bytes)).To(Equal(test.expected))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
It("Decode", func() {
|
||||||
|
Context("valid IP", func() {
|
||||||
|
testCases := []struct {
|
||||||
|
text string
|
||||||
|
expected *IP
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
`"192.168.0.1"`,
|
||||||
|
newIP(net.IPv4(192, 168, 0, 1), nil),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`"192.168.0.1/24"`,
|
||||||
|
newIP(net.IPv4(192, 168, 0, 1), net.IPv4Mask(255, 255, 255, 0)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`"2001:db8::1"`,
|
||||||
|
newIP(net.ParseIP("2001:db8::1"), nil),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`"2001:db8::1/64"`,
|
||||||
|
newIP(net.ParseIP("2001:db8::1"), net.CIDRMask(64, 128)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
ip := &IP{}
|
||||||
|
err := json.Unmarshal([]byte(test.text), ip)
|
||||||
|
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(ip).To(Equal(test.expected))
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
Context("empty text", func() {
|
||||||
|
ip := &IP{}
|
||||||
|
err := json.Unmarshal([]byte(`""`), ip)
|
||||||
|
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(ip).To(Equal(newIP(nil, nil)))
|
||||||
|
})
|
||||||
|
|
||||||
|
Context("invalid IP", func() {
|
||||||
|
testCases := []struct {
|
||||||
|
text string
|
||||||
|
expectedErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
`"192.168.0.1000"`,
|
||||||
|
fmt.Errorf("invalid IP address 192.168.0.1000"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`"2001:db8::1/256"`,
|
||||||
|
fmt.Errorf("invalid IP address 2001:db8::1/256"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`"test"`,
|
||||||
|
fmt.Errorf("invalid IP address test"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
err := json.Unmarshal([]byte(test.text), &IP{})
|
||||||
|
|
||||||
|
Expect(err).To(HaveOccurred())
|
||||||
|
Expect(err).To(Equal(test.expectedErr))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Context("IP slice", func() {
|
||||||
|
testCases := []struct {
|
||||||
|
text string
|
||||||
|
expected []*IP
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
`["192.168.0.1/24","192.168.0.2","2001:db8::1","2001:db8::1/64",""]`,
|
||||||
|
[]*IP{
|
||||||
|
newIP(net.IPv4(192, 168, 0, 1), net.IPv4Mask(255, 255, 255, 0)),
|
||||||
|
newIP(net.IPv4(192, 168, 0, 2), nil),
|
||||||
|
newIP(net.ParseIP("2001:db8::1"), nil),
|
||||||
|
newIP(net.ParseIP("2001:db8::1"), net.CIDRMask(64, 128)),
|
||||||
|
newIP(nil, nil),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
ips := make([]*IP, 0)
|
||||||
|
err := json.Unmarshal([]byte(test.text), &ips)
|
||||||
|
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(ips).To(Equal(test.expected))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
@ -21,6 +21,8 @@ import (
|
|||||||
|
|
||||||
"github.com/containernetworking/cni/pkg/types"
|
"github.com/containernetworking/cni/pkg/types"
|
||||||
"github.com/containernetworking/cni/pkg/version"
|
"github.com/containernetworking/cni/pkg/version"
|
||||||
|
|
||||||
|
"github.com/containernetworking/plugins/pkg/ip"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The top-level network config - IPAM plugins are passed the full configuration
|
// The top-level network config - IPAM plugins are passed the full configuration
|
||||||
@ -29,8 +31,10 @@ type Net struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
CNIVersion string `json:"cniVersion"`
|
CNIVersion string `json:"cniVersion"`
|
||||||
IPAM *IPAMConfig `json:"ipam"`
|
IPAM *IPAMConfig `json:"ipam"`
|
||||||
RuntimeConfig struct { // The capability arg
|
RuntimeConfig struct {
|
||||||
|
// The capability arg
|
||||||
IPRanges []RangeSet `json:"ipRanges,omitempty"`
|
IPRanges []RangeSet `json:"ipRanges,omitempty"`
|
||||||
|
IPs []*ip.IP `json:"ips,omitempty"`
|
||||||
} `json:"runtimeConfig,omitempty"`
|
} `json:"runtimeConfig,omitempty"`
|
||||||
Args *struct {
|
Args *struct {
|
||||||
A *IPAMArgs `json:"cni"`
|
A *IPAMArgs `json:"cni"`
|
||||||
@ -48,7 +52,7 @@ type IPAMConfig struct {
|
|||||||
DataDir string `json:"dataDir"`
|
DataDir string `json:"dataDir"`
|
||||||
ResolvConf string `json:"resolvConf"`
|
ResolvConf string `json:"resolvConf"`
|
||||||
Ranges []RangeSet `json:"ranges"`
|
Ranges []RangeSet `json:"ranges"`
|
||||||
IPArgs []net.IP `json:"-"` // Requested IPs from CNI_ARGS and args
|
IPArgs []net.IP `json:"-"` // Requested IPs from CNI_ARGS, args and capabilities
|
||||||
}
|
}
|
||||||
|
|
||||||
type IPAMEnvArgs struct {
|
type IPAMEnvArgs struct {
|
||||||
@ -80,7 +84,8 @@ func LoadIPAMConfig(bytes []byte, envArgs string) (*IPAMConfig, string, error) {
|
|||||||
return nil, "", fmt.Errorf("IPAM config missing 'ipam' key")
|
return nil, "", fmt.Errorf("IPAM config missing 'ipam' key")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse custom IP from both env args *and* the top-level args config
|
// Parse custom IP from env args, the top-level args config and capabilities
|
||||||
|
// in runtime configuration
|
||||||
if envArgs != "" {
|
if envArgs != "" {
|
||||||
e := IPAMEnvArgs{}
|
e := IPAMEnvArgs{}
|
||||||
err := types.LoadArgs(envArgs, &e)
|
err := types.LoadArgs(envArgs, &e)
|
||||||
@ -97,6 +102,12 @@ func LoadIPAMConfig(bytes []byte, envArgs string) (*IPAMConfig, string, error) {
|
|||||||
n.IPAM.IPArgs = append(n.IPAM.IPArgs, n.Args.A.IPs...)
|
n.IPAM.IPArgs = append(n.IPAM.IPArgs, n.Args.A.IPs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(n.RuntimeConfig.IPs) > 0 {
|
||||||
|
for _, i := range n.RuntimeConfig.IPs {
|
||||||
|
n.IPAM.IPArgs = append(n.IPAM.IPArgs, i.ToIP())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for idx := range n.IPAM.IPArgs {
|
for idx := range n.IPAM.IPArgs {
|
||||||
if err := canonicalizeIP(&n.IPAM.IPArgs[idx]); err != nil {
|
if err := canonicalizeIP(&n.IPAM.IPArgs[idx]); err != nil {
|
||||||
return nil, "", fmt.Errorf("cannot understand ip: %v", err)
|
return nil, "", fmt.Errorf("cannot understand ip: %v", err)
|
||||||
|
@ -379,4 +379,29 @@ var _ = Describe("IPAM config", func() {
|
|||||||
_, _, err := LoadIPAMConfig([]byte(input), "")
|
_, _, err := LoadIPAMConfig([]byte(input), "")
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
It("Should parse custom IPs from runtime configuration", func() {
|
||||||
|
input := `{
|
||||||
|
"cniVersion": "0.3.1",
|
||||||
|
"name": "mynet",
|
||||||
|
"type": "ipvlan",
|
||||||
|
"master": "foo0",
|
||||||
|
"runtimeConfig": {
|
||||||
|
"ips": ["192.168.0.1", "192.168.0.5/24", "2001:db8::1/64"]
|
||||||
|
},
|
||||||
|
"ipam": {
|
||||||
|
"type": "host-local",
|
||||||
|
"subnet": "10.1.2.0/24"
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
conf, version, err := LoadIPAMConfig([]byte(input), "")
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(version).Should(Equal("0.3.1"))
|
||||||
|
|
||||||
|
Expect(conf.IPArgs).To(Equal([]net.IP{
|
||||||
|
net.IPv4(192, 168, 0, 1).To4(),
|
||||||
|
net.IPv4(192, 168, 0, 5).To4(),
|
||||||
|
net.ParseIP("2001:db8::1"),
|
||||||
|
}))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
Loading…
x
Reference in New Issue
Block a user