diff --git a/engine/graph/sendrecv.go b/engine/graph/sendrecv.go index ef0a99dc..a519c609 100644 --- a/engine/graph/sendrecv.go +++ b/engine/graph/sendrecv.go @@ -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 { diff --git a/engine/traits/sendrecv.go b/engine/traits/sendrecv.go index 7771b920..184a556f 100644 --- a/engine/traits/sendrecv.go +++ b/engine/traits/sendrecv.go @@ -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 } diff --git a/examples/lang/sendrecv0.mcl b/examples/lang/sendrecv0.mcl index e75f9688..82fdc1cd 100644 --- a/examples/lang/sendrecv0.mcl +++ b/examples/lang/sendrecv0.mcl @@ -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 diff --git a/lang/interpret.go b/lang/interpret.go index 1eb7b28e..bbdb8397 100644 --- a/lang/interpret.go +++ b/lang/interpret.go @@ -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 diff --git a/lang/interpret_test.go b/lang/interpret_test.go index 1196e5e1..a9e0fa6d 100644 --- a/lang/interpret_test.go +++ b/lang/interpret_test.go @@ -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, }) diff --git a/lang/interpret_test/TestAstFunc2/send-recv-0.output b/lang/interpret_test/TestAstFunc2/send-recv-0.output new file mode 100644 index 00000000..0754c939 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/send-recv-0.output @@ -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] diff --git a/lang/interpret_test/TestAstFunc2/send-recv-0/main.mcl b/lang/interpret_test/TestAstFunc2/send-recv-0/main.mcl new file mode 100644 index 00000000..82fdc1cd --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/send-recv-0/main.mcl @@ -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 diff --git a/lang/interpret_test/TestAstFunc2/send-recv-1.output b/lang/interpret_test/TestAstFunc2/send-recv-1.output new file mode 100644 index 00000000..7f454155 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/send-recv-1.output @@ -0,0 +1 @@ +# err: err3: cannot send/recv from exec[exec0].shell to file[/tmp/command-output].content: key not found in send struct diff --git a/lang/interpret_test/TestAstFunc2/send-recv-1/main.mcl b/lang/interpret_test/TestAstFunc2/send-recv-1/main.mcl new file mode 100644 index 00000000..bbb88f6a --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/send-recv-1/main.mcl @@ -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 diff --git a/lang/interpret_test/TestAstFunc2/send-recv-2.output b/lang/interpret_test/TestAstFunc2/send-recv-2.output new file mode 100644 index 00000000..44729481 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/send-recv-2.output @@ -0,0 +1 @@ +# err: err5: resource: `file[/tmp/command-output]` has duplicate receive on: `content` param diff --git a/lang/interpret_test/TestAstFunc2/send-recv-2/main.mcl b/lang/interpret_test/TestAstFunc2/send-recv-2/main.mcl new file mode 100644 index 00000000..f88187b9 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/send-recv-2/main.mcl @@ -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 diff --git a/lang/structs.go b/lang/structs.go index 3d54e4f4..89697939 100644 --- a/lang/structs.go +++ b/lang/structs.go @@ -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 {