Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading