Implement `CreateNetwork` and `DeleteNetwork`

pull/3/head
Jack O'Sullivan 4 years ago
parent bd8e73f41a
commit f8a70d5222

@ -1,6 +1,7 @@
PLUGIN_NAME = devplayer0/net-dhcp
PLUGIN_TAG ?= golang
SOURCES = $(shell find pkg/ pkg/ -name '*.go')
BINARY = bin/net-dhcp
PLUGIN_DIR = plugin
@ -8,7 +9,7 @@ PLUGIN_DIR = plugin
all: create enable
$(BINARY): cmd/net-dhcp/main.go
$(BINARY): $(SOURCES)
CGO_ENABLED=0 go build -o $@ ./cmd/net-dhcp
debug: $(BINARY)

@ -29,26 +29,30 @@ func main() {
f, err := os.OpenFile(*logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
log.WithError(err).Fatal("Failed to open log file for writing")
return
}
defer f.Close()
log.StandardLogger().Out = f
}
p := plugin.NewPlugin()
p, err := plugin.NewPlugin()
if err != nil {
log.WithError(err).Fatal("Failed to create plugin")
}
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, unix.SIGINT, unix.SIGTERM)
go func() {
log.Info("Starting server...")
if err := p.Start(*bindSock); err != nil {
log.WithError(err).Fatal("Failed to start server")
if err := p.Listen(*bindSock); err != nil {
log.WithError(err).Fatal("Failed to start plugin")
}
}()
<-sigs
log.Info("Shutting down...")
p.Stop()
if err := p.Close(); err != nil {
log.WithError(err).Fatal("Failed to stop plugin")
}
}

@ -3,6 +3,14 @@ module github.com/devplayer0/docker-net-dhcp
go 1.14
require (
github.com/docker/distribution v2.7.1+incompatible // indirect
github.com/docker/docker v1.13.1
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/sirupsen/logrus v1.6.0
github.com/vishvananda/netlink v1.1.0
golang.org/x/net v0.0.0-20200513185701-a91f0712d120 // indirect
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9
)

@ -1,12 +1,35 @@
github.com/containous/traefik v1.7.24 h1:iFkoJBpQUQh1URdblBjbh32Wav8Ctl/WjLtAtvBzHis=
github.com/containous/traefik v1.7.24/go.mod h1:epDRqge3JzKOhlSWzOpNYEEKXmM6yfN5tPzDGKk3ljo=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v1.13.1 h1:IkZjBSIc8hBjLpqeAbeE5mca5mNgeatLHBy3GO78BWo=
github.com/docker/docker v1.13.1/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0=
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df h1:OviZH7qLw/7ZovXvuNyL3XQl8UFofeikI1NW1Gypu7k=
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120 h1:EZ3cVSzKOlJxAd8e8YAJ7no8nNypTxexh/YE/xW3ZEY=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9 h1:YTzHMGlqJu67/uEo1lBv0n3wBXhXNeUbB1XfN2vmTm0=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

@ -2,6 +2,7 @@ package plugin
import (
"encoding/json"
"errors"
"fmt"
"net/http"
@ -11,7 +12,6 @@ import (
// ParseJSONBody attempts to parse the request body as JSON
func ParseJSONBody(v interface{}, w http.ResponseWriter, r *http.Request) error {
d := json.NewDecoder(r.Body)
d.DisallowUnknownFields()
if err := d.Decode(v); err != nil {
JSONErrResponse(w, fmt.Errorf("failed to parse request body: %w", err), http.StatusBadRequest)
return err
@ -25,8 +25,7 @@ func JSONResponse(w http.ResponseWriter, v interface{}, statusCode int) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
enc := json.NewEncoder(w)
if err := enc.Encode(v); err != nil {
if err := json.NewEncoder(w).Encode(v); err != nil {
log.WithField("err", err).Error("Failed to serialize JSON payload")
w.WriteHeader(http.StatusInternalServerError)
@ -34,8 +33,9 @@ func JSONResponse(w http.ResponseWriter, v interface{}, statusCode int) {
}
}
type jsonError struct {
Message string `json:"message"`
// ErrorResponse is a formatted error message that libnetwork can understand
type ErrorResponse struct {
Err string
}
// JSONErrResponse Sends an `error` as a JSON object with a `message` property
@ -43,8 +43,16 @@ func JSONErrResponse(w http.ResponseWriter, err error, statusCode int) {
w.Header().Set("Content-Type", "application/problem+json")
w.WriteHeader(statusCode)
enc := json.NewEncoder(w)
enc.Encode(jsonError{err.Error()})
json.NewEncoder(w).Encode(ErrorResponse{err.Error()})
}
func errToStatus(err error) int {
switch {
case errors.Is(err, ErrIPAM), errors.Is(err, ErrBridge):
return http.StatusBadRequest
default:
return http.StatusInternalServerError
}
}
// CapabilitiesResponse returns whether or not this network is global or local
@ -53,9 +61,69 @@ type CapabilitiesResponse struct {
ConnectivityScope string
}
func apiGetCapabilities(w http.ResponseWriter, r *http.Request) {
func (p *Plugin) apiGetCapabilities(w http.ResponseWriter, r *http.Request) {
JSONResponse(w, CapabilitiesResponse{
Scope: "local",
ConnectivityScope: "global",
}, http.StatusOK)
}
// IPAMData contains IPv4 or IPv6 addressing information
type IPAMData struct {
AddressSpace string
Pool string
Gateway string
AuxAddresses map[string]interface{}
}
// CreateNetworkGenericOptions contains options for the DHCP network driver
type CreateNetworkGenericOptions struct {
Bridge string
IPv6 bool
}
// CreateNetworkOptions contains a map of options for the network driver
type CreateNetworkOptions struct {
Generic CreateNetworkGenericOptions `json:"com.docker.network.generic"`
}
// CreateNetworkRequest is sent by the daemon when a network needs to be created
type CreateNetworkRequest struct {
NetworkID string
Options CreateNetworkOptions
IPv4Data []*IPAMData
IPv6Data []*IPAMData
}
func (p *Plugin) apiCreateNetwork(w http.ResponseWriter, r *http.Request) {
var req CreateNetworkRequest
if err := ParseJSONBody(&req, w, r); err != nil {
return
}
if err := p.CreateNetwork(req); err != nil {
JSONErrResponse(w, err, errToStatus(err))
return
}
JSONResponse(w, struct{}{}, http.StatusOK)
}
// DeleteNetworkRequest is sent by the daemon when a network needs to be removed
type DeleteNetworkRequest struct {
NetworkID string
}
func (p *Plugin) apiDeleteNetwork(w http.ResponseWriter, r *http.Request) {
var req DeleteNetworkRequest
if err := ParseJSONBody(&req, w, r); err != nil {
return
}
if err := p.DeleteNetwork(); err != nil {
JSONErrResponse(w, err, errToStatus(err))
return
}
JSONResponse(w, struct{}{}, http.StatusOK)
}

@ -0,0 +1,96 @@
package plugin
import (
"context"
"errors"
"fmt"
"net"
log "github.com/sirupsen/logrus"
"github.com/vishvananda/netlink"
"golang.org/x/sys/unix"
dTypes "github.com/docker/docker/api/types"
)
// CLIOptionsKey is the key used in create network options by the CLI for custom options
const CLIOptionsKey string = "com.docker.network.generic"
var (
// ErrIPAM indicates an unsupported IPAM driver was used
ErrIPAM = errors.New("only the null IPAM driver is supported")
// ErrBridge indicates that a bridge is unavailable for use
ErrBridge = errors.New("bridge not found or already in use by Docker")
)
// CreateNetwork "creates" a new DHCP network (just checks if the provided bridge exists and the null IPAM driver is
// used)
func (p *Plugin) CreateNetwork(r CreateNetworkRequest) error {
for _, d := range r.IPv4Data {
if d.AddressSpace != "null" || d.Pool != "0.0.0.0/0" {
return ErrIPAM
}
}
links, err := netlink.LinkList()
if err != nil {
return fmt.Errorf("failed to retrieve list of network interfaces: %w", err)
}
nets, err := p.docker.NetworkList(context.Background(), dTypes.NetworkListOptions{})
if err != nil {
return fmt.Errorf("failed to retrieve list of networks from Docker: %w", err)
}
found := false
for _, l := range links {
attrs := l.Attrs()
if l.Type() != "bridge" || attrs.Name != r.Options.Generic.Bridge {
continue
}
v4Addrs, err := netlink.AddrList(l, unix.AF_INET)
if err != nil {
return fmt.Errorf("failed to retrieve IPv4 addresses for %v: %w", attrs.Name, err)
}
v6Addrs, err := netlink.AddrList(l, unix.AF_INET6)
if err != nil {
return fmt.Errorf("failed to retrieve IPv6 addresses for %v: %w", attrs.Name, err)
}
addrs := append(v4Addrs, v6Addrs...)
// Make sure the addresses on this bridge aren't used by another network
for _, n := range nets {
for _, c := range n.IPAM.Config {
_, cidr, err := net.ParseCIDR(c.Subnet)
if err != nil {
return fmt.Errorf("failed to parse subnet %v on Docker network %v: %w", c.Subnet, n.ID, err)
}
for _, linkAddr := range addrs {
if linkAddr.IPNet.Contains(cidr.IP) || cidr.Contains(linkAddr.IP) {
return ErrBridge
}
}
}
}
found = true
break
}
if !found {
return ErrBridge
}
log.WithFields(log.Fields{
"network": r.NetworkID,
"bridge": r.Options.Generic.Bridge,
"ipv6": r.Options.Generic.IPv6,
}).Info("Creating network")
return nil
}
// DeleteNetwork "deletes" a DHCP network (does nothing, the bridge is managed by the user)
func (p *Plugin) DeleteNetwork() error {
return nil
}

@ -1,31 +1,47 @@
package plugin
import (
"fmt"
"net"
"net/http"
docker "github.com/docker/docker/client"
)
// DriverName is the name of the Docker Network Driver
const DriverName string = "net-dhcp"
// Plugin is the DHCP network plugin
type Plugin struct {
docker *docker.Client
server http.Server
}
// NewPlugin creates a new Plugin
func NewPlugin() *Plugin {
p := Plugin{}
func NewPlugin() (*Plugin, error) {
client, err := docker.NewClient("unix:///run/docker.sock", "v1.13.1", nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to create docker client: %w", err)
}
p := Plugin{
docker: client,
}
mux := http.NewServeMux()
mux.HandleFunc("/NetworkDriver.GetCapabilities", apiGetCapabilities)
mux.HandleFunc("/NetworkDriver.GetCapabilities", p.apiGetCapabilities)
mux.HandleFunc("/NetworkDriver.CreateNetwork", p.apiCreateNetwork)
mux.HandleFunc("/NetworkDriver.DeleteNetwork", p.apiDeleteNetwork)
p.server = http.Server{
Handler: mux,
}
return &p
return &p, nil
}
// Start starts the plugin server
func (p *Plugin) Start(bindSock string) error {
// Listen starts the plugin server
func (p *Plugin) Listen(bindSock string) error {
l, err := net.Listen("unix", bindSock)
if err != nil {
return err
@ -34,7 +50,15 @@ func (p *Plugin) Start(bindSock string) error {
return p.server.Serve(l)
}
// Stop stops the plugin server
func (p *Plugin) Stop() error {
return p.server.Close()
// Close stops the plugin server
func (p *Plugin) Close() error {
if err := p.docker.Close(); err != nil {
return fmt.Errorf("failed to close docker client: %w", err)
}
if err := p.server.Close(); err != nil {
return fmt.Errorf("failed to close http server: %w", err)
}
return nil
}

Loading…
Cancel
Save