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" "context"
"fmt" "fmt"
"io" "io"
"os"
"os/exec" "os/exec"
"os/user" "os/user"
"sort" "sort"
@@ -47,6 +48,7 @@ import (
"github.com/purpleidea/mgmt/engine/traits" "github.com/purpleidea/mgmt/engine/traits"
engineUtil "github.com/purpleidea/mgmt/engine/util" engineUtil "github.com/purpleidea/mgmt/engine/util"
"github.com/purpleidea/mgmt/util/errwrap" "github.com/purpleidea/mgmt/util/errwrap"
"github.com/purpleidea/mgmt/util/recwatch"
) )
func init() { func init() {
@@ -119,6 +121,13 @@ type ExecRes struct {
// IfShell is the Shell for the IfCmd. See the docs for Shell. // IfShell is the Shell for the IfCmd. See the docs for Shell.
IfShell string `lang:"ifshell" yaml:"ifshell"` 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 // DoneCmd is the command that runs after the regular Cmd runs
// successfully. This is a useful pattern to avoid the shelling out to // successfully. This is a useful pattern to avoid the shelling out to
// bash simply to do `$cmd && echo done > /tmp/donefile`. If this // 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") 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 // check that, if an user or a group is set, we're running as root
if obj.User != "" || obj.Group != "" { if obj.User != "" || obj.Group != "" {
currentUser, err := user.Current() 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. // Watch is the primary listener for this resource and it outputs events.
func (obj *ExecRes) Watch(ctx context.Context) error { func (obj *ExecRes) Watch(ctx context.Context) error {
ioChan := make(chan *cmdOutput)
defer obj.wg.Wait() defer obj.wg.Wait()
ioChan := make(chan *cmdOutput)
rwChan := make(chan recwatch.Event)
var watchCmd *exec.Cmd var watchCmd *exec.Cmd
if obj.WatchCmd != "" { if obj.WatchCmd != "" {
var cmdName string 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 obj.init.Running() // when started, notify engine that we're running
var send = false // send event? var send = false // send event?
@@ -299,6 +322,15 @@ func (obj *ExecRes) Watch(ctx context.Context) error {
send = true 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 case <-ctx.Done(): // closed by the engine to signal shutdown
return nil 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 // state is not okay, no work done, exit, but without error
if !apply { if !apply {
return false, nil return false, nil
@@ -667,6 +706,10 @@ func (obj *ExecRes) Cmp(r engine.Res) error {
return fmt.Errorf("the IfShell differs") return fmt.Errorf("the IfShell differs")
} }
if obj.Creates != res.Creates {
return fmt.Errorf("the Creates differs")
}
if obj.DoneCmd != res.DoneCmd { if obj.DoneCmd != res.DoneCmd {
return fmt.Errorf("the DoneCmd differs") return fmt.Errorf("the DoneCmd differs")
} }

View File

@@ -0,0 +1,6 @@
exec "exec0" {
cmd => "echo hello world > /tmp/whatever",
shell => "/bin/bash",
creates => "/tmp/whatever", # a watch event is taken on this file path!
}