-
-
Notifications
You must be signed in to change notification settings - Fork 254
feat: support template functions in custom step types #2020
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
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
2d07b26
feat: support template functions in custom step types
yottahmd 16ee167
test: stabilize queue stale proc cleanup
yottahmd a532d7f
test: use direct sleep command in queue concurrency test
yottahmd 76b39a0
test: modernize queue sleep duration clamp
yottahmd File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,157 @@ | ||
| // Copyright (C) 2026 Yota Hamada | ||
| // SPDX-License-Identifier: GPL-3.0-or-later | ||
|
|
||
| package templatefuncs | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "reflect" | ||
| "strings" | ||
| "text/template" | ||
|
|
||
| sprig "github.com/go-task/slim-sprig/v3" | ||
| ) | ||
|
|
||
| // FuncMap returns Dagu's hermetic template function map. | ||
| // | ||
| // The map is built from slim-sprig's hermetic text functions, removes | ||
| // functions that should not be available in DAG templates, and applies | ||
| // Dagu-specific pipeline-friendly overrides. | ||
| func FuncMap() template.FuncMap { | ||
| // Start from the hermetic (no env/network/random) slim-sprig set. | ||
| m := sprig.HermeticTxtFuncMap() | ||
|
|
||
| // Defense-in-depth: remove any functions that should never be available in | ||
| // DAG templates. Some of these are not currently present in the hermetic | ||
| // set; keep the blocklist here so future slim-sprig changes cannot expose | ||
| // them accidentally. | ||
| for _, name := range blockedFuncs { | ||
| delete(m, name) | ||
| } | ||
|
|
||
| // Dagu-specific overrides. These preserve pipeline-compatible argument | ||
| // order (pipeline value as last arg) and existing behavior. Each override is | ||
| // intentional; slim-sprig defines overlapping names with different arg order | ||
| // or semantics. | ||
| m["split"] = func(sep, s string) []string { | ||
| return strings.Split(s, sep) | ||
| } | ||
| m["join"] = func(sep string, v any) (string, error) { | ||
| if v == nil { | ||
| return "", nil | ||
| } | ||
| switch elems := v.(type) { | ||
| case []string: | ||
| return strings.Join(elems, sep), nil | ||
| case []any: | ||
| strs := make([]string, len(elems)) | ||
| for i, e := range elems { | ||
| strs[i] = fmt.Sprint(e) | ||
| } | ||
| return strings.Join(strs, sep), nil | ||
| default: | ||
| rv := reflect.ValueOf(v) | ||
| if rv.IsValid() && (rv.Kind() == reflect.Slice || rv.Kind() == reflect.Array) { | ||
| strs := make([]string, rv.Len()) | ||
| for i := range strs { | ||
| strs[i] = fmt.Sprint(rv.Index(i).Interface()) | ||
| } | ||
| return strings.Join(strs, sep), nil | ||
| } | ||
| return "", fmt.Errorf("join: unsupported type %T", v) | ||
| } | ||
| } | ||
| m["count"] = func(v any) (int, error) { | ||
| if v == nil { | ||
| return 0, nil | ||
| } | ||
| rv := reflect.ValueOf(v) | ||
| switch rv.Kind() { //nolint:exhaustive // unsupported kinds return an error below | ||
| case reflect.Slice, reflect.Map, reflect.Array: | ||
| return rv.Len(), nil | ||
| case reflect.String: | ||
| return rv.Len(), nil | ||
| default: | ||
| return 0, fmt.Errorf("count: unsupported type %T", v) | ||
| } | ||
| } | ||
| m["add"] = func(b, a int) int { | ||
| return a + b | ||
| } | ||
| m["empty"] = func(v any) bool { | ||
| if v == nil { | ||
| return true | ||
| } | ||
| rv := reflect.ValueOf(v) | ||
| switch rv.Kind() { //nolint:exhaustive // non-empty scalar kinds are handled by IsZero below | ||
| case reflect.String: | ||
| return rv.Len() == 0 | ||
| case reflect.Slice, reflect.Map, reflect.Array: | ||
| return rv.Len() == 0 | ||
| default: | ||
| return rv.IsZero() | ||
| } | ||
| } | ||
| m["upper"] = func(s string) string { | ||
| return strings.ToUpper(s) | ||
| } | ||
| m["lower"] = func(s string) string { | ||
| return strings.ToLower(s) | ||
| } | ||
| m["trim"] = func(s string) string { | ||
| return strings.TrimSpace(s) | ||
| } | ||
| m["default"] = func(def, val any) any { | ||
| if val == nil { | ||
| return def | ||
| } | ||
| rv := reflect.ValueOf(val) | ||
| switch rv.Kind() { //nolint:exhaustive // scalar zero values are handled by IsZero below | ||
| case reflect.String: | ||
| if rv.Len() == 0 { | ||
| return def | ||
| } | ||
| case reflect.Slice, reflect.Map, reflect.Array: | ||
| if rv.Len() == 0 { | ||
| return def | ||
| } | ||
| default: | ||
| if rv.IsZero() { | ||
| return def | ||
| } | ||
| } | ||
| return val | ||
| } | ||
|
|
||
| return m | ||
| } | ||
|
|
||
| // blockedFuncs are removed even from the hermetic set as defense-in-depth. | ||
| // Some names are not present in slim-sprig v3 today; keep them blocked so | ||
| // future or forked slim-sprig versions cannot expose non-hermetic helpers. | ||
| var blockedFuncs = []string{ | ||
| // Environment variable access | ||
| "env", "expandenv", | ||
| // Network I/O | ||
| "getHostByName", | ||
| // Non-deterministic time | ||
| "now", "date", "dateInZone", "date_in_zone", | ||
| "dateModify", "date_modify", "mustDateModify", "must_date_modify", | ||
| "ago", "duration", "durationRound", | ||
| "unixEpoch", "toDate", "mustToDate", | ||
| "htmlDate", "htmlDateInZone", | ||
| // Crypto key generation | ||
| "genPrivateKey", "derivePassword", | ||
| "buildCustomCert", "genCA", | ||
| "genSelfSignedCert", "genSignedCert", | ||
| // Non-deterministic random | ||
| "randBytes", "randString", "randNumeric", | ||
| "randAlphaNum", "randAlpha", "randAscii", "randInt", | ||
| "uuidv4", | ||
| } | ||
|
|
||
| // BlockedFuncNames returns the names removed from the hermetic slim-sprig | ||
| // function map. | ||
| func BlockedFuncNames() []string { | ||
| return append([]string(nil), blockedFuncs...) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| // Copyright (C) 2026 Yota Hamada | ||
| // SPDX-License-Identifier: GPL-3.0-or-later | ||
|
|
||
| package templatefuncs | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "testing" | ||
| "text/template" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| func TestFuncMapJoinRejectsUnsupportedInput(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| tmpl, err := template.New("test").Funcs(FuncMap()).Parse(`{{ . | join "," }}`) | ||
| require.NoError(t, err) | ||
|
|
||
| var out bytes.Buffer | ||
| err = tmpl.Execute(&out, map[string]string{"a": "b"}) | ||
| require.Error(t, err) | ||
| assert.Contains(t, err.Error(), "join: unsupported type map[string]string") | ||
| assert.Empty(t, out.String()) | ||
| } | ||
|
|
||
| func TestFuncMapCountNilIsZero(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| tmpl, err := template.New("test").Funcs(FuncMap()).Parse(`{{ count . }}`) | ||
| require.NoError(t, err) | ||
|
|
||
| var out bytes.Buffer | ||
| require.NoError(t, tmpl.Execute(&out, nil)) | ||
| assert.Equal(t, "0", out.String()) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.