~nesv/govern

669793bd19a388a834fec30944e1460d3bfc586a — Nick Saika 1 year, 6 months ago 923d1d0 + d515fed
Merge branch 'lua'
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)