├── .buildkite └── pipeline.yml ├── .gitignore ├── Dockerfile ├── LICENSE.txt ├── README.md ├── cmd └── sockguard │ └── main.go ├── director.go ├── director_test.go ├── director_upstream_state_test.go ├── docker-compose.yml ├── examples └── cgroup_parent │ ├── Dockerfile │ ├── README.md │ ├── ci_agent_dev │ ├── Dockerfile │ ├── README.md │ └── start.sh │ └── docker-compose.yml ├── fixtures ├── containers_create_10_expected.json ├── containers_create_10_in.json ├── containers_create_11_expected.json ├── containers_create_11_in.json ├── containers_create_12_expected.json ├── containers_create_12_in.json ├── containers_create_13_expected.json ├── containers_create_13_in.json ├── containers_create_14_expected.json ├── containers_create_14_in.json ├── containers_create_1_expected.json ├── containers_create_1_in.json ├── containers_create_2_expected.json ├── containers_create_2_in.json ├── containers_create_3_expected.json ├── containers_create_3_in.json ├── containers_create_4_expected.json ├── containers_create_4_in.json ├── containers_create_5_expected.json ├── containers_create_5_in.json ├── containers_create_6_expected.json ├── containers_create_6_in.json ├── containers_create_7_expected.json ├── containers_create_7_in.json ├── containers_create_8_expected.json ├── containers_create_8_in.json ├── containers_create_9_expected.json ├── containers_create_9_in.json ├── networks_create_1_expected.json ├── networks_create_1_in.json ├── networks_create_2_expected.json ├── networks_create_2_in.json ├── networks_create_3_expected.json ├── networks_create_3_in.json ├── networks_create_4_expected.json └── networks_create_4_in.json ├── go.mod ├── go.sum └── socketproxy ├── proxy.go └── proxy_test.go /.buildkite/pipeline.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - label: "🔎" 3 | command: go test ./... 4 | plugins: 5 | - golang#v2.0.0: 6 | version: 1.11.1 7 | import: github.com/buildkite/sockguard 8 | environment: 9 | - GO111MODULE=on 10 | 11 | - wait 12 | - label: "🛠" 13 | plugins: 14 | - golang-cross-compile#v1.3.0: 15 | build: "." 16 | import: github.com/buildkite/sockguard 17 | targets: 18 | - version: 1.11.1 19 | goos: linux 20 | goarch: amd64 21 | gomodule: "on" 22 | - version: 1.11.1 23 | goos: windows 24 | goarch: amd64 25 | gomodule: "on" 26 | - version: 1.11.1 27 | goos: darwin 28 | goarch: amd64 29 | gomodule: "on" 30 | 31 | - wait 32 | - label: ":docker:" 33 | branches: master 34 | plugins: 35 | - docker-login#v2.0.1: ~ 36 | - docker-compose#v2.5.1: 37 | push: sockguard 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /sockguard 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.11-alpine as builder 2 | RUN apk add --no-cache ca-certificates git 3 | ENV GO111MODULE=on 4 | WORKDIR /go/src/github.com/buildkite/sockguard 5 | ADD go.mod go.sum ./ 6 | RUN go mod download 7 | COPY . . 8 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ 9 | go build -a -installsuffix cgo -ldflags="-w -s" -o /go/bin/sockguard ./cmd/sockguard 10 | 11 | FROM scratch 12 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 13 | COPY --from=builder /go/bin/sockguard /sockguard 14 | ENTRYPOINT [ "/sockguard" ] 15 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2014-2018 Buildkite Pty Ltd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > :warning: **This is a discontinued experiment**: Much better technology now exists to solve this problem, such as [secure docker-in-docker with sysbox](https://github.com/nestybox/sysbox). 2 | 3 | # Sockguard 4 | 5 | Safely providing access to a docker daemon to untrusted containers is [challenging](https://docs.docker.com/engine/security/https/). By design docker doesn't provide any sort of access control over what can be done over that socket, so anything which has the socket has the same influence over your system as the user that docker is running as. This includes the host filesystem via mounts. To compound this, the default configuration of most docker installations has docker running with root privileges. 6 | 7 | In a CI environment, builds need to be able to create containers, networks and volumes with access to a limit set of filesystem directories on the host. They need to have access to the resources they create and be able to destroy them as makes sense in the build. 8 | 9 | ## Usage 10 | 11 | This runs a guarded socket that is then passed into a container for Docker outside of Docker usage. 12 | 13 | ``` 14 | sockguard --upstream-socket /var/run/docker.sock --allow-bind "$PWD" & 15 | docker -H unix://$PWD/sockguard.sock run --rm -v $PWD/sockguard.sock:/var/lib/docker.sock buildkite/agent:3 16 | ``` 17 | 18 | ## How it works 19 | 20 | Sockguard provides a proxy around the docker socket that is passed to the container that safely runs the build. The proxied socket adds restrictions around what can be accessed via the socket. 21 | 22 | When an image, container, volume or network is created it gets given a label of `com.buildkite.sockguard.owner={identifier}`, which is the identifier of the specific instance of the socket proxy. Each subsequent operation is checked against this ownership socket and only a match (or in the case of images, the lack of an owner), is allowed to proceed for read or write operations. 23 | 24 | In addition, creation of containers imposes certain restrictions to ensure that containers are contained: 25 | 26 | * No `privileged` mode is allowed 27 | * By default no host bind mounts are allowed, but certain paths can be white-listed with `--allow-bind` 28 | * No `host` network mode is allowed 29 | 30 | There is also an option to set `cgroup-parent` on container creation. This is useful for restricting CPU/Memory resources of containers spawned via this proxy (eg. when using a container scheduler). 31 | 32 | ## How is this solved elsewhere? 33 | 34 | Docker provides an ACL system in their Enterprise product, and also provides a plugin API with authorization hooks. At this stage the plugin eco-system is still pretty new. The advantage of using a local socket is that you can use filesystem permissions to control access to it. 35 | 36 | Another approach is Docker-in-docker, which is unfortunately [slow and fraught with issues](https://jpetazzo.github.io/2015/09/03/do-not-use-docker-in-docker-for-ci/). 37 | 38 | ## Implementation status 39 | 40 | Very alpha! Most of the high risk endpoints are covered decently. Not yet ready for production usage. 41 | 42 | Based off https://docs.docker.com/engine/api/v1.32. 43 | 44 | ### Containers (Done) 45 | 46 | - [x] GET /containers/json (filtered) 47 | - [x] POST /containers/create (label added) 48 | - [x] GET /containers/{id}/json (ownership check) 49 | - [x] GET /containers/{id}/top (ownership check) 50 | - [x] GET /containers/{id}/logs (ownership check) 51 | - [x] GET /containers/{id}/changes (ownership check) 52 | - [x] GET /containers/{id}/export (ownership check) 53 | - [x] GET /containers/{id}/stats (ownership check) 54 | - [x] POST /containers/{id}/resize (ownership check) 55 | - [x] POST /containers/{id}/start (ownership check) 56 | - [x] POST /containers/{id}/stop (ownership check) 57 | - [x] POST /containers/{id}/restart (ownership check) 58 | - [x] POST /containers/{id}/kill (ownership check) 59 | - [x] POST /containers/{id}/update (ownership check) 60 | - [x] POST /containers/{id}/rename (ownership check) 61 | - [x] POST /containers/{id}/pause (ownership check) 62 | - [x] POST /containers/{id}/unpause (ownership check) 63 | - [x] POST /containers/{id}/attach (ownership check) 64 | - [x] GET /containers/{id}/attach/ws (ownership check) 65 | - [x] POST /containers/{id}/wait (ownership check) 66 | - [x] DELETE /containers/{id} (ownership check) 67 | - [x] HEAD /containers/{id}/archive (ownership check) 68 | - [x] GET /containers/{id}/archive (ownership check) 69 | - [x] PUT /containers/{id}/archive (ownership check) 70 | - [x] POST /containers/{id}/exec (ownership check) 71 | - [x] POST /containers/prune (filtered) 72 | - [x] POST /exec/{id}/start 73 | - [x] POST /exec/{id}/resize 74 | - [x] GET /exec/{id}/json 75 | 76 | ### Images (Partial) 77 | 78 | - [x] GET /images/json (filtered) 79 | - [x] POST /build (label added) 80 | - [x] POST /build/prune (filtered) 81 | - [ ] POST /images/create 82 | - [x] GET /images/{name}/json 83 | - [x] GET /images/{name}/history 84 | - [x] PUSH /images/{name}/push 85 | - [x] POST /images/{name}/tag 86 | - [x] REMOVE /images/{name} 87 | - [ ] GET /images/search 88 | - [x] POST /images/prune 89 | - [ ] POST /commit 90 | - [x] POST /images/{name}/get 91 | - [ ] GET /images/get 92 | - [ ] POST /images/load 93 | 94 | ### Networks (Done) 95 | 96 | - [x] GET /networks 97 | - [x] GET /networks/{id} 98 | - [x] POST /networks/create 99 | - [x] POST /networks/{id}/connect 100 | - [x] POST /networks/{id}/disconnect 101 | - [x] POST /networks/prune 102 | 103 | ### Volumes 104 | 105 | - [x] GET /volumes 106 | - [x] POST /volumes/create 107 | - [x] GET /volumes/{name} 108 | - [x] DELETE /volumes/{name} 109 | - [x] POST /volumes/prune 110 | 111 | ### Swarm (Disabled) 112 | 113 | - [ ] GET /swarm 114 | - [ ] POST /swarm/init 115 | - [ ] POST /swarm/join 116 | - [ ] POST /swarm/leave 117 | - [ ] POST /swarm/update 118 | - [ ] GET /swarm/unlockkey 119 | - [ ] POST /swarm/unlock 120 | - [ ] GET /nodes 121 | - [ ] GET /nodes/{id} 122 | - [ ] DELETE /nodes/{id} 123 | - [ ] POST /nodes/{id}/update 124 | - [ ] GET /services 125 | - [ ] POST /services/create 126 | - [ ] GET /services/{id} 127 | - [ ] DELETE /services/{id} 128 | - [ ] POST /services/{id}/update 129 | - [ ] GET /services/{id}/logs 130 | - [ ] GET /tasks 131 | - [ ] GET /tasks/{id} 132 | - [ ] GET /tasks/{id}/logs 133 | - [ ] GET /secrets 134 | - [ ] POST /secrets/create 135 | - [ ] GET /secrets/{id} 136 | - [ ] DELETE /secrets/{id} 137 | - [ ] POST /secrets/{id}/update 138 | 139 | ### Plugins (Disabled) 140 | 141 | - [ ] GET /plugins 142 | - [ ] GET /plugins/privileges 143 | - [ ] POST /plugins/pull 144 | - [ ] GET /plugins/{name}/json 145 | - [ ] DELETE /plugins/{name} 146 | - [ ] POST /plugins/{name}/enable 147 | - [ ] POST /plugins/{name}/disable 148 | - [ ] POST /plugins/{name}/upgrade 149 | - [ ] POST /plugins/create 150 | - [ ] POST /plugins/{name}/set 151 | 152 | ### System 153 | 154 | - [ ] POST /auth 155 | - [x] POST /info 156 | - [ ] GET /version 157 | - [x] GET /_ping (direct) 158 | - [x] GET /events 159 | - [ ] GET /system/df 160 | - [ ] GET /distribution/{name}/json 161 | - [ ] POST /session 162 | 163 | ### Configs 164 | 165 | - [ ] GET /configs 166 | - [ ] POST /configs/create 167 | - [ ] GET /configs/{id} 168 | - [ ] DELETE /configs/{id} 169 | - [ ] POST /configs/{id}/update 170 | 171 | ## Example: Running in Amazon ECS with CgroupParent 172 | 173 | Let's say you are spawning a `sockguard` instance per ECS task, to pass through a guarded Docker socker to some worker (eg. a CI worker). You may want to apply the same CPU/Memory constraints as the ECS task. This can be done via a bash wrapper to `/sockguard` in a sidecar container (ensure you have `bash`, `curl` and `jq` available): 174 | 175 | ```bash 176 | #!/bin/bash 177 | 178 | set -euo pipefail 179 | 180 | ########################### 181 | 182 | # Detect CgroupParent first 183 | 184 | # A) Use the container ID from /proc/self/cgroup 185 | # (note: this works fine on a systemd based system, need to adjust the grep on pre-systemd? fine for us right now) 186 | container_id=$(awk -F/ '/1:name=systemd/ {print $NF}' /proc/self/cgroup) 187 | 188 | # B) Use the hostname 189 | # (note: works, as long as someone doesnt start the container with --hostname. A) preferred for now) 190 | # container_id="$HOSTNAME" 191 | 192 | if [ -z "$container_id" ]; then 193 | echo "sockguard/start.sh: container_id empty?" 194 | exit 1 195 | fi 196 | 197 | # Get the CgroupParent via the Docker API 198 | container_inspect_url="http:/v1.37/containers/${container_id}/json" 199 | cgroup_parent=$(curl -s --unix-socket /var/run/docker.sock "$container_inspect_url" | jq -r .HostConfig.CgroupParent) 200 | 201 | if [ -z "$cgroup_parent" ]; then 202 | echo "sockguard/start.sh: cgroup_parent empty? (from Docker API)" 203 | exit 1 204 | fi 205 | 206 | ########################### 207 | 208 | # Start sockguard with some args 209 | exec /sockguard -cgroup-parent '${cgroup_parent}' -owner-label '${cgroup_parent}' ...other args... 210 | ``` 211 | 212 | ## Development 213 | 214 | Sockguard is built with Golang 1.11 and modules. 215 | 216 | ``` 217 | export GO111MODULE=on 218 | go run ./cmd/sockguard 219 | ``` 220 | -------------------------------------------------------------------------------- /cmd/sockguard/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "net" 9 | "net/http" 10 | "os" 11 | "os/signal" 12 | "strconv" 13 | "strings" 14 | "syscall" 15 | 16 | "github.com/buildkite/sockguard" 17 | "github.com/buildkite/sockguard/socketproxy" 18 | ) 19 | 20 | var ( 21 | debug bool 22 | ) 23 | 24 | func init() { 25 | flag.BoolVar(&debug, "debug", false, "Show debugging logging for the socket") 26 | } 27 | 28 | func main() { 29 | filename := flag.String("filename", "sockguard.sock", "The guarded socket to create") 30 | socketMode := flag.String("mode", "0600", "Permissions of the guarded socket") 31 | socketUid := flag.Int("uid", -1, "The UID (owner) of the guarded socket (defaults to -1 - process owner)") 32 | socketGid := flag.Int("gid", -1, "The GID (group) of the guarded socket (defaults to -1 - process group)") 33 | upstream := flag.String("upstream-socket", "/var/run/docker.sock", "The path to the original docker socket") 34 | owner := flag.String("owner-label", "", "The value to use as the owner of the socket, defaults to the process id") 35 | allowBind := flag.String("allow-bind", "", "A path to allow host binds to occur under") 36 | allowHostModeNetworking := flag.Bool("allow-host-mode-networking", false, "Allow containers to run with --net host") 37 | cgroupParent := flag.String("cgroup-parent", "", "Set CgroupParent to an arbitrary value on new containers") 38 | user := flag.String("user", "", "Forces --user on containers") 39 | dockerLink := flag.String("docker-link", "", "Add a Docker --link from any spawned containers to another container") 40 | containerJoinNetwork := flag.String("container-join-network", "", "Always connect this container to new user defined bridge networks (and disconnect on delete)") 41 | containerJoinNetworkAlias := flag.String("container-join-network-alias", "", "Alias for network connection of specified container (Requires -container-join-network)") 42 | flag.Parse() 43 | 44 | if debug { 45 | socketproxy.Debug = true 46 | } 47 | 48 | if *socketUid == -1 { 49 | // Default to the process UID 50 | sockUid := os.Getuid() 51 | socketUid = &sockUid 52 | } 53 | if *socketGid == -1 { 54 | // Default to the process GID 55 | sockGid := os.Getgid() 56 | socketGid = &sockGid 57 | } 58 | 59 | useSocketMode, err := strconv.ParseUint(*socketMode, 0, 32) 60 | if err != nil { 61 | log.Fatal(err) 62 | } 63 | 64 | if *owner == "" { 65 | *owner = fmt.Sprintf("sockguard-pid-%d", os.Getpid()) 66 | } 67 | 68 | var allowBinds []string 69 | 70 | if *allowBind != "" { 71 | allowBinds = strings.Split(*allowBind, ",") 72 | } 73 | 74 | if *cgroupParent != "" { 75 | debugf("Setting CgroupParent on new containers to '%s'", *cgroupParent) 76 | } 77 | 78 | // These should not be used together, one or the other 79 | if *dockerLink != "" && *containerJoinNetwork != "" { 80 | log.Fatal("Error: -docker-link and -join-network should not be used together.") 81 | } 82 | 83 | // Make sure -container-join-network-alias is only specified if -container-join-network is set 84 | if *containerJoinNetworkAlias != "" && *containerJoinNetwork == "" { 85 | log.Fatal("Error: -container-join-network-alias requires -container-join-network") 86 | } 87 | 88 | proxyHttpClient := http.Client{ 89 | Transport: &http.Transport{ 90 | DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { 91 | debugf("Dialing directly") 92 | return net.Dial("unix", *upstream) 93 | }, 94 | }, 95 | } 96 | 97 | if *dockerLink != "" { 98 | container, _, err := parseDockerLink(*dockerLink) 99 | if err != nil { 100 | log.Fatal(err) 101 | } 102 | dockerLinkContainerExists, err := sockguard.CheckContainerExists(&proxyHttpClient, container) 103 | if err != nil { 104 | log.Fatal(err.Error()) 105 | } 106 | if dockerLinkContainerExists == false { 107 | log.Fatalf("Error: -docker-link '%s' specified but this container does not exist", container) 108 | } 109 | debugf("Adding a Docker --link to new containers: '%s'", *dockerLink) 110 | } 111 | 112 | if *containerJoinNetwork != "" { 113 | // TODOLATER: how much does it matter that this container is running? 114 | joinNetworkContainerExists, err := sockguard.CheckContainerExists(&proxyHttpClient, *containerJoinNetwork) 115 | if err != nil { 116 | log.Fatal(err.Error()) 117 | } 118 | if joinNetworkContainerExists == false { 119 | log.Fatalf("Error: -container-join-network '%s' specified but this container does not exist", *containerJoinNetwork) 120 | } 121 | debugContainerJoinNetworkAlias := "" 122 | if *containerJoinNetworkAlias != "" { 123 | debugContainerJoinNetworkAlias = fmt.Sprintf(" (using alias '%s')", *containerJoinNetworkAlias) 124 | } 125 | debugf("Container '%s'%s will always be connected to user defined bridged networks created via sockguard", *containerJoinNetwork, debugContainerJoinNetworkAlias) 126 | } 127 | 128 | proxy := socketproxy.New(*upstream, &sockguard.RulesDirector{ 129 | AllowBinds: allowBinds, 130 | AllowHostModeNetworking: *allowHostModeNetworking, 131 | ContainerCgroupParent: *cgroupParent, 132 | ContainerDockerLink: *dockerLink, 133 | ContainerJoinNetwork: *containerJoinNetwork, 134 | ContainerJoinNetworkAlias: *containerJoinNetworkAlias, 135 | Owner: *owner, 136 | User: *user, 137 | Client: &proxyHttpClient, 138 | }) 139 | listener, err := net.Listen("unix", *filename) 140 | if err != nil { 141 | log.Fatal(err) 142 | } 143 | 144 | if *socketUid >= 0 && *socketGid >= 0 { 145 | if err = os.Chown(*filename, *socketUid, *socketGid); err != nil { 146 | _ = listener.Close() 147 | log.Fatal(err) 148 | } 149 | } 150 | 151 | if err = os.Chmod(*filename, os.FileMode(useSocketMode)); err != nil { 152 | _ = listener.Close() 153 | log.Fatal(err) 154 | } 155 | 156 | fmt.Printf("Listening on %s (socket UID %d GID %d permissions %s), upstream is %s\n", *filename, *socketUid, *socketGid, *socketMode, *upstream) 157 | 158 | sigCh := make(chan os.Signal, 1) 159 | signal.Notify(sigCh, os.Interrupt, os.Kill, syscall.SIGTERM) 160 | 161 | go func() { 162 | sig := <-sigCh 163 | debugf("Caught signal %s: shutting down.", sig) 164 | _ = listener.Close() 165 | os.Exit(0) 166 | }() 167 | 168 | if err = http.Serve(listener, proxy); err != nil { 169 | log.Fatal(err) 170 | } 171 | } 172 | 173 | // extractd from director.go, to be refactored out 174 | func parseDockerLink(input string) (string, string, error) { 175 | if splitInput := strings.Split(input, ":"); len(splitInput) == 1 { 176 | // container 177 | return splitInput[0], splitInput[0], nil 178 | } else if len(splitInput) == 2 { 179 | // container:alias 180 | return splitInput[0], splitInput[1], nil 181 | } 182 | return "", "", fmt.Errorf( 183 | "Unable to parse docker link %q, expected container:alias", input) 184 | } 185 | 186 | func debugf(format string, v ...interface{}) { 187 | if debug { 188 | fmt.Printf(format+"\n", v...) 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /director.go: -------------------------------------------------------------------------------- 1 | package sockguard 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "log" 10 | "net/http" 11 | "path" 12 | "path/filepath" 13 | "regexp" 14 | "strings" 15 | 16 | "github.com/buildkite/sockguard/socketproxy" 17 | ) 18 | 19 | const ( 20 | apiVersion = "1.32" 21 | ownerKey = "com.buildkite.sockguard.owner" 22 | ) 23 | 24 | var ( 25 | versionRegex = regexp.MustCompile(`^/v\d\.\d+\b`) 26 | ) 27 | 28 | type RulesDirector struct { 29 | Client *http.Client 30 | Owner string 31 | AllowBinds []string 32 | AllowHostModeNetworking bool 33 | ContainerCgroupParent string 34 | // TODOLATER: some enforcement at the struct level to ensure DockerLink + JoinNetwork are mutually exclusive (pick one) 35 | ContainerDockerLink string 36 | ContainerJoinNetwork string 37 | ContainerJoinNetworkAlias string 38 | User string 39 | } 40 | 41 | func writeError(w http.ResponseWriter, msg string, code int) { 42 | w.Header().Set("Content-Type", "application/json") 43 | w.WriteHeader(code) 44 | _ = json.NewEncoder(w).Encode(map[string]string{ 45 | "message": msg, 46 | }) 47 | } 48 | 49 | func (r *RulesDirector) Direct(l socketproxy.Logger, req *http.Request, upstream http.Handler) http.Handler { 50 | var match = func(method string, pattern string) bool { 51 | if method != "*" && method != req.Method { 52 | return false 53 | } 54 | path := req.URL.Path 55 | if versionRegex.MatchString(path) { 56 | path = versionRegex.ReplaceAllString(path, "") 57 | } 58 | re := regexp.MustCompile(pattern) 59 | return re.MatchString(path) 60 | } 61 | 62 | var errorHandler = func(msg string, code int) http.Handler { 63 | return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 64 | l.Printf("Handler returned error %q", msg) 65 | writeError(w, msg, code) 66 | return 67 | }) 68 | } 69 | 70 | switch { 71 | case match(`GET`, `^/(_ping|version|info)$`): 72 | return upstream 73 | case match(`GET`, `^/events$`): 74 | return r.addLabelsToQueryStringFilters(l, req, upstream) 75 | 76 | // Container related endpoints 77 | case match(`POST`, `^/containers/create$`): 78 | return r.handleContainerCreate(l, req, upstream) 79 | case match(`POST`, `^/containers/prune$`): 80 | return r.addLabelsToQueryStringFilters(l, req, upstream) 81 | case match(`GET`, `^/containers/json$`): 82 | return r.addLabelsToQueryStringFilters(l, req, upstream) 83 | case match(`*`, `^/(containers|exec)/(\w+)\b`): 84 | if ok, err := r.checkOwner(l, "containers", false, req); ok { 85 | return upstream 86 | } else if err == errInspectNotFound { 87 | l.Printf("Container not found, allowing") 88 | return upstream 89 | } else if err != nil { 90 | return errorHandler(err.Error(), http.StatusInternalServerError) 91 | } 92 | return errorHandler("Unauthorized access to container", http.StatusUnauthorized) 93 | 94 | // Build related endpoints 95 | case match(`POST`, `^/build$`): 96 | return r.handleBuild(l, req, upstream) 97 | 98 | // Image related endpoints 99 | case match(`GET`, `^/images/json$`): 100 | return r.addLabelsToQueryStringFilters(l, req, upstream) 101 | case match(`POST`, `^/images/create$`): 102 | return upstream 103 | case match(`POST`, `^/images/(create|search|get|load)$`): 104 | break 105 | case match(`POST`, `^/images/prune$`): 106 | return r.addLabelsToQueryStringFilters(l, req, upstream) 107 | case match(`*`, `^/images/(\w+)\b`): 108 | if ok, err := r.checkOwner(l, "images", true, req); ok { 109 | return upstream 110 | } else if err == errInspectNotFound { 111 | l.Printf("Image not found, allowing") 112 | return upstream 113 | } else if err != nil { 114 | return errorHandler(err.Error(), http.StatusInternalServerError) 115 | } 116 | return errorHandler("Unauthorized access to image", http.StatusUnauthorized) 117 | 118 | // Network related endpoints 119 | case match(`GET`, `^/networks$`): 120 | return r.addLabelsToQueryStringFilters(l, req, upstream) 121 | case match(`POST`, `^/networks/create$`): 122 | return r.handleNetworkCreate(l, req, upstream) 123 | case match(`POST`, `^/networks/prune$`): 124 | return r.addLabelsToQueryStringFilters(l, req, upstream) 125 | case match(`DELETE`, `^/networks/(.+)$`): 126 | return r.handleNetworkDelete(l, req, upstream) 127 | case match(`GET`, `^/networks/(.+)$`), 128 | match(`POST`, `^/networks/(.+)/(connect|disconnect)$`): 129 | if ok, err := r.checkOwner(l, "networks", true, req); ok { 130 | return upstream 131 | } else if err == errInspectNotFound { 132 | l.Printf("Network not found, allowing") 133 | return upstream 134 | } else if err != nil { 135 | return errorHandler(err.Error(), http.StatusInternalServerError) 136 | } 137 | return errorHandler("Unauthorized access to network", http.StatusUnauthorized) 138 | 139 | // Volumes related endpoints 140 | case match(`GET`, `^/volumes$`): 141 | return r.addLabelsToQueryStringFilters(l, req, upstream) 142 | case match(`POST`, `^/volumes/create$`): 143 | return r.addLabelsToBody(l, req, upstream) 144 | case match(`POST`, `^/volumes/prune$`): 145 | return r.addLabelsToQueryStringFilters(l, req, upstream) 146 | case match(`GET`, `^/volumes/([-\w]+)$`), match(`DELETE`, `^/volumes/(-\w+)$`): 147 | if ok, err := r.checkOwner(l, "volumes", true, req); ok { 148 | return upstream 149 | } else if err == errInspectNotFound { 150 | l.Printf("Volume not found, allowing") 151 | return upstream 152 | } else if err != nil { 153 | return errorHandler(err.Error(), http.StatusInternalServerError) 154 | } 155 | return errorHandler("Unauthorized access to volume", http.StatusUnauthorized) 156 | 157 | } 158 | 159 | return errorHandler(req.Method+" "+req.URL.Path+" not implemented yet", http.StatusNotImplemented) 160 | } 161 | 162 | var identifierPatterns = []*regexp.Regexp{ 163 | regexp.MustCompile(`^/containers/(.+?)(?:/\w+)?$`), 164 | regexp.MustCompile(`^/networks/(.+?)(?:/\w+)?$`), 165 | regexp.MustCompile(`^/volumes/([-\w]+?)(?:/\w+)?$`), 166 | regexp.MustCompile(`^/images/(.+?)/(?:json|history|push|tag)$`), 167 | regexp.MustCompile(`^/images/([^/]+)$`), 168 | regexp.MustCompile(`^/images/(\w+/[^/]+)$`), 169 | } 170 | 171 | // Check owner takes a request for /vx.x/{kind}/{id} and uses inspect to see if it's 172 | // got the correct owner label. 173 | func (r *RulesDirector) checkOwner(l socketproxy.Logger, kind string, allowEmpty bool, req *http.Request) (bool, error) { 174 | path := req.URL.Path 175 | if versionRegex.MatchString(path) { 176 | path = versionRegex.ReplaceAllString(path, "") 177 | } 178 | 179 | var identifier string 180 | 181 | for _, re := range identifierPatterns { 182 | if m := re.FindStringSubmatch(path); len(m) > 0 { 183 | identifier = m[1] 184 | break 185 | } 186 | } 187 | 188 | if identifier == "" { 189 | return false, fmt.Errorf("Unable to find an identifier in %s", path) 190 | } 191 | 192 | return r.checkIdentifierOwner(l, kind, identifier, allowEmpty) 193 | } 194 | 195 | func (r *RulesDirector) checkIdentifierOwner(l socketproxy.Logger, kind string, identifier string, allowEmpty bool) (bool, error) { 196 | 197 | l.Printf("Looking up identifier %q", identifier) 198 | 199 | labels, err := r.inspectLabels(kind, identifier) 200 | if err != nil { 201 | return false, err 202 | } 203 | 204 | l.Printf("Labels for %s/%s: %v", kind, identifier, labels) 205 | 206 | if val, exists := labels[ownerKey]; exists && val == r.Owner { 207 | l.Printf("Allow, %s/%s matches owner %q", kind, identifier, r.Owner) 208 | return true, nil 209 | } else if !exists && allowEmpty { 210 | l.Printf("Allow, %s/%s has no owner", kind, identifier) 211 | return true, nil 212 | } else { 213 | l.Printf("Deny, %s/%s has owner %q, wanted %q", kind, identifier, val, r.Owner) 214 | return false, nil 215 | } 216 | } 217 | 218 | func (r *RulesDirector) handleContainerCreate(l socketproxy.Logger, req *http.Request, upstream http.Handler) http.Handler { 219 | return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 220 | var decoded map[string]interface{} 221 | 222 | if err := json.NewDecoder(req.Body).Decode(&decoded); err != nil { 223 | writeError(w, err.Error(), http.StatusBadRequest) 224 | return 225 | } 226 | 227 | // first we add our labels 228 | addLabel(ownerKey, r.Owner, decoded["Labels"]) 229 | 230 | l.Printf("Labels: %#v", decoded["Labels"]) 231 | 232 | // prevent privileged mode 233 | privileged, ok := decoded["HostConfig"].(map[string]interface{})["Privileged"].(bool) 234 | if ok && privileged { 235 | l.Printf("Denied privileged on container create") 236 | writeError(w, "Containers aren't allowed to run as privileged", http.StatusUnauthorized) 237 | return 238 | } 239 | 240 | // filter binds, don't allow host binds 241 | binds, ok := decoded["HostConfig"].(map[string]interface{})["Binds"].([]interface{}) 242 | if ok { 243 | for _, bind := range binds { 244 | isAllowed, err := r.isBindAllowed(l, bind.(string), r.AllowBinds, req) 245 | if err != nil { 246 | writeError(w, err.Error(), http.StatusBadRequest) 247 | return 248 | } 249 | if !isAllowed { 250 | l.Printf("Denied host bind %q", bind) 251 | writeError(w, "Host binds aren't allowed", http.StatusUnauthorized) 252 | return 253 | } 254 | } 255 | } 256 | 257 | // prevent host and container network mode 258 | networkMode, ok := decoded["HostConfig"].(map[string]interface{})["NetworkMode"].(string) 259 | if ok && networkMode == "host" && (!r.AllowHostModeNetworking) { 260 | l.Printf("Denied host network mode on container create") 261 | writeError(w, "Containers aren't allowed to use host networking", http.StatusUnauthorized) 262 | return 263 | } 264 | 265 | if r.ContainerCgroupParent == "" { 266 | // Flag is disable,d prevent setting a user defined CgroupParent for host safety 267 | cgroupParent, ok := decoded["HostConfig"].(map[string]interface{})["CgroupParent"].(string) 268 | if ok == true && cgroupParent != "" { 269 | l.Printf("Denied requested CgroupParent '%s' on container create (flag disabled)", cgroupParent) 270 | writeError(w, fmt.Sprintf("Containers aren't allowed to set their own CgroupParent (received '%s')", cgroupParent), http.StatusUnauthorized) 271 | return 272 | } 273 | } else { 274 | // Apply the specified CgroupParent, flag enabled 275 | l.Printf("Applied CgroupParent '%s'", r.ContainerCgroupParent) 276 | decoded["HostConfig"].(map[string]interface{})["CgroupParent"] = r.ContainerCgroupParent 277 | } 278 | 279 | // apply ContainerDockerLink if enabled 280 | if r.ContainerDockerLink != "" { 281 | // NOTE: The way Links are parsed out is not elegant, but doing it in two phases was the only answer 282 | // I had to avoid nil panics in the end, while being able to iterate over non-nil slices of interfaces. 283 | links, ok := decoded["HostConfig"].(map[string]interface{})["Links"] 284 | if ok { 285 | // Need to populate this from the interface value 286 | newLinks := []string{} 287 | if links != nil { 288 | useLinks := links.([]interface{}) 289 | newLinks = make([]string, len(useLinks)) 290 | for i, v := range useLinks { 291 | newLinks[i] = fmt.Sprint(v) 292 | } 293 | } 294 | l.Printf("Appending '%s' to Links for /containers/create", r.ContainerDockerLink) 295 | newLinks = append(newLinks, r.ContainerDockerLink) 296 | decoded["HostConfig"].(map[string]interface{})["Links"] = newLinks 297 | } else { 298 | l.Printf("Denied container create: unable to parse Links %+v", links) 299 | writeError(w, fmt.Sprintf("Denied container create: unable to parse Links %+v", links), http.StatusBadRequest) 300 | return 301 | } 302 | } 303 | 304 | // force user 305 | if r.User != "" { 306 | decoded["User"] = r.User 307 | l.Printf("Forcing user to '%s'", r.User) 308 | } 309 | 310 | encoded, err := json.Marshal(decoded) 311 | if err != nil { 312 | writeError(w, err.Error(), http.StatusBadRequest) 313 | return 314 | } 315 | 316 | // reset it so that upstream can read it again 317 | req.ContentLength = int64(len(encoded)) 318 | req.Body = ioutil.NopCloser(bytes.NewReader(encoded)) 319 | 320 | upstream.ServeHTTP(w, req) 321 | }) 322 | } 323 | 324 | func (r *RulesDirector) isBindAllowed(l socketproxy.Logger, bind string, allowed []string, req *http.Request) (bool, error) { 325 | 326 | chunks := strings.Split(bind, ":") 327 | 328 | // host-src:container-dest 329 | // host-src:container-dest:ro 330 | // volume-name:container-dest 331 | // volume-name:container-dest:ro 332 | 333 | // TODO: better heuristic for host-src vs volume-name 334 | if strings.ContainsAny(chunks[0], ".\\/") { 335 | hostSrc := filepath.FromSlash(path.Clean("/" + chunks[0])) 336 | 337 | for _, allowedPath := range allowed { 338 | if allowedPath == hostSrc || strings.HasPrefix(hostSrc, allowedPath+"/") { 339 | return true, nil 340 | } 341 | } 342 | 343 | return false, nil 344 | } 345 | 346 | // There is a request to bind volume, let's check the ownership 347 | volumeName := chunks[0] 348 | isOwner, err := r.checkIdentifierOwner(l, "volumes", volumeName, false) 349 | if err != nil { 350 | return false, err 351 | } 352 | 353 | return isOwner, nil 354 | } 355 | 356 | type containerDockerLink struct { 357 | // ID or Name 358 | Container string 359 | Alias string 360 | } 361 | 362 | func splitContainerDockerLink(input string) (*containerDockerLink, error) { 363 | if input == "" { 364 | return &containerDockerLink{}, fmt.Errorf("Container Link is empty string, cannot proceed") 365 | } 366 | splitInput := strings.Split(input, ":") 367 | if len(splitInput) == 1 { 368 | // container 369 | return &containerDockerLink{Container: splitInput[0], Alias: splitInput[0]}, nil 370 | } else if len(splitInput) == 2 { 371 | // container:alias 372 | return &containerDockerLink{Container: splitInput[0], Alias: splitInput[1]}, nil 373 | } else { 374 | return &containerDockerLink{}, fmt.Errorf("Expected 'name-or-id' or 'name-or-id:alias' (1 or 2 elements, : delimited), got %d elements from '%s'", len(splitInput), input) 375 | } 376 | } 377 | 378 | func (r *RulesDirector) handleNetworkCreate(l socketproxy.Logger, req *http.Request, upstream http.Handler) http.Handler { 379 | return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 380 | // Not using modifyRequestBody since we need the decoded network name further down, less duplication this way 381 | var decoded map[string]interface{} 382 | 383 | if err := json.NewDecoder(req.Body).Decode(&decoded); err != nil { 384 | http.Error(w, err.Error(), http.StatusBadRequest) 385 | return 386 | } 387 | // Get the newly created network name from original request, for use later (if ContainerDockerLink or ContainerJoinNetwork is enabled) 388 | networkIdOrName, ok := decoded["Name"].(string) 389 | if ok == false { 390 | http.Error(w, "Failed to obtain network name from request", http.StatusBadRequest) 391 | return 392 | } 393 | 394 | addLabel(ownerKey, r.Owner, decoded["Labels"]) 395 | 396 | encoded, err := json.Marshal(decoded) 397 | if err != nil { 398 | http.Error(w, err.Error(), http.StatusBadRequest) 399 | return 400 | } 401 | 402 | // reset it so that upstream can read it again 403 | req.ContentLength = int64(len(encoded)) 404 | req.Body = ioutil.NopCloser(bytes.NewReader(encoded)) 405 | 406 | // Do the network creation 407 | upstream.ServeHTTP(w, req) 408 | 409 | // If ContainerDockerLink or ContainerJoinNetwork is enabled, link the container to the newly created network 410 | if r.ContainerDockerLink != "" || r.ContainerJoinNetwork != "" { 411 | // We have networkIdOrName already, see above 412 | 413 | useContainer := "" 414 | useContainerEndpointConfig := "" 415 | useContainerAlias := "" 416 | if r.ContainerDockerLink != "" { 417 | // Parse the ContainerDockerLink out 418 | cdl, err := splitContainerDockerLink(r.ContainerDockerLink) 419 | if err != nil { 420 | http.Error(w, err.Error(), http.StatusBadRequest) 421 | return 422 | } 423 | useContainer = cdl.Container 424 | } else if r.ContainerJoinNetwork != "" { 425 | useContainer = r.ContainerJoinNetwork 426 | // If network alias specified, set it. 427 | if r.ContainerJoinNetworkAlias != "" { 428 | useContainerEndpointConfig = fmt.Sprintf(",\"EndpointConfig\":{\"Aliases\":[\"%s\"]}", r.ContainerJoinNetworkAlias) 429 | useContainerAlias = fmt.Sprintf(" (with Alias '%s')", r.ContainerJoinNetworkAlias) 430 | } 431 | } 432 | 433 | // Do the container attach 434 | attachJson := fmt.Sprintf("{\"Container\":\"%s\"%s}", useContainer, useContainerEndpointConfig) 435 | attachReq, err := http.NewRequest("POST", fmt.Sprintf("http://unix/v%s/networks/%s/connect", apiVersion, networkIdOrName), strings.NewReader(attachJson)) 436 | attachReq.Header.Set("Content-Type", "application/json") 437 | //debugf("Network Connect Request: %+v\n", attachReq) 438 | if err != nil { 439 | http.Error(w, err.Error(), http.StatusBadRequest) 440 | return 441 | } 442 | attachResp, err := r.Client.Do(attachReq) 443 | if err != nil { 444 | http.Error(w, err.Error(), http.StatusBadRequest) 445 | return 446 | } 447 | if attachResp.StatusCode != 200 { 448 | http.Error(w, fmt.Sprintf("Expected 200 got %d when attaching Container ID/Name '%s' to Network '%s' (after creating)", attachResp.StatusCode, useContainer, networkIdOrName), http.StatusBadRequest) 449 | return 450 | } 451 | // Attached, move on 452 | l.Printf("Attached Container ID/Name '%s'%s to Network '%s' (after creating)", useContainer, useContainerAlias, networkIdOrName) 453 | } 454 | }) 455 | } 456 | 457 | func (r *RulesDirector) handleNetworkDelete(l socketproxy.Logger, req *http.Request, upstream http.Handler) http.Handler { 458 | return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 459 | ok, err := r.checkOwner(l, "networks", true, req) 460 | if ok == false { 461 | errMsg := fmt.Sprintf("Deleting network denied, no error") 462 | if err != nil { 463 | errMsg = fmt.Sprintf("Deleting network denied: %s", err.Error()) 464 | } 465 | l.Printf(errMsg) 466 | http.Error(w, errMsg, http.StatusUnauthorized) 467 | return 468 | } 469 | 470 | // If ContainerDockerLink or ContainerJoinNetwork is enabled, detach the container from the network before deleting 471 | if r.ContainerDockerLink != "" || r.ContainerJoinNetwork != "" { 472 | // Parse out the Network ID (or Name) to use for detaching linked container 473 | splitPath := strings.Split(req.URL.String(), "/") 474 | if len(splitPath) != 4 { 475 | http.Error(w, fmt.Sprintf("Unable to parse out URL '%s', expected 4 components, got %d", req.URL.String(), len(splitPath)), http.StatusBadRequest) 476 | return 477 | } 478 | networkIdOrName := splitPath[3] 479 | 480 | useContainer := "" 481 | if r.ContainerDockerLink != "" { 482 | // Parse the ContainerDockerLink out 483 | cdl, err := splitContainerDockerLink(r.ContainerDockerLink) 484 | if err != nil { 485 | http.Error(w, err.Error(), http.StatusBadRequest) 486 | return 487 | } 488 | useContainer = cdl.Container 489 | } else if r.ContainerJoinNetwork != "" { 490 | useContainer = r.ContainerJoinNetwork 491 | } 492 | 493 | // Do the container detach (forced, so we can delete the network) 494 | detachJson := fmt.Sprintf("{\"Container\":\"%s\",\"Force\":true}", useContainer) 495 | detachReq, err := http.NewRequest("POST", fmt.Sprintf("http://unix/v%s/networks/%s/disconnect", apiVersion, networkIdOrName), strings.NewReader(detachJson)) 496 | detachReq.Header.Set("Content-Type", "application/json") 497 | //debugf("Network Disconnect Request: %+v\n", detachReq) 498 | if err != nil { 499 | http.Error(w, err.Error(), http.StatusBadRequest) 500 | return 501 | } 502 | detachResp, err := r.Client.Do(detachReq) 503 | if err != nil { 504 | http.Error(w, err.Error(), http.StatusBadRequest) 505 | return 506 | } 507 | if detachResp.StatusCode != 200 { 508 | errString := fmt.Sprintf("Expected 200 got %d when detaching Container ID/Name '%s' from Network '%s' (before deleting)", detachResp.StatusCode, useContainer, networkIdOrName) 509 | l.Printf(errString) 510 | http.Error(w, errString, http.StatusBadRequest) 511 | return 512 | } 513 | // Detached, move on 514 | l.Printf("Detached Container ID/Name '%s' from Network '%s' (before deleting)", useContainer, networkIdOrName) 515 | } 516 | 517 | // Do the network delete 518 | upstream.ServeHTTP(w, req) 519 | }) 520 | } 521 | 522 | func addLabel(label, value string, into interface{}) { 523 | switch t := into.(type) { 524 | case map[string]interface{}: 525 | t[label] = value 526 | default: 527 | log.Printf("Found unhandled label type %T: %v", into, t) 528 | } 529 | } 530 | 531 | func (r *RulesDirector) addLabelsToBody(l socketproxy.Logger, req *http.Request, upstream http.Handler) http.Handler { 532 | return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 533 | err := modifyRequestBody(req, func(decoded map[string]interface{}) { 534 | addLabel(ownerKey, r.Owner, decoded["Labels"]) 535 | }) 536 | if err != nil { 537 | http.Error(w, err.Error(), http.StatusBadRequest) 538 | return 539 | } 540 | upstream.ServeHTTP(w, req) 541 | }) 542 | } 543 | 544 | func (r *RulesDirector) addLabelsToQueryStringFilters(l socketproxy.Logger, req *http.Request, upstream http.Handler) http.Handler { 545 | return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 546 | var q = req.URL.Query() 547 | var filters = map[string][]interface{}{} 548 | 549 | // parse existing filters from querystring 550 | if qf := q.Get("filters"); qf != "" { 551 | var existing map[string]interface{} 552 | 553 | if err := json.NewDecoder(strings.NewReader(qf)).Decode(&existing); err != nil { 554 | http.Error(w, err.Error(), http.StatusBadRequest) 555 | return 556 | } 557 | 558 | // different docker implementations send us different data structures 559 | for k, v := range existing { 560 | switch tv := v.(type) { 561 | // sometimes we get a map of value=true 562 | case map[string]interface{}: 563 | for mk := range tv { 564 | filters[k] = append(filters[k], mk) 565 | } 566 | // sometimes we get a slice of values (from docker-compose) 567 | case []interface{}: 568 | filters[k] = append(filters[k], tv...) 569 | default: 570 | http.Error(w, fmt.Sprintf("Unhandled filter type of %T", v), http.StatusBadRequest) 571 | return 572 | } 573 | } 574 | } 575 | 576 | // add an label slice if none exists 577 | if _, exists := filters["label"]; !exists { 578 | filters["label"] = []interface{}{} 579 | } 580 | 581 | // add an owner label 582 | label := ownerKey + "=" + r.Owner 583 | l.Printf("Adding label %v to label filters %v", label, filters["label"]) 584 | filters["label"] = append(filters["label"], label) 585 | 586 | // encode back into json 587 | encoded, err := json.Marshal(filters) 588 | if err != nil { 589 | http.Error(w, err.Error(), http.StatusBadRequest) 590 | return 591 | } 592 | 593 | q.Set("filters", string(encoded)) 594 | req.URL.RawQuery = q.Encode() 595 | 596 | upstream.ServeHTTP(w, req) 597 | }) 598 | } 599 | 600 | func (r *RulesDirector) handleBuild(l socketproxy.Logger, req *http.Request, upstream http.Handler) http.Handler { 601 | return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 602 | // Parse out query string to modify it 603 | var q = req.URL.Query() 604 | 605 | // Owner label 606 | l.Printf("Adding label %s=%s to querystring: %s %s", 607 | ownerKey, r.Owner, req.URL.Path, req.URL.RawQuery) 608 | var labels = map[string]string{} 609 | if encoded := q.Get("labels"); encoded != "" { 610 | if err := json.NewDecoder(strings.NewReader(encoded)).Decode(&labels); err != nil { 611 | http.Error(w, err.Error(), http.StatusBadRequest) 612 | return 613 | } 614 | } 615 | labels[ownerKey] = r.Owner 616 | encoded, err := json.Marshal(labels) 617 | if err != nil { 618 | http.Error(w, err.Error(), http.StatusBadRequest) 619 | return 620 | } 621 | q.Set("labels", string(encoded)) 622 | 623 | // CgroupParent 624 | cgroupParent := q.Get("cgroupparent") 625 | // Prevent setting a CgroupParent if flag is disabled, for host safety 626 | if cgroupParent != "" { 627 | l.Printf("Denied requested CgroupParent '%s' on build (flag disabled)", cgroupParent) 628 | writeError(w, fmt.Sprintf("Image builds aren't allowed to set their own CgroupParent (received '%s')", cgroupParent), http.StatusUnauthorized) 629 | return 630 | } 631 | // Apply the specified CgroupParent, if flag enabled 632 | if r.ContainerCgroupParent != "" { 633 | l.Printf("Applied CgroupParent '%s' to image build", r.ContainerCgroupParent) 634 | q.Set("cgroupparent", r.ContainerCgroupParent) 635 | } 636 | 637 | // Rebuild the query string ready to forward request 638 | req.URL.RawQuery = q.Encode() 639 | 640 | upstream.ServeHTTP(w, req) 641 | }) 642 | } 643 | 644 | var errInspectNotFound = errors.New("Not found") 645 | 646 | func (r *RulesDirector) getInto(into interface{}, path string, arg ...interface{}) error { 647 | u := fmt.Sprintf("http://docker/v%s%s", apiVersion, fmt.Sprintf(path, arg...)) 648 | 649 | resp, err := r.Client.Get(u) 650 | if err != nil { 651 | return err 652 | } 653 | defer resp.Body.Close() 654 | 655 | if resp.StatusCode == http.StatusNotFound { 656 | return errInspectNotFound 657 | } else if resp.StatusCode != http.StatusOK { 658 | return fmt.Errorf("Request to %q failed: %s", u, resp.Status) 659 | } 660 | 661 | return json.NewDecoder(resp.Body).Decode(into) 662 | } 663 | 664 | func (r *RulesDirector) inspectLabels(kind, id string) (map[string]string, error) { 665 | switch kind { 666 | case "containers", "images": 667 | var result struct { 668 | Config struct { 669 | Labels map[string]string 670 | } 671 | } 672 | 673 | if err := r.getInto(&result, "/"+kind+"/%s/json", id); err != nil { 674 | return nil, err 675 | } 676 | 677 | return result.Config.Labels, nil 678 | case "networks", "volumes": 679 | var result struct { 680 | Labels map[string]string 681 | } 682 | 683 | if err := r.getInto(&result, "/"+kind+"/%s", id); err != nil { 684 | return nil, err 685 | } 686 | 687 | return result.Labels, nil 688 | } 689 | 690 | return nil, fmt.Errorf("Unknown kind %q", kind) 691 | } 692 | 693 | func modifyRequestBody(req *http.Request, f func(filters map[string]interface{})) error { 694 | var decoded map[string]interface{} 695 | 696 | if err := json.NewDecoder(req.Body).Decode(&decoded); err != nil { 697 | return err 698 | } 699 | 700 | f(decoded) 701 | 702 | encoded, err := json.Marshal(decoded) 703 | if err != nil { 704 | return err 705 | } 706 | 707 | // reset it so that upstream can read it again 708 | req.ContentLength = int64(len(encoded)) 709 | req.Body = ioutil.NopCloser(bytes.NewReader(encoded)) 710 | 711 | return nil 712 | } 713 | 714 | // For -join-network startup pre-check 715 | func CheckContainerExists(client *http.Client, idOrName string) (bool, error) { 716 | inspectReq, err := http.NewRequest("GET", fmt.Sprintf("http://unix/v%s/containers/%s/json", apiVersion, idOrName), nil) 717 | if err != nil { 718 | return false, err 719 | } 720 | 721 | resp, err := client.Do(inspectReq) 722 | if err != nil { 723 | return false, err 724 | } 725 | 726 | if resp.StatusCode == http.StatusOK { 727 | // Exists 728 | return true, nil 729 | } else if resp.StatusCode == http.StatusNotFound { 730 | // Does not exist 731 | return false, nil 732 | } else { 733 | return false, fmt.Errorf("Unexpected response code %d received from Docker daemon when checking if Container '%s' exists", resp.StatusCode, idOrName) 734 | } 735 | } 736 | -------------------------------------------------------------------------------- /director_test.go: -------------------------------------------------------------------------------- 1 | package sockguard 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "net/http" 10 | "net/http/httptest" 11 | "net/url" 12 | "os" 13 | "regexp" 14 | "strings" 15 | "testing" 16 | 17 | "github.com/google/go-cmp/cmp" 18 | ) 19 | 20 | // Credit: http://hassansin.github.io/Unit-Testing-http-client-in-Go 21 | type roundTripFunc func(req *http.Request) *http.Response 22 | 23 | func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { 24 | return f(req), nil 25 | } 26 | 27 | // Reusable mock RulesDirector instance 28 | func mockRulesDirector() *RulesDirector { 29 | return &RulesDirector{ 30 | Client: &http.Client{}, 31 | Owner: "test-owner", 32 | AllowHostModeNetworking: false, 33 | } 34 | } 35 | 36 | // Reusable mock RulesDirector instance - with "state" management of mocked upstream Docker daemon 37 | // Just containers/networks initially 38 | func mockRulesDirectorWithUpstreamState(us *upstreamState) *RulesDirector { 39 | rd := mockRulesDirector() 40 | rd.Client = mockRulesDirectorHttpClientWithUpstreamState(us) 41 | return rd 42 | } 43 | 44 | func mockRulesDirectorHttpClientWithUpstreamState(us *upstreamState) *http.Client { 45 | return &http.Client{ 46 | Transport: roundTripFunc(func(req *http.Request) *http.Response { 47 | resp := http.Response{ 48 | // Must be set to non-nil value or it panics 49 | Header: make(http.Header), 50 | } 51 | re1 := regexp.MustCompile("^/v(.*)/containers/(.*)/json$") 52 | // TODOLATER: adjust re2 to make /json suffix optional, for non-GET? 53 | re2 := regexp.MustCompile("^/v(.*)/images/(.*)/json$") 54 | // NOTE: this regex may not cover all name variations, but will cover enough to fulfil tests 55 | re3 := regexp.MustCompile("^/v(.*)/networks/([A-Za-z0-9]+)(/connect|/disconnect)?$") 56 | re4 := regexp.MustCompile("^/v(.*)/volumes/(.*)$") 57 | switch { 58 | case re1.MatchString(req.URL.Path): 59 | if req.Method == "GET" { 60 | // inspect container - /containers/{id}/json 61 | parsePath := re1.FindStringSubmatch(req.URL.Path) 62 | if len(parsePath) == 3 { 63 | // Vary the response based on container ID (easiest option) 64 | // Partial JSON result, enough to satisfy the inspectLabels() struct 65 | if us.doesContainerExist(parsePath[2]) == false { 66 | resp.StatusCode = 404 67 | resp.Body = ioutil.NopCloser(bytes.NewBufferString(fmt.Sprintf("{\"message\":\"No such container: %s\"}", parsePath[2]))) 68 | } else { 69 | containerOwnerLabel := us.ownerLabelContent(us.getContainerOwner(parsePath[2])) 70 | resp.StatusCode = 200 71 | resp.Body = ioutil.NopCloser(bytes.NewBufferString(fmt.Sprintf("{\"Id\":\"%s\",\"Config\":{\"Labels\":{%s}}}", parsePath[2], containerOwnerLabel))) 72 | } 73 | } else { 74 | resp.StatusCode = 501 75 | resp.Body = ioutil.NopCloser(bytes.NewBufferString(fmt.Sprintf("Failure parsing container ID from path - %s\n", req.URL.Path))) 76 | } 77 | } else { 78 | resp.StatusCode = 501 79 | resp.Body = ioutil.NopCloser(bytes.NewBufferString(fmt.Sprintf("Unsupported HTTP method %s for %s\n", req.Method, req.URL.Path))) 80 | } 81 | case re2.MatchString(req.URL.Path): 82 | switch req.Method { 83 | case "GET": 84 | // inspect image - /images/{id}/json 85 | parsePath := re2.FindStringSubmatch(req.URL.Path) 86 | if len(parsePath) == 3 { 87 | // Vary the response based on image ID (easiest option) 88 | // Partial JSON result, enough to satisfy the inspectLabels() struct 89 | if us.doesImageExist(parsePath[2]) == false { 90 | resp.StatusCode = 404 91 | resp.Body = ioutil.NopCloser(bytes.NewBufferString(fmt.Sprintf("{\"message\":\"no such image: %s: No such image: %s:latest\"}", parsePath[2], parsePath[2]))) 92 | } else { 93 | imageOwnerLabel := us.ownerLabelContent(us.getImageOwner(parsePath[2])) 94 | resp.StatusCode = 200 95 | resp.Body = ioutil.NopCloser(bytes.NewBufferString(fmt.Sprintf("{\"Id\":\"%s\",\"Config\":{\"Labels\":{%s}}}", parsePath[2], imageOwnerLabel))) 96 | } 97 | } else { 98 | resp.StatusCode = 501 99 | resp.Body = ioutil.NopCloser(bytes.NewBufferString(fmt.Sprintf("Failure parsing image ID from path - %s\n", req.URL.Path))) 100 | } 101 | default: 102 | resp.StatusCode = 501 103 | resp.Body = ioutil.NopCloser(bytes.NewBufferString(fmt.Sprintf("Unsupported HTTP method %s for %s\n", req.Method, req.URL.Path))) 104 | } 105 | case re3.MatchString(req.URL.Path): 106 | parsePath := re3.FindStringSubmatch(req.URL.Path) 107 | if len(parsePath) != 4 { 108 | resp.StatusCode = 501 109 | resp.Body = ioutil.NopCloser(bytes.NewBufferString(fmt.Sprintf("Failure parsing network ID/target from path - %s\n", req.URL.Path))) 110 | return &resp 111 | } 112 | switch req.Method { 113 | case "GET": 114 | // inspect network - /networks/{id} 115 | // Vary the response based on network ID (easiest option) 116 | // Partial JSON result, enough to satisfy the inspectLabels() struct 117 | if us.doesNetworkExist(parsePath[2]) == false { 118 | resp.StatusCode = 404 119 | resp.Body = ioutil.NopCloser(bytes.NewBufferString(fmt.Sprintf("{\"message\":\"network %s not found\"}", parsePath[2]))) 120 | } else { 121 | networkOwnerLabel := us.ownerLabelContent(us.getNetworkOwner(parsePath[2])) 122 | resp.StatusCode = 200 123 | resp.Body = ioutil.NopCloser(bytes.NewBufferString(fmt.Sprintf("{\"Id\":\"%s\",\"Labels\":{%s}}", parsePath[2], networkOwnerLabel))) 124 | } 125 | case "POST": 126 | switch parsePath[3] { 127 | case "/connect", "/disconnect": 128 | // connect container to network - /networks/{id}/connect 129 | // disconnect container to network - /networks/{id}/disconnect 130 | // Verify the Content-Type = application/json, will 400 without it on Docker daemon 131 | contentType := req.Header.Get("Content-Type") 132 | if contentType != "application/json" { 133 | resp.StatusCode = 400 134 | resp.Body = ioutil.NopCloser(bytes.NewBufferString(fmt.Sprintf("{\"message\":\"Content-Type specified (%s) must be 'application/json'\"}", contentType))) 135 | return &resp 136 | } 137 | // Parse out the Container from request body 138 | var decoded map[string]interface{} 139 | if err := json.NewDecoder(req.Body).Decode(&decoded); err != nil { 140 | resp.StatusCode = 500 141 | resp.Body = ioutil.NopCloser(bytes.NewBufferString(err.Error())) 142 | return &resp 143 | } 144 | useContainer := decoded["Container"].(string) 145 | // Bare minimum response format here, mostly response code 146 | if us.doesNetworkExist(parsePath[2]) == false { 147 | resp.StatusCode = 404 148 | resp.Body = ioutil.NopCloser(bytes.NewBufferString(fmt.Sprintf("{\"message\":\"network %s not found\"}", parsePath[2]))) 149 | } else { 150 | var err error 151 | if parsePath[3] == "/connect" { 152 | useContainerAliases := []string{} 153 | // If there are Aliases specified, pass them in. 154 | parseContainerEndpointConfig, ok := decoded["EndpointConfig"] 155 | if ok { 156 | parseContainerAliases, ok2 := parseContainerEndpointConfig.(map[string]interface{})["Aliases"].([]interface{}) 157 | if ok2 { 158 | for _, parseContainerAlias := range parseContainerAliases { 159 | parsedContainerAlias := parseContainerAlias.(string) 160 | if parsedContainerAlias != "" { 161 | useContainerAliases = append(useContainerAliases, parsedContainerAlias) 162 | } 163 | } 164 | } 165 | } 166 | err = us.connectContainerToNetwork(useContainer, parsePath[2], useContainerAliases) 167 | } else if parsePath[3] == "/disconnect" { 168 | err = us.disconnectContainerToNetwork(useContainer, parsePath[2]) 169 | } 170 | if err != nil { 171 | resp.StatusCode = 500 172 | resp.Body = ioutil.NopCloser(bytes.NewBufferString(fmt.Sprintf("{\"message\":\"error %sing container '%s' to/from network '%s': %s\"}", parsePath[3], useContainer, parsePath[2], err.Error()))) 173 | return &resp 174 | } 175 | resp.StatusCode = 200 176 | resp.Body = ioutil.NopCloser(bytes.NewBufferString("OK")) 177 | } 178 | default: 179 | // unknown 180 | resp.StatusCode = 501 181 | resp.Body = ioutil.NopCloser(bytes.NewBufferString(fmt.Sprintf("POST not supported for %s\n", req.URL.Path))) 182 | } 183 | case "DELETE": 184 | // delete network - /networks/{id} 185 | // Bare minimum response format here, mostly response code 186 | if us.doesNetworkExist(parsePath[2]) == false { 187 | resp.StatusCode = 404 188 | resp.Body = ioutil.NopCloser(bytes.NewBufferString(fmt.Sprintf("{\"message\":\"network %s not found\"}", parsePath[2]))) 189 | } else { 190 | us.deleteNetwork(parsePath[2]) 191 | resp.StatusCode = 200 192 | resp.Body = ioutil.NopCloser(bytes.NewBufferString("OK")) 193 | } 194 | default: 195 | resp.StatusCode = 501 196 | resp.Body = ioutil.NopCloser(bytes.NewBufferString(fmt.Sprintf("Unsupported HTTP method %s for %s\n", req.Method, req.URL.Path))) 197 | } 198 | case re4.MatchString(req.URL.Path): 199 | switch req.Method { 200 | case "GET": 201 | // inspect volume - /volume/{name} 202 | parsePath := re4.FindStringSubmatch(req.URL.Path) 203 | if len(parsePath) == 3 { 204 | // Vary the response based on volume name (easiest option) 205 | // Partial JSON result, enough to satisfy the inspectLabels() struct 206 | if us.doesVolumeExist(parsePath[2]) == false { 207 | resp.StatusCode = 404 208 | resp.Body = ioutil.NopCloser(bytes.NewBufferString(fmt.Sprintf("{\"message\":\"get %s: no such volume\"}", parsePath[2]))) 209 | } else { 210 | volumeOwnerLabel := us.ownerLabelContent(us.getVolumeOwner(parsePath[2])) 211 | resp.StatusCode = 200 212 | resp.Body = ioutil.NopCloser(bytes.NewBufferString(fmt.Sprintf("{\"Name\":\"%s\",\"Labels\":{%s}}", parsePath[2], volumeOwnerLabel))) 213 | } 214 | } else { 215 | resp.StatusCode = 501 216 | resp.Body = ioutil.NopCloser(bytes.NewBufferString(fmt.Sprintf("Failure parsing volume name from path - %s\n", req.URL.Path))) 217 | } 218 | default: 219 | resp.StatusCode = 501 220 | resp.Body = ioutil.NopCloser(bytes.NewBufferString(fmt.Sprintf("Unsupported HTTP method %s for %s\n", req.Method, req.URL.Path))) 221 | } 222 | default: 223 | resp.StatusCode = 501 224 | resp.Body = ioutil.NopCloser(bytes.NewBufferString(fmt.Sprintf("Path %s not implemented\n", req.URL.Path))) 225 | } 226 | return &resp 227 | }), 228 | } 229 | } 230 | 231 | // Reusable mock log.Logger instance 232 | func mockLogger() *log.Logger { 233 | return log.New(os.Stderr, "MOCK: ", log.Ltime|log.Lmicroseconds) 234 | } 235 | 236 | func TestAddLabelsToQueryStringFilters(t *testing.T) { 237 | l := mockLogger() 238 | r := mockRulesDirector() 239 | 240 | // key = client side URL (inc query params) 241 | // value = expected request URL on upstream side (inc query params) 242 | // TODOLATER: would it be more elegant to write these as URL decoded for readability? will need to change the map[string]string to still send the full docker-compose ps URLs 243 | tests := map[string]string{ 244 | // docker ps - without any filters 245 | "/v1.32/containers/json": "/v1.32/containers/json?filters=%7B%22label%22%3A%5B%22com.buildkite.sockguard.owner%3Dtest-owner%22%5D%7D", 246 | // docker ps - with a key=value: true filter 247 | "/v1.32/containers/json?filters=%7B%22label%22%3A%7B%22test%3Dblah%22%3Atrue%7D%7D": "/v1.32/containers/json?filters=%7B%22label%22%3A%5B%22test%3Dblah%22%2C%22com.buildkite.sockguard.owner%3Dtest-owner%22%5D%7D", 248 | // docker-compose ps - first list API call 249 | "/v1.32/containers/json?limit=-1&all=1&size=0&trunc_cmd=0&filters=%7B%22label%22%3A+%5B%22com.docker.compose.project%3Dblah%22%2C+%22com.docker.compose.oneoff%3DFalse%22%5D%7D": "/v1.32/containers/json?all=1&filters=%7B%22label%22%3A%5B%22com.docker.compose.project%3Dblah%22%2C%22com.docker.compose.oneoff%3DFalse%22%2C%22com.buildkite.sockguard.owner%3Dtest-owner%22%5D%7D&limit=-1&size=0&trunc_cmd=0", 250 | // docker-compose ps - second list API call 251 | "/v1.32/containers/json?limit=-1&all=0&size=0&trunc_cmd=0&filters=%7B%22label%22%3A+%5B%22com.docker.compose.project%3Dblah%22%2C+%22com.docker.compose.oneoff%3DTrue%22%5D%7D": "/v1.32/containers/json?all=0&filters=%7B%22label%22%3A%5B%22com.docker.compose.project%3Dblah%22%2C%22com.docker.compose.oneoff%3DTrue%22%2C%22com.buildkite.sockguard.owner%3Dtest-owner%22%5D%7D&limit=-1&size=0&trunc_cmd=0", 252 | } 253 | 254 | for cReqUrl, uReqUrl := range tests { 255 | upstream := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 256 | if req.Method != "GET" { 257 | t.Errorf("%s : Expected HTTP method GET got %s", uReqUrl, req.Method) 258 | } 259 | 260 | // log.Printf("%s %s", req.Method, req.URL.String()) 261 | // Validate the request URL against expected. 262 | if req.URL.String() != uReqUrl { 263 | decodeUReqUrl, err1 := url.QueryUnescape(uReqUrl) 264 | decodeInReqUrl, err2 := url.QueryUnescape(req.URL.String()) 265 | if err1 == nil && err2 == nil { 266 | t.Errorf("Expected:\n%s\ngot:\n%s\n\n(URL decoded) Expected:\n%s\ngot:\n%s\n", uReqUrl, req.URL.String(), decodeUReqUrl, decodeInReqUrl) 267 | } else { 268 | t.Errorf("Expected:\n%s\ngot:\n%s\n\n(errors trying to URL decode)\n", uReqUrl, req.URL.String()) 269 | } 270 | } 271 | 272 | // Return empty JSON, the request is whats important not the response 273 | fmt.Fprintf(w, `{}`) 274 | }) 275 | 276 | // Credit: https://blog.questionable.services/article/testing-http-handlers-go/ 277 | // Create a request to pass to our handler 278 | req, err := http.NewRequest("GET", cReqUrl, nil) 279 | if err != nil { 280 | t.Fatal(err) 281 | } 282 | // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. 283 | rr := httptest.NewRecorder() 284 | handler := r.addLabelsToQueryStringFilters(l, req, upstream) 285 | 286 | // Our handlers satisfy http.Handler, so we can call their ServeHTTP method 287 | // directly and pass in our Request and ResponseRecorder. 288 | handler.ServeHTTP(rr, req) 289 | 290 | // Check the status code is what we expect. 291 | if status := rr.Code; status != http.StatusOK { 292 | t.Errorf("%s : handler returned wrong status code: got %v want %v", cReqUrl, status, http.StatusOK) 293 | } 294 | 295 | // Don't bother checking the response, it's not relevant in mocked context. The request side is more important here. 296 | } 297 | } 298 | 299 | func loadFixtureFile(filename_part string) (string, error) { 300 | data, err := ioutil.ReadFile(fmt.Sprintf("./fixtures/%s.json", filename_part)) 301 | if err != nil { 302 | return "", err 303 | } 304 | // Remove any whitespace/newlines from the start/end of the file 305 | return strings.TrimSpace(string(data)), nil 306 | } 307 | 308 | // Used for handleContainerCreate(), handleNetworkCreate(), and friends 309 | type handleCreateTests struct { 310 | rd *RulesDirector 311 | // Expected StatusCode 312 | esc int 313 | } 314 | 315 | func TestHandleContainerCreate(t *testing.T) { 316 | l := mockLogger() 317 | 318 | // For each of the tests below, there will be 2 files in the fixtures/ dir: 319 | // - _in.json - the client request sent to the director 320 | // - _expected.json - the expected request sent to the upstream 321 | tests := map[string]handleCreateTests{ 322 | // Defaults 323 | "containers_create_1": handleCreateTests{ 324 | rd: &RulesDirector{ 325 | Client: &http.Client{}, 326 | // This is what's set in main() as the default, assuming running in a container so PID 1 327 | Owner: "sockguard-pid-1", 328 | }, 329 | esc: 200, 330 | }, 331 | // Defaults + custom Owner 332 | "containers_create_2": handleCreateTests{ 333 | rd: &RulesDirector{ 334 | Client: &http.Client{}, 335 | Owner: "test-owner", 336 | }, 337 | esc: 200, 338 | }, 339 | // Defaults with Binds disabled, and a bind sent (should fail) 340 | "containers_create_3": handleCreateTests{ 341 | rd: &RulesDirector{ 342 | Client: &http.Client{}, 343 | // This is what's set in main() as the default, assuming running in a container so PID 1 344 | Owner: "sockguard-pid-1", 345 | AllowBinds: []string{}, 346 | }, 347 | esc: 401, 348 | }, 349 | // Defaults + Binds enabled + a matching bind (should pass) 350 | "containers_create_4": handleCreateTests{ 351 | rd: &RulesDirector{ 352 | Client: &http.Client{}, 353 | // This is what's set in main() as the default, assuming running in a container so PID 1 354 | Owner: "sockguard-pid-1", 355 | AllowBinds: []string{"/tmp"}, 356 | }, 357 | esc: 200, 358 | }, 359 | // Defaults + Binds enabled + a non-matching bind (should fail) 360 | "containers_create_5": handleCreateTests{ 361 | rd: &RulesDirector{ 362 | Client: &http.Client{}, 363 | // This is what's set in main() as the default, assuming running in a container so PID 1 364 | Owner: "sockguard-pid-1", 365 | AllowBinds: []string{"/tmp"}, 366 | }, 367 | esc: 401, 368 | }, 369 | // Defaults + Host Mode Networking + request with NetworkMode=host (should pass) 370 | "containers_create_6": handleCreateTests{ 371 | rd: &RulesDirector{ 372 | Client: &http.Client{}, 373 | // This is what's set in main() as the default, assuming running in a container so PID 1 374 | Owner: "sockguard-pid-1", 375 | AllowHostModeNetworking: true, 376 | }, 377 | esc: 200, 378 | }, 379 | // Defaults + Host Mode Networking disabled + request with NetworkMode=host (should fail) 380 | "containers_create_7": handleCreateTests{ 381 | rd: &RulesDirector{ 382 | Client: &http.Client{}, 383 | // This is what's set in main() as the default, assuming running in a container so PID 1 384 | Owner: "sockguard-pid-1", 385 | AllowHostModeNetworking: false, 386 | }, 387 | esc: 401, 388 | }, 389 | // Defaults + Cgroup Parent 390 | "containers_create_8": handleCreateTests{ 391 | rd: &RulesDirector{ 392 | Client: &http.Client{}, 393 | // This is what's set in main() as the default, assuming running in a container so PID 1 394 | Owner: "sockguard-pid-1", 395 | ContainerCgroupParent: "some-cgroup", 396 | }, 397 | esc: 200, 398 | }, 399 | // Defaults + Force User 400 | "containers_create_9": handleCreateTests{ 401 | rd: &RulesDirector{ 402 | Client: &http.Client{}, 403 | // This is what's set in main() as the default, assuming running in a container so PID 1 404 | Owner: "sockguard-pid-1", 405 | User: "someuser", 406 | }, 407 | esc: 200, 408 | }, 409 | // Defaults + a custom label on request 410 | "containers_create_10": handleCreateTests{ 411 | rd: &RulesDirector{ 412 | Client: &http.Client{}, 413 | // This is what's set in main() as the default, assuming running in a container so PID 1 414 | Owner: "sockguard-pid-1", 415 | }, 416 | esc: 200, 417 | }, 418 | // Defaults + -docker-link sockguard + requesting default bridge network 419 | "containers_create_11": handleCreateTests{ 420 | rd: &RulesDirector{ 421 | Client: &http.Client{}, 422 | // This is what's set in main() as the default, assuming running in a container so PID 1 423 | Owner: "sockguard-pid-1", 424 | ContainerDockerLink: "asdf:zzzz", 425 | }, 426 | esc: 200, 427 | }, 428 | // Defaults + -docker-link sockguard flag + requesting a user defined bridge network 429 | "containers_create_12": handleCreateTests{ 430 | rd: &RulesDirector{ 431 | Client: &http.Client{}, 432 | // This is what's set in main() as the default, assuming running in a container so PID 1 433 | Owner: "sockguard-pid-1", 434 | ContainerDockerLink: "asdf:zzzz", 435 | }, 436 | esc: 200, 437 | }, 438 | // Defaults + try set a CgroupParent (should fail, only permitted if sockguard started with -cgroup-parent) 439 | "containers_create_13": handleCreateTests{ 440 | rd: &RulesDirector{ 441 | Client: &http.Client{}, 442 | // This is what's set in main() as the default, assuming running in a container so PID 1 443 | Owner: "sockguard-pid-1", 444 | }, 445 | esc: 401, 446 | }, 447 | // Defaults + -docker-link sockguard flag + requesting default bridge network + another arbitrary --link from client 448 | "containers_create_14": handleCreateTests{ 449 | rd: &RulesDirector{ 450 | Client: &http.Client{}, 451 | // This is what's set in main() as the default, assuming running in a container so PID 1 452 | Owner: "sockguard-pid-1", 453 | ContainerDockerLink: "cccc:dddd", 454 | }, 455 | esc: 200, 456 | }, 457 | } 458 | 459 | reqUrl := "/v1.37/containers/create" 460 | expectedUrl := "/v1.37/containers/create" 461 | 462 | // TODOLATER: consolidate/DRY this with TestHandleNetworkCreate()? 463 | for k, v := range tests { 464 | 465 | expectedReqJson, err := loadFixtureFile(fmt.Sprintf("%s_expected", k)) 466 | if err != nil { 467 | t.Fatal(err) 468 | } 469 | 470 | upstream := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 471 | if req.Method != "POST" { 472 | t.Errorf("%s : Expected HTTP method POST got %s", k, req.Method) 473 | } 474 | 475 | // log.Printf("%s %s", req.Method, req.URL.String()) 476 | // Validate the request URL against expected. 477 | if req.URL.String() != expectedUrl { 478 | t.Errorf("%s : Expected URL %s got %s", k, expectedUrl, req.URL.String()) 479 | } 480 | // Validate the body has been modified as expected 481 | body, err := ioutil.ReadAll(req.Body) 482 | if err != nil { 483 | t.Fatal(err) 484 | } 485 | if string(body) != string(expectedReqJson) { 486 | t.Errorf("%s : Expected request body JSON:\n%s\nGot request body JSON:\n%s\n", k, string(expectedReqJson), string(body)) 487 | } 488 | 489 | // TODOLATER: append to "us" (upstream state) the new container, and any connected networks? we only check the ciagentcontainer 490 | // when verifying state further down right now, which is the key consideration. 491 | 492 | // Return empty JSON, the request is whats important not the response 493 | fmt.Fprintf(w, `{}`) 494 | }) 495 | 496 | // Credit: https://blog.questionable.services/article/testing-http-handlers-go/ 497 | // Create a request to pass to our handler 498 | containerCreateJson, err := loadFixtureFile(fmt.Sprintf("%s_in", k)) 499 | if err != nil { 500 | t.Fatal(err) 501 | } 502 | req, err := http.NewRequest("POST", reqUrl, strings.NewReader(containerCreateJson)) 503 | if err != nil { 504 | t.Fatal(err) 505 | } 506 | // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. 507 | rr := httptest.NewRecorder() 508 | handler := v.rd.handleContainerCreate(l, req, upstream) 509 | 510 | // Our handlers satisfy http.Handler, so we can call their ServeHTTP method 511 | // directly and pass in our Request and ResponseRecorder. 512 | handler.ServeHTTP(rr, req) 513 | 514 | // Check the status code is what we expect. 515 | //fmt.Printf("%s : SC %d ESC %d\n", k, rr.Code, v.esc) 516 | if status := rr.Code; status != v.esc { 517 | // Get the body out of the response to return with the error 518 | respBody, err := ioutil.ReadAll(rr.Body) 519 | if err == nil { 520 | t.Errorf("%s : handler returned wrong status code: got %v want %v. Response body: %s", k, status, v.esc, string(respBody)) 521 | } else { 522 | t.Errorf("%s : handler returned wrong status code: got %v want %v. Error reading response body: %s", k, status, v.esc, err.Error()) 523 | } 524 | } 525 | 526 | // State of ciagentcontainer network attachments is not relevant for a general container creation call, 527 | // only matters for network create/delete. 528 | 529 | // Don't bother checking the response, it's not relevant in mocked context. The request side is more important here. 530 | } 531 | } 532 | 533 | func TestSplitContainerDockerLink(t *testing.T) { 534 | goodTests := map[string]containerDockerLink{ 535 | "38e5c22c7120": containerDockerLink{Container: "38e5c22c7120", Alias: "38e5c22c7120"}, 536 | "38e5c22c7120:asdf": containerDockerLink{Container: "38e5c22c7120", Alias: "asdf"}, 537 | "somename": containerDockerLink{Container: "somename", Alias: "somename"}, 538 | "somename:zzzz": containerDockerLink{Container: "somename", Alias: "zzzz"}, 539 | } 540 | badTests := []string{ 541 | "", 542 | "somename:zzzz:aaaa", 543 | } 544 | for k1, v1 := range goodTests { 545 | result1, err := splitContainerDockerLink(k1) 546 | if err != nil { 547 | t.Errorf("%s : %s", k1, err.Error()) 548 | } 549 | if cmp.Equal(*result1, v1) != true { 550 | t.Errorf("'%s' : Expected %+v, got %+v\n", k1, v1, result1) 551 | } 552 | } 553 | for _, v2 := range badTests { 554 | _, err := splitContainerDockerLink(v2) 555 | if err == nil { 556 | t.Errorf("'%s' : Expected error, got nil", v2) 557 | } 558 | } 559 | } 560 | 561 | func TestHandleNetworkCreate(t *testing.T) { 562 | l := mockLogger() 563 | 564 | // Pre-populated simplified upstream state that "exists" before tests execute. 565 | us := upstreamState{ 566 | containers: map[string]upstreamStateContainer{ 567 | "ciagentcontainer": upstreamStateContainer{ 568 | // No ownership checking at this level (intentionally), due to chicken-and-egg situation 569 | // (CI container is a sibling/sidecar of sockguard itself, not a child) 570 | owner: "foreign", 571 | attachedNetworks: []upstreamStateContainerAttachedNetwork{}, 572 | }, 573 | }, 574 | networks: map[string]upstreamStateNetwork{}, 575 | } 576 | 577 | // For each of the tests below, there will be 2 files in the fixtures/ dir: 578 | // - _in.json - the client request sent to the director 579 | // - _expected.json - the expected request sent to the upstream 580 | tests := map[string]handleCreateTests{ 581 | // Defaults 582 | "networks_create_1": handleCreateTests{ 583 | rd: &RulesDirector{ 584 | Client: mockRulesDirectorHttpClientWithUpstreamState(&us), 585 | // This is what's set in main() as the default, assuming running in a container so PID 1 586 | Owner: "sockguard-pid-1", 587 | }, 588 | esc: 200, 589 | }, 590 | // Defaults + -docker-link enabled 591 | "networks_create_2": handleCreateTests{ 592 | rd: &RulesDirector{ 593 | Client: mockRulesDirectorHttpClientWithUpstreamState(&us), 594 | // This is what's set in main() as the default, assuming running in a container so PID 1 595 | Owner: "sockguard-pid-1", 596 | ContainerDockerLink: "ciagentcontainer:cccc", 597 | }, 598 | esc: 200, 599 | }, 600 | // Defaults + -container-join-network enabled 601 | "networks_create_3": handleCreateTests{ 602 | rd: &RulesDirector{ 603 | Client: mockRulesDirectorHttpClientWithUpstreamState(&us), 604 | // This is what's set in main() as the default, assuming running in a container so PID 1 605 | Owner: "sockguard-pid-1", 606 | ContainerJoinNetwork: "ciagentcontainer", 607 | }, 608 | esc: 200, 609 | }, 610 | // Defaults + -container-join-network + -container-join-network-alias enabled 611 | "networks_create_4": handleCreateTests{ 612 | rd: &RulesDirector{ 613 | Client: mockRulesDirectorHttpClientWithUpstreamState(&us), 614 | // This is what's set in main() as the default, assuming running in a container so PID 1 615 | Owner: "sockguard-pid-1", 616 | ContainerJoinNetwork: "ciagentcontainer", 617 | ContainerJoinNetworkAlias: "ciagentalias", 618 | }, 619 | esc: 200, 620 | }, 621 | } 622 | 623 | reqUrl := "/v1.37/networks/create" 624 | expectedUrl := "/v1.37/networks/create" 625 | 626 | // TODOLATER: consolidate/DRY this with TestHandleContainerCreate()? 627 | for k, v := range tests { 628 | expectedReqJson, err := loadFixtureFile(fmt.Sprintf("%s_expected", k)) 629 | if err != nil { 630 | t.Fatal(err) 631 | } 632 | upstream := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 633 | if req.Method != "POST" { 634 | t.Errorf("%s : Expected HTTP method POST got %s", k, req.Method) 635 | } 636 | 637 | // log.Printf("%s %s", req.Method, req.URL.String()) 638 | // Validate the request URL against expected. 639 | if req.URL.String() != expectedUrl { 640 | t.Errorf("%s : Expected URL %s got %s", k, expectedUrl, req.URL.String()) 641 | } 642 | // Validate the body has been modified as expected 643 | body, err := ioutil.ReadAll(req.Body) 644 | if err != nil { 645 | t.Fatal(err) 646 | } 647 | if string(body) != string(expectedReqJson) { 648 | t.Errorf("%s : Expected request body JSON:\n%s\nGot request body JSON:\n%s\n", k, string(expectedReqJson), string(body)) 649 | } 650 | 651 | var decoded map[string]interface{} 652 | if err := json.Unmarshal(body, &decoded); err != nil { 653 | t.Fatal(err) 654 | } 655 | newNetworkName := decoded["Name"].(string) 656 | newNetworkOwner := "" 657 | switch lab := decoded["Labels"].(type) { 658 | case map[string]interface{}: 659 | newNetworkOwner = lab["com.buildkite.sockguard.owner"].(string) 660 | default: 661 | t.Fatal("Error: Cannot parse Labels from request JSON on network create") 662 | } 663 | if us.doesNetworkExist(newNetworkName) == true { 664 | t.Fatalf("Network '%s' already exists", newNetworkName) 665 | } 666 | us.createNetwork(newNetworkName, newNetworkOwner) 667 | 668 | // Return empty JSON, the request is whats important not the response 669 | fmt.Fprintf(w, `{}`) 670 | }) 671 | // Credit: https://blog.questionable.services/article/testing-http-handlers-go/ 672 | // Create a request to pass to our handler 673 | containerCreateJson, err := loadFixtureFile(fmt.Sprintf("%s_in", k)) 674 | if err != nil { 675 | t.Fatal(err) 676 | } 677 | 678 | // Parse out the new network name from containerCreateJson, for use in further checks below 679 | var decodedIn map[string]interface{} 680 | if err := json.Unmarshal([]byte(containerCreateJson), &decodedIn); err != nil { 681 | t.Fatal(err) 682 | } 683 | inNewNetworkName := decodedIn["Name"].(string) 684 | 685 | req, err := http.NewRequest("POST", reqUrl, strings.NewReader(containerCreateJson)) 686 | if err != nil { 687 | t.Fatal(err) 688 | } 689 | 690 | // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. 691 | rr := httptest.NewRecorder() 692 | handler := v.rd.handleNetworkCreate(l, req, upstream) 693 | 694 | // Our handlers satisfy http.Handler, so we can call their ServeHTTP method 695 | // directly and pass in our Request and ResponseRecorder. 696 | handler.ServeHTTP(rr, req) 697 | 698 | // Check the status code is what we expect. 699 | //fmt.Printf("%s : SC %d ESC %d\n", k, rr.Code, v.esc) 700 | if status := rr.Code; status != v.esc { 701 | // Get the body out of the response to return with the error 702 | respBody, err := ioutil.ReadAll(rr.Body) 703 | if err == nil { 704 | t.Errorf("%s : handler returned wrong status code: got %v want %v. Response body: %s", k, status, v.esc, string(respBody)) 705 | } else { 706 | t.Errorf("%s : handler returned wrong status code: got %v want %v. Error reading response body: %s", k, status, v.esc, err.Error()) 707 | } 708 | } 709 | 710 | // Verify the network was added to upstreamState 711 | if rr.Code == 200 && us.doesNetworkExist(inNewNetworkName) == false { 712 | t.Errorf("%s : %d response code, but network '%s' does not exist, should have been created in mock upstream state", k, rr.Code, inNewNetworkName) 713 | } else if rr.Code != 200 && us.doesNetworkExist(inNewNetworkName) == true { 714 | t.Errorf("%s : %d response code, but network '%s' exists, should not have been created", k, rr.Code, inNewNetworkName) 715 | } 716 | 717 | // Verify the ciagentcontainer was connected to the new network (if applicable) 718 | if v.rd.ContainerDockerLink != "" || v.rd.ContainerJoinNetwork != "" { 719 | ciAgentAttachedNetworks := us.getContainerAttachedNetworks("ciagentcontainer") 720 | ciAgentAttachedToNetwork := false 721 | ciAgentAttachedToNetworkWithAlias := false 722 | for _, vn := range ciAgentAttachedNetworks { 723 | if vn.name == inNewNetworkName { 724 | ciAgentAttachedToNetwork = true 725 | if v.rd.ContainerJoinNetworkAlias == "" { 726 | // No alias set, consider this a success 727 | ciAgentAttachedToNetworkWithAlias = true 728 | } else if cmp.Equal(vn.aliases, []string{v.rd.ContainerJoinNetworkAlias}) == true { 729 | // Should also have the correct alias set 730 | ciAgentAttachedToNetworkWithAlias = true 731 | } 732 | break 733 | } 734 | } 735 | if ciAgentAttachedToNetwork == false { 736 | t.Errorf("%s : network '%s' exists (or should exist), but ciagentcontainer is not attached", k, inNewNetworkName) 737 | } 738 | if ciAgentAttachedToNetworkWithAlias == false { 739 | t.Errorf("%s : network '%s' exists (or should exist), but ciagentcontainer does not have the alias '%s'", k, inNewNetworkName, v.rd.ContainerJoinNetworkAlias) 740 | } 741 | } 742 | 743 | // Don't bother checking the response, it's not relevant in mocked context. The request side is more important here. 744 | } 745 | } 746 | 747 | func TestHandleNetworkDelete(t *testing.T) { 748 | l := mockLogger() 749 | 750 | // Pre-populated simplified upstream state that "exists" before tests execute. 751 | us := upstreamState{ 752 | containers: map[string]upstreamStateContainer{ 753 | "ciagentcontainer": upstreamStateContainer{ 754 | // No ownership checking at this level (intentionally), due to chicken-and-egg situation 755 | // (CI container is a sibling/sidecar of sockguard itself, not a child) 756 | owner: "foreign", 757 | attachedNetworks: []upstreamStateContainerAttachedNetwork{ 758 | upstreamStateContainerAttachedNetwork{ 759 | name: "whatevernetwork", 760 | }, 761 | upstreamStateContainerAttachedNetwork{ 762 | name: "alwaysjoinnetwork", 763 | }, 764 | upstreamStateContainerAttachedNetwork{ 765 | name: "alwaysjoinnetworkwithalias", 766 | aliases: []string{"ciagentalias"}, 767 | }, 768 | }, 769 | }, 770 | }, 771 | networks: map[string]upstreamStateNetwork{ 772 | "somenetwork": upstreamStateNetwork{ 773 | owner: "sockguard-pid-1", 774 | }, 775 | "anothernetwork": upstreamStateNetwork{ 776 | owner: "adifferentowner", 777 | }, 778 | "whatevernetwork": upstreamStateNetwork{ 779 | owner: "sockguard-pid-1", 780 | }, 781 | "alwaysjoinnetwork": upstreamStateNetwork{ 782 | owner: "sockguard-pid-1", 783 | }, 784 | "alwaysjoinnetworkwithalias": upstreamStateNetwork{ 785 | owner: "sockguard-pid-1", 786 | }, 787 | }, 788 | } 789 | 790 | // Key = the network name that will be deleted (or attempted) 791 | tests := map[string]handleCreateTests{ 792 | // Defaults (owner label matches, should pass) 793 | "somenetwork": handleCreateTests{ 794 | rd: &RulesDirector{ 795 | Client: mockRulesDirectorHttpClientWithUpstreamState(&us), 796 | // This is what's set in main() as the default, assuming running in a container so PID 1 797 | Owner: "sockguard-pid-1", 798 | }, 799 | esc: 200, 800 | }, 801 | // Defaults (owner label does not match, should fail) 802 | "anothernetwork": handleCreateTests{ 803 | rd: &RulesDirector{ 804 | Client: mockRulesDirectorHttpClientWithUpstreamState(&us), 805 | // This is what's set in main() as the default, assuming running in a container so PID 1 806 | Owner: "sockguard-pid-1", 807 | }, 808 | esc: 401, 809 | }, 810 | // Defaults + -docker-link enabled 811 | "whatevernetwork": handleCreateTests{ 812 | rd: &RulesDirector{ 813 | Client: mockRulesDirectorHttpClientWithUpstreamState(&us), 814 | // This is what's set in main() as the default, assuming running in a container so PID 1 815 | Owner: "sockguard-pid-1", 816 | ContainerDockerLink: "ciagentcontainer:ffff", 817 | }, 818 | esc: 200, 819 | }, 820 | // Defaults + -container-join-network enabled 821 | "alwaysjoinnetwork": handleCreateTests{ 822 | rd: &RulesDirector{ 823 | Client: mockRulesDirectorHttpClientWithUpstreamState(&us), 824 | // This is what's set in main() as the default, assuming running in a container so PID 1 825 | Owner: "sockguard-pid-1", 826 | ContainerJoinNetwork: "ciagentcontainer", 827 | }, 828 | esc: 200, 829 | }, 830 | // Defaults + -container-join-network + -container-join-network-alias enabled 831 | // Technically we don't do anything different to the prior here, but added for completeness 832 | "alwaysjoinnetworkwithalias": handleCreateTests{ 833 | rd: &RulesDirector{ 834 | Client: mockRulesDirectorHttpClientWithUpstreamState(&us), 835 | // This is what's set in main() as the default, assuming running in a container so PID 1 836 | Owner: "sockguard-pid-1", 837 | ContainerJoinNetwork: "ciagentcontainer", 838 | ContainerJoinNetworkAlias: "ciagentalias", 839 | }, 840 | esc: 200, 841 | }, 842 | } 843 | 844 | pathIdRegex := regexp.MustCompile("^/v(.*)/networks/(.*)$") 845 | // TODOLATER: consolidate/DRY this with TestHandleContainerCreate()? 846 | for k, v := range tests { 847 | reqUrl := fmt.Sprintf("/v1.37/networks/%s", k) 848 | expectedUrl := fmt.Sprintf("/v1.37/networks/%s", k) 849 | upstream := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 850 | if req.Method != "DELETE" { 851 | t.Errorf("%s : Expected HTTP method DELETE got %s", k, req.Method) 852 | } 853 | 854 | // log.Printf("%s %s", req.Method, req.URL.String()) 855 | // Validate the request URL against expected. 856 | if req.URL.String() != expectedUrl { 857 | t.Errorf("%s : Expected URL %s got %s", k, expectedUrl, req.URL.String()) 858 | } 859 | 860 | // No request body for these DELETE calls 861 | 862 | // Parse out request URI 863 | if pathIdRegex.MatchString(req.URL.Path) == false { 864 | t.Fatalf("%s : URL path did not match expected /vx.xx/networks/{id|name}", k) 865 | } 866 | parsePath := pathIdRegex.FindStringSubmatch(req.URL.Path) 867 | if len(parsePath) != 3 { 868 | t.Fatalf("%s : URL path regex split mismatch, expected 3 got %d", k, len(parsePath)) 869 | } 870 | 871 | // "Delete" the network (from mocked upstream state) 872 | err := us.deleteNetwork(parsePath[2]) 873 | if err != nil { 874 | t.Fatal(err) 875 | } 876 | 877 | // Return empty JSON, the request is whats important not the response 878 | fmt.Fprintf(w, `{}`) 879 | }) 880 | // Credit: https://blog.questionable.services/article/testing-http-handlers-go/ 881 | // Create a request to pass to our handler 882 | req, err := http.NewRequest("DELETE", reqUrl, nil) 883 | if err != nil { 884 | t.Fatal(err) 885 | } 886 | 887 | // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. 888 | rr := httptest.NewRecorder() 889 | handler := v.rd.handleNetworkDelete(l, req, upstream) 890 | 891 | // Our handlers satisfy http.Handler, so we can call their ServeHTTP method 892 | // directly and pass in our Request and ResponseRecorder. 893 | handler.ServeHTTP(rr, req) 894 | 895 | // Check the status code is what we expect. 896 | //fmt.Printf("%s : SC %d ESC %d\n", k, rr.Code, v.esc) 897 | if status := rr.Code; status != v.esc { 898 | // Get the body out of the response to return with the error 899 | respBody, err := ioutil.ReadAll(rr.Body) 900 | if err == nil { 901 | t.Errorf("%s : handler returned wrong status code: got %v want %v. Response body: %s", k, status, v.esc, string(respBody)) 902 | } else { 903 | t.Errorf("%s : handler returned wrong status code: got %v want %v. Error reading response body: %s", k, status, v.esc, err.Error()) 904 | } 905 | } 906 | 907 | // Verify the network was deleted from mock upstream state (or not deleted on error) 908 | if rr.Code == 200 && us.doesNetworkExist(k) == true { 909 | t.Errorf("%s : %d response code, but network still exists, should have been deleted from mock upstream state", k, rr.Code) 910 | } else if rr.Code != 200 && us.doesNetworkExist(k) == false { 911 | t.Errorf("%s : %d response code, but network does not exist, should not have been deleted", k, rr.Code) 912 | } 913 | 914 | // Don't bother checking the response, it's not relevant in mocked context. The request side is more important here. 915 | } 916 | } 917 | 918 | // TODOLATER: would it make more sense to implement a TestDirect, or TestDirect* (break it into variations by path or method)? 919 | // Since that would also cover Direct() + CheckOwner(). Or do we do both...? 920 | func TestCheckOwner(t *testing.T) { 921 | l := mockLogger() 922 | 923 | // Pre-populated simplified upstream state that "exists" before tests execute. 924 | us := upstreamState{ 925 | containers: map[string]upstreamStateContainer{ 926 | "idwithnolabel": upstreamStateContainer{ 927 | // Empty owner = no label 928 | owner: "", 929 | }, 930 | "idwithlabel1": upstreamStateContainer{ 931 | owner: "test-owner", 932 | }, 933 | }, 934 | images: map[string]upstreamStateImage{ 935 | "idwithnolabel": upstreamStateImage{ 936 | // Empty owner = no label 937 | owner: "", 938 | }, 939 | "idwithlabel1": upstreamStateImage{ 940 | owner: "test-owner", 941 | }, 942 | }, 943 | networks: map[string]upstreamStateNetwork{ 944 | "idwithnolabel": upstreamStateNetwork{ 945 | // Empty owner = no label 946 | owner: "", 947 | }, 948 | "idwithlabel1": upstreamStateNetwork{ 949 | owner: "test-owner", 950 | }, 951 | }, 952 | volumes: map[string]upstreamStateVolume{ 953 | "namewithnolabel": upstreamStateVolume{ 954 | // Empty owner = no label 955 | owner: "", 956 | }, 957 | "namewithlabel1": upstreamStateVolume{ 958 | owner: "test-owner", 959 | }, 960 | "name-with-label2": upstreamStateVolume{ 961 | owner: "test-owner", 962 | }, 963 | }, 964 | } 965 | 966 | r := mockRulesDirectorWithUpstreamState(&us) 967 | 968 | tests := map[string]struct { 969 | Type string 970 | ExpResult bool 971 | }{ 972 | // A container that will match 973 | "/v1.37/containers/idwithlabel1/logs": {"containers", true}, 974 | // A container that won't match 975 | "/v1.37/containers/idwithnolabel/logs": {"containers", false}, 976 | // An image that will match 977 | "/v1.37/images/idwithlabel1/json": {"images", true}, 978 | // An image that won't match 979 | "/v1.37/images/idwithnolabel/json": {"images", false}, 980 | // A network that will match 981 | "/v1.37/networks/idwithlabel1": {"networks", true}, 982 | // A network that won't match 983 | "/v1.37/networks/idwithnolabel": {"networks", false}, 984 | // A volume that will match 985 | "/v1.37/volumes/namewithlabel1": {"volumes", true}, 986 | // A volume that will match 987 | "/v1.37/volumes/name-with-label2": {"volumes", true}, 988 | // A volume that won't match 989 | "/v1.37/volumes/namewithnolabel": {"volumes", false}, 990 | } 991 | 992 | for k, v := range tests { 993 | kReq, err := http.NewRequest("GET", k, nil) 994 | if err != nil { 995 | t.Fatal(err) 996 | } 997 | result, err := r.checkOwner(l, v.Type, false, kReq) 998 | if err != nil { 999 | t.Errorf("%s : Error - %s", kReq.URL.String(), err.Error()) 1000 | } 1001 | if v.ExpResult != result { 1002 | t.Errorf("%s : Expected %t, got %t", kReq.URL.String(), v.ExpResult, result) 1003 | } 1004 | } 1005 | } 1006 | 1007 | type handleBuildTest struct { 1008 | rd *RulesDirector 1009 | // Expected StatusCode 1010 | esc int 1011 | 1012 | // These are short enough, store inline rather than in fixtures files 1013 | inQueryString string 1014 | expectedQueryString string 1015 | } 1016 | 1017 | func TestHandleBuild(t *testing.T) { 1018 | l := mockLogger() 1019 | tests := []handleBuildTest{ 1020 | // Defaults 1021 | handleBuildTest{ 1022 | rd: &RulesDirector{ 1023 | Client: &http.Client{}, 1024 | // This is what's set in main() as the default, assuming running in a container so PID 1 1025 | Owner: "sockguard-pid-1", 1026 | }, 1027 | esc: 200, 1028 | inQueryString: `buildargs={}&cachefrom=[]&cgroupparent=&cpuperiod=0&cpuquota=0&cpusetcpus=&cpusetmems=&cpushares=0&dockerfile=Dockerfile&labels={}&memory=0&memswap=0&networkmode=default&rm=1&shmsize=0&target=&ulimits=null&version=1`, 1029 | expectedQueryString: `buildargs={}&cachefrom=[]&cgroupparent=&cpuperiod=0&cpuquota=0&cpusetcpus=&cpusetmems=&cpushares=0&dockerfile=Dockerfile&labels={"com.buildkite.sockguard.owner":"sockguard-pid-1"}&memory=0&memswap=0&networkmode=default&rm=1&shmsize=0&target=&ulimits=null&version=1`, 1030 | }, 1031 | // Defaults + custom label 1032 | handleBuildTest{ 1033 | rd: &RulesDirector{ 1034 | Client: &http.Client{}, 1035 | // This is what's set in main() as the default, assuming running in a container so PID 1 1036 | Owner: "sockguard-pid-1", 1037 | }, 1038 | esc: 200, 1039 | inQueryString: `buildargs={}&cachefrom=[]&cgroupparent=&cpuperiod=0&cpuquota=0&cpusetcpus=&cpusetmems=&cpushares=0&dockerfile=Dockerfile&labels={"somelabel":"somevalue"}&memory=0&memswap=0&networkmode=default&rm=1&shmsize=0&target=&ulimits=null&version=1`, 1040 | expectedQueryString: `buildargs={}&cachefrom=[]&cgroupparent=&cpuperiod=0&cpuquota=0&cpusetcpus=&cpusetmems=&cpushares=0&dockerfile=Dockerfile&labels={"com.buildkite.sockguard.owner":"sockguard-pid-1","somelabel":"somevalue"}&memory=0&memswap=0&networkmode=default&rm=1&shmsize=0&target=&ulimits=null&version=1`, 1041 | }, 1042 | // Defaults + CgroupParent in config (should pass) 1043 | handleBuildTest{ 1044 | rd: &RulesDirector{ 1045 | Client: &http.Client{}, 1046 | // This is what's set in main() as the default, assuming running in a container so PID 1 1047 | Owner: "sockguard-pid-1", 1048 | ContainerCgroupParent: "somecgroup", 1049 | }, 1050 | esc: 200, 1051 | inQueryString: `buildargs={}&cachefrom=[]&cgroupparent=&cpuperiod=0&cpuquota=0&cpusetcpus=&cpusetmems=&cpushares=0&dockerfile=Dockerfile&labels={}&memory=0&memswap=0&networkmode=default&rm=1&shmsize=0&target=&ulimits=null&version=1`, 1052 | expectedQueryString: `buildargs={}&cachefrom=[]&cgroupparent=somecgroup&cpuperiod=0&cpuquota=0&cpusetcpus=&cpusetmems=&cpushares=0&dockerfile=Dockerfile&labels={"com.buildkite.sockguard.owner":"sockguard-pid-1"}&memory=0&memswap=0&networkmode=default&rm=1&shmsize=0&target=&ulimits=null&version=1`, 1053 | }, 1054 | // Defaults + CgroupParent in API request (should fail) 1055 | handleBuildTest{ 1056 | rd: &RulesDirector{ 1057 | Client: &http.Client{}, 1058 | // This is what's set in main() as the default, assuming running in a container so PID 1 1059 | Owner: "sockguard-pid-1", 1060 | }, 1061 | esc: 401, 1062 | inQueryString: `buildargs={}&cachefrom=[]&cgroupparent=anothercgroup&cpuperiod=0&cpuquota=0&cpusetcpus=&cpusetmems=&cpushares=0&dockerfile=Dockerfile&labels={}&memory=0&memswap=0&networkmode=default&rm=1&shmsize=0&target=&ulimits=null&version=1`, 1063 | expectedQueryString: ``, 1064 | }, 1065 | } 1066 | reqUrlPath := "/v1.37/build" 1067 | expectedUrlPath := "/v1.37/build" 1068 | for _, v := range tests { 1069 | upstream := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 1070 | // log.Printf("%s %s", req.Method, req.URL.Path) 1071 | // Validate the request URL path against expected. 1072 | if req.URL.Path != expectedUrlPath { 1073 | t.Error("Expected URL path", expectedUrlPath, "got", req.URL.Path) 1074 | } 1075 | 1076 | // Validate the query string matches expected 1077 | unescapeQueryString, err := url.QueryUnescape(req.URL.RawQuery) 1078 | if err != nil { 1079 | t.Fatal(err) 1080 | } 1081 | if unescapeQueryString != v.expectedQueryString { 1082 | t.Errorf("Expected URL query string:\n%s\nGot:\n%s\n\n", v.expectedQueryString, unescapeQueryString) 1083 | } 1084 | 1085 | // We don't validate the request body here, as it is a build context tar (which isn't modified), not relevant 1086 | 1087 | // Return empty JSON, the request is whats important not the response 1088 | fmt.Fprintf(w, `{}`) 1089 | }) 1090 | // Credit: https://blog.questionable.services/article/testing-http-handlers-go/ 1091 | // Create a request to pass to our handler, using an empty request body for now (not relevant) 1092 | r, err := http.NewRequest("POST", fmt.Sprintf("%s?%s", reqUrlPath, v.inQueryString), nil) 1093 | if err != nil { 1094 | t.Fatal(err) 1095 | } 1096 | // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. 1097 | rr := httptest.NewRecorder() 1098 | handler := v.rd.handleBuild(l, r, upstream) 1099 | // Our handlers satisfy http.Handler, so we can call their ServeHTTP method 1100 | // directly and pass in our Request and ResponseRecorder. 1101 | handler.ServeHTTP(rr, r) 1102 | // Check the status code is what we expect. 1103 | //fmt.Printf("%s : SC %d ESC %d\n", k, rr.Code, v.esc) 1104 | if status := rr.Code; status != v.esc { 1105 | // Get the body out of the response to return with the error 1106 | respBody, err := ioutil.ReadAll(rr.Body) 1107 | if err == nil { 1108 | t.Errorf("%s : handler returned wrong status code: got %v want %v. Response body: %s", v.inQueryString, status, v.esc, string(respBody)) 1109 | } else { 1110 | t.Errorf("%s : handler returned wrong status code: got %v want %v. Error reading response body: %s", v.inQueryString, status, v.esc, err.Error()) 1111 | } 1112 | } 1113 | // Don't bother checking the response, it's not relevant in mocked context. The request side is more important here. 1114 | } 1115 | } 1116 | -------------------------------------------------------------------------------- /director_upstream_state_test.go: -------------------------------------------------------------------------------- 1 | package sockguard 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Simplified mocked out upstream state of Docker networks, for use in create container/create network/delete network/check owner tests 8 | // NOTE: there is no locking around accesses in this type, assumed that each test block will have it's own instance 9 | type upstreamState struct { 10 | // Key = container name/ID 11 | containers map[string]upstreamStateContainer 12 | // Key = image name/ID 13 | images map[string]upstreamStateImage 14 | // Key = network name/ID 15 | networks map[string]upstreamStateNetwork 16 | // Key = volume name 17 | volumes map[string]upstreamStateVolume 18 | } 19 | 20 | type upstreamStateContainer struct { 21 | owner string 22 | attachedNetworks []upstreamStateContainerAttachedNetwork 23 | } 24 | 25 | type upstreamStateContainerAttachedNetwork struct { 26 | name string 27 | // Alias hostnames used to talk to this container via this attached network 28 | // Can be empty. Also more than 1 container can have the same alias, and Docker will round-robin them. 29 | aliases []string 30 | } 31 | 32 | type upstreamStateImage struct { 33 | owner string 34 | } 35 | 36 | type upstreamStateNetwork struct { 37 | owner string 38 | } 39 | 40 | type upstreamStateVolume struct { 41 | owner string 42 | } 43 | 44 | func (u *upstreamState) ownerLabelContent(owner string) string { 45 | ownerLabel := "" 46 | if owner != "" { 47 | ownerLabel = fmt.Sprintf("\"com.buildkite.sockguard.owner\":\"%s\"", owner) 48 | } 49 | return ownerLabel 50 | } 51 | 52 | ////////////// 53 | // containers 54 | 55 | func (u *upstreamState) createContainer(idOrName string, theOwner string, networks []upstreamStateContainerAttachedNetwork) error { 56 | // Deny if already exists 57 | if u.doesContainerExist(idOrName) == true { 58 | return fmt.Errorf("Cannot create container with ID/Name '%s', already exists", idOrName) 59 | } 60 | // "Create" it 61 | u.containers[idOrName] = upstreamStateContainer{ 62 | owner: theOwner, 63 | attachedNetworks: networks, 64 | } 65 | return nil 66 | } 67 | 68 | func (u *upstreamState) deleteContainer(idOrName string) error { 69 | // Deny if does not exist 70 | if u.doesContainerExist(idOrName) == false { 71 | return fmt.Errorf("Cannot delete container with ID/Name '%s', does not exist", idOrName) 72 | } 73 | // "Delete" it 74 | delete(u.containers, idOrName) 75 | return nil 76 | } 77 | 78 | func (u *upstreamState) doesContainerExist(idOrName string) bool { 79 | _, ok := u.containers[idOrName] 80 | return ok 81 | } 82 | 83 | func (u *upstreamState) getContainerOwner(idOrName string) string { 84 | return u.containers[idOrName].owner 85 | } 86 | 87 | func (u *upstreamState) getContainerAttachedNetworks(idOrName string) []upstreamStateContainerAttachedNetwork { 88 | return u.containers[idOrName].attachedNetworks 89 | } 90 | 91 | ////////////// 92 | // images 93 | 94 | func (u *upstreamState) createImage(idOrName string, theOwner string) error { 95 | // Deny if already exists 96 | if u.doesImageExist(idOrName) == true { 97 | return fmt.Errorf("Cannot create image with ID/Name '%s', already exists", idOrName) 98 | } 99 | // "Create" it 100 | u.images[idOrName] = upstreamStateImage{ 101 | owner: theOwner, 102 | } 103 | return nil 104 | } 105 | 106 | func (u *upstreamState) deleteImage(idOrName string) error { 107 | // Deny if does not exist 108 | if u.doesImageExist(idOrName) == false { 109 | return fmt.Errorf("Cannot delete image with ID/Name '%s', does not exist", idOrName) 110 | } 111 | // TODOLATER: images cannot be deleted if a container is using them, add logic if/when test coverage requires it 112 | // "Delete" it 113 | delete(u.images, idOrName) 114 | return nil 115 | } 116 | 117 | func (u *upstreamState) doesImageExist(idOrName string) bool { 118 | _, ok := u.images[idOrName] 119 | return ok 120 | } 121 | 122 | func (u *upstreamState) getImageOwner(idOrName string) string { 123 | return u.images[idOrName].owner 124 | } 125 | 126 | ////////////// 127 | // networks 128 | 129 | func (u *upstreamState) createNetwork(idOrName string, theOwner string) error { 130 | // Deny if already exists 131 | if _, ok := u.networks[idOrName]; ok { 132 | return fmt.Errorf("Cannot create network with ID/Name '%s', already exists", idOrName) 133 | } 134 | // "Create" it 135 | u.networks[idOrName] = upstreamStateNetwork{ 136 | owner: theOwner, 137 | } 138 | return nil 139 | } 140 | 141 | func (u *upstreamState) deleteNetwork(idOrName string) error { 142 | // Deny if does not exist 143 | if _, ok := u.networks[idOrName]; ok == false { 144 | return fmt.Errorf("Cannot delete network with ID/Name '%s', does not exist", idOrName) 145 | } 146 | // You can't delete a network that has attached "endpoints" on a real Docker daemon, simulate 147 | // that for containers only for now. 148 | for k1, v1 := range u.containers { 149 | for _, v2 := range v1.attachedNetworks { 150 | if v2.name == idOrName { 151 | return fmt.Errorf("Cannot delete network with ID/Name '%s', endpoint still attached (container '%s')", idOrName, k1) 152 | } 153 | } 154 | } 155 | // "Delete" it 156 | delete(u.networks, idOrName) 157 | return nil 158 | } 159 | 160 | func (u *upstreamState) doesNetworkExist(idOrName string) bool { 161 | _, ok := u.networks[idOrName] 162 | return ok 163 | } 164 | 165 | func (u *upstreamState) getNetworkOwner(idOrName string) string { 166 | return u.networks[idOrName].owner 167 | } 168 | 169 | func (u *upstreamState) networkConnectDisconnectChecks(containerIdOrName string, networkIdOrName string) error { 170 | if _, ok := u.containers[containerIdOrName]; ok == false { 171 | return fmt.Errorf("container does not exist") 172 | } 173 | if _, ok := u.networks[networkIdOrName]; ok == false { 174 | return fmt.Errorf("network does not exist") 175 | } 176 | return nil 177 | } 178 | 179 | func (u *upstreamState) isContainerConnectedToNetwork(containerIdOrName string, networkIdOrName string) bool { 180 | // TODOLATER: check the container exists before proceeding? considering what's executing this, skipping duplication for now 181 | for _, v := range u.containers[containerIdOrName].attachedNetworks { 182 | if v.name == networkIdOrName { 183 | return true 184 | } 185 | } 186 | return false 187 | } 188 | 189 | func (u *upstreamState) connectContainerToNetwork(containerIdOrName string, networkIdOrName string, containerAliases []string) error { 190 | // Deny if container or network does not exist 191 | if err := u.networkConnectDisconnectChecks(containerIdOrName, networkIdOrName); err != nil { 192 | return fmt.Errorf("Cannot connect container '%s' to network '%s', %s", containerIdOrName, networkIdOrName, err.Error()) 193 | } 194 | // Check if container is already attached to this network, if so deny 195 | if u.isContainerConnectedToNetwork(containerIdOrName, networkIdOrName) == true { 196 | return fmt.Errorf("Cannot connect container '%s' to network '%s', already attached", containerIdOrName, networkIdOrName) 197 | } 198 | // "Connect" the container to the network 199 | container := u.containers[containerIdOrName] 200 | containerNetwork := upstreamStateContainerAttachedNetwork{ 201 | name: networkIdOrName, 202 | aliases: containerAliases, 203 | } 204 | container.attachedNetworks = append(container.attachedNetworks, containerNetwork) 205 | u.containers[containerIdOrName] = container 206 | return nil 207 | } 208 | 209 | func (u *upstreamState) disconnectContainerToNetwork(containerIdOrName string, networkIdOrName string) error { 210 | // Deny if container or network does not exist 211 | if err := u.networkConnectDisconnectChecks(containerIdOrName, networkIdOrName); err != nil { 212 | return fmt.Errorf("Cannot disconnect container '%s' from network '%s', %s", containerIdOrName, networkIdOrName, err.Error()) 213 | } 214 | // Check if container is already attached to this network, if not deny 215 | if u.isContainerConnectedToNetwork(containerIdOrName, networkIdOrName) == false { 216 | return fmt.Errorf("Cannot disconnect container '%s' from network '%s', not attached", containerIdOrName, networkIdOrName) 217 | } 218 | // "Disconnect" the container from the network 219 | newAttachedNetworks := []upstreamStateContainerAttachedNetwork{} 220 | for _, v := range u.containers[containerIdOrName].attachedNetworks { 221 | if v.name != networkIdOrName { 222 | newAttachedNetworks = append(newAttachedNetworks, v) 223 | } 224 | } 225 | container := u.containers[containerIdOrName] 226 | container.attachedNetworks = newAttachedNetworks 227 | u.containers[containerIdOrName] = container 228 | return nil 229 | } 230 | 231 | ////////////// 232 | // volumes 233 | 234 | func (u *upstreamState) createVolume(name string, theOwner string) error { 235 | // Deny if already exists 236 | if u.doesVolumeExist(name) == true { 237 | return fmt.Errorf("Cannot create volume with Name '%s', already exists", name) 238 | } 239 | // "Create" it 240 | u.volumes[name] = upstreamStateVolume{ 241 | owner: theOwner, 242 | } 243 | return nil 244 | } 245 | 246 | func (u *upstreamState) deleteVolume(name string) error { 247 | // Deny if does not exist 248 | if u.doesVolumeExist(name) == false { 249 | return fmt.Errorf("Cannot delete volume with Name '%s', does not exist", name) 250 | } 251 | // "Delete" it 252 | delete(u.volumes, name) 253 | return nil 254 | } 255 | 256 | func (u *upstreamState) doesVolumeExist(name string) bool { 257 | _, ok := u.volumes[name] 258 | return ok 259 | } 260 | 261 | func (u *upstreamState) getVolumeOwner(name string) string { 262 | return u.volumes[name].owner 263 | } 264 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | sockguard: 5 | image: buildkite/sockguard 6 | build: 7 | context: . -------------------------------------------------------------------------------- /examples/cgroup_parent/Dockerfile: -------------------------------------------------------------------------------- 1 | # Duplicated build process (to ../../Dockerfile), to ease iterating locally using this stack in containers 2 | # If there was an INCLUDE instruction in Dockerfile's we could avoid some duplication here. 3 | # 4 | # For real world usage, use buildkite/sockguard:latest from Docker Hub 5 | # 6 | FROM golang:1.10-alpine as builder 7 | RUN apk add --no-cache ca-certificates 8 | WORKDIR /go/src/github.com/buildkite/sockguard 9 | COPY . . 10 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ 11 | go build -a -installsuffix cgo -ldflags="-w -s" -o /go/bin/sockguard 12 | 13 | # So we can get a shell if we need to dig around for local testing. Main Dockerfile uses scratch 14 | FROM alpine:3.8 15 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 16 | COPY --from=builder /go/bin/sockguard /sockguard 17 | ENTRYPOINT [ "/sockguard" ] 18 | -------------------------------------------------------------------------------- /examples/cgroup_parent/README.md: -------------------------------------------------------------------------------- 1 | Note: if you hit an error like: 2 | 3 | ``` 4 | sockguard_1 | 2018/08/22 20:22:11 listen unix /var/run/docker/sockguard.sock: bind: address already in use 5 | ``` 6 | 7 | starting this, ensure you do a `docker-compose down -v` before `docker-compose up`. The Docker volume retains the old socket on shutdown sometimes. 8 | -------------------------------------------------------------------------------- /examples/cgroup_parent/ci_agent_dev/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:stretch 2 | 3 | RUN apt-get update && \ 4 | apt-get install -y curl && \ 5 | rm -rf /var/run/apt/lists/* 6 | 7 | # Install docker so we can test the unix socket 8 | ENV DOCKER_VERSION 18.06.1 9 | RUN curl -O "https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKER_VERSION}-ce.tgz" && \ 10 | tar xzf "docker-${DOCKER_VERSION}-ce.tgz" && \ 11 | mv /docker/docker /usr/bin/docker && \ 12 | rm -rf /docker && \ 13 | rm -rf "docker-${DOCKER_VERSION}-ce.tgz" 14 | 15 | # Also install docker-compose, for testing/misc 16 | ENV DOCKER_COMPOSE_VERSION 1.22.0 17 | RUN curl -L "https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-$(uname -s)-$(uname -m)" -o /usr/bin/docker-compose && \ 18 | chmod +x /usr/bin/docker-compose 19 | 20 | COPY ./start.sh /start.sh 21 | 22 | RUN chmod +x /start.sh && \ 23 | ln -sf /var/run/docker/sockguard.sock /var/run/docker.sock 24 | 25 | CMD [ "/start.sh" ] 26 | -------------------------------------------------------------------------------- /examples/cgroup_parent/ci_agent_dev/README.md: -------------------------------------------------------------------------------- 1 | This is to simulate the equivalent of a Jenkins/Buildkite (or other CI) agent container. 2 | 3 | Mostly to validate the volume mapping aspects of the Docker daemon socket. 4 | -------------------------------------------------------------------------------- /examples/cgroup_parent/ci_agent_dev/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | trap 'exit 1' SIGINT SIGTERM 4 | 5 | while true; do 6 | sleep 1 7 | done 8 | -------------------------------------------------------------------------------- /examples/cgroup_parent/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # This file validates how you might use sockguard as a sidecar container to some CI worker/agent, 2 | # including the (non-trivial) socket volume mapping. This is required due to the chicken-and-egg 3 | # of creating the socket before the volume otherwise. 4 | 5 | version: '2' 6 | 7 | services: 8 | ci_agent: 9 | build: 10 | context: ./ci_agent_dev 11 | volumes: 12 | - "docker-socket-vol:/var/run/docker/" 13 | sockguard: 14 | build: 15 | context: ../../ 16 | dockerfile: ./examples/cgroup_parent/Dockerfile 17 | command: -filename /var/run/docker/sockguard.sock -cgroup-parent this-container 18 | cgroup_parent: test-cgroup 19 | volumes: 20 | - "docker-socket-vol:/var/run/docker/" 21 | - "/var/run/docker.sock:/var/run/docker.sock" 22 | 23 | volumes: 24 | docker-socket-vol: 25 | -------------------------------------------------------------------------------- /fixtures/containers_create_10_expected.json: -------------------------------------------------------------------------------- 1 | {"AttachStderr":true,"AttachStdin":true,"AttachStdout":true,"Cmd":["sh"],"Domainname":"","Entrypoint":null,"Env":[],"HostConfig":{"AutoRemove":true,"Binds":null,"BlkioDeviceReadBps":null,"BlkioDeviceReadIOps":null,"BlkioDeviceWriteBps":null,"BlkioDeviceWriteIOps":null,"BlkioWeight":0,"BlkioWeightDevice":[],"CapAdd":null,"CapDrop":null,"Cgroup":"","CgroupParent":"","ConsoleSize":[0,0],"ContainerIDFile":"","CpuCount":0,"CpuPercent":0,"CpuPeriod":0,"CpuQuota":0,"CpuRealtimePeriod":0,"CpuRealtimeRuntime":0,"CpuShares":0,"CpusetCpus":"","CpusetMems":"","DeviceCgroupRules":null,"Devices":[],"DiskQuota":0,"Dns":[],"DnsOptions":[],"DnsSearch":[],"ExtraHosts":null,"GroupAdd":null,"IOMaximumBandwidth":0,"IOMaximumIOps":0,"IpcMode":"","Isolation":"","KernelMemory":0,"Links":null,"LogConfig":{"Config":{},"Type":""},"MaskedPaths":null,"Memory":0,"MemoryReservation":0,"MemorySwap":0,"MemorySwappiness":-1,"NanoCpus":0,"NetworkMode":"default","OomKillDisable":false,"OomScoreAdj":0,"PidMode":"","PidsLimit":0,"PortBindings":{},"Privileged":false,"PublishAllPorts":false,"ReadonlyPaths":null,"ReadonlyRootfs":false,"RestartPolicy":{"MaximumRetryCount":0,"Name":"no"},"SecurityOpt":null,"ShmSize":0,"UTSMode":"","Ulimits":null,"UsernsMode":"","VolumeDriver":"","VolumesFrom":null},"Hostname":"","Image":"alpine:3.8","Labels":{"com.buildkite.sockguard.owner":"sockguard-pid-1","somelabel":"somevalue"},"NetworkingConfig":{"EndpointsConfig":{}},"OnBuild":null,"OpenStdin":true,"StdinOnce":true,"Tty":true,"User":"","Volumes":{},"WorkingDir":""} 2 | -------------------------------------------------------------------------------- /fixtures/containers_create_10_in.json: -------------------------------------------------------------------------------- 1 | {"Hostname":"","Domainname":"","User":"","AttachStdin":true,"AttachStdout":true,"AttachStderr":true,"Tty":true,"OpenStdin":true,"StdinOnce":true,"Env":[],"Cmd":["sh"],"Image":"alpine:3.8","Volumes":{},"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":{"somelabel":"somevalue"},"HostConfig":{"Binds":null,"ContainerIDFile":"","LogConfig":{"Type":"","Config":{}},"NetworkMode":"default","PortBindings":{},"RestartPolicy":{"Name":"no","MaximumRetryCount":0},"AutoRemove":true,"VolumeDriver":"","VolumesFrom":null,"CapAdd":null,"CapDrop":null,"Dns":[],"DnsOptions":[],"DnsSearch":[],"ExtraHosts":null,"GroupAdd":null,"IpcMode":"","Cgroup":"","Links":null,"OomScoreAdj":0,"PidMode":"","Privileged":false,"PublishAllPorts":false,"ReadonlyRootfs":false,"SecurityOpt":null,"UTSMode":"","UsernsMode":"","ShmSize":0,"ConsoleSize":[0,0],"Isolation":"","CpuShares":0,"Memory":0,"NanoCpus":0,"CgroupParent":"","BlkioWeight":0,"BlkioWeightDevice":[],"BlkioDeviceReadBps":null,"BlkioDeviceWriteBps":null,"BlkioDeviceReadIOps":null,"BlkioDeviceWriteIOps":null,"CpuPeriod":0,"CpuQuota":0,"CpuRealtimePeriod":0,"CpuRealtimeRuntime":0,"CpusetCpus":"","CpusetMems":"","Devices":[],"DeviceCgroupRules":null,"DiskQuota":0,"KernelMemory":0,"MemoryReservation":0,"MemorySwap":0,"MemorySwappiness":-1,"OomKillDisable":false,"PidsLimit":0,"Ulimits":null,"CpuCount":0,"CpuPercent":0,"IOMaximumIOps":0,"IOMaximumBandwidth":0,"MaskedPaths":null,"ReadonlyPaths":null},"NetworkingConfig":{"EndpointsConfig":{}}} 2 | -------------------------------------------------------------------------------- /fixtures/containers_create_11_expected.json: -------------------------------------------------------------------------------- 1 | {"AttachStderr":true,"AttachStdin":true,"AttachStdout":true,"Cmd":["sh"],"Domainname":"","Entrypoint":null,"Env":[],"HostConfig":{"AutoRemove":true,"Binds":null,"BlkioDeviceReadBps":null,"BlkioDeviceReadIOps":null,"BlkioDeviceWriteBps":null,"BlkioDeviceWriteIOps":null,"BlkioWeight":0,"BlkioWeightDevice":[],"CapAdd":null,"CapDrop":null,"Cgroup":"","CgroupParent":"","ConsoleSize":[0,0],"ContainerIDFile":"","CpuCount":0,"CpuPercent":0,"CpuPeriod":0,"CpuQuota":0,"CpuRealtimePeriod":0,"CpuRealtimeRuntime":0,"CpuShares":0,"CpusetCpus":"","CpusetMems":"","DeviceCgroupRules":null,"Devices":[],"DiskQuota":0,"Dns":[],"DnsOptions":[],"DnsSearch":[],"ExtraHosts":null,"GroupAdd":null,"IOMaximumBandwidth":0,"IOMaximumIOps":0,"IpcMode":"","Isolation":"","KernelMemory":0,"Links":["asdf:zzzz"],"LogConfig":{"Config":{},"Type":""},"MaskedPaths":null,"Memory":0,"MemoryReservation":0,"MemorySwap":0,"MemorySwappiness":-1,"NanoCpus":0,"NetworkMode":"default","OomKillDisable":false,"OomScoreAdj":0,"PidMode":"","PidsLimit":0,"PortBindings":{},"Privileged":false,"PublishAllPorts":false,"ReadonlyPaths":null,"ReadonlyRootfs":false,"RestartPolicy":{"MaximumRetryCount":0,"Name":"no"},"SecurityOpt":null,"ShmSize":0,"UTSMode":"","Ulimits":null,"UsernsMode":"","VolumeDriver":"","VolumesFrom":null},"Hostname":"","Image":"alpine:3.8","Labels":{"com.buildkite.sockguard.owner":"sockguard-pid-1"},"NetworkingConfig":{"EndpointsConfig":{}},"OnBuild":null,"OpenStdin":true,"StdinOnce":true,"Tty":true,"User":"","Volumes":{},"WorkingDir":""} 2 | -------------------------------------------------------------------------------- /fixtures/containers_create_11_in.json: -------------------------------------------------------------------------------- 1 | {"Hostname":"","Domainname":"","User":"","AttachStdin":true,"AttachStdout":true,"AttachStderr":true,"Tty":true,"OpenStdin":true,"StdinOnce":true,"Env":[],"Cmd":["sh"],"Image":"alpine:3.8","Volumes":{},"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":{},"HostConfig":{"Binds":null,"ContainerIDFile":"","LogConfig":{"Type":"","Config":{}},"NetworkMode":"default","PortBindings":{},"RestartPolicy":{"Name":"no","MaximumRetryCount":0},"AutoRemove":true,"VolumeDriver":"","VolumesFrom":null,"CapAdd":null,"CapDrop":null,"Dns":[],"DnsOptions":[],"DnsSearch":[],"ExtraHosts":null,"GroupAdd":null,"IpcMode":"","Cgroup":"","Links":null,"OomScoreAdj":0,"PidMode":"","Privileged":false,"PublishAllPorts":false,"ReadonlyRootfs":false,"SecurityOpt":null,"UTSMode":"","UsernsMode":"","ShmSize":0,"ConsoleSize":[0,0],"Isolation":"","CpuShares":0,"Memory":0,"NanoCpus":0,"CgroupParent":"","BlkioWeight":0,"BlkioWeightDevice":[],"BlkioDeviceReadBps":null,"BlkioDeviceWriteBps":null,"BlkioDeviceReadIOps":null,"BlkioDeviceWriteIOps":null,"CpuPeriod":0,"CpuQuota":0,"CpuRealtimePeriod":0,"CpuRealtimeRuntime":0,"CpusetCpus":"","CpusetMems":"","Devices":[],"DeviceCgroupRules":null,"DiskQuota":0,"KernelMemory":0,"MemoryReservation":0,"MemorySwap":0,"MemorySwappiness":-1,"OomKillDisable":false,"PidsLimit":0,"Ulimits":null,"CpuCount":0,"CpuPercent":0,"IOMaximumIOps":0,"IOMaximumBandwidth":0,"MaskedPaths":null,"ReadonlyPaths":null},"NetworkingConfig":{"EndpointsConfig":{}}} 2 | -------------------------------------------------------------------------------- /fixtures/containers_create_12_expected.json: -------------------------------------------------------------------------------- 1 | {"AttachStderr":true,"AttachStdin":true,"AttachStdout":true,"Cmd":["sh"],"Domainname":"","Entrypoint":null,"Env":[],"HostConfig":{"AutoRemove":true,"Binds":null,"BlkioDeviceReadBps":null,"BlkioDeviceReadIOps":null,"BlkioDeviceWriteBps":null,"BlkioDeviceWriteIOps":null,"BlkioWeight":0,"BlkioWeightDevice":[],"CapAdd":null,"CapDrop":null,"Cgroup":"","CgroupParent":"","ConsoleSize":[0,0],"ContainerIDFile":"","CpuCount":0,"CpuPercent":0,"CpuPeriod":0,"CpuQuota":0,"CpuRealtimePeriod":0,"CpuRealtimeRuntime":0,"CpuShares":0,"CpusetCpus":"","CpusetMems":"","DeviceCgroupRules":null,"Devices":[],"DiskQuota":0,"Dns":[],"DnsOptions":[],"DnsSearch":[],"ExtraHosts":null,"GroupAdd":null,"IOMaximumBandwidth":0,"IOMaximumIOps":0,"IpcMode":"","Isolation":"","KernelMemory":0,"Links":["asdf:zzzz"],"LogConfig":{"Config":{},"Type":""},"MaskedPaths":null,"Memory":0,"MemoryReservation":0,"MemorySwap":0,"MemorySwappiness":-1,"NanoCpus":0,"NetworkMode":"default","OomKillDisable":false,"OomScoreAdj":0,"PidMode":"","PidsLimit":0,"PortBindings":{},"Privileged":false,"PublishAllPorts":false,"ReadonlyPaths":null,"ReadonlyRootfs":false,"RestartPolicy":{"MaximumRetryCount":0,"Name":"no"},"SecurityOpt":null,"ShmSize":0,"UTSMode":"","Ulimits":null,"UsernsMode":"","VolumeDriver":"","VolumesFrom":null},"Hostname":"","Image":"alpine:3.8","Labels":{"com.buildkite.sockguard.owner":"sockguard-pid-1"},"NetworkingConfig":{"EndpointsConfig":{"somenetwork":{}}},"OnBuild":null,"OpenStdin":true,"StdinOnce":true,"Tty":true,"User":"","Volumes":{},"WorkingDir":""} 2 | -------------------------------------------------------------------------------- /fixtures/containers_create_12_in.json: -------------------------------------------------------------------------------- 1 | {"Hostname":"","Domainname":"","User":"","AttachStdin":true,"AttachStdout":true,"AttachStderr":true,"Tty":true,"OpenStdin":true,"StdinOnce":true,"Env":[],"Cmd":["sh"],"Image":"alpine:3.8","Volumes":{},"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":{},"HostConfig":{"Binds":null,"ContainerIDFile":"","LogConfig":{"Type":"","Config":{}},"NetworkMode":"default","PortBindings":{},"RestartPolicy":{"Name":"no","MaximumRetryCount":0},"AutoRemove":true,"VolumeDriver":"","VolumesFrom":null,"CapAdd":null,"CapDrop":null,"Dns":[],"DnsOptions":[],"DnsSearch":[],"ExtraHosts":null,"GroupAdd":null,"IpcMode":"","Cgroup":"","Links":null,"OomScoreAdj":0,"PidMode":"","Privileged":false,"PublishAllPorts":false,"ReadonlyRootfs":false,"SecurityOpt":null,"UTSMode":"","UsernsMode":"","ShmSize":0,"ConsoleSize":[0,0],"Isolation":"","CpuShares":0,"Memory":0,"NanoCpus":0,"CgroupParent":"","BlkioWeight":0,"BlkioWeightDevice":[],"BlkioDeviceReadBps":null,"BlkioDeviceWriteBps":null,"BlkioDeviceReadIOps":null,"BlkioDeviceWriteIOps":null,"CpuPeriod":0,"CpuQuota":0,"CpuRealtimePeriod":0,"CpuRealtimeRuntime":0,"CpusetCpus":"","CpusetMems":"","Devices":[],"DeviceCgroupRules":null,"DiskQuota":0,"KernelMemory":0,"MemoryReservation":0,"MemorySwap":0,"MemorySwappiness":-1,"OomKillDisable":false,"PidsLimit":0,"Ulimits":null,"CpuCount":0,"CpuPercent":0,"IOMaximumIOps":0,"IOMaximumBandwidth":0,"MaskedPaths":null,"ReadonlyPaths":null},"NetworkingConfig":{"EndpointsConfig":{"somenetwork":{}}}} 2 | -------------------------------------------------------------------------------- /fixtures/containers_create_13_expected.json: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /fixtures/containers_create_13_in.json: -------------------------------------------------------------------------------- 1 | {"Hostname":"","Domainname":"","User":"","AttachStdin":true,"AttachStdout":true,"AttachStderr":true,"Tty":true,"OpenStdin":true,"StdinOnce":true,"Env":[],"Cmd":["sh"],"Image":"alpine:3.8","Volumes":{},"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":{},"HostConfig":{"Binds":null,"ContainerIDFile":"","LogConfig":{"Type":"","Config":{}},"NetworkMode":"default","PortBindings":{},"RestartPolicy":{"Name":"no","MaximumRetryCount":0},"AutoRemove":true,"VolumeDriver":"","VolumesFrom":null,"CapAdd":null,"CapDrop":null,"Dns":[],"DnsOptions":[],"DnsSearch":[],"ExtraHosts":null,"GroupAdd":null,"IpcMode":"","Cgroup":"","Links":null,"OomScoreAdj":0,"PidMode":"","Privileged":false,"PublishAllPorts":false,"ReadonlyRootfs":false,"SecurityOpt":null,"UTSMode":"","UsernsMode":"","ShmSize":0,"ConsoleSize":[0,0],"Isolation":"","CpuShares":0,"Memory":0,"NanoCpus":0,"CgroupParent":"some-cgroup","BlkioWeight":0,"BlkioWeightDevice":[],"BlkioDeviceReadBps":null,"BlkioDeviceWriteBps":null,"BlkioDeviceReadIOps":null,"BlkioDeviceWriteIOps":null,"CpuPeriod":0,"CpuQuota":0,"CpuRealtimePeriod":0,"CpuRealtimeRuntime":0,"CpusetCpus":"","CpusetMems":"","Devices":[],"DeviceCgroupRules":null,"DiskQuota":0,"KernelMemory":0,"MemoryReservation":0,"MemorySwap":0,"MemorySwappiness":-1,"OomKillDisable":false,"PidsLimit":0,"Ulimits":null,"CpuCount":0,"CpuPercent":0,"IOMaximumIOps":0,"IOMaximumBandwidth":0,"MaskedPaths":null,"ReadonlyPaths":null},"NetworkingConfig":{"EndpointsConfig":{}}} 2 | -------------------------------------------------------------------------------- /fixtures/containers_create_14_expected.json: -------------------------------------------------------------------------------- 1 | {"AttachStderr":true,"AttachStdin":true,"AttachStdout":true,"Cmd":["sh"],"Domainname":"","Entrypoint":null,"Env":[],"HostConfig":{"AutoRemove":true,"Binds":null,"BlkioDeviceReadBps":null,"BlkioDeviceReadIOps":null,"BlkioDeviceWriteBps":null,"BlkioDeviceWriteIOps":null,"BlkioWeight":0,"BlkioWeightDevice":[],"CapAdd":null,"CapDrop":null,"Cgroup":"","CgroupParent":"","ConsoleSize":[0,0],"ContainerIDFile":"","CpuCount":0,"CpuPercent":0,"CpuPeriod":0,"CpuQuota":0,"CpuRealtimePeriod":0,"CpuRealtimeRuntime":0,"CpuShares":0,"CpusetCpus":"","CpusetMems":"","DeviceCgroupRules":null,"Devices":[],"DiskQuota":0,"Dns":[],"DnsOptions":[],"DnsSearch":[],"ExtraHosts":null,"GroupAdd":null,"IOMaximumBandwidth":0,"IOMaximumIOps":0,"IpcMode":"","Isolation":"","KernelMemory":0,"Links":["xxxx:yyyy","cccc:dddd"],"LogConfig":{"Config":{},"Type":""},"MaskedPaths":null,"Memory":0,"MemoryReservation":0,"MemorySwap":0,"MemorySwappiness":-1,"NanoCpus":0,"NetworkMode":"default","OomKillDisable":false,"OomScoreAdj":0,"PidMode":"","PidsLimit":0,"PortBindings":{},"Privileged":false,"PublishAllPorts":false,"ReadonlyPaths":null,"ReadonlyRootfs":false,"RestartPolicy":{"MaximumRetryCount":0,"Name":"no"},"SecurityOpt":null,"ShmSize":0,"UTSMode":"","Ulimits":null,"UsernsMode":"","VolumeDriver":"","VolumesFrom":null},"Hostname":"","Image":"alpine:3.8","Labels":{"com.buildkite.sockguard.owner":"sockguard-pid-1"},"NetworkingConfig":{"EndpointsConfig":{}},"OnBuild":null,"OpenStdin":true,"StdinOnce":true,"Tty":true,"User":"","Volumes":{},"WorkingDir":""} 2 | -------------------------------------------------------------------------------- /fixtures/containers_create_14_in.json: -------------------------------------------------------------------------------- 1 | {"Hostname":"","Domainname":"","User":"","AttachStdin":true,"AttachStdout":true,"AttachStderr":true,"Tty":true,"OpenStdin":true,"StdinOnce":true,"Env":[],"Cmd":["sh"],"Image":"alpine:3.8","Volumes":{},"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":{},"HostConfig":{"Binds":null,"ContainerIDFile":"","LogConfig":{"Type":"","Config":{}},"NetworkMode":"default","PortBindings":{},"RestartPolicy":{"Name":"no","MaximumRetryCount":0},"AutoRemove":true,"VolumeDriver":"","VolumesFrom":null,"CapAdd":null,"CapDrop":null,"Dns":[],"DnsOptions":[],"DnsSearch":[],"ExtraHosts":null,"GroupAdd":null,"IpcMode":"","Cgroup":"","Links":["xxxx:yyyy"],"OomScoreAdj":0,"PidMode":"","Privileged":false,"PublishAllPorts":false,"ReadonlyRootfs":false,"SecurityOpt":null,"UTSMode":"","UsernsMode":"","ShmSize":0,"ConsoleSize":[0,0],"Isolation":"","CpuShares":0,"Memory":0,"NanoCpus":0,"CgroupParent":"","BlkioWeight":0,"BlkioWeightDevice":[],"BlkioDeviceReadBps":null,"BlkioDeviceWriteBps":null,"BlkioDeviceReadIOps":null,"BlkioDeviceWriteIOps":null,"CpuPeriod":0,"CpuQuota":0,"CpuRealtimePeriod":0,"CpuRealtimeRuntime":0,"CpusetCpus":"","CpusetMems":"","Devices":[],"DeviceCgroupRules":null,"DiskQuota":0,"KernelMemory":0,"MemoryReservation":0,"MemorySwap":0,"MemorySwappiness":-1,"OomKillDisable":false,"PidsLimit":0,"Ulimits":null,"CpuCount":0,"CpuPercent":0,"IOMaximumIOps":0,"IOMaximumBandwidth":0,"MaskedPaths":null,"ReadonlyPaths":null},"NetworkingConfig":{"EndpointsConfig":{}}} 2 | -------------------------------------------------------------------------------- /fixtures/containers_create_1_expected.json: -------------------------------------------------------------------------------- 1 | {"AttachStderr":true,"AttachStdin":true,"AttachStdout":true,"Cmd":["sh"],"Domainname":"","Entrypoint":null,"Env":[],"HostConfig":{"AutoRemove":true,"Binds":null,"BlkioDeviceReadBps":null,"BlkioDeviceReadIOps":null,"BlkioDeviceWriteBps":null,"BlkioDeviceWriteIOps":null,"BlkioWeight":0,"BlkioWeightDevice":[],"CapAdd":null,"CapDrop":null,"Cgroup":"","CgroupParent":"","ConsoleSize":[0,0],"ContainerIDFile":"","CpuCount":0,"CpuPercent":0,"CpuPeriod":0,"CpuQuota":0,"CpuRealtimePeriod":0,"CpuRealtimeRuntime":0,"CpuShares":0,"CpusetCpus":"","CpusetMems":"","DeviceCgroupRules":null,"Devices":[],"DiskQuota":0,"Dns":[],"DnsOptions":[],"DnsSearch":[],"ExtraHosts":null,"GroupAdd":null,"IOMaximumBandwidth":0,"IOMaximumIOps":0,"IpcMode":"","Isolation":"","KernelMemory":0,"Links":null,"LogConfig":{"Config":{},"Type":""},"MaskedPaths":null,"Memory":0,"MemoryReservation":0,"MemorySwap":0,"MemorySwappiness":-1,"NanoCpus":0,"NetworkMode":"default","OomKillDisable":false,"OomScoreAdj":0,"PidMode":"","PidsLimit":0,"PortBindings":{},"Privileged":false,"PublishAllPorts":false,"ReadonlyPaths":null,"ReadonlyRootfs":false,"RestartPolicy":{"MaximumRetryCount":0,"Name":"no"},"SecurityOpt":null,"ShmSize":0,"UTSMode":"","Ulimits":null,"UsernsMode":"","VolumeDriver":"","VolumesFrom":null},"Hostname":"","Image":"alpine:3.8","Labels":{"com.buildkite.sockguard.owner":"sockguard-pid-1"},"NetworkingConfig":{"EndpointsConfig":{}},"OnBuild":null,"OpenStdin":true,"StdinOnce":true,"Tty":true,"User":"","Volumes":{},"WorkingDir":""} 2 | -------------------------------------------------------------------------------- /fixtures/containers_create_1_in.json: -------------------------------------------------------------------------------- 1 | {"Hostname":"","Domainname":"","User":"","AttachStdin":true,"AttachStdout":true,"AttachStderr":true,"Tty":true,"OpenStdin":true,"StdinOnce":true,"Env":[],"Cmd":["sh"],"Image":"alpine:3.8","Volumes":{},"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":{},"HostConfig":{"Binds":null,"ContainerIDFile":"","LogConfig":{"Type":"","Config":{}},"NetworkMode":"default","PortBindings":{},"RestartPolicy":{"Name":"no","MaximumRetryCount":0},"AutoRemove":true,"VolumeDriver":"","VolumesFrom":null,"CapAdd":null,"CapDrop":null,"Dns":[],"DnsOptions":[],"DnsSearch":[],"ExtraHosts":null,"GroupAdd":null,"IpcMode":"","Cgroup":"","Links":null,"OomScoreAdj":0,"PidMode":"","Privileged":false,"PublishAllPorts":false,"ReadonlyRootfs":false,"SecurityOpt":null,"UTSMode":"","UsernsMode":"","ShmSize":0,"ConsoleSize":[0,0],"Isolation":"","CpuShares":0,"Memory":0,"NanoCpus":0,"CgroupParent":"","BlkioWeight":0,"BlkioWeightDevice":[],"BlkioDeviceReadBps":null,"BlkioDeviceWriteBps":null,"BlkioDeviceReadIOps":null,"BlkioDeviceWriteIOps":null,"CpuPeriod":0,"CpuQuota":0,"CpuRealtimePeriod":0,"CpuRealtimeRuntime":0,"CpusetCpus":"","CpusetMems":"","Devices":[],"DeviceCgroupRules":null,"DiskQuota":0,"KernelMemory":0,"MemoryReservation":0,"MemorySwap":0,"MemorySwappiness":-1,"OomKillDisable":false,"PidsLimit":0,"Ulimits":null,"CpuCount":0,"CpuPercent":0,"IOMaximumIOps":0,"IOMaximumBandwidth":0,"MaskedPaths":null,"ReadonlyPaths":null},"NetworkingConfig":{"EndpointsConfig":{}}} 2 | -------------------------------------------------------------------------------- /fixtures/containers_create_2_expected.json: -------------------------------------------------------------------------------- 1 | {"AttachStderr":true,"AttachStdin":true,"AttachStdout":true,"Cmd":["sh"],"Domainname":"","Entrypoint":null,"Env":[],"HostConfig":{"AutoRemove":true,"Binds":null,"BlkioDeviceReadBps":null,"BlkioDeviceReadIOps":null,"BlkioDeviceWriteBps":null,"BlkioDeviceWriteIOps":null,"BlkioWeight":0,"BlkioWeightDevice":[],"CapAdd":null,"CapDrop":null,"Cgroup":"","CgroupParent":"","ConsoleSize":[0,0],"ContainerIDFile":"","CpuCount":0,"CpuPercent":0,"CpuPeriod":0,"CpuQuota":0,"CpuRealtimePeriod":0,"CpuRealtimeRuntime":0,"CpuShares":0,"CpusetCpus":"","CpusetMems":"","DeviceCgroupRules":null,"Devices":[],"DiskQuota":0,"Dns":[],"DnsOptions":[],"DnsSearch":[],"ExtraHosts":null,"GroupAdd":null,"IOMaximumBandwidth":0,"IOMaximumIOps":0,"IpcMode":"","Isolation":"","KernelMemory":0,"Links":null,"LogConfig":{"Config":{},"Type":""},"MaskedPaths":null,"Memory":0,"MemoryReservation":0,"MemorySwap":0,"MemorySwappiness":-1,"NanoCpus":0,"NetworkMode":"default","OomKillDisable":false,"OomScoreAdj":0,"PidMode":"","PidsLimit":0,"PortBindings":{},"Privileged":false,"PublishAllPorts":false,"ReadonlyPaths":null,"ReadonlyRootfs":false,"RestartPolicy":{"MaximumRetryCount":0,"Name":"no"},"SecurityOpt":null,"ShmSize":0,"UTSMode":"","Ulimits":null,"UsernsMode":"","VolumeDriver":"","VolumesFrom":null},"Hostname":"","Image":"alpine:3.8","Labels":{"com.buildkite.sockguard.owner":"test-owner"},"NetworkingConfig":{"EndpointsConfig":{}},"OnBuild":null,"OpenStdin":true,"StdinOnce":true,"Tty":true,"User":"","Volumes":{},"WorkingDir":""} 2 | -------------------------------------------------------------------------------- /fixtures/containers_create_2_in.json: -------------------------------------------------------------------------------- 1 | {"Hostname":"","Domainname":"","User":"","AttachStdin":true,"AttachStdout":true,"AttachStderr":true,"Tty":true,"OpenStdin":true,"StdinOnce":true,"Env":[],"Cmd":["sh"],"Image":"alpine:3.8","Volumes":{},"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":{},"HostConfig":{"Binds":null,"ContainerIDFile":"","LogConfig":{"Type":"","Config":{}},"NetworkMode":"default","PortBindings":{},"RestartPolicy":{"Name":"no","MaximumRetryCount":0},"AutoRemove":true,"VolumeDriver":"","VolumesFrom":null,"CapAdd":null,"CapDrop":null,"Dns":[],"DnsOptions":[],"DnsSearch":[],"ExtraHosts":null,"GroupAdd":null,"IpcMode":"","Cgroup":"","Links":null,"OomScoreAdj":0,"PidMode":"","Privileged":false,"PublishAllPorts":false,"ReadonlyRootfs":false,"SecurityOpt":null,"UTSMode":"","UsernsMode":"","ShmSize":0,"ConsoleSize":[0,0],"Isolation":"","CpuShares":0,"Memory":0,"NanoCpus":0,"CgroupParent":"","BlkioWeight":0,"BlkioWeightDevice":[],"BlkioDeviceReadBps":null,"BlkioDeviceWriteBps":null,"BlkioDeviceReadIOps":null,"BlkioDeviceWriteIOps":null,"CpuPeriod":0,"CpuQuota":0,"CpuRealtimePeriod":0,"CpuRealtimeRuntime":0,"CpusetCpus":"","CpusetMems":"","Devices":[],"DeviceCgroupRules":null,"DiskQuota":0,"KernelMemory":0,"MemoryReservation":0,"MemorySwap":0,"MemorySwappiness":-1,"OomKillDisable":false,"PidsLimit":0,"Ulimits":null,"CpuCount":0,"CpuPercent":0,"IOMaximumIOps":0,"IOMaximumBandwidth":0,"MaskedPaths":null,"ReadonlyPaths":null},"NetworkingConfig":{"EndpointsConfig":{}}} 2 | -------------------------------------------------------------------------------- /fixtures/containers_create_3_expected.json: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /fixtures/containers_create_3_in.json: -------------------------------------------------------------------------------- 1 | {"Hostname":"","Domainname":"","User":"","AttachStdin":true,"AttachStdout":true,"AttachStderr":true,"Tty":true,"OpenStdin":true,"StdinOnce":true,"Env":[],"Cmd":["sh"],"Image":"alpine:3.8","Volumes":{},"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":{},"HostConfig":{"Binds":["/tmp:/tmp"],"ContainerIDFile":"","LogConfig":{"Type":"","Config":{}},"NetworkMode":"default","PortBindings":{},"RestartPolicy":{"Name":"no","MaximumRetryCount":0},"AutoRemove":true,"VolumeDriver":"","VolumesFrom":null,"CapAdd":null,"CapDrop":null,"Dns":[],"DnsOptions":[],"DnsSearch":[],"ExtraHosts":null,"GroupAdd":null,"IpcMode":"","Cgroup":"","Links":null,"OomScoreAdj":0,"PidMode":"","Privileged":false,"PublishAllPorts":false,"ReadonlyRootfs":false,"SecurityOpt":null,"UTSMode":"","UsernsMode":"","ShmSize":0,"ConsoleSize":[0,0],"Isolation":"","CpuShares":0,"Memory":0,"NanoCpus":0,"CgroupParent":"","BlkioWeight":0,"BlkioWeightDevice":[],"BlkioDeviceReadBps":null,"BlkioDeviceWriteBps":null,"BlkioDeviceReadIOps":null,"BlkioDeviceWriteIOps":null,"CpuPeriod":0,"CpuQuota":0,"CpuRealtimePeriod":0,"CpuRealtimeRuntime":0,"CpusetCpus":"","CpusetMems":"","Devices":[],"DeviceCgroupRules":null,"DiskQuota":0,"KernelMemory":0,"MemoryReservation":0,"MemorySwap":0,"MemorySwappiness":-1,"OomKillDisable":false,"PidsLimit":0,"Ulimits":null,"CpuCount":0,"CpuPercent":0,"IOMaximumIOps":0,"IOMaximumBandwidth":0,"MaskedPaths":null,"ReadonlyPaths":null},"NetworkingConfig":{"EndpointsConfig":{}}} 2 | -------------------------------------------------------------------------------- /fixtures/containers_create_4_expected.json: -------------------------------------------------------------------------------- 1 | {"AttachStderr":true,"AttachStdin":true,"AttachStdout":true,"Cmd":["sh"],"Domainname":"","Entrypoint":null,"Env":[],"HostConfig":{"AutoRemove":true,"Binds":["/tmp:/tmp"],"BlkioDeviceReadBps":null,"BlkioDeviceReadIOps":null,"BlkioDeviceWriteBps":null,"BlkioDeviceWriteIOps":null,"BlkioWeight":0,"BlkioWeightDevice":[],"CapAdd":null,"CapDrop":null,"Cgroup":"","CgroupParent":"","ConsoleSize":[0,0],"ContainerIDFile":"","CpuCount":0,"CpuPercent":0,"CpuPeriod":0,"CpuQuota":0,"CpuRealtimePeriod":0,"CpuRealtimeRuntime":0,"CpuShares":0,"CpusetCpus":"","CpusetMems":"","DeviceCgroupRules":null,"Devices":[],"DiskQuota":0,"Dns":[],"DnsOptions":[],"DnsSearch":[],"ExtraHosts":null,"GroupAdd":null,"IOMaximumBandwidth":0,"IOMaximumIOps":0,"IpcMode":"","Isolation":"","KernelMemory":0,"Links":null,"LogConfig":{"Config":{},"Type":""},"MaskedPaths":null,"Memory":0,"MemoryReservation":0,"MemorySwap":0,"MemorySwappiness":-1,"NanoCpus":0,"NetworkMode":"default","OomKillDisable":false,"OomScoreAdj":0,"PidMode":"","PidsLimit":0,"PortBindings":{},"Privileged":false,"PublishAllPorts":false,"ReadonlyPaths":null,"ReadonlyRootfs":false,"RestartPolicy":{"MaximumRetryCount":0,"Name":"no"},"SecurityOpt":null,"ShmSize":0,"UTSMode":"","Ulimits":null,"UsernsMode":"","VolumeDriver":"","VolumesFrom":null},"Hostname":"","Image":"alpine:3.8","Labels":{"com.buildkite.sockguard.owner":"sockguard-pid-1"},"NetworkingConfig":{"EndpointsConfig":{}},"OnBuild":null,"OpenStdin":true,"StdinOnce":true,"Tty":true,"User":"","Volumes":{},"WorkingDir":""} 2 | -------------------------------------------------------------------------------- /fixtures/containers_create_4_in.json: -------------------------------------------------------------------------------- 1 | {"Hostname":"","Domainname":"","User":"","AttachStdin":true,"AttachStdout":true,"AttachStderr":true,"Tty":true,"OpenStdin":true,"StdinOnce":true,"Env":[],"Cmd":["sh"],"Image":"alpine:3.8","Volumes":{},"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":{},"HostConfig":{"Binds":["/tmp:/tmp"],"ContainerIDFile":"","LogConfig":{"Type":"","Config":{}},"NetworkMode":"default","PortBindings":{},"RestartPolicy":{"Name":"no","MaximumRetryCount":0},"AutoRemove":true,"VolumeDriver":"","VolumesFrom":null,"CapAdd":null,"CapDrop":null,"Dns":[],"DnsOptions":[],"DnsSearch":[],"ExtraHosts":null,"GroupAdd":null,"IpcMode":"","Cgroup":"","Links":null,"OomScoreAdj":0,"PidMode":"","Privileged":false,"PublishAllPorts":false,"ReadonlyRootfs":false,"SecurityOpt":null,"UTSMode":"","UsernsMode":"","ShmSize":0,"ConsoleSize":[0,0],"Isolation":"","CpuShares":0,"Memory":0,"NanoCpus":0,"CgroupParent":"","BlkioWeight":0,"BlkioWeightDevice":[],"BlkioDeviceReadBps":null,"BlkioDeviceWriteBps":null,"BlkioDeviceReadIOps":null,"BlkioDeviceWriteIOps":null,"CpuPeriod":0,"CpuQuota":0,"CpuRealtimePeriod":0,"CpuRealtimeRuntime":0,"CpusetCpus":"","CpusetMems":"","Devices":[],"DeviceCgroupRules":null,"DiskQuota":0,"KernelMemory":0,"MemoryReservation":0,"MemorySwap":0,"MemorySwappiness":-1,"OomKillDisable":false,"PidsLimit":0,"Ulimits":null,"CpuCount":0,"CpuPercent":0,"IOMaximumIOps":0,"IOMaximumBandwidth":0,"MaskedPaths":null,"ReadonlyPaths":null},"NetworkingConfig":{"EndpointsConfig":{}}} 2 | -------------------------------------------------------------------------------- /fixtures/containers_create_5_expected.json: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /fixtures/containers_create_5_in.json: -------------------------------------------------------------------------------- 1 | {"Hostname":"","Domainname":"","User":"","AttachStdin":true,"AttachStdout":true,"AttachStderr":true,"Tty":true,"OpenStdin":true,"StdinOnce":true,"Env":[],"Cmd":["sh"],"Image":"alpine:3.8","Volumes":{},"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":{},"HostConfig":{"Binds":["/root:/root"],"ContainerIDFile":"","LogConfig":{"Type":"","Config":{}},"NetworkMode":"default","PortBindings":{},"RestartPolicy":{"Name":"no","MaximumRetryCount":0},"AutoRemove":true,"VolumeDriver":"","VolumesFrom":null,"CapAdd":null,"CapDrop":null,"Dns":[],"DnsOptions":[],"DnsSearch":[],"ExtraHosts":null,"GroupAdd":null,"IpcMode":"","Cgroup":"","Links":null,"OomScoreAdj":0,"PidMode":"","Privileged":false,"PublishAllPorts":false,"ReadonlyRootfs":false,"SecurityOpt":null,"UTSMode":"","UsernsMode":"","ShmSize":0,"ConsoleSize":[0,0],"Isolation":"","CpuShares":0,"Memory":0,"NanoCpus":0,"CgroupParent":"","BlkioWeight":0,"BlkioWeightDevice":[],"BlkioDeviceReadBps":null,"BlkioDeviceWriteBps":null,"BlkioDeviceReadIOps":null,"BlkioDeviceWriteIOps":null,"CpuPeriod":0,"CpuQuota":0,"CpuRealtimePeriod":0,"CpuRealtimeRuntime":0,"CpusetCpus":"","CpusetMems":"","Devices":[],"DeviceCgroupRules":null,"DiskQuota":0,"KernelMemory":0,"MemoryReservation":0,"MemorySwap":0,"MemorySwappiness":-1,"OomKillDisable":false,"PidsLimit":0,"Ulimits":null,"CpuCount":0,"CpuPercent":0,"IOMaximumIOps":0,"IOMaximumBandwidth":0,"MaskedPaths":null,"ReadonlyPaths":null},"NetworkingConfig":{"EndpointsConfig":{}}} 2 | -------------------------------------------------------------------------------- /fixtures/containers_create_6_expected.json: -------------------------------------------------------------------------------- 1 | {"AttachStderr":true,"AttachStdin":true,"AttachStdout":true,"Cmd":["sh"],"Domainname":"","Entrypoint":null,"Env":[],"HostConfig":{"AutoRemove":true,"Binds":null,"BlkioDeviceReadBps":null,"BlkioDeviceReadIOps":null,"BlkioDeviceWriteBps":null,"BlkioDeviceWriteIOps":null,"BlkioWeight":0,"BlkioWeightDevice":[],"CapAdd":null,"CapDrop":null,"Cgroup":"","CgroupParent":"","ConsoleSize":[0,0],"ContainerIDFile":"","CpuCount":0,"CpuPercent":0,"CpuPeriod":0,"CpuQuota":0,"CpuRealtimePeriod":0,"CpuRealtimeRuntime":0,"CpuShares":0,"CpusetCpus":"","CpusetMems":"","DeviceCgroupRules":null,"Devices":[],"DiskQuota":0,"Dns":[],"DnsOptions":[],"DnsSearch":[],"ExtraHosts":null,"GroupAdd":null,"IOMaximumBandwidth":0,"IOMaximumIOps":0,"IpcMode":"","Isolation":"","KernelMemory":0,"Links":null,"LogConfig":{"Config":{},"Type":""},"MaskedPaths":null,"Memory":0,"MemoryReservation":0,"MemorySwap":0,"MemorySwappiness":-1,"NanoCpus":0,"NetworkMode":"host","OomKillDisable":false,"OomScoreAdj":0,"PidMode":"","PidsLimit":0,"PortBindings":{},"Privileged":false,"PublishAllPorts":false,"ReadonlyPaths":null,"ReadonlyRootfs":false,"RestartPolicy":{"MaximumRetryCount":0,"Name":"no"},"SecurityOpt":null,"ShmSize":0,"UTSMode":"","Ulimits":null,"UsernsMode":"","VolumeDriver":"","VolumesFrom":null},"Hostname":"","Image":"alpine:3.8","Labels":{"com.buildkite.sockguard.owner":"sockguard-pid-1"},"NetworkingConfig":{"EndpointsConfig":{}},"OnBuild":null,"OpenStdin":true,"StdinOnce":true,"Tty":true,"User":"","Volumes":{},"WorkingDir":""} 2 | -------------------------------------------------------------------------------- /fixtures/containers_create_6_in.json: -------------------------------------------------------------------------------- 1 | {"Hostname":"","Domainname":"","User":"","AttachStdin":true,"AttachStdout":true,"AttachStderr":true,"Tty":true,"OpenStdin":true,"StdinOnce":true,"Env":[],"Cmd":["sh"],"Image":"alpine:3.8","Volumes":{},"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":{},"HostConfig":{"Binds":null,"ContainerIDFile":"","LogConfig":{"Type":"","Config":{}},"NetworkMode":"host","PortBindings":{},"RestartPolicy":{"Name":"no","MaximumRetryCount":0},"AutoRemove":true,"VolumeDriver":"","VolumesFrom":null,"CapAdd":null,"CapDrop":null,"Dns":[],"DnsOptions":[],"DnsSearch":[],"ExtraHosts":null,"GroupAdd":null,"IpcMode":"","Cgroup":"","Links":null,"OomScoreAdj":0,"PidMode":"","Privileged":false,"PublishAllPorts":false,"ReadonlyRootfs":false,"SecurityOpt":null,"UTSMode":"","UsernsMode":"","ShmSize":0,"ConsoleSize":[0,0],"Isolation":"","CpuShares":0,"Memory":0,"NanoCpus":0,"CgroupParent":"","BlkioWeight":0,"BlkioWeightDevice":[],"BlkioDeviceReadBps":null,"BlkioDeviceWriteBps":null,"BlkioDeviceReadIOps":null,"BlkioDeviceWriteIOps":null,"CpuPeriod":0,"CpuQuota":0,"CpuRealtimePeriod":0,"CpuRealtimeRuntime":0,"CpusetCpus":"","CpusetMems":"","Devices":[],"DeviceCgroupRules":null,"DiskQuota":0,"KernelMemory":0,"MemoryReservation":0,"MemorySwap":0,"MemorySwappiness":-1,"OomKillDisable":false,"PidsLimit":0,"Ulimits":null,"CpuCount":0,"CpuPercent":0,"IOMaximumIOps":0,"IOMaximumBandwidth":0,"MaskedPaths":null,"ReadonlyPaths":null},"NetworkingConfig":{"EndpointsConfig":{}}} 2 | -------------------------------------------------------------------------------- /fixtures/containers_create_7_expected.json: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /fixtures/containers_create_7_in.json: -------------------------------------------------------------------------------- 1 | {"Hostname":"","Domainname":"","User":"","AttachStdin":true,"AttachStdout":true,"AttachStderr":true,"Tty":true,"OpenStdin":true,"StdinOnce":true,"Env":[],"Cmd":["sh"],"Image":"alpine:3.8","Volumes":{},"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":{},"HostConfig":{"Binds":null,"ContainerIDFile":"","LogConfig":{"Type":"","Config":{}},"NetworkMode":"host","PortBindings":{},"RestartPolicy":{"Name":"no","MaximumRetryCount":0},"AutoRemove":true,"VolumeDriver":"","VolumesFrom":null,"CapAdd":null,"CapDrop":null,"Dns":[],"DnsOptions":[],"DnsSearch":[],"ExtraHosts":null,"GroupAdd":null,"IpcMode":"","Cgroup":"","Links":null,"OomScoreAdj":0,"PidMode":"","Privileged":false,"PublishAllPorts":false,"ReadonlyRootfs":false,"SecurityOpt":null,"UTSMode":"","UsernsMode":"","ShmSize":0,"ConsoleSize":[0,0],"Isolation":"","CpuShares":0,"Memory":0,"NanoCpus":0,"CgroupParent":"","BlkioWeight":0,"BlkioWeightDevice":[],"BlkioDeviceReadBps":null,"BlkioDeviceWriteBps":null,"BlkioDeviceReadIOps":null,"BlkioDeviceWriteIOps":null,"CpuPeriod":0,"CpuQuota":0,"CpuRealtimePeriod":0,"CpuRealtimeRuntime":0,"CpusetCpus":"","CpusetMems":"","Devices":[],"DeviceCgroupRules":null,"DiskQuota":0,"KernelMemory":0,"MemoryReservation":0,"MemorySwap":0,"MemorySwappiness":-1,"OomKillDisable":false,"PidsLimit":0,"Ulimits":null,"CpuCount":0,"CpuPercent":0,"IOMaximumIOps":0,"IOMaximumBandwidth":0,"MaskedPaths":null,"ReadonlyPaths":null},"NetworkingConfig":{"EndpointsConfig":{}}} 2 | -------------------------------------------------------------------------------- /fixtures/containers_create_8_expected.json: -------------------------------------------------------------------------------- 1 | {"AttachStderr":true,"AttachStdin":true,"AttachStdout":true,"Cmd":["sh"],"Domainname":"","Entrypoint":null,"Env":[],"HostConfig":{"AutoRemove":true,"Binds":null,"BlkioDeviceReadBps":null,"BlkioDeviceReadIOps":null,"BlkioDeviceWriteBps":null,"BlkioDeviceWriteIOps":null,"BlkioWeight":0,"BlkioWeightDevice":[],"CapAdd":null,"CapDrop":null,"Cgroup":"","CgroupParent":"some-cgroup","ConsoleSize":[0,0],"ContainerIDFile":"","CpuCount":0,"CpuPercent":0,"CpuPeriod":0,"CpuQuota":0,"CpuRealtimePeriod":0,"CpuRealtimeRuntime":0,"CpuShares":0,"CpusetCpus":"","CpusetMems":"","DeviceCgroupRules":null,"Devices":[],"DiskQuota":0,"Dns":[],"DnsOptions":[],"DnsSearch":[],"ExtraHosts":null,"GroupAdd":null,"IOMaximumBandwidth":0,"IOMaximumIOps":0,"IpcMode":"","Isolation":"","KernelMemory":0,"Links":null,"LogConfig":{"Config":{},"Type":""},"MaskedPaths":null,"Memory":0,"MemoryReservation":0,"MemorySwap":0,"MemorySwappiness":-1,"NanoCpus":0,"NetworkMode":"default","OomKillDisable":false,"OomScoreAdj":0,"PidMode":"","PidsLimit":0,"PortBindings":{},"Privileged":false,"PublishAllPorts":false,"ReadonlyPaths":null,"ReadonlyRootfs":false,"RestartPolicy":{"MaximumRetryCount":0,"Name":"no"},"SecurityOpt":null,"ShmSize":0,"UTSMode":"","Ulimits":null,"UsernsMode":"","VolumeDriver":"","VolumesFrom":null},"Hostname":"","Image":"alpine:3.8","Labels":{"com.buildkite.sockguard.owner":"sockguard-pid-1"},"NetworkingConfig":{"EndpointsConfig":{}},"OnBuild":null,"OpenStdin":true,"StdinOnce":true,"Tty":true,"User":"","Volumes":{},"WorkingDir":""} 2 | -------------------------------------------------------------------------------- /fixtures/containers_create_8_in.json: -------------------------------------------------------------------------------- 1 | {"Hostname":"","Domainname":"","User":"","AttachStdin":true,"AttachStdout":true,"AttachStderr":true,"Tty":true,"OpenStdin":true,"StdinOnce":true,"Env":[],"Cmd":["sh"],"Image":"alpine:3.8","Volumes":{},"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":{},"HostConfig":{"Binds":null,"ContainerIDFile":"","LogConfig":{"Type":"","Config":{}},"NetworkMode":"default","PortBindings":{},"RestartPolicy":{"Name":"no","MaximumRetryCount":0},"AutoRemove":true,"VolumeDriver":"","VolumesFrom":null,"CapAdd":null,"CapDrop":null,"Dns":[],"DnsOptions":[],"DnsSearch":[],"ExtraHosts":null,"GroupAdd":null,"IpcMode":"","Cgroup":"","Links":null,"OomScoreAdj":0,"PidMode":"","Privileged":false,"PublishAllPorts":false,"ReadonlyRootfs":false,"SecurityOpt":null,"UTSMode":"","UsernsMode":"","ShmSize":0,"ConsoleSize":[0,0],"Isolation":"","CpuShares":0,"Memory":0,"NanoCpus":0,"CgroupParent":"","BlkioWeight":0,"BlkioWeightDevice":[],"BlkioDeviceReadBps":null,"BlkioDeviceWriteBps":null,"BlkioDeviceReadIOps":null,"BlkioDeviceWriteIOps":null,"CpuPeriod":0,"CpuQuota":0,"CpuRealtimePeriod":0,"CpuRealtimeRuntime":0,"CpusetCpus":"","CpusetMems":"","Devices":[],"DeviceCgroupRules":null,"DiskQuota":0,"KernelMemory":0,"MemoryReservation":0,"MemorySwap":0,"MemorySwappiness":-1,"OomKillDisable":false,"PidsLimit":0,"Ulimits":null,"CpuCount":0,"CpuPercent":0,"IOMaximumIOps":0,"IOMaximumBandwidth":0,"MaskedPaths":null,"ReadonlyPaths":null},"NetworkingConfig":{"EndpointsConfig":{}}} 2 | -------------------------------------------------------------------------------- /fixtures/containers_create_9_expected.json: -------------------------------------------------------------------------------- 1 | {"AttachStderr":true,"AttachStdin":true,"AttachStdout":true,"Cmd":["sh"],"Domainname":"","Entrypoint":null,"Env":[],"HostConfig":{"AutoRemove":true,"Binds":null,"BlkioDeviceReadBps":null,"BlkioDeviceReadIOps":null,"BlkioDeviceWriteBps":null,"BlkioDeviceWriteIOps":null,"BlkioWeight":0,"BlkioWeightDevice":[],"CapAdd":null,"CapDrop":null,"Cgroup":"","CgroupParent":"","ConsoleSize":[0,0],"ContainerIDFile":"","CpuCount":0,"CpuPercent":0,"CpuPeriod":0,"CpuQuota":0,"CpuRealtimePeriod":0,"CpuRealtimeRuntime":0,"CpuShares":0,"CpusetCpus":"","CpusetMems":"","DeviceCgroupRules":null,"Devices":[],"DiskQuota":0,"Dns":[],"DnsOptions":[],"DnsSearch":[],"ExtraHosts":null,"GroupAdd":null,"IOMaximumBandwidth":0,"IOMaximumIOps":0,"IpcMode":"","Isolation":"","KernelMemory":0,"Links":null,"LogConfig":{"Config":{},"Type":""},"MaskedPaths":null,"Memory":0,"MemoryReservation":0,"MemorySwap":0,"MemorySwappiness":-1,"NanoCpus":0,"NetworkMode":"default","OomKillDisable":false,"OomScoreAdj":0,"PidMode":"","PidsLimit":0,"PortBindings":{},"Privileged":false,"PublishAllPorts":false,"ReadonlyPaths":null,"ReadonlyRootfs":false,"RestartPolicy":{"MaximumRetryCount":0,"Name":"no"},"SecurityOpt":null,"ShmSize":0,"UTSMode":"","Ulimits":null,"UsernsMode":"","VolumeDriver":"","VolumesFrom":null},"Hostname":"","Image":"alpine:3.8","Labels":{"com.buildkite.sockguard.owner":"sockguard-pid-1"},"NetworkingConfig":{"EndpointsConfig":{}},"OnBuild":null,"OpenStdin":true,"StdinOnce":true,"Tty":true,"User":"someuser","Volumes":{},"WorkingDir":""} 2 | -------------------------------------------------------------------------------- /fixtures/containers_create_9_in.json: -------------------------------------------------------------------------------- 1 | {"Hostname":"","Domainname":"","User":"","AttachStdin":true,"AttachStdout":true,"AttachStderr":true,"Tty":true,"OpenStdin":true,"StdinOnce":true,"Env":[],"Cmd":["sh"],"Image":"alpine:3.8","Volumes":{},"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":{},"HostConfig":{"Binds":null,"ContainerIDFile":"","LogConfig":{"Type":"","Config":{}},"NetworkMode":"default","PortBindings":{},"RestartPolicy":{"Name":"no","MaximumRetryCount":0},"AutoRemove":true,"VolumeDriver":"","VolumesFrom":null,"CapAdd":null,"CapDrop":null,"Dns":[],"DnsOptions":[],"DnsSearch":[],"ExtraHosts":null,"GroupAdd":null,"IpcMode":"","Cgroup":"","Links":null,"OomScoreAdj":0,"PidMode":"","Privileged":false,"PublishAllPorts":false,"ReadonlyRootfs":false,"SecurityOpt":null,"UTSMode":"","UsernsMode":"","ShmSize":0,"ConsoleSize":[0,0],"Isolation":"","CpuShares":0,"Memory":0,"NanoCpus":0,"CgroupParent":"","BlkioWeight":0,"BlkioWeightDevice":[],"BlkioDeviceReadBps":null,"BlkioDeviceWriteBps":null,"BlkioDeviceReadIOps":null,"BlkioDeviceWriteIOps":null,"CpuPeriod":0,"CpuQuota":0,"CpuRealtimePeriod":0,"CpuRealtimeRuntime":0,"CpusetCpus":"","CpusetMems":"","Devices":[],"DeviceCgroupRules":null,"DiskQuota":0,"KernelMemory":0,"MemoryReservation":0,"MemorySwap":0,"MemorySwappiness":-1,"OomKillDisable":false,"PidsLimit":0,"Ulimits":null,"CpuCount":0,"CpuPercent":0,"IOMaximumIOps":0,"IOMaximumBandwidth":0,"MaskedPaths":null,"ReadonlyPaths":null},"NetworkingConfig":{"EndpointsConfig":{}}} 2 | -------------------------------------------------------------------------------- /fixtures/networks_create_1_expected.json: -------------------------------------------------------------------------------- 1 | {"Attachable":false,"CheckDuplicate":true,"ConfigFrom":null,"ConfigOnly":false,"Driver":"bridge","EnableIPv6":false,"IPAM":{"Config":[],"Driver":"default","Options":{}},"Ingress":false,"Internal":false,"Labels":{"com.buildkite.sockguard.owner":"sockguard-pid-1"},"Name":"somenetwork","Options":{},"Scope":""} 2 | -------------------------------------------------------------------------------- /fixtures/networks_create_1_in.json: -------------------------------------------------------------------------------- 1 | {"CheckDuplicate":true,"Driver":"bridge","Scope":"","EnableIPv6":false,"IPAM":{"Driver":"default","Options":{},"Config":[]},"Internal":false,"Attachable":false,"Ingress":false,"ConfigOnly":false,"ConfigFrom":null,"Options":{},"Labels":{},"Name":"somenetwork"} 2 | -------------------------------------------------------------------------------- /fixtures/networks_create_2_expected.json: -------------------------------------------------------------------------------- 1 | {"Attachable":false,"CheckDuplicate":true,"ConfigFrom":null,"ConfigOnly":false,"Driver":"bridge","EnableIPv6":false,"IPAM":{"Config":[],"Driver":"default","Options":{}},"Ingress":false,"Internal":false,"Labels":{"com.buildkite.sockguard.owner":"sockguard-pid-1"},"Name":"anothernetwork","Options":{},"Scope":""} 2 | -------------------------------------------------------------------------------- /fixtures/networks_create_2_in.json: -------------------------------------------------------------------------------- 1 | {"CheckDuplicate":true,"Driver":"bridge","Scope":"","EnableIPv6":false,"IPAM":{"Driver":"default","Options":{},"Config":[]},"Internal":false,"Attachable":false,"Ingress":false,"ConfigOnly":false,"ConfigFrom":null,"Options":{},"Labels":{},"Name":"anothernetwork"} 2 | -------------------------------------------------------------------------------- /fixtures/networks_create_3_expected.json: -------------------------------------------------------------------------------- 1 | {"Attachable":false,"CheckDuplicate":true,"ConfigFrom":null,"ConfigOnly":false,"Driver":"bridge","EnableIPv6":false,"IPAM":{"Config":[],"Driver":"default","Options":{}},"Ingress":false,"Internal":false,"Labels":{"com.buildkite.sockguard.owner":"sockguard-pid-1"},"Name":"alwaysjoinnetwork","Options":{},"Scope":""} 2 | -------------------------------------------------------------------------------- /fixtures/networks_create_3_in.json: -------------------------------------------------------------------------------- 1 | {"CheckDuplicate":true,"Driver":"bridge","Scope":"","EnableIPv6":false,"IPAM":{"Driver":"default","Options":{},"Config":[]},"Internal":false,"Attachable":false,"Ingress":false,"ConfigOnly":false,"ConfigFrom":null,"Options":{},"Labels":{},"Name":"alwaysjoinnetwork"} 2 | -------------------------------------------------------------------------------- /fixtures/networks_create_4_expected.json: -------------------------------------------------------------------------------- 1 | {"Attachable":false,"CheckDuplicate":true,"ConfigFrom":null,"ConfigOnly":false,"Driver":"bridge","EnableIPv6":false,"IPAM":{"Config":[],"Driver":"default","Options":{}},"Ingress":false,"Internal":false,"Labels":{"com.buildkite.sockguard.owner":"sockguard-pid-1"},"Name":"alwaysjoinnetworkwithalias","Options":{},"Scope":""} 2 | -------------------------------------------------------------------------------- /fixtures/networks_create_4_in.json: -------------------------------------------------------------------------------- 1 | {"CheckDuplicate":true,"Driver":"bridge","Scope":"","EnableIPv6":false,"IPAM":{"Driver":"default","Options":{},"Config":[]},"Internal":false,"Attachable":false,"Ingress":false,"ConfigOnly":false,"ConfigFrom":null,"Options":{},"Labels":{},"Name":"alwaysjoinnetworkwithalias"} 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/buildkite/sockguard 2 | 3 | require ( 4 | github.com/google/go-cmp v0.2.0 5 | github.com/kvz/logstreamer v0.0.0-20150507115422-a635b98146f0 6 | ) 7 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= 2 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 3 | github.com/kvz/logstreamer v0.0.0-20150507115422-a635b98146f0 h1:3tLzEnUizyN9YLWFTT9loC30lSBvh2y70LTDcZOTs1s= 4 | github.com/kvz/logstreamer v0.0.0-20150507115422-a635b98146f0/go.mod h1:8/LTPeDLaklcUjgSQBHbhBF1ibKAFxzS5o+H7USfMSA= 5 | -------------------------------------------------------------------------------- /socketproxy/proxy.go: -------------------------------------------------------------------------------- 1 | package socketproxy 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | "log" 8 | "net" 9 | "net/http" 10 | "os" 11 | "sync" 12 | "sync/atomic" 13 | 14 | "github.com/kvz/logstreamer" 15 | ) 16 | 17 | var ( 18 | Debug bool 19 | ) 20 | 21 | type SocketProxy struct { 22 | path string 23 | sock net.Conn 24 | counter uint64 25 | director Director 26 | } 27 | 28 | // Logger is a subset of log.Logger used in a Proxy request 29 | type Logger interface { 30 | Printf(format string, v ...interface{}) 31 | } 32 | 33 | // Director returns an http.Handler that either passes through to 34 | // an upstream handler or imposes some logic of it's own on the request. 35 | type Director interface { 36 | Direct(l Logger, req *http.Request, upstream http.Handler) http.Handler 37 | } 38 | 39 | type DirectorFunc func(l Logger, req *http.Request, upstream http.Handler) http.Handler 40 | 41 | func (d DirectorFunc) Direct(l Logger, req *http.Request, upstream http.Handler) http.Handler { 42 | return d(l, req, upstream) 43 | } 44 | 45 | // New returns a SocketProxy that proxies requests to the provided upstream unix socket 46 | func New(upstream string, director Director) *SocketProxy { 47 | return &SocketProxy{ 48 | path: upstream, 49 | director: director, 50 | } 51 | } 52 | 53 | func (s *SocketProxy) ServeHTTP(w http.ResponseWriter, req *http.Request) { 54 | requestID := atomic.AddUint64(&s.counter, 1) 55 | path := req.URL.Path 56 | 57 | if req.URL.RawQuery != "" { 58 | path += "?" + req.URL.RawQuery 59 | } 60 | 61 | l := log.New(os.Stderr, fmt.Sprintf("#%d ", requestID), log.Ltime|log.Lmicroseconds) 62 | l.Printf("%s - %s - %db", req.Method, path, req.ContentLength) 63 | 64 | var passUpstream = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 65 | s.ServeViaUpstreamSocket(l, w, req) 66 | }) 67 | 68 | s.director.Direct(l, req, passUpstream).ServeHTTP(w, req) 69 | } 70 | 71 | func (s *SocketProxy) ServeViaUpstreamSocket(l *log.Logger, w http.ResponseWriter, req *http.Request) { 72 | var sockDebug = ioutil.Discard 73 | var connDebug = ioutil.Discard 74 | 75 | if Debug == true { 76 | sockStreamer := logstreamer.NewLogstreamer(l, "> ", false) 77 | sockDebug = sockStreamer 78 | defer sockStreamer.Close() 79 | 80 | connStreamer := logstreamer.NewLogstreamer(l, "< ", false) 81 | connDebug = connStreamer 82 | defer connStreamer.Close() 83 | } 84 | 85 | // Dial a new socket connection for this request. Re-use might be possible, but this gets 86 | // things working reliably to start with 87 | sock, err := net.Dial("unix", s.path) 88 | if err != nil { 89 | http.Error(w, "Error contacting backend server.", 500) 90 | return 91 | } 92 | 93 | defer sock.Close() 94 | 95 | hj, ok := w.(http.Hijacker) 96 | if !ok { 97 | http.Error(w, "Not a Hijacker?", 500) 98 | return 99 | } 100 | 101 | reqConn, bufrw, err := hj.Hijack() 102 | if err != nil { 103 | l.Printf("Hijack error: %v", err) 104 | return 105 | } 106 | 107 | defer reqConn.Close() 108 | 109 | // This is really important, otherwise subsequent requests will be streamed in without 110 | // being passed via the director 111 | req.Header.Set("Connection", "close") 112 | 113 | // write the request to the remote side 114 | err = req.Write(io.MultiWriter(sock, sockDebug)) 115 | if err != nil { 116 | l.Printf("Error copying request to target: %v", err) 117 | return 118 | } 119 | 120 | // handle anything already buffered from before the hijack 121 | if bufrw.Reader.Buffered() > 0 { 122 | l.Printf("Found %d bytes buffered in reader", bufrw.Reader.Buffered()) 123 | rbuf, err := bufrw.Reader.Peek(bufrw.Reader.Buffered()) 124 | if err != nil { 125 | panic(err) 126 | } 127 | 128 | // TODO: deal with this 129 | l.Printf("Buffered: %s", rbuf) 130 | panic("Buffered bytes not handled") 131 | } 132 | 133 | var wg sync.WaitGroup 134 | wg.Add(2) 135 | 136 | // Copy from request to socket 137 | go func() { 138 | defer wg.Done() 139 | n, err := io.Copy(io.MultiWriter(sock, sockDebug), reqConn) 140 | if err != nil { 141 | l.Printf("Error copying request to socket: %v", err) 142 | } 143 | l.Printf("Copied %d bytes from downstream connection", n) 144 | }() 145 | 146 | // copy from socket to request 147 | go func() { 148 | defer wg.Done() 149 | n, err := io.Copy(io.MultiWriter(reqConn, connDebug), sock) 150 | if err != nil { 151 | l.Printf("Error copying socket to request: %v", err) 152 | } 153 | l.Printf("Copied %d bytes from upstream socket", n) 154 | 155 | if err := bufrw.Flush(); err != nil { 156 | l.Printf("Error flushing buffer: %v", err) 157 | } 158 | if err := reqConn.Close(); err != nil { 159 | l.Printf("Error closing connection: %v", err) 160 | } 161 | }() 162 | 163 | wg.Wait() 164 | l.Printf("Done, closing") 165 | } 166 | -------------------------------------------------------------------------------- /socketproxy/proxy_test.go: -------------------------------------------------------------------------------- 1 | package socketproxy_test 2 | 3 | import ( 4 | "context" 5 | "io/ioutil" 6 | "net" 7 | "net/http" 8 | "os" 9 | "testing" 10 | 11 | "github.com/buildkite/sockguard/socketproxy" 12 | ) 13 | 14 | func TestGetRequestOverSocketProxy(t *testing.T) { 15 | upstreamSock, close1 := startSocketServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 16 | w.Write([]byte("llamas")) 17 | })) 18 | defer close1() 19 | 20 | proxy := socketproxy.New(upstreamSock, socketproxy.DirectorFunc(func(l socketproxy.Logger, req *http.Request, upstream http.Handler) http.Handler { 21 | return upstream 22 | })) 23 | 24 | proxySock, close2 := startSocketServer(t, proxy) 25 | defer close2() 26 | 27 | client := createSocketClient(t, proxySock) 28 | 29 | res, err := client.Get("http://llamas/test") 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | 34 | greeting, err := ioutil.ReadAll(res.Body) 35 | defer res.Body.Close() 36 | 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | 41 | if string(greeting) != "llamas" { 42 | t.Fatalf("Unexpected response %q, expected %q", greeting, "llamas") 43 | } 44 | } 45 | 46 | func startSocketServer(t *testing.T, h http.Handler) (sock string, close func()) { 47 | server := http.Server{ 48 | Handler: h, 49 | } 50 | 51 | sockFile, err := ioutil.TempFile("", "testsock") 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | 56 | if err := os.Remove(sockFile.Name()); err != nil { 57 | t.Fatal(err) 58 | } 59 | 60 | unixListener, err := net.Listen("unix", sockFile.Name()) 61 | if err != nil { 62 | t.Fatal(err) 63 | } 64 | 65 | go func() { 66 | _ = server.Serve(unixListener) 67 | }() 68 | 69 | return sockFile.Name(), func() { 70 | _ = unixListener.Close() 71 | _ = os.Remove(sockFile.Name()) 72 | } 73 | } 74 | 75 | func createSocketClient(t *testing.T, sock string) *http.Client { 76 | return &http.Client{ 77 | Transport: &http.Transport{ 78 | DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { 79 | return net.Dial("unix", sock) 80 | }, 81 | }, 82 | } 83 | } 84 | --------------------------------------------------------------------------------