├── sdk ├── unix_listener_unsupported.go ├── unix_listener_nosystemd.go ├── windows_pipe_config.go ├── windows_listener_unsupported.go ├── pool.go ├── unix_listener.go ├── tcp_listener.go ├── encoder.go ├── unix_listener_systemd.go ├── spec_file_generator.go ├── windows_listener.go └── handler.go ├── .gitignore ├── .github └── workflows │ └── ci.yaml ├── NOTICE ├── Makefile ├── ipam ├── README.md └── api.go ├── secrets ├── README.md ├── api_test.go └── api.go ├── README.md ├── volume ├── README.md ├── shim │ ├── shim_test.go │ └── shim.go ├── api_test.go └── api.go ├── authorization ├── README.md ├── api.go └── api_test.go ├── MAINTAINERS ├── network ├── README.md ├── api_test.go └── api.go ├── CONTRIBUTING.md └── LICENSE /sdk/unix_listener_unsupported.go: -------------------------------------------------------------------------------- 1 | //go:build !linux && !freebsd 2 | 3 | package sdk 4 | 5 | import ( 6 | "errors" 7 | "net" 8 | ) 9 | 10 | func newUnixListener(pluginName string, gid int) (net.Listener, string, error) { 11 | return nil, "", errors.New("unix socket creation is only supported on Linux and FreeBSD") 12 | } 13 | -------------------------------------------------------------------------------- /sdk/unix_listener_nosystemd.go: -------------------------------------------------------------------------------- 1 | //go:build (linux || freebsd) && nosystemd 2 | 3 | package sdk 4 | 5 | import "net" 6 | 7 | // FIXME(thaJeztah): this code was added in https://github.com/docker/go-plugins-helpers/commit/008703b825c10311af1840deeaf5f4769df7b59e, but is not used anywhere 8 | func setupSocketActivation() (net.Listener, error) { 9 | return nil, nil 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /sdk/windows_pipe_config.go: -------------------------------------------------------------------------------- 1 | package sdk 2 | 3 | // WindowsPipeConfig is a helper structure for configuring named pipe parameters on Windows. 4 | type WindowsPipeConfig struct { 5 | // SecurityDescriptor contains a Windows security descriptor in SDDL format. 6 | SecurityDescriptor string 7 | 8 | // InBufferSize in bytes. 9 | InBufferSize int32 10 | 11 | // OutBufferSize in bytes. 12 | OutBufferSize int32 13 | } 14 | -------------------------------------------------------------------------------- /sdk/windows_listener_unsupported.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package sdk 4 | 5 | import ( 6 | "errors" 7 | "net" 8 | ) 9 | 10 | func newWindowsListener(address, pluginName, daemonRoot string, pipeConfig *WindowsPipeConfig) (net.Listener, string, error) { 11 | return nil, "", errors.New("named pipe creation is only supported on Windows") 12 | } 13 | 14 | func windowsCreateDirectoryWithACL(name string) error { 15 | return nil 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - master 7 | pull_request: 8 | branches: 9 | - main 10 | - master 11 | jobs: 12 | test: 13 | strategy: 14 | matrix: 15 | platform: [ubuntu-20.04] 16 | runs-on: ${{ matrix.platform }} 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v4 20 | - name: test 21 | run: make test 22 | -------------------------------------------------------------------------------- /sdk/pool.go: -------------------------------------------------------------------------------- 1 | package sdk 2 | 3 | import ( 4 | "io" 5 | "sync" 6 | ) 7 | 8 | const buffer32K = 32 * 1024 9 | 10 | var buffer32KPool = &sync.Pool{New: func() interface{} { return make([]byte, buffer32K) }} 11 | 12 | // copyBuf uses a shared buffer pool with io.CopyBuffer 13 | func copyBuf(w io.Writer, r io.Reader) (int64, error) { 14 | buf := buffer32KPool.Get().([]byte) 15 | written, err := io.CopyBuffer(w, r, buf) 16 | buffer32KPool.Put(buf) 17 | return written, err 18 | } 19 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Docker 2 | Copyright 2012-2015 Docker, Inc. 3 | 4 | This product includes software developed at Docker, Inc. (https://www.docker.com). 5 | 6 | This product contains software (https://github.com/kr/pty) developed 7 | by Keith Rarick, licensed under the MIT License. 8 | 9 | The following is courtesy of our legal counsel: 10 | 11 | 12 | Use and transfer of Docker may be subject to certain restrictions by the 13 | United States and other governments. 14 | It is your responsibility to ensure that your use and/or transfer does not 15 | violate applicable laws. 16 | 17 | For more information, please see https://www.bis.doc.gov 18 | 19 | See also https://www.apache.org/dev/crypto.html and/or seek legal counsel. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all test test-local install-deps lint fmt vet 2 | 3 | REPO_NAME = go-plugins-helpers 4 | REPO_OWNER = docker 5 | PKG_NAME = github.com/${REPO_OWNER}/${REPO_NAME} 6 | IMAGE = golang:1.21 7 | 8 | all: test 9 | 10 | test-local: install-deps fmt lint vet 11 | @echo "+ $@" 12 | @go test -v ./... 13 | 14 | test: 15 | @docker run -e GO111MODULE=off -v ${shell pwd}:/go/src/${PKG_NAME} -w /go/src/${PKG_NAME} ${IMAGE} make test-local 16 | 17 | install-deps: 18 | @echo "+ $@" 19 | @go get -u golang.org/x/lint/golint 20 | @go get -d -t ./... 21 | 22 | lint: 23 | @echo "+ $@" 24 | @test -z "$$(golint ./... | tee /dev/stderr)" 25 | 26 | fmt: 27 | @echo "+ $@" 28 | @test -z "$$(gofmt -s -l . | tee /dev/stderr)" 29 | 30 | vet: 31 | @echo "+ $@" 32 | @go vet ./... 33 | 34 | -------------------------------------------------------------------------------- /ipam/README.md: -------------------------------------------------------------------------------- 1 | # Docker IPAM extension API 2 | 3 | Go handler to create external IPAM extensions for Docker. 4 | 5 | ## Usage 6 | 7 | This library is designed to be integrated in your program. 8 | 9 | 1. Implement the `ipam.Driver` interface. 10 | 2. Initialize a `ipam.Handler` with your implementation. 11 | 3. Call either `ServeTCP` or `ServeUnix` from the `ipam.Handler`. 12 | 13 | ### Example using TCP sockets: 14 | 15 | ```go 16 | import "github.com/docker/go-plugins-helpers/ipam" 17 | 18 | d := MyIPAMDriver{} 19 | h := ipam.NewHandler(d) 20 | h.ServeTCP("test_ipam", ":8080") 21 | ``` 22 | 23 | ### Example using Unix sockets: 24 | 25 | ```go 26 | import "github.com/docker/go-plugins-helpers/ipam" 27 | 28 | d := MyIPAMDriver{} 29 | h := ipam.NewHandler(d) 30 | h.ServeUnix("root", "test_ipam") 31 | ``` 32 | -------------------------------------------------------------------------------- /secrets/README.md: -------------------------------------------------------------------------------- 1 | # Docker secrets extension API 2 | 3 | Go handler to get secrets from external secret stores in Docker. 4 | 5 | ## Usage 6 | 7 | This library is designed to be integrated in your program. 8 | 9 | 1. Implement the `secrets.Driver` interface. 10 | 2. Initialize a `secrets.Handler` with your implementation. 11 | 3. Call either `ServeTCP` or `ServeUnix` from the `secrets.Handler`. 12 | 13 | ### Example using TCP sockets: 14 | 15 | ```go 16 | import "github.com/docker/go-plugins-helpers/secrets" 17 | 18 | d := MySecretsDriver{} 19 | h := secrets.NewHandler(d) 20 | h.ServeTCP("test_secrets", ":8080") 21 | ``` 22 | 23 | ### Example using Unix sockets: 24 | 25 | ```go 26 | import "github.com/docker/go-plugins-helpers/secrets" 27 | 28 | d := MySecretsDriver{} 29 | h := secrets.NewHandler(d) 30 | h.ServeUnix("test_secrets", 0) 31 | ``` 32 | -------------------------------------------------------------------------------- /sdk/unix_listener.go: -------------------------------------------------------------------------------- 1 | //go:build linux || freebsd 2 | 3 | package sdk 4 | 5 | import ( 6 | "net" 7 | "os" 8 | "path/filepath" 9 | 10 | "github.com/docker/go-connections/sockets" 11 | ) 12 | 13 | const pluginSockDir = "/run/docker/plugins" 14 | 15 | func newUnixListener(pluginName string, gid int) (net.Listener, string, error) { 16 | path, err := fullSocketAddress(pluginName) 17 | if err != nil { 18 | return nil, "", err 19 | } 20 | listener, err := sockets.NewUnixSocket(path, gid) 21 | if err != nil { 22 | return nil, "", err 23 | } 24 | return listener, path, nil 25 | } 26 | 27 | func fullSocketAddress(address string) (string, error) { 28 | if err := os.MkdirAll(pluginSockDir, 0755); err != nil { 29 | return "", err 30 | } 31 | if filepath.IsAbs(address) { 32 | return address, nil 33 | } 34 | return filepath.Join(pluginSockDir, address+".sock"), nil 35 | } 36 | -------------------------------------------------------------------------------- /sdk/tcp_listener.go: -------------------------------------------------------------------------------- 1 | package sdk 2 | 3 | import ( 4 | "crypto/tls" 5 | "net" 6 | "runtime" 7 | 8 | "github.com/docker/go-connections/sockets" 9 | ) 10 | 11 | func newTCPListener(address, pluginName, daemonDir string, tlsConfig *tls.Config) (net.Listener, string, error) { 12 | listener, err := sockets.NewTCPSocket(address, tlsConfig) 13 | if err != nil { 14 | return nil, "", err 15 | } 16 | 17 | addr := listener.Addr().String() 18 | 19 | var specDir string 20 | if runtime.GOOS == "windows" { 21 | specDir, err = createPluginSpecDirWindows(pluginName, addr, daemonDir) 22 | } else { 23 | specDir, err = createPluginSpecDirUnix(pluginName, addr) 24 | } 25 | if err != nil { 26 | return nil, "", err 27 | } 28 | 29 | specFile, err := writeSpecFile(pluginName, addr, specDir, protoTCP) 30 | if err != nil { 31 | return nil, "", err 32 | } 33 | return listener, specFile, nil 34 | } 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-plugins-helpers 2 | 3 | A collection of helper packages to extend Docker Engine in Go 4 | 5 | | Plugin type | Documentation | Description | 6 | |---------------|-----------------------------------------------------------------------|------------------------------------| 7 | | Authorization | [Link](https://docs.docker.com/engine/extend/authorization/) | Extend API authorization mechanism | 8 | | Network | [Link](https://docs.docker.com/engine/extend/plugins_network/) | Extend network management | 9 | | Volume | [Link](https://docs.docker.com/engine/extend/plugins_volume/) | Extend persistent storage | 10 | | IPAM | [Link](https://github.com/docker/libnetwork/blob/master/docs/ipam.md) | Extend IP address management | 11 | 12 | See the [understand Docker plugins documentation section](https://docs.docker.com/engine/extend/). 13 | -------------------------------------------------------------------------------- /volume/README.md: -------------------------------------------------------------------------------- 1 | # Docker volume extension api. 2 | 3 | Go handler to create external volume extensions for Docker. 4 | 5 | ## Usage 6 | 7 | This library is designed to be integrated in your program. 8 | 9 | 1. Implement the `volume.Driver` interface. 10 | 2. Initialize a `volume.Handler` with your implementation. 11 | 3. Call either `ServeTCP` or `ServeUnix` from the `volume.Handler`. 12 | 13 | ### Example using TCP sockets: 14 | 15 | ```go 16 | d := MyVolumeDriver{} 17 | h := volume.NewHandler(d) 18 | h.ServeTCP("test_volume", ":8080") 19 | ``` 20 | 21 | ### Example using Unix sockets: 22 | 23 | ```go 24 | d := MyVolumeDriver{} 25 | h := volume.NewHandler(d) 26 | u, _ := user.Lookup("root") 27 | gid, _ := strconv.Atoi(u.Gid) 28 | h.ServeUnix("test_volume", gid) 29 | ``` 30 | 31 | ## Full example plugins 32 | 33 | - https://github.com/calavera/docker-volume-glusterfs 34 | - https://github.com/calavera/docker-volume-keywhiz 35 | - https://github.com/quobyte/docker-volume 36 | - https://github.com/NimbleStorage/Nemo 37 | -------------------------------------------------------------------------------- /authorization/README.md: -------------------------------------------------------------------------------- 1 | # Docker authorization extension api. 2 | 3 | Go handler to create external authorization extensions for Docker. 4 | 5 | ## Usage 6 | 7 | This library is designed to be integrated in your program. 8 | 9 | 1. Implement the `authorization.Plugin` interface. 10 | 2. Initialize a `authorization.Handler` with your implementation. 11 | 3. Call either `ServeTCP` or `ServeUnix` from the `authorization.Handler`. 12 | 13 | ### Example using TCP sockets: 14 | 15 | ```go 16 | p := MyAuthZPlugin{} 17 | h := authorization.NewHandler(p) 18 | h.ServeTCP("test_plugin", ":8080") 19 | ``` 20 | 21 | ### Example using Unix sockets: 22 | 23 | ```go 24 | p := MyAuthZPlugin{} 25 | h := authorization.NewHandler(p) 26 | u, _ := user.Lookup("root") 27 | gid, _ := strconv.Atoi(u.Gid) 28 | h.ServeUnix("test_plugin", gid) 29 | ``` 30 | 31 | ## Full example plugins 32 | 33 | - https://github.com/projectatomic/docker-novolume-plugin 34 | - https://github.com/cpdevws/img-authz-plugin 35 | - https://github.com/casbin/casbin-authz-plugin 36 | - https://github.com/kassisol/hbm 37 | - https://github.com/leogr/docker-authz-plugin 38 | 39 | ## License 40 | 41 | MIT 42 | -------------------------------------------------------------------------------- /sdk/encoder.go: -------------------------------------------------------------------------------- 1 | package sdk 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | ) 9 | 10 | // DefaultContentTypeV1_1 is the default content type accepted and sent by the plugins. 11 | const DefaultContentTypeV1_1 = "application/vnd.docker.plugins.v1.1+json" 12 | 13 | // DecodeRequest decodes an http request into a given structure. 14 | func DecodeRequest(w http.ResponseWriter, r *http.Request, req interface{}) (err error) { 15 | if err = json.NewDecoder(r.Body).Decode(req); err != nil { 16 | http.Error(w, err.Error(), http.StatusBadRequest) 17 | } 18 | return 19 | } 20 | 21 | // EncodeResponse encodes the given structure into an http response. 22 | func EncodeResponse(w http.ResponseWriter, res interface{}, err bool) { 23 | w.Header().Set("Content-Type", DefaultContentTypeV1_1) 24 | if err { 25 | w.WriteHeader(http.StatusInternalServerError) 26 | } 27 | json.NewEncoder(w).Encode(res) 28 | } 29 | 30 | // StreamResponse streams a response object to the client 31 | func StreamResponse(w http.ResponseWriter, data io.ReadCloser) { 32 | w.Header().Set("Content-Type", DefaultContentTypeV1_1) 33 | if _, err := copyBuf(w, data); err != nil { 34 | fmt.Printf("ERROR in stream: %v\n", err) 35 | } 36 | data.Close() 37 | } 38 | -------------------------------------------------------------------------------- /sdk/unix_listener_systemd.go: -------------------------------------------------------------------------------- 1 | //go:build (linux || freebsd) && !nosystemd 2 | 3 | package sdk 4 | 5 | import ( 6 | "fmt" 7 | "net" 8 | "os" 9 | 10 | "github.com/coreos/go-systemd/activation" 11 | ) 12 | 13 | // isRunningSystemd checks whether the host was booted with systemd as its init 14 | // system. This functions similarly to systemd's `sd_booted(3)`: internally, it 15 | // checks whether /run/systemd/system/ exists and is a directory. 16 | // http://www.freedesktop.org/software/systemd/man/sd_booted.html 17 | // 18 | // Copied from github.com/coreos/go-systemd/util.IsRunningSystemd 19 | func isRunningSystemd() bool { 20 | fi, err := os.Lstat("/run/systemd/system") 21 | if err != nil { 22 | return false 23 | } 24 | return fi.IsDir() 25 | } 26 | 27 | // FIXME(thaJeztah): this code was added in https://github.com/docker/go-plugins-helpers/commit/008703b825c10311af1840deeaf5f4769df7b59e, but is not used anywhere 28 | func setupSocketActivation() (net.Listener, error) { 29 | if !isRunningSystemd() { 30 | return nil, nil 31 | } 32 | listenFds := activation.Files(false) 33 | if len(listenFds) > 1 { 34 | return nil, fmt.Errorf("expected only one socket from systemd, got %d", len(listenFds)) 35 | } 36 | var listener net.Listener 37 | if len(listenFds) == 1 { 38 | l, err := net.FileListener(listenFds[0]) 39 | if err != nil { 40 | return nil, err 41 | } 42 | listener = l 43 | } 44 | return listener, nil 45 | } 46 | -------------------------------------------------------------------------------- /sdk/spec_file_generator.go: -------------------------------------------------------------------------------- 1 | package sdk 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | type protocol string 10 | 11 | const ( 12 | protoTCP protocol = "tcp" 13 | protoNamedPipe protocol = "npipe" 14 | ) 15 | 16 | // PluginSpecDir returns plugin spec dir in relation to daemon root directory. 17 | func PluginSpecDir(daemonRoot string) string { 18 | return ([]string{filepath.Join(daemonRoot, "plugins")})[0] 19 | } 20 | 21 | // WindowsDefaultDaemonRootDir returns default data directory of docker daemon on Windows. 22 | func WindowsDefaultDaemonRootDir() string { 23 | return filepath.Join(os.Getenv("programdata"), "docker") 24 | } 25 | 26 | func createPluginSpecDirWindows(name, address, daemonRoot string) (string, error) { 27 | _, err := os.Stat(daemonRoot) 28 | if os.IsNotExist(err) { 29 | return "", fmt.Errorf("Deamon root directory must already exist: %s", err) 30 | } 31 | 32 | pluginSpecDir := PluginSpecDir(daemonRoot) 33 | 34 | if err := windowsCreateDirectoryWithACL(pluginSpecDir); err != nil { 35 | return "", err 36 | } 37 | return pluginSpecDir, nil 38 | } 39 | 40 | func createPluginSpecDirUnix(name, address string) (string, error) { 41 | pluginSpecDir := PluginSpecDir("/etc/docker") 42 | if err := os.MkdirAll(pluginSpecDir, 0755); err != nil { 43 | return "", err 44 | } 45 | return pluginSpecDir, nil 46 | } 47 | 48 | func writeSpecFile(name, address, pluginSpecDir string, proto protocol) (string, error) { 49 | specFileDir := filepath.Join(pluginSpecDir, name+".spec") 50 | 51 | url := string(proto) + "://" + address 52 | if err := os.WriteFile(specFileDir, []byte(url), 0644); err != nil { 53 | return "", err 54 | } 55 | 56 | return specFileDir, nil 57 | } 58 | -------------------------------------------------------------------------------- /MAINTAINERS: -------------------------------------------------------------------------------- 1 | # go-plugins-helpers maintainers file 2 | # 3 | # This file describes who runs the docker/go-plugins-helpers project and how. 4 | # This is a living document - if you see something out of date or missing, speak up! 5 | # 6 | # It is structured to be consumable by both humans and programs. 7 | # To extract its contents programmatically, use any TOML-compliant parser. 8 | # 9 | # This file is compiled into the MAINTAINERS file in docker/opensource. 10 | # 11 | [Org] 12 | [Org."Core maintainers"] 13 | people = [ 14 | "akerouanton", 15 | "laurazard", 16 | "neersighted", 17 | "robmry", 18 | "rumpl", 19 | "thajeztah", 20 | "vvoland", 21 | ] 22 | 23 | [people] 24 | 25 | # A reference list of all people associated with the project. 26 | # All other sections should refer to people by their canonical key 27 | # in the people section. 28 | 29 | # ADD YOURSELF HERE IN ALPHABETICAL ORDER 30 | 31 | [people.akerouanton] 32 | Name = "Albin Kerouanton" 33 | Email = "albinker@gmail.com" 34 | GitHub = "akerouanton" 35 | 36 | [people.laurazard] 37 | Name = "Laura Brehm" 38 | Email = "laura.brehm@docker.com" 39 | GitHub = "laurazard" 40 | 41 | [people.neersighted] 42 | Name = "Bjorn Neergaard" 43 | Email = "bjorn@neersighted.com" 44 | GitHub = "neersighted" 45 | 46 | [people.robmry] 47 | Name = "Rob Murray" 48 | Email = "rob.murray@docker.com" 49 | GitHub = "robmry" 50 | 51 | [people.rumpl] 52 | Name = "Djordje Lukic" 53 | Email = "djordje.lukic@docker.com" 54 | GitHub = "rumpl" 55 | 56 | [people.thajeztah] 57 | Name = "Sebastiaan van Stijn" 58 | Email = "github@gone.nl" 59 | GitHub = "thaJeztah" 60 | 61 | [people.vvoland] 62 | Name = "Paweł Gronowski" 63 | Email = "pawel.gronowski@docker.com" 64 | GitHub = "vvoland" 65 | -------------------------------------------------------------------------------- /network/README.md: -------------------------------------------------------------------------------- 1 | # Docker network extension API 2 | 3 | Go handler to create external network extensions for Docker. 4 | 5 | ## Usage 6 | 7 | This library is designed to be integrated in your program. 8 | 9 | 1. Implement the `network.Driver` interface. 10 | 2. Initialize a `network.Handler` with your implementation. 11 | 3. Call either `ServeTCP`, `ServeUnix` or `ServeWindows` from the `network.Handler`. 12 | 4. On Windows, docker daemon data dir must be provided for ServeTCP and ServeWindows functions. 13 | On Unix, this parameter is ignored. 14 | 15 | ### Example using TCP sockets: 16 | 17 | ```go 18 | import "github.com/docker/go-plugins-helpers/network" 19 | 20 | d := MyNetworkDriver{} 21 | h := network.NewHandler(d) 22 | h.ServeTCP("test_network", ":8080", "") 23 | // on windows: 24 | h.ServeTCP("test_network", ":8080", WindowsDefaultDaemonRootDir()) 25 | ``` 26 | 27 | ### Example using Unix sockets: 28 | 29 | ```go 30 | import "github.com/docker/go-plugins-helpers/network" 31 | 32 | d := MyNetworkDriver{} 33 | h := network.NewHandler(d) 34 | h.ServeUnix("test_network", 0) 35 | ``` 36 | 37 | ### Example using Windows named pipes: 38 | 39 | ```go 40 | import "github.com/docker/go-plugins-helpers/network" 41 | import "github.com/docker/go-plugins-helpers/sdk" 42 | 43 | d := MyNetworkDriver{} 44 | h := network.NewHandler(d) 45 | 46 | config := sdk.WindowsPipeConfig{ 47 | // open, read, write permissions for everyone 48 | // (uses Windows Security Descriptor Definition Language) 49 | SecurityDescriptor: AllowServiceSystemAdmin, 50 | InBufferSize: 4096, 51 | OutBufferSize: 4096, 52 | } 53 | 54 | h.ServeWindows("//./pipe/testpipe", "test_network", WindowsDefaultDaemonRootDir(), &config) 55 | ``` 56 | 57 | ## Full example plugins 58 | 59 | - [docker-ovs-plugin](https://github.com/gopher-net/docker-ovs-plugin) - An Open vSwitch Networking Plugin 60 | -------------------------------------------------------------------------------- /volume/shim/shim_test.go: -------------------------------------------------------------------------------- 1 | package shim 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/http" 7 | "testing" 8 | 9 | "github.com/docker/docker/volume" 10 | "github.com/docker/go-connections/sockets" 11 | volumeplugin "github.com/docker/go-plugins-helpers/volume" 12 | ) 13 | 14 | type testVolumeDriver struct{} 15 | 16 | func (testVolumeDriver) Name() string { return "" } 17 | func (testVolumeDriver) Create(string, map[string]string) (volume.Volume, error) { return nil, nil } 18 | func (testVolumeDriver) Remove(volume.Volume) error { return nil } 19 | func (testVolumeDriver) List() ([]volume.Volume, error) { return nil, nil } 20 | func (testVolumeDriver) Get(name string) (volume.Volume, error) { return nil, nil } 21 | func (testVolumeDriver) Scope() string { return "local" } 22 | 23 | func TestVolumeDriver(t *testing.T) { 24 | h := NewHandlerFromVolumeDriver(testVolumeDriver{}) 25 | l := sockets.NewInmemSocket("test", 0) 26 | go h.Serve(l) 27 | defer l.Close() 28 | 29 | client := &http.Client{Transport: &http.Transport{ 30 | Dial: l.Dial, 31 | }} 32 | 33 | resp, err := pluginRequest(client, "/VolumeDriver.Create", &volumeplugin.CreateRequest{Name: "foo"}) 34 | if err != nil { 35 | t.Fatalf(err.Error()) 36 | } 37 | 38 | if resp.Err != "" { 39 | t.Fatalf("error while creating volume: %v", err) 40 | } 41 | } 42 | 43 | func pluginRequest(client *http.Client, method string, req *volumeplugin.CreateRequest) (*volumeplugin.ErrorResponse, error) { 44 | b, err := json.Marshal(req) 45 | if err != nil { 46 | return nil, err 47 | } 48 | resp, err := client.Post("http://localhost"+method, "application/json", bytes.NewReader(b)) 49 | if err != nil { 50 | return nil, err 51 | } 52 | var vResp volumeplugin.ErrorResponse 53 | err = json.NewDecoder(resp.Body).Decode(&vResp) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | return &vResp, nil 59 | } 60 | -------------------------------------------------------------------------------- /sdk/windows_listener.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package sdk 4 | 5 | import ( 6 | "net" 7 | "os" 8 | "syscall" 9 | "unsafe" 10 | 11 | "github.com/Microsoft/go-winio" 12 | ) 13 | 14 | // Named pipes use Windows Security Descriptor Definition Language to define ACL. Following are 15 | // some useful definitions. 16 | const ( 17 | // AllowEveryone grants full access permissions for everyone. 18 | AllowEveryone = "S:(ML;;NW;;;LW)D:(A;;0x12019f;;;WD)" 19 | 20 | // AllowServiceSystemAdmin grants full access permissions for Service, System, Administrator group and account. 21 | AllowServiceSystemAdmin = "D:(A;ID;FA;;;SY)(A;ID;FA;;;BA)(A;ID;FA;;;LA)(A;ID;FA;;;LS)" 22 | ) 23 | 24 | func newWindowsListener(address, pluginName, daemonRoot string, pipeConfig *WindowsPipeConfig) (net.Listener, string, error) { 25 | listener, err := winio.ListenPipe(address, &winio.PipeConfig{ 26 | SecurityDescriptor: pipeConfig.SecurityDescriptor, 27 | InputBufferSize: pipeConfig.InBufferSize, 28 | OutputBufferSize: pipeConfig.OutBufferSize, 29 | }) 30 | if err != nil { 31 | return nil, "", err 32 | } 33 | 34 | addr := listener.Addr().String() 35 | 36 | specDir, err := createPluginSpecDirWindows(pluginName, addr, daemonRoot) 37 | if err != nil { 38 | return nil, "", err 39 | } 40 | 41 | spec, err := writeSpecFile(pluginName, addr, specDir, protoNamedPipe) 42 | if err != nil { 43 | return nil, "", err 44 | } 45 | return listener, spec, nil 46 | } 47 | 48 | func windowsCreateDirectoryWithACL(name string) error { 49 | sa := syscall.SecurityAttributes{Length: 0} 50 | const sddl = "D:P(A;OICI;GA;;;BA)(A;OICI;GA;;;SY)" 51 | sd, err := winio.SddlToSecurityDescriptor(sddl) 52 | if err != nil { 53 | return &os.PathError{Op: "mkdir", Path: name, Err: err} 54 | } 55 | sa.Length = uint32(unsafe.Sizeof(sa)) 56 | sa.InheritHandle = 1 57 | sa.SecurityDescriptor = uintptr(unsafe.Pointer(&sd[0])) 58 | 59 | namep, err := syscall.UTF16PtrFromString(name) 60 | if err != nil { 61 | return &os.PathError{Op: "mkdir", Path: name, Err: err} 62 | } 63 | 64 | e := syscall.CreateDirectory(namep, &sa) 65 | if e != nil { 66 | return &os.PathError{Op: "mkdir", Path: name, Err: e} 67 | } 68 | return nil 69 | } 70 | -------------------------------------------------------------------------------- /secrets/api_test.go: -------------------------------------------------------------------------------- 1 | package secrets 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/http" 7 | "testing" 8 | 9 | "github.com/docker/go-connections/sockets" 10 | ) 11 | 12 | func TestHandler(t *testing.T) { 13 | p := &testPlugin{} 14 | h := NewHandler(p) 15 | l := sockets.NewInmemSocket("test", 0) 16 | go h.Serve(l) 17 | defer l.Close() 18 | 19 | client := &http.Client{Transport: &http.Transport{ 20 | Dial: l.Dial, 21 | }} 22 | 23 | resp, err := pluginRequest(client, getPath, Request{SecretName: "my-secret"}) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | if resp.Err != "" { 28 | t.Fatalf("error while getting secret: %v", resp.Err) 29 | } 30 | if p.get != 1 { 31 | t.Fatalf("expected get 1, got %d", p.get) 32 | } 33 | if !bytes.EqualFold(secret, resp.Value) { 34 | t.Fatalf("expecting secret value %s, got %s", secret, resp.Value) 35 | } 36 | resp, err = pluginRequest(client, getPath, Request{SecretName: ""}) 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | if p.get != 2 { 41 | t.Fatalf("expected get 2, got %d", p.get) 42 | } 43 | if resp.Err == "" { 44 | t.Fatalf("expected missing secret") 45 | } 46 | resp, err = pluginRequest(client, getPath, Request{SecretName: "another-secret", SecretLabels: map[string]string{"prefix": "p-"}}) 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | if resp.Err != "" { 51 | t.Fatalf("error while getting secret: %v", resp.Err) 52 | } 53 | if !bytes.EqualFold(append([]byte("p-"), secret...), resp.Value) { 54 | t.Fatalf("expecting secret value %s, got %s", secret, resp.Value) 55 | } 56 | } 57 | 58 | func pluginRequest(client *http.Client, method string, req Request) (*Response, error) { 59 | b, err := json.Marshal(req) 60 | if err != nil { 61 | return nil, err 62 | } 63 | resp, err := client.Post("http://localhost"+method, "application/json", bytes.NewReader(b)) 64 | if err != nil { 65 | return nil, err 66 | } 67 | var vResp Response 68 | err = json.NewDecoder(resp.Body).Decode(&vResp) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | return &vResp, nil 74 | } 75 | 76 | type testPlugin struct { 77 | get int 78 | } 79 | 80 | var secret = []byte("secret") 81 | 82 | func (p *testPlugin) Get(req Request) Response { 83 | p.get++ 84 | if req.SecretName == "" { 85 | return Response{Err: "missing secret name"} 86 | } 87 | if prefix, exists := req.SecretLabels["prefix"]; exists { 88 | return Response{Value: append([]byte(prefix), secret...)} 89 | } 90 | return Response{Value: secret} 91 | } 92 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Sign your work 2 | 3 | The sign-off is a simple line at the end of the explanation for the patch. Your 4 | signature certifies that you wrote the patch or otherwise have the right to pass 5 | it on as an open-source patch. The rules are pretty simple: if you can certify 6 | the below (from [developercertificate.org](http://developercertificate.org/)): 7 | 8 | ``` 9 | Developer Certificate of Origin 10 | Version 1.1 11 | 12 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 13 | 660 York Street, Suite 102, 14 | San Francisco, CA 94110 USA 15 | 16 | Everyone is permitted to copy and distribute verbatim copies of this 17 | license document, but changing it is not allowed. 18 | 19 | Developer's Certificate of Origin 1.1 20 | 21 | By making a contribution to this project, I certify that: 22 | 23 | (a) The contribution was created in whole or in part by me and I 24 | have the right to submit it under the open source license 25 | indicated in the file; or 26 | 27 | (b) The contribution is based upon previous work that, to the best 28 | of my knowledge, is covered under an appropriate open source 29 | license and I have the right under that license to submit that 30 | work with modifications, whether created in whole or in part 31 | by me, under the same open source license (unless I am 32 | permitted to submit under a different license), as indicated 33 | in the file; or 34 | 35 | (c) The contribution was provided directly to me by some other 36 | person who certified (a), (b) or (c) and I have not modified 37 | it. 38 | 39 | (d) I understand and agree that this project and the contribution 40 | are public and that a record of the contribution (including all 41 | personal information I submit with it, including my sign-off) is 42 | maintained indefinitely and may be redistributed consistent with 43 | this project or the open source license(s) involved. 44 | ``` 45 | 46 | Then you just add a line to every git commit message: 47 | 48 | Signed-off-by: Joe Smith 49 | 50 | Use your real name (sorry, no pseudonyms or anonymous contributions.) 51 | 52 | If you set your `user.name` and `user.email` git configs, you can sign your 53 | commit automatically with `git commit -s`. 54 | 55 | Note that the old-style `Docker-DCO-1.1-Signed-off-by: ...` format is still 56 | accepted, so there is no need to update outstanding pull requests to the new 57 | format right away, but please do adjust your processes for future contributions. 58 | -------------------------------------------------------------------------------- /sdk/handler.go: -------------------------------------------------------------------------------- 1 | package sdk 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "net" 7 | "net/http" 8 | "os" 9 | ) 10 | 11 | const activatePath = "/Plugin.Activate" 12 | 13 | // Handler is the base to create plugin handlers. 14 | // It initializes connections and sockets to listen to. 15 | type Handler struct { 16 | mux *http.ServeMux 17 | } 18 | 19 | // NewHandler creates a new Handler with an http mux. 20 | func NewHandler(manifest string) Handler { 21 | mux := http.NewServeMux() 22 | 23 | mux.HandleFunc(activatePath, func(w http.ResponseWriter, r *http.Request) { 24 | w.Header().Set("Content-Type", DefaultContentTypeV1_1) 25 | fmt.Fprintln(w, manifest) 26 | }) 27 | 28 | return Handler{mux: mux} 29 | } 30 | 31 | // Serve sets up the handler to serve requests on the passed in listener 32 | func (h Handler) Serve(l net.Listener) error { 33 | server := http.Server{ 34 | Addr: l.Addr().String(), 35 | Handler: h.mux, 36 | } 37 | return server.Serve(l) 38 | } 39 | 40 | // ServeTCP makes the handler to listen for request in a given TCP address. 41 | // It also writes the spec file in the right directory for docker to read. 42 | // Due to constrains for running Docker in Docker on Windows, data-root directory 43 | // of docker daemon must be provided. To get default directory, use 44 | // WindowsDefaultDaemonRootDir() function. On Unix, this parameter is ignored. 45 | func (h Handler) ServeTCP(pluginName, addr, daemonDir string, tlsConfig *tls.Config) error { 46 | l, spec, err := newTCPListener(addr, pluginName, daemonDir, tlsConfig) 47 | if err != nil { 48 | return err 49 | } 50 | if spec != "" { 51 | defer os.Remove(spec) 52 | } 53 | return h.Serve(l) 54 | } 55 | 56 | // ServeUnix makes the handler to listen for requests in a unix socket. 57 | // It also creates the socket file in the right directory for docker to read. 58 | func (h Handler) ServeUnix(addr string, gid int) error { 59 | l, spec, err := newUnixListener(addr, gid) 60 | if err != nil { 61 | return err 62 | } 63 | if spec != "" { 64 | defer os.Remove(spec) 65 | } 66 | return h.Serve(l) 67 | } 68 | 69 | // ServeWindows makes the handler to listen for request in a Windows named pipe. 70 | // It also creates the spec file in the right directory for docker to read. 71 | // Due to constrains for running Docker in Docker on Windows, data-root directory 72 | // of docker daemon must be provided. To get default directory, use 73 | // WindowsDefaultDaemonRootDir() function. On Unix, this parameter is ignored. 74 | func (h Handler) ServeWindows(addr, pluginName, daemonDir string, pipeConfig *WindowsPipeConfig) error { 75 | l, spec, err := newWindowsListener(addr, pluginName, daemonDir, pipeConfig) 76 | if err != nil { 77 | return err 78 | } 79 | if spec != "" { 80 | defer os.Remove(spec) 81 | } 82 | return h.Serve(l) 83 | } 84 | 85 | // HandleFunc registers a function to handle a request path with. 86 | func (h Handler) HandleFunc(path string, fn func(w http.ResponseWriter, r *http.Request)) { 87 | h.mux.HandleFunc(path, fn) 88 | } 89 | -------------------------------------------------------------------------------- /volume/shim/shim.go: -------------------------------------------------------------------------------- 1 | package shim 2 | 3 | import ( 4 | "github.com/docker/docker/volume" 5 | volumeplugin "github.com/docker/go-plugins-helpers/volume" 6 | ) 7 | 8 | type shimDriver struct { 9 | d volume.Driver 10 | } 11 | 12 | // NewHandlerFromVolumeDriver creates a plugin handler from an existing volume 13 | // driver. This could be used, for instance, by the `local` volume driver built-in 14 | // to Docker Engine and it would create a plugin from it that maps plugin API calls 15 | // directly to any volume driver that satisfies the volume.Driver interface from 16 | // Docker Engine. 17 | func NewHandlerFromVolumeDriver(d volume.Driver) *volumeplugin.Handler { 18 | return volumeplugin.NewHandler(&shimDriver{d}) 19 | } 20 | 21 | func (d *shimDriver) Create(req *volumeplugin.CreateRequest) error { 22 | _, err := d.d.Create(req.Name, req.Options) 23 | return err 24 | } 25 | 26 | func (d *shimDriver) List() (*volumeplugin.ListResponse, error) { 27 | var res *volumeplugin.ListResponse 28 | ls, err := d.d.List() 29 | if err != nil { 30 | return &volumeplugin.ListResponse{}, err 31 | } 32 | vols := make([]*volumeplugin.Volume, len(ls)) 33 | 34 | for i, v := range ls { 35 | vol := &volumeplugin.Volume{ 36 | Name: v.Name(), 37 | Mountpoint: v.Path(), 38 | } 39 | vols[i] = vol 40 | } 41 | res.Volumes = vols 42 | return res, nil 43 | } 44 | 45 | func (d *shimDriver) Get(req *volumeplugin.GetRequest) (*volumeplugin.GetResponse, error) { 46 | var res *volumeplugin.GetResponse 47 | v, err := d.d.Get(req.Name) 48 | if err != nil { 49 | return &volumeplugin.GetResponse{}, err 50 | } 51 | res.Volume = &volumeplugin.Volume{ 52 | Name: v.Name(), 53 | Mountpoint: v.Path(), 54 | Status: v.Status(), 55 | } 56 | return &volumeplugin.GetResponse{}, nil 57 | } 58 | 59 | func (d *shimDriver) Remove(req *volumeplugin.RemoveRequest) error { 60 | v, err := d.d.Get(req.Name) 61 | if err != nil { 62 | return err 63 | } 64 | if err := d.d.Remove(v); err != nil { 65 | return err 66 | } 67 | return nil 68 | } 69 | 70 | func (d *shimDriver) Path(req *volumeplugin.PathRequest) (*volumeplugin.PathResponse, error) { 71 | var res *volumeplugin.PathResponse 72 | v, err := d.d.Get(req.Name) 73 | if err != nil { 74 | return &volumeplugin.PathResponse{}, err 75 | } 76 | res.Mountpoint = v.Path() 77 | return res, nil 78 | } 79 | 80 | func (d *shimDriver) Mount(req *volumeplugin.MountRequest) (*volumeplugin.MountResponse, error) { 81 | var res *volumeplugin.MountResponse 82 | v, err := d.d.Get(req.Name) 83 | if err != nil { 84 | return &volumeplugin.MountResponse{}, err 85 | } 86 | pth, err := v.Mount(req.ID) 87 | if err != nil { 88 | return &volumeplugin.MountResponse{}, err 89 | } 90 | res.Mountpoint = pth 91 | return res, nil 92 | } 93 | 94 | func (d *shimDriver) Unmount(req *volumeplugin.UnmountRequest) error { 95 | v, err := d.d.Get(req.Name) 96 | if err != nil { 97 | return err 98 | } 99 | if err := v.Unmount(req.ID); err != nil { 100 | return err 101 | } 102 | return nil 103 | } 104 | 105 | func (d *shimDriver) Capabilities() *volumeplugin.CapabilitiesResponse { 106 | var res *volumeplugin.CapabilitiesResponse 107 | res.Capabilities = volumeplugin.Capability{Scope: d.d.Scope()} 108 | return res 109 | } 110 | -------------------------------------------------------------------------------- /secrets/api.go: -------------------------------------------------------------------------------- 1 | package secrets 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/docker/go-plugins-helpers/sdk" 7 | ) 8 | 9 | const ( 10 | manifest = `{"Implements": ["secretprovider"]}` 11 | getPath = "/SecretProvider.GetSecret" 12 | ) 13 | 14 | // Request is the plugin secret request 15 | type Request struct { 16 | SecretName string `json:",omitempty"` // SecretName is the name of the secret to request from the plugin 17 | SecretLabels map[string]string `json:",omitempty"` // SecretLabels capture environment names and other metadata pertaining to the secret 18 | ServiceHostname string `json:",omitempty"` // ServiceHostname is the hostname of the service, can be used for x509 certificate 19 | ServiceName string `json:",omitempty"` // ServiceName is the name of the service that requested the secret 20 | ServiceID string `json:",omitempty"` // ServiceID is the name of the service that requested the secret 21 | ServiceLabels map[string]string `json:",omitempty"` // ServiceLabels capture environment names and other metadata pertaining to the service 22 | TaskID string `json:",omitempty"` // TaskID is the ID of the task that the secret is assigned to 23 | TaskName string `json:",omitempty"` // TaskName is the name of the task that the secret is assigned to 24 | TaskImage string `json:",omitempty"` // TaskName is the image of the task that the secret is assigned to 25 | ServiceEndpointSpec *EndpointSpec `json:",omitempty"` // ServiceEndpointSpec holds the specification for endpoints 26 | } 27 | 28 | // Response contains the plugin secret value 29 | type Response struct { 30 | Value []byte `json:",omitempty"` // Value is the value of the secret 31 | Err string `json:",omitempty"` // Err is the error response of the plugin 32 | 33 | // DoNotReuse indicates that the secret returned from this request should 34 | // only be used for one task, and any further tasks should call the secret 35 | // driver again. 36 | DoNotReuse bool `json:",omitempty"` 37 | } 38 | 39 | // EndpointSpec represents the spec of an endpoint. 40 | type EndpointSpec struct { 41 | Mode int32 `json:",omitempty"` 42 | Ports []PortConfig `json:",omitempty"` 43 | } 44 | 45 | // PortConfig represents the config of a port. 46 | type PortConfig struct { 47 | Name string `json:",omitempty"` 48 | Protocol int32 `json:",omitempty"` 49 | // TargetPort is the port inside the container 50 | TargetPort uint32 `json:",omitempty"` 51 | // PublishedPort is the port on the swarm hosts 52 | PublishedPort uint32 `json:",omitempty"` 53 | // PublishMode is the mode in which port is published 54 | PublishMode int32 `json:",omitempty"` 55 | } 56 | 57 | // Driver represent the interface a driver must fulfill. 58 | type Driver interface { 59 | // Get gets a secret from a remote secret store 60 | Get(Request) Response 61 | } 62 | 63 | // Handler forwards requests and responses between the docker daemon and the plugin. 64 | type Handler struct { 65 | driver Driver 66 | sdk.Handler 67 | } 68 | 69 | // NewHandler initializes the request handler with a driver implementation. 70 | func NewHandler(driver Driver) *Handler { 71 | h := &Handler{driver, sdk.NewHandler(manifest)} 72 | h.initMux() 73 | return h 74 | } 75 | 76 | func (h *Handler) initMux() { 77 | h.HandleFunc(getPath, func(w http.ResponseWriter, r *http.Request) { 78 | var req Request 79 | if err := sdk.DecodeRequest(w, r, &req); err != nil { 80 | return 81 | } 82 | res := h.driver.Get(req) 83 | sdk.EncodeResponse(w, res, res.Err != "") 84 | }) 85 | } 86 | -------------------------------------------------------------------------------- /authorization/api.go: -------------------------------------------------------------------------------- 1 | package authorization 2 | 3 | import ( 4 | "crypto/x509" 5 | "encoding/json" 6 | "encoding/pem" 7 | "net/http" 8 | 9 | "github.com/docker/go-plugins-helpers/sdk" 10 | ) 11 | 12 | const ( 13 | // AuthZApiRequest is the url for daemon request authorization 14 | AuthZApiRequest = "AuthZPlugin.AuthZReq" 15 | 16 | // AuthZApiResponse is the url for daemon response authorization 17 | AuthZApiResponse = "AuthZPlugin.AuthZRes" 18 | 19 | // AuthZApiImplements is the name of the interface all AuthZ plugins implement 20 | AuthZApiImplements = "authz" 21 | 22 | manifest = `{"Implements": ["` + AuthZApiImplements + `"]}` 23 | reqPath = "/" + AuthZApiRequest 24 | resPath = "/" + AuthZApiResponse 25 | ) 26 | 27 | // PeerCertificate is a wrapper around x509.Certificate which provides a sane 28 | // encoding/decoding to/from PEM format and JSON. 29 | type PeerCertificate x509.Certificate 30 | 31 | // MarshalJSON returns the JSON encoded pem bytes of a PeerCertificate. 32 | func (pc *PeerCertificate) MarshalJSON() ([]byte, error) { 33 | b := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: pc.Raw}) 34 | return json.Marshal(b) 35 | } 36 | 37 | // UnmarshalJSON populates a new PeerCertificate struct from JSON data. 38 | func (pc *PeerCertificate) UnmarshalJSON(b []byte) error { 39 | var buf []byte 40 | if err := json.Unmarshal(b, &buf); err != nil { 41 | return err 42 | } 43 | derBytes, _ := pem.Decode(buf) 44 | c, err := x509.ParseCertificate(derBytes.Bytes) 45 | if err != nil { 46 | return err 47 | } 48 | *pc = PeerCertificate(*c) 49 | return nil 50 | } 51 | 52 | // Request holds data required for authZ plugins 53 | type Request struct { 54 | // User holds the user extracted by AuthN mechanism 55 | User string `json:"User,omitempty"` 56 | 57 | // UserAuthNMethod holds the mechanism used to extract user details (e.g., krb) 58 | UserAuthNMethod string `json:"UserAuthNMethod,omitempty"` 59 | 60 | // RequestMethod holds the HTTP method (GET/POST/PUT) 61 | RequestMethod string `json:"RequestMethod,omitempty"` 62 | 63 | // RequestUri holds the full HTTP uri (e.g., /v1.21/version) 64 | RequestURI string `json:"RequestUri,omitempty"` 65 | 66 | // RequestBody stores the raw request body sent to the docker daemon 67 | RequestBody []byte `json:"RequestBody,omitempty"` 68 | 69 | // RequestHeaders stores the raw request headers sent to the docker daemon 70 | RequestHeaders map[string]string `json:"RequestHeaders,omitempty"` 71 | 72 | // RequestPeerCertificates stores the request's TLS peer certificates in PEM format 73 | RequestPeerCertificates []*PeerCertificate `json:"RequestPeerCertificates,omitempty"` 74 | 75 | // ResponseStatusCode stores the status code returned from docker daemon 76 | ResponseStatusCode int `json:"ResponseStatusCode,omitempty"` 77 | 78 | // ResponseBody stores the raw response body sent from docker daemon 79 | ResponseBody []byte `json:"ResponseBody,omitempty"` 80 | 81 | // ResponseHeaders stores the response headers sent to the docker daemon 82 | ResponseHeaders map[string]string `json:"ResponseHeaders,omitempty"` 83 | } 84 | 85 | // Response represents authZ plugin response 86 | type Response struct { 87 | // Allow indicating whether the user is allowed or not 88 | Allow bool `json:"Allow"` 89 | 90 | // Msg stores the authorization message 91 | Msg string `json:"Msg,omitempty"` 92 | 93 | // Err stores a message in case there's an error 94 | Err string `json:"Err,omitempty"` 95 | } 96 | 97 | // Plugin represent the interface a plugin must fulfill. 98 | type Plugin interface { 99 | AuthZReq(Request) Response 100 | AuthZRes(Request) Response 101 | } 102 | 103 | // Handler forwards requests and responses between the docker daemon and the plugin. 104 | type Handler struct { 105 | plugin Plugin 106 | sdk.Handler 107 | } 108 | 109 | // NewHandler initializes the request handler with a plugin implementation. 110 | func NewHandler(plugin Plugin) *Handler { 111 | h := &Handler{plugin, sdk.NewHandler(manifest)} 112 | h.initMux() 113 | return h 114 | } 115 | 116 | func (h *Handler) initMux() { 117 | h.handle(reqPath, func(req Request) Response { 118 | return h.plugin.AuthZReq(req) 119 | }) 120 | 121 | h.handle(resPath, func(req Request) Response { 122 | return h.plugin.AuthZRes(req) 123 | }) 124 | } 125 | 126 | type actionHandler func(Request) Response 127 | 128 | func (h *Handler) handle(name string, actionCall actionHandler) { 129 | h.HandleFunc(name, func(w http.ResponseWriter, r *http.Request) { 130 | var ( 131 | req Request 132 | d = json.NewDecoder(r.Body) 133 | ) 134 | d.UseNumber() 135 | if err := d.Decode(&req); err != nil { 136 | http.Error(w, err.Error(), http.StatusBadRequest) 137 | } 138 | 139 | res := actionCall(req) 140 | 141 | sdk.EncodeResponse(w, res, res.Err != "") 142 | }) 143 | } 144 | -------------------------------------------------------------------------------- /ipam/api.go: -------------------------------------------------------------------------------- 1 | package ipam 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/docker/go-plugins-helpers/sdk" 7 | ) 8 | 9 | const ( 10 | manifest = `{"Implements": ["IpamDriver"]}` 11 | 12 | capabilitiesPath = "/IpamDriver.GetCapabilities" 13 | addressSpacesPath = "/IpamDriver.GetDefaultAddressSpaces" 14 | requestPoolPath = "/IpamDriver.RequestPool" 15 | releasePoolPath = "/IpamDriver.ReleasePool" 16 | requestAddressPath = "/IpamDriver.RequestAddress" 17 | releaseAddressPath = "/IpamDriver.ReleaseAddress" 18 | ) 19 | 20 | // Ipam represent the interface a driver must fulfill. 21 | type Ipam interface { 22 | GetCapabilities() (*CapabilitiesResponse, error) 23 | GetDefaultAddressSpaces() (*AddressSpacesResponse, error) 24 | RequestPool(*RequestPoolRequest) (*RequestPoolResponse, error) 25 | ReleasePool(*ReleasePoolRequest) error 26 | RequestAddress(*RequestAddressRequest) (*RequestAddressResponse, error) 27 | ReleaseAddress(*ReleaseAddressRequest) error 28 | } 29 | 30 | // CapabilitiesResponse returns whether or not this IPAM required pre-made MAC 31 | type CapabilitiesResponse struct { 32 | RequiresMACAddress bool 33 | } 34 | 35 | // AddressSpacesResponse returns the default local and global address space names for this IPAM 36 | type AddressSpacesResponse struct { 37 | LocalDefaultAddressSpace string 38 | GlobalDefaultAddressSpace string 39 | } 40 | 41 | // RequestPoolRequest is sent by the daemon when a pool needs to be created 42 | type RequestPoolRequest struct { 43 | AddressSpace string 44 | Pool string 45 | SubPool string 46 | Options map[string]string 47 | V6 bool 48 | } 49 | 50 | // RequestPoolResponse returns a registered address pool with the IPAM driver 51 | type RequestPoolResponse struct { 52 | PoolID string 53 | Pool string 54 | Data map[string]string 55 | } 56 | 57 | // ReleasePoolRequest is sent when releasing a previously registered address pool 58 | type ReleasePoolRequest struct { 59 | PoolID string 60 | } 61 | 62 | // RequestAddressRequest is sent when requesting an address from IPAM 63 | type RequestAddressRequest struct { 64 | PoolID string 65 | Address string 66 | Options map[string]string 67 | } 68 | 69 | // RequestAddressResponse is formed with allocated address by IPAM 70 | type RequestAddressResponse struct { 71 | Address string 72 | Data map[string]string 73 | } 74 | 75 | // ReleaseAddressRequest is sent in order to release an address from the pool 76 | type ReleaseAddressRequest struct { 77 | PoolID string 78 | Address string 79 | } 80 | 81 | // ErrorResponse is a formatted error message that libnetwork can understand 82 | type ErrorResponse struct { 83 | Err string 84 | } 85 | 86 | // NewErrorResponse creates an ErrorResponse with the provided message 87 | func NewErrorResponse(msg string) *ErrorResponse { 88 | return &ErrorResponse{Err: msg} 89 | } 90 | 91 | // Handler forwards requests and responses between the docker daemon and the plugin. 92 | type Handler struct { 93 | ipam Ipam 94 | sdk.Handler 95 | } 96 | 97 | // NewHandler initializes the request handler with a driver implementation. 98 | func NewHandler(ipam Ipam) *Handler { 99 | h := &Handler{ipam, sdk.NewHandler(manifest)} 100 | h.initMux() 101 | return h 102 | } 103 | 104 | func (h *Handler) initMux() { 105 | h.HandleFunc(capabilitiesPath, func(w http.ResponseWriter, r *http.Request) { 106 | res, err := h.ipam.GetCapabilities() 107 | if err != nil { 108 | sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) 109 | return 110 | } 111 | sdk.EncodeResponse(w, res, false) 112 | }) 113 | h.HandleFunc(addressSpacesPath, func(w http.ResponseWriter, r *http.Request) { 114 | res, err := h.ipam.GetDefaultAddressSpaces() 115 | if err != nil { 116 | sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) 117 | return 118 | } 119 | sdk.EncodeResponse(w, res, false) 120 | }) 121 | h.HandleFunc(requestPoolPath, func(w http.ResponseWriter, r *http.Request) { 122 | req := &RequestPoolRequest{} 123 | err := sdk.DecodeRequest(w, r, req) 124 | if err != nil { 125 | return 126 | } 127 | res, err := h.ipam.RequestPool(req) 128 | if err != nil { 129 | sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) 130 | return 131 | } 132 | sdk.EncodeResponse(w, res, false) 133 | }) 134 | h.HandleFunc(releasePoolPath, func(w http.ResponseWriter, r *http.Request) { 135 | req := &ReleasePoolRequest{} 136 | err := sdk.DecodeRequest(w, r, req) 137 | if err != nil { 138 | return 139 | } 140 | err = h.ipam.ReleasePool(req) 141 | if err != nil { 142 | sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) 143 | return 144 | } 145 | sdk.EncodeResponse(w, struct{}{}, false) 146 | }) 147 | h.HandleFunc(requestAddressPath, func(w http.ResponseWriter, r *http.Request) { 148 | req := &RequestAddressRequest{} 149 | err := sdk.DecodeRequest(w, r, req) 150 | if err != nil { 151 | return 152 | } 153 | res, err := h.ipam.RequestAddress(req) 154 | if err != nil { 155 | sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) 156 | return 157 | } 158 | sdk.EncodeResponse(w, res, false) 159 | }) 160 | h.HandleFunc(releaseAddressPath, func(w http.ResponseWriter, r *http.Request) { 161 | req := &ReleaseAddressRequest{} 162 | err := sdk.DecodeRequest(w, r, req) 163 | if err != nil { 164 | return 165 | } 166 | err = h.ipam.ReleaseAddress(req) 167 | if err != nil { 168 | sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) 169 | return 170 | } 171 | sdk.EncodeResponse(w, struct{}{}, false) 172 | }) 173 | } 174 | -------------------------------------------------------------------------------- /volume/api_test.go: -------------------------------------------------------------------------------- 1 | package volume 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "testing" 10 | 11 | "github.com/docker/go-connections/sockets" 12 | ) 13 | 14 | func TestHandler(t *testing.T) { 15 | p := &testPlugin{} 16 | h := NewHandler(p) 17 | l := sockets.NewInmemSocket("test", 0) 18 | go h.Serve(l) 19 | defer l.Close() 20 | 21 | client := &http.Client{Transport: &http.Transport{ 22 | Dial: l.Dial, 23 | }} 24 | 25 | // Create 26 | _, err := pluginRequest(client, createPath, &CreateRequest{Name: "foo"}) 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | if p.create != 1 { 31 | t.Fatalf("expected create 1, got %d", p.create) 32 | } 33 | 34 | // Get 35 | resp, err := pluginRequest(client, getPath, &GetRequest{Name: "foo"}) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | var gResp *GetResponse 40 | if err := json.NewDecoder(resp).Decode(&gResp); err != nil { 41 | t.Fatal(err) 42 | } 43 | if gResp.Volume.Name != "foo" { 44 | t.Fatalf("expected volume `foo`, got %v", gResp.Volume) 45 | } 46 | if p.get != 1 { 47 | t.Fatalf("expected get 1, got %d", p.get) 48 | } 49 | 50 | // List 51 | resp, err = pluginRequest(client, listPath, nil) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | var lResp *ListResponse 56 | if err := json.NewDecoder(resp).Decode(&lResp); err != nil { 57 | t.Fatal(err) 58 | } 59 | if len(lResp.Volumes) != 1 { 60 | t.Fatalf("expected 1 volume, got %v", lResp.Volumes) 61 | } 62 | if lResp.Volumes[0].Name != "foo" { 63 | t.Fatalf("expected volume `foo`, got %v", lResp.Volumes[0]) 64 | } 65 | if p.list != 1 { 66 | t.Fatalf("expected list 1, got %d", p.list) 67 | } 68 | 69 | // Path 70 | if _, err := pluginRequest(client, hostVirtualPath, &PathRequest{Name: "foo"}); err != nil { 71 | t.Fatal(err) 72 | } 73 | if p.path != 1 { 74 | t.Fatalf("expected path 1, got %d", p.path) 75 | } 76 | 77 | // Mount 78 | if _, err := pluginRequest(client, mountPath, &MountRequest{Name: "foo"}); err != nil { 79 | t.Fatal(err) 80 | } 81 | if p.mount != 1 { 82 | t.Fatalf("expected mount 1, got %d", p.mount) 83 | } 84 | 85 | // Unmount 86 | if _, err := pluginRequest(client, unmountPath, &UnmountRequest{Name: "foo"}); err != nil { 87 | t.Fatal(err) 88 | } 89 | if p.unmount != 1 { 90 | t.Fatalf("expected unmount 1, got %d", p.unmount) 91 | } 92 | 93 | // Remove 94 | _, err = pluginRequest(client, removePath, &RemoveRequest{Name: "foo"}) 95 | if err != nil { 96 | t.Fatal(err) 97 | } 98 | if p.remove != 1 { 99 | t.Fatalf("expected remove 1, got %d", p.remove) 100 | } 101 | 102 | // Capabilities 103 | resp, err = pluginRequest(client, capabilitiesPath, nil) 104 | var cResp *CapabilitiesResponse 105 | if err := json.NewDecoder(resp).Decode(&cResp); err != nil { 106 | t.Fatal(err) 107 | } 108 | 109 | if p.capabilities != 1 { 110 | t.Fatalf("expected remove 1, got %d", p.capabilities) 111 | } 112 | } 113 | 114 | func pluginRequest(client *http.Client, method string, req interface{}) (io.Reader, error) { 115 | b, err := json.Marshal(req) 116 | if err != nil { 117 | return nil, err 118 | } 119 | if req == nil { 120 | b = []byte{} 121 | } 122 | resp, err := client.Post("http://localhost"+method, "application/json", bytes.NewReader(b)) 123 | if err != nil { 124 | return nil, err 125 | } 126 | 127 | return resp.Body, nil 128 | } 129 | 130 | type testPlugin struct { 131 | volumes []string 132 | create int 133 | get int 134 | list int 135 | path int 136 | mount int 137 | unmount int 138 | remove int 139 | capabilities int 140 | } 141 | 142 | func (p *testPlugin) Create(req *CreateRequest) error { 143 | p.create++ 144 | p.volumes = append(p.volumes, req.Name) 145 | return nil 146 | } 147 | 148 | func (p *testPlugin) Get(req *GetRequest) (*GetResponse, error) { 149 | p.get++ 150 | for _, v := range p.volumes { 151 | if v == req.Name { 152 | return &GetResponse{Volume: &Volume{Name: v}}, nil 153 | } 154 | } 155 | return &GetResponse{}, fmt.Errorf("no such volume") 156 | } 157 | 158 | func (p *testPlugin) List() (*ListResponse, error) { 159 | p.list++ 160 | var vols []*Volume 161 | for _, v := range p.volumes { 162 | vols = append(vols, &Volume{Name: v}) 163 | } 164 | return &ListResponse{Volumes: vols}, nil 165 | } 166 | 167 | func (p *testPlugin) Remove(req *RemoveRequest) error { 168 | p.remove++ 169 | for i, v := range p.volumes { 170 | if v == req.Name { 171 | p.volumes = append(p.volumes[:i], p.volumes[i+1:]...) 172 | return nil 173 | } 174 | } 175 | return fmt.Errorf("no such volume") 176 | } 177 | 178 | func (p *testPlugin) Path(req *PathRequest) (*PathResponse, error) { 179 | p.path++ 180 | for _, v := range p.volumes { 181 | if v == req.Name { 182 | return &PathResponse{}, nil 183 | } 184 | } 185 | return &PathResponse{}, fmt.Errorf("no such volume") 186 | } 187 | 188 | func (p *testPlugin) Mount(req *MountRequest) (*MountResponse, error) { 189 | p.mount++ 190 | for _, v := range p.volumes { 191 | if v == req.Name { 192 | return &MountResponse{}, nil 193 | } 194 | } 195 | return &MountResponse{}, fmt.Errorf("no such volume") 196 | } 197 | 198 | func (p *testPlugin) Unmount(req *UnmountRequest) error { 199 | p.unmount++ 200 | for _, v := range p.volumes { 201 | if v == req.Name { 202 | return nil 203 | } 204 | } 205 | return fmt.Errorf("no such volume") 206 | } 207 | 208 | func (p *testPlugin) Capabilities() *CapabilitiesResponse { 209 | p.capabilities++ 210 | return &CapabilitiesResponse{Capabilities: Capability{Scope: "local"}} 211 | } 212 | -------------------------------------------------------------------------------- /authorization/api_test.go: -------------------------------------------------------------------------------- 1 | package authorization 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/rsa" 6 | "crypto/tls" 7 | "crypto/x509" 8 | "crypto/x509/pkix" 9 | "encoding/json" 10 | "fmt" 11 | "io" 12 | "math/big" 13 | "net/http" 14 | "os" 15 | "strings" 16 | "testing" 17 | "time" 18 | 19 | "github.com/docker/go-plugins-helpers/sdk" 20 | "github.com/stretchr/testify/require" 21 | ) 22 | 23 | type TestPlugin struct { 24 | Plugin 25 | } 26 | 27 | func (p *TestPlugin) AuthZReq(r Request) Response { 28 | return Response{ 29 | Allow: false, 30 | Msg: "You are not authorized", 31 | Err: "", 32 | } 33 | } 34 | 35 | func (p *TestPlugin) AuthZRes(r Request) Response { 36 | return Response{ 37 | Allow: false, 38 | Msg: "You are not authorized", 39 | Err: "", 40 | } 41 | } 42 | 43 | func TestActivate(t *testing.T) { 44 | response, err := http.Get("http://localhost:32456/Plugin.Activate") 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | 49 | defer response.Body.Close() 50 | 51 | body, err := io.ReadAll(response.Body) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | 56 | if string(body) != manifest+"\n" { 57 | t.Fatalf("Expected %s, got %s\n", manifest+"\n", string(body)) 58 | } 59 | } 60 | 61 | func TestAuthZReq(t *testing.T) { 62 | request := `{"User":"bob","UserAuthNMethod":"","RequestMethod":"POST","RequestURI":"http://127.0.0.1/v.1.23/containers/json","RequestBody":"","RequestHeader":"","RequestStatusCode":""}` 63 | 64 | response, err := http.Post( 65 | "http://localhost:32456/AuthZPlugin.AuthZReq", 66 | sdk.DefaultContentTypeV1_1, 67 | strings.NewReader(request), 68 | ) 69 | if err != nil { 70 | t.Fatal(err) 71 | } 72 | 73 | defer response.Body.Close() 74 | 75 | body, err := io.ReadAll(response.Body) 76 | if err != nil { 77 | t.Fatal(err) 78 | } 79 | 80 | var r Response 81 | if err := json.Unmarshal(body, &r); err != nil { 82 | t.Fatal(err) 83 | } 84 | 85 | if r.Msg != "You are not authorized" { 86 | t.Fatal("Authorization message does not match") 87 | } 88 | 89 | if r.Allow { 90 | t.Fatal("The request has been allowed while should not be") 91 | } 92 | 93 | if r.Err != "" { 94 | t.Fatal("Authorization Error should be empty") 95 | } 96 | } 97 | 98 | func TestAuthZRes(t *testing.T) { 99 | request := `{"User":"bob","UserAuthNMethod":"","RequestMethod":"POST","RequestURI":"http://127.0.0.1/v.1.23/containers/json","RequestBody":"","RequestHeader":"","RequestStatusCode":"", "ResponseBody":"","ResponseHeader":"","ResponseStatusCode":200}` 100 | 101 | response, err := http.Post( 102 | "http://localhost:32456/AuthZPlugin.AuthZRes", 103 | sdk.DefaultContentTypeV1_1, 104 | strings.NewReader(request), 105 | ) 106 | if err != nil { 107 | t.Fatal(err) 108 | } 109 | 110 | defer response.Body.Close() 111 | 112 | body, err := io.ReadAll(response.Body) 113 | if err != nil { 114 | t.Fatal(err) 115 | } 116 | 117 | var r Response 118 | if err := json.Unmarshal(body, &r); err != nil { 119 | t.Fatal(err) 120 | } 121 | 122 | if r.Msg != "You are not authorized" { 123 | t.Fatal("Authorization message does not match") 124 | } 125 | 126 | if r.Allow { 127 | t.Fatal("The request has been allowed while should not be") 128 | } 129 | 130 | if r.Err != "" { 131 | t.Fatal("Authorization Error should be empty") 132 | } 133 | } 134 | 135 | func TestPeerCertificateMarshalJSON(t *testing.T) { 136 | template := &x509.Certificate{ 137 | IsCA: true, 138 | BasicConstraintsValid: true, 139 | SubjectKeyId: []byte{1, 2, 3}, 140 | SerialNumber: big.NewInt(1234), 141 | Subject: pkix.Name{ 142 | Country: []string{"Earth"}, 143 | Organization: []string{"Mother Nature"}, 144 | }, 145 | NotBefore: time.Now(), 146 | NotAfter: time.Now().AddDate(5, 5, 5), 147 | 148 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, 149 | KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, 150 | } 151 | // generate private key 152 | privatekey, err := rsa.GenerateKey(rand.Reader, 2048) 153 | require.NoError(t, err) 154 | publickey := &privatekey.PublicKey 155 | 156 | // create a self-signed certificate. template = parent 157 | parent := template 158 | raw, err := x509.CreateCertificate(rand.Reader, template, parent, publickey, privatekey) 159 | require.NoError(t, err) 160 | 161 | cert, err := x509.ParseCertificate(raw) 162 | require.NoError(t, err) 163 | 164 | certs := []*x509.Certificate{cert} 165 | addr := "www.authz.com/auth" 166 | req, err := http.NewRequest("GET", addr, nil) 167 | require.NoError(t, err) 168 | 169 | req.RequestURI = addr 170 | req.TLS = &tls.ConnectionState{} 171 | req.TLS.PeerCertificates = certs 172 | req.Header.Add("header", "value") 173 | 174 | for _, c := range req.TLS.PeerCertificates { 175 | pcObj := PeerCertificate(*c) 176 | 177 | t.Run("Marshalling :", func(t *testing.T) { 178 | raw, err = pcObj.MarshalJSON() 179 | require.NotNil(t, raw) 180 | require.Nil(t, err) 181 | }) 182 | 183 | t.Run("UnMarshalling :", func(t *testing.T) { 184 | err := pcObj.UnmarshalJSON(raw) 185 | require.Nil(t, err) 186 | require.Equal(t, "Earth", pcObj.Subject.Country[0]) 187 | require.Equal(t, true, pcObj.IsCA) 188 | }) 189 | 190 | } 191 | } 192 | 193 | func callURL(url string) { 194 | c := http.Client{ 195 | Timeout: 10 * time.Millisecond, 196 | } 197 | res := make(chan interface{}, 1) 198 | go func() { 199 | for { 200 | _, err := c.Get(url) 201 | if err == nil { 202 | res <- nil 203 | } 204 | } 205 | }() 206 | 207 | select { 208 | case <-res: 209 | return 210 | case <-time.After(5 * time.Second): 211 | fmt.Printf("Timeout connecting to %s\n", url) 212 | os.Exit(1) 213 | } 214 | } 215 | 216 | func TestMain(m *testing.M) { 217 | d := &TestPlugin{} 218 | h := NewHandler(d) 219 | go h.ServeTCP("test", "localhost:32456", "", nil) 220 | 221 | callURL("http://localhost:32456/Plugin.Activate") 222 | 223 | os.Exit(m.Run()) 224 | } 225 | -------------------------------------------------------------------------------- /volume/api.go: -------------------------------------------------------------------------------- 1 | package volume 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/docker/go-plugins-helpers/sdk" 7 | ) 8 | 9 | const ( 10 | // DefaultDockerRootDirectory is the default directory where volumes will be created. 11 | DefaultDockerRootDirectory = "/var/lib/docker-volumes" 12 | 13 | manifest = `{"Implements": ["VolumeDriver"]}` 14 | createPath = "/VolumeDriver.Create" 15 | getPath = "/VolumeDriver.Get" 16 | listPath = "/VolumeDriver.List" 17 | removePath = "/VolumeDriver.Remove" 18 | hostVirtualPath = "/VolumeDriver.Path" 19 | mountPath = "/VolumeDriver.Mount" 20 | unmountPath = "/VolumeDriver.Unmount" 21 | capabilitiesPath = "/VolumeDriver.Capabilities" 22 | ) 23 | 24 | // CreateRequest is the structure that docker's requests are deserialized to. 25 | type CreateRequest struct { 26 | Name string 27 | Options map[string]string `json:"Opts,omitempty"` 28 | } 29 | 30 | // RemoveRequest structure for a volume remove request 31 | type RemoveRequest struct { 32 | Name string 33 | } 34 | 35 | // MountRequest structure for a volume mount request 36 | type MountRequest struct { 37 | Name string 38 | ID string 39 | } 40 | 41 | // MountResponse structure for a volume mount response 42 | type MountResponse struct { 43 | Mountpoint string 44 | } 45 | 46 | // UnmountRequest structure for a volume unmount request 47 | type UnmountRequest struct { 48 | Name string 49 | ID string 50 | } 51 | 52 | // PathRequest structure for a volume path request 53 | type PathRequest struct { 54 | Name string 55 | } 56 | 57 | // PathResponse structure for a volume path response 58 | type PathResponse struct { 59 | Mountpoint string 60 | } 61 | 62 | // GetRequest structure for a volume get request 63 | type GetRequest struct { 64 | Name string 65 | } 66 | 67 | // GetResponse structure for a volume get response 68 | type GetResponse struct { 69 | Volume *Volume 70 | } 71 | 72 | // ListResponse structure for a volume list response 73 | type ListResponse struct { 74 | Volumes []*Volume 75 | } 76 | 77 | // CapabilitiesResponse structure for a volume capability response 78 | type CapabilitiesResponse struct { 79 | Capabilities Capability 80 | } 81 | 82 | // Volume represents a volume object for use with `Get` and `List` requests 83 | type Volume struct { 84 | Name string 85 | Mountpoint string `json:",omitempty"` 86 | CreatedAt string `json:",omitempty"` 87 | Status map[string]interface{} `json:",omitempty"` 88 | } 89 | 90 | // Capability represents the list of capabilities a volume driver can return 91 | type Capability struct { 92 | Scope string 93 | } 94 | 95 | // ErrorResponse is a formatted error message that docker can understand 96 | type ErrorResponse struct { 97 | Err string 98 | } 99 | 100 | // NewErrorResponse creates an ErrorResponse with the provided message 101 | func NewErrorResponse(msg string) *ErrorResponse { 102 | return &ErrorResponse{Err: msg} 103 | } 104 | 105 | // Driver represent the interface a driver must fulfill. 106 | type Driver interface { 107 | Create(*CreateRequest) error 108 | List() (*ListResponse, error) 109 | Get(*GetRequest) (*GetResponse, error) 110 | Remove(*RemoveRequest) error 111 | Path(*PathRequest) (*PathResponse, error) 112 | Mount(*MountRequest) (*MountResponse, error) 113 | Unmount(*UnmountRequest) error 114 | Capabilities() *CapabilitiesResponse 115 | } 116 | 117 | // Handler forwards requests and responses between the docker daemon and the plugin. 118 | type Handler struct { 119 | driver Driver 120 | sdk.Handler 121 | } 122 | 123 | // NewHandler initializes the request handler with a driver implementation. 124 | func NewHandler(driver Driver) *Handler { 125 | h := &Handler{driver, sdk.NewHandler(manifest)} 126 | h.initMux() 127 | return h 128 | } 129 | 130 | func (h *Handler) initMux() { 131 | h.HandleFunc(createPath, func(w http.ResponseWriter, r *http.Request) { 132 | req := &CreateRequest{} 133 | err := sdk.DecodeRequest(w, r, req) 134 | if err != nil { 135 | return 136 | } 137 | err = h.driver.Create(req) 138 | if err != nil { 139 | sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) 140 | return 141 | } 142 | sdk.EncodeResponse(w, struct{}{}, false) 143 | }) 144 | h.HandleFunc(removePath, func(w http.ResponseWriter, r *http.Request) { 145 | req := &RemoveRequest{} 146 | err := sdk.DecodeRequest(w, r, req) 147 | if err != nil { 148 | return 149 | } 150 | err = h.driver.Remove(req) 151 | if err != nil { 152 | sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) 153 | return 154 | } 155 | sdk.EncodeResponse(w, struct{}{}, false) 156 | }) 157 | h.HandleFunc(mountPath, func(w http.ResponseWriter, r *http.Request) { 158 | req := &MountRequest{} 159 | err := sdk.DecodeRequest(w, r, req) 160 | if err != nil { 161 | return 162 | } 163 | res, err := h.driver.Mount(req) 164 | if err != nil { 165 | sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) 166 | return 167 | } 168 | sdk.EncodeResponse(w, res, false) 169 | }) 170 | h.HandleFunc(hostVirtualPath, func(w http.ResponseWriter, r *http.Request) { 171 | req := &PathRequest{} 172 | err := sdk.DecodeRequest(w, r, req) 173 | if err != nil { 174 | return 175 | } 176 | res, err := h.driver.Path(req) 177 | if err != nil { 178 | sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) 179 | return 180 | } 181 | sdk.EncodeResponse(w, res, false) 182 | }) 183 | h.HandleFunc(getPath, func(w http.ResponseWriter, r *http.Request) { 184 | req := &GetRequest{} 185 | err := sdk.DecodeRequest(w, r, req) 186 | if err != nil { 187 | return 188 | } 189 | res, err := h.driver.Get(req) 190 | if err != nil { 191 | sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) 192 | return 193 | } 194 | sdk.EncodeResponse(w, res, false) 195 | }) 196 | h.HandleFunc(unmountPath, func(w http.ResponseWriter, r *http.Request) { 197 | req := &UnmountRequest{} 198 | err := sdk.DecodeRequest(w, r, req) 199 | if err != nil { 200 | return 201 | } 202 | err = h.driver.Unmount(req) 203 | if err != nil { 204 | sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) 205 | return 206 | } 207 | sdk.EncodeResponse(w, struct{}{}, false) 208 | }) 209 | h.HandleFunc(listPath, func(w http.ResponseWriter, r *http.Request) { 210 | res, err := h.driver.List() 211 | if err != nil { 212 | sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) 213 | return 214 | } 215 | sdk.EncodeResponse(w, res, false) 216 | }) 217 | 218 | h.HandleFunc(capabilitiesPath, func(w http.ResponseWriter, r *http.Request) { 219 | sdk.EncodeResponse(w, h.driver.Capabilities(), false) 220 | }) 221 | } 222 | -------------------------------------------------------------------------------- /network/api_test.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "os" 9 | "strings" 10 | "testing" 11 | "time" 12 | 13 | "github.com/docker/go-plugins-helpers/sdk" 14 | ) 15 | 16 | type TestDriver struct { 17 | Driver 18 | } 19 | 20 | func (t *TestDriver) GetCapabilities() (*CapabilitiesResponse, error) { 21 | return &CapabilitiesResponse{Scope: LocalScope, ConnectivityScope: GlobalScope}, nil 22 | } 23 | 24 | func (t *TestDriver) CreateNetwork(r *CreateNetworkRequest) error { 25 | return nil 26 | } 27 | 28 | func (t *TestDriver) DeleteNetwork(r *DeleteNetworkRequest) error { 29 | return nil 30 | } 31 | 32 | func (t *TestDriver) CreateEndpoint(r *CreateEndpointRequest) (*CreateEndpointResponse, error) { 33 | return &CreateEndpointResponse{}, nil 34 | } 35 | 36 | func (t *TestDriver) DeleteEndpoint(r *DeleteEndpointRequest) error { 37 | return nil 38 | } 39 | 40 | func (t *TestDriver) Join(r *JoinRequest) (*JoinResponse, error) { 41 | return &JoinResponse{}, nil 42 | } 43 | 44 | func (t *TestDriver) Leave(r *LeaveRequest) error { 45 | return nil 46 | } 47 | 48 | func (t *TestDriver) ProgramExternalConnectivity(r *ProgramExternalConnectivityRequest) error { 49 | i := r.Options["com.docker.network.endpoint.exposedports"] 50 | epl, ok := i.([]interface{}) 51 | if !ok { 52 | return fmt.Errorf("invalid data in request: %v (%T)", i, i) 53 | } 54 | ep, ok := epl[0].(map[string]interface{}) 55 | if !ok { 56 | return fmt.Errorf("invalid data in request: %v (%T)", epl[0], epl[0]) 57 | } 58 | if ep["Proto"].(float64) != 6 || ep["Port"].(float64) != 70 { 59 | return fmt.Errorf("Unexpected exposed ports in request: %v", ep) 60 | } 61 | return nil 62 | } 63 | 64 | func (t *TestDriver) RevokeExternalConnectivity(r *RevokeExternalConnectivityRequest) error { 65 | return nil 66 | } 67 | 68 | type ErrDriver struct { 69 | Driver 70 | } 71 | 72 | func (e *ErrDriver) GetCapabilities() (*CapabilitiesResponse, error) { 73 | return nil, fmt.Errorf("I CAN HAZ ERRORZ") 74 | } 75 | 76 | func (e *ErrDriver) CreateNetwork(r *CreateNetworkRequest) error { 77 | return fmt.Errorf("I CAN HAZ ERRORZ") 78 | } 79 | 80 | func (e *ErrDriver) DeleteNetwork(r *DeleteNetworkRequest) error { 81 | return errors.New("I CAN HAZ ERRORZ") 82 | } 83 | 84 | func (e *ErrDriver) CreateEndpoint(r *CreateEndpointRequest) (*CreateEndpointResponse, error) { 85 | return nil, errors.New("I CAN HAZ ERRORZ") 86 | } 87 | 88 | func (e *ErrDriver) DeleteEndpoint(r *DeleteEndpointRequest) error { 89 | return errors.New("I CAN HAZ ERRORZ") 90 | } 91 | 92 | func (e *ErrDriver) Join(r *JoinRequest) (*JoinResponse, error) { 93 | return nil, errors.New("I CAN HAZ ERRORZ") 94 | } 95 | 96 | func (e *ErrDriver) Leave(r *LeaveRequest) error { 97 | return errors.New("I CAN HAZ ERRORZ") 98 | } 99 | 100 | func callURL(url string) { 101 | c := http.Client{ 102 | Timeout: 10 * time.Millisecond, 103 | } 104 | res := make(chan interface{}, 1) 105 | go func() { 106 | for { 107 | _, err := c.Get(url) 108 | if err == nil { 109 | res <- nil 110 | } 111 | } 112 | }() 113 | 114 | select { 115 | case <-res: 116 | return 117 | case <-time.After(5 * time.Second): 118 | fmt.Printf("Timeout connecting to %s\n", url) 119 | os.Exit(1) 120 | } 121 | } 122 | 123 | func TestMain(m *testing.M) { 124 | d := &TestDriver{} 125 | h1 := NewHandler(d) 126 | go h1.ServeTCP("test", "localhost:32234", "", nil) 127 | 128 | e := &ErrDriver{} 129 | h2 := NewHandler(e) 130 | go h2.ServeTCP("err", "localhost:32567", "", nil) 131 | 132 | // Test that the ServeTCP is ready for use. 133 | callURL("http://localhost:32234/Plugin.Activate") 134 | callURL("http://localhost:32567/Plugin.Activate") 135 | 136 | os.Exit(m.Run()) 137 | } 138 | 139 | func TestActivate(t *testing.T) { 140 | response, err := http.Get("http://localhost:32234/Plugin.Activate") 141 | if err != nil { 142 | t.Fatal(err) 143 | } 144 | defer response.Body.Close() 145 | body, err := io.ReadAll(response.Body) 146 | 147 | if string(body) != manifest+"\n" { 148 | t.Fatalf("Expected %s, got %s\n", manifest+"\n", string(body)) 149 | } 150 | } 151 | 152 | func TestCapabilitiesExchange(t *testing.T) { 153 | response, err := http.Get("http://localhost:32234/NetworkDriver.GetCapabilities") 154 | if err != nil { 155 | t.Fatal(err) 156 | } 157 | defer response.Body.Close() 158 | body, err := io.ReadAll(response.Body) 159 | 160 | expected := `{"Scope":"local","ConnectivityScope":"global"}` 161 | if string(body) != expected+"\n" { 162 | t.Fatalf("Expected %s, got %s\n", expected+"\n", string(body)) 163 | } 164 | } 165 | 166 | func TestCreateNetworkSuccess(t *testing.T) { 167 | request := `{"NetworkID":"d76cfa51738e8a12c5eca71ee69e9d65010a4b48eaad74adab439be7e61b9aaf","Options":{"com.docker.network.generic":{}},"IPv4Data":[{"AddressSpace":"","Gateway":"172.18.0.1/16","Pool":"172.18.0.0/16"}],"IPv6Data":[]}` 168 | 169 | response, err := http.Post("http://localhost:32234/NetworkDriver.CreateNetwork", 170 | sdk.DefaultContentTypeV1_1, 171 | strings.NewReader(request), 172 | ) 173 | if err != nil { 174 | t.Fatal(err) 175 | } 176 | defer response.Body.Close() 177 | body, err := io.ReadAll(response.Body) 178 | 179 | if response.StatusCode != http.StatusOK { 180 | t.Fatalf("Expected 200, got %d\n", response.StatusCode) 181 | } 182 | if string(body) != "{}\n" { 183 | t.Fatalf("Expected %s, got %s\n", "{}\n", string(body)) 184 | } 185 | } 186 | 187 | func TestCreateNetworkError(t *testing.T) { 188 | request := `{"NetworkID":"d76cfa51738e8a12c5eca71ee69e9d65010a4b48eaad74adab439be7e61b9aaf","Options":{"com.docker.network.generic": {}},"IPv4Data":[{"AddressSpace":"","Gateway":"172.18.0.1/16","Pool":"172.18.0.0/16"}],"IPv6Data":[]}` 189 | response, err := http.Post("http://localhost:32567/NetworkDriver.CreateNetwork", 190 | sdk.DefaultContentTypeV1_1, 191 | strings.NewReader(request)) 192 | if err != nil { 193 | t.Fatal(err) 194 | } 195 | defer response.Body.Close() 196 | body, err := io.ReadAll(response.Body) 197 | 198 | if response.StatusCode != http.StatusInternalServerError { 199 | t.Fatalf("Expected 500, got %d\n", response.StatusCode) 200 | } 201 | if string(body) != "{\"Err\":\"I CAN HAZ ERRORZ\"}\n" { 202 | t.Fatalf("Expected %s, got %s\n", "{\"Err\":\"I CAN HAZ ERRORZ\"}\n", string(body)) 203 | } 204 | } 205 | 206 | func TestProgramExternalConnectivity(t *testing.T) { 207 | request := `{"NetworkID":"d76cfa51738e8a12c5eca71ee69e9d65010a4b48eaad74adab439be7e61b9aaf","EndpointID":"abccfa51738e8a12c5eca71ee69e9d65010a4b48eaad74adab439be7e61b9aaf","Options":{"com.docker.network.endpoint.exposedports":[{"Proto":6,"Port":70}],"com.docker.network.portmap":[{"Proto":6,"IP":"","Port":70,"HostIP":"","HostPort":7000,"HostPortEnd":7000}]}}` 208 | response, err := http.Post("http://localhost:32234/NetworkDriver.ProgramExternalConnectivity", 209 | sdk.DefaultContentTypeV1_1, 210 | strings.NewReader(request)) 211 | if err != nil { 212 | t.Fatal(err) 213 | } 214 | defer response.Body.Close() 215 | 216 | if response.StatusCode != http.StatusOK { 217 | body, _ := io.ReadAll(response.Body) 218 | t.Fatalf("Expected %d, got %d: %s\n", http.StatusOK, response.StatusCode, string(body)) 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /network/api.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/docker/go-plugins-helpers/sdk" 7 | ) 8 | 9 | const ( 10 | manifest = `{"Implements": ["NetworkDriver"]}` 11 | // LocalScope is the correct scope response for a local scope driver 12 | LocalScope = `local` 13 | // GlobalScope is the correct scope response for a global scope driver 14 | GlobalScope = `global` 15 | 16 | capabilitiesPath = "/NetworkDriver.GetCapabilities" 17 | allocateNetworkPath = "/NetworkDriver.AllocateNetwork" 18 | freeNetworkPath = "/NetworkDriver.FreeNetwork" 19 | createNetworkPath = "/NetworkDriver.CreateNetwork" 20 | deleteNetworkPath = "/NetworkDriver.DeleteNetwork" 21 | createEndpointPath = "/NetworkDriver.CreateEndpoint" 22 | endpointInfoPath = "/NetworkDriver.EndpointOperInfo" 23 | deleteEndpointPath = "/NetworkDriver.DeleteEndpoint" 24 | joinPath = "/NetworkDriver.Join" 25 | leavePath = "/NetworkDriver.Leave" 26 | discoverNewPath = "/NetworkDriver.DiscoverNew" 27 | discoverDeletePath = "/NetworkDriver.DiscoverDelete" 28 | programExtConnPath = "/NetworkDriver.ProgramExternalConnectivity" 29 | revokeExtConnPath = "/NetworkDriver.RevokeExternalConnectivity" 30 | ) 31 | 32 | // Driver represent the interface a driver must fulfill. 33 | type Driver interface { 34 | GetCapabilities() (*CapabilitiesResponse, error) 35 | CreateNetwork(*CreateNetworkRequest) error 36 | AllocateNetwork(*AllocateNetworkRequest) (*AllocateNetworkResponse, error) 37 | DeleteNetwork(*DeleteNetworkRequest) error 38 | FreeNetwork(*FreeNetworkRequest) error 39 | CreateEndpoint(*CreateEndpointRequest) (*CreateEndpointResponse, error) 40 | DeleteEndpoint(*DeleteEndpointRequest) error 41 | EndpointInfo(*InfoRequest) (*InfoResponse, error) 42 | Join(*JoinRequest) (*JoinResponse, error) 43 | Leave(*LeaveRequest) error 44 | DiscoverNew(*DiscoveryNotification) error 45 | DiscoverDelete(*DiscoveryNotification) error 46 | ProgramExternalConnectivity(*ProgramExternalConnectivityRequest) error 47 | RevokeExternalConnectivity(*RevokeExternalConnectivityRequest) error 48 | } 49 | 50 | // CapabilitiesResponse returns whether or not this network is global or local 51 | type CapabilitiesResponse struct { 52 | Scope string 53 | ConnectivityScope string 54 | } 55 | 56 | // AllocateNetworkRequest requests allocation of new network by manager 57 | type AllocateNetworkRequest struct { 58 | // A network ID that remote plugins are expected to store for future 59 | // reference. 60 | NetworkID string 61 | 62 | // A free form map->object interface for communication of options. 63 | Options map[string]string 64 | 65 | // IPAMData contains the address pool information for this network 66 | IPv4Data, IPv6Data []IPAMData 67 | } 68 | 69 | // AllocateNetworkResponse is the response to the AllocateNetworkRequest. 70 | type AllocateNetworkResponse struct { 71 | // A free form plugin specific string->string object to be sent in 72 | // CreateNetworkRequest call in the libnetwork agents 73 | Options map[string]string 74 | } 75 | 76 | // FreeNetworkRequest is the request to free allocated network in the manager 77 | type FreeNetworkRequest struct { 78 | // The ID of the network to be freed. 79 | NetworkID string 80 | } 81 | 82 | // CreateNetworkRequest is sent by the daemon when a network needs to be created 83 | type CreateNetworkRequest struct { 84 | NetworkID string 85 | Options map[string]interface{} 86 | IPv4Data []*IPAMData 87 | IPv6Data []*IPAMData 88 | } 89 | 90 | // IPAMData contains IPv4 or IPv6 addressing information 91 | type IPAMData struct { 92 | AddressSpace string 93 | Pool string 94 | Gateway string 95 | AuxAddresses map[string]interface{} 96 | } 97 | 98 | // DeleteNetworkRequest is sent by the daemon when a network needs to be removed 99 | type DeleteNetworkRequest struct { 100 | NetworkID string 101 | } 102 | 103 | // CreateEndpointRequest is sent by the daemon when an endpoint should be created 104 | type CreateEndpointRequest struct { 105 | NetworkID string 106 | EndpointID string 107 | Interface *EndpointInterface 108 | Options map[string]interface{} 109 | } 110 | 111 | // CreateEndpointResponse is sent as a response to a CreateEndpointRequest 112 | type CreateEndpointResponse struct { 113 | Interface *EndpointInterface 114 | } 115 | 116 | // EndpointInterface contains endpoint interface information 117 | type EndpointInterface struct { 118 | Address string 119 | AddressIPv6 string 120 | MacAddress string 121 | } 122 | 123 | // DeleteEndpointRequest is sent by the daemon when an endpoint needs to be removed 124 | type DeleteEndpointRequest struct { 125 | NetworkID string 126 | EndpointID string 127 | } 128 | 129 | // InterfaceName consists of the name of the interface in the global netns and 130 | // the desired prefix to be appended to the interface inside the container netns 131 | type InterfaceName struct { 132 | SrcName string 133 | DstPrefix string 134 | } 135 | 136 | // InfoRequest is send by the daemon when querying endpoint information 137 | type InfoRequest struct { 138 | NetworkID string 139 | EndpointID string 140 | } 141 | 142 | // InfoResponse is endpoint information sent in response to an InfoRequest 143 | type InfoResponse struct { 144 | Value map[string]string 145 | } 146 | 147 | // JoinRequest is sent by the Daemon when an endpoint needs be joined to a network 148 | type JoinRequest struct { 149 | NetworkID string 150 | EndpointID string 151 | SandboxKey string 152 | Options map[string]interface{} 153 | } 154 | 155 | // StaticRoute contains static route information 156 | type StaticRoute struct { 157 | Destination string 158 | RouteType int 159 | NextHop string 160 | } 161 | 162 | // JoinResponse is sent in response to a JoinRequest 163 | type JoinResponse struct { 164 | InterfaceName InterfaceName 165 | Gateway string 166 | GatewayIPv6 string 167 | StaticRoutes []*StaticRoute 168 | DisableGatewayService bool 169 | } 170 | 171 | // LeaveRequest is send by the daemon when a endpoint is leaving a network 172 | type LeaveRequest struct { 173 | NetworkID string 174 | EndpointID string 175 | } 176 | 177 | // ErrorResponse is a formatted error message that libnetwork can understand 178 | type ErrorResponse struct { 179 | Err string 180 | } 181 | 182 | // DiscoveryNotification is sent by the daemon when a new discovery event occurs 183 | type DiscoveryNotification struct { 184 | DiscoveryType int 185 | DiscoveryData interface{} 186 | } 187 | 188 | // ProgramExternalConnectivityRequest specifies the L4 data 189 | // and the endpoint for which programming has to be done 190 | type ProgramExternalConnectivityRequest struct { 191 | NetworkID string 192 | EndpointID string 193 | Options map[string]interface{} 194 | } 195 | 196 | // RevokeExternalConnectivityRequest specifies the endpoint 197 | // for which the L4 programming has to be removed 198 | type RevokeExternalConnectivityRequest struct { 199 | NetworkID string 200 | EndpointID string 201 | } 202 | 203 | // NewErrorResponse creates an ErrorResponse with the provided message 204 | func NewErrorResponse(msg string) *ErrorResponse { 205 | return &ErrorResponse{Err: msg} 206 | } 207 | 208 | // Handler forwards requests and responses between the docker daemon and the plugin. 209 | type Handler struct { 210 | driver Driver 211 | sdk.Handler 212 | } 213 | 214 | // NewHandler initializes the request handler with a driver implementation. 215 | func NewHandler(driver Driver) *Handler { 216 | h := &Handler{driver, sdk.NewHandler(manifest)} 217 | h.initMux() 218 | return h 219 | } 220 | 221 | func (h *Handler) initMux() { 222 | h.HandleFunc(capabilitiesPath, func(w http.ResponseWriter, r *http.Request) { 223 | res, err := h.driver.GetCapabilities() 224 | if err != nil { 225 | sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) 226 | return 227 | } 228 | if res == nil { 229 | sdk.EncodeResponse(w, NewErrorResponse("Network driver must implement GetCapabilities"), true) 230 | return 231 | } 232 | sdk.EncodeResponse(w, res, false) 233 | }) 234 | h.HandleFunc(createNetworkPath, func(w http.ResponseWriter, r *http.Request) { 235 | req := &CreateNetworkRequest{} 236 | err := sdk.DecodeRequest(w, r, req) 237 | if err != nil { 238 | return 239 | } 240 | err = h.driver.CreateNetwork(req) 241 | if err != nil { 242 | sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) 243 | return 244 | } 245 | sdk.EncodeResponse(w, struct{}{}, false) 246 | }) 247 | h.HandleFunc(allocateNetworkPath, func(w http.ResponseWriter, r *http.Request) { 248 | req := &AllocateNetworkRequest{} 249 | err := sdk.DecodeRequest(w, r, req) 250 | if err != nil { 251 | return 252 | } 253 | res, err := h.driver.AllocateNetwork(req) 254 | if err != nil { 255 | sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) 256 | return 257 | } 258 | sdk.EncodeResponse(w, res, false) 259 | }) 260 | h.HandleFunc(deleteNetworkPath, func(w http.ResponseWriter, r *http.Request) { 261 | req := &DeleteNetworkRequest{} 262 | err := sdk.DecodeRequest(w, r, req) 263 | if err != nil { 264 | return 265 | } 266 | err = h.driver.DeleteNetwork(req) 267 | if err != nil { 268 | sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) 269 | return 270 | } 271 | sdk.EncodeResponse(w, struct{}{}, false) 272 | }) 273 | h.HandleFunc(freeNetworkPath, func(w http.ResponseWriter, r *http.Request) { 274 | req := &FreeNetworkRequest{} 275 | err := sdk.DecodeRequest(w, r, req) 276 | if err != nil { 277 | return 278 | } 279 | err = h.driver.FreeNetwork(req) 280 | if err != nil { 281 | sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) 282 | return 283 | } 284 | sdk.EncodeResponse(w, struct{}{}, false) 285 | }) 286 | h.HandleFunc(createEndpointPath, func(w http.ResponseWriter, r *http.Request) { 287 | req := &CreateEndpointRequest{} 288 | err := sdk.DecodeRequest(w, r, req) 289 | if err != nil { 290 | return 291 | } 292 | res, err := h.driver.CreateEndpoint(req) 293 | if err != nil { 294 | sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) 295 | return 296 | } 297 | sdk.EncodeResponse(w, res, false) 298 | }) 299 | h.HandleFunc(deleteEndpointPath, func(w http.ResponseWriter, r *http.Request) { 300 | req := &DeleteEndpointRequest{} 301 | err := sdk.DecodeRequest(w, r, req) 302 | if err != nil { 303 | return 304 | } 305 | err = h.driver.DeleteEndpoint(req) 306 | if err != nil { 307 | sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) 308 | return 309 | } 310 | sdk.EncodeResponse(w, struct{}{}, false) 311 | }) 312 | h.HandleFunc(endpointInfoPath, func(w http.ResponseWriter, r *http.Request) { 313 | req := &InfoRequest{} 314 | err := sdk.DecodeRequest(w, r, req) 315 | if err != nil { 316 | return 317 | } 318 | res, err := h.driver.EndpointInfo(req) 319 | if err != nil { 320 | sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) 321 | return 322 | } 323 | sdk.EncodeResponse(w, res, false) 324 | }) 325 | h.HandleFunc(joinPath, func(w http.ResponseWriter, r *http.Request) { 326 | req := &JoinRequest{} 327 | err := sdk.DecodeRequest(w, r, req) 328 | if err != nil { 329 | return 330 | } 331 | res, err := h.driver.Join(req) 332 | if err != nil { 333 | sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) 334 | return 335 | } 336 | sdk.EncodeResponse(w, res, false) 337 | }) 338 | h.HandleFunc(leavePath, func(w http.ResponseWriter, r *http.Request) { 339 | req := &LeaveRequest{} 340 | err := sdk.DecodeRequest(w, r, req) 341 | if err != nil { 342 | return 343 | } 344 | err = h.driver.Leave(req) 345 | if err != nil { 346 | sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) 347 | return 348 | } 349 | sdk.EncodeResponse(w, struct{}{}, false) 350 | }) 351 | h.HandleFunc(discoverNewPath, func(w http.ResponseWriter, r *http.Request) { 352 | req := &DiscoveryNotification{} 353 | err := sdk.DecodeRequest(w, r, req) 354 | if err != nil { 355 | return 356 | } 357 | err = h.driver.DiscoverNew(req) 358 | if err != nil { 359 | sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) 360 | return 361 | } 362 | sdk.EncodeResponse(w, struct{}{}, false) 363 | }) 364 | h.HandleFunc(discoverDeletePath, func(w http.ResponseWriter, r *http.Request) { 365 | req := &DiscoveryNotification{} 366 | err := sdk.DecodeRequest(w, r, req) 367 | if err != nil { 368 | return 369 | } 370 | err = h.driver.DiscoverDelete(req) 371 | if err != nil { 372 | sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) 373 | return 374 | } 375 | sdk.EncodeResponse(w, struct{}{}, false) 376 | }) 377 | h.HandleFunc(programExtConnPath, func(w http.ResponseWriter, r *http.Request) { 378 | req := &ProgramExternalConnectivityRequest{} 379 | err := sdk.DecodeRequest(w, r, req) 380 | if err != nil { 381 | return 382 | } 383 | err = h.driver.ProgramExternalConnectivity(req) 384 | if err != nil { 385 | sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) 386 | return 387 | } 388 | sdk.EncodeResponse(w, struct{}{}, false) 389 | }) 390 | h.HandleFunc(revokeExtConnPath, func(w http.ResponseWriter, r *http.Request) { 391 | req := &RevokeExternalConnectivityRequest{} 392 | err := sdk.DecodeRequest(w, r, req) 393 | if err != nil { 394 | return 395 | } 396 | err = h.driver.RevokeExternalConnectivity(req) 397 | if err != nil { 398 | sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) 399 | return 400 | } 401 | sdk.EncodeResponse(w, struct{}{}, false) 402 | }) 403 | } 404 | --------------------------------------------------------------------------------