engine: Fix up some send/recv corner cases
Initially I wasn't 100% clear or decided on the send/recv semantics. After some experimenting, I think this is much closer to what we want. Nothing should break or regress here, this only enables more possibilities.
This commit is contained in:
@@ -688,8 +688,41 @@ if val, exists := obj.init.Recv()["some_key"]; exists {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The specifics of resource sending are not currently documented. Please send a
|
A resource can send a value during CheckApply by running the `obj.init.Send()`
|
||||||
patch here!
|
method. It must always send a value if (1) it's not erroring in CheckApply, and
|
||||||
|
(2) if the `obj.SendActive()` method inside of CheckApply returns true. It is
|
||||||
|
not harmful to run the Send method if CheckApply is going to error, or if
|
||||||
|
`obj.SendActive()` returns false, just unnecessary. In the `!apply` case where
|
||||||
|
we're running in "noop" mode, and where the state is not correct, then you
|
||||||
|
should still attempt to send a value, but it is a bit ambiguous which value to
|
||||||
|
send. This behaviour may be specified in the future, but at the moment it's
|
||||||
|
mostly inconsequential. At the moment, `obj.SendActive()` is disabled at compile
|
||||||
|
time, but can be enabled if you have a legitimate use-case for it.
|
||||||
|
|
||||||
|
```golang
|
||||||
|
// inside CheckApply, somewhere near the end usually
|
||||||
|
if err := obj.init.Send(&ExecSends{ // send the special data structure
|
||||||
|
Output: obj.output,
|
||||||
|
Stdout: obj.stdout,
|
||||||
|
Stderr: obj.stderr,
|
||||||
|
}); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You must also implement the `Sends()` method which should return the above
|
||||||
|
sending struct with all of the fields containing their default or values. Please
|
||||||
|
note, that those fields must have their struct tags set appropriately.
|
||||||
|
|
||||||
|
### Safety
|
||||||
|
|
||||||
|
Lastly, please note that in order for a resource to send a useful value, even
|
||||||
|
when its state is already correct (it may have run earlier for example) then it
|
||||||
|
may require the implementation of CheckApply to cache a return value for later
|
||||||
|
use. Keep in mind that you should store this securely should there be a chance
|
||||||
|
that sensitive info is contained within, and that an untrusted user could put
|
||||||
|
malicious data in the cache if you are not careful. It's best to make sure the
|
||||||
|
users of your resource are aware of its implementation details here.
|
||||||
|
|
||||||
## Composite resources
|
## Composite resources
|
||||||
|
|
||||||
|
|||||||
@@ -128,6 +128,21 @@ func SendRecv(res engine.RecvableRes, fn RecvFn) (map[engine.RecvableRes]map[str
|
|||||||
}
|
}
|
||||||
|
|
||||||
if st == nil {
|
if st == nil {
|
||||||
|
// This can happen if there is a send->recv between two
|
||||||
|
// resources where the producer does not send a value.
|
||||||
|
// This can happen for a few reasons. (1) If the
|
||||||
|
// programmer made a mistake and has a non-erroring
|
||||||
|
// CheckApply without a return. Note that it should send
|
||||||
|
// a value for the (true, nil) CheckApply cases too.
|
||||||
|
// (2) If the resource that's sending started off in the
|
||||||
|
// "good" state right at first run, and never produced a
|
||||||
|
// value to send. This may be a programming error since
|
||||||
|
// the implementation must always either produce a value
|
||||||
|
// or be okay that there's an error. It could be a valid
|
||||||
|
// error if the resource was intended to not be run in a
|
||||||
|
// way where it wouldn't initially have a value to send,
|
||||||
|
// whether cached or otherwise, but this scenario should
|
||||||
|
// be rare.
|
||||||
e := fmt.Errorf("received nil value from: %s", v.Res)
|
e := fmt.Errorf("received nil value from: %s", v.Res)
|
||||||
err = errwrap.Append(err, e) // list of errors
|
err = errwrap.Append(err, e) // list of errors
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -159,7 +159,6 @@ var AwsRegions = []string{
|
|||||||
// http://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html
|
// http://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html
|
||||||
type AwsEc2Res struct {
|
type AwsEc2Res struct {
|
||||||
traits.Base // add the base methods without re-implementation
|
traits.Base // add the base methods without re-implementation
|
||||||
traits.Sendable
|
|
||||||
|
|
||||||
init *engine.Init
|
init *engine.Init
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"os/user"
|
"os/user"
|
||||||
|
"path"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -161,10 +162,28 @@ type ExecRes struct {
|
|||||||
// used for any command being run.
|
// used for any command being run.
|
||||||
Group string `lang:"group" yaml:"group"`
|
Group string `lang:"group" yaml:"group"`
|
||||||
|
|
||||||
|
// SendOutput is a value which can be sent for the Send/Recv Output
|
||||||
|
// field if no value is available in the cache. This is used in very
|
||||||
|
// specialized scenarios (particularly prototyping and unclean
|
||||||
|
// environments) and should not be used routinely. It should be used
|
||||||
|
// only in situations where we didn't produce our own sending values,
|
||||||
|
// and there are none in the cache, and instead are relying on a runtime
|
||||||
|
// mechanism to help us out. This can commonly occur if you wish to make
|
||||||
|
// incremental progress when locally testing some code using Send/Recv,
|
||||||
|
// but you are combining it with --tmp-prefix for other reasons.
|
||||||
|
SendOutput *string `lang:"send_output" yaml:"send_output"`
|
||||||
|
|
||||||
|
// SendStdout is like SendOutput but for stdout alone. See those docs.
|
||||||
|
SendStdout *string `lang:"send_stdout" yaml:"send_stdout"`
|
||||||
|
|
||||||
|
// SendStderr is like SendOutput but for stderr alone. See those docs.
|
||||||
|
SendStderr *string `lang:"send_stderr" yaml:"send_stderr"`
|
||||||
|
|
||||||
output *string // all cmd output, read only, do not set!
|
output *string // all cmd output, read only, do not set!
|
||||||
stdout *string // the cmd stdout, read only, do not set!
|
stdout *string // the cmd stdout, read only, do not set!
|
||||||
stderr *string // the cmd stderr, read only, do not set!
|
stderr *string // the cmd stderr, read only, do not set!
|
||||||
|
|
||||||
|
dir string // the path to local storage
|
||||||
interruptChan chan struct{}
|
interruptChan chan struct{}
|
||||||
wg *sync.WaitGroup
|
wg *sync.WaitGroup
|
||||||
}
|
}
|
||||||
@@ -225,6 +244,12 @@ func (obj *ExecRes) Validate() error {
|
|||||||
func (obj *ExecRes) Init(init *engine.Init) error {
|
func (obj *ExecRes) Init(init *engine.Init) error {
|
||||||
obj.init = init // save for later
|
obj.init = init // save for later
|
||||||
|
|
||||||
|
dir, err := obj.init.VarDir("")
|
||||||
|
if err != nil {
|
||||||
|
return errwrap.Wrapf(err, "could not get VarDir in Init()")
|
||||||
|
}
|
||||||
|
obj.dir = dir
|
||||||
|
|
||||||
obj.interruptChan = make(chan struct{})
|
obj.interruptChan = make(chan struct{})
|
||||||
obj.wg = &sync.WaitGroup{}
|
obj.wg = &sync.WaitGroup{}
|
||||||
|
|
||||||
@@ -364,6 +389,10 @@ func (obj *ExecRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
|
|||||||
// check and this will run. It is still guarded by the IfCmd, but it can
|
// check and this will run. It is still guarded by the IfCmd, but it can
|
||||||
// have a chance to execute, and all without the check of obj.Refresh()!
|
// have a chance to execute, and all without the check of obj.Refresh()!
|
||||||
|
|
||||||
|
if err := obj.checkApplyReadCache(); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
if obj.IfCmd != "" { // if there is no onlyif check, we should just run
|
if obj.IfCmd != "" { // if there is no onlyif check, we should just run
|
||||||
var cmdName string
|
var cmdName string
|
||||||
var cmdArgs []string
|
var cmdArgs []string
|
||||||
@@ -423,6 +452,13 @@ func (obj *ExecRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
|
|||||||
obj.init.Logf("ifcmd out:")
|
obj.init.Logf("ifcmd out:")
|
||||||
obj.init.Logf("%s", s)
|
obj.init.Logf("%s", s)
|
||||||
}
|
}
|
||||||
|
//if err := obj.checkApplyWriteCache(); err != nil {
|
||||||
|
// return false, err
|
||||||
|
//}
|
||||||
|
obj.safety()
|
||||||
|
if err := obj.send(); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
return true, nil // don't run
|
return true, nil // don't run
|
||||||
}
|
}
|
||||||
if s := out.String(); s == "" {
|
if s := out.String(); s == "" {
|
||||||
@@ -436,12 +472,26 @@ func (obj *ExecRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
|
|||||||
if obj.Creates != "" { // gate the extra syscall
|
if obj.Creates != "" { // gate the extra syscall
|
||||||
if _, err := os.Stat(obj.Creates); err == nil {
|
if _, err := os.Stat(obj.Creates); err == nil {
|
||||||
obj.init.Logf("creates file exists, skipping cmd")
|
obj.init.Logf("creates file exists, skipping cmd")
|
||||||
|
//if err := obj.checkApplyWriteCache(); err != nil {
|
||||||
|
// return false, err
|
||||||
|
//}
|
||||||
|
obj.safety()
|
||||||
|
if err := obj.send(); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
return true, nil // don't run
|
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 {
|
||||||
|
//if err := obj.checkApplyWriteCache(); err != nil {
|
||||||
|
// return false, err
|
||||||
|
//}
|
||||||
|
//obj.safety()
|
||||||
|
if err := obj.send(); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -654,11 +704,10 @@ func (obj *ExecRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := obj.init.Send(&ExecSends{
|
if err := obj.checkApplyWriteCache(); err != nil {
|
||||||
Output: obj.output,
|
return false, err
|
||||||
Stdout: obj.stdout,
|
}
|
||||||
Stderr: obj.stderr,
|
if err := obj.send(); err != nil {
|
||||||
}); err != nil {
|
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -670,6 +719,77 @@ func (obj *ExecRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
|
|||||||
return false, nil // success
|
return false, nil // success
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// send is a helper to avoid duplication of the same send operation.
|
||||||
|
func (obj *ExecRes) send() error {
|
||||||
|
return obj.init.Send(&ExecSends{
|
||||||
|
Output: obj.output,
|
||||||
|
Stdout: obj.stdout,
|
||||||
|
Stderr: obj.stderr,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// safety is a helper function that populates the cached "send" values if they
|
||||||
|
// are empty. It must only be called right before actually sending any values,
|
||||||
|
// and right before CheckApply returns. It should be used only in situations
|
||||||
|
// where we didn't produce our own sending values, and there are none in the
|
||||||
|
// cache, and instead are relying on a runtime mechanism to help us out. This
|
||||||
|
// mechanism is useful as a backstop for when we're running in unclean
|
||||||
|
// scenarios.
|
||||||
|
func (obj *ExecRes) safety() {
|
||||||
|
if x := obj.SendOutput; x != nil && obj.output == nil {
|
||||||
|
s := *x // copy
|
||||||
|
obj.output = &s
|
||||||
|
}
|
||||||
|
if x := obj.SendStdout; x != nil && obj.stdout == nil {
|
||||||
|
s := *x // copy
|
||||||
|
obj.stdout = &s
|
||||||
|
}
|
||||||
|
if x := obj.SendStderr; x != nil && obj.stderr == nil {
|
||||||
|
s := *x // copy
|
||||||
|
obj.stderr = &s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkApplyReadCache is a helper to do all our reading from the cache.
|
||||||
|
func (obj *ExecRes) checkApplyReadCache() error {
|
||||||
|
output, err := engineUtil.ReadData(path.Join(obj.dir, "output"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
obj.output = output
|
||||||
|
|
||||||
|
stdout, err := engineUtil.ReadData(path.Join(obj.dir, "stdout"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
obj.stdout = stdout
|
||||||
|
|
||||||
|
stderr, err := engineUtil.ReadData(path.Join(obj.dir, "stderr"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
obj.stderr = stderr
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkApplyWriteCache is a helper to do all our writing into the cache.
|
||||||
|
func (obj *ExecRes) checkApplyWriteCache() error {
|
||||||
|
if _, err := engineUtil.WriteData(path.Join(obj.dir, "output"), obj.output); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := engineUtil.WriteData(path.Join(obj.dir, "stdout"), obj.stdout); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := engineUtil.WriteData(path.Join(obj.dir, "stderr"), obj.stderr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||||
func (obj *ExecRes) Cmp(r engine.Res) error {
|
func (obj *ExecRes) Cmp(r engine.Res) error {
|
||||||
// we can only compare ExecRes to others of the same resource kind
|
// we can only compare ExecRes to others of the same resource kind
|
||||||
@@ -740,6 +860,16 @@ func (obj *ExecRes) Cmp(r engine.Res) error {
|
|||||||
return fmt.Errorf("the Group differs")
|
return fmt.Errorf("the Group differs")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := engineUtil.StrPtrCmp(obj.SendOutput, res.SendOutput); err != nil {
|
||||||
|
return errwrap.Wrapf(err, "the SendOutput differs")
|
||||||
|
}
|
||||||
|
if err := engineUtil.StrPtrCmp(obj.SendStdout, res.SendStdout); err != nil {
|
||||||
|
return errwrap.Wrapf(err, "the SendStdout differs")
|
||||||
|
}
|
||||||
|
if err := engineUtil.StrPtrCmp(obj.SendStderr, res.SendStderr); err != nil {
|
||||||
|
return errwrap.Wrapf(err, "the SendStderr differs")
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -46,7 +47,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func fakeExecInit(t *testing.T) (*engine.Init, *ExecSends) {
|
func fakeExecInit(t *testing.T) (*engine.Init, *ExecSends) {
|
||||||
debug := testing.Verbose() // set via the -test.v flag to `go test`
|
tmpdir := fmt.Sprintf("%s/", t.TempDir()) // gets cleaned up at end, new dir for each call
|
||||||
|
debug := testing.Verbose() // set via the -test.v flag to `go test`
|
||||||
logf := func(format string, v ...interface{}) {
|
logf := func(format string, v ...interface{}) {
|
||||||
t.Logf("test: "+format, v...)
|
t.Logf("test: "+format, v...)
|
||||||
}
|
}
|
||||||
@@ -60,6 +62,9 @@ func fakeExecInit(t *testing.T) (*engine.Init, *ExecSends) {
|
|||||||
*execSends = *x // set
|
*execSends = *x // set
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
VarDir: func(p string) (string, error) {
|
||||||
|
return path.Join(tmpdir, p), nil
|
||||||
|
},
|
||||||
Debug: debug,
|
Debug: debug,
|
||||||
Logf: logf,
|
Logf: logf,
|
||||||
}, execSends
|
}, execSends
|
||||||
|
|||||||
@@ -1373,6 +1373,7 @@ func (obj *FileRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
|
|||||||
// might not have a new value to copy, and therefore we won't see this
|
// might not have a new value to copy, and therefore we won't see this
|
||||||
// notification of change. Therefore, it is important to process these
|
// notification of change. Therefore, it is important to process these
|
||||||
// promptly, if they must not be lost, such as for cache invalidation.
|
// promptly, if they must not be lost, such as for cache invalidation.
|
||||||
|
// NOTE: Modern send/recv doesn't really have this limitation anymore.
|
||||||
if val, exists := obj.init.Recv()["content"]; exists && val.Changed {
|
if val, exists := obj.init.Recv()["content"]; exists && val.Changed {
|
||||||
// if we received on Content, and it changed, invalidate the cache!
|
// if we received on Content, and it changed, invalidate the cache!
|
||||||
obj.init.Logf("contentCheckApply: invalidating sha256sum of `content`")
|
obj.init.Logf("contentCheckApply: invalidating sha256sum of `content`")
|
||||||
|
|||||||
@@ -89,7 +89,6 @@ type HTTPProxyRes struct {
|
|||||||
traits.Base // add the base methods without re-implementation
|
traits.Base // add the base methods without re-implementation
|
||||||
traits.Edgeable // XXX: add autoedge support
|
traits.Edgeable // XXX: add autoedge support
|
||||||
traits.Groupable // can be grouped into HTTPServerRes
|
traits.Groupable // can be grouped into HTTPServerRes
|
||||||
traits.Sendable
|
|
||||||
|
|
||||||
init *engine.Init
|
init *engine.Init
|
||||||
|
|
||||||
@@ -520,20 +519,6 @@ func (obj *HTTPProxyRes) Cmp(r engine.Res) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTPProxySends is the struct of data which is sent after a successful Apply.
|
|
||||||
type HTTPProxySends struct {
|
|
||||||
// Data is the received value being sent.
|
|
||||||
// TODO: should this be []byte or *[]byte instead?
|
|
||||||
Data *string `lang:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sends represents the default struct of values we can send using Send/Recv.
|
|
||||||
func (obj *HTTPProxyRes) Sends() interface{} {
|
|
||||||
return &HTTPProxySends{
|
|
||||||
Data: nil,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||||
// primarily useful for setting the defaults.
|
// primarily useful for setting the defaults.
|
||||||
func (obj *HTTPProxyRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
func (obj *HTTPProxyRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
|
|||||||
@@ -465,10 +465,20 @@ func (obj *HTTPUIInputRes) valueCheckApply(ctx context.Context, apply bool) (boo
|
|||||||
obj.mutex.Unlock()
|
obj.mutex.Unlock()
|
||||||
|
|
||||||
if obj.last != nil && *obj.last == value {
|
if obj.last != nil && *obj.last == value {
|
||||||
|
if err := obj.init.Send(&HTTPUIInputSends{
|
||||||
|
Value: &value,
|
||||||
|
}); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
return true, nil // expected value has already been sent
|
return true, nil // expected value has already been sent
|
||||||
}
|
}
|
||||||
|
|
||||||
if !apply { // XXX: does this break send/recv if we end early?
|
if !apply {
|
||||||
|
if err := obj.init.Send(&HTTPUIInputSends{
|
||||||
|
Value: &value, // XXX: arbitrary since we're in noop mode
|
||||||
|
}); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -509,10 +519,20 @@ func (obj *HTTPUIInputRes) storeCheckApply(ctx context.Context, apply bool) (boo
|
|||||||
obj.mutex.Unlock()
|
obj.mutex.Unlock()
|
||||||
|
|
||||||
if exists && v1 == v2 { // both sides are happy
|
if exists && v1 == v2 { // both sides are happy
|
||||||
|
if err := obj.init.Send(&HTTPUIInputSends{
|
||||||
|
Value: &v2,
|
||||||
|
}); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !apply { // XXX: does this break send/recv if we end early?
|
if !apply {
|
||||||
|
if err := obj.init.Send(&HTTPUIInputSends{
|
||||||
|
Value: &v2, // XXX: arbitrary since we're in noop mode
|
||||||
|
}); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import (
|
|||||||
|
|
||||||
"github.com/purpleidea/mgmt/engine"
|
"github.com/purpleidea/mgmt/engine"
|
||||||
"github.com/purpleidea/mgmt/engine/traits"
|
"github.com/purpleidea/mgmt/engine/traits"
|
||||||
|
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"
|
"github.com/purpleidea/mgmt/util/recwatch"
|
||||||
)
|
)
|
||||||
@@ -115,6 +116,8 @@ func (obj *PasswordRes) Cleanup() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// read is a helper to read the data from disk. This is similar to an engineUtil
|
||||||
|
// function named ReadData but is kept separate for safety anyways.
|
||||||
func (obj *PasswordRes) read() (string, error) {
|
func (obj *PasswordRes) read() (string, error) {
|
||||||
file, err := os.Open(obj.path) // open a handle to read the file
|
file, err := os.Open(obj.path) // open a handle to read the file
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -128,14 +131,28 @@ func (obj *PasswordRes) read() (string, error) {
|
|||||||
return strings.TrimSpace(string(data)), nil
|
return strings.TrimSpace(string(data)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// write is a helper to store the data on disk. This is similar to an engineUtil
|
||||||
|
// function named WriteData but is kept separate for safety anyways.
|
||||||
func (obj *PasswordRes) write(password string) (int, error) {
|
func (obj *PasswordRes) write(password string) (int, error) {
|
||||||
file, err := os.Create(obj.path) // open a handle to create the file
|
uid, gid, err := engineUtil.GetUIDGID()
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chmod it before we write the secret data.
|
||||||
|
file, err := os.OpenFile(obj.path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return -1, errwrap.Wrapf(err, "can't create file")
|
return -1, errwrap.Wrapf(err, "can't create file")
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
var c int
|
|
||||||
if c, err = file.Write([]byte(password + newline)); err != nil {
|
// Chown it before we write the secret data.
|
||||||
|
if err := file.Chown(uid, gid); err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := file.Write([]byte(password + newline))
|
||||||
|
if err != nil {
|
||||||
return c, errwrap.Wrapf(err, "can't write file")
|
return c, errwrap.Wrapf(err, "can't write file")
|
||||||
}
|
}
|
||||||
return c, file.Sync()
|
return c, file.Sync()
|
||||||
@@ -269,11 +286,21 @@ func (obj *PasswordRes) CheckApply(ctx context.Context, apply bool) (bool, error
|
|||||||
//}
|
//}
|
||||||
|
|
||||||
if !refresh && exists && !generate && !write { // nothing to do, done!
|
if !refresh && exists && !generate && !write { // nothing to do, done!
|
||||||
|
if err := obj.init.Send(&PasswordSends{
|
||||||
|
Password: &password,
|
||||||
|
}); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
// a refresh was requested, the token doesn't exist, or the check failed
|
// a refresh was requested, the token doesn't exist, or the check failed
|
||||||
|
|
||||||
if !apply {
|
if !apply {
|
||||||
|
if err := obj.init.Send(&PasswordSends{
|
||||||
|
Password: &password, // XXX: arbitrary since we're in noop mode
|
||||||
|
}); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -501,7 +501,8 @@ func TestResources1(t *testing.T) {
|
|||||||
doneCtx, doneCtxCancel := context.WithCancel(context.Background())
|
doneCtx, doneCtxCancel := context.WithCancel(context.Background())
|
||||||
defer doneCtxCancel()
|
defer doneCtxCancel()
|
||||||
|
|
||||||
debug := testing.Verbose() // set via the -test.v flag to `go test`
|
tmpdir := fmt.Sprintf("%s/", t.TempDir()) // gets cleaned up at end, new dir for each call
|
||||||
|
debug := testing.Verbose() // set via the -test.v flag to `go test`
|
||||||
logf := func(format string, v ...interface{}) {
|
logf := func(format string, v ...interface{}) {
|
||||||
t.Logf(fmt.Sprintf("test #%d: ", index)+format, v...)
|
t.Logf(fmt.Sprintf("test #%d: ", index)+format, v...)
|
||||||
}
|
}
|
||||||
@@ -521,6 +522,10 @@ func TestResources1(t *testing.T) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
VarDir: func(p string) (string, error) {
|
||||||
|
return path.Join(tmpdir, p), nil
|
||||||
|
},
|
||||||
|
|
||||||
// Watch listens on this for close/pause events.
|
// Watch listens on this for close/pause events.
|
||||||
Debug: debug,
|
Debug: debug,
|
||||||
Logf: logf,
|
Logf: logf,
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ func (obj *ValueRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
|
|||||||
// might not have a new value to copy, and therefore we won't see this
|
// might not have a new value to copy, and therefore we won't see this
|
||||||
// notification of change. Therefore, it is important to process these
|
// notification of change. Therefore, it is important to process these
|
||||||
// promptly, if they must not be lost, such as for cache invalidation.
|
// promptly, if they must not be lost, such as for cache invalidation.
|
||||||
|
// NOTE: Modern send/recv doesn't really have this limitation anymore.
|
||||||
if !obj.isSet {
|
if !obj.isSet {
|
||||||
obj.cachedAny = obj.Any // store anything we have if any
|
obj.cachedAny = obj.Any // store anything we have if any
|
||||||
}
|
}
|
||||||
@@ -173,7 +174,12 @@ func (obj *ValueRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
|
|||||||
checkOK = true
|
checkOK = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if !apply { // XXX: does this break send/recv if we end early?
|
if !apply {
|
||||||
|
if err := obj.init.Send(&ValueSends{
|
||||||
|
Any: obj.cachedAny,
|
||||||
|
}); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
return checkOK, nil
|
return checkOK, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,7 +197,7 @@ func (obj *ValueRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// send
|
// send
|
||||||
//if obj.cachedAny != nil { // TODO: okay to send if value got removed too?
|
//if obj.cachedAny != nil { // XXX: okay to send if value got removed too?
|
||||||
if err := obj.init.Send(&ValueSends{
|
if err := obj.init.Send(&ValueSends{
|
||||||
Any: obj.cachedAny,
|
Any: obj.cachedAny,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
|
|||||||
@@ -56,6 +56,28 @@ type SendableRes interface {
|
|||||||
|
|
||||||
// Sent returns the most recently sent data. This is used by the engine.
|
// Sent returns the most recently sent data. This is used by the engine.
|
||||||
Sent() interface{}
|
Sent() interface{}
|
||||||
|
|
||||||
|
// SendActive let's the resource know if it must send a value. This is
|
||||||
|
// usually called during CheckApply, but it's probably safe to check it
|
||||||
|
// during Init as well.
|
||||||
|
// XXX: Not doing this for now. If a send/recv edge wasn't initially on,
|
||||||
|
// and the sender ran CheckApply, but didn't cache the value to send,
|
||||||
|
// and then the edge flipped on, we'd have to error. Better to always
|
||||||
|
// generate the cache, and only consider adding this if we have a more
|
||||||
|
// important privacy or performance situation that requires it.
|
||||||
|
//SendActive() bool
|
||||||
|
|
||||||
|
// SendSetActive is used by the compiler to store the "SendActive" bool
|
||||||
|
// so that it will later know if it will need to send or not. Only the
|
||||||
|
// engine should call this function.
|
||||||
|
// TODO: We could instead pass in the various edges we will be sending,
|
||||||
|
// and store a map of those for the resource to know more precisely.
|
||||||
|
// XXX: Not doing this for now. If a send/recv edge wasn't initially on,
|
||||||
|
// and the sender ran CheckApply, but didn't cache the value to send,
|
||||||
|
// and then the edge flipped on, we'd have to error. Better to always
|
||||||
|
// generate the cache, and only consider adding this if we have a more
|
||||||
|
// important privacy or performance situation that requires it.
|
||||||
|
//SendSetActive(bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RecvableRes is the interface a resource must implement to support receiving
|
// RecvableRes is the interface a resource must implement to support receiving
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import (
|
|||||||
// the Sends method, and call the Send method in CheckApply via the Init API.
|
// the Sends method, and call the Send method in CheckApply via the Init API.
|
||||||
type Sendable struct {
|
type Sendable struct {
|
||||||
send interface{}
|
send interface{}
|
||||||
|
//sendIsActive bool // TODO: public?
|
||||||
|
|
||||||
// Bug5819 works around issue https://github.com/golang/go/issues/5819
|
// Bug5819 works around issue https://github.com/golang/go/issues/5819
|
||||||
Bug5819 interface{} // XXX: workaround
|
Bug5819 interface{} // XXX: workaround
|
||||||
@@ -63,6 +64,24 @@ func (obj *Sendable) Sent() interface{} {
|
|||||||
return obj.send
|
return obj.send
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SendActive let's the resource know if it must send a value. This is usually
|
||||||
|
// called during CheckApply, but it's probably safe to check it during Init as
|
||||||
|
// well. This is the implementation of this function.
|
||||||
|
// XXX: Not doing this for now, see the interface for more information.
|
||||||
|
//func (obj *Sendable) SendActive() bool {
|
||||||
|
// return obj.sendIsActive
|
||||||
|
//}
|
||||||
|
|
||||||
|
// SendSetActive is used by the compiler to store the "SendActive" bool so that
|
||||||
|
// it will later know if it will need to send or not. Only the engine should
|
||||||
|
// call this function. This is the implementation of this function.
|
||||||
|
// TODO: We could instead pass in the various edges we will be sending, and
|
||||||
|
// store a map of those for the resource to know more precisely.
|
||||||
|
// XXX: Not doing this for now, see the interface for more information.
|
||||||
|
//func (obj *Sendable) SendSetActive(b bool) {
|
||||||
|
// obj.sendIsActive = b
|
||||||
|
//}
|
||||||
|
|
||||||
// Recvable contains a general implementation with some of the properties and
|
// Recvable contains a general implementation with some of the properties and
|
||||||
// methods needed to implement receiving from resources.
|
// methods needed to implement receiving from resources.
|
||||||
type Recvable struct {
|
type Recvable struct {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/user"
|
"os/user"
|
||||||
"reflect"
|
"reflect"
|
||||||
@@ -387,6 +388,83 @@ func GetGID(group string) (int, error) {
|
|||||||
return -1, errwrap.Wrapf(err, "group lookup error (%s)", group)
|
return -1, errwrap.Wrapf(err, "group lookup error (%s)", group)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUIDGID is a small helper function to return the current uid and gid of the
|
||||||
|
// user running this program. If invoked under `sudo` it works as expected to
|
||||||
|
// return the root uid and gid.
|
||||||
|
func GetUIDGID() (int, int, error) {
|
||||||
|
currentUser, err := user.Current()
|
||||||
|
if err != nil {
|
||||||
|
return -1, -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
uid, err := strconv.Atoi(currentUser.Uid)
|
||||||
|
if err != nil {
|
||||||
|
return -1, -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
gid, err := strconv.Atoi(currentUser.Gid)
|
||||||
|
if err != nil {
|
||||||
|
return -1, -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return uid, gid, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadData is a helper to read data, usually from our vardir directory. If the
|
||||||
|
// data is nil, this means the file didn't exist.
|
||||||
|
func ReadData(p string) (*string, error) {
|
||||||
|
file, err := os.Open(p) // open a handle to read the file
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil, nil // no file
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
data, err := io.ReadAll(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s := string(data)
|
||||||
|
return &s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteData is a helper to write data, usually into our vardir directory. If
|
||||||
|
// the data is nil, this counts as a request to delete the file.
|
||||||
|
func WriteData(p string, data *string) (int, error) {
|
||||||
|
if data == nil {
|
||||||
|
err := os.Remove(p)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return 0, nil // no file
|
||||||
|
}
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
uid, gid, err := GetUIDGID()
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chmod it before we write the secret data.
|
||||||
|
file, err := os.OpenFile(p, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||||
|
//file, err := os.Create(p) // open a handle to create the file
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// Chown it before we write the secret data.
|
||||||
|
if err := file.Chown(uid, gid); err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := file.Write([]byte(*data))
|
||||||
|
if err != nil {
|
||||||
|
return c, errwrap.Wrapf(err, "can't write file")
|
||||||
|
}
|
||||||
|
return c, file.Sync()
|
||||||
|
}
|
||||||
|
|
||||||
// RestartUnit restarts the given dbus unit and waits for it to finish starting.
|
// RestartUnit restarts the given dbus unit and waits for it to finish starting.
|
||||||
func RestartUnit(ctx context.Context, conn *dbus.Conn, unit string) error {
|
func RestartUnit(ctx context.Context, conn *dbus.Conn, unit string) error {
|
||||||
return unitStateAction(ctx, conn, unit, DBusRestartUnit)
|
return unitStateAction(ctx, conn, unit, DBusRestartUnit)
|
||||||
|
|||||||
11
examples/lang/sendrecv-creates.mcl
Normal file
11
examples/lang/sendrecv-creates.mcl
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
exec "a" {
|
||||||
|
shell => "/bin/bash",
|
||||||
|
cmd => "echo hello | tee /tmp/hello",
|
||||||
|
creates => "/tmp/hello",
|
||||||
|
}
|
||||||
|
|
||||||
|
file "/tmp/hello" {
|
||||||
|
mode => "ug=rw,o=",
|
||||||
|
}
|
||||||
|
|
||||||
|
Exec["a"].output -> File["/tmp/hello"].content
|
||||||
20
examples/lang/sendrecv-export.mcl
Normal file
20
examples/lang/sendrecv-export.mcl
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import "os"
|
||||||
|
|
||||||
|
$f = "/tmp/export1"
|
||||||
|
exec "a" {
|
||||||
|
shell => "/bin/bash",
|
||||||
|
cmd => "echo hello | tee ${f}",
|
||||||
|
creates => "${f}",
|
||||||
|
send_output => os.readfile("${f}"), # extra graph event but safer in debug
|
||||||
|
|
||||||
|
Before => File["${f}"],
|
||||||
|
}
|
||||||
|
|
||||||
|
file "${f}" {
|
||||||
|
state => $const.res.file.state.exists,
|
||||||
|
mode => "ug=rw,o=",
|
||||||
|
|
||||||
|
Meta:export => ["*",],
|
||||||
|
}
|
||||||
|
|
||||||
|
Exec["a"].output -> File["${f}"].content
|
||||||
@@ -393,6 +393,10 @@ func (obj *Interpreter) makeSendRecv(v1, v2 pgraph.Vertex, edge *interfaces.Edge
|
|||||||
return errwrap.Wrapf(err, "cannot send/recv from %s.%s to %s.%s", engine.Stringer(res1), edge.Send, engine.Stringer(res2), edge.Recv)
|
return errwrap.Wrapf(err, "cannot send/recv from %s.%s to %s.%s", engine.Stringer(res1), edge.Send, engine.Stringer(res2), edge.Recv)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// XXX: Not doing this for now, see the interface for more information.
|
||||||
|
// TODO: We could instead pass in edge.Send so it would know precisely!
|
||||||
|
//res1.SendSetActive(true) // tell it that it will be sending (optimization)
|
||||||
|
|
||||||
// store mapping for later
|
// store mapping for later
|
||||||
obj.receive[ruid][edge.Recv] = &engine.Send{Res: res1, Key: edge.Send}
|
obj.receive[ruid][edge.Recv] = &engine.Send{Res: res1, Key: edge.Send}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user