~nesv/govern

e7e6910384ac48c2e73e3f95826f9c064d0be450 — Nick Saika 1 year, 3 months ago 7fd852e
runner: Require runners to specify which facts they plan on getting

To take a little inspiration from OpenBSD's pledge(2) and unveil(2)
syscalls, this change requires runners to explicitly state which facts
they plan on calling, before they call them. Failure to do so will
result in an error when the runner executes.
1 files changed, 80 insertions(+), 11 deletions(-)

M runner/runner.go
M runner/runner.go => runner/runner.go +80 -11
@@ 34,6 34,12 @@ type Runner struct {
	// Note that govern does not make any impositions on what a "valid"
	// state name is.
	States map[string]StateFunc

	// A listing of facts runners intend to use, and require to be
	// available on the underlying system.
	// When Runner.Exec is called, each of these facts will be checked
	// to ensure they exist.
	RequiredFacts []string
}

// StateFunc defines a function type that is responsible for ensuring a


@@ 63,10 69,21 @@ func (r *Runner) Exec() error {

// ExecArgs executes the runner with the resource name and any arguments.
func (r *Runner) ExecArgs(name string, args ...string) error {
	if len(r.States) == 0{
	if len(r.States) == 0 {
		return errors.New("no state handlers registered")
	}

	// Make sure all of our required facts exist.
	for _, name := range r.RequiredFacts {
		exists, err := requireFact(name)
		if err != nil {
			return fmt.Errorf("require fact %q: %w", name, err)
		}
		if !exists {
			return fmt.Errorf("required fact is not available: %s", name)
		}
	}

	// Parse the command-line args into attributes.
	// They are expected to be "key=value" pairs.
	attrs := make(map[string]string)


@@ 80,10 97,10 @@ func (r *Runner) ExecArgs(name string, args ...string) error {

	state, ok := attrs["state"]
	if !ok {
		state=r.DefaultState
		state = r.DefaultState
	}

	handler, ok  := r.States[state]
	handler, ok := r.States[state]
	if !ok {
		return fmt.Errorf("no handler registered for state: %s", state)
	}


@@ 91,12 108,58 @@ func (r *Runner) ExecArgs(name string, args ...string) error {
	return handler(r, name, attrs)
}

func requireFact(name string) (exists bool, err error) {
	factsPath := os.Getenv("GOVERN_FACTS_PATH")
	if factsPath == "" {
		return false, errors.New("GOVERN_FACTS_PATH environment variable is not set")
	}

	for _, dir := range strings.Split(factsPath, ":") {
		fpath := filepath.Join(dir, name)

		// Stat the path.
		// If the path does not exist, loop around again, and try the
		// next directory in the list.
		info, err := os.Stat(fpath)
		if errors.Is(err, fs.ErrNotExist) {
			continue
		} else if err != nil {
			return false, err
		}

		// Error out if the given fact name points at a directory.
		if info.IsDir() {
			return false, errors.New("refers to a directory")
		}

		return true, nil
	}

	return false, nil
}

// Fact retrieves the fact with the given name,
// by invoking the govern binary provided in the GOVERN environment variable.
// Fact requires the GOVERN,
// and GOVERN_FACTS_PATH
// to be set in the environment.
func (r *Runner) Fact(name string) (string, error) {
	var found bool
	for _, v := range r.RequiredFacts {
		if name == v {
			found = true
			break
		}
	}

	if !found {
		return "", fmt.Errorf("fact was not declared as required: %s", name)
	}

	return getFact(name)
}

func getFact(name string) (string, error) {
	// Make sure the GOVERN environment variable is set,
	// clean the file path,
	// and make sure the path we were given exists and is executable.


@@ 136,18 199,24 @@ func (r *Runner) Fact(name string) (string, error) {
		return "", err
	}

	return r.parseFact(name, output)
	return parseFact(name, output)
}

func (r *Runner) parseFact(name string, output []byte) (string, error) {
func parseFact(name string, output []byte) (string, error) {
	needle := []byte(name)

	for _, line := range bytes.Split(output, []byte("\n")) {
		if bytes.HasPrefix(line, []byte(name)) {
			var (
				fields = bytes.Fields(line)
				value  = bytes.Join(fields[1:], []byte(" "))
			)
			return string(value), nil
		if !bytes.HasPrefix(line, needle) {
			continue
		}

		fields := bytes.Fields(line)
		if !bytes.Equal(needle, fields[0]) {
			continue
		}

		return string(bytes.Join(fields[1:], []byte(" "))), nil

	}

	return "", fmt.Errorf("no such fact: %s", name)