Skip to content

Commit

Permalink
wip: use expr language for executable templates
Browse files Browse the repository at this point in the history
  • Loading branch information
jahvon committed Dec 27, 2024
1 parent ee5d737 commit 2c47590
Show file tree
Hide file tree
Showing 15 changed files with 546 additions and 133 deletions.
2 changes: 1 addition & 1 deletion docs/guide/conditional.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ using a simple expression language that provides access to system information, e

### Expression Language

Flow uses the [Expr](https://expr-lang.org) language for evaluating conditions. The language supports common
flow uses the [Expr](https://expr-lang.org) language for evaluating conditions. The language supports common
operators and functions while providing access to flow executable-specific context data.

**See the [Expr language documentation](https://expr-lang.org/docs/language-definition) for more information on the
Expand Down
87 changes: 54 additions & 33 deletions docs/guide/templating.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,18 +100,18 @@ files will only be copied if the `Type` field is set to `Helm`. The `deploy.sh`
artifacts:
- srcName: "helm-deploy.sh"
srcDir: "scripts" # By default, the file will be copied from the template directory. This field can be used to specify a different directory.
if: "{{ eq .Type 'Helm' }}" # This will only copy the file if the Helm field is true
if: form["Type"] == "Helm" # This will only copy the file if the Helm field is true
- srcName: "deploy.sh"
srcDir: "scripts"
if: "{{ eq .Type 'K8s' }}" # This will only copy the file if the K8s field is true
if: form["Type"] == "K8s" # This will only copy the file if the K8s field is true
- srcName: "values.yaml.tmpl"
asTemplate: true
dstName: "values.yaml"
if: "{{ eq .Type 'Helm' }}" # This will only copy the file if the Helm field is true
if: form["Type"] == "Helm" # This will only copy the file if the Helm field is true
- srcName: "resources.yaml.tmpl"
asTemplate: true
dstName: "resources.yaml"
if: "{{ eq .Type 'K8s' }}" # This will only copy the file if the K8s field is true
if: form["Type"] == "K8s" # This will only copy the file if the K8s field is true
```

### flowfile template string
Expand All @@ -126,22 +126,22 @@ template: |
tags: [k8s]
executables:
- verb: deploy
name: "{{ .FlowFileName }}"
name: "{{ name }}"
exec:
file: "{{ if eq .Type 'Helm' }}helm-deploy.sh{{ else }}deploy.sh{{ end }}"
file: "{{ if form["Type"] == 'Helm' }}helm-deploy.sh{{ else }}deploy.sh{{ end }}"
params:
- envKey: "NAMESPACE"
text: "{{ .Namespace }}"
text: "{{ form["Namespace"] }}"
- envKey: "APP_NAME"
text: "{{ .FlowFileName }}"
text: "{{ name }}"
- verb: restart
name: "{{ .FlowFileName }}"
name: "{{ name }}"
exec:
cmd: "kubectl rollout restart deployment/{{ .FlowFileName }} -n {{ .Namespace }}"
cmd: "kubectl rollout restart deployment/{{ name }} -n {{ form["Namespace"] }}"
- verb: open
name: "{{ .FlowFileName }}"
name: "{{ name }}"
launch:
uri: "https://{{ .FlowFileName }}.my.haus"
uri: "https://{{ name }}.my.haus"
```

### Pre- and post- run executables
Expand All @@ -156,14 +156,15 @@ Before exiting, it will also run a simple command and either open the flowfile i
preRun:
- ref: "validate k8s/validation:context" # You can reference other executables that you have on your system
args: ["homelab"]
if: "{{ .Deploy }}"
if: form["Deploy"]
postRun:
- cmd: "echo 'Rendered {{ if .Helm }}Helm values{{ else }}k8s manifest{{end}}'; ls -al"
- cmd: |
echo 'Rendered {{ if form["Helm"] }}Helm values{{ else }}k8s manifest{{end}}'; ls -al
- ref: "edit vscode"
args: ["{{ .FlowFilePath }}"]
if: "{{ not .Deploy }}"
- ref: "deploy {{ .FlowFileName }}"
if: "{{ .Deploy }}"
args: ["{{ flowFilePath }}"]
if: not form["Deploy"]
- ref: "deploy {{ name }}"
if: form["Deploy"]
```

**Note**: preRun executables are run from the template directory, while postRun executables are run from the output directory.
Expand Down Expand Up @@ -215,36 +216,56 @@ postRun:
- ref: "edit vscode"
args: ["{{ .FlowFilePath }}"]
if: "{{ not .Deploy }}"
- ref: "deploy {{ .FlowFileName }}"
- ref: "deploy {{ .name }}"
if: "{{ .Deploy }}"
template: |
tags: [k8s]
executables:
- verb: deploy
name: "{{ .FlowFileName }}"
name: "{{ name }}"
exec:
file: "{{ if eq .Type 'Helm' }}helm-deploy.sh{{ else }}deploy.sh{{ end }}"
params:
- envKey: "NAMESPACE"
text: "{{ .Namespace }}"
text: "{{ .form.namespace }}"
- envKey: "APP_NAME"
text: "{{ .FlowFileName }}"
text: "{{ name }}"
- verb: restart
name: "{{ .FlowFileName }}"
name: "{{ name }}"
exec:
cmd: "kubectl rollout restart deployment/{{ .FlowFileName }} -n {{ .Namespace }}"
- verb: open
name: "{{ .FlowFileName }}"
name: "{{ name }}"
launch:
uri: "https://{{ .FlowFileName }}.my.haus"
```
uri: "https://{{ name }}.my.haus"
```
### Templating language

flow uses a hybrid of [Go text templating](https://pkg.go.dev/text/template) and [Expr](https://expr-lang.org) language for
rendering templates.

Aside from the `if` field in the artifacts, preRun, and postRun sections, all other fields that use templating
will need to include the `{{` and `}}` delimiters to indicate that the text should be rendered as a template.

**Template Variables**

The following variables are automatically available in all template expressions:

### Template helpers
| Variable | Type | Description |
|-----------------|------------------------|------------------------------------------------------------------|
| `os` | string | Operating system identifier (e.g., "linux", "darwin", "windows") |
| `arch` | string | System architecture (e.g., "amd64", "arm64") |
| `workspace` | string | Target workspace name |
| `workspacePath` | string | Full path to the target workspace root directory |
| `name` | string | Name provided for the newly rendered flow file |
| `directory` | string | Target directory that the template will be render in to |
| `flowFilePath` | string | Full path to the target flow file |
| `templatePath` | string | Path to the template file being rendered |
| `env` | map (string -> string) | Environment variables accessible to the template |
| `form` | map (string -> any) | Values provided through template form inputs |

Templates can use the [Sprig functions](https://masterminds.github.io/sprig/) to manipulate data during the rendering process.
Additionally, the following keys are available to the template:
**See the [Expr language documentation](https://expr-lang.org/docs/language-definition) for more information on the
additional expression functions and syntax.**

- `FlowFileName`: The name of the flowfile being rendered. This is the argument passed into the `generate` command.
- `FlowFilePath`: The output path to the flowfile being rendered.
- `FlowWorkspace`: The name of the workspace that the template is rendering into.
- `FlowWorkspacePath`: The path to the workspace root directory.
> [!NOTE]
> The `env` map contains environment variables that were present when the template was rendered. The `form` map contains values from any form inputs defined in the template configuration.
4 changes: 2 additions & 2 deletions docs/schemas/template_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"default": ""
},
"if": {
"description": "A condition to determine if the artifact should be copied. The condition is evaluated using Go templating \nfrom the form data. If the condition is not met, the artifact will not be copied.\n[Sprig functions](https://masterminds.github.io/sprig/) are available for use in the condition.\n\nFor example, to copy the artifact only if the `name` field is set:\n```\n{{ if .name }}true{{ end }}\n```\n",
"description": "An expression that determines whether the the artifact should be copied, using the Expr language syntax. \nThe expression is evaluated at runtime and must resolve to a boolean value. If the condition is not met, \nthe artifact will not be copied.\n\nThe expression has access to OS/architecture information (os, arch), environment variables (env), form input \n(form), and context information (name, workspace, directory, etc.).\n\nSee the [flow documentation](https://flowexec.io/#/guide/templating) for more information.\n",
"type": "string",
"default": ""
},
Expand Down Expand Up @@ -121,7 +121,7 @@
"default": ""
},
"if": {
"description": "A condition to determine if the executable should be run. The condition is evaluated using Go templating \nfrom the form data. If the condition is not met, the executable run will be skipped.\n[Sprig functions](https://masterminds.github.io/sprig/) are available for use in the condition.\n\nFor example, to run a command only if the `name` field is set:\n```\n{{ if .name }}true{{ end }}\n```\n",
"description": "An expression that determines whether the executable should be run, using the Expr language syntax. \nThe expression is evaluated at runtime and must resolve to a boolean value. If the condition is not met, \nthe executable will be skipped.\n\nThe expression has access to OS/architecture information (os, arch), environment variables (env), form input \n(form), and context information (name, workspace, directory, etc.).\n\nSee the [flow documentation](https://flowexec.io/#/guide/templating) for more information.\n",
"type": "string",
"default": ""
},
Expand Down
4 changes: 2 additions & 2 deletions docs/types/template.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Go templating from form data is supported in all fields.
| `asTemplate` | If true, the artifact will be copied as a template file. The file will be rendered using Go templating from the form data. [Sprig functions](https://masterminds.github.io/sprig/) are available for use in the template. | `boolean` | false | |
| `dstDir` | The directory to copy the file to. If not set, the file will be copied to the root of the flow file directory. The directory will be created if it does not exist. | `string` | | |
| `dstName` | The name of the file to copy to. If not set, the file will be copied with the same name. | `string` | | |
| `if` | A condition to determine if the artifact should be copied. The condition is evaluated using Go templating from the form data. If the condition is not met, the artifact will not be copied. [Sprig functions](https://masterminds.github.io/sprig/) are available for use in the condition. For example, to copy the artifact only if the `name` field is set: ``` {{ if .name }}true{{ end }} ``` | `string` | | |
| `if` | An expression that determines whether the the artifact should be copied, using the Expr language syntax. The expression is evaluated at runtime and must resolve to a boolean value. If the condition is not met, the artifact will not be copied. The expression has access to OS/architecture information (os, arch), environment variables (env), form input (form), and context information (name, workspace, directory, etc.). See the [flow documentation](https://flowexec.io/#/guide/templating) for more information. | `string` | | |
| `srcDir` | The directory to copy the file from. If not set, the file will be copied from the directory of the template file. | `string` | | |
| `srcName` | The name of the file to copy. | `string` | <no value> | |

Expand Down Expand Up @@ -94,7 +94,7 @@ Configuration for a template executable.
| ----- | ----------- | ---- | ------- | :--------: |
| `args` | Arguments to pass to the executable. | `array` (`string`) | [] | |
| `cmd` | The command to execute. One of `cmd` or `ref` must be set. | `string` | | |
| `if` | A condition to determine if the executable should be run. The condition is evaluated using Go templating from the form data. If the condition is not met, the executable run will be skipped. [Sprig functions](https://masterminds.github.io/sprig/) are available for use in the condition. For example, to run a command only if the `name` field is set: ``` {{ if .name }}true{{ end }} ``` | `string` | | |
| `if` | An expression that determines whether the executable should be run, using the Expr language syntax. The expression is evaluated at runtime and must resolve to a boolean value. If the condition is not met, the executable will be skipped. The expression has access to OS/architecture information (os, arch), environment variables (env), form input (form), and context information (name, workspace, directory, etc.). See the [flow documentation](https://flowexec.io/#/guide/templating) for more information. | `string` | | |
| `ref` | A reference to another executable to run in serial. One of `cmd` or `ref` must be set. | [ExecutableRef](#ExecutableRef) | | |


4 changes: 2 additions & 2 deletions examples/exec-template.flow.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ template: |
namespace: examples
executables:
- verb: run
name: "{{ .Color }}-msg"
description: Created from a template in {{ .FlowWorkspace }}
name: {{ form["Color"] }}-msg
description: Created from a template in {{ workspace }}
exec:
cmd: cat message.txt
11 changes: 7 additions & 4 deletions internal/services/expr/expr.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
"github.com/jahvon/flow/types/executable"
)

func IsTruthy(ex string, env *ExpressionData) (bool, error) {
func IsTruthy(ex string, env any) (bool, error) {
output, err := Evaluate(ex, env)
if err != nil {
return false, err
Expand All @@ -35,7 +35,7 @@ func IsTruthy(ex string, env *ExpressionData) (bool, error) {
}
}

func Evaluate(ex string, env *ExpressionData) (interface{}, error) {
func Evaluate(ex string, env any) (interface{}, error) {
program, err := expr.Compile(ex, expr.Env(env))
if err != nil {
return nil, err
Expand All @@ -48,7 +48,7 @@ func Evaluate(ex string, env *ExpressionData) (interface{}, error) {
return output, nil
}

func EvaluateString(ex string, env *ExpressionData) (string, error) {
func EvaluateString(ex string, env any) (string, error) {
output, err := Evaluate(ex, env)
if err != nil {
return "", err
Expand All @@ -64,6 +64,7 @@ type CtxData struct {
Workspace string `expr:"workspace"`
Namespace string `expr:"namespace"`
WorkspacePath string `expr:"workspacePath"`
FlowFileName string `expr:"flowFileName"`
FlowFilePath string `expr:"flowFilePath"`
FlowFileDir string `expr:"flowFileDir"`
}
Expand All @@ -81,13 +82,15 @@ func ExpressionEnv(
executable *executable.Executable,
dataMap, envMap map[string]string,
) ExpressionData {
fn := filepath.Base(filepath.Base(executable.FlowFilePath()))
return ExpressionData{
OS: runtime.GOOS,
Arch: runtime.GOARCH,
Ctx: &CtxData{
Workspace: ctx.CurrentWorkspace.AssignedName(),
Namespace: ctx.CurrentWorkspace.AssignedName(),
Namespace: ctx.Config.CurrentNamespace,
WorkspacePath: executable.WorkspacePath(),
FlowFileName: fn,
FlowFilePath: executable.FlowFilePath(),
FlowFileDir: filepath.Dir(executable.FlowFilePath()),
},
Expand Down
2 changes: 1 addition & 1 deletion internal/services/expr/expr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ var _ = Describe("Expr", func() {
})
})

var _ = Describe("ExpressionData", func() {
Describe("ExpressionData", func() {
var (
data *expr.ExpressionData
)
Expand Down
Loading

0 comments on commit 2c47590

Please sign in to comment.