Skip to content

Commit

Permalink
Merge pull request #171 from choria-io/170
Browse files Browse the repository at this point in the history
(#170) Support including definitions
  • Loading branch information
ripienaar authored Aug 25, 2024
2 parents c2dc583 + 5b33856 commit 4120c06
Show file tree
Hide file tree
Showing 11 changed files with 225 additions and 49 deletions.
32 changes: 32 additions & 0 deletions builder/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,38 @@ func (b *AppBuilder) loadDefinitionBytes(cfg []byte, path string) (*Definition,
return nil, fmt.Errorf("%w: %v", ErrInvalidDefinition, err)
}

if d.IncludeFile != "" {
f, err := os.ReadFile(d.IncludeFile)
if err != nil {
return nil, err
}
cfgj, err := yaml.YAMLToJSON(f)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrInvalidDefinition, err)
}

def := &Definition{}
err = json.Unmarshal(cfgj, def)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrInvalidDefinition, err)
}

if d.Name != "" {
def.Name = d.Name
}
if d.Author != "" {
def.Author = d.Author
}
if d.Description != "" {
def.Description = d.Description
}
if d.Version != "" {
def.Version = d.Version
}

d = def
}

err = b.createCommands(d, d.Commands)
if err != nil {
return nil, err
Expand Down
1 change: 1 addition & 0 deletions builder/definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type Definition struct {
Author string `json:"author"`
Cheats *AppCheat `json:"cheat"`
HelpTemplate string `json:"help_template"`
IncludeFile string `json:"include_file"`

GenericSubCommands

Expand Down
34 changes: 34 additions & 0 deletions commands/parent/parent.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/ghodss/yaml"
"os"
"strings"

"github.com/choria-io/appbuilder/builder"
"github.com/choria-io/fisk"
)

type Command struct {
IncludeFile string `json:"include_file"`
builder.GenericSubCommands
builder.GenericCommand
}
Expand Down Expand Up @@ -42,9 +45,40 @@ func NewParentCommand(_ *builder.AppBuilder, j json.RawMessage, _ builder.Logger
return nil, fmt.Errorf("%w: %v", builder.ErrInvalidDefinition, err)
}

if parent.def.IncludeFile != "" {
err = parent.includeCommands()
if err != nil {
return nil, err
}
parent.def.IncludeFile = ""
}

return parent, nil
}

func (p *Parent) includeCommands() error {
b, err := os.ReadFile(p.def.IncludeFile)
if err != nil {
return err
}

name := p.def.Name

cfgj, err := yaml.YAMLToJSON(b)
if err != nil {
return err
}

err = json.Unmarshal(cfgj, p.def)
if err != nil {
return err
}

p.def.Name = name

return nil
}

func (p *Parent) String() string { return fmt.Sprintf("%s (parent)", p.def.Name) }

func (p *Parent) Validate(log builder.Logger) error {
Expand Down
60 changes: 25 additions & 35 deletions docs/content/experiments/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,31 @@ pre = "<b>4. </b>"

Some features are ongoing experiments and not part of the supported feature set, this section will call them out.

## Including other definitions

Since version 0.10.0 an entire definition can be included from another file or just the commands in a parent.

```yaml
name: include
description: An include based app
version: 0.2.2
author: [email protected]

include_file: sample-app.yaml
```
Here we include the entire application from another file but we override the name, description, version and author.
A specific `parent` can load all it's commands from a file:

```yaml
- name: include
type: parent
include_file: go.yaml
```

In this case the go.yaml would be the full `parent` definition.

## Form based data generation wizards

The general flow of applications is to expose Arguments and Flags when then can be used in templates to create files
Expand Down Expand Up @@ -147,41 +172,6 @@ above) and will only ask this `thing` when the user opted to add accounts:
conditional: Input.accounts != nil
```

## Argument and Flag Validations
One might need to ensure that the input provided by a user passes some validation, for example when passing commands
to shell scripts one has to be careful about [Shell Injection](https://en.wikipedia.org/wiki/Code_injection#Shell_injection).
We support custom validators on Arguments and Flags using the [Expr Language](https://expr.medv.io/docs/Language-Definition)
{{% notice secondary "Version Hint" code-branch %}}
This is available since version `0.8.0`.
{{% /notice %}}

Based on the Getting Started example that calls `cowsay` we might wish to limit the length of the message to what
would work well with `cowsay` and also ensure there is no shell escaping happening.

```yaml
arguments:
- name: message
description: The message to display
required: true
validate: len(value) < 20 && is_shellsafe(value)
```
We support the standard `expr` language grammar - that has a large number of functions that can assist the
validation needs - we then add a few extra functions that makes sense for operation teams.

In each case accessing `value` would be the value passed from the user

| Function | Description |
|----------------------|---------------------------------------------------------------|
| `isIP(value)` | Checks if `value` is an IPv4 or IPv6 address |
| `isIPv4(value)` | Checks if `value` is an IPv4 address |
| `isIPv6(value)` | Checks if `value` is an IPv6 address |
| `isInt(value)` | Checks if `value` is an Integer |
| `isFloat(value)` | Checks if `value` is a Float |
| `isShellSafe(value)` | Checks if `value` is attempting to to do shell escape attacks |

## Compiled Applications

It's nice that you do not need to compile App Builder apps into binaries as it allows for fast iteration, but sometimes
Expand Down
42 changes: 37 additions & 5 deletions docs/content/reference/common-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,7 @@ commands:
Here we show the initial options that define the application followed by commands. All the top settings are required except `help_template`, it's value may be one of `compact`, `long`, `short` or `default`. When not set it will equal `default`. Experiment with these options to see which help format suits your app best (requires version 0.0.9).

Since version `0.0.6` you can emit a banner before invoking the commands in an exec, use this to show a warning or extra
information to users before running a command. Perhaps to warn them that a config override is in use like here:
You can emit a banner before invoking the commands in an exec, use this to show a warning or extra information to users before running a command. Perhaps to warn them that a config override is in use like here:

```yaml
- name: say
Expand All @@ -98,7 +97,7 @@ information to users before running a command. Perhaps to warn them that a conf
required: true
```

Since version `0.0.7` we support Cheat Sheet style help, see the [dedicated guide](../cheats/) about that.
We support Cheat Sheet style help, see the [dedicated guide](../cheats/) about that.

#### Arguments

Expand Down Expand Up @@ -133,8 +132,6 @@ A `flag` is a option passed to the application using something like `--flag`, ty

##### Boolean Flags

We support boolean flags since version `0.1.1`:

```yaml
- name: delete
description: Delete the data
Expand All @@ -153,6 +150,41 @@ We support boolean flags since version `0.1.1`:

Here we have a `--force` flag that is used to influence the command. Booleans can have their default set to `true` or `"true`" which will then add a `--no-flag-name` option added to negate it.

#### Argument and Flag Validations

One might need to ensure that the input provided by a user passes some validation, for example when passing commands
to shell scripts one has to be careful about [Shell Injection](https://en.wikipedia.org/wiki/Code_injection#Shell_injection).

We support custom validators on Arguments and Flags using the [Expr Language](https://expr.medv.io/docs/Language-Definition)

{{% notice secondary "Version Hint" code-branch %}}
This is available since version `0.8.0`.
{{% /notice %}}

Based on the Getting Started example that calls `cowsay` we might wish to limit the length of the message to what
would work well with `cowsay` and also ensure there is no shell escaping happening.

```yaml
arguments:
- name: message
description: The message to display
required: true
validate: len(value) < 20 && is_shellsafe(value)
```
We support the standard `expr` language grammar - that has a large number of functions that can assist the
validation needs - we then add a few extra functions that makes sense for operation teams.

In each case accessing `value` would be the value passed from the user

| Function | Description |
|----------------------|---------------------------------------------------------------|
| `isIP(value)` | Checks if `value` is an IPv4 or IPv6 address |
| `isIPv4(value)` | Checks if `value` is an IPv4 address |
| `isIPv6(value)` | Checks if `value` is an IPv6 address |
| `isInt(value)` | Checks if `value` is an Integer |
| `isFloat(value)` | Checks if `value` is a Float |
| `isShellSafe(value)` | Checks if `value` is attempting to to do shell escape attacks |

### Confirmations

You can prompt for confirmation from a user for performing an action:
Expand Down
33 changes: 33 additions & 0 deletions example/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,39 @@ var _ = Describe("Example Application", func() {
})

Describe("Basics", func() {
Describe("Command Includes", func() {
It("Should include the commands from go.yaml", func() {
cmd.MustParseWithUsage(strings.Fields("include required ginkgo"))
Expect(usageBuf.String()).To(ContainSubstring("Hello ginkgo"))
})
})

Describe("App includes", func() {
It("Should include the app includes", func() {
app, err = builder.New(context.Background(), "include",
builder.WithAppDefinitionFile("include-app.yaml"),
builder.WithLogger(&builder.NoopLogger{}),
builder.WithContextualUsageOnError(),
builder.WithStdout(usageBuf),
builder.WithStderr(usageBuf))
Expect(err).ToNot(HaveOccurred())
cmd, err = app.FiskApplication()
Expect(err).ToNot(HaveOccurred())
cmd.Terminate(func(int) {})

usageBuf.Reset()
cmd.Writer(usageBuf)

usageBuf.Reset()

cmd.MustParseWithUsage(strings.Fields("--help"))
usageBuf.Reset()

cmd.MustParseWithUsage(strings.Fields("basics required ginkgo"))
Expect(usageBuf.String()).To(ContainSubstring("Hello ginkgo"))
})
})

Describe("required", func() {
It("Should require a name", func() {
cmd.MustParseWithUsage(strings.Fields("basics required"))
Expand Down
44 changes: 44 additions & 0 deletions example/go.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: go
description: Demonstrates basic features such as flags and arguments
type: parent
commands:
# Demonstrates the use of arguments and flags, mixing in arguments for main
# functionality and flags for optional behavior changes.
#
# Also uses Go templates to adjust the command based on the flags given and
# checks input is valid.
- name: required
description: Greet someone by name, name+surname with a customizable greeting
type: exec
# Here we use arguments for the name and surname, the name is required but surname is optional.
arguments:
- name: name
description: The name of the person to greet
required: true
validate: len(value) < 20
- name: surname
description: An optional surname of the person to greet
# We add an optional flag to override the "Hello" greeting
# It accepts only one of the 3 valid values
flags:
- name: greeting
description: The greeting to use instead of Hello
default: Hello
env: GREETING
short: g
enum:
- Hello
- Morning
- Halo

# We use go templates and the default + require functions to ensure users do not set empty
# strings such as "sample basics required ''" which would set the name to an empty string,
# in that case we would fail stating a name is required.
#
# In the case of an empty greeting, we fall back to the default "Hello"
command: |
{{ if .Arguments.surname }}
echo '{{ .Flags.greeting }} {{.Arguments.surname}}, {{ require .Arguments.name "a name is required" }} {{.Arguments.surname}}'
{{ else }}
echo '{{ .Flags.greeting }} {{ require .Arguments.name "a name is required" }}'
{{ end }}
6 changes: 6 additions & 0 deletions example/include-app.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
name: include
description: An include based app
version: 0.2.2
author: [email protected]

include_file: sample-app.yaml
4 changes: 4 additions & 0 deletions example/sample-app.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,10 @@ commands:
empty: absent
conditional: Input.accounts != nil

- name: include
type: parent
include_file: go.yaml

- name: transforms
description: Demonstrate transform features
type: parent
Expand Down
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.22.0

require (
al.essio.dev/pkg/shellescape v1.5.0
dario.cat/mergo v1.0.0
dario.cat/mergo v1.0.1
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/Masterminds/goutils v1.1.1
github.com/Masterminds/semver/v3 v3.2.1
Expand All @@ -21,7 +21,7 @@ require (
github.com/itchyny/gojq v0.12.16
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/mitchellh/copystructure v1.2.0
github.com/onsi/ginkgo/v2 v2.20.0
github.com/onsi/ginkgo/v2 v2.20.1
github.com/onsi/gomega v1.34.1
github.com/shopspring/decimal v1.4.0
github.com/sirupsen/logrus v1.9.3
Expand All @@ -44,7 +44,7 @@ require (
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa // indirect
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/sys v0.24.0 // indirect
golang.org/x/term v0.23.0 // indirect
Expand Down
Loading

0 comments on commit 4120c06

Please sign in to comment.