engine, lang: Improve send/recv significantly

Part of this was rotten, and not fully functional. This fixes the rot,
adds some tests, and improves the type checking that occurs when sending
and receiving values. In addition, a significant portion of this happens
at compile time.

There is still more work to be done here, but this should get us a good
chunk of the way for now.
This commit is contained in:
James Shubin
2019-03-09 16:19:24 -05:00
parent aa6b701b77
commit de43569fa2
12 changed files with 148 additions and 14 deletions

View File

@@ -22,6 +22,7 @@ import (
"reflect"
"github.com/purpleidea/mgmt/engine"
engineUtil "github.com/purpleidea/mgmt/engine/util"
multierr "github.com/hashicorp/go-multierror"
errwrap "github.com/pkg/errors"
@@ -47,16 +48,51 @@ func (obj *Engine) SendRecv(res engine.RecvableRes) (map[string]bool, error) {
st = v.Res.Sent()
}
if st == nil {
e := fmt.Errorf("received nil value from: %s", v.Res)
err = multierr.Append(err, e) // list of errors
continue
}
if e := engineUtil.StructFieldCompat(st, v.Key, res, k); e != nil {
err = multierr.Append(err, e) // list of errors
continue
}
// send
m1, e := engineUtil.StructTagToFieldName(st)
if e != nil {
err = multierr.Append(err, e) // list of errors
continue
}
key1, exists := m1[v.Key]
if !exists {
e := fmt.Errorf("requested key of `%s` not found in send struct", v.Key)
err = multierr.Append(err, e) // list of errors
continue
}
obj1 := reflect.Indirect(reflect.ValueOf(st))
type1 := obj1.Type()
value1 := obj1.FieldByName(v.Key)
value1 := obj1.FieldByName(key1)
kind1 := value1.Kind()
// recv
m2, e := engineUtil.StructTagToFieldName(res)
if e != nil {
err = multierr.Append(err, e) // list of errors
continue
}
key2, exists := m2[k]
if !exists {
e := fmt.Errorf("requested key of `%s` not found in recv struct", k)
err = multierr.Append(err, e) // list of errors
continue
}
obj2 := reflect.Indirect(reflect.ValueOf(res)) // pass in full struct
type2 := obj2.Type()
value2 := obj2.FieldByName(k)
value2 := obj2.FieldByName(key2)
kind2 := value2.Kind()
if obj.Debug {

View File

@@ -33,7 +33,7 @@ type Sendable struct {
// Sends returns a struct containing the defaults of the type we send. This
// needs to be implemented (overridden) by the struct with the Sendable trait to
// be able to send any values. The public struct field names are the keys used.
// be able to send any values. The field struct tag names are the keys used.
func (obj *Sendable) Sends() interface{} {
return nil
}

View File

@@ -1,9 +1,12 @@
exec "exec0" {
cmd => "echo hello world && echo goodbye world 1>&2", # to stdout && stderr
cmd => "echo this is stdout && echo this is stderr 1>&2", # to stdout && stderr
shell => "/bin/bash",
}
print "print0" {
file ["/tmp/command-output", "/tmp/command-stdout", "/tmp/command-stderr",] {
state => "exists",
}
Exec["exec0"].output -> Print["print0"].msg
Exec["exec0"].output -> File["/tmp/command-output"].content
Exec["exec0"].stdout -> File["/tmp/command-stdout"].content
Exec["exec0"].stderr -> File["/tmp/command-stderr"].content

View File

@@ -21,6 +21,7 @@ import (
"fmt"
"github.com/purpleidea/mgmt/engine"
engineUtil "github.com/purpleidea/mgmt/engine/util"
"github.com/purpleidea/mgmt/lang/interfaces"
"github.com/purpleidea/mgmt/pgraph"
@@ -143,18 +144,25 @@ func interpret(ast interfaces.Stmt) (*pgraph.Graph, error) {
// ignore identical duplicates
// TODO: does this safe ignore work with duplicate compatible resources?
if existingSend.Res != v1 || existingSend.Key != e.Send {
return nil, fmt.Errorf("resource kind: %s named: `%s` already receives on `%s`", e.Kind2, e.Name2, e.Recv)
return nil, fmt.Errorf("resource: `%s` has duplicate receive on: `%s` param", engine.Repr(e.Kind2, e.Name2), e.Recv)
}
}
res, ok := v1.(engine.SendableRes)
res1, ok := v1.(engine.SendableRes)
if !ok {
return nil, fmt.Errorf("cannot send from resource: %s", engine.Stringer(v1))
}
// XXX: type check the send/recv relationship somewhere
res2, ok := v2.(engine.RecvableRes)
if !ok {
return nil, fmt.Errorf("cannot recv to resource: %s", engine.Stringer(v2))
}
if err := engineUtil.StructFieldCompat(res1.Sends(), e.Send, res2, e.Recv); err != nil {
return nil, errwrap.Wrapf(err, "cannot send/recv from %s.%s to %s.%s", engine.Stringer(v1), e.Send, engine.Stringer(v2), e.Recv)
}
// store mapping for later
receive[e.Kind2][e.Name2][e.Recv] = &engine.Send{Res: res, Key: e.Send}
receive[e.Kind2][e.Name2][e.Recv] = &engine.Send{Res: res1, Key: e.Send}
}
// we need to first build up a map of all the resources handles, because

View File

@@ -1439,7 +1439,7 @@ func TestAstInterpret0(t *testing.T) {
int64ptr => 13,
}
Test["t1"].foosend -> Test["t2"].barrecv # send/recv
Test["t1"].hello -> Test["t2"].stringptr # send/recv
`,
graph: graph,
})

View File

@@ -0,0 +1,7 @@
Edge: exec[exec0] -> file[/tmp/command-output] # exec[exec0] -> file[/tmp/command-output]
Edge: exec[exec0] -> file[/tmp/command-stderr] # exec[exec0] -> file[/tmp/command-stderr]
Edge: exec[exec0] -> file[/tmp/command-stdout] # exec[exec0] -> file[/tmp/command-stdout]
Vertex: exec[exec0]
Vertex: file[/tmp/command-output]
Vertex: file[/tmp/command-stderr]
Vertex: file[/tmp/command-stdout]

View File

@@ -0,0 +1,12 @@
exec "exec0" {
cmd => "echo this is stdout && echo this is stderr 1>&2", # to stdout && stderr
shell => "/bin/bash",
}
file ["/tmp/command-output", "/tmp/command-stdout", "/tmp/command-stderr",] {
state => "exists",
}
Exec["exec0"].output -> File["/tmp/command-output"].content
Exec["exec0"].stdout -> File["/tmp/command-stdout"].content
Exec["exec0"].stderr -> File["/tmp/command-stderr"].content

View File

@@ -0,0 +1 @@
# err: err3: cannot send/recv from exec[exec0].shell to file[/tmp/command-output].content: key not found in send struct

View File

@@ -0,0 +1,11 @@
exec "exec0" {
cmd => "echo whatever",
shell => "/bin/bash",
}
file "/tmp/command-output" {
state => "exists",
}
# this is an error because the shell send key doesn't exist in exec
Exec["exec0"].shell -> File["/tmp/command-output"].content

View File

@@ -0,0 +1 @@
# err: err5: resource: `file[/tmp/command-output]` has duplicate receive on: `content` param

View File

@@ -0,0 +1,11 @@
exec ["exec0", "exec1",] {
cmd => "echo whatever",
shell => "/bin/bash",
}
file "/tmp/command-output" {
state => "exists",
}
# this is an error because two senders cannot send to the same receiver key
Exec[["exec0", "exec1",]].output -> File["/tmp/command-output"].content

View File

@@ -1478,12 +1478,56 @@ func (obj *StmtEdge) Unify() ([]interfaces.Invariant, error) {
return nil, fmt.Errorf("can't create an edge with only one half")
}
if len(obj.EdgeHalfList) == 2 {
if (obj.EdgeHalfList[0].SendRecv == "") != (obj.EdgeHalfList[1].SendRecv == "") { // xor
sr1 := obj.EdgeHalfList[0].SendRecv
sr2 := obj.EdgeHalfList[1].SendRecv
if (sr1 == "") != (sr2 == "") { // xor
return nil, fmt.Errorf("you must specify both send/recv fields or neither")
}
// XXX: check that the kind1:send -> kind2:recv fields are type
// compatible! XXX: we won't know the names yet, but it's okay.
if sr1 != "" && sr2 != "" {
k1 := obj.EdgeHalfList[0].Kind
k2 := obj.EdgeHalfList[1].Kind
r1, err := engine.NewResource(k1)
if err != nil {
return nil, err
}
r2, err := engine.NewResource(k2)
if err != nil {
return nil, err
}
res1, ok := r1.(engine.SendableRes)
if !ok {
return nil, fmt.Errorf("cannot send from resource of kind: %s", k1)
}
res2, ok := r2.(engine.RecvableRes)
if !ok {
return nil, fmt.Errorf("cannot recv to resource of kind: %s", k2)
}
// Check that the kind1:send -> kind2:recv fields are type
// compatible! We won't know the names yet, but it's okay.
if err := engineUtil.StructFieldCompat(res1.Sends(), sr1, res2, sr2); err != nil {
p1 := k1 // print defaults
p2 := k2
if v, err := obj.EdgeHalfList[0].Name.Value(); err == nil { // statically known
// display something nicer
if v.Type().Kind == types.KindStr {
p1 = engine.Repr(k1, v.Str())
} else if v.Type().Cmp(types.NewType("[]str")) == nil {
p1 = engine.Repr(k1, v.String())
}
}
if v, err := obj.EdgeHalfList[1].Name.Value(); err == nil {
if v.Type().Kind == types.KindStr {
p2 = engine.Repr(k2, v.Str())
} else if v.Type().Cmp(types.NewType("[]str")) == nil {
p2 = engine.Repr(k2, v.String())
}
}
return nil, errwrap.Wrapf(err, "cannot send/recv from %s.%s to %s.%s", p1, sr1, p2, sr2)
}
}
}
for _, x := range obj.EdgeHalfList {