A do-thing.lua => do-thing.lua +2 -0
@@ 0,0 1,2 @@
+cpuModel = govern:GetFact("sys/cpu/model")
+print(cpuModel)
M go.mod => go.mod +3 -0
@@ 9,7 9,9 @@ require (
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.6.0
k8s.io/apimachinery v0.20.4
)
@@ 20,5 22,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.6.0 // indirect
golang.org/x/text v0.3.5 // indirect
)
M go.sum => go.sum +6 -0
@@ 127,6 127,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,6 168,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.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 internal/facts/facts.go => internal/facts/facts.go +13 -0
@@ 172,3 172,16 @@ func (f *Facts) Get(name string) (string, error) {
p, err := io.ReadAll(fact)
return string(p), errors.Wrap(err, "read fact")
}
+
+// Names returns the names of all available facts.
+func (f *Facts) Names() []string {
+ f.mu.RLock()
+ defer f.mu.RUnlock()
+
+ names := make([]string, 0, len(f.facts))
+ for name := range f.facts {
+ names = append(names, name)
+ }
+
+ return names
+}
A lua.go => lua.go +144 -0
@@ 0,0 1,144 @@
+package main
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "strings"
+
+ "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
+ }
+
+ ff, err := c.collectFacts()
+ if err != nil {
+ return fmt.Errorf("collect facts: %w", err)
+ }
+
+ 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),
+ })
+
+ 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
+ }
+
+ switch cmd := strings.TrimSpace(line); cmd {
+ case "exit", "quit":
+ fmt.Fprintln(terminal, "g'bye!")
+ return nil
+ }
+
+ // fmt.Fprintf(os.Stderr, "debug: command = %q\r\n", strings.TrimSpace(line))
+
+ 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
+ }
+
+ fmt.Fprint(terminal, "\r")
+ }
+}
+
+func stdio() io.ReadWriter {
+ return &readwriter{r: os.Stdin, w: os.Stdout}
+}
+
+type readwriter struct {
+ r io.Reader
+ w io.Writer
+}
+
+func (rw *readwriter) Read(p []byte) (int, error) { return rw.r.Read(p) }
+func (rw *readwriter) Write(p []byte) (int, error) { return rw.w.Write(p) }
+
+// NOTE(nesv): When implementing runners in Lua,
+// maybe there shoud be functions regarding each of the supported "states"
+// (e.g. state = "installed").
+// All of the other key = value pairs in a resource block should be passed into
+// each "state function" as a table.
+
+// NOTE(nesv): Even though govern supports writing runners in any language,
+// all govern really cares about is that it's an executable file,
+// users should be able to implement their own govern-flavoured Lua runners
+// following the "state function" approach.
+
+// TODO(nesv): Custom functions.
+// Retrieve a fact.
+
+// getFact is a function that gets loaded into the Lua runtime.
+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 {
+ panic(fmt.Sprintf("cannot find fact %q: %s", name, err))
+ }
+
+ 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))
+ }
+
+ ls.Push(names)
+ return 1
+ }
+}
M main.go => main.go +4 -0
@@ 71,6 71,10 @@ func main() {
render.Description = "Render a template to STDOUT"
root.AddCmd(render)
+ shell := cmndr.New("shell", cmd.runLuaShell)
+ shell.Description = "Drop into a Lua shell"
+ root.AddCmd(shell)
+
root.Exec()
}
A share/lua/facts/hostname => share/lua/facts/hostname +6 -0
@@ 0,0 1,6 @@
+-- vim: syntax=lua:ts=4:sts=4:sw=4
+local handle = io.popen("hostname -f", "r")
+local result = string.gsub(handle:read("*a"), "[\r\n]+$", "")
+handle:close()
+
+print(result)
A share/lua/facts/os/distribution => share/lua/facts/os/distribution +13 -0
@@ 0,0 1,13 @@
+-- vim: syntax=lua:ts=4:sts=4:sw=4
+local handle = io.open("/etc/os-release", "r")
+ if handle == nil then
+ io.close(handle)
+os.exit(false)
+ end
+
+ for line in handle:lines(1) do
+print(line)
+ end
+
+ local release = string.gsub(handle:read("*a"), "[\r\n]+$", "")
+print(release)