Skip to content

Commit 7cf5a34

Browse files
Preliminary support for @SUMMONDOCKERARGS
1 parent 5a73e4a commit 7cf5a34

File tree

10 files changed

+288
-11
lines changed

10 files changed

+288
-11
lines changed

Dockerfile

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ COPY go.mod go.sum ./
99

1010
RUN apk add --no-cache bash \
1111
build-base \
12+
docker-cli \
1213
git && \
1314
go mod download && \
1415
go get -u github.com/jstemmer/go-junit-report && \

Dockerfile.acceptance

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ RUN apk add --no-cache bash \
55
git \
66
libffi-dev \
77
ruby-bundler \
8-
ruby-dev
8+
ruby-dev \
9+
docker-cli
910

1011
# Install summon prerequisites
1112
WORKDIR /summon

docs/_includes/docker.md

+61-3
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,66 @@ Since Summon has pluggable providers, you aren't locked into any one solution fo
1616
managing your secrets.
1717

1818
Summon makes it easy to inject secrets as environment variables into your Docker
19-
containers by taking advantage of Docker's `--env-file` argument. This is done
20-
on-demand by using the variable `@SUMMONENVFILE` in the arguments of the process
19+
containers by taking advantage of Docker's CLI arguments (`--env-file` or, `--env` and `--volume`. There are two options available. It's possible to mix and match as you see fit.
20+
21+
## --env and --volume arguments
22+
This is done on-demand by using the variable `@SUMMONDOCKERARGS` in the arguments of the process
23+
you are running with Summon. This variable is replaced by combinations of the Docker arguments `--env` and `--volume` such that the secrets injected by summon are passed into the Docker container. The `--volume` arguments allow memory-mapped temporary files from variables with the `!file` tag to be resolvable inside the container.
24+
25+
**NOTE:** Using the `!file` tag with `@SUMMONDOCKERARGS` assumes that the Docker CLI is being run on the host that is used to create volume mounts to the container. For when this is not the case simply avoid using the `!file` tag, but be mindful that in that case you lose the benefits of memory-mapped temporary files.
26+
27+
```bash
28+
$ summon -p keyring.py -D env=dev docker run @SUMMONDOCKERARGS deployer
29+
Checking credentials
30+
Deploying application
31+
```
32+
33+
### Example
34+
The example below demonstrates the use @SUMMONDOCKERARGS. For the sake of brevity
35+
we use an inline `secrets.yml` and the `/bin/echo` provider. Some points to note:
36+
1. `summon` is
37+
invoking docker as the child process.
38+
2. `@SUMMONDOCKERARGS` is replaced with a combination of `--env` and `--volume`
39+
arguments.
40+
3. Variable `D` uses the `!file` tag and therefore is the only one that
41+
results in a `--volume` argument. The path to this variable inside the container
42+
is as it is on the host.
43+
44+
```bash
45+
secretsyml='
46+
A: |-
47+
A_value with
48+
multiple lines
49+
B: B_value
50+
C: !var C_value
51+
D: !var:file D_value
52+
'
53+
54+
# The substitution of @SUMMONDOCKERARGS the docker run command below results in
55+
# something of the form:
56+
#
57+
# docker run --rm \
58+
# --env A --env B --env C --env D \
59+
# --volume /path/to/D:/path/to/D
60+
# alpine ...
61+
#
62+
# The output from the command is shown below the command.
63+
64+
summon --provider /bin/echo --yaml "${secretsyml}" \
65+
docker run --rm @SUMMONDOCKERARGS alpine sh -c '
66+
printenv A;
67+
printenv B;
68+
printenv C;
69+
cat $(printenv D);
70+
'
71+
# A_value with
72+
# multiple lines
73+
# B_value
74+
# C_value
75+
# D_value
76+
```
77+
## --env-file argument
78+
This is done on-demand by using the variable `@SUMMONENVFILE` in the arguments of the process
2179
you are running with Summon. This variable points to a memory-mapped file containing
2280
the variables and values from secrets.yml in VAR=VAL format.
2381

@@ -27,7 +85,7 @@ Checking credentials
2785
Deploying application
2886
```
2987

30-
## Example
88+
### Example
3189

3290
Let's say we have a deploy script that needs to access our application servers on
3391
AWS and pull the latest version of our code. It should record the outcome of the

go.mod

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ module github.com/cyberark/summon
22

33
require (
44
github.com/codegangsta/cli v1.20.0
5+
github.com/docker/docker v20.10.2+incompatible
6+
github.com/docker/go-connections v0.4.0 // indirect
7+
github.com/docker/go-units v0.4.0 // indirect
58
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect
69
github.com/jtolds/gls v4.20.0+incompatible // indirect
710
github.com/kr/pretty v0.1.0 // indirect

go.sum

+7
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@ github.com/codegangsta/cli v1.20.0 h1:iX1FXEgwzd5+XN6wk5cVHOGQj6Q3Dcp20lUeS4lHNT
22
github.com/codegangsta/cli v1.20.0/go.mod h1:/qJNoX69yVSKu5o4jLyXAENLRyk1uhi7zkbQ3slBdOA=
33
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
44
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5+
github.com/docker/docker v1.13.1 h1:IkZjBSIc8hBjLpqeAbeE5mca5mNgeatLHBy3GO78BWo=
6+
github.com/docker/docker v20.10.2+incompatible h1:vFgEHPqWBTp4pTjdLwjAA4bSo3gvIGOYwuJTlEjVBCw=
7+
github.com/docker/docker v20.10.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
8+
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
9+
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
10+
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
11+
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
512
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=
613
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
714
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=

internal/command/action.go

+60-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package command
33
import (
44
"bytes"
55
"fmt"
6+
"io"
67
"os"
78
"os/exec"
89
"path/filepath"
@@ -11,13 +12,17 @@ import (
1112
"syscall"
1213

1314
"github.com/codegangsta/cli"
15+
1416
prov "github.com/cyberark/summon/provider"
1517
"github.com/cyberark/summon/secretsyml"
1618
)
1719

1820
// ActionConfig is an object that holds all the info needed to run
1921
// a Summon instance
2022
type ActionConfig struct {
23+
StdIn io.Reader
24+
StdOut io.Writer
25+
StdErr io.Writer
2126
Args []string
2227
Provider string
2328
Filepath string
@@ -31,6 +36,7 @@ type ActionConfig struct {
3136
}
3237

3338
const ENV_FILE_MAGIC = "@SUMMONENVFILE"
39+
const DOCKER_ARGS_MAGIC = "@SUMMONDOCKERARGS"
3440
const SUMMON_ENV_KEY_NAME = "SUMMON_ENV"
3541

3642
// Action is the runner for the main program logic
@@ -122,6 +128,9 @@ func runAction(ac *ActionConfig) error {
122128
results := make(chan Result, len(secrets))
123129
var wg sync.WaitGroup
124130

131+
var dockerArgs []string
132+
var dockerArgsMutex sync.Mutex
133+
125134
for key, spec := range secrets {
126135
wg.Add(1)
127136
go func(key string, spec secretsyml.SecretSpec) {
@@ -144,6 +153,15 @@ func runAction(ac *ActionConfig) error {
144153
}
145154

146155
k, v := formatForEnv(key, value, spec, &tempFactory)
156+
157+
// Generate @SUMMONDOCKERARGS
158+
dockerArgsMutex.Lock()
159+
defer dockerArgsMutex.Unlock()
160+
if spec.IsFile() {
161+
dockerArgs = append(dockerArgs, "--volume", v+":"+v)
162+
}
163+
dockerArgs = append(dockerArgs, "--env", k)
164+
147165
results <- Result{k, v, nil}
148166
wg.Done()
149167
}(key, spec)
@@ -176,12 +194,52 @@ EnvLoop:
176194

177195
setupEnvFile(ac.Args, env, &tempFactory)
178196

197+
// Setup Docker args
198+
var argsWithDockerArgs []string
199+
for _, arg := range ac.Args {
200+
if arg == DOCKER_ARGS_MAGIC {
201+
// Replace argument with slice of docker options
202+
argsWithDockerArgs = append(argsWithDockerArgs, dockerArgs...)
203+
continue
204+
}
205+
206+
// TODO: we need to decide which of these if we want to support (2)
207+
// 1. summon [...] docker run @SUMMONDOCKERARGS [...], replace only entire top-level arg. The
208+
// top-level arg is replaced by is replaced by N>0 args equating to @SUMMONDOCKERARGS.
209+
// 2. summon ... sh -c "docker run @SUMMONDOCKERARGS [...]", also replace substrings
210+
// inside args but the replacement is as a single string.
211+
//
212+
// The code below should support (2). There'll be some ambiguity though...
213+
// e.g. summon ... echo "@SUMMONDOCKERARGS" will fall under both (1) and (2), though (1)
214+
// takes precedence. I'm not sure if the behaviors of (1) and (2) are equivalent
215+
// when there's such ambiguity.
216+
//
217+
//idx := strings.Index(arg, DOCKER_ARGS_MAGIC)
218+
//if idx >= 0 {
219+
// // Replace argument with slice of docker options
220+
// argsWithDockerArgs = append(
221+
// argsWithDockerArgs,
222+
// strings.Replace(arg, DOCKER_ARGS_MAGIC, strings.Join(dockerArgs, " "), -1),
223+
// )
224+
// continue
225+
//}
226+
227+
argsWithDockerArgs = append(argsWithDockerArgs, arg)
228+
}
229+
ac.Args = argsWithDockerArgs
230+
179231
var e []string
180232
for k, v := range env {
181233
e = append(e, fmt.Sprintf("%s=%s", k, v))
182234
}
183235

184-
return runSubcommand(ac.Args, append(os.Environ(), e...))
236+
return runSubcommand(
237+
ac.Args,
238+
append(os.Environ(), e...),
239+
ac.StdIn,
240+
ac.StdOut,
241+
ac.StdErr,
242+
)
185243
}
186244

187245
// formatForEnv returns a string in %k=%v format, where %k=namespace of the secret and
@@ -240,7 +298,7 @@ func findInParentTree(secretsFile string, leafDir string) (string, error) {
240298
}
241299
}
242300

243-
// scans arguments for the magic string; if found,
301+
// scans arguments for the envfile magic string; if found,
244302
// creates a tempfile to which all the environment mappings are dumped
245303
// and replaces the magic string with its path.
246304
// Returns the path if so, returns an empty string otherwise.

internal/command/action_test.go

+130-1
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,27 @@
11
package command
22

33
import (
4+
"bytes"
5+
"encoding/json"
46
"errors"
57
"fmt"
68
"io/ioutil"
9+
"net/http"
10+
"net/http/httptest"
711
"os"
812
"os/exec"
913
"path/filepath"
14+
"regexp"
1015
"strconv"
16+
"strings"
1117
"testing"
1218
"time"
1319

14-
"github.com/cyberark/summon/secretsyml"
20+
"github.com/docker/docker/api/types/container"
1521
. "github.com/smartystreets/goconvey/convey"
1622
_ "golang.org/x/net/context"
23+
24+
"github.com/cyberark/summon/secretsyml"
1725
)
1826

1927
func TestConvertSubsToMap(t *testing.T) {
@@ -120,6 +128,127 @@ func TestRunAction(t *testing.T) {
120128

121129
So(string(content), ShouldEqual, expectedValue)
122130
})
131+
132+
Convey("Docker options correctly injected", t, func() {
133+
// This is a test case for @SUMMONDOCKERARGS. It exercises Docker CLI pointed to a mock
134+
// server. It asserts on the request payload received on the container creation
135+
// endpoint, the volume mounts and environment variables injected by summon are
136+
// expected to be present.
137+
138+
expected := map[string]string{
139+
"A": "A's multiple line\nvalue",
140+
"B": "B_value",
141+
"C": "C_value",
142+
"D": "D_value",
143+
}
144+
volumeBinds := map[string]struct{
145+
ContainerPath string
146+
FileContents string
147+
}{}
148+
envvars := map[string]string{}
149+
150+
// Mock server for handling API calls by `docker run`
151+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
152+
var payload struct {
153+
*container.Config
154+
HostConfig *container.HostConfig
155+
}
156+
157+
if !regexp.MustCompile("/.*/containers/create").MatchString(r.URL.Path) {
158+
// Mock response to all the other endpoints called as part of `docker run`
159+
w.WriteHeader(200)
160+
fmt.Fprintln(w, "{}")
161+
return
162+
}
163+
payloadBytes, err := ioutil.ReadAll(r.Body)
164+
165+
if err != nil {
166+
t.Errorf("failure reading payload from docker cli: %s", err)
167+
return
168+
}
169+
err = json.Unmarshal(payloadBytes, &payload)
170+
if err != nil {
171+
t.Errorf("payload from docker cli could not be parsed: %s", err)
172+
return
173+
}
174+
175+
for _, env := range payload.Env {
176+
nameAndValue := strings.SplitN(env, "=", 2)
177+
name := nameAndValue[0]
178+
value := nameAndValue[1]
179+
180+
envvars[name] = value
181+
}
182+
183+
for _, volumeBind := range payload.HostConfig.Binds {
184+
fromAndTo := strings.SplitN(volumeBind, ":", 2)
185+
from := fromAndTo[0]
186+
to := fromAndTo[1]
187+
188+
fileContents, _ := ioutil.ReadFile(from)
189+
volumeBinds[from] = struct {
190+
ContainerPath string
191+
FileContents string
192+
}{
193+
ContainerPath: to,
194+
FileContents: string(fileContents),
195+
}
196+
}
197+
198+
w.WriteHeader(201)
199+
200+
// Mock response to container create endpoint
201+
fmt.Fprintln(w, `{"Id": "e90e34656806", "Warnings": []}`)
202+
}))
203+
defer ts.Close()
204+
205+
var buff bytes.Buffer
206+
var dockerCommand = []string{
207+
"docker",
208+
"-H", strings.Replace(ts.URL, "http://", "tcp://", 1),
209+
"run",
210+
"--rm", "-d", "@SUMMONDOCKERARGS",
211+
"alpine",
212+
}
213+
err := runAction(&ActionConfig{
214+
Provider: "/bin/echo",
215+
StdOut: &buff,
216+
Args: dockerCommand,
217+
YamlInline: `
218+
A: |-
219+
A's multiple line
220+
value
221+
B: !var B_value
222+
C: !file C_value
223+
D: !var:file D_value
224+
`,
225+
})
226+
227+
code, err := returnStatusOfError(err)
228+
So(err, ShouldBeNil)
229+
So(code, ShouldEqual, 0)
230+
231+
if err != nil || code != 0 {
232+
return
233+
}
234+
235+
So(err, ShouldBeNil)
236+
if err != nil {
237+
return
238+
}
239+
240+
for from, volumeBind := range volumeBinds {
241+
// The volume mount are expected to take the form
242+
// 'host_path:container_path', where host_path is equal to container_path
243+
So(from, ShouldEqual, volumeBind.ContainerPath)
244+
}
245+
246+
// Ensure envvars and volumemounts passed to Docker match expectations
247+
So(envvars["A"], ShouldEqual, expected["A"])
248+
So(envvars["B"], ShouldEqual, expected["B"])
249+
So(volumeBinds[envvars["C"]].FileContents, ShouldEqual, expected["C"])
250+
So(volumeBinds[envvars["D"]].FileContents, ShouldEqual, expected["D"])
251+
})
123252
}
124253

125254
func TestDefaultVariableResolution(t *testing.T) {

0 commit comments

Comments
 (0)