Skip to content

Commit 88f350b

Browse files
authored
Merge pull request #3313 from jandubois/tmpl-yq
Create `limactl tmpl yq` command to query a template
2 parents 9468aff + be64942 commit 88f350b

File tree

3 files changed

+105
-52
lines changed

3 files changed

+105
-52
lines changed

cmd/limactl/template.go

+67-16
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/lima-vm/lima/pkg/limatmpl"
1313
"github.com/lima-vm/lima/pkg/limayaml"
1414
"github.com/lima-vm/lima/pkg/store/dirnames"
15+
"github.com/lima-vm/lima/pkg/yqutil"
1516
"github.com/sirupsen/logrus"
1617
"github.com/spf13/cobra"
1718
)
@@ -34,6 +35,7 @@ func newTemplateCommand() *cobra.Command {
3435
templateCommand.AddCommand(
3536
newTemplateCopyCommand(),
3637
newTemplateValidateCommand(),
38+
newTemplateYQCommand(),
3739
)
3840
return templateCommand
3941
}
@@ -71,7 +73,24 @@ func newTemplateCopyCommand() *cobra.Command {
7173
return templateCopyCommand
7274
}
7375

76+
func fillDefaults(tmpl *limatmpl.Template) error {
77+
limaDir, err := dirnames.LimaDir()
78+
if err != nil {
79+
return err
80+
}
81+
// Load() will merge the template with override.yaml and default.yaml via FillDefaults().
82+
// FillDefaults() needs the potential instance directory to validate host templates using {{.Dir}}.
83+
filePath := filepath.Join(limaDir, tmpl.Name+".yaml")
84+
tmpl.Config, err = limayaml.Load(tmpl.Bytes, filePath)
85+
if err == nil {
86+
tmpl.Bytes, err = limayaml.Marshal(tmpl.Config, false)
87+
}
88+
return err
89+
}
90+
7491
func templateCopyAction(cmd *cobra.Command, args []string) error {
92+
source := args[0]
93+
target := args[1]
7594
embed, err := cmd.Flags().GetBool("embed")
7695
if err != nil {
7796
return err
@@ -97,12 +116,12 @@ func templateCopyAction(cmd *cobra.Command, args []string) error {
97116
if embed && verbatim {
98117
return errors.New("--verbatim cannot be used with any of --embed, --embed-all, or --fill")
99118
}
100-
tmpl, err := limatmpl.Read(cmd.Context(), "", args[0])
119+
tmpl, err := limatmpl.Read(cmd.Context(), "", source)
101120
if err != nil {
102121
return err
103122
}
104123
if len(tmpl.Bytes) == 0 {
105-
return fmt.Errorf("don't know how to interpret %q as a template locator", args[0])
124+
return fmt.Errorf("don't know how to interpret %q as a template locator", source)
106125
}
107126
if !verbatim {
108127
if embed {
@@ -117,24 +136,11 @@ func templateCopyAction(cmd *cobra.Command, args []string) error {
117136
}
118137
}
119138
if fill {
120-
limaDir, err := dirnames.LimaDir()
121-
if err != nil {
122-
return err
123-
}
124-
// Load() will merge the template with override.yaml and default.yaml via FillDefaults().
125-
// FillDefaults() needs the potential instance directory to validate host templates using {{.Dir}}.
126-
filePath := filepath.Join(limaDir, tmpl.Name+".yaml")
127-
tmpl.Config, err = limayaml.Load(tmpl.Bytes, filePath)
128-
if err != nil {
129-
return err
130-
}
131-
tmpl.Bytes, err = limayaml.Marshal(tmpl.Config, false)
132-
if err != nil {
139+
if err := fillDefaults(tmpl); err != nil {
133140
return err
134141
}
135142
}
136143
writer := cmd.OutOrStdout()
137-
target := args[1]
138144
if target != "-" {
139145
file, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
140146
if err != nil {
@@ -147,6 +153,51 @@ func templateCopyAction(cmd *cobra.Command, args []string) error {
147153
return err
148154
}
149155

156+
const templateYQHelp = `Use the builtin YQ evaluator to extract information from a template.
157+
External references are embedded and default values are filled in
158+
before the YQ expression is evaluated.
159+
160+
Example:
161+
limactl template yq template://default '.images[].location'
162+
163+
The example command is equivalent to using an external yq command like this:
164+
limactl template copy --fill template://default - | yq '.images[].location'
165+
`
166+
167+
func newTemplateYQCommand() *cobra.Command {
168+
templateYQCommand := &cobra.Command{
169+
Use: "yq TEMPLATE EXPR",
170+
Short: "Query template expressions",
171+
Long: templateYQHelp,
172+
Args: WrapArgsError(cobra.ExactArgs(2)),
173+
RunE: templateYQAction,
174+
}
175+
return templateYQCommand
176+
}
177+
178+
func templateYQAction(cmd *cobra.Command, args []string) error {
179+
locator := args[0]
180+
expr := args[1]
181+
tmpl, err := limatmpl.Read(cmd.Context(), "", locator)
182+
if err != nil {
183+
return err
184+
}
185+
if len(tmpl.Bytes) == 0 {
186+
return fmt.Errorf("don't know how to interpret %q as a template locator", locator)
187+
}
188+
if err := tmpl.Embed(cmd.Context(), true, true); err != nil {
189+
return err
190+
}
191+
if err := fillDefaults(tmpl); err != nil {
192+
return err
193+
}
194+
out, err := yqutil.EvaluateExpressionPlain(expr, string(tmpl.Bytes))
195+
if err == nil {
196+
_, err = fmt.Fprint(cmd.OutOrStdout(), out)
197+
}
198+
return err
199+
}
200+
150201
func newTemplateValidateCommand() *cobra.Command {
151202
templateValidateCommand := &cobra.Command{
152203
Use: "validate TEMPLATE [TEMPLATE, ...]",

hack/test-templates.sh

+9-20
Original file line numberDiff line numberDiff line change
@@ -102,26 +102,15 @@ if limactl ls -q | grep -q "$NAME"; then
102102
exit 1
103103
fi
104104

105-
# Create ${NAME}-tmp to inspect the enabled features.
106-
# TODO: skip downloading and converting the image here.
107-
# Probably `limactl create` should have "dry run" mode that just generates `lima.yaml`.
108-
# shellcheck disable=SC2086
109-
"${LIMACTL_CREATE[@]}" ${LIMACTL_CREATE_ARGS} --set ".additionalDisks=null" --name="${NAME}-tmp" "$FILE_HOST"
110-
# skipping the missing yq as it is not a fatal error because networks we are looking for are not supported on windows
111-
if command -v yq &>/dev/null; then
112-
case "$(yq '.networks[].lima' "${LIMA_HOME}/${NAME}-tmp/lima.yaml")" in
113-
"shared")
114-
CHECKS["vmnet"]=1
115-
;;
116-
"user-v2")
117-
CHECKS["port-forwards"]=""
118-
CHECKS["user-v2"]=1
119-
;;
120-
esac
121-
else
122-
WARNING "yq not found. Skipping network checks"
123-
fi
124-
limactl rm -f "${NAME}-tmp"
105+
case "$(limactl tmpl yq "$FILE_HOST" '.networks[].lima')" in
106+
"shared")
107+
CHECKS["vmnet"]=1
108+
;;
109+
"user-v2")
110+
CHECKS["port-forwards"]=""
111+
CHECKS["user-v2"]=1
112+
;;
113+
esac
125114

126115
if [[ -n ${CHECKS["port-forwards"]} ]]; then
127116
tmpconfig="$HOME_HOST/lima-config-tmp"

pkg/yqutil/yqutil.go

+29-16
Original file line numberDiff line numberDiff line change
@@ -39,25 +39,12 @@ func ValidateContent(content []byte) error {
3939
return err
4040
}
4141

42-
// EvaluateExpression evaluates the yq expression and returns the modified yaml.
43-
func EvaluateExpression(expression string, content []byte) ([]byte, error) {
42+
// EvaluateExpressionPlain evaluates the yq expression and returns the yq result.
43+
func EvaluateExpressionPlain(expression, content string) (string, error) {
4444
if expression == "" {
4545
return content, nil
4646
}
4747
logrus.Debugf("Evaluating yq expression: %q", expression)
48-
formatter, err := yamlfmtBasicFormatter()
49-
if err != nil {
50-
return nil, err
51-
}
52-
// `ApplyFeatures()` is being called directly before passing content to `yqlib`.
53-
// This results in `ApplyFeatures()` being called twice with `FeatureApplyBefore`:
54-
// once here and once inside `formatter.Format`.
55-
// Currently, calling `ApplyFeatures()` with `FeatureApplyBefore` twice is not an issue,
56-
// but future changes to `yamlfmt` might cause problems if it is called twice.
57-
_, contentModified, err := formatter.Features.ApplyFeatures(context.Background(), content, yamlfmt.FeatureApplyBefore)
58-
if err != nil {
59-
return nil, err
60-
}
6148
memory := logging.NewMemoryBackend(0)
6249
backend := logging.AddModuleLevel(memory)
6350
logging.SetBackend(backend)
@@ -68,7 +55,7 @@ func EvaluateExpression(expression string, content []byte) ([]byte, error) {
6855
encoderPrefs.ColorsEnabled = false
6956
encoder := yqlib.NewYamlEncoder(encoderPrefs)
7057
decoder := yqlib.NewYamlDecoder(yqlib.ConfiguredYamlPreferences)
71-
out, err := yqlib.NewStringEvaluator().EvaluateAll(expression, string(contentModified), encoder, decoder)
58+
out, err := yqlib.NewStringEvaluator().EvaluateAll(expression, content, encoder, decoder)
7259
if err != nil {
7360
logger := logrus.StandardLogger()
7461
for node := memory.Head(); node != nil; node = node.Next() {
@@ -90,6 +77,32 @@ func EvaluateExpression(expression string, content []byte) ([]byte, error) {
9077
entry.Debug(message)
9178
}
9279
}
80+
return "", err
81+
}
82+
return out, nil
83+
}
84+
85+
// EvaluateExpression evaluates the yq expression and returns the output formatted with yamlfmt.
86+
func EvaluateExpression(expression string, content []byte) ([]byte, error) {
87+
if expression == "" {
88+
return content, nil
89+
}
90+
formatter, err := yamlfmtBasicFormatter()
91+
if err != nil {
92+
return nil, err
93+
}
94+
// `ApplyFeatures()` is being called directly before passing content to `yqlib`.
95+
// This results in `ApplyFeatures()` being called twice with `FeatureApplyBefore`:
96+
// once here and once inside `formatter.Format`.
97+
// Currently, calling `ApplyFeatures()` with `FeatureApplyBefore` twice is not an issue,
98+
// but future changes to `yamlfmt` might cause problems if it is called twice.
99+
_, contentModified, err := formatter.Features.ApplyFeatures(context.Background(), content, yamlfmt.FeatureApplyBefore)
100+
if err != nil {
101+
return nil, err
102+
}
103+
104+
out, err := EvaluateExpressionPlain(expression, string(contentModified))
105+
if err != nil {
93106
return nil, err
94107
}
95108
return formatter.Format([]byte(out))

0 commit comments

Comments
 (0)