engine: resources: Add a creates field for exec

This adds a standard gate that prevents execution if a file exists. Of
note, this also adds a watch on it, so we can have a proper watched exec
resource without a watch cmd.
This commit is contained in:
James Shubin
2024-03-16 01:04:59 -04:00
parent 4d18044851
commit 849de648f0
2 changed files with 50 additions and 1 deletions

View File

@@ -35,6 +35,7 @@ import (
"context"
"fmt"
"io"
"os"
"os/exec"
"os/user"
"sort"
@@ -47,6 +48,7 @@ import (
"github.com/purpleidea/mgmt/engine/traits"
engineUtil "github.com/purpleidea/mgmt/engine/util"
"github.com/purpleidea/mgmt/util/errwrap"
"github.com/purpleidea/mgmt/util/recwatch"
)
func init() {
@@ -119,6 +121,13 @@ type ExecRes struct {
// IfShell is the Shell for the IfCmd. See the docs for Shell.
IfShell string `lang:"ifshell" yaml:"ifshell"`
// Creates is the absolute file path to check for before running the
// main cmd. If this path exists, then the cmd will not run. More
// precisely we attempt to `stat` the file, so it must succeed for a
// skip. This also adds a watch on this path which re-checks things when
// it changes.
Creates string `lang:"creates" yaml:"creates"`
// DoneCmd is the command that runs after the regular Cmd runs
// successfully. This is a useful pattern to avoid the shelling out to
// bash simply to do `$cmd && echo done > /tmp/donefile`. If this
@@ -175,6 +184,10 @@ func (obj *ExecRes) Validate() error {
return fmt.Errorf("the Args param can't be used when Cmd has args")
}
if obj.Creates != "" && !strings.HasPrefix(obj.Creates, "/") {
return fmt.Errorf("the Creates param must be an absolute path")
}
// check that, if an user or a group is set, we're running as root
if obj.User != "" || obj.Group != "" {
currentUser, err := user.Current()
@@ -212,9 +225,10 @@ func (obj *ExecRes) Cleanup() error {
// Watch is the primary listener for this resource and it outputs events.
func (obj *ExecRes) Watch(ctx context.Context) error {
ioChan := make(chan *cmdOutput)
defer obj.wg.Wait()
ioChan := make(chan *cmdOutput)
rwChan := make(chan recwatch.Event)
var watchCmd *exec.Cmd
if obj.WatchCmd != "" {
var cmdName string
@@ -254,6 +268,15 @@ func (obj *ExecRes) Watch(ctx context.Context) error {
}
}
if obj.Creates != "" {
recWatcher, err := recwatch.NewRecWatcher(obj.Creates, false)
if err != nil {
return err
}
defer recWatcher.Close()
rwChan = recWatcher.Events()
}
obj.init.Running() // when started, notify engine that we're running
var send = false // send event?
@@ -299,6 +322,15 @@ func (obj *ExecRes) Watch(ctx context.Context) error {
send = true
}
case event, ok := <-rwChan:
if !ok { // channel shutdown
return fmt.Errorf("unexpected recwatch shutdown")
}
if err := event.Error; err != nil {
return errwrap.Wrapf(err, "unknown %s watcher error", obj)
}
send = true
case <-ctx.Done(): // closed by the engine to signal shutdown
return nil
}
@@ -388,6 +420,13 @@ func (obj *ExecRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
}
}
if obj.Creates != "" { // gate the extra syscall
if _, err := os.Stat(obj.Creates); err == nil {
obj.init.Logf("creates file exists, skipping cmd")
return true, nil // don't run
}
}
// state is not okay, no work done, exit, but without error
if !apply {
return false, nil
@@ -667,6 +706,10 @@ func (obj *ExecRes) Cmp(r engine.Res) error {
return fmt.Errorf("the IfShell differs")
}
if obj.Creates != res.Creates {
return fmt.Errorf("the Creates differs")
}
if obj.DoneCmd != res.DoneCmd {
return fmt.Errorf("the DoneCmd differs")
}