diff --git a/cmd/limactl/template.go b/cmd/limactl/template.go index 996aed59e72..1444ac2fbe3 100644 --- a/cmd/limactl/template.go +++ b/cmd/limactl/template.go @@ -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" ) @@ -34,6 +35,7 @@ func newTemplateCommand() *cobra.Command { templateCommand.AddCommand( newTemplateCopyCommand(), newTemplateValidateCommand(), + newTemplateYQCommand(), ) return templateCommand } @@ -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 @@ -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 { @@ -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 { @@ -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, ...]", diff --git a/hack/test-templates.sh b/hack/test-templates.sh index 0c32d762be2..c5ddc4a0f63 100755 --- a/hack/test-templates.sh +++ b/hack/test-templates.sh @@ -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" diff --git a/pkg/yqutil/yqutil.go b/pkg/yqutil/yqutil.go index 64c83b575e0..0ee68b831fb 100644 --- a/pkg/yqutil/yqutil.go +++ b/pkg/yqutil/yqutil.go @@ -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) @@ -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() { @@ -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))