Skip to content

Commit a3235b9

Browse files
committed
Add YQ template func.
1 parent 8f551e1 commit a3235b9

File tree

2 files changed

+160
-12
lines changed

2 files changed

+160
-12
lines changed

plugins/builtinprocessors/plugin.go

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22
package builtinprocessors
33

44
import (
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
9+
"github.com/knadh/koanf"
10+
yamlparser "github.com/knadh/koanf/parsers/yaml"
11+
"github.com/knadh/koanf/providers/rawbytes"
12+
513
"github.com/launchrctl/launchr/internal/launchr"
614
"github.com/launchrctl/launchr/pkg/action"
715
"github.com/launchrctl/launchr/pkg/jsonschema"
@@ -53,6 +61,10 @@ func addValueProcessors(tp *action.TemplateProcessors, cfg launchr.Config) {
5361
tp.AddValueProcessor(procGetConfigValue, procCfg)
5462
tplCfg := &configTemplateFunc{cfg: cfg}
5563
tp.AddTemplateFunc("config", tplCfg.Get)
64+
tp.AddTemplateFunc("yq", func(ctx action.TemplateFuncContext) any {
65+
tplYq := &yamlQueryTemplateFunc{action: ctx.Action()}
66+
return tplYq.Get
67+
})
5668
}
5769

5870
func processorConfigGetByKey(v any, opts ConfigGetProcessorOptions, ctx action.ValueProcessorContext, cfg launchr.Config) (any, error) {
@@ -71,16 +83,16 @@ func processorConfigGetByKey(v any, opts ConfigGetProcessorOptions, ctx action.V
7183
return jsonschema.EnsureType(ctx.DefParam.Type, res)
7284
}
7385

74-
// configKeyNotFound holds a config key element that was not found in config.
75-
// It will print a message in a template when a config key is missing.
76-
type configKeyNotFound string
86+
// tplKeyNotFound holds a key path element that was not found.
87+
// It will print a message in a template when a key is missing.
88+
type tplKeyNotFound string
7789

7890
// IsEmpty implements a special interface to support "default" template function
7991
// Example: {{ Config "foo.bar" | default "buz" }}
80-
func (s configKeyNotFound) IsEmpty() bool { return true }
92+
func (s tplKeyNotFound) IsEmpty() bool { return true }
8193

8294
// String implements [fmt.Stringer] to output a missing key to a template.
83-
func (s configKeyNotFound) String() string { return "<config key not found \"" + string(s) + "\">" }
95+
func (s tplKeyNotFound) String() string { return "<key not found \"" + string(s) + "\">" }
8496

8597
// configTemplateFunc is a set of template functions to interact with [launchr.Config] in [action.TemplateProcessors].
8698
type configTemplateFunc struct {
@@ -98,11 +110,52 @@ type configTemplateFunc struct {
98110
func (t *configTemplateFunc) Get(path string) (any, error) {
99111
var res any
100112
if !t.cfg.Exists(path) {
101-
return configKeyNotFound(path), nil
113+
return tplKeyNotFound(path), nil
102114
}
103115
err := t.cfg.Get(path, &res)
104116
if err != nil {
105117
return nil, err
106118
}
107119
return res, nil
108120
}
121+
122+
// yamlQueryTemplateFunc is a set of template funciton to parse and query yaml files like `yq`.
123+
type yamlQueryTemplateFunc struct {
124+
action *action.Action
125+
}
126+
127+
// Get returns a yaml file value by a key path.
128+
//
129+
// Usage:
130+
//
131+
// {{ yq "foo.bar" }} - retrieves value of any type
132+
// {{ index (yq "foo.array-elem") 1 }} - retrieves specific array element
133+
// {{ yq "foo.null-elem" | default "foo" }} - uses default if value is nil
134+
// {{ yq "foo.missing-elem" | default "bar" }} - uses default if key doesn't exist
135+
func (t *yamlQueryTemplateFunc) Get(filename, key string) (any, error) {
136+
k := koanf.New(".")
137+
absPath := filepath.ToSlash(filename)
138+
if !filepath.IsAbs(absPath) {
139+
absPath = filepath.Join(t.action.WorkDir(), absPath)
140+
}
141+
142+
content, err := os.ReadFile(absPath) //nolint:gosec // G301 File inclusion is expected.
143+
if err != nil {
144+
if os.IsNotExist(err) {
145+
return nil, fmt.Errorf("can't find yaml file %q", filename)
146+
}
147+
return nil, fmt.Errorf("can't read yaml file %q: %w", filename, err)
148+
}
149+
150+
err = k.Load(rawbytes.Provider(content), yamlparser.Parser())
151+
if err != nil {
152+
return nil, err
153+
}
154+
155+
if !k.Exists(key) {
156+
return tplKeyNotFound(filename + ":" + key), nil
157+
}
158+
159+
val := k.Get(key)
160+
return val, nil
161+
}

plugins/builtinprocessors/plugin_test.go

Lines changed: 101 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package builtinprocessors
22

33
import (
4+
"os"
5+
"path/filepath"
46
"testing"
57
"testing/fstest"
68

@@ -65,7 +67,7 @@ action:
6567
- processor: config.GetValue
6668
`
6769

68-
const testTplConfigGet = `
70+
const testTplConfig = `
6971
action:
7072
title: test config
7173
runtime:
@@ -81,7 +83,7 @@ runtime:
8183
- '{{ config "my.missing" | default "bar" }}'
8284
`
8385

84-
const testTplConfigGetMissing = `
86+
const testTplConfigMissing = `
8587
action:
8688
title: test config
8789
runtime:
@@ -91,7 +93,7 @@ runtime:
9193
- '{{ config "my.missing" }}'
9294
`
9395

94-
const testTplConfigGetBadArgs = `
96+
const testTplConfigBadArgs = `
9597
action:
9698
title: test config
9799
runtime:
@@ -110,6 +112,54 @@ my:
110112
null: null
111113
`
112114

115+
const testTplYq = `
116+
action:
117+
title: test yq
118+
options:
119+
- name: yamlPath
120+
default: "foo/bar.yaml"
121+
runtime:
122+
type: container
123+
image: alpine
124+
command:
125+
- '{{ yq .yamlPath "foo.bar" }}'
126+
- '{{ index (yq .yamlPath "foo") "bar" }}'
127+
- '{{ yq .yamlPath "foo.null" | default "foo" }}'
128+
- '{{ yq .yamlPath "my.missing" | default "bar" }}'
129+
`
130+
131+
const testTplYqMissing = `
132+
action:
133+
title: test yq
134+
options:
135+
- name: yamlPath
136+
default: "foo/bar.yaml"
137+
runtime:
138+
type: container
139+
image: alpine
140+
command:
141+
- '{{ yq .yamlPath "my.missing" }}'
142+
`
143+
144+
const testTplYqBadArgs = `
145+
action:
146+
title: test yq
147+
options:
148+
- name: yamlPath
149+
default: "foo/bar.yaml"
150+
runtime:
151+
type: container
152+
image: alpine
153+
command:
154+
- '{{ yq "1" "2" "3" }}'
155+
`
156+
157+
const testYqFileContent = `
158+
foo:
159+
bar: buz
160+
null: null
161+
`
162+
113163
func testConfigFS(s string) launchr.Config {
114164
m := fstest.MapFS{
115165
"config.yaml": &fstest.MapFile{Data: []byte(s)},
@@ -167,15 +217,60 @@ func Test_ConfigTemplateFunc(t *testing.T) {
167217
}
168218

169219
tt := []testCase{
170-
{Name: "valid", Yaml: testTplConfigGet, Exp: []string{"my_str", "42", "true", "[1 2 3]", "2", "foo", "bar"}},
171-
{Name: "key not found", Yaml: testTplConfigGetMissing, Exp: []string{"<config key not found \"my.missing\">"}},
172-
{Name: "incorrect call", Yaml: testTplConfigGetBadArgs, Err: "wrong number of args for config: want 1 got 2"},
220+
{Name: "valid", Yaml: testTplConfig, Exp: []string{"my_str", "42", "true", "[1 2 3]", "2", "foo", "bar"}},
221+
{Name: "key not found", Yaml: testTplConfigMissing, Exp: []string{"<key not found \"my.missing\">"}},
222+
{Name: "incorrect call", Yaml: testTplConfigBadArgs, Err: "wrong number of args for config: want 1 got 2"},
223+
}
224+
for _, tt := range tt {
225+
tt := tt
226+
t.Run(tt.Name, func(t *testing.T) {
227+
t.Parallel()
228+
a := action.NewFromYAML(tt.Name, []byte(tt.Yaml))
229+
a.SetServices(svc)
230+
err := a.EnsureLoaded()
231+
if tt.Err != "" {
232+
require.ErrorContains(t, err, tt.Err)
233+
return
234+
}
235+
require.NoError(t, err)
236+
rdef := a.RuntimeDef()
237+
assert.Equal(t, tt.Exp, []string(rdef.Container.Command))
238+
})
239+
}
240+
}
241+
242+
func Test_YqTemplateFunc(t *testing.T) {
243+
// Prepare services.
244+
tp := action.NewTemplateProcessors()
245+
addValueProcessors(tp, nil)
246+
svc := launchr.NewServiceManager()
247+
svc.Add(tp)
248+
249+
// Prepare test data.
250+
wd := t.TempDir()
251+
err := os.MkdirAll(filepath.Join(wd, "foo"), 0750)
252+
require.NoError(t, err)
253+
err = os.WriteFile(filepath.Join(wd, "foo", "bar.yaml"), []byte(testYqFileContent), 0600)
254+
require.NoError(t, err)
255+
256+
type testCase struct {
257+
Name string
258+
Yaml string
259+
Exp []string
260+
Err string
261+
}
262+
263+
tt := []testCase{
264+
{Name: "valid", Yaml: testTplYq, Exp: []string{"buz", "buz", "foo", "bar"}},
265+
{Name: "key not found", Yaml: testTplYqMissing, Exp: []string{"<key not found \"foo/bar.yaml:my.missing\">"}},
266+
{Name: "incorrect call", Yaml: testTplYqBadArgs, Err: "wrong number of args for yq: want 2 got 3"},
173267
}
174268
for _, tt := range tt {
175269
tt := tt
176270
t.Run(tt.Name, func(t *testing.T) {
177271
t.Parallel()
178272
a := action.NewFromYAML(tt.Name, []byte(tt.Yaml))
273+
a.SetWorkDir(wd)
179274
a.SetServices(svc)
180275
err := a.EnsureLoaded()
181276
if tt.Err != "" {

0 commit comments

Comments
 (0)