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:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
7
lang/interpret_test/TestAstFunc2/send-recv-0.output
Normal file
7
lang/interpret_test/TestAstFunc2/send-recv-0.output
Normal 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]
|
||||
12
lang/interpret_test/TestAstFunc2/send-recv-0/main.mcl
Normal file
12
lang/interpret_test/TestAstFunc2/send-recv-0/main.mcl
Normal 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
|
||||
1
lang/interpret_test/TestAstFunc2/send-recv-1.output
Normal file
1
lang/interpret_test/TestAstFunc2/send-recv-1.output
Normal 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
|
||||
11
lang/interpret_test/TestAstFunc2/send-recv-1/main.mcl
Normal file
11
lang/interpret_test/TestAstFunc2/send-recv-1/main.mcl
Normal 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
|
||||
1
lang/interpret_test/TestAstFunc2/send-recv-2.output
Normal file
1
lang/interpret_test/TestAstFunc2/send-recv-2.output
Normal file
@@ -0,0 +1 @@
|
||||
# err: err5: resource: `file[/tmp/command-output]` has duplicate receive on: `content` param
|
||||
11
lang/interpret_test/TestAstFunc2/send-recv-2/main.mcl
Normal file
11
lang/interpret_test/TestAstFunc2/send-recv-2/main.mcl
Normal 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
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user