diff options
42 files changed, 4040 insertions, 0 deletions
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..4319d63 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,21 @@ +name: CI +on: + push: + branches: + - main + - master + pull_request: + branches: + - main + - master +jobs: + test: + strategy: + matrix: + platform: [ubuntu-20.04] + runs-on: ${{ matrix.platform }} + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: test + run: make test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..daf913b --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..ff294d8 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,57 @@ +# Sign your work + +The sign-off is a simple line at the end of the explanation for the patch. Your +signature certifies that you wrote the patch or otherwise have the right to pass +it on as an open-source patch. The rules are pretty simple: if you can certify +the below (from [developercertificate.org](http://developercertificate.org/)): + +``` +Developer Certificate of Origin +Version 1.1 + +Copyright (C) 2004, 2006 The Linux Foundation and its contributors. +660 York Street, Suite 102, +San Francisco, CA 94110 USA + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + +Developer's Certificate of Origin 1.1 + +By making a contribution to this project, I certify that: + +(a) The contribution was created in whole or in part by me and I + have the right to submit it under the open source license + indicated in the file; or + +(b) The contribution is based upon previous work that, to the best + of my knowledge, is covered under an appropriate open source + license and I have the right under that license to submit that + work with modifications, whether created in whole or in part + by me, under the same open source license (unless I am + permitted to submit under a different license), as indicated + in the file; or + +(c) The contribution was provided directly to me by some other + person who certified (a), (b) or (c) and I have not modified + it. + +(d) I understand and agree that this project and the contribution + are public and that a record of the contribution (including all + personal information I submit with it, including my sign-off) is + maintained indefinitely and may be redistributed consistent with + this project or the open source license(s) involved. +``` + +Then you just add a line to every git commit message: + + Signed-off-by: Joe Smith <joe.smith@email.com> + +Use your real name (sorry, no pseudonyms or anonymous contributions.) + +If you set your `user.name` and `user.email` git configs, you can sign your +commit automatically with `git commit -s`. + +Note that the old-style `Docker-DCO-1.1-Signed-off-by: ...` format is still +accepted, so there is no need to update outstanding pull requests to the new +format right away, but please do adjust your processes for future contributions. @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/MAINTAINERS b/MAINTAINERS new file mode 100644 index 0000000..264ed1b --- /dev/null +++ b/MAINTAINERS @@ -0,0 +1,39 @@ +# go-plugins-helpers maintainers file +# +# This file describes who runs the docker/go-plugins-helpers project and how. +# This is a living document - if you see something out of date or missing, speak up! +# +# It is structured to be consumable by both humans and programs. +# To extract its contents programmatically, use any TOML-compliant parser. +# +# This file is compiled into the MAINTAINERS file in docker/opensource. +# +[Org] + [Org."Core maintainers"] + people = [ + "calavera", + "dave-tucker", + "runcom", + ] + +[people] + +# A reference list of all people associated with the project. +# All other sections should refer to people by their canonical key +# in the people section. + + # ADD YOURSELF HERE IN ALPHABETICAL ORDER + [people.calavera] + Name = "David Calavera" + Email = "david.calavera@gmail.com" + GitHub = "calavera" + + [people.dave-tucker] + Name = "Dave Tucker" + Email = "dt@docker.com" + GitHub = "dave-tucker" + + [people.runcom] + Name = "Antonio Murdaca" + Email = "runcom@redhat.com" + GitHub = "runcom" diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..efaab6a --- /dev/null +++ b/Makefile @@ -0,0 +1,33 @@ +.PHONY: all test test-local install-deps lint fmt vet + +REPO_NAME = go-plugins-helpers +REPO_OWNER = docker +PKG_NAME = github.com/${REPO_OWNER}/${REPO_NAME} +IMAGE = golang:1.16 + +all: test + +test-local: install-deps fmt lint vet + @echo "+ $@" + @go test -v ./... + +test: + @docker run -e GO111MODULE=off -v ${shell pwd}:/go/src/${PKG_NAME} -w /go/src/${PKG_NAME} ${IMAGE} make test-local + +install-deps: + @echo "+ $@" + @go get -u golang.org/x/lint/golint + @go get -d -t ./... + +lint: + @echo "+ $@" + @test -z "$$(golint ./... | tee /dev/stderr)" + +fmt: + @echo "+ $@" + @test -z "$$(gofmt -s -l . | tee /dev/stderr)" + +vet: + @echo "+ $@" + @go vet ./... + @@ -0,0 +1,19 @@ +Docker +Copyright 2012-2015 Docker, Inc. + +This product includes software developed at Docker, Inc. (https://www.docker.com). + +This product contains software (https://github.com/kr/pty) developed +by Keith Rarick, licensed under the MIT License. + +The following is courtesy of our legal counsel: + + +Use and transfer of Docker may be subject to certain restrictions by the +United States and other governments. +It is your responsibility to ensure that your use and/or transfer does not +violate applicable laws. + +For more information, please see https://www.bis.doc.gov + +See also https://www.apache.org/dev/crypto.html and/or seek legal counsel. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0c4fc45 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# go-plugins-helpers + +A collection of helper packages to extend Docker Engine in Go + + Plugin type | Documentation | Description + --------------|---------------|-------------------------------------------------- + Authorization | [Link](https://docs.docker.com/engine/extend/authorization/) | Extend API authorization mechanism + Network | [Link](https://docs.docker.com/engine/extend/plugins_network/) | Extend network management + Volume | [Link](https://docs.docker.com/engine/extend/plugins_volume/) | Extend persistent storage + IPAM | [Link](https://github.com/docker/libnetwork/blob/master/docs/ipam.md) | Extend IP address management + Graph (experimental) | [Link](https://github.com/docker/cli/blob/master/docs/extend/plugins_graphdriver.md)| Extend image and container fs storage + +See the [understand Docker plugins documentation section](https://docs.docker.com/engine/extend/plugins/). diff --git a/authorization/README.md b/authorization/README.md new file mode 100644 index 0000000..4f9384c --- /dev/null +++ b/authorization/README.md @@ -0,0 +1,41 @@ +# Docker authorization extension api. + +Go handler to create external authorization extensions for Docker. + +## Usage + +This library is designed to be integrated in your program. + +1. Implement the `authorization.Plugin` interface. +2. Initialize a `authorization.Handler` with your implementation. +3. Call either `ServeTCP` or `ServeUnix` from the `authorization.Handler`. + +### Example using TCP sockets: + +```go + p := MyAuthZPlugin{} + h := authorization.NewHandler(p) + h.ServeTCP("test_plugin", ":8080") +``` + +### Example using Unix sockets: + +```go + p := MyAuthZPlugin{} + h := authorization.NewHandler(p) + u, _ := user.Lookup("root") + gid, _ := strconv.Atoi(u.Gid) + h.ServeUnix("test_plugin", gid) +``` + +## Full example plugins + +- https://github.com/projectatomic/docker-novolume-plugin +- https://github.com/cpdevws/img-authz-plugin +- https://github.com/casbin/casbin-authz-plugin +- https://github.com/kassisol/hbm +- https://github.com/leogr/docker-authz-plugin + +## License + +MIT diff --git a/authorization/api.go b/authorization/api.go new file mode 100644 index 0000000..7e157f7 --- /dev/null +++ b/authorization/api.go @@ -0,0 +1,143 @@ +package authorization + +import ( + "crypto/x509" + "encoding/json" + "encoding/pem" + "net/http" + + "github.com/docker/go-plugins-helpers/sdk" +) + +const ( + // AuthZApiRequest is the url for daemon request authorization + AuthZApiRequest = "AuthZPlugin.AuthZReq" + + // AuthZApiResponse is the url for daemon response authorization + AuthZApiResponse = "AuthZPlugin.AuthZRes" + + // AuthZApiImplements is the name of the interface all AuthZ plugins implement + AuthZApiImplements = "authz" + + manifest = `{"Implements": ["` + AuthZApiImplements + `"]}` + reqPath = "/" + AuthZApiRequest + resPath = "/" + AuthZApiResponse +) + +// PeerCertificate is a wrapper around x509.Certificate which provides a sane +// encoding/decoding to/from PEM format and JSON. +type PeerCertificate x509.Certificate + +// MarshalJSON returns the JSON encoded pem bytes of a PeerCertificate. +func (pc *PeerCertificate) MarshalJSON() ([]byte, error) { + b := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: pc.Raw}) + return json.Marshal(b) +} + +// UnmarshalJSON populates a new PeerCertificate struct from JSON data. +func (pc *PeerCertificate) UnmarshalJSON(b []byte) error { + var buf []byte + if err := json.Unmarshal(b, &buf); err != nil { + return err + } + derBytes, _ := pem.Decode(buf) + c, err := x509.ParseCertificate(derBytes.Bytes) + if err != nil { + return err + } + *pc = PeerCertificate(*c) + return nil +} + +// Request holds data required for authZ plugins +type Request struct { + // User holds the user extracted by AuthN mechanism + User string `json:"User,omitempty"` + + // UserAuthNMethod holds the mechanism used to extract user details (e.g., krb) + UserAuthNMethod string `json:"UserAuthNMethod,omitempty"` + + // RequestMethod holds the HTTP method (GET/POST/PUT) + RequestMethod string `json:"RequestMethod,omitempty"` + + // RequestUri holds the full HTTP uri (e.g., /v1.21/version) + RequestURI string `json:"RequestUri,omitempty"` + + // RequestBody stores the raw request body sent to the docker daemon + RequestBody []byte `json:"RequestBody,omitempty"` + + // RequestHeaders stores the raw request headers sent to the docker daemon + RequestHeaders map[string]string `json:"RequestHeaders,omitempty"` + + // RequestPeerCertificates stores the request's TLS peer certificates in PEM format + RequestPeerCertificates []*PeerCertificate `json:"RequestPeerCertificates,omitempty"` + + // ResponseStatusCode stores the status code returned from docker daemon + ResponseStatusCode int `json:"ResponseStatusCode,omitempty"` + + // ResponseBody stores the raw response body sent from docker daemon + ResponseBody []byte `json:"ResponseBody,omitempty"` + + // ResponseHeaders stores the response headers sent to the docker daemon + ResponseHeaders map[string]string `json:"ResponseHeaders,omitempty"` +} + +// Response represents authZ plugin response +type Response struct { + // Allow indicating whether the user is allowed or not + Allow bool `json:"Allow"` + + // Msg stores the authorization message + Msg string `json:"Msg,omitempty"` + + // Err stores a message in case there's an error + Err string `json:"Err,omitempty"` +} + +// Plugin represent the interface a plugin must fulfill. +type Plugin interface { + AuthZReq(Request) Response + AuthZRes(Request) Response +} + +// Handler forwards requests and responses between the docker daemon and the plugin. +type Handler struct { + plugin Plugin + sdk.Handler +} + +// NewHandler initializes the request handler with a plugin implementation. +func NewHandler(plugin Plugin) *Handler { + h := &Handler{plugin, sdk.NewHandler(manifest)} + h.initMux() + return h +} + +func (h *Handler) initMux() { + h.handle(reqPath, func(req Request) Response { + return h.plugin.AuthZReq(req) + }) + + h.handle(resPath, func(req Request) Response { + return h.plugin.AuthZRes(req) + }) +} + +type actionHandler func(Request) Response + +func (h *Handler) handle(name string, actionCall actionHandler) { + h.HandleFunc(name, func(w http.ResponseWriter, r *http.Request) { + var ( + req Request + d = json.NewDecoder(r.Body) + ) + d.UseNumber() + if err := d.Decode(&req); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + } + + res := actionCall(req) + + sdk.EncodeResponse(w, res, res.Err != "") + }) +} diff --git a/authorization/api_test.go b/authorization/api_test.go new file mode 100644 index 0000000..407c65e --- /dev/null +++ b/authorization/api_test.go @@ -0,0 +1,232 @@ +package authorization + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/json" + "fmt" + "io/ioutil" + "math/big" + "net/http" + "os" + "strings" + "testing" + "time" + + "github.com/docker/go-plugins-helpers/sdk" + "github.com/stretchr/testify/require" +) + +type TestPlugin struct { + Plugin +} + +func (p *TestPlugin) AuthZReq(r Request) Response { + return Response{ + Allow: false, + Msg: "You are not authorized", + Err: "", + } +} + +func (p *TestPlugin) AuthZRes(r Request) Response { + return Response{ + Allow: false, + Msg: "You are not authorized", + Err: "", + } +} + +func TestActivate(t *testing.T) { + response, err := http.Get("http://localhost:32456/Plugin.Activate") + + if err != nil { + t.Fatal(err) + } + + defer response.Body.Close() + + body, err := ioutil.ReadAll(response.Body) + + if err != nil { + t.Fatal(err) + } + + if string(body) != manifest+"\n" { + t.Fatalf("Expected %s, got %s\n", manifest+"\n", string(body)) + } +} + +func TestAuthZReq(t *testing.T) { + request := `{"User":"bob","UserAuthNMethod":"","RequestMethod":"POST","RequestURI":"http://127.0.0.1/v.1.23/containers/json","RequestBody":"","RequestHeader":"","RequestStatusCode":""}` + + response, err := http.Post( + "http://localhost:32456/AuthZPlugin.AuthZReq", + sdk.DefaultContentTypeV1_1, + strings.NewReader(request), + ) + + if err != nil { + t.Fatal(err) + } + + defer response.Body.Close() + + body, err := ioutil.ReadAll(response.Body) + + if err != nil { + t.Fatal(err) + } + + var r Response + if err := json.Unmarshal(body, &r); err != nil { + t.Fatal(err) + } + + if r.Msg != "You are not authorized" { + t.Fatal("Authorization message does not match") + } + + if r.Allow { + t.Fatal("The request has been allowed while should not be") + } + + if r.Err != "" { + t.Fatal("Authorization Error should be empty") + } +} + +func TestAuthZRes(t *testing.T) { + request := `{"User":"bob","UserAuthNMethod":"","RequestMethod":"POST","RequestURI":"http://127.0.0.1/v.1.23/containers/json","RequestBody":"","RequestHeader":"","RequestStatusCode":"", "ResponseBody":"","ResponseHeader":"","ResponseStatusCode":200}` + + response, err := http.Post( + "http://localhost:32456/AuthZPlugin.AuthZRes", + sdk.DefaultContentTypeV1_1, + strings.NewReader(request), + ) + + if err != nil { + t.Fatal(err) + } + + defer response.Body.Close() + + body, err := ioutil.ReadAll(response.Body) + + if err != nil { + t.Fatal(err) + } + + var r Response + if err := json.Unmarshal(body, &r); err != nil { + t.Fatal(err) + } + + if r.Msg != "You are not authorized" { + t.Fatal("Authorization message does not match") + } + + if r.Allow { + t.Fatal("The request has been allowed while should not be") + } + + if r.Err != "" { + t.Fatal("Authorization Error should be empty") + } +} + +func TestPeerCertificateMarshalJSON(t *testing.T) { + template := &x509.Certificate{ + IsCA: true, + BasicConstraintsValid: true, + SubjectKeyId: []byte{1, 2, 3}, + SerialNumber: big.NewInt(1234), + Subject: pkix.Name{ + Country: []string{"Earth"}, + Organization: []string{"Mother Nature"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(5, 5, 5), + + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + } + // generate private key + privatekey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + publickey := &privatekey.PublicKey + + // create a self-signed certificate. template = parent + var parent = template + raw, err := x509.CreateCertificate(rand.Reader, template, parent, publickey, privatekey) + require.NoError(t, err) + + cert, err := x509.ParseCertificate(raw) + require.NoError(t, err) + + var certs = []*x509.Certificate{cert} + addr := "www.authz.com/auth" + req, err := http.NewRequest("GET", addr, nil) + require.NoError(t, err) + + req.RequestURI = addr + req.TLS = &tls.ConnectionState{} + req.TLS.PeerCertificates = certs + req.Header.Add("header", "value") + + for _, c := range req.TLS.PeerCertificates { + pcObj := PeerCertificate(*c) + + t.Run("Marshalling :", func(t *testing.T) { + raw, err = pcObj.MarshalJSON() + require.NotNil(t, raw) + require.Nil(t, err) + }) + + t.Run("UnMarshalling :", func(t *testing.T) { + err := pcObj.UnmarshalJSON(raw) + require.Nil(t, err) + require.Equal(t, "Earth", pcObj.Subject.Country[0]) + require.Equal(t, true, pcObj.IsCA) + + }) + + } + +} + +func callURL(url string) { + c := http.Client{ + Timeout: 10 * time.Millisecond, + } + res := make(chan interface{}, 1) + go func() { + for { + _, err := c.Get(url) + if err == nil { + res <- nil + } + } + }() + + select { + case <-res: + return + case <-time.After(5 * time.Second): + fmt.Printf("Timeout connecting to %s\n", url) + os.Exit(1) + } +} + +func TestMain(m *testing.M) { + d := &TestPlugin{} + h := NewHandler(d) + go h.ServeTCP("test", "localhost:32456", "", nil) + + callURL("http://localhost:32456/Plugin.Activate") + + os.Exit(m.Run()) +} diff --git a/graphdriver/README.md b/graphdriver/README.md new file mode 100644 index 0000000..8c55c8a --- /dev/null +++ b/graphdriver/README.md @@ -0,0 +1,27 @@ +# Docker volume extension api. + +Go handler to create external graphdriver extensions for Docker. + +## Usage + +This library is designed to be integrated in your program. + +1. Implement the `graphdriver.Driver` interface. +2. Initialize a `graphdriver.Handler` with your implementation. +3. Call either `ServeTCP` or `ServeUnix` from the `graphdriver.Handler`. + +### Example using TCP sockets: + +```go + d := MyGraphDriver{} + h := graphdriver.NewHandler(d) + h.ServeTCP("test_graph", ":8080") +``` + +### Example using Unix sockets: + +```go + d := MyGraphDriver{} + h := graphdriver.NewHandler(d) + h.ServeUnix("root", "test_graph") +``` diff --git a/graphdriver/api.go b/graphdriver/api.go new file mode 100644 index 0000000..193f77a --- /dev/null +++ b/graphdriver/api.go @@ -0,0 +1,408 @@ +package graphdriver + +// See https://github.com/docker/cli/blob/master/docs/extend/plugins_graphdriver.md + +import ( + "io" + "net/http" + + graphDriver "github.com/docker/docker/daemon/graphdriver" + "github.com/docker/docker/pkg/containerfs" + "github.com/docker/docker/pkg/idtools" + "github.com/docker/go-plugins-helpers/sdk" +) + +const ( + // DefaultDockerRootDirectory is the default directory where graph drivers will be created. + DefaultDockerRootDirectory = "/var/lib/docker/graph" + + manifest = `{"Implements": ["GraphDriver"]}` + initPath = "/GraphDriver.Init" + createPath = "/GraphDriver.Create" + createRWPath = "/GraphDriver.CreateReadWrite" + removePath = "/GraphDriver.Remove" + getPath = "/GraphDriver.Get" + putPath = "/GraphDriver.Put" + existsPath = "/GraphDriver.Exists" + statusPath = "/GraphDriver.Status" + getMetadataPath = "/GraphDriver.GetMetadata" + cleanupPath = "/GraphDriver.Cleanup" + diffPath = "/GraphDriver.Diff" + changesPath = "/GraphDriver.Changes" + applyDiffPath = "/GraphDriver.ApplyDiff" + diffSizePath = "/GraphDriver.DiffSize" + capabilitiesPath = "/GraphDriver.Capabilities" +) + +// Init + +// InitRequest is the structure that docker's init requests are deserialized to. +type InitRequest struct { + Home string + Options []string `json:"Opts"` + UIDMaps []idtools.IDMap `json:"UIDMaps"` + GIDMaps []idtools.IDMap `json:"GIDMaps"` +} + +// Create + +// CreateRequest is the structure that docker's create requests are deserialized to. +type CreateRequest struct { + ID string + Parent string + MountLabel string + StorageOpt map[string]string +} + +// Remove + +// RemoveRequest is the structure that docker's remove requests are deserialized to. +type RemoveRequest struct { + ID string +} + +// Get + +// GetRequest is the structure that docker's get requests are deserialized to. +type GetRequest struct { + ID string + MountLabel string +} + +// GetResponse is the strucutre that docker's remove responses are serialized to. +type GetResponse struct { + Dir string +} + +// Put + +// PutRequest is the structure that docker's put requests are deserialized to. +type PutRequest struct { + ID string +} + +// Exists + +// ExistsRequest is the structure that docker's exists requests are deserialized to. +type ExistsRequest struct { + ID string +} + +// ExistsResponse is the structure that docker's exists responses are serialized to. +type ExistsResponse struct { + Exists bool +} + +// Status + +// StatusRequest is the structure that docker's status requests are deserialized to. +type StatusRequest struct{} + +// StatusResponse is the structure that docker's status responses are serialized to. +type StatusResponse struct { + Status [][2]string +} + +// GetMetadata + +// GetMetadataRequest is the structure that docker's getMetadata requests are deserialized to. +type GetMetadataRequest struct { + ID string +} + +// GetMetadataResponse is the structure that docker's getMetadata responses are serialized to. +type GetMetadataResponse struct { + Metadata map[string]string +} + +// Cleanup + +// CleanupRequest is the structure that docker's cleanup requests are deserialized to. +type CleanupRequest struct{} + +// Diff + +// DiffRequest is the structure that docker's diff requests are deserialized to. +type DiffRequest struct { + ID string + Parent string +} + +// DiffResponse is the structure that docker's diff responses are serialized to. +type DiffResponse struct { + Stream io.ReadCloser // TAR STREAM +} + +// Changes + +// ChangesRequest is the structure that docker's changes requests are deserialized to. +type ChangesRequest struct { + ID string + Parent string +} + +// ChangesResponse is the structure that docker's changes responses are serialized to. +type ChangesResponse struct { + Changes []Change +} + +// ChangeKind represents the type of change mage +type ChangeKind int + +const ( + // Modified is a ChangeKind used when an item has been modified + Modified ChangeKind = iota + // Added is a ChangeKind used when an item has been added + Added + // Deleted is a ChangeKind used when an item has been deleted + Deleted +) + +// Change is the structure that docker's individual changes are serialized to. +type Change struct { + Path string + Kind ChangeKind +} + +// ApplyDiff + +// ApplyDiffRequest is the structure that docker's applyDiff requests are deserialized to. +type ApplyDiffRequest struct { + Stream io.Reader // TAR STREAM + ID string + Parent string +} + +// ApplyDiffResponse is the structure that docker's applyDiff responses are serialized to. +type ApplyDiffResponse struct { + Size int64 +} + +// DiffSize + +// DiffSizeRequest is the structure that docker's diffSize requests are deserialized to. +type DiffSizeRequest struct { + ID string + Parent string +} + +// DiffSizeResponse is the structure that docker's diffSize responses are serialized to. +type DiffSizeResponse struct { + Size int64 +} + +// CapabilitiesRequest is the structure that docker's capabilities requests are deserialized to. +type CapabilitiesRequest struct{} + +// CapabilitiesResponse is the structure that docker's capabilities responses are serialized to. +type CapabilitiesResponse struct { + Capabilities graphDriver.Capabilities +} + +// ErrorResponse is a formatted error message that docker can understand +type ErrorResponse struct { + Err string +} + +// NewErrorResponse creates an ErrorResponse with the provided message +func NewErrorResponse(msg string) *ErrorResponse { + return &ErrorResponse{Err: msg} +} + +// Driver represent the interface a driver must fulfill. +type Driver interface { + Init(home string, options []string, uidMaps, gidMaps []idtools.IDMap) error + Create(id, parent, mountlabel string, storageOpt map[string]string) error + CreateReadWrite(id, parent, mountlabel string, storageOpt map[string]string) error + Remove(id string) error + Get(id, mountLabel string) (containerfs.ContainerFS, error) + Put(id string) error + Exists(id string) bool + Status() [][2]string + GetMetadata(id string) (map[string]string, error) + Cleanup() error + Diff(id, parent string) io.ReadCloser + Changes(id, parent string) ([]Change, error) + ApplyDiff(id, parent string, archive io.Reader) (int64, error) + DiffSize(id, parent string) (int64, error) + Capabilities() graphDriver.Capabilities +} + +// Handler forwards requests and responses between the docker daemon and the plugin. +type Handler struct { + driver Driver + sdk.Handler +} + +// NewHandler initializes the request handler with a driver implementation. +func NewHandler(driver Driver) *Handler { + h := &Handler{driver, sdk.NewHandler(manifest)} + h.initMux() + return h +} + +func (h *Handler) initMux() { + h.HandleFunc(initPath, func(w http.ResponseWriter, r *http.Request) { + req := InitRequest{} + err := sdk.DecodeRequest(w, r, &req) + if err != nil { + return + } + err = h.driver.Init(req.Home, req.Options, req.UIDMaps, req.GIDMaps) + if err != nil { + sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) + return + } + sdk.EncodeResponse(w, struct{}{}, false) + }) + h.HandleFunc(createPath, func(w http.ResponseWriter, r *http.Request) { + req := CreateRequest{} + err := sdk.DecodeRequest(w, r, &req) + if err != nil { + return + } + err = h.driver.Create(req.ID, req.Parent, req.MountLabel, req.StorageOpt) + if err != nil { + sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) + return + } + sdk.EncodeResponse(w, struct{}{}, false) + }) + h.HandleFunc(createRWPath, func(w http.ResponseWriter, r *http.Request) { + req := CreateRequest{} + err := sdk.DecodeRequest(w, r, &req) + if err != nil { + return + } + err = h.driver.CreateReadWrite(req.ID, req.Parent, req.MountLabel, req.StorageOpt) + if err != nil { + sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) + return + } + sdk.EncodeResponse(w, struct{}{}, false) + }) + h.HandleFunc(removePath, func(w http.ResponseWriter, r *http.Request) { + req := RemoveRequest{} + err := sdk.DecodeRequest(w, r, &req) + if err != nil { + return + } + err = h.driver.Remove(req.ID) + if err != nil { + sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) + return + } + sdk.EncodeResponse(w, struct{}{}, false) + + }) + h.HandleFunc(getPath, func(w http.ResponseWriter, r *http.Request) { + req := GetRequest{} + err := sdk.DecodeRequest(w, r, &req) + if err != nil { + return + } + dir, err := h.driver.Get(req.ID, req.MountLabel) + if err != nil { + sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) + return + } + sdk.EncodeResponse(w, &GetResponse{Dir: dir.Path()}, false) + }) + h.HandleFunc(putPath, func(w http.ResponseWriter, r *http.Request) { + req := PutRequest{} + err := sdk.DecodeRequest(w, r, &req) + if err != nil { + return + } + err = h.driver.Put(req.ID) + if err != nil { + sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) + return + } + sdk.EncodeResponse(w, struct{}{}, false) + }) + h.HandleFunc(existsPath, func(w http.ResponseWriter, r *http.Request) { + req := ExistsRequest{} + err := sdk.DecodeRequest(w, r, &req) + if err != nil { + return + } + exists := h.driver.Exists(req.ID) + sdk.EncodeResponse(w, &ExistsResponse{Exists: exists}, false) + }) + h.HandleFunc(statusPath, func(w http.ResponseWriter, r *http.Request) { + status := h.driver.Status() + sdk.EncodeResponse(w, &StatusResponse{Status: status}, false) + }) + h.HandleFunc(getMetadataPath, func(w http.ResponseWriter, r *http.Request) { + req := GetMetadataRequest{} + err := sdk.DecodeRequest(w, r, &req) + if err != nil { + return + } + metadata, err := h.driver.GetMetadata(req.ID) + if err != nil { + sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) + return + } + sdk.EncodeResponse(w, &GetMetadataResponse{Metadata: metadata}, false) + }) + h.HandleFunc(cleanupPath, func(w http.ResponseWriter, r *http.Request) { + err := h.driver.Cleanup() + if err != nil { + sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) + return + } + sdk.EncodeResponse(w, struct{}{}, false) + }) + h.HandleFunc(diffPath, func(w http.ResponseWriter, r *http.Request) { + req := DiffRequest{} + err := sdk.DecodeRequest(w, r, &req) + if err != nil { + return + } + stream := h.driver.Diff(req.ID, req.Parent) + sdk.StreamResponse(w, stream) + }) + h.HandleFunc(changesPath, func(w http.ResponseWriter, r *http.Request) { + req := ChangesRequest{} + err := sdk.DecodeRequest(w, r, &req) + if err != nil { + return + } + changes, err := h.driver.Changes(req.ID, req.Parent) + if err != nil { + sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) + return + } + sdk.EncodeResponse(w, &ChangesResponse{Changes: changes}, false) + }) + h.HandleFunc(applyDiffPath, func(w http.ResponseWriter, r *http.Request) { + params := r.URL.Query() + id := params.Get("id") + parent := params.Get("parent") + size, err := h.driver.ApplyDiff(id, parent, r.Body) + if err != nil { + sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) + return + } + sdk.EncodeResponse(w, &ApplyDiffResponse{Size: size}, false) + }) + h.HandleFunc(diffSizePath, func(w http.ResponseWriter, r *http.Request) { + req := DiffRequest{} + err := sdk.DecodeRequest(w, r, &req) + if err != nil { + return + } + size, err := h.driver.DiffSize(req.ID, req.Parent) + if err != nil { + sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) + return + } + sdk.EncodeResponse(w, &DiffSizeResponse{Size: size}, false) + }) + h.HandleFunc(capabilitiesPath, func(w http.ResponseWriter, r *http.Request) { + caps := h.driver.Capabilities() + sdk.EncodeResponse(w, &CapabilitiesResponse{Capabilities: caps}, false) + }) +} diff --git a/graphdriver/api_test.go b/graphdriver/api_test.go new file mode 100644 index 0000000..b2c97c4 --- /dev/null +++ b/graphdriver/api_test.go @@ -0,0 +1,320 @@ +package graphdriver + +import ( + "bytes" + "encoding/json" + "io" + "io/ioutil" + "net/http" + "testing" + + graphDriver "github.com/docker/docker/daemon/graphdriver" + "github.com/docker/docker/pkg/containerfs" + "github.com/docker/docker/pkg/idtools" + "github.com/docker/go-connections/sockets" +) + +const url = "http://localhost" + +func TestHandler(t *testing.T) { + p := &testPlugin{} + h := NewHandler(p) + l := sockets.NewInmemSocket("test", 0) + go h.Serve(l) + defer l.Close() + + client := &http.Client{Transport: &http.Transport{ + Dial: l.Dial, + }} + + // Init + _, err := pluginRequest(client, initPath, &InitRequest{Home: "foo"}) + if err != nil { + t.Fatal(err) + } + if p.init != 1 { + t.Fatalf("expected init 1, got %d", p.init) + } + + // Create + _, err = pluginRequest(client, createPath, &CreateRequest{ID: "foo", Parent: "bar"}) + if err != nil { + t.Fatal(err) + } + if p.create != 1 { + t.Fatalf("expected create 1, got %d", p.create) + } + + // CreateReadWrite + _, err = pluginRequest(client, createRWPath, &CreateRequest{ID: "foo", Parent: "bar", MountLabel: "toto"}) + if err != nil { + t.Fatal(err) + } + if p.createRW != 1 { + t.Fatalf("expected createReadWrite 1, got %d", p.createRW) + } + + // Remove + _, err = pluginRequest(client, removePath, RemoveRequest{ID: "foo"}) + if err != nil { + t.Fatal(err) + } + if p.remove != 1 { + t.Fatalf("expected remove 1, got %d", p.remove) + } + + // Get + resp, err := pluginRequest(client, getPath, GetRequest{ID: "foo", MountLabel: "bar"}) + if err != nil { + t.Fatal(err) + } + var gResp *GetResponse + if err := json.NewDecoder(resp).Decode(&gResp); err != nil { + t.Fatal(err) + } + if gResp.Dir != "baz" { + t.Fatalf("expected dir = 'baz', got %s", gResp.Dir) + } + if p.get != 1 { + t.Fatalf("expected get 1, got %d", p.get) + } + + // Put + _, err = pluginRequest(client, putPath, PutRequest{ID: "foo"}) + if err != nil { + t.Fatal(err) + } + if p.put != 1 { + t.Fatalf("expected put 1, got %d", p.put) + } + + // Exists + resp, err = pluginRequest(client, existsPath, ExistsRequest{ID: "foo"}) + if err != nil { + t.Fatal(err) + } + var eResp *ExistsResponse + if err := json.NewDecoder(resp).Decode(&eResp); err != nil { + t.Fatal(err) + } + if !eResp.Exists { + t.Fatalf("got error testing for existence of graph drivers: %v", eResp.Exists) + } + if p.exists != 1 { + t.Fatalf("expected exists 1, got %d", p.exists) + } + + // Status + resp, err = pluginRequest(client, statusPath, StatusRequest{}) + if err != nil { + t.Fatal(err) + } + var sResp *StatusResponse + if err := json.NewDecoder(resp).Decode(&sResp); err != nil { + t.Fatal(err) + } + if p.status != 1 { + t.Fatalf("expected get 1, got %d", p.status) + } + + // GetMetadata + resp, err = pluginRequest(client, getMetadataPath, GetMetadataRequest{ID: "foo"}) + if err != nil { + t.Fatal(err) + } + var gmResp *GetMetadataResponse + if err := json.NewDecoder(resp).Decode(&gmResp); err != nil { + t.Fatal(err) + } + if p.getMetadata != 1 { + t.Fatalf("expected getMetadata 1, got %d", p.getMetadata) + } + + // Cleanup + _, err = pluginRequest(client, cleanupPath, CleanupRequest{}) + if err != nil { + t.Fatal(err) + } + if p.cleanup != 1 { + t.Fatalf("expected cleanup 1, got %d", p.cleanup) + } + + // Diff + _, err = pluginRequest(client, diffPath, DiffRequest{ID: "foo", Parent: "bar"}) + if err != nil { + t.Fatal(err) + } + if p.diff != 1 { + t.Fatalf("expected diff 1, got %d", p.diff) + } + + // Changes + resp, err = pluginRequest(client, changesPath, ChangesRequest{ID: "foo", Parent: "bar"}) + if err != nil { + t.Fatal(err) + } + var cResp *ChangesResponse + if err := json.NewDecoder(resp).Decode(&cResp); err != nil { + t.Fatal(err) + } + if p.status != 1 { + t.Fatalf("expected get 1, got %d", p.get) + } + + // ApplyDiff + b := new(bytes.Buffer) + stream := bytes.NewReader(b.Bytes()) + resp, err = pluginRequest(client, applyDiffPath, &ApplyDiffRequest{ID: "foo", Parent: "bar", Stream: stream}) + if err != nil { + t.Fatal(err) + } + var adResp *ApplyDiffResponse + if err := json.NewDecoder(resp).Decode(&adResp); err != nil { + t.Fatal(err) + } + if p.status != 1 { + t.Fatalf("expected applyDiff 1, got %d", p.applyDiff) + } + + // DiffSize + resp, err = pluginRequest(client, diffSizePath, DiffSizeRequest{ID: "foo", Parent: "bar"}) + if err != nil { + t.Fatal(err) + } + var dsResp *DiffSizeResponse + if err := json.NewDecoder(resp).Decode(&dsResp); err != nil { + t.Fatal(err) + } + if p.diffSize != 1 { + t.Fatalf("expected diffSize 1, got %d", p.diffSize) + } + + // Capabilities + resp, err = pluginRequest(client, capabilitiesPath, CapabilitiesRequest{}) + if err != nil { + t.Fatal(err) + } + var caResp *CapabilitiesResponse + if err := json.NewDecoder(resp).Decode(&caResp); err != nil { + t.Fatal(err) + } + if caResp.Capabilities.ReproducesExactDiffs != true { + t.Fatalf("got error getting capabilities for graph drivers: %v", caResp.Capabilities) + } + if p.capabilities != 1 { + t.Fatalf("expected get 1, got %d", p.get) + } +} + +func pluginRequest(client *http.Client, method string, req interface{}) (io.Reader, error) { + b, err := json.Marshal(req) + if err != nil { + return nil, err + } + if req == nil { + b = []byte{} + } + resp, err := client.Post("http://localhost"+method, "application/json", bytes.NewReader(b)) + if err != nil { + return nil, err + } + + return resp.Body, nil +} + +type testPlugin struct { + init int + create int + createRW int + remove int + get int + put int + exists int + status int + getMetadata int + cleanup int + diff int + changes int + applyDiff int + diffSize int + capabilities int +} + +var _ Driver = &testPlugin{} + +func (p *testPlugin) Init(string, []string, []idtools.IDMap, []idtools.IDMap) error { + p.init++ + return nil +} + +func (p *testPlugin) Create(string, string, string, map[string]string) error { + p.create++ + return nil +} + +func (p *testPlugin) CreateReadWrite(string, string, string, map[string]string) error { + p.createRW++ + return nil +} + +func (p *testPlugin) Remove(string) error { + p.remove++ + return nil +} + +func (p *testPlugin) Get(string, string) (containerfs.ContainerFS, error) { + p.get++ + return containerfs.NewLocalContainerFS("baz"), nil +} + +func (p *testPlugin) Put(string) error { + p.put++ + return nil +} + +func (p *testPlugin) Exists(string) bool { + p.exists++ + return true +} + +func (p *testPlugin) Status() [][2]string { + p.status++ + return nil +} + +func (p *testPlugin) GetMetadata(string) (map[string]string, error) { + p.getMetadata++ + return nil, nil +} + +func (p *testPlugin) Cleanup() error { + p.cleanup++ + return nil +} + +func (p *testPlugin) Diff(string, string) io.ReadCloser { + p.diff++ + b := new(bytes.Buffer) + x := ioutil.NopCloser(bytes.NewReader(b.Bytes())) + return x +} + +func (p *testPlugin) Changes(string, string) ([]Change, error) { + p.changes++ + return nil, nil +} + +func (p *testPlugin) ApplyDiff(string, string, io.Reader) (int64, error) { + p.applyDiff++ + return 42, nil +} + +func (p *testPlugin) DiffSize(string, string) (int64, error) { + p.diffSize++ + return 42, nil +} + +func (p *testPlugin) Capabilities() graphDriver.Capabilities { + p.capabilities++ + return graphDriver.Capabilities{ReproducesExactDiffs: true} +} diff --git a/graphdriver/shim/shim.go b/graphdriver/shim/shim.go new file mode 100644 index 0000000..bec1f59 --- /dev/null +++ b/graphdriver/shim/shim.go @@ -0,0 +1,176 @@ +package shim + +import ( + "errors" + "io" + "log" + + graphDriver "github.com/docker/docker/daemon/graphdriver" + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/containerfs" + "github.com/docker/docker/pkg/idtools" + graphPlugin "github.com/docker/go-plugins-helpers/graphdriver" +) + +type shimDriver struct { + driver graphDriver.Driver + init graphDriver.InitFunc +} + +// NewHandlerFromGraphDriver creates a plugin handler from an existing graph +// driver. This could be used, for instance, by the `overlayfs` graph driver +// built-in to Docker Engine and it would create a plugin from it that maps +// plugin API calls directly to any volume driver that satifies the +// graphdriver.Driver interface from Docker Engine. +func NewHandlerFromGraphDriver(init graphDriver.InitFunc) *graphPlugin.Handler { + return graphPlugin.NewHandler(&shimDriver{driver: nil, init: init}) +} + +func (d *shimDriver) Init(home string, options []string, uidMaps, gidMaps []idtools.IDMap) error { + driver, err := d.init(home, options, uidMaps, gidMaps) + if err != nil { + return err + } + d.driver = driver + return nil +} + +var errNotInitialized = errors.New("Not initialized") + +func (d *shimDriver) Create(id, parent, mountLabel string, storageOpt map[string]string) error { + if d == nil { + return errNotInitialized + } + opts := graphDriver.CreateOpts{ + MountLabel: mountLabel, + StorageOpt: storageOpt, + } + return d.driver.Create(id, parent, &opts) +} + +func (d *shimDriver) CreateReadWrite(id, parent, mountLabel string, storageOpt map[string]string) error { + if d == nil { + return errNotInitialized + } + opts := graphDriver.CreateOpts{ + MountLabel: mountLabel, + StorageOpt: storageOpt, + } + return d.driver.CreateReadWrite(id, parent, &opts) +} + +func (d *shimDriver) Remove(id string) error { + if d == nil { + return errNotInitialized + } + return d.driver.Remove(id) +} + +func (d *shimDriver) Get(id, mountLabel string) (containerfs.ContainerFS, error) { + if d == nil { + return nil, errNotInitialized + } + return d.driver.Get(id, mountLabel) +} + +func (d *shimDriver) Put(id string) error { + if d == nil { + return errNotInitialized + } + return d.driver.Put(id) +} + +func (d *shimDriver) Exists(id string) bool { + if d == nil { + return false + } + return d.driver.Exists(id) +} + +func (d *shimDriver) Status() [][2]string { + if d == nil { + return nil + } + return d.driver.Status() +} + +func (d *shimDriver) GetMetadata(id string) (map[string]string, error) { + if d == nil { + return nil, errNotInitialized + } + return d.driver.GetMetadata(id) +} + +func (d *shimDriver) Cleanup() error { + if d == nil { + return errNotInitialized + } + return d.driver.Cleanup() +} + +func (d *shimDriver) Diff(id, parent string) io.ReadCloser { + if d == nil { + return nil + } + // FIXME(samoht): how do we pass the error to the driver? + archive, err := d.driver.Diff(id, parent) + if err != nil { + log.Fatalf("Diff: error in stream %v", err) + } + return archive +} + +func changeKind(c archive.ChangeType) graphPlugin.ChangeKind { + switch c { + case archive.ChangeModify: + return graphPlugin.Modified + case archive.ChangeAdd: + return graphPlugin.Added + case archive.ChangeDelete: + return graphPlugin.Deleted + } + return 0 +} + +func (d *shimDriver) Changes(id, parent string) ([]graphPlugin.Change, error) { + if d == nil { + return nil, errNotInitialized + } + cs, err := d.driver.Changes(id, parent) + if err != nil { + return nil, err + } + changes := make([]graphPlugin.Change, len(cs)) + for _, c := range cs { + change := graphPlugin.Change{ + Path: c.Path, + Kind: changeKind(c.Kind), + } + changes = append(changes, change) + } + return changes, nil +} + +func (d *shimDriver) ApplyDiff(id, parent string, archive io.Reader) (int64, error) { + if d == nil { + return 0, errNotInitialized + } + return d.driver.ApplyDiff(id, parent, archive) +} + +func (d *shimDriver) DiffSize(id, parent string) (int64, error) { + if d == nil { + return 0, errNotInitialized + } + return d.driver.DiffSize(id, parent) +} + +func (d *shimDriver) Capabilities() graphDriver.Capabilities { + if d == nil { + return graphDriver.Capabilities{} + } + if capDriver, ok := d.driver.(graphDriver.CapabilityDriver); ok { + return capDriver.Capabilities() + } + return graphDriver.Capabilities{} +} diff --git a/graphdriver/shim/shim_test.go b/graphdriver/shim/shim_test.go new file mode 100644 index 0000000..032301a --- /dev/null +++ b/graphdriver/shim/shim_test.go @@ -0,0 +1,98 @@ +package shim + +import ( + "bytes" + "encoding/json" + "net/http" + "testing" + + "github.com/docker/docker/pkg/containerfs" + + "github.com/docker/docker/daemon/graphdriver" + "github.com/docker/docker/pkg/idtools" + "github.com/docker/go-connections/sockets" + graphPlugin "github.com/docker/go-plugins-helpers/graphdriver" +) + +type testGraphDriver struct{} + +// ProtoDriver +var _ graphdriver.ProtoDriver = &testGraphDriver{} + +func (t *testGraphDriver) String() string { + return "" +} + +// FIXME(samoht): this doesn't seem to be called by the plugins +func (t *testGraphDriver) CreateReadWrite(id, parent string, opts *graphdriver.CreateOpts) error { + return nil +} +func (t *testGraphDriver) Create(id, parent string, opts *graphdriver.CreateOpts) error { + return nil +} +func (t *testGraphDriver) Remove(id string) error { + return nil +} +func (t *testGraphDriver) Get(id, mountLabel string) (dir containerfs.ContainerFS, err error) { + return containerfs.NewLocalContainerFS(""), nil +} +func (t *testGraphDriver) Put(id string) error { + return nil +} +func (t *testGraphDriver) Exists(id string) bool { + return false +} +func (t *testGraphDriver) Status() [][2]string { + return nil +} +func (t *testGraphDriver) GetMetadata(id string) (map[string]string, error) { + return nil, nil +} +func (t *testGraphDriver) Cleanup() error { + return nil +} +func (t *testGraphDriver) Capabilities() graphdriver.Capabilities { + return graphdriver.Capabilities{} +} + +func Init(root string, options []string, uidMaps, gidMaps []idtools.IDMap) (graphdriver.Driver, error) { + d := graphdriver.NewNaiveDiffDriver(&testGraphDriver{}, uidMaps, gidMaps) + return d, nil +} + +func TestGraphDriver(t *testing.T) { + h := NewHandlerFromGraphDriver(Init) + l := sockets.NewInmemSocket("test", 0) + go h.Serve(l) + defer l.Close() + + client := &http.Client{Transport: &http.Transport{ + Dial: l.Dial, + }} + + resp, err := pluginRequest(client, "/GraphDriver.Init", &graphPlugin.InitRequest{Home: "foo"}) + if err != nil { + t.Fatal(err) + } + if resp.Err != "" { + t.Fatalf("error while creating GraphDriver: %v", err) + } +} + +func pluginRequest(client *http.Client, method string, req *graphPlugin.InitRequest) (*graphPlugin.ErrorResponse, error) { + b, err := json.Marshal(req) + if err != nil { + return nil, err + } + resp, err := client.Post("http://localhost"+method, "application/json", bytes.NewReader(b)) + if err != nil { + return nil, err + } + var gResp graphPlugin.ErrorResponse + err = json.NewDecoder(resp.Body).Decode(&gResp) + if err != nil { + return nil, err + } + + return &gResp, nil +} diff --git a/ipam/README.md b/ipam/README.md new file mode 100644 index 0000000..fcd0079 --- /dev/null +++ b/ipam/README.md @@ -0,0 +1,31 @@ +# Docker IPAM extension API + +Go handler to create external IPAM extensions for Docker. + +## Usage + +This library is designed to be integrated in your program. + +1. Implement the `ipam.Driver` interface. +2. Initialize a `ipam.Handler` with your implementation. +3. Call either `ServeTCP` or `ServeUnix` from the `ipam.Handler`. + +### Example using TCP sockets: + +```go + import "github.com/docker/go-plugins-helpers/ipam" + + d := MyIPAMDriver{} + h := ipam.NewHandler(d) + h.ServeTCP("test_ipam", ":8080") +``` + +### Example using Unix sockets: + +```go + import "github.com/docker/go-plugins-helpers/ipam" + + d := MyIPAMDriver{} + h := ipam.NewHandler(d) + h.ServeUnix("root", "test_ipam") +``` diff --git a/ipam/api.go b/ipam/api.go new file mode 100644 index 0000000..7ebc906 --- /dev/null +++ b/ipam/api.go @@ -0,0 +1,173 @@ +package ipam + +import ( + "net/http" + + "github.com/docker/go-plugins-helpers/sdk" +) + +const ( + manifest = `{"Implements": ["IpamDriver"]}` + + capabilitiesPath = "/IpamDriver.GetCapabilities" + addressSpacesPath = "/IpamDriver.GetDefaultAddressSpaces" + requestPoolPath = "/IpamDriver.RequestPool" + releasePoolPath = "/IpamDriver.ReleasePool" + requestAddressPath = "/IpamDriver.RequestAddress" + releaseAddressPath = "/IpamDriver.ReleaseAddress" +) + +// Ipam represent the interface a driver must fulfill. +type Ipam interface { + GetCapabilities() (*CapabilitiesResponse, error) + GetDefaultAddressSpaces() (*AddressSpacesResponse, error) + RequestPool(*RequestPoolRequest) (*RequestPoolResponse, error) + ReleasePool(*ReleasePoolRequest) error + RequestAddress(*RequestAddressRequest) (*RequestAddressResponse, error) + ReleaseAddress(*ReleaseAddressRequest) error +} + +// CapabilitiesResponse returns whether or not this IPAM required pre-made MAC +type CapabilitiesResponse struct { + RequiresMACAddress bool +} + +// AddressSpacesResponse returns the default local and global address space names for this IPAM +type AddressSpacesResponse struct { + LocalDefaultAddressSpace string + GlobalDefaultAddressSpace string +} + +// RequestPoolRequest is sent by the daemon when a pool needs to be created +type RequestPoolRequest struct { + AddressSpace string + Pool string + SubPool string + Options map[string]string + V6 bool +} + +// RequestPoolResponse returns a registered address pool with the IPAM driver +type RequestPoolResponse struct { + PoolID string + Pool string + Data map[string]string +} + +// ReleasePoolRequest is sent when releasing a previously registered address pool +type ReleasePoolRequest struct { + PoolID string +} + +// RequestAddressRequest is sent when requesting an address from IPAM +type RequestAddressRequest struct { + PoolID string + Address string + Options map[string]string +} + +// RequestAddressResponse is formed with allocated address by IPAM +type RequestAddressResponse struct { + Address string + Data map[string]string +} + +// ReleaseAddressRequest is sent in order to release an address from the pool +type ReleaseAddressRequest struct { + PoolID string + Address string +} + +// ErrorResponse is a formatted error message that libnetwork can understand +type ErrorResponse struct { + Err string +} + +// NewErrorResponse creates an ErrorResponse with the provided message +func NewErrorResponse(msg string) *ErrorResponse { + return &ErrorResponse{Err: msg} +} + +// Handler forwards requests and responses between the docker daemon and the plugin. +type Handler struct { + ipam Ipam + sdk.Handler +} + +// NewHandler initializes the request handler with a driver implementation. +func NewHandler(ipam Ipam) *Handler { + h := &Handler{ipam, sdk.NewHandler(manifest)} + h.initMux() + return h +} + +func (h *Handler) initMux() { + h.HandleFunc(capabilitiesPath, func(w http.ResponseWriter, r *http.Request) { + res, err := h.ipam.GetCapabilities() + if err != nil { + sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) + return + } + sdk.EncodeResponse(w, res, false) + }) + h.HandleFunc(addressSpacesPath, func(w http.ResponseWriter, r *http.Request) { + res, err := h.ipam.GetDefaultAddressSpaces() + if err != nil { + sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) + return + } + sdk.EncodeResponse(w, res, false) + }) + h.HandleFunc(requestPoolPath, func(w http.ResponseWriter, r *http.Request) { + req := &RequestPoolRequest{} + err := sdk.DecodeRequest(w, r, req) + if err != nil { + return + } + res, err := h.ipam.RequestPool(req) + if err != nil { + sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) + return + } + sdk.EncodeResponse(w, res, false) + }) + h.HandleFunc(releasePoolPath, func(w http.ResponseWriter, r *http.Request) { + req := &ReleasePoolRequest{} + err := sdk.DecodeRequest(w, r, req) + if err != nil { + return + } + err = h.ipam.ReleasePool(req) + if err != nil { + sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) + return + } + sdk.EncodeResponse(w, struct{}{}, false) + }) + h.HandleFunc(requestAddressPath, func(w http.ResponseWriter, r *http.Request) { + req := &RequestAddressRequest{} + err := sdk.DecodeRequest(w, r, req) + if err != nil { + return + } + res, err := h.ipam.RequestAddress(req) + if err != nil { + sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) + return + } + sdk.EncodeResponse(w, res, false) + }) + h.HandleFunc(releaseAddressPath, func(w http.ResponseWriter, r *http.Request) { + req := &ReleaseAddressRequest{} + err := sdk.DecodeRequest(w, r, req) + if err != nil { + return + } + err = h.ipam.ReleaseAddress(req) + if err != nil { + sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) + return + } + sdk.EncodeResponse(w, struct{}{}, false) + }) +} diff --git a/network/README.md b/network/README.md new file mode 100644 index 0000000..5c28fb1 --- /dev/null +++ b/network/README.md @@ -0,0 +1,59 @@ +# Docker network extension API + +Go handler to create external network extensions for Docker. + +## Usage + +This library is designed to be integrated in your program. + +1. Implement the `network.Driver` interface. +2. Initialize a `network.Handler` with your implementation. +3. Call either `ServeTCP`, `ServeUnix` or `ServeWindows` from the `network.Handler`. +4. On Windows, docker daemon data dir must be provided for ServeTCP and ServeWindows functions. +On Unix, this parameter is ignored. + +### Example using TCP sockets: + +```go + import "github.com/docker/go-plugins-helpers/network" + + d := MyNetworkDriver{} + h := network.NewHandler(d) + h.ServeTCP("test_network", ":8080", "") + // on windows: + h.ServeTCP("test_network", ":8080", WindowsDefaultDaemonRootDir()) +``` + +### Example using Unix sockets: + +```go + import "github.com/docker/go-plugins-helpers/network" + + d := MyNetworkDriver{} + h := network.NewHandler(d) + h.ServeUnix("test_network", 0) +``` + +### Example using Windows named pipes: + +```go +import "github.com/docker/go-plugins-helpers/network" +import "github.com/docker/go-plugins-helpers/sdk" + +d := MyNetworkDriver{} +h := network.NewHandler(d) + +config := sdk.WindowsPipeConfig{ + // open, read, write permissions for everyone + // (uses Windows Security Descriptor Definition Language) + SecurityDescriptor: AllowServiceSystemAdmin, + InBufferSize: 4096, + OutBufferSize: 4096, +} + +h.ServeWindows("//./pipe/testpipe", "test_network", WindowsDefaultDaemonRootDir(), &config) +``` + +## Full example plugins + +- [docker-ovs-plugin](https://github.com/gopher-net/docker-ovs-plugin) - An Open vSwitch Networking Plugin diff --git a/network/api.go b/network/api.go new file mode 100644 index 0000000..43bdb87 --- /dev/null +++ b/network/api.go @@ -0,0 +1,403 @@ +package network + +import ( + "net/http" + + "github.com/docker/go-plugins-helpers/sdk" +) + +const ( + manifest = `{"Implements": ["NetworkDriver"]}` + // LocalScope is the correct scope response for a local scope driver + LocalScope = `local` + // GlobalScope is the correct scope response for a global scope driver + GlobalScope = `global` + + capabilitiesPath = "/NetworkDriver.GetCapabilities" + allocateNetworkPath = "/NetworkDriver.AllocateNetwork" + freeNetworkPath = "/NetworkDriver.FreeNetwork" + createNetworkPath = "/NetworkDriver.CreateNetwork" + deleteNetworkPath = "/NetworkDriver.DeleteNetwork" + createEndpointPath = "/NetworkDriver.CreateEndpoint" + endpointInfoPath = "/NetworkDriver.EndpointOperInfo" + deleteEndpointPath = "/NetworkDriver.DeleteEndpoint" + joinPath = "/NetworkDriver.Join" + leavePath = "/NetworkDriver.Leave" + discoverNewPath = "/NetworkDriver.DiscoverNew" + discoverDeletePath = "/NetworkDriver.DiscoverDelete" + programExtConnPath = "/NetworkDriver.ProgramExternalConnectivity" + revokeExtConnPath = "/NetworkDriver.RevokeExternalConnectivity" +) + +// Driver represent the interface a driver must fulfill. +type Driver interface { + GetCapabilities() (*CapabilitiesResponse, error) + CreateNetwork(*CreateNetworkRequest) error + AllocateNetwork(*AllocateNetworkRequest) (*AllocateNetworkResponse, error) + DeleteNetwork(*DeleteNetworkRequest) error + FreeNetwork(*FreeNetworkRequest) error + CreateEndpoint(*CreateEndpointRequest) (*CreateEndpointResponse, error) + DeleteEndpoint(*DeleteEndpointRequest) error + EndpointInfo(*InfoRequest) (*InfoResponse, error) + Join(*JoinRequest) (*JoinResponse, error) + Leave(*LeaveRequest) error + DiscoverNew(*DiscoveryNotification) error + DiscoverDelete(*DiscoveryNotification) error + ProgramExternalConnectivity(*ProgramExternalConnectivityRequest) error + RevokeExternalConnectivity(*RevokeExternalConnectivityRequest) error +} + +// CapabilitiesResponse returns whether or not this network is global or local +type CapabilitiesResponse struct { + Scope string + ConnectivityScope string +} + +// AllocateNetworkRequest requests allocation of new network by manager +type AllocateNetworkRequest struct { + // A network ID that remote plugins are expected to store for future + // reference. + NetworkID string + + // A free form map->object interface for communication of options. + Options map[string]string + + // IPAMData contains the address pool information for this network + IPv4Data, IPv6Data []IPAMData +} + +// AllocateNetworkResponse is the response to the AllocateNetworkRequest. +type AllocateNetworkResponse struct { + // A free form plugin specific string->string object to be sent in + // CreateNetworkRequest call in the libnetwork agents + Options map[string]string +} + +// FreeNetworkRequest is the request to free allocated network in the manager +type FreeNetworkRequest struct { + // The ID of the network to be freed. + NetworkID string +} + +// CreateNetworkRequest is sent by the daemon when a network needs to be created +type CreateNetworkRequest struct { + NetworkID string + Options map[string]interface{} + IPv4Data []*IPAMData + IPv6Data []*IPAMData +} + +// IPAMData contains IPv4 or IPv6 addressing information +type IPAMData struct { + AddressSpace string + Pool string + Gateway string + AuxAddresses map[string]interface{} +} + +// DeleteNetworkRequest is sent by the daemon when a network needs to be removed +type DeleteNetworkRequest struct { + NetworkID string +} + +// CreateEndpointRequest is sent by the daemon when an endpoint should be created +type CreateEndpointRequest struct { + NetworkID string + EndpointID string + Interface *EndpointInterface + Options map[string]interface{} +} + +// CreateEndpointResponse is sent as a response to a CreateEndpointRequest +type CreateEndpointResponse struct { + Interface *EndpointInterface +} + +// EndpointInterface contains endpoint interface information +type EndpointInterface struct { + Address string + AddressIPv6 string + MacAddress string +} + +// DeleteEndpointRequest is sent by the daemon when an endpoint needs to be removed +type DeleteEndpointRequest struct { + NetworkID string + EndpointID string +} + +// InterfaceName consists of the name of the interface in the global netns and +// the desired prefix to be appended to the interface inside the container netns +type InterfaceName struct { + SrcName string + DstPrefix string +} + +// InfoRequest is send by the daemon when querying endpoint information +type InfoRequest struct { + NetworkID string + EndpointID string +} + +// InfoResponse is endpoint information sent in response to an InfoRequest +type InfoResponse struct { + Value map[string]string +} + +// JoinRequest is sent by the Daemon when an endpoint needs be joined to a network +type JoinRequest struct { + NetworkID string + EndpointID string + SandboxKey string + Options map[string]interface{} +} + +// StaticRoute contains static route information +type StaticRoute struct { + Destination string + RouteType int + NextHop string +} + +// JoinResponse is sent in response to a JoinRequest +type JoinResponse struct { + InterfaceName InterfaceName + Gateway string + GatewayIPv6 string + StaticRoutes []*StaticRoute + DisableGatewayService bool +} + +// LeaveRequest is send by the daemon when a endpoint is leaving a network +type LeaveRequest struct { + NetworkID string + EndpointID string +} + +// ErrorResponse is a formatted error message that libnetwork can understand +type ErrorResponse struct { + Err string +} + +// DiscoveryNotification is sent by the daemon when a new discovery event occurs +type DiscoveryNotification struct { + DiscoveryType int + DiscoveryData interface{} +} + +// ProgramExternalConnectivityRequest specifies the L4 data +// and the endpoint for which programming has to be done +type ProgramExternalConnectivityRequest struct { + NetworkID string + EndpointID string + Options map[string]interface{} +} + +// RevokeExternalConnectivityRequest specifies the endpoint +// for which the L4 programming has to be removed +type RevokeExternalConnectivityRequest struct { + NetworkID string + EndpointID string +} + +// NewErrorResponse creates an ErrorResponse with the provided message +func NewErrorResponse(msg string) *ErrorResponse { + return &ErrorResponse{Err: msg} +} + +// Handler forwards requests and responses between the docker daemon and the plugin. +type Handler struct { + driver Driver + sdk.Handler +} + +// NewHandler initializes the request handler with a driver implementation. +func NewHandler(driver Driver) *Handler { + h := &Handler{driver, sdk.NewHandler(manifest)} + h.initMux() + return h +} + +func (h *Handler) initMux() { + h.HandleFunc(capabilitiesPath, func(w http.ResponseWriter, r *http.Request) { + res, err := h.driver.GetCapabilities() + if err != nil { + sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) + return + } + if res == nil { + sdk.EncodeResponse(w, NewErrorResponse("Network driver must implement GetCapabilities"), true) + return + } + sdk.EncodeResponse(w, res, false) + }) + h.HandleFunc(createNetworkPath, func(w http.ResponseWriter, r *http.Request) { + req := &CreateNetworkRequest{} + err := sdk.DecodeRequest(w, r, req) + if err != nil { + return + } + err = h.driver.CreateNetwork(req) + if err != nil { + sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) + return + } + sdk.EncodeResponse(w, struct{}{}, false) + }) + h.HandleFunc(allocateNetworkPath, func(w http.ResponseWriter, r *http.Request) { + req := &AllocateNetworkRequest{} + err := sdk.DecodeRequest(w, r, req) + if err != nil { + return + } + res, err := h.driver.AllocateNetwork(req) + if err != nil { + sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) + return + } + sdk.EncodeResponse(w, res, false) + }) + h.HandleFunc(deleteNetworkPath, func(w http.ResponseWriter, r *http.Request) { + req := &DeleteNetworkRequest{} + err := sdk.DecodeRequest(w, r, req) + if err != nil { + return + } + err = h.driver.DeleteNetwork(req) + if err != nil { + sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) + return + } + sdk.EncodeResponse(w, struct{}{}, false) + }) + h.HandleFunc(freeNetworkPath, func(w http.ResponseWriter, r *http.Request) { + req := &FreeNetworkRequest{} + err := sdk.DecodeRequest(w, r, req) + if err != nil { + return + } + err = h.driver.FreeNetwork(req) + if err != nil { + sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) + return + } + sdk.EncodeResponse(w, struct{}{}, false) + }) + h.HandleFunc(createEndpointPath, func(w http.ResponseWriter, r *http.Request) { + req := &CreateEndpointRequest{} + err := sdk.DecodeRequest(w, r, req) + if err != nil { + return + } + res, err := h.driver.CreateEndpoint(req) + if err != nil { + sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) + return + } + sdk.EncodeResponse(w, res, false) + }) + h.HandleFunc(deleteEndpointPath, func(w http.ResponseWriter, r *http.Request) { + req := &DeleteEndpointRequest{} + err := sdk.DecodeRequest(w, r, req) + if err != nil { + return + } + err = h.driver.DeleteEndpoint(req) + if err != nil { + sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) + return + } + sdk.EncodeResponse(w, struct{}{}, false) + }) + h.HandleFunc(endpointInfoPath, func(w http.ResponseWriter, r *http.Request) { + req := &InfoRequest{} + err := sdk.DecodeRequest(w, r, req) + if err != nil { + return + } + res, err := h.driver.EndpointInfo(req) + if err != nil { + sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) + return + } + sdk.EncodeResponse(w, res, false) + }) + h.HandleFunc(joinPath, func(w http.ResponseWriter, r *http.Request) { + req := &JoinRequest{} + err := sdk.DecodeRequest(w, r, req) + if err != nil { + return + } + res, err := h.driver.Join(req) + if err != nil { + sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) + return + } + sdk.EncodeResponse(w, res, false) + }) + h.HandleFunc(leavePath, func(w http.ResponseWriter, r *http.Request) { + req := &LeaveRequest{} + err := sdk.DecodeRequest(w, r, req) + if err != nil { + return + } + err = h.driver.Leave(req) + if err != nil { + sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) + return + } + sdk.EncodeResponse(w, struct{}{}, false) + }) + h.HandleFunc(discoverNewPath, func(w http.ResponseWriter, r *http.Request) { + req := &DiscoveryNotification{} + err := sdk.DecodeRequest(w, r, req) + if err != nil { + return + } + err = h.driver.DiscoverNew(req) + if err != nil { + sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) + return + } + sdk.EncodeResponse(w, struct{}{}, false) + }) + h.HandleFunc(discoverDeletePath, func(w http.ResponseWriter, r *http.Request) { + req := &DiscoveryNotification{} + err := sdk.DecodeRequest(w, r, req) + if err != nil { + return + } + err = h.driver.DiscoverDelete(req) + if err != nil { + sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) + return + } + sdk.EncodeResponse(w, struct{}{}, false) + }) + h.HandleFunc(programExtConnPath, func(w http.ResponseWriter, r *http.Request) { + req := &ProgramExternalConnectivityRequest{} + err := sdk.DecodeRequest(w, r, req) + if err != nil { + return + } + err = h.driver.ProgramExternalConnectivity(req) + if err != nil { + sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) + return + } + sdk.EncodeResponse(w, struct{}{}, false) + }) + h.HandleFunc(revokeExtConnPath, func(w http.ResponseWriter, r *http.Request) { + req := &RevokeExternalConnectivityRequest{} + err := sdk.DecodeRequest(w, r, req) + if err != nil { + return + } + err = h.driver.RevokeExternalConnectivity(req) + if err != nil { + sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) + return + } + sdk.EncodeResponse(w, struct{}{}, false) + }) +} diff --git a/network/api_test.go b/network/api_test.go new file mode 100644 index 0000000..66281a9 --- /dev/null +++ b/network/api_test.go @@ -0,0 +1,220 @@ +package network + +import ( + "errors" + "fmt" + "io/ioutil" + "net/http" + "os" + "strings" + "testing" + "time" + + "github.com/docker/go-plugins-helpers/sdk" +) + +type TestDriver struct { + Driver +} + +func (t *TestDriver) GetCapabilities() (*CapabilitiesResponse, error) { + return &CapabilitiesResponse{Scope: LocalScope, ConnectivityScope: GlobalScope}, nil +} + +func (t *TestDriver) CreateNetwork(r *CreateNetworkRequest) error { + return nil +} + +func (t *TestDriver) DeleteNetwork(r *DeleteNetworkRequest) error { + return nil +} + +func (t *TestDriver) CreateEndpoint(r *CreateEndpointRequest) (*CreateEndpointResponse, error) { + return &CreateEndpointResponse{}, nil +} + +func (t *TestDriver) DeleteEndpoint(r *DeleteEndpointRequest) error { + return nil +} + +func (t *TestDriver) Join(r *JoinRequest) (*JoinResponse, error) { + return &JoinResponse{}, nil +} + +func (t *TestDriver) Leave(r *LeaveRequest) error { + return nil +} + +func (t *TestDriver) ProgramExternalConnectivity(r *ProgramExternalConnectivityRequest) error { + i := r.Options["com.docker.network.endpoint.exposedports"] + epl, ok := i.([]interface{}) + if !ok { + return fmt.Errorf("invalid data in request: %v (%T)", i, i) + } + ep, ok := epl[0].(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid data in request: %v (%T)", epl[0], epl[0]) + } + if ep["Proto"].(float64) != 6 || ep["Port"].(float64) != 70 { + return fmt.Errorf("Unexpected exposed ports in request: %v", ep) + } + return nil +} + +func (t *TestDriver) RevokeExternalConnectivity(r *RevokeExternalConnectivityRequest) error { + return nil +} + +type ErrDriver struct { + Driver +} + +func (e *ErrDriver) GetCapabilities() (*CapabilitiesResponse, error) { + return nil, fmt.Errorf("I CAN HAZ ERRORZ") +} + +func (e *ErrDriver) CreateNetwork(r *CreateNetworkRequest) error { + return fmt.Errorf("I CAN HAZ ERRORZ") +} + +func (e *ErrDriver) DeleteNetwork(r *DeleteNetworkRequest) error { + return errors.New("I CAN HAZ ERRORZ") +} + +func (e *ErrDriver) CreateEndpoint(r *CreateEndpointRequest) (*CreateEndpointResponse, error) { + return nil, errors.New("I CAN HAZ ERRORZ") +} + +func (e *ErrDriver) DeleteEndpoint(r *DeleteEndpointRequest) error { + return errors.New("I CAN HAZ ERRORZ") +} + +func (e *ErrDriver) Join(r *JoinRequest) (*JoinResponse, error) { + return nil, errors.New("I CAN HAZ ERRORZ") +} + +func (e *ErrDriver) Leave(r *LeaveRequest) error { + return errors.New("I CAN HAZ ERRORZ") +} + +func callURL(url string) { + c := http.Client{ + Timeout: 10 * time.Millisecond, + } + res := make(chan interface{}, 1) + go func() { + for { + _, err := c.Get(url) + if err == nil { + res <- nil + } + } + }() + + select { + case <-res: + return + case <-time.After(5 * time.Second): + fmt.Printf("Timeout connecting to %s\n", url) + os.Exit(1) + } +} + +func TestMain(m *testing.M) { + d := &TestDriver{} + h1 := NewHandler(d) + go h1.ServeTCP("test", "localhost:32234", "", nil) + + e := &ErrDriver{} + h2 := NewHandler(e) + go h2.ServeTCP("err", "localhost:32567", "", nil) + + // Test that the ServeTCP is ready for use. + callURL("http://localhost:32234/Plugin.Activate") + callURL("http://localhost:32567/Plugin.Activate") + + os.Exit(m.Run()) +} + +func TestActivate(t *testing.T) { + response, err := http.Get("http://localhost:32234/Plugin.Activate") + if err != nil { + t.Fatal(err) + } + defer response.Body.Close() + body, err := ioutil.ReadAll(response.Body) + + if string(body) != manifest+"\n" { + t.Fatalf("Expected %s, got %s\n", manifest+"\n", string(body)) + } +} + +func TestCapabilitiesExchange(t *testing.T) { + response, err := http.Get("http://localhost:32234/NetworkDriver.GetCapabilities") + if err != nil { + t.Fatal(err) + } + defer response.Body.Close() + body, err := ioutil.ReadAll(response.Body) + + expected := `{"Scope":"local","ConnectivityScope":"global"}` + if string(body) != expected+"\n" { + t.Fatalf("Expected %s, got %s\n", expected+"\n", string(body)) + } +} + +func TestCreateNetworkSuccess(t *testing.T) { + request := `{"NetworkID":"d76cfa51738e8a12c5eca71ee69e9d65010a4b48eaad74adab439be7e61b9aaf","Options":{"com.docker.network.generic":{}},"IPv4Data":[{"AddressSpace":"","Gateway":"172.18.0.1/16","Pool":"172.18.0.0/16"}],"IPv6Data":[]}` + + response, err := http.Post("http://localhost:32234/NetworkDriver.CreateNetwork", + sdk.DefaultContentTypeV1_1, + strings.NewReader(request), + ) + if err != nil { + t.Fatal(err) + } + defer response.Body.Close() + body, err := ioutil.ReadAll(response.Body) + + if response.StatusCode != http.StatusOK { + t.Fatalf("Expected 200, got %d\n", response.StatusCode) + } + if string(body) != "{}\n" { + t.Fatalf("Expected %s, got %s\n", "{}\n", string(body)) + } +} + +func TestCreateNetworkError(t *testing.T) { + request := `{"NetworkID":"d76cfa51738e8a12c5eca71ee69e9d65010a4b48eaad74adab439be7e61b9aaf","Options":{"com.docker.network.generic": {}},"IPv4Data":[{"AddressSpace":"","Gateway":"172.18.0.1/16","Pool":"172.18.0.0/16"}],"IPv6Data":[]}` + response, err := http.Post("http://localhost:32567/NetworkDriver.CreateNetwork", + sdk.DefaultContentTypeV1_1, + strings.NewReader(request)) + if err != nil { + t.Fatal(err) + } + defer response.Body.Close() + body, err := ioutil.ReadAll(response.Body) + + if response.StatusCode != http.StatusInternalServerError { + t.Fatalf("Expected 500, got %d\n", response.StatusCode) + } + if string(body) != "{\"Err\":\"I CAN HAZ ERRORZ\"}\n" { + t.Fatalf("Expected %s, got %s\n", "{\"Err\":\"I CAN HAZ ERRORZ\"}\n", string(body)) + } +} + +func TestProgramExternalConnectivity(t *testing.T) { + 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}]}}` + response, err := http.Post("http://localhost:32234/NetworkDriver.ProgramExternalConnectivity", + sdk.DefaultContentTypeV1_1, + strings.NewReader(request)) + if err != nil { + t.Fatal(err) + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + body, _ := ioutil.ReadAll(response.Body) + t.Fatalf("Expected %d, got %d: %s\n", http.StatusOK, response.StatusCode, string(body)) + } +} diff --git a/sdk/encoder.go b/sdk/encoder.go new file mode 100644 index 0000000..195812a --- /dev/null +++ b/sdk/encoder.go @@ -0,0 +1,37 @@ +package sdk + +import ( + "encoding/json" + "fmt" + "io" + "net/http" +) + +// DefaultContentTypeV1_1 is the default content type accepted and sent by the plugins. +const DefaultContentTypeV1_1 = "application/vnd.docker.plugins.v1.1+json" + +// DecodeRequest decodes an http request into a given structure. +func DecodeRequest(w http.ResponseWriter, r *http.Request, req interface{}) (err error) { + if err = json.NewDecoder(r.Body).Decode(req); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + } + return +} + +// EncodeResponse encodes the given structure into an http response. +func EncodeResponse(w http.ResponseWriter, res interface{}, err bool) { + w.Header().Set("Content-Type", DefaultContentTypeV1_1) + if err { + w.WriteHeader(http.StatusInternalServerError) + } + json.NewEncoder(w).Encode(res) +} + +// StreamResponse streams a response object to the client +func StreamResponse(w http.ResponseWriter, data io.ReadCloser) { + w.Header().Set("Content-Type", DefaultContentTypeV1_1) + if _, err := copyBuf(w, data); err != nil { + fmt.Printf("ERROR in stream: %v\n", err) + } + data.Close() +} diff --git a/sdk/handler.go b/sdk/handler.go new file mode 100644 index 0000000..c0d042e --- /dev/null +++ b/sdk/handler.go @@ -0,0 +1,88 @@ +package sdk + +import ( + "crypto/tls" + "fmt" + "net" + "net/http" + "os" +) + +const activatePath = "/Plugin.Activate" + +// Handler is the base to create plugin handlers. +// It initializes connections and sockets to listen to. +type Handler struct { + mux *http.ServeMux +} + +// NewHandler creates a new Handler with an http mux. +func NewHandler(manifest string) Handler { + mux := http.NewServeMux() + + mux.HandleFunc(activatePath, func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", DefaultContentTypeV1_1) + fmt.Fprintln(w, manifest) + }) + + return Handler{mux: mux} +} + +// Serve sets up the handler to serve requests on the passed in listener +func (h Handler) Serve(l net.Listener) error { + server := http.Server{ + Addr: l.Addr().String(), + Handler: h.mux, + } + return server.Serve(l) +} + +// ServeTCP makes the handler to listen for request in a given TCP address. +// It also writes the spec file in the right directory for docker to read. +// Due to constrains for running Docker in Docker on Windows, data-root directory +// of docker daemon must be provided. To get default directory, use +// WindowsDefaultDaemonRootDir() function. On Unix, this parameter is ignored. +func (h Handler) ServeTCP(pluginName, addr, daemonDir string, tlsConfig *tls.Config) error { + l, spec, err := newTCPListener(addr, pluginName, daemonDir, tlsConfig) + if err != nil { + return err + } + if spec != "" { + defer os.Remove(spec) + } + return h.Serve(l) +} + +// ServeUnix makes the handler to listen for requests in a unix socket. +// It also creates the socket file in the right directory for docker to read. +func (h Handler) ServeUnix(addr string, gid int) error { + l, spec, err := newUnixListener(addr, gid) + if err != nil { + return err + } + if spec != "" { + defer os.Remove(spec) + } + return h.Serve(l) +} + +// ServeWindows makes the handler to listen for request in a Windows named pipe. +// It also creates the spec file in the right directory for docker to read. +// Due to constrains for running Docker in Docker on Windows, data-root directory +// of docker daemon must be provided. To get default directory, use +// WindowsDefaultDaemonRootDir() function. On Unix, this parameter is ignored. +func (h Handler) ServeWindows(addr, pluginName, daemonDir string, pipeConfig *WindowsPipeConfig) error { + l, spec, err := newWindowsListener(addr, pluginName, daemonDir, pipeConfig) + if err != nil { + return err + } + if spec != "" { + defer os.Remove(spec) + } + return h.Serve(l) +} + +// HandleFunc registers a function to handle a request path with. +func (h Handler) HandleFunc(path string, fn func(w http.ResponseWriter, r *http.Request)) { + h.mux.HandleFunc(path, fn) +} diff --git a/sdk/pool.go b/sdk/pool.go new file mode 100644 index 0000000..3167759 --- /dev/null +++ b/sdk/pool.go @@ -0,0 +1,18 @@ +package sdk + +import ( + "io" + "sync" +) + +const buffer32K = 32 * 1024 + +var buffer32KPool = &sync.Pool{New: func() interface{} { return make([]byte, buffer32K) }} + +// copyBuf uses a shared buffer pool with io.CopyBuffer +func copyBuf(w io.Writer, r io.Reader) (int64, error) { + buf := buffer32KPool.Get().([]byte) + written, err := io.CopyBuffer(w, r, buf) + buffer32KPool.Put(buf) + return written, err +} diff --git a/sdk/sdk_test.go b/sdk/sdk_test.go new file mode 100644 index 0000000..b157c69 --- /dev/null +++ b/sdk/sdk_test.go @@ -0,0 +1,7 @@ +package sdk + +import "testing" + +func TestTrue(t *testing.T) { + // FIXME: Add tests +} diff --git a/sdk/spec_file_generator.go b/sdk/spec_file_generator.go new file mode 100644 index 0000000..bc8cfc6 --- /dev/null +++ b/sdk/spec_file_generator.go @@ -0,0 +1,58 @@ +package sdk + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" +) + +type protocol string + +const ( + protoTCP protocol = "tcp" + protoNamedPipe protocol = "npipe" +) + +// PluginSpecDir returns plugin spec dir in relation to daemon root directory. +func PluginSpecDir(daemonRoot string) string { + return ([]string{filepath.Join(daemonRoot, "plugins")})[0] +} + +// WindowsDefaultDaemonRootDir returns default data directory of docker daemon on Windows. +func WindowsDefaultDaemonRootDir() string { + return filepath.Join(os.Getenv("programdata"), "docker") +} + +func createPluginSpecDirWindows(name, address, daemonRoot string) (string, error) { + _, err := os.Stat(daemonRoot) + if os.IsNotExist(err) { + return "", fmt.Errorf("Deamon root directory must already exist: %s", err) + } + + pluginSpecDir := PluginSpecDir(daemonRoot) + + if err := windowsCreateDirectoryWithACL(pluginSpecDir); err != nil { + return "", err + } + return pluginSpecDir, nil +} + +func createPluginSpecDirUnix(name, address string) (string, error) { + pluginSpecDir := PluginSpecDir("/etc/docker") + if err := os.MkdirAll(pluginSpecDir, 0755); err != nil { + return "", err + } + return pluginSpecDir, nil +} + +func writeSpecFile(name, address, pluginSpecDir string, proto protocol) (string, error) { + specFileDir := filepath.Join(pluginSpecDir, name+".spec") + + url := string(proto) + "://" + address + if err := ioutil.WriteFile(specFileDir, []byte(url), 0644); err != nil { + return "", err + } + + return specFileDir, nil +} diff --git a/sdk/tcp_listener.go b/sdk/tcp_listener.go new file mode 100644 index 0000000..bad85f7 --- /dev/null +++ b/sdk/tcp_listener.go @@ -0,0 +1,34 @@ +package sdk + +import ( + "crypto/tls" + "net" + "runtime" + + "github.com/docker/go-connections/sockets" +) + +func newTCPListener(address, pluginName, daemonDir string, tlsConfig *tls.Config) (net.Listener, string, error) { + listener, err := sockets.NewTCPSocket(address, tlsConfig) + if err != nil { + return nil, "", err + } + + addr := listener.Addr().String() + + var specDir string + if runtime.GOOS == "windows" { + specDir, err = createPluginSpecDirWindows(pluginName, addr, daemonDir) + } else { + specDir, err = createPluginSpecDirUnix(pluginName, addr) + } + if err != nil { + return nil, "", err + } + + specFile, err := writeSpecFile(pluginName, addr, specDir, protoTCP) + if err != nil { + return nil, "", err + } + return listener, specFile, nil +} diff --git a/sdk/unix_listener.go b/sdk/unix_listener.go new file mode 100644 index 0000000..54b9a6d --- /dev/null +++ b/sdk/unix_listener.go @@ -0,0 +1,35 @@ +// +build linux freebsd + +package sdk + +import ( + "net" + "os" + "path/filepath" + + "github.com/docker/go-connections/sockets" +) + +const pluginSockDir = "/run/docker/plugins" + +func newUnixListener(pluginName string, gid int) (net.Listener, string, error) { + path, err := fullSocketAddress(pluginName) + if err != nil { + return nil, "", err + } + listener, err := sockets.NewUnixSocket(path, gid) + if err != nil { + return nil, "", err + } + return listener, path, nil +} + +func fullSocketAddress(address string) (string, error) { + if err := os.MkdirAll(pluginSockDir, 0755); err != nil { + return "", err + } + if filepath.IsAbs(address) { + return address, nil + } + return filepath.Join(pluginSockDir, address+".sock"), nil +} diff --git a/sdk/unix_listener_nosystemd.go b/sdk/unix_listener_nosystemd.go new file mode 100644 index 0000000..a798b87 --- /dev/null +++ b/sdk/unix_listener_nosystemd.go @@ -0,0 +1,10 @@ +// +build linux freebsd +// +build nosystemd + +package sdk + +import "net" + +func setupSocketActivation() (net.Listener, error) { + return nil, nil +} diff --git a/sdk/unix_listener_systemd.go b/sdk/unix_listener_systemd.go new file mode 100644 index 0000000..5d5d8f4 --- /dev/null +++ b/sdk/unix_listener_systemd.go @@ -0,0 +1,45 @@ +// +build linux freebsd +// +build !nosystemd + +package sdk + +import ( + "fmt" + "net" + "os" + + "github.com/coreos/go-systemd/activation" +) + +// isRunningSystemd checks whether the host was booted with systemd as its init +// system. This functions similarly to systemd's `sd_booted(3)`: internally, it +// checks whether /run/systemd/system/ exists and is a directory. +// http://www.freedesktop.org/software/systemd/man/sd_booted.html +// +// Copied from github.com/coreos/go-systemd/util.IsRunningSystemd +func isRunningSystemd() bool { + fi, err := os.Lstat("/run/systemd/system") + if err != nil { + return false + } + return fi.IsDir() +} + +func setupSocketActivation() (net.Listener, error) { + if !isRunningSystemd() { + return nil, nil + } + listenFds := activation.Files(false) + if len(listenFds) > 1 { + return nil, fmt.Errorf("expected only one socket from systemd, got %d", len(listenFds)) + } + var listener net.Listener + if len(listenFds) == 1 { + l, err := net.FileListener(listenFds[0]) + if err != nil { + return nil, err + } + listener = l + } + return listener, nil +} diff --git a/sdk/unix_listener_unsupported.go b/sdk/unix_listener_unsupported.go new file mode 100644 index 0000000..344cf75 --- /dev/null +++ b/sdk/unix_listener_unsupported.go @@ -0,0 +1,16 @@ +// +build !linux,!freebsd + +package sdk + +import ( + "errors" + "net" +) + +var ( + errOnlySupportedOnLinuxAndFreeBSD = errors.New("unix socket creation is only supported on Linux and FreeBSD") +) + +func newUnixListener(pluginName string, gid int) (net.Listener, string, error) { + return nil, "", errOnlySupportedOnLinuxAndFreeBSD +} diff --git a/sdk/windows_listener.go b/sdk/windows_listener.go new file mode 100644 index 0000000..b5deaba --- /dev/null +++ b/sdk/windows_listener.go @@ -0,0 +1,70 @@ +// +build windows + +package sdk + +import ( + "net" + "os" + "syscall" + "unsafe" + + "github.com/Microsoft/go-winio" +) + +// Named pipes use Windows Security Descriptor Definition Language to define ACL. Following are +// some useful definitions. +const ( + // This will set permissions for everyone to have full access + AllowEveryone = "S:(ML;;NW;;;LW)D:(A;;0x12019f;;;WD)" + + // This will set permissions for Service, System, Adminstrator group and account to have full access + AllowServiceSystemAdmin = "D:(A;ID;FA;;;SY)(A;ID;FA;;;BA)(A;ID;FA;;;LA)(A;ID;FA;;;LS)" +) + +func newWindowsListener(address, pluginName, daemonRoot string, pipeConfig *WindowsPipeConfig) (net.Listener, string, error) { + winioPipeConfig := winio.PipeConfig{ + SecurityDescriptor: pipeConfig.SecurityDescriptor, + InputBufferSize: pipeConfig.InBufferSize, + OutputBufferSize: pipeConfig.OutBufferSize, + } + listener, err := winio.ListenPipe(address, &winioPipeConfig) + if err != nil { + return nil, "", err + } + + addr := listener.Addr().String() + + specDir, err := createPluginSpecDirWindows(pluginName, addr, daemonRoot) + if err != nil { + return nil, "", err + } + + spec, err := writeSpecFile(pluginName, addr, specDir, protoNamedPipe) + if err != nil { + return nil, "", err + } + return listener, spec, nil +} + +func windowsCreateDirectoryWithACL(name string) error { + sa := syscall.SecurityAttributes{Length: 0} + sddl := "D:P(A;OICI;GA;;;BA)(A;OICI;GA;;;SY)" + sd, err := winio.SddlToSecurityDescriptor(sddl) + if err != nil { + return &os.PathError{Op: "mkdir", Path: name, Err: err} + } + sa.Length = uint32(unsafe.Sizeof(sa)) + sa.InheritHandle = 1 + sa.SecurityDescriptor = uintptr(unsafe.Pointer(&sd[0])) + + namep, err := syscall.UTF16PtrFromString(name) + if err != nil { + return &os.PathError{Op: "mkdir", Path: name, Err: err} + } + + e := syscall.CreateDirectory(namep, &sa) + if e != nil { + return &os.PathError{Op: "mkdir", Path: name, Err: e} + } + return nil +} diff --git a/sdk/windows_listener_unsupported.go b/sdk/windows_listener_unsupported.go new file mode 100644 index 0000000..0f5e113 --- /dev/null +++ b/sdk/windows_listener_unsupported.go @@ -0,0 +1,20 @@ +// +build !windows + +package sdk + +import ( + "errors" + "net" +) + +var ( + errOnlySupportedOnWindows = errors.New("named pipe creation is only supported on Windows") +) + +func newWindowsListener(address, pluginName, daemonRoot string, pipeConfig *WindowsPipeConfig) (net.Listener, string, error) { + return nil, "", errOnlySupportedOnWindows +} + +func windowsCreateDirectoryWithACL(name string) error { + return nil +} diff --git a/sdk/windows_pipe_config.go b/sdk/windows_pipe_config.go new file mode 100644 index 0000000..256fa3d --- /dev/null +++ b/sdk/windows_pipe_config.go @@ -0,0 +1,13 @@ +package sdk + +// WindowsPipeConfig is a helper structure for configuring named pipe parameters on Windows. +type WindowsPipeConfig struct { + // SecurityDescriptor contains a Windows security descriptor in SDDL format. + SecurityDescriptor string + + // InBufferSize in bytes. + InBufferSize int32 + + // OutBufferSize in bytes. + OutBufferSize int32 +} diff --git a/secrets/README.md b/secrets/README.md new file mode 100644 index 0000000..3fbc0f4 --- /dev/null +++ b/secrets/README.md @@ -0,0 +1,31 @@ +# Docker secrets extension API + +Go handler to get secrets from external secret stores in Docker. + +## Usage + +This library is designed to be integrated in your program. + +1. Implement the `secrets.Driver` interface. +2. Initialize a `secrets.Handler` with your implementation. +3. Call either `ServeTCP` or `ServeUnix` from the `secrets.Handler`. + +### Example using TCP sockets: + +```go + import "github.com/docker/go-plugins-helpers/secrets" + + d := MySecretsDriver{} + h := secrets.NewHandler(d) + h.ServeTCP("test_secrets", ":8080") +``` + +### Example using Unix sockets: + +```go + import "github.com/docker/go-plugins-helpers/secrets" + + d := MySecretsDriver{} + h := secrets.NewHandler(d) + h.ServeUnix("test_secrets", 0) +``` diff --git a/secrets/api.go b/secrets/api.go new file mode 100644 index 0000000..e05fd38 --- /dev/null +++ b/secrets/api.go @@ -0,0 +1,85 @@ +package secrets + +import ( + "net/http" + + "github.com/docker/go-plugins-helpers/sdk" +) + +const ( + manifest = `{"Implements": ["secretprovider"]}` + getPath = "/SecretProvider.GetSecret" +) + +// Request is the plugin secret request +type Request struct { + SecretName string `json:",omitempty"` // SecretName is the name of the secret to request from the plugin + SecretLabels map[string]string `json:",omitempty"` // SecretLabels capture environment names and other metadata pertaining to the secret + ServiceHostname string `json:",omitempty"` // ServiceHostname is the hostname of the service, can be used for x509 certificate + ServiceName string `json:",omitempty"` // ServiceName is the name of the service that requested the secret + ServiceID string `json:",omitempty"` // ServiceID is the name of the service that requested the secret + ServiceLabels map[string]string `json:",omitempty"` // ServiceLabels capture environment names and other metadata pertaining to the service + TaskID string `json:",omitempty"` // TaskID is the ID of the task that the secret is assigned to + TaskName string `json:",omitempty"` // TaskName is the name of the task that the secret is assigned to + TaskImage string `json:",omitempty"` // TaskName is the image of the task that the secret is assigned to + ServiceEndpointSpec *EndpointSpec `json:",omitempty"` // ServiceEndpointSpec holds the specification for endpoints +} + +// Response contains the plugin secret value +type Response struct { + Value []byte `json:",omitempty"` // Value is the value of the secret + Err string `json:",omitempty"` // Err is the error response of the plugin + + // DoNotReuse indicates that the secret returned from this request should + // only be used for one task, and any further tasks should call the secret + // driver again. + DoNotReuse bool `json:",omitempty"` +} + +// EndpointSpec represents the spec of an endpoint. +type EndpointSpec struct { + Mode int32 `json:",omitempty"` + Ports []PortConfig `json:",omitempty"` +} + +// PortConfig represents the config of a port. +type PortConfig struct { + Name string `json:",omitempty"` + Protocol int32 `json:",omitempty"` + // TargetPort is the port inside the container + TargetPort uint32 `json:",omitempty"` + // PublishedPort is the port on the swarm hosts + PublishedPort uint32 `json:",omitempty"` + // PublishMode is the mode in which port is published + PublishMode int32 `json:",omitempty"` +} + +// Driver represent the interface a driver must fulfill. +type Driver interface { + // Get gets a secret from a remote secret store + Get(Request) Response +} + +// Handler forwards requests and responses between the docker daemon and the plugin. +type Handler struct { + driver Driver + sdk.Handler +} + +// NewHandler initializes the request handler with a driver implementation. +func NewHandler(driver Driver) *Handler { + h := &Handler{driver, sdk.NewHandler(manifest)} + h.initMux() + return h +} + +func (h *Handler) initMux() { + h.HandleFunc(getPath, func(w http.ResponseWriter, r *http.Request) { + var req Request + if err := sdk.DecodeRequest(w, r, &req); err != nil { + return + } + res := h.driver.Get(req) + sdk.EncodeResponse(w, res, res.Err != "") + }) +} diff --git a/secrets/api_test.go b/secrets/api_test.go new file mode 100644 index 0000000..9763a4e --- /dev/null +++ b/secrets/api_test.go @@ -0,0 +1,91 @@ +package secrets + +import ( + "bytes" + "encoding/json" + "net/http" + "testing" + + "github.com/docker/go-connections/sockets" +) + +func TestHandler(t *testing.T) { + p := &testPlugin{} + h := NewHandler(p) + l := sockets.NewInmemSocket("test", 0) + go h.Serve(l) + defer l.Close() + + client := &http.Client{Transport: &http.Transport{ + Dial: l.Dial, + }} + + resp, err := pluginRequest(client, getPath, Request{SecretName: "my-secret"}) + if err != nil { + t.Fatal(err) + } + if resp.Err != "" { + t.Fatalf("error while getting secret: %v", resp.Err) + } + if p.get != 1 { + t.Fatalf("expected get 1, got %d", p.get) + } + if !bytes.EqualFold(secret, resp.Value) { + t.Fatalf("expecting secret value %s, got %s", secret, resp.Value) + } + resp, err = pluginRequest(client, getPath, Request{SecretName: ""}) + if err != nil { + t.Fatal(err) + } + if p.get != 2 { + t.Fatalf("expected get 2, got %d", p.get) + } + if resp.Err == "" { + t.Fatalf("expected missing secret") + } + resp, err = pluginRequest(client, getPath, Request{SecretName: "another-secret", SecretLabels: map[string]string{"prefix": "p-"}}) + if err != nil { + t.Fatal(err) + } + if resp.Err != "" { + t.Fatalf("error while getting secret: %v", resp.Err) + } + if !bytes.EqualFold(append([]byte("p-"), secret...), resp.Value) { + t.Fatalf("expecting secret value %s, got %s", secret, resp.Value) + } +} + +func pluginRequest(client *http.Client, method string, req Request) (*Response, error) { + b, err := json.Marshal(req) + if err != nil { + return nil, err + } + resp, err := client.Post("http://localhost"+method, "application/json", bytes.NewReader(b)) + if err != nil { + return nil, err + } + var vResp Response + err = json.NewDecoder(resp.Body).Decode(&vResp) + if err != nil { + return nil, err + } + + return &vResp, nil +} + +type testPlugin struct { + get int +} + +var secret = []byte("secret") + +func (p *testPlugin) Get(req Request) Response { + p.get++ + if req.SecretName == "" { + return Response{Err: "missing secret name"} + } + if prefix, exists := req.SecretLabels["prefix"]; exists { + return Response{Value: append([]byte(prefix), secret...)} + } + return Response{Value: secret} +} diff --git a/volume/README.md b/volume/README.md new file mode 100644 index 0000000..395aa64 --- /dev/null +++ b/volume/README.md @@ -0,0 +1,36 @@ +# Docker volume extension api. + +Go handler to create external volume extensions for Docker. + +## Usage + +This library is designed to be integrated in your program. + +1. Implement the `volume.Driver` interface. +2. Initialize a `volume.Handler` with your implementation. +3. Call either `ServeTCP` or `ServeUnix` from the `volume.Handler`. + +### Example using TCP sockets: + +```go + d := MyVolumeDriver{} + h := volume.NewHandler(d) + h.ServeTCP("test_volume", ":8080") +``` + +### Example using Unix sockets: + +```go + d := MyVolumeDriver{} + h := volume.NewHandler(d) + u, _ := user.Lookup("root") + gid, _ := strconv.Atoi(u.Gid) + h.ServeUnix("test_volume", gid) +``` + +## Full example plugins + +- https://github.com/calavera/docker-volume-glusterfs +- https://github.com/calavera/docker-volume-keywhiz +- https://github.com/quobyte/docker-volume +- https://github.com/NimbleStorage/Nemo diff --git a/volume/api.go b/volume/api.go new file mode 100644 index 0000000..387e82a --- /dev/null +++ b/volume/api.go @@ -0,0 +1,221 @@ +package volume + +import ( + "net/http" + + "github.com/docker/go-plugins-helpers/sdk" +) + +const ( + // DefaultDockerRootDirectory is the default directory where volumes will be created. + DefaultDockerRootDirectory = "/var/lib/docker-volumes" + + manifest = `{"Implements": ["VolumeDriver"]}` + createPath = "/VolumeDriver.Create" + getPath = "/VolumeDriver.Get" + listPath = "/VolumeDriver.List" + removePath = "/VolumeDriver.Remove" + hostVirtualPath = "/VolumeDriver.Path" + mountPath = "/VolumeDriver.Mount" + unmountPath = "/VolumeDriver.Unmount" + capabilitiesPath = "/VolumeDriver.Capabilities" +) + +// CreateRequest is the structure that docker's requests are deserialized to. +type CreateRequest struct { + Name string + Options map[string]string `json:"Opts,omitempty"` +} + +// RemoveRequest structure for a volume remove request +type RemoveRequest struct { + Name string +} + +// MountRequest structure for a volume mount request +type MountRequest struct { + Name string + ID string +} + +// MountResponse structure for a volume mount response +type MountResponse struct { + Mountpoint string +} + +// UnmountRequest structure for a volume unmount request +type UnmountRequest struct { + Name string + ID string +} + +// PathRequest structure for a volume path request +type PathRequest struct { + Name string +} + +// PathResponse structure for a volume path response +type PathResponse struct { + Mountpoint string +} + +// GetRequest structure for a volume get request +type GetRequest struct { + Name string +} + +// GetResponse structure for a volume get response +type GetResponse struct { + Volume *Volume +} + +// ListResponse structure for a volume list response +type ListResponse struct { + Volumes []*Volume +} + +// CapabilitiesResponse structure for a volume capability response +type CapabilitiesResponse struct { + Capabilities Capability +} + +// Volume represents a volume object for use with `Get` and `List` requests +type Volume struct { + Name string + Mountpoint string `json:",omitempty"` + CreatedAt string `json:",omitempty"` + Status map[string]interface{} `json:",omitempty"` +} + +// Capability represents the list of capabilities a volume driver can return +type Capability struct { + Scope string +} + +// ErrorResponse is a formatted error message that docker can understand +type ErrorResponse struct { + Err string +} + +// NewErrorResponse creates an ErrorResponse with the provided message +func NewErrorResponse(msg string) *ErrorResponse { + return &ErrorResponse{Err: msg} +} + +// Driver represent the interface a driver must fulfill. +type Driver interface { + Create(*CreateRequest) error + List() (*ListResponse, error) + Get(*GetRequest) (*GetResponse, error) + Remove(*RemoveRequest) error + Path(*PathRequest) (*PathResponse, error) + Mount(*MountRequest) (*MountResponse, error) + Unmount(*UnmountRequest) error + Capabilities() *CapabilitiesResponse +} + +// Handler forwards requests and responses between the docker daemon and the plugin. +type Handler struct { + driver Driver + sdk.Handler +} + +// NewHandler initializes the request handler with a driver implementation. +func NewHandler(driver Driver) *Handler { + h := &Handler{driver, sdk.NewHandler(manifest)} + h.initMux() + return h +} + +func (h *Handler) initMux() { + h.HandleFunc(createPath, func(w http.ResponseWriter, r *http.Request) { + req := &CreateRequest{} + err := sdk.DecodeRequest(w, r, req) + if err != nil { + return + } + err = h.driver.Create(req) + if err != nil { + sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) + return + } + sdk.EncodeResponse(w, struct{}{}, false) + }) + h.HandleFunc(removePath, func(w http.ResponseWriter, r *http.Request) { + req := &RemoveRequest{} + err := sdk.DecodeRequest(w, r, req) + if err != nil { + return + } + err = h.driver.Remove(req) + if err != nil { + sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) + return + } + sdk.EncodeResponse(w, struct{}{}, false) + }) + h.HandleFunc(mountPath, func(w http.ResponseWriter, r *http.Request) { + req := &MountRequest{} + err := sdk.DecodeRequest(w, r, req) + if err != nil { + return + } + res, err := h.driver.Mount(req) + if err != nil { + sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) + return + } + sdk.EncodeResponse(w, res, false) + }) + h.HandleFunc(hostVirtualPath, func(w http.ResponseWriter, r *http.Request) { + req := &PathRequest{} + err := sdk.DecodeRequest(w, r, req) + if err != nil { + return + } + res, err := h.driver.Path(req) + if err != nil { + sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) + return + } + sdk.EncodeResponse(w, res, false) + }) + h.HandleFunc(getPath, func(w http.ResponseWriter, r *http.Request) { + req := &GetRequest{} + err := sdk.DecodeRequest(w, r, req) + if err != nil { + return + } + res, err := h.driver.Get(req) + if err != nil { + sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) + return + } + sdk.EncodeResponse(w, res, false) + }) + h.HandleFunc(unmountPath, func(w http.ResponseWriter, r *http.Request) { + req := &UnmountRequest{} + err := sdk.DecodeRequest(w, r, req) + if err != nil { + return + } + err = h.driver.Unmount(req) + if err != nil { + sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) + return + } + sdk.EncodeResponse(w, struct{}{}, false) + }) + h.HandleFunc(listPath, func(w http.ResponseWriter, r *http.Request) { + res, err := h.driver.List() + if err != nil { + sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) + return + } + sdk.EncodeResponse(w, res, false) + }) + + h.HandleFunc(capabilitiesPath, func(w http.ResponseWriter, r *http.Request) { + sdk.EncodeResponse(w, h.driver.Capabilities(), false) + }) +} diff --git a/volume/api_test.go b/volume/api_test.go new file mode 100644 index 0000000..69b5f9b --- /dev/null +++ b/volume/api_test.go @@ -0,0 +1,211 @@ +package volume + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "testing" + + "github.com/docker/go-connections/sockets" +) + +func TestHandler(t *testing.T) { + p := &testPlugin{} + h := NewHandler(p) + l := sockets.NewInmemSocket("test", 0) + go h.Serve(l) + defer l.Close() + + client := &http.Client{Transport: &http.Transport{ + Dial: l.Dial, + }} + + // Create + _, err := pluginRequest(client, createPath, &CreateRequest{Name: "foo"}) + if err != nil { + t.Fatal(err) + } + if p.create != 1 { + t.Fatalf("expected create 1, got %d", p.create) + } + + // Get + resp, err := pluginRequest(client, getPath, &GetRequest{Name: "foo"}) + if err != nil { + t.Fatal(err) + } + var gResp *GetResponse + if err := json.NewDecoder(resp).Decode(&gResp); err != nil { + t.Fatal(err) + } + if gResp.Volume.Name != "foo" { + t.Fatalf("expected volume `foo`, got %v", gResp.Volume) + } + if p.get != 1 { + t.Fatalf("expected get 1, got %d", p.get) + } + + // List + resp, err = pluginRequest(client, listPath, nil) + if err != nil { + t.Fatal(err) + } + var lResp *ListResponse + if err := json.NewDecoder(resp).Decode(&lResp); err != nil { + t.Fatal(err) + } + if len(lResp.Volumes) != 1 { + t.Fatalf("expected 1 volume, got %v", lResp.Volumes) + } + if lResp.Volumes[0].Name != "foo" { + t.Fatalf("expected volume `foo`, got %v", lResp.Volumes[0]) + } + if p.list != 1 { + t.Fatalf("expected list 1, got %d", p.list) + } + + // Path + if _, err := pluginRequest(client, hostVirtualPath, &PathRequest{Name: "foo"}); err != nil { + t.Fatal(err) + } + if p.path != 1 { + t.Fatalf("expected path 1, got %d", p.path) + } + + // Mount + if _, err := pluginRequest(client, mountPath, &MountRequest{Name: "foo"}); err != nil { + t.Fatal(err) + } + if p.mount != 1 { + t.Fatalf("expected mount 1, got %d", p.mount) + } + + // Unmount + if _, err := pluginRequest(client, unmountPath, &UnmountRequest{Name: "foo"}); err != nil { + t.Fatal(err) + } + if p.unmount != 1 { + t.Fatalf("expected unmount 1, got %d", p.unmount) + } + + // Remove + _, err = pluginRequest(client, removePath, &RemoveRequest{Name: "foo"}) + if err != nil { + t.Fatal(err) + } + if p.remove != 1 { + t.Fatalf("expected remove 1, got %d", p.remove) + } + + // Capabilities + resp, err = pluginRequest(client, capabilitiesPath, nil) + var cResp *CapabilitiesResponse + if err := json.NewDecoder(resp).Decode(&cResp); err != nil { + t.Fatal(err) + } + + if p.capabilities != 1 { + t.Fatalf("expected remove 1, got %d", p.capabilities) + } +} + +func pluginRequest(client *http.Client, method string, req interface{}) (io.Reader, error) { + b, err := json.Marshal(req) + if err != nil { + return nil, err + } + if req == nil { + b = []byte{} + } + resp, err := client.Post("http://localhost"+method, "application/json", bytes.NewReader(b)) + if err != nil { + return nil, err + } + + return resp.Body, nil +} + +type testPlugin struct { + volumes []string + create int + get int + list int + path int + mount int + unmount int + remove int + capabilities int +} + +func (p *testPlugin) Create(req *CreateRequest) error { + p.create++ + p.volumes = append(p.volumes, req.Name) + return nil +} + +func (p *testPlugin) Get(req *GetRequest) (*GetResponse, error) { + p.get++ + for _, v := range p.volumes { + if v == req.Name { + return &GetResponse{Volume: &Volume{Name: v}}, nil + } + } + return &GetResponse{}, fmt.Errorf("no such volume") +} + +func (p *testPlugin) List() (*ListResponse, error) { + p.list++ + var vols []*Volume + for _, v := range p.volumes { + vols = append(vols, &Volume{Name: v}) + } + return &ListResponse{Volumes: vols}, nil +} + +func (p *testPlugin) Remove(req *RemoveRequest) error { + p.remove++ + for i, v := range p.volumes { + if v == req.Name { + p.volumes = append(p.volumes[:i], p.volumes[i+1:]...) + return nil + } + } + return fmt.Errorf("no such volume") +} + +func (p *testPlugin) Path(req *PathRequest) (*PathResponse, error) { + p.path++ + for _, v := range p.volumes { + if v == req.Name { + return &PathResponse{}, nil + } + } + return &PathResponse{}, fmt.Errorf("no such volume") +} + +func (p *testPlugin) Mount(req *MountRequest) (*MountResponse, error) { + p.mount++ + for _, v := range p.volumes { + if v == req.Name { + return &MountResponse{}, nil + } + } + return &MountResponse{}, fmt.Errorf("no such volume") +} + +func (p *testPlugin) Unmount(req *UnmountRequest) error { + p.unmount++ + for _, v := range p.volumes { + if v == req.Name { + return nil + } + } + return fmt.Errorf("no such volume") +} + +func (p *testPlugin) Capabilities() *CapabilitiesResponse { + p.capabilities++ + return &CapabilitiesResponse{Capabilities: Capability{Scope: "local"}} +} diff --git a/volume/shim/shim.go b/volume/shim/shim.go new file mode 100644 index 0000000..197d9b4 --- /dev/null +++ b/volume/shim/shim.go @@ -0,0 +1,109 @@ +package shim + +import ( + "github.com/docker/docker/volume" + volumeplugin "github.com/docker/go-plugins-helpers/volume" +) + +type shimDriver struct { + d volume.Driver +} + +// NewHandlerFromVolumeDriver creates a plugin handler from an existing volume +// driver. This could be used, for instance, by the `local` volume driver built-in +// to Docker Engine and it would create a plugin from it that maps plugin API calls +// directly to any volume driver that satifies the volume.Driver interface from +// Docker Engine. +func NewHandlerFromVolumeDriver(d volume.Driver) *volumeplugin.Handler { + return volumeplugin.NewHandler(&shimDriver{d}) +} + +func (d *shimDriver) Create(req *volumeplugin.CreateRequest) error { + _, err := d.d.Create(req.Name, req.Options) + return err +} + +func (d *shimDriver) List() (*volumeplugin.ListResponse, error) { + var res *volumeplugin.ListResponse + ls, err := d.d.List() + if err != nil { + return &volumeplugin.ListResponse{}, err + } + vols := make([]*volumeplugin.Volume, len(ls)) + + for i, v := range ls { + vol := &volumeplugin.Volume{ + Name: v.Name(), + Mountpoint: v.Path(), + } + vols[i] = vol + } + res.Volumes = vols + return res, nil +} + +func (d *shimDriver) Get(req *volumeplugin.GetRequest) (*volumeplugin.GetResponse, error) { + var res *volumeplugin.GetResponse + v, err := d.d.Get(req.Name) + if err != nil { + return &volumeplugin.GetResponse{}, err + } + res.Volume = &volumeplugin.Volume{ + Name: v.Name(), + Mountpoint: v.Path(), + Status: v.Status(), + } + return &volumeplugin.GetResponse{}, nil +} + +func (d *shimDriver) Remove(req *volumeplugin.RemoveRequest) error { + v, err := d.d.Get(req.Name) + if err != nil { + return err + } + if err := d.d.Remove(v); err != nil { + return err + } + return nil +} + +func (d *shimDriver) Path(req *volumeplugin.PathRequest) (*volumeplugin.PathResponse, error) { + var res *volumeplugin.PathResponse + v, err := d.d.Get(req.Name) + if err != nil { + return &volumeplugin.PathResponse{}, err + } + res.Mountpoint = v.Path() + return res, nil +} + +func (d *shimDriver) Mount(req *volumeplugin.MountRequest) (*volumeplugin.MountResponse, error) { + var res *volumeplugin.MountResponse + v, err := d.d.Get(req.Name) + if err != nil { + return &volumeplugin.MountResponse{}, err + } + pth, err := v.Mount(req.ID) + if err != nil { + return &volumeplugin.MountResponse{}, err + } + res.Mountpoint = pth + return res, nil +} + +func (d *shimDriver) Unmount(req *volumeplugin.UnmountRequest) error { + v, err := d.d.Get(req.Name) + if err != nil { + return err + } + if err := v.Unmount(req.ID); err != nil { + return err + } + return nil +} + +func (d *shimDriver) Capabilities() *volumeplugin.CapabilitiesResponse { + var res *volumeplugin.CapabilitiesResponse + res.Capabilities = volumeplugin.Capability{Scope: d.d.Scope()} + return res +} diff --git a/volume/shim/shim_test.go b/volume/shim/shim_test.go new file mode 100644 index 0000000..7a3c3f1 --- /dev/null +++ b/volume/shim/shim_test.go @@ -0,0 +1,66 @@ +package shim + +import ( + "bytes" + "encoding/json" + "net/http" + "testing" + + "github.com/docker/docker/volume" + "github.com/docker/go-connections/sockets" + volumeplugin "github.com/docker/go-plugins-helpers/volume" +) + +type testVolumeDriver struct{} +type testVolume struct{} + +func (testVolume) Name() string { return "" } +func (testVolume) Path() string { return "" } +func (testVolume) Mount() (string, error) { return "", nil } +func (testVolume) Unmount() error { return nil } +func (testVolume) DriverName() string { return "" } + +func (testVolumeDriver) Name() string { return "" } +func (testVolumeDriver) Create(string, map[string]string) (volume.Volume, error) { return nil, nil } +func (testVolumeDriver) Remove(volume.Volume) error { return nil } +func (testVolumeDriver) List() ([]volume.Volume, error) { return nil, nil } +func (testVolumeDriver) Get(name string) (volume.Volume, error) { return nil, nil } +func (testVolumeDriver) Scope() string { return "local" } + +func TestVolumeDriver(t *testing.T) { + h := NewHandlerFromVolumeDriver(testVolumeDriver{}) + l := sockets.NewInmemSocket("test", 0) + go h.Serve(l) + defer l.Close() + + client := &http.Client{Transport: &http.Transport{ + Dial: l.Dial, + }} + + resp, err := pluginRequest(client, "/VolumeDriver.Create", &volumeplugin.CreateRequest{Name: "foo"}) + if err != nil { + t.Fatalf(err.Error()) + } + + if resp.Err != "" { + t.Fatalf("error while creating volume: %v", err) + } +} + +func pluginRequest(client *http.Client, method string, req *volumeplugin.CreateRequest) (*volumeplugin.ErrorResponse, error) { + b, err := json.Marshal(req) + if err != nil { + return nil, err + } + resp, err := client.Post("http://localhost"+method, "application/json", bytes.NewReader(b)) + if err != nil { + return nil, err + } + var vResp volumeplugin.ErrorResponse + err = json.NewDecoder(resp.Body).Decode(&vResp) + if err != nil { + return nil, err + } + + return &vResp, nil +} |