Skip to content

Commit

Permalink
Merge pull request #3313 from jandubois/tmpl-yq
Browse files Browse the repository at this point in the history
Create `limactl tmpl yq` command to query a template
  • Loading branch information
AkihiroSuda authored Mar 7, 2025
2 parents 9468aff + be64942 commit 88f350b
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 52 deletions.
83 changes: 67 additions & 16 deletions cmd/limactl/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/lima-vm/lima/pkg/limatmpl"
"github.com/lima-vm/lima/pkg/limayaml"
"github.com/lima-vm/lima/pkg/store/dirnames"
"github.com/lima-vm/lima/pkg/yqutil"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
Expand All @@ -34,6 +35,7 @@ func newTemplateCommand() *cobra.Command {
templateCommand.AddCommand(
newTemplateCopyCommand(),
newTemplateValidateCommand(),
newTemplateYQCommand(),
)
return templateCommand
}
Expand Down Expand Up @@ -71,7 +73,24 @@ func newTemplateCopyCommand() *cobra.Command {
return templateCopyCommand
}

func fillDefaults(tmpl *limatmpl.Template) error {
limaDir, err := dirnames.LimaDir()
if err != nil {
return err
}
// Load() will merge the template with override.yaml and default.yaml via FillDefaults().
// FillDefaults() needs the potential instance directory to validate host templates using {{.Dir}}.
filePath := filepath.Join(limaDir, tmpl.Name+".yaml")
tmpl.Config, err = limayaml.Load(tmpl.Bytes, filePath)
if err == nil {
tmpl.Bytes, err = limayaml.Marshal(tmpl.Config, false)
}
return err
}

func templateCopyAction(cmd *cobra.Command, args []string) error {
source := args[0]
target := args[1]
embed, err := cmd.Flags().GetBool("embed")
if err != nil {
return err
Expand All @@ -97,12 +116,12 @@ func templateCopyAction(cmd *cobra.Command, args []string) error {
if embed && verbatim {
return errors.New("--verbatim cannot be used with any of --embed, --embed-all, or --fill")
}
tmpl, err := limatmpl.Read(cmd.Context(), "", args[0])
tmpl, err := limatmpl.Read(cmd.Context(), "", source)
if err != nil {
return err
}
if len(tmpl.Bytes) == 0 {
return fmt.Errorf("don't know how to interpret %q as a template locator", args[0])
return fmt.Errorf("don't know how to interpret %q as a template locator", source)
}
if !verbatim {
if embed {
Expand All @@ -117,24 +136,11 @@ func templateCopyAction(cmd *cobra.Command, args []string) error {
}
}
if fill {
limaDir, err := dirnames.LimaDir()
if err != nil {
return err
}
// Load() will merge the template with override.yaml and default.yaml via FillDefaults().
// FillDefaults() needs the potential instance directory to validate host templates using {{.Dir}}.
filePath := filepath.Join(limaDir, tmpl.Name+".yaml")
tmpl.Config, err = limayaml.Load(tmpl.Bytes, filePath)
if err != nil {
return err
}
tmpl.Bytes, err = limayaml.Marshal(tmpl.Config, false)
if err != nil {
if err := fillDefaults(tmpl); err != nil {
return err
}
}
writer := cmd.OutOrStdout()
target := args[1]
if target != "-" {
file, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
if err != nil {
Expand All @@ -147,6 +153,51 @@ func templateCopyAction(cmd *cobra.Command, args []string) error {
return err
}

const templateYQHelp = `Use the builtin YQ evaluator to extract information from a template.
External references are embedded and default values are filled in
before the YQ expression is evaluated.
Example:
limactl template yq template://default '.images[].location'
The example command is equivalent to using an external yq command like this:
limactl template copy --fill template://default - | yq '.images[].location'
`

func newTemplateYQCommand() *cobra.Command {
templateYQCommand := &cobra.Command{
Use: "yq TEMPLATE EXPR",
Short: "Query template expressions",
Long: templateYQHelp,
Args: WrapArgsError(cobra.ExactArgs(2)),
RunE: templateYQAction,
}
return templateYQCommand
}

func templateYQAction(cmd *cobra.Command, args []string) error {
locator := args[0]
expr := args[1]
tmpl, err := limatmpl.Read(cmd.Context(), "", locator)
if err != nil {
return err
}
if len(tmpl.Bytes) == 0 {
return fmt.Errorf("don't know how to interpret %q as a template locator", locator)
}
if err := tmpl.Embed(cmd.Context(), true, true); err != nil {
return err
}
if err := fillDefaults(tmpl); err != nil {
return err
}
out, err := yqutil.EvaluateExpressionPlain(expr, string(tmpl.Bytes))
if err == nil {
_, err = fmt.Fprint(cmd.OutOrStdout(), out)
}
return err
}

func newTemplateValidateCommand() *cobra.Command {
templateValidateCommand := &cobra.Command{
Use: "validate TEMPLATE [TEMPLATE, ...]",
Expand Down
29 changes: 9 additions & 20 deletions hack/test-templates.sh
Original file line number Diff line number Diff line change
Expand Up @@ -102,26 +102,15 @@ if limactl ls -q | grep -q "$NAME"; then
exit 1
fi

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

if [[ -n ${CHECKS["port-forwards"]} ]]; then
tmpconfig="$HOME_HOST/lima-config-tmp"
Expand Down
45 changes: 29 additions & 16 deletions pkg/yqutil/yqutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,25 +39,12 @@ func ValidateContent(content []byte) error {
return err
}

// EvaluateExpression evaluates the yq expression and returns the modified yaml.
func EvaluateExpression(expression string, content []byte) ([]byte, error) {
// EvaluateExpressionPlain evaluates the yq expression and returns the yq result.
func EvaluateExpressionPlain(expression, content string) (string, error) {
if expression == "" {
return content, nil
}
logrus.Debugf("Evaluating yq expression: %q", expression)
formatter, err := yamlfmtBasicFormatter()
if err != nil {
return nil, err
}
// `ApplyFeatures()` is being called directly before passing content to `yqlib`.
// This results in `ApplyFeatures()` being called twice with `FeatureApplyBefore`:
// once here and once inside `formatter.Format`.
// Currently, calling `ApplyFeatures()` with `FeatureApplyBefore` twice is not an issue,
// but future changes to `yamlfmt` might cause problems if it is called twice.
_, contentModified, err := formatter.Features.ApplyFeatures(context.Background(), content, yamlfmt.FeatureApplyBefore)
if err != nil {
return nil, err
}
memory := logging.NewMemoryBackend(0)
backend := logging.AddModuleLevel(memory)
logging.SetBackend(backend)
Expand All @@ -68,7 +55,7 @@ func EvaluateExpression(expression string, content []byte) ([]byte, error) {
encoderPrefs.ColorsEnabled = false
encoder := yqlib.NewYamlEncoder(encoderPrefs)
decoder := yqlib.NewYamlDecoder(yqlib.ConfiguredYamlPreferences)
out, err := yqlib.NewStringEvaluator().EvaluateAll(expression, string(contentModified), encoder, decoder)
out, err := yqlib.NewStringEvaluator().EvaluateAll(expression, content, encoder, decoder)
if err != nil {
logger := logrus.StandardLogger()
for node := memory.Head(); node != nil; node = node.Next() {
Expand All @@ -90,6 +77,32 @@ func EvaluateExpression(expression string, content []byte) ([]byte, error) {
entry.Debug(message)
}
}
return "", err
}
return out, nil
}

// EvaluateExpression evaluates the yq expression and returns the output formatted with yamlfmt.
func EvaluateExpression(expression string, content []byte) ([]byte, error) {
if expression == "" {
return content, nil
}
formatter, err := yamlfmtBasicFormatter()
if err != nil {
return nil, err
}
// `ApplyFeatures()` is being called directly before passing content to `yqlib`.
// This results in `ApplyFeatures()` being called twice with `FeatureApplyBefore`:
// once here and once inside `formatter.Format`.
// Currently, calling `ApplyFeatures()` with `FeatureApplyBefore` twice is not an issue,
// but future changes to `yamlfmt` might cause problems if it is called twice.
_, contentModified, err := formatter.Features.ApplyFeatures(context.Background(), content, yamlfmt.FeatureApplyBefore)
if err != nil {
return nil, err
}

out, err := EvaluateExpressionPlain(expression, string(contentModified))
if err != nil {
return nil, err
}
return formatter.Format([]byte(out))
Expand Down

0 comments on commit 88f350b

Please sign in to comment.