From 849de648f050ebbe50ea2d8cc7a86e09c71d7e4f Mon Sep 17 00:00:00 2001 From: James Shubin Date: Sat, 16 Mar 2024 01:04:59 -0400 Subject: [PATCH] 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. --- engine/resources/exec.go | 45 +++++++++++++++++++++++++++++++++- examples/lang/exec-creates.mcl | 6 +++++ 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 examples/lang/exec-creates.mcl diff --git a/engine/resources/exec.go b/engine/resources/exec.go index 9a529d4b..0dd562f7 100644 --- a/engine/resources/exec.go +++ b/engine/resources/exec.go @@ -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") } diff --git a/examples/lang/exec-creates.mcl b/examples/lang/exec-creates.mcl new file mode 100644 index 00000000..16f031e4 --- /dev/null +++ b/examples/lang/exec-creates.mcl @@ -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! +}