Skip to content
This repository was archived by the owner on Jan 21, 2020. It is now read-only.

Commit ecf667f

Browse files
author
David Chung
authored
Infrakit CLI improvements (#450)
Signed-off-by: David Chung <[email protected]>
1 parent c2b84b0 commit ecf667f

File tree

10 files changed

+235
-33
lines changed

10 files changed

+235
-33
lines changed
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

pkg/cli/local/context.go

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"strings"
1010

1111
"github.com/docker/infrakit/pkg/template"
12+
"github.com/docker/infrakit/pkg/util/exec"
1213
"github.com/spf13/cobra"
1314
)
1415

@@ -160,17 +161,31 @@ func (c *Context) loadBackend() error {
160161
}
161162
t.AddFunc("print",
162163
func() string {
163-
c.run = func(view string) error {
164-
fmt.Println(view)
164+
c.run = func(script string) error {
165+
fmt.Println(script)
165166
return nil
166167
}
167168
return ""
168169
})
169170
t.AddFunc("sh",
170-
func() string {
171-
c.run = func(view string) error {
172-
fmt.Println(view)
173-
return nil
171+
func(opts ...string) string {
172+
c.run = func(script string) error {
173+
174+
cmd := strings.Join(append([]string{"/bin/sh"}, opts...), " ")
175+
log.Debug("sh", "cmd", cmd)
176+
177+
return exec.Command(cmd).
178+
InheritEnvs(true).StartWithStreams(
179+
180+
exec.Do(exec.SendInput(
181+
func(stdin io.WriteCloser) error {
182+
_, err := stdin.Write([]byte(script))
183+
return err
184+
})).Then(
185+
exec.RedirectStdout(os.Stdout)).Then(
186+
exec.RedirectStderr(os.Stderr),
187+
).Done(),
188+
)
174189
}
175190
return ""
176191
})

pkg/cli/local/context_test.go

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ func TestContext(t *testing.T) {
2323
// the cobra command
2424

2525
script := `
26-
{{/* The directive here tells infrakit to run this script with sh: =% sh %= */}}
26+
{{/* The directive here tells infrakit to run this script with sh: =% print %= */}}
2727
2828
{{/* The function 'flag' will create a flag in the CLI; the function 'prompt' will ask user for input */}}
2929
@@ -89,3 +89,38 @@ func TestContext(t *testing.T) {
8989
"instanceType": "large",
9090
}).String(), types.AnyValueMust(m).String())
9191
}
92+
93+
func TestContextRunShell(t *testing.T) {
94+
95+
script := `#!/bin/bash
96+
{{/* The directive here tells infrakit to run this script with sh: =% sh "-s" "--" %= */}}
97+
{{ $lines := flag "lines" "int" "the number of lines" 5 }}
98+
99+
for i in $(seq {{$lines}}); do
100+
echo line $i
101+
done
102+
`
103+
104+
c := &Context{
105+
cmd: &cobra.Command{
106+
Use: "test",
107+
Short: "test",
108+
},
109+
src: "str://" + script,
110+
}
111+
112+
c.exec = false
113+
err := c.buildFlags()
114+
require.NoError(t, err)
115+
116+
err = c.cmd.Flags().Parse(strings.Split("--lines 3", " "))
117+
require.NoError(t, err)
118+
119+
err = c.loadBackend()
120+
require.NoError(t, err)
121+
require.NotNil(t, c.run)
122+
123+
err = c.execute()
124+
require.NoError(t, err)
125+
126+
}

pkg/util/exec/exec.go

Lines changed: 150 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
package exec
22

33
import (
4+
"io"
5+
"os"
46
"os/exec"
57
"strings"
68

9+
logutil "github.com/docker/infrakit/pkg/log"
710
"github.com/docker/infrakit/pkg/template"
8-
log "github.com/golang/glog"
911
)
1012

13+
var log = logutil.New("module", "util/exec")
14+
1115
// Command is the template which is rendered before it's executed
1216
type Command string
1317

@@ -26,35 +30,50 @@ func (c Command) Run(args ...string) error {
2630
return c.builder().Run(args...)
2731
}
2832

33+
// String returns the interpolated version of the command
34+
func (c Command) String(args ...string) (string, error) {
35+
p, err := c.builder().generate(args...)
36+
if err == nil {
37+
return strings.Join(p, " "), nil
38+
}
39+
return string(c), err
40+
}
41+
2942
// WithOptions adds the template options
3043
func (c Command) WithOptions(options template.Options) *Builder {
31-
b := c.builder()
32-
b.options = options
33-
return b
44+
return c.builder().WithOptions(options)
3445
}
3546

3647
// WithFunc adds a function that can be used in the template
3748
func (c Command) WithFunc(name string, function interface{}) *Builder {
38-
b := c.builder()
39-
b.funcs[name] = function
40-
return b
49+
return c.builder().WithFunc(name, function)
4150
}
4251

4352
// WithContext sets the context for the template
4453
func (c Command) WithContext(context interface{}) *Builder {
45-
b := c.builder()
46-
b.context = context
47-
return b
54+
return c.builder().WithContext(context)
55+
}
56+
57+
// InheritEnvs determines whether the process should inherit the envs of the parent
58+
func (c Command) InheritEnvs(v bool) *Builder {
59+
return c.builder().InheritEnvs(v)
60+
}
61+
62+
// NewCommand creates an instance of the command builder to allow detailed configuration
63+
func NewCommand(s string) *Builder {
64+
return Command(s).builder()
4865
}
4966

5067
// Builder collects options until it's run
5168
type Builder struct {
52-
command Command
53-
options template.Options
54-
funcs map[string]interface{}
55-
context interface{}
56-
rendered string // rendered command string
57-
cmd *exec.Cmd
69+
command Command
70+
options template.Options
71+
inheritEnvs bool
72+
envs []string
73+
funcs map[string]interface{}
74+
context interface{}
75+
rendered string // rendered command string
76+
cmd *exec.Cmd
5877
}
5978

6079
func (c Command) builder() *Builder {
@@ -64,6 +83,18 @@ func (c Command) builder() *Builder {
6483
}
6584
}
6685

86+
// InheritEnvs determines whether the process should inherit the envs of the parent
87+
func (b *Builder) InheritEnvs(v bool) *Builder {
88+
b.inheritEnvs = v
89+
return b
90+
}
91+
92+
// WithEnvs adds environment variables for the exec, in format of key=value
93+
func (b *Builder) WithEnvs(kv ...string) *Builder {
94+
b.envs = append(b.envs, kv...)
95+
return b
96+
}
97+
6798
// WithOptions adds the template options
6899
func (b *Builder) WithOptions(options template.Options) *Builder {
69100
b.options = options
@@ -100,6 +131,104 @@ func (b *Builder) Start(args ...string) error {
100131
return run.Start()
101132
}
102133

134+
// Step is something you do with the processes streams
135+
type Step func(stdin io.WriteCloser, stdout io.ReadCloser, stderr io.ReadCloser) error
136+
137+
// Thenable is a fluent builder for chaining tasks
138+
type Thenable struct {
139+
steps []Step
140+
}
141+
142+
// Do creates a thenable
143+
func Do(f Step) *Thenable {
144+
return &Thenable{
145+
steps: []Step{f},
146+
}
147+
}
148+
149+
// Then adds another step
150+
func (t *Thenable) Then(then Step) *Thenable {
151+
t.steps = append(t.steps, then)
152+
return t
153+
}
154+
155+
// Done returns the final function
156+
func (t *Thenable) Done() Step {
157+
steps := t.steps
158+
return func(stdin io.WriteCloser, stdout, stderr io.ReadCloser) error {
159+
for _, step := range steps {
160+
if err := step(stdin, stdout, stderr); err != nil {
161+
return err
162+
}
163+
}
164+
return nil
165+
}
166+
}
167+
168+
// SendInput is a convenience function for writing to the exec process's stdin. When the function completes, the
169+
// stdin is closed.
170+
func SendInput(f func(io.WriteCloser) error) Step {
171+
return func(stdin io.WriteCloser, stdout, stderr io.ReadCloser) error {
172+
defer stdin.Close()
173+
return f(stdin)
174+
}
175+
}
176+
177+
// RedirectStdout sends stdout to given writer
178+
func RedirectStdout(out io.Writer) Step {
179+
return func(stdin io.WriteCloser, stdout, stderr io.ReadCloser) error {
180+
_, err := io.Copy(out, stdout)
181+
return err
182+
}
183+
}
184+
185+
// RedirectStderr sends stdout to given writer
186+
func RedirectStderr(out io.Writer) Step {
187+
return func(stdin io.WriteCloser, stdout, stderr io.ReadCloser) error {
188+
_, err := io.Copy(out, stderr)
189+
return err
190+
}
191+
}
192+
193+
// MergeOutput combines the stdout and stderr into the given stream
194+
func MergeOutput(out io.Writer) Step {
195+
return func(stdin io.WriteCloser, stdout, stderr io.ReadCloser) error {
196+
_, err := io.Copy(out, io.MultiReader(stdout, stderr))
197+
return err
198+
}
199+
}
200+
201+
// StartWithStreams starts the the process and then calls the function which allows
202+
// the streams to be wired. Calling the provided function blocks.
203+
func (b *Builder) StartWithStreams(f Step,
204+
args ...string) error {
205+
206+
_, err := b.exec(args...)
207+
if err != nil {
208+
return err
209+
}
210+
211+
pOut, err := b.cmd.StdoutPipe()
212+
if err != nil {
213+
return err
214+
}
215+
pErr, err := b.cmd.StderrPipe()
216+
if err != nil {
217+
return err
218+
}
219+
pIn, err := b.cmd.StdinPipe()
220+
if err != nil {
221+
return err
222+
}
223+
224+
err = b.cmd.Start()
225+
if err != nil {
226+
return err
227+
}
228+
229+
return f(pIn, pOut, pErr)
230+
}
231+
103232
// Run does a Cmd.Run on the command
104233
func (b *Builder) Run(args ...string) error {
105234
run, err := b.exec(args...)
@@ -143,7 +272,11 @@ func (b *Builder) exec(args ...string) (*exec.Cmd, error) {
143272
if err != nil {
144273
return nil, err
145274
}
146-
log.V(50).Infoln("exec:", command)
275+
log.Debug("exec", "command", command)
147276
b.cmd = exec.Command(command[0], command[1:]...)
277+
if b.inheritEnvs {
278+
b.cmd.Env = append(os.Environ(), b.envs...)
279+
}
280+
148281
return b.cmd, nil
149282
}

pkg/util/exec/exec_test.go

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package exec
22

33
import (
4+
"io"
5+
"os"
46
"strings"
57
"testing"
68
"time"
@@ -21,10 +23,11 @@ docker run --rm \
2123
busyboxLs Command = `docker run --rm \
2224
busybox ls {{ arg 1 }}
2325
`
24-
busyboxDateStream Command = `docker run --rm --name {{ .container }} \
25-
busybox /bin/sh -c 'while true; do date; sleep {{ .sleep }}; done'`
26+
busyboxSh Command = `docker run --rm -ti --name {{ .container }} busybox /bin/sh`
2627

2728
dockerStop Command = `docker stop {{ arg 1 }}`
29+
30+
dateStream Command = `docker run --rm --name {{ arg 1 }} chungers/timer streamer`
2831
)
2932

3033
func TestBuilder(t *testing.T) {
@@ -44,11 +47,10 @@ func TestBuilder(t *testing.T) {
4447
require.NoError(t, err)
4548
require.Equal(t, []string{"docker", "run", "--rm", "busybox", "ls", "sys"}, cmd)
4649

47-
b = busyboxDateStream.builder().WithContext(map[string]interface{}{"container": "bob", "sleep": "1"})
50+
b = busyboxSh.builder().WithContext(map[string]interface{}{"container": "bob"})
4851
cmd, err = b.generate()
4952
require.NoError(t, err)
50-
require.Equal(t, []string{"docker", "run", "--rm", "--name", "bob", "busybox", "/bin/sh", "-c",
51-
"'while", "true;", "do", "date;", "sleep", "1;", "done'"}, cmd)
53+
require.Equal(t, []string{"docker", "run", "--rm", "-ti", "--name", "bob", "busybox", "/bin/sh"}, cmd)
5254

5355
}
5456

@@ -71,11 +73,28 @@ func TestRun(t *testing.T) {
7173
require.Equal(t, []string{"spool", "www"}, strings.Split(strings.Trim(string(output), " \n"), "\n"))
7274

7375
name := "stream-test"
74-
err = busyboxDateStream.WithContext(map[string]interface{}{"container": name, "sleep": 1}).Start()
76+
go func() {
77+
<-time.After(2 * time.Second)
78+
err := dockerStop.Run(name)
79+
if err != nil {
80+
panic(err)
81+
}
82+
}()
83+
84+
err = dateStream.InheritEnvs(true).StartWithStreams(MergeOutput(os.Stderr),
85+
name, // arg 1 for container name
86+
)
7587
require.NoError(t, err)
7688

77-
time.Sleep(1 * time.Second)
78-
79-
err = dockerStop.Run(name)
89+
// testing with stdin
90+
err = Command("/bin/sh").InheritEnvs(true).StartWithStreams(
91+
Do(SendInput(
92+
func(stdin io.WriteCloser) error {
93+
stdin.Write([]byte(`for i in $(seq 10); do echo $i; sleep 1; done`))
94+
return nil
95+
})).Then(MergeOutput(os.Stderr)).Done(),
96+
name, // arg 1 for container name
97+
)
8098
require.NoError(t, err)
99+
81100
}

0 commit comments

Comments
 (0)