mirror of
https://github.com/moby/moby.git
synced 2026-08-03 14:41:03 +00:00
256 lines
8.0 KiB
Go
256 lines
8.0 KiB
Go
package remote
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
|
|
"github.com/containerd/log"
|
|
"github.com/moby/moby/v2/daemon/libnetwork/ipamapi"
|
|
"github.com/moby/moby/v2/daemon/libnetwork/ipams/remote/api"
|
|
"github.com/moby/moby/v2/daemon/libnetwork/types"
|
|
"github.com/moby/moby/v2/pkg/plugingetter"
|
|
"github.com/moby/moby/v2/pkg/plugins"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
type allocator struct {
|
|
endpoint *plugins.Client
|
|
name string
|
|
}
|
|
|
|
// PluginResponse is the interface for the plugin request responses
|
|
type PluginResponse interface {
|
|
IsSuccess() bool
|
|
GetError() string
|
|
}
|
|
|
|
func newAllocator(name string, client *plugins.Client) ipamapi.Ipam {
|
|
a := &allocator{name: name, endpoint: client}
|
|
return a
|
|
}
|
|
|
|
// Register registers a remote ipam when its plugin is activated.
|
|
func Register(cb ipamapi.Registerer, pg plugingetter.PluginGetter) error {
|
|
newPluginHandler := func(name string, client *plugins.Client) {
|
|
a := newAllocator(name, client)
|
|
if cps, err := a.(*allocator).getCapabilities(); err == nil {
|
|
if err := cb.RegisterIpamDriverWithCapabilities(name, a, cps); err != nil {
|
|
log.G(context.TODO()).Errorf("error registering remote ipam driver %s due to %v", name, err)
|
|
}
|
|
} else {
|
|
log.G(context.TODO()).Infof("remote ipam driver %s does not support capabilities", name)
|
|
log.G(context.TODO()).Debug(err)
|
|
if err := cb.RegisterIpamDriver(name, a); err != nil {
|
|
log.G(context.TODO()).Errorf("error registering remote ipam driver %s due to %v", name, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Unit test code is unaware of a true PluginStore. So we fall back to v1 plugins.
|
|
handleFunc := plugins.Handle
|
|
if pg != nil {
|
|
handleFunc = pg.Handle
|
|
activePlugins := pg.GetAllManagedPluginsByCap(ipamapi.PluginEndpointType)
|
|
for _, ap := range activePlugins {
|
|
client, err := getPluginClient(ap)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
newPluginHandler(ap.Name(), client)
|
|
}
|
|
}
|
|
handleFunc(ipamapi.PluginEndpointType, newPluginHandler)
|
|
return nil
|
|
}
|
|
|
|
func getPluginClient(p plugingetter.CompatPlugin) (*plugins.Client, error) {
|
|
if v1, ok := p.(plugingetter.PluginWithV1Client); ok {
|
|
return v1.Client(), nil
|
|
}
|
|
|
|
pa, ok := p.(plugingetter.PluginAddr)
|
|
if !ok {
|
|
return nil, errors.Errorf("unknown plugin type %T", p)
|
|
}
|
|
|
|
if pa.Protocol() != plugins.ProtocolSchemeHTTPV1 {
|
|
return nil, errors.Errorf("unsupported plugin protocol %s", pa.Protocol())
|
|
}
|
|
|
|
addr := pa.Addr()
|
|
client, err := plugins.NewClientWithTimeout(addr.Network()+"://"+addr.String(), nil, pa.Timeout())
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "error creating plugin client")
|
|
}
|
|
return client, nil
|
|
}
|
|
|
|
func (a *allocator) call(methodName string, arg any, retVal PluginResponse) error {
|
|
method := ipamapi.PluginEndpointType + "." + methodName
|
|
err := a.endpoint.Call(method, arg, retVal)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !retVal.IsSuccess() {
|
|
return fmt.Errorf("remote: %s", retVal.GetError())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a *allocator) getCapabilities() (*ipamapi.Capability, error) {
|
|
var res api.GetCapabilityResponse
|
|
if err := a.call("GetCapabilities", nil, &res); err != nil {
|
|
return nil, err
|
|
}
|
|
return res.ToCapability(), nil
|
|
}
|
|
|
|
// GetDefaultAddressSpaces returns the local and global default address spaces
|
|
func (a *allocator) GetDefaultAddressSpaces() (string, string, error) {
|
|
res := &api.GetAddressSpacesResponse{}
|
|
if err := a.call("GetDefaultAddressSpaces", nil, res); err != nil {
|
|
return "", "", err
|
|
}
|
|
return res.LocalDefaultAddressSpace, res.GlobalDefaultAddressSpace, nil
|
|
}
|
|
|
|
// RequestPool requests an address pool in the specified address space.
|
|
//
|
|
// This is a bug-for-bug re-implementation of the logic originally found in
|
|
// requestPoolHelper prior to v27. See https://github.com/moby/moby/blob/faf84d7f0a1f2e6badff6f720a3e1e559c356fff/libnetwork/network.go#L1518-L1570
|
|
func (a *allocator) RequestPool(req ipamapi.PoolRequest) (ipamapi.AllocatedPool, error) {
|
|
var tmpPoolLeases []string
|
|
defer func() {
|
|
// Release all pools we held on to.
|
|
for _, pID := range tmpPoolLeases {
|
|
if err := a.ReleasePool(pID); err != nil {
|
|
log.G(context.TODO()).Warnf("Failed to release overlapping pool")
|
|
}
|
|
}
|
|
}()
|
|
|
|
_, globalSpace, err := a.GetDefaultAddressSpaces()
|
|
if err != nil {
|
|
return ipamapi.AllocatedPool{}, err
|
|
}
|
|
|
|
remoteReq := &api.RequestPoolRequest{
|
|
AddressSpace: req.AddressSpace,
|
|
Pool: req.Pool,
|
|
SubPool: req.SubPool,
|
|
Options: req.Options,
|
|
V6: req.V6,
|
|
}
|
|
|
|
for {
|
|
alloc, err := a.requestPool(remoteReq)
|
|
if err != nil {
|
|
return alloc, err
|
|
}
|
|
|
|
// If the network pool was explicitly chosen, the network belongs to
|
|
// global address space, or it is invalid ("0.0.0.0/0"), then we don't
|
|
// perform check for overlaps.
|
|
//
|
|
// FIXME(thaJeztah): why are we ignoring invalid pools here?
|
|
//
|
|
// The "invalid" conditions was added in [libnetwork#1095][1], which
|
|
// moved code to reduce os-specific dependencies in the ipam package,
|
|
// but also introduced a types.IsIPNetValid() function, which considers
|
|
// "0.0.0.0/0" invalid, and added it to the conditions below.
|
|
//
|
|
// Unfortunately review does not mention this change, so there's no
|
|
// context why. Possibly this was done to prevent errors further down
|
|
// the line (when checking for overlaps), but returning an error here
|
|
// instead would likely have avoided that as well, so we can only guess.
|
|
//
|
|
// [1]: https://github.com/moby/libnetwork/commit/5ca79d6b87873264516323a7b76f0af7d0298492#diff-bdcd879439d041827d334846f9aba01de6e3683ed8fdd01e63917dae6df23846
|
|
if req.Pool != "" || req.AddressSpace == globalSpace || alloc.Pool.String() == "0.0.0.0/0" {
|
|
return alloc, nil
|
|
}
|
|
|
|
// Check for overlap and if none found, we have found the right pool.
|
|
if !checkOverlaps(alloc, req.Exclude) {
|
|
return alloc, nil
|
|
}
|
|
|
|
// Pool obtained in this iteration is overlapping. Hold onto the pool
|
|
// and don't release it yet, because we don't want IPAM to give us back
|
|
// the same pool over again. But make sure we still do a deferred release
|
|
// when we have either obtained a non-overlapping pool or ran out of
|
|
// pre-defined pools.
|
|
tmpPoolLeases = append(tmpPoolLeases, alloc.PoolID)
|
|
}
|
|
}
|
|
|
|
func (a *allocator) requestPool(req *api.RequestPoolRequest) (ipamapi.AllocatedPool, error) {
|
|
res := &api.RequestPoolResponse{}
|
|
if err := a.call("RequestPool", req, res); err != nil {
|
|
return ipamapi.AllocatedPool{}, err
|
|
}
|
|
|
|
retPool, err := netip.ParsePrefix(res.Pool)
|
|
return ipamapi.AllocatedPool{
|
|
PoolID: res.PoolID,
|
|
Pool: retPool,
|
|
Meta: res.Data,
|
|
}, err
|
|
}
|
|
|
|
// checkOverlaps returns true if the 'pool' overlaps with some prefix in 'reserved'.
|
|
func checkOverlaps(pool ipamapi.AllocatedPool, reserved []netip.Prefix) bool {
|
|
for _, r := range reserved {
|
|
if r.Overlaps(pool.Pool) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// ReleasePool removes an address pool from the specified address space
|
|
func (a *allocator) ReleasePool(poolID string) error {
|
|
req := &api.ReleasePoolRequest{PoolID: poolID}
|
|
res := &api.ReleasePoolResponse{}
|
|
return a.call("ReleasePool", req, res)
|
|
}
|
|
|
|
// RequestAddress requests an address from the address pool
|
|
func (a *allocator) RequestAddress(poolID string, address net.IP, options map[string]string) (*net.IPNet, map[string]string, error) {
|
|
var (
|
|
prefAddress string
|
|
retAddress *net.IPNet
|
|
err error
|
|
)
|
|
if address != nil {
|
|
prefAddress = address.String()
|
|
}
|
|
req := &api.RequestAddressRequest{PoolID: poolID, Address: prefAddress, Options: options}
|
|
res := &api.RequestAddressResponse{}
|
|
if err := a.call("RequestAddress", req, res); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if res.Address != "" {
|
|
retAddress, err = types.ParseCIDR(res.Address)
|
|
} else {
|
|
return nil, nil, ipamapi.ErrNoIPReturned
|
|
}
|
|
return retAddress, res.Data, err
|
|
}
|
|
|
|
// ReleaseAddress releases the address from the specified address pool
|
|
func (a *allocator) ReleaseAddress(poolID string, address net.IP) error {
|
|
var relAddress string
|
|
if address != nil {
|
|
relAddress = address.String()
|
|
}
|
|
req := &api.ReleaseAddressRequest{PoolID: poolID, Address: relAddress}
|
|
res := &api.ReleaseAddressResponse{}
|
|
return a.call("ReleaseAddress", req, res)
|
|
}
|
|
|
|
func (a *allocator) IsBuiltIn() bool {
|
|
return false
|
|
}
|