Skip to content

Commit

Permalink
Merge pull request #75 from davidovich/allow-running-commands
Browse files Browse the repository at this point in the history
Allow running commands
  • Loading branch information
davidovich authored Oct 30, 2021
2 parents e831307 + 31f1384 commit d6a3c34
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 62 deletions.
110 changes: 76 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@
- [Templating](#templating)
- [Running A Binary](#running-a-binary)
- [Templated Invokables](#templated-invokables)
- [Templated References](#templated-references)
- [Keeping DRY](#keeping-dry)
- [Using Args](#using-args)
- [Template Functions Available in Summon](#template-functions-available-in-summon)
- [Summon Function](#summon-function)
- [Arg and Args Function](#arg-and-args-function)
- [Run Function](#run-function)
- [Removing the run subcommand](#removing-the-run-subcommand)
- [Dump the Data at a Location](#dump-the-data-at-a-location)
- [Output a File to stdout](#output-a-file-to-stdout)
Expand Down Expand Up @@ -262,6 +264,7 @@ will yield:
```

### Running A Binary

`summon run [executable]` allows to run executables declared in the
[config file](#/summon-config-file).

Expand All @@ -278,34 +281,16 @@ in the config file:
```yaml
...
exec:
docker run -v {{ env "PWD" }}:/mounted-app alpine ls:
list: [/mounted-app]
docker run -v {{ env "PWD" }}:/mounted-app alpine:
ls: [ls, /mounted-app]
```
Calling `summon run list` would render the [{{ env "PWD" }}](https://masterminds.github.io/sprig/os.html) part to the current directory, resulting in this call:
Calling `summon run ls` would render the
[{{ env "PWD" }}](https://masterminds.github.io/sprig/os.html) part to the
current directory, resulting in this call:

`docker run -v [working-dir]:/mounted-app alpine ls /mounted-app`

#### Templated References

Say you would like to bundle a script in the data repo and also use it as an
invocable (new in v.0.10.0). You would use the `summon` template function bundled in summon:

```yaml
exec:
bash -c:
hello: ['{{ summon "hello.sh" }}']
```

Assuming you have a `hello.sh` file in the assets repo, this would result in sommoning the file in a temp dir and calling the invoker:

```
bash -c /tmp/hello.sh
```

> Note that `hello.sh` could also contain templates that will be
rendered at instanciation time.

#### Keeping DRY

> New in v0.12.0
Expand All @@ -315,7 +300,6 @@ parameters will always need to be passed (volume mounts for example). You
can use YAML anchors to define the static (but required) params in
`summon.config.yaml`:


```yaml
.base: &baseargs
- echo
Expand All @@ -330,7 +314,33 @@ exec:
Here, when you run with the `echo` handle, the arrays will be flattened to produce
`[echo, b, c, d]` for the construction of the command.

#### Using Args
#### Template Functions Available in Summon

Summon comes with template functions that can be used in the config file or
contained assets.

##### Summon Function

Say you would like to bundle a script in the data repo and also use it as an
invocable (new in v.0.10.0). You would use the `summon` template function bundled in summon:

```yaml
exec:
bash -c:
hello: ['{{ summon "hello.sh" }}']
```

Assuming you have a `hello.sh` file in the assets repo, this would result in
sommoning the file in a temp dir and calling the invoker:

```shell
bash -c /tmp/hello.sh
```

> Note that `hello.sh` could also contain templates that will be
rendered at instanciation time.

##### Arg and Args Function

> New in v0.12.0

Expand Down Expand Up @@ -366,6 +376,38 @@ summon would only append the unconsumed args (after index 0).
If the result of using args is a string representation of an array, like
`[a b c d]` this array will be flattened to the final args array.

##### Run Function

> New in v0.12.0

The `run` function allows running a configured handle of the config file, right
inside the config file. This effectively opens many use cases of executing
code to control arguments. Called subprocesses can have side effects and can
be used to execute pre conditions.

Consider:

We want to mount volumes of a docker container, conditionally.

```yaml
exec:
docker run -it --rm {{ run "eval-mounts" }} alpine:
ls: ['ls']
bash -c:
eval-mounts: ["echo -v {{ env PWD }}:/app"]
```

When inovking `summon run ls`, summon will first invoke:

`bash -c 'echo -v current_dir:/app'` which yields `-v current_dir:/app` and
then call the `ls` handle to produce:

`docker run -it --rm -v current_dir:/app alpine`

> Warning: `run` must not start a recursive process as `summon` does not currently
> protect from this type of call. The consequence of doing this will probably
> result in a fork bomb.

#### Removing the run subcommand

> New in v0.12.0
Expand Down Expand Up @@ -430,29 +472,29 @@ source <(summon completion)

## TODO

* [ ] Add a `required` template function to enforce `.args` presence, and error
- [ ] Add a `required` template function to enforce `.args` presence, and error
with a message.
* [ ] Add help documentation for proxied commands
* [ ] Explore ways to hook completions from proxied commands.
- [ ] Add help documentation for proxied commands
- [ ] Explore ways to hook completions from proxied commands.

## FAQ

* Why is the `exec:` config file ordered by "invoker" ?
- Why is the `exec:` config file ordered by "invoker" ?

Summon is oriented at providing an easy CLI interface to complex sub programs.
In this regard, it tends to group invocations in the same execution "environment".

This helps in scenarios of supplying a dev container from which are surfaced
tools for your team.

* Why not use git directly ?
- Why not use git directly ?

While you could use git directly to bring an asset directory with a simple git clone, the result does not have executable properties.

In summon you leverage go execution to bootstrap in one phase. So your data can do:

```bash
gobin -run github.com/davidovich/summon-example-assets/summon --help
go run github.com/davidovich/summon-example-assets/summon --help
# or list the data deliverables
gobin -run github.com/davidovich/summon-example-assets/summon ls
go run github.com/davidovich/summon-example-assets/summon ls
```
22 changes: 11 additions & 11 deletions internal/scaffold/packrd/packed-packr.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pkg/summon/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ func (d *Driver) Run(opts ...Option) error {

if !d.opts.dryrun {
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stdout = d.opts.out
cmd.Stderr = os.Stderr

return cmd.Run()
Expand Down
58 changes: 42 additions & 16 deletions pkg/summon/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,30 +20,40 @@ func TestRun(t *testing.T) {
name string
helper string
ref string
expect string
contains string
expect []string
contains []string
args []string
wantErr bool
}{
{
name: "composite-invoker", // python -c
helper: "TestSummonRunHelper",
ref: "hello",
expect: "python -c print(\"hello from python!\")",
expect: []string{"python -c print(\"hello from python!\")"},
wantErr: false,
},
{
name: "simple-invoker", // bash
helper: "TestSummonRunHelper",
ref: "hello-bash",
expect: "bash hello.sh",
expect: []string{"bash hello.sh"},
wantErr: false,
},
{
name: "self-reference-invoker", // bash
helper: "TestSummonRunHelper",
ref: "bash-self-ref",
expect: fmt.Sprintf("bash %s", filepath.Join(os.TempDir(), "hello.sh")),
expect: []string{fmt.Sprintf("bash %s", filepath.Join(os.TempDir(), "hello.sh"))},
wantErr: false,
},
{
name: "self-reference-run", // bash
helper: "TestSubCommandTemplateRunCall",
ref: "run-example",
expect: []string{
"bash hello.sh", // run first call (returns "hello from subcmd")
"bash hello from subcmd", // actual run-example call with args
},
wantErr: false,
},
{
Expand All @@ -62,45 +72,45 @@ func TestRun(t *testing.T) {
name: "renderable invoker",
helper: "TestSummonRunHelper",
ref: "docker",
expect: "docker info",
expect: []string{"docker info"},
wantErr: false,
},
{
name: "args access",
helper: "TestSummonRunHelper",
ref: "args",
args: []string{"a c", "b"},
expect: "bash args: a c b",
expect: []string{"bash args: a c b"},
wantErr: false,
},
{
name: "one arg access remainder passed",
helper: "TestSummonRunHelper",
ref: "one-arg",
args: []string{"\"acce ssed\"", "remainder1", "remainder2"},
expect: "bash args: \"acce ssed\" remainder1 remainder2",
expect: []string{"bash args: \"acce ssed\" remainder1 remainder2"},
wantErr: false,
},
{
name: "all args access no remainder passed",
helper: "TestSummonRunHelper",
ref: "all-args",
args: []string{"a", "b", "c", "d"},
expect: "bash args: a b c d",
expect: []string{"bash args: a b c d"},
wantErr: false,
},
{
name: "osArgs access",
helper: "TestSummonRunHelper",
ref: "osArgs",
contains: "test",
contains: []string{"test"},
wantErr: false,
},
{
name: "global template render",
helper: "TestSummonRunHelper",
ref: "templateref",
contains: "bash 1.2.3",
contains: []string{"bash 1.2.3"},
wantErr: false,
},
}
Expand All @@ -122,10 +132,17 @@ func TestRun(t *testing.T) {

if tt.wantErr {
assert.Len(t, c.Calls, 0)
} else if tt.expect != "" {
assert.Equal(t, tt.expect, c.Calls[0].Args)
} else if tt.contains != "" {
assert.Contains(t, c.Calls[0].Args, tt.contains)
} else {
if len(tt.expect) != 0 {
for i, e := range tt.expect {
assert.Equal(t, e, c.Calls[i].Args)
}
}
if len(tt.contains) != 0 {
for i, e := range tt.contains {
assert.Contains(t, c.Calls[i].Args, e)
}
}
}
})
}
Expand All @@ -139,11 +156,20 @@ func TestSummonRunHelper(t *testing.T) {
testutil.TestSummonRunHelper()
}

func TestSubCommandTemplateRunCall(t *testing.T) {
if testutil.IsHelper() {
defer os.Exit(0)
testutil.WriteCall(testutil.MakeCall())

fmt.Fprint(os.Stdout, "hello from subcmd")
}
}

func TestListInvocables(t *testing.T) {
box := packr.New("test run box", "testdata")

s, _ := New(box)

inv := s.ListInvocables()
assert.ElementsMatch(t, []string{"hello-bash", "bash-self-ref", "docker", "gobin", "gohack", "hello", "args", "one-arg", "all-args", "osArgs", "templateref"}, inv)
assert.ElementsMatch(t, []string{"hello-bash", "bash-self-ref", "run-example", "docker", "gobin", "gohack", "hello", "args", "one-arg", "all-args", "osArgs", "templateref"}, inv)
}
14 changes: 14 additions & 0 deletions pkg/summon/summon.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,20 @@ func (d *Driver) resolveAlias(alias string) string {

func summonFuncMap(d *Driver) template.FuncMap {
return template.FuncMap{
"run": func(args ...string) (string, error) {
driverCopy := Driver{
opts: d.opts,
Config: d.Config,
box: d.box,
templateCtx: d.templateCtx,
execCommand: d.execCommand,
configRead: d.configRead,
}
b := &strings.Builder{}
err := driverCopy.Run(Ref(args[0]), Args(args[1:]...), out(b))

return strings.TrimSpace(b.String()), err
},
"summon": func(path string) (string, error) {
return d.Summon(Filename(path), Dest(os.TempDir()))
},
Expand Down
1 change: 1 addition & 0 deletions pkg/summon/testdata/summon.config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ exec:
bash:
hello-bash: [ hello.sh ]
bash-self-ref: ['{{ summon "hello.sh" }}']
run-example: ['{{ run "hello-bash" }}']
args: ['args:', '{{ arg 0 "" }}']
one-arg: ['args:', '{{arg 0 "" }}']
all-args: ['args:', '{{ args }}']
Expand Down

0 comments on commit d6a3c34

Please sign in to comment.