~nesv/govern

d515fedc9a3d87d8bf7e55e8bf72ce582bdcce6e — Nick Saika 1 year, 6 months ago 1ae1b3d
govern: New "shell" command!

This is the first step in bundling a Lua interpreter into govern. <3

This will enable the writing of facts and runners in Lua, without
requiring an external Lua interpreter to be installed. Govern *is* the
interpreter.
4 files changed, 84 insertions(+), 55 deletions(-)

M .gitignore
M go.mod
M go.sum
M lua.go
M .gitignore => .gitignore +2 -1
@@ 1,4 1,5 @@
bin/**
libexec/**

# Created by https://www.toptal.com/developers/gitignore/api/go,emacs,vim,vscode
# Edit at https://www.toptal.com/developers/gitignore?templates=go,emacs,vim,vscode


@@ 104,4 105,4 @@ tags
!.vscode/extensions.json
*.code-workspace

# End of https://www.toptal.com/developers/gitignore/api/go,emacs,vim,vscode
\ No newline at end of file
# End of https://www.toptal.com/developers/gitignore/api/go,emacs,vim,vscode

M go.mod => go.mod +3 -3
@@ 3,14 3,14 @@ module git.sr.ht/~nesv/govern
go 1.19

require (
	github.com/Shopify/go-lua v0.0.0-20221004153744-91867de107cf
	github.com/go-mangos/mangos v1.2.0
	github.com/hashicorp/hcl/v2 v2.9.1
	github.com/nesv/cmndr v1.1.0
	github.com/pkg/errors v0.9.1
	github.com/rs/xid v0.0.0-20170604230408-02dd45c33376
	github.com/yuin/gopher-lua v1.1.0
	github.com/zclconf/go-cty v1.8.0
	golang.org/x/term v0.3.0
	golang.org/x/term v0.6.0
	k8s.io/apimachinery v0.20.4
)



@@ 21,6 21,6 @@ require (
	github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
	github.com/spf13/pflag v1.0.5 // indirect
	github.com/stretchr/testify v1.7.0 // indirect
	golang.org/x/sys v0.3.0 // indirect
	golang.org/x/sys v0.6.0 // indirect
	golang.org/x/text v0.3.5 // indirect
)

M go.sum => go.sum +6 -6
@@ 3,8 3,6 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/Shopify/go-lua v0.0.0-20221004153744-91867de107cf h1:VSUCKpFV0AfYGslgQSRdBI9HCVp1TCXJmScWD0VXL5g=
github.com/Shopify/go-lua v0.0.0-20221004153744-91867de107cf/go.mod h1:UCCi6u6YA8oXfJ15vEMiMGIaWWfLiOacinBPHxLXtEU=
github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM=


@@ 127,6 125,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=
github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
github.com/yuin/gopher-lua v1.1.0 h1:BojcDhfyDWgU2f2TOzYK/g5p2gxMrku8oupLDqlnSqE=
github.com/yuin/gopher-lua v1.1.0/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8=
github.com/zclconf/go-cty v1.8.0 h1:s4AvqaeQzJIu3ndv4gVIhplVD0krU+bgrcLSVUnaWuA=
github.com/zclconf/go-cty v1.8.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=


@@ 166,10 166,10 @@ golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=

M lua.go => lua.go +73 -45
@@ 1,74 1,90 @@
package main

import (
	"bufio"
	"errors"
	"fmt"
	"io"
	"os"
	"strings"

	"github.com/Shopify/go-lua"
	"github.com/nesv/cmndr"
	lua "github.com/yuin/gopher-lua"
	"golang.org/x/term"

	"git.sr.ht/~nesv/govern/internal/facts"
)

const luaShellBanner = `
   ____ _____ _   _____  _________ 
  / __  / __ \ | / / _ \/ ___/ __ \
 / /_/ / /_/ / |/ /  __/ /  / / / /
 \__, /\____/|___/\___/_/  /_/ /_/ 
/____/`

func (c *command) runLuaShell(cmd *cmndr.Cmd, args []string) error {
	if err := c.init(); err != nil {
		return err
	}

	state, err := c.initializeLuaRuntime()
	ff, err := c.collectFacts()
	if err != nil {
		return fmt.Errorf("initialize lua runtime: %w", err)
		return fmt.Errorf("collect facts: %w", err)
	}

	r := bufio.NewReader(os.Stdin)
	for {
		fmt.Fprint(os.Stderr, ":: ")
	vm := lua.NewState()
	defer vm.Close()

	vm.PreloadModule("govern", func(state *lua.LState) int {
		module := state.SetFuncs(state.NewTable(), map[string]lua.LGFunction{
			"GetFact":        getFact(ff),
			"AvailableFacts": availableFacts(ff),
		})

		line, err := r.ReadString('\n')
		state.SetField(module, "name", lua.LString("govern"))

		state.Push(module)
		return 1
	})

	fmt.Fprintln(os.Stderr, luaShellBanner)

	oldTermState, err := term.MakeRaw(int(os.Stdin.Fd()))
	if err != nil {
		return fmt.Errorf("make raw terminal: %w", err)
	}
	defer term.Restore(int(os.Stdin.Fd()), oldTermState)

	terminal := term.NewTerminal(&readwriter{
		r: os.Stdin,
		w: os.Stdout,
	}, "govern-> ")

	for {
		line, err := terminal.ReadLine()
		if errors.Is(err, io.EOF) {
			fmt.Fprintln(terminal, "arrivederci!")
			return nil
		} else if err != nil {
			fmt.Fprintln(os.Stderr, "!!", err)
			continue
		}

		if err := lua.DoString(state, strings.TrimSpace(line)); err != nil {
			fmt.Fprintln(os.Stderr, err)
			continue
		switch cmd := strings.TrimSpace(line); cmd {
		case "exit", "quit":
			fmt.Fprintln(terminal, "g'bye!")
			return nil
		}
	}
}

func (c *command) initializeLuaRuntime() (*lua.State, error) {
	state := lua.NewState()
	lua.OpenLibraries(state)
		// fmt.Fprintf(os.Stderr, "debug: command = %q\r\n", strings.TrimSpace(line))

	// Load all facts and push them into the Lua runtime's global state.
	ff, err := c.collectFacts()
	if err != nil {
		return nil, fmt.Errorf("collect facts: %w", err)
	}

	// TODO(nesv): Register custom functions.
	lua.Require(state, "govern", func(state *lua.State) int {
		lua.NewLibrary(state, []lua.RegistryFunction{
			{
				Name:     "GetFact",
				Function: getFact(ff),
			},
		})
		return 1
	}, false)

	return state, nil
}
		if err := vm.DoString(strings.TrimSpace(line)); err != nil {
			errMsg := strings.ReplaceAll(err.Error(), "\n", "\r\n")
			fmt.Fprintf(os.Stderr, "%s\r\n", errMsg)
			continue
		}

func (c *command) loadBuiltinLuaRunners(state *lua.State) error {
	return nil // TODO
		fmt.Fprint(terminal, "\r")
	}
}

func stdio() io.ReadWriter {


@@ 98,19 114,31 @@ func (rw *readwriter) Write(p []byte) (int, error) { return rw.w.Write(p) }
// Retrieve a fact.

// getFact is a function that gets loaded into the Lua runtime.
func getFact(ff *facts.Facts) lua.Function {
	return func(state *lua.State) int {
		name := lua.CheckString(state, 1)
		fv, err := ff.Get(name)
func getFact(ff *facts.Facts) lua.LGFunction {
	return func(state *lua.LState) int {
		name := state.ToString(1)
		if name == "" {
			panic("empty string")
		}

		factValue, err := ff.Get(name)
		if err != nil {
			lua.ArgumentError(state, 1, err.Error())
			panic(fmt.Sprintf("cannot find fact %q: %s", name, err))
		}

		if name == "" {
			lua.ArgumentError(state, 1, "no fact name specified")
		state.Push(lua.LString(factValue))
		return 1
	}
}

func availableFacts(ff *facts.Facts) lua.LGFunction {
	return func(ls *lua.LState) int {
		names := ls.NewTable()
		for _, name := range ff.Names() {
			names.Append(lua.LString(name))
		}

		state.PushString(fv)
		ls.Push(names)
		return 1
	}
}