Skip to content

#62 Add new processor to skip arguments in declaration #84

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions docs/actions.schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
- [Predefined Variables](#predefined-variables)
- [Environment Variables](#environment-variables)
- [Example](#example)
- [Available Command Template Functions](#available-command-template-functions)
4. [Runtimes](#runtimes)
- [Container](#container)
- [Command](#command)
Expand Down Expand Up @@ -228,6 +229,85 @@ runtime:
- {{ .optBool }}
```

### Available Command Template Functions

### `removeLine`
**Description:** A special template directive that removes the entire line from the final output.

**Usage:**

``` yaml
- "{{ if condition }}value{{ else }}{{ removeLine }}{{ end }}"
```

### `isNil`

**Description:** Checks if a value is nil.

**Usage:**

```yaml
- "{{ if not (isNil .param_name) }}--param={{ .param_name }}{{ else }}{{ removeLine }}{{ end }}"
```

### `isSet`

**Description:** Checks if a value has been set (opposite of `isNil`).

```yaml
- "{{ if isSet .param_name }}--param={{ .param_name }}{{else}}{{ removeLine }}{{ end }}"
```

### `isChanged`

**Description:** Checks if an option or argument value has been changed (dirty).

**Usage:**

```yaml
- '{{ if isChanged "param_name"}}--param={{.param_name}}{{else}}{{ removeLine }}{{ end }}'
```

### `removeLineIfNil`
**Description:** Removes the entire command line if the value is nil.

**Usage:**

```yaml
- "{{ removeLineIfNil .param_name }}"
```

### `removeLineIfSet`
**Description:** Removes the entire command line if the value is set (has no nil value).

**Usage:**

```yaml
- "{{ removeLineIfSet .param_name }}"
```

### `removeLineIfChanged`

**Description:** Removes the command line entry if the option/argument value has changed.

**Usage:**

``` yaml
- '{{ removeLineIfChanged "param_name" }}'
```

### `removeLineIfNotChanged`

**Description:** Removes the command line entry if the option/argument value has not changed by the user.
Opposite of `removeLineIfChanged`

**Usage:**

``` yaml
- '{{ removeLineIfNotChanged "param_name" }}'
```


## Runtimes

Action can be executed in different runtime environments. This section covers their declaration.
Expand Down
13 changes: 11 additions & 2 deletions example/actions/arguments/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ action:
description: Option to do something
type: boolean
default: false
- name: thirdoption
title: Third option
type: string

runtime:
type: container
Expand All @@ -29,5 +32,11 @@ runtime:
- /action/main.sh
- "{{ .arg1 }}"
- "{{ .arg2 }}"
- "{{ .firstoption }}"
- "{{ .secondoption }}"
- "{{ .firstoption|removeLineIfNil }}"
- "{{ if not (isNil .secondoption) }}--secondoption={{ .secondoption }}{{ else }}{{ removeLine }}{{ end }}"
- "{{ if isSet .thirdoption }}--thirdoption={{ .thirdoption }}{{else}}Third option is not set{{ end }}"
- "{{ removeLineIfSet .thirdoption }}"
- '{{ if not (isChanged "thirdoption")}}Third Option is not Changed{{else}}{{ removeLine }}{{ end }}'
- '{{ removeLineIfChanged "thirdoption" }}'
- '{{ if isChanged "thirdoption"}}Third Option is Changed{{else}}{{ removeLine }}{{ end }}'
- '{{ removeLineIfNotChanged "thirdoption" }}'
94 changes: 90 additions & 4 deletions pkg/action/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import (
"github.com/launchrctl/launchr/internal/launchr"
)

const tokenRmLine = "<TOKEN_REMOVE_THIS_LINE>" //nolint:gosec // G101: Not a credential.

var rgxTokenRmLine = regexp.MustCompile(`.*` + tokenRmLine + `.*\n?`)

// Loader is an interface for loading an action file.
type Loader interface {
// Content returns the raw file content.
Expand Down Expand Up @@ -75,7 +79,7 @@ func getenv(key string) string {

type inputProcessor struct{}

var rgxTplVar = regexp.MustCompile(`{{.*?\.(\S+).*?}}`)
var rgxTplVar = regexp.MustCompile(`{{.*?\.([a-zA-Z][a-zA-Z0-9_]*).*?}}`)

type errMissingVar struct {
vars map[string]struct{}
Expand All @@ -90,6 +94,84 @@ func (err errMissingVar) Error() string {
return fmt.Sprintf("the following variables were used but never defined: %v", f)
}

// actionTplFuncs defined template functions available during parsing of an action yaml.
func actionTplFuncs(input *Input) template.FuncMap {
// Helper function to get value by name from args or opts
getValue := func(name string) any {
args := input.Args()
if arg, ok := args[name]; ok {
return arg
}

opts := input.Opts()
if opt, ok := opts[name]; ok {
return opt
}

return nil
}

// Helper function to check if a parameter is changed
isParamChanged := func(name string) bool {
return input.IsOptChanged(name) || input.IsArgChanged(name)
}

return template.FuncMap{
// Checks if a value is nil. Used in conditions.
"isNil": func(v any) bool {
return v == nil
},
// Checks if a value is not nil. Used in conditions.
"isSet": func(v any) bool {
return v != nil
},
// Checks if a value is changed. Used in conditions.
"isChanged": func(v any) bool {
name, ok := v.(string)
if !ok {
return false
}

return isParamChanged(name)
},
// Removes a line if a given value is nil or pass through.
"removeLineIfNil": func(v any) any {
if v == nil {
return tokenRmLine
}
return v
},
// Removes a line if a given value is not nil or pass through.
"removeLineIfSet": func(v any) any {
if v != nil {
return tokenRmLine
}

return v
},
// Removes a line if a given value is changed or pass through.
"removeLineIfChanged": func(name string) any {
if isParamChanged(name) {
return tokenRmLine
}

return getValue(name)
},
// Removes a line if a given value is not changed or pass through.
"removeLineIfNotChanged": func(name string) any {
if !isParamChanged(name) {
return tokenRmLine
}

return getValue(name)
},
// Removes current line.
"removeLine": func() string {
return tokenRmLine
},
}
}

func (p inputProcessor) Process(ctx LoadContext, b []byte) ([]byte, error) {
if ctx.Action == nil {
return b, nil
Expand All @@ -101,7 +183,8 @@ func (p inputProcessor) Process(ctx LoadContext, b []byte) ([]byte, error) {
addPredefinedVariables(data, a)

// Parse action without variables to validate
tpl := template.New(a.ID)
tpl := template.New(a.ID).Funcs(actionTplFuncs(a.Input()))

_, err := tpl.Parse(string(b))
if err != nil {
// Check if variables have dashes to show the error properly.
Expand All @@ -125,7 +208,7 @@ Action definition is correct, but dashes are not allowed in templates, replace "
return nil, err
}

// Find if some vars were used but not defined.
// Find if some vars were used but not defined in arguments or options.
miss := make(map[string]struct{})
res := buf.Bytes()
if bytes.Contains(res, []byte("<no value>")) {
Expand All @@ -136,13 +219,16 @@ Action definition is correct, but dashes are not allowed in templates, replace "
miss[k] = struct{}{}
}
}
// If we don't have parameter names, the values probably were nil.
// If we don't have parameter names here, it means that all parameters are defined but the values were nil.
// It's ok, users will be able to identify missing parameters.
if len(miss) != 0 {
return nil, errMissingVar{miss}
}
}

// Remove all lines containing [tokenRmLine].
res = rgxTokenRmLine.ReplaceAll(res, []byte(""))

return res, nil
}

Expand Down
30 changes: 29 additions & 1 deletion pkg/action/loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,38 @@ func Test_InputProcessor(t *testing.T) {
assert.Equal(t, "", string(res))

// Check that we have an error when missing variables are not handled.
errMissVars := errMissingVar{vars: map[string]struct{}{"optUnd": {}, "arg2": {}}}
s = "{{ .arg2 }},{{ .optUnd }}"
res, err = proc.Process(ctx, []byte(s))
assert.Equal(t, err, errMissingVar{vars: map[string]struct{}{"optUnd": {}, "arg2": {}}})
assert.Equal(t, errMissVars, err)
assert.Equal(t, "", string(res))

// Remove line if a variable not exists or is nil.
s = `- "{{ .arg1 | removeLineIfNil }}"
- "{{ .optUnd | removeLineIfNil }}" # Piping with new line
- "{{ if not (isNil .arg1) }}arg1 is not nil{{end}}"
- "{{ if (isNil .optUnd) }}{{ removeLine }}{{ end }}" # Function call without new line`
res, err = proc.Process(ctx, []byte(s))
assert.NoError(t, err)
assert.Equal(t, "- \"arg1\"\n- \"arg1 is not nil\"\n", string(res))

// Remove line if a variable not exists or is nil, 1 argument is not defined and not checked.
s = `- "{{ .arg1 | removeLineIfNil }}"
- "{{ .optUnd|removeLineIfNil }}" # Piping with new line
- "{{ .arg2 }}"
- "{{ if not (isNil .arg1) }}arg1 is not nil{{end}}"
- "{{ if (isNil .optUnd) }}{{ removeLine }}{{ end }}" # Function call without new line`
_, err = proc.Process(ctx, []byte(s))
assert.Equal(t, errMissVars, err)

s = `- "{{ if isSet .arg1 }}arg1 is set"{{end}}
- "{{ removeLineIfSet .arg1 }}" # Function call without new line
- "{{ if isChanged .arg1 }}arg1 is changed{{end}}"
- '{{ removeLineIfNotChanged "arg1" }}'
- '{{ removeLineIfChanged "arg1" }}' # Function call without new line`
res, err = proc.Process(ctx, []byte(s))
assert.NoError(t, err)
assert.Equal(t, "- \"arg1 is set\"\n- \"arg1 is changed\"\n- 'arg1'\n", string(res))
}

func Test_YamlTplCommentsProcessor(t *testing.T) {
Expand Down