@@ 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)