lang: Add edges to lexer and parser
This adds some initial syntax for external edges to the language. There are still improvements which are necessary for send/recv.
This commit is contained in:
@@ -11,7 +11,7 @@ frontend. This guide describes some of the internals of the language.
|
||||
|
||||
The mgmt language is a declarative (immutable) functional, reactive programming
|
||||
language. It is implemented in `golang`. A longer introduction to the language
|
||||
is coming soon!
|
||||
is [available as a blog post here](https://purpleidea.com/blog/2018/02/05/mgmt-configuration-language/)!
|
||||
|
||||
### Types
|
||||
|
||||
@@ -83,10 +83,68 @@ These docs will be expanded on when things are more certain to be stable.
|
||||
|
||||
### Statements
|
||||
|
||||
Statements, and the `Stmt` interface need to be better documented. For now
|
||||
please consume
|
||||
[lang/interfaces/ast.go](https://github.com/purpleidea/mgmt/tree/master/lang/interfaces/ast.go).
|
||||
These docs will be expanded on when things are more certain to be stable.
|
||||
There are a very small number of statements in our language. They include:
|
||||
|
||||
- **bind**: bind's an expression to a variable within that scope
|
||||
- eg: `$x = 42`
|
||||
- **if**: produces up to one branch of statements based on a conditional
|
||||
expression
|
||||
|
||||
```
|
||||
if <conditional> {
|
||||
<statements>
|
||||
} else {
|
||||
# the else branch is optional for if statements
|
||||
<statements>
|
||||
}
|
||||
```
|
||||
|
||||
- **resource**: produces a resource
|
||||
|
||||
```
|
||||
file "/tmp/hello" {
|
||||
content => "world",
|
||||
mode => "o=rwx",
|
||||
}
|
||||
```
|
||||
|
||||
- **edge**: produces an edge
|
||||
|
||||
```
|
||||
File["/tmp/hello"] -> Print["alert4"]
|
||||
```
|
||||
|
||||
All statements produce _output_. Output consists of between zero and more
|
||||
`edges` and `resources`. A resource statement can produce a resource, whereas an
|
||||
`if` statement produces whatever the chosen branch produces. Ultimately the goal
|
||||
of executing our programs is to produce a list of `resources`, which along with
|
||||
the produced `edges`, is built into a resource graph. This graph is then passed
|
||||
to the engine for desired state application.
|
||||
|
||||
#### Bind
|
||||
|
||||
This section needs better documentation.
|
||||
|
||||
#### If
|
||||
|
||||
This section needs better documentation.
|
||||
|
||||
#### Resource
|
||||
|
||||
This section needs better documentation.
|
||||
|
||||
#### Edge
|
||||
|
||||
Edges express dependencies in the graph of resources which are output. They can
|
||||
be chained as a pair, or in any greater number. For example, you may write:
|
||||
|
||||
```
|
||||
Pkg["drbd"] -> File["/etc/drbd.conf"] -> Svc["drbd"]
|
||||
```
|
||||
|
||||
to express a relationship between three resources. The first character in the
|
||||
resource kind must be capitalized so that the parser can't ascertain
|
||||
unambiguously that we are referring to a dependency relationship.
|
||||
|
||||
### Stages
|
||||
|
||||
|
||||
14
examples/lang/edges0.mcl
Normal file
14
examples/lang/edges0.mcl
Normal file
@@ -0,0 +1,14 @@
|
||||
exec "exec0" {
|
||||
cmd => "sleep 10s",
|
||||
shell => "/bin/bash",
|
||||
}
|
||||
exec "exec1" {
|
||||
cmd => "sleep 10s",
|
||||
shell => "/bin/bash",
|
||||
}
|
||||
exec "exec2" {
|
||||
cmd => "sleep 10s",
|
||||
shell => "/bin/bash",
|
||||
}
|
||||
|
||||
Exec["exec0"] -> Exec["exec1"] -> Exec["exec2"]
|
||||
8
examples/lang/password0.mcl
Normal file
8
examples/lang/password0.mcl
Normal file
@@ -0,0 +1,8 @@
|
||||
password "pass0" {
|
||||
length => 8,
|
||||
}
|
||||
|
||||
file "/tmp/mgmt/password" {
|
||||
}
|
||||
|
||||
Password["pass0"].password -> File["/tmp/mgmt/password"].content
|
||||
9
examples/lang/sendrecv0.mcl
Normal file
9
examples/lang/sendrecv0.mcl
Normal file
@@ -0,0 +1,9 @@
|
||||
exec "exec0" {
|
||||
cmd => "echo hello world && echo goodbye world 1>&2", # to stdout && stderr
|
||||
shell => "/bin/bash",
|
||||
}
|
||||
|
||||
print "print0" {
|
||||
}
|
||||
|
||||
Exec["exec0"].output -> Print["print0"].msg
|
||||
14
examples/lang/sendrecv1.mcl
Normal file
14
examples/lang/sendrecv1.mcl
Normal file
@@ -0,0 +1,14 @@
|
||||
file "/tmp/mgmt/foo" {
|
||||
content => "hello from foo\n",
|
||||
}
|
||||
|
||||
print "print0" {
|
||||
}
|
||||
|
||||
File["/tmp/mgmt/foo"].content -> Print["print0"].msg
|
||||
|
||||
print "print1" {
|
||||
msg => "hello",
|
||||
}
|
||||
|
||||
Print["print0"] -> Print["print1"]
|
||||
8
examples/lang/sendrecv2.mcl
Normal file
8
examples/lang/sendrecv2.mcl
Normal file
@@ -0,0 +1,8 @@
|
||||
file "/tmp/mgmt/foo" {
|
||||
content => "hello from foo\n",
|
||||
}
|
||||
|
||||
file "/tmp/mgmt/bar" {
|
||||
}
|
||||
|
||||
File["/tmp/mgmt/foo"].content -> File["/tmp/mgmt/bar"].content
|
||||
21
examples/lang/sendrecv3.mcl
Normal file
21
examples/lang/sendrecv3.mcl
Normal file
@@ -0,0 +1,21 @@
|
||||
$ns = "estate"
|
||||
$exchanged = kvlookup($ns)
|
||||
$state = maplookup($exchanged, $hostname, "default")
|
||||
|
||||
exec "exec0" {
|
||||
cmd => "echo hello world && echo goodbye world 1>&2", # to stdout && stderr
|
||||
shell => "/bin/bash",
|
||||
}
|
||||
|
||||
kv "kv0" {
|
||||
key => $ns,
|
||||
#value => "two",
|
||||
}
|
||||
|
||||
Exec["exec0"].output -> Kv["kv0"].value
|
||||
|
||||
if $state != "default" {
|
||||
file "/tmp/mgmt/state" {
|
||||
content => printf("state: %s\n", $state),
|
||||
}
|
||||
}
|
||||
@@ -8,28 +8,43 @@ if $state == "one" || $state == "default" {
|
||||
file "/tmp/mgmt/state" {
|
||||
content => "state: one\n",
|
||||
}
|
||||
|
||||
exec "timer" {
|
||||
cmd => "/usr/bin/sleep 1s",
|
||||
}
|
||||
kv "${ns}" {
|
||||
key => $ns,
|
||||
value => "two",
|
||||
}
|
||||
Exec["timer"] -> Kv["${ns}"]
|
||||
}
|
||||
if $state == "two" {
|
||||
|
||||
file "/tmp/mgmt/state" {
|
||||
content => "state: two\n",
|
||||
}
|
||||
|
||||
exec "timer" {
|
||||
cmd => "/usr/bin/sleep 1s",
|
||||
}
|
||||
kv "${ns}" {
|
||||
key => $ns,
|
||||
value => "three",
|
||||
}
|
||||
Exec["timer"] -> Kv["${ns}"]
|
||||
}
|
||||
if $state == "three" {
|
||||
|
||||
file "/tmp/mgmt/state" {
|
||||
content => "state: three\n",
|
||||
}
|
||||
|
||||
exec "timer" {
|
||||
cmd => "/usr/bin/sleep 1s",
|
||||
}
|
||||
kv "${ns}" {
|
||||
key => $ns,
|
||||
value => "one",
|
||||
}
|
||||
Exec["timer"] -> Kv["${ns}"]
|
||||
}
|
||||
|
||||
@@ -537,6 +537,43 @@ func TestAstInterpret0(t *testing.T) {
|
||||
graph: graph,
|
||||
})
|
||||
}
|
||||
{
|
||||
// FIXME: add a better vertexCmpFn so we can compare send/recv!
|
||||
graph, _ := pgraph.NewGraph("g")
|
||||
t1, _ := resources.NewNamedResource("test", "t1")
|
||||
{
|
||||
x := t1.(*resources.TestRes)
|
||||
int64Ptr := int64(42)
|
||||
x.Int64Ptr = &int64Ptr
|
||||
graph.AddVertex(t1)
|
||||
}
|
||||
t2, _ := resources.NewNamedResource("test", "t2")
|
||||
{
|
||||
x := t2.(*resources.TestRes)
|
||||
int64Ptr := int64(13)
|
||||
x.Int64Ptr = &int64Ptr
|
||||
graph.AddVertex(t2)
|
||||
}
|
||||
edge := &resources.Edge{
|
||||
Name: fmt.Sprintf("%s -> %s", t1, t2),
|
||||
Notify: false,
|
||||
}
|
||||
graph.AddEdge(t1, t2, edge)
|
||||
values = append(values, test{
|
||||
name: "two resources and send/recv edge",
|
||||
code: `
|
||||
test "t1" {
|
||||
int64ptr => 42,
|
||||
}
|
||||
test "t2" {
|
||||
int64ptr => 13,
|
||||
}
|
||||
|
||||
Test["t1"].foosend -> Test["t2"].barrecv # send/recv
|
||||
`,
|
||||
graph: graph,
|
||||
})
|
||||
}
|
||||
|
||||
for index, test := range values { // run all the tests
|
||||
name, code, fail, exp := test.name, test.code, test.fail, test.graph
|
||||
|
||||
@@ -134,6 +134,16 @@
|
||||
lval.str = yylex.Text()
|
||||
return IN
|
||||
}
|
||||
/\->/ {
|
||||
yylex.pos(lval) // our pos
|
||||
lval.str = yylex.Text()
|
||||
return ARROW
|
||||
}
|
||||
/\./ {
|
||||
yylex.pos(lval) // our pos
|
||||
lval.str = yylex.Text()
|
||||
return DOT
|
||||
}
|
||||
/bool/ {
|
||||
yylex.pos(lval) // our pos
|
||||
lval.str = yylex.Text()
|
||||
@@ -295,6 +305,13 @@
|
||||
lval.str = s[1:len(s)] // remove the leading $
|
||||
return VAR_IDENTIFIER
|
||||
}
|
||||
/[A-Z][a-z0-9]*/
|
||||
{
|
||||
yylex.pos(lval) // our pos
|
||||
s := yylex.Text()
|
||||
lval.str = strings.ToLower(s) // uncapitalize it
|
||||
return CAPITALIZED_IDENTIFIER
|
||||
}
|
||||
/[a-z][a-z0-9]*/
|
||||
{
|
||||
yylex.pos(lval) // our pos
|
||||
|
||||
@@ -752,6 +752,73 @@ func TestLexParse0(t *testing.T) {
|
||||
exp: exp,
|
||||
})
|
||||
}
|
||||
{
|
||||
exp := &StmtProg{
|
||||
Prog: []interfaces.Stmt{
|
||||
&StmtRes{
|
||||
Kind: "test",
|
||||
Name: &ExprStr{
|
||||
V: "t1",
|
||||
},
|
||||
Fields: []*StmtResField{
|
||||
{
|
||||
Field: "int64ptr",
|
||||
Value: &ExprInt{
|
||||
V: 42,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&StmtRes{
|
||||
Kind: "test",
|
||||
Name: &ExprStr{
|
||||
V: "t2",
|
||||
},
|
||||
Fields: []*StmtResField{
|
||||
{
|
||||
Field: "int64ptr",
|
||||
Value: &ExprInt{
|
||||
V: 13,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&StmtEdge{
|
||||
EdgeHalfList: []*StmtEdgeHalf{
|
||||
{
|
||||
Kind: "test",
|
||||
Name: &ExprStr{
|
||||
V: "t1",
|
||||
},
|
||||
SendRecv: "foosend",
|
||||
},
|
||||
{
|
||||
Kind: "test",
|
||||
Name: &ExprStr{
|
||||
V: "t2",
|
||||
},
|
||||
SendRecv: "barrecv",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
values = append(values, test{
|
||||
name: "edge stmt",
|
||||
code: `
|
||||
test "t1" {
|
||||
int64ptr => 42,
|
||||
}
|
||||
test "t2" {
|
||||
int64ptr => 13,
|
||||
}
|
||||
|
||||
Test["t1"].foosend -> Test["t2"].barrecv # send/recv
|
||||
`,
|
||||
fail: false,
|
||||
exp: exp,
|
||||
})
|
||||
}
|
||||
|
||||
for index, test := range values { // run all the tests
|
||||
name, code, fail, exp := test.name, test.code, test.fail, test.exp
|
||||
@@ -789,8 +856,8 @@ func TestLexParse0(t *testing.T) {
|
||||
if !reflect.DeepEqual(ast, exp) {
|
||||
t.Errorf("test #%d: AST did not match expected", index)
|
||||
// TODO: consider making our own recursive print function
|
||||
t.Logf("test #%d: actual: \n%s", index, spew.Sdump(ast))
|
||||
t.Logf("test #%d: expected: \n%s", index, spew.Sdump(exp))
|
||||
t.Logf("test #%d: actual: \n\n%s\n", index, spew.Sdump(ast))
|
||||
t.Logf("test #%d: expected: \n\n%s", index, spew.Sdump(exp))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,9 @@ func init() {
|
||||
|
||||
resFields []*StmtResField
|
||||
resField *StmtResField
|
||||
|
||||
edgeHalfList []*StmtEdgeHalf
|
||||
edgeHalf *StmtEdgeHalf
|
||||
}
|
||||
|
||||
%token OPEN_CURLY CLOSE_CURLY
|
||||
@@ -74,10 +77,10 @@ func init() {
|
||||
%token STRING BOOL INTEGER FLOAT
|
||||
%token EQUALS
|
||||
%token COMMA COLON SEMICOLON
|
||||
%token ROCKET
|
||||
%token ROCKET ARROW DOT
|
||||
%token STR_IDENTIFIER BOOL_IDENTIFIER INT_IDENTIFIER FLOAT_IDENTIFIER
|
||||
%token STRUCT_IDENTIFIER VARIANT_IDENTIFIER VAR_IDENTIFIER IDENTIFIER
|
||||
%token VAR_IDENTIFIER_HX
|
||||
%token VAR_IDENTIFIER_HX CAPITALIZED_IDENTIFIER
|
||||
%token COMMENT ERROR
|
||||
|
||||
// precedence table
|
||||
@@ -158,6 +161,11 @@ stmt:
|
||||
posLast(yylex, yyDollar) // our pos
|
||||
$$.stmt = $1.stmt
|
||||
}
|
||||
| edge
|
||||
{
|
||||
posLast(yylex, yyDollar) // our pos
|
||||
$$.stmt = $1.stmt
|
||||
}
|
||||
| IF expr OPEN_CURLY prog CLOSE_CURLY
|
||||
{
|
||||
posLast(yylex, yyDollar) // our pos
|
||||
@@ -685,6 +693,67 @@ resource_field:
|
||||
}
|
||||
}
|
||||
;
|
||||
edge:
|
||||
// TODO: we could technically prevent single edge_half pieces from being
|
||||
// parsed, but it's probably more work than is necessary...
|
||||
// Test["t1"] -> Test["t2"] -> Test["t3"] # chain or pair
|
||||
edge_half_list
|
||||
{
|
||||
posLast(yylex, yyDollar) // our pos
|
||||
$$.stmt = &StmtEdge{
|
||||
EdgeHalfList: $1.edgeHalfList,
|
||||
//Notify: false, // unused here
|
||||
}
|
||||
}
|
||||
// Test["t1"].foo_send -> Test["t2"].blah_recv # send/recv
|
||||
| edge_half_sendrecv ARROW edge_half_sendrecv
|
||||
{
|
||||
posLast(yylex, yyDollar) // our pos
|
||||
$$.stmt = &StmtEdge{
|
||||
EdgeHalfList: []*StmtEdgeHalf{
|
||||
$1.edgeHalf,
|
||||
$3.edgeHalf,
|
||||
},
|
||||
//Notify: false, // unused here, it is implied (i think)
|
||||
}
|
||||
}
|
||||
;
|
||||
edge_half_list:
|
||||
edge_half
|
||||
{
|
||||
posLast(yylex, yyDollar) // our pos
|
||||
$$.edgeHalfList = []*StmtEdgeHalf{$1.edgeHalf}
|
||||
}
|
||||
| edge_half_list ARROW edge_half
|
||||
{
|
||||
posLast(yylex, yyDollar) // our pos
|
||||
$$.edgeHalfList = append($1.edgeHalfList, $3.edgeHalf)
|
||||
}
|
||||
;
|
||||
edge_half:
|
||||
// eg: Test["t1"]
|
||||
CAPITALIZED_IDENTIFIER OPEN_BRACK expr CLOSE_BRACK
|
||||
{
|
||||
posLast(yylex, yyDollar) // our pos
|
||||
$$.edgeHalf = &StmtEdgeHalf{
|
||||
Kind: $1.str,
|
||||
Name: $3.expr,
|
||||
//SendRecv: "", // unused
|
||||
}
|
||||
}
|
||||
;
|
||||
edge_half_sendrecv:
|
||||
// eg: Test["t1"].foo_send
|
||||
CAPITALIZED_IDENTIFIER OPEN_BRACK expr CLOSE_BRACK DOT IDENTIFIER
|
||||
{
|
||||
posLast(yylex, yyDollar) // our pos
|
||||
$$.edgeHalf = &StmtEdgeHalf{
|
||||
Kind: $1.str,
|
||||
Name: $3.expr,
|
||||
SendRecv: $6.str,
|
||||
}
|
||||
}
|
||||
;
|
||||
type:
|
||||
BOOL_IDENTIFIER
|
||||
{
|
||||
|
||||
@@ -836,6 +836,7 @@ func (obj *StmtProg) Graph() (*pgraph.Graph, error) {
|
||||
// called by this Output function if they are needed to produce the output.
|
||||
func (obj *StmtProg) Output() (*interfaces.Output, error) {
|
||||
resources := []resources.Res{}
|
||||
edges := []*interfaces.Edge{}
|
||||
|
||||
for _, stmt := range obj.Prog {
|
||||
output, err := stmt.Output()
|
||||
@@ -844,13 +845,13 @@ func (obj *StmtProg) Output() (*interfaces.Output, error) {
|
||||
}
|
||||
if output != nil {
|
||||
resources = append(resources, output.Resources...)
|
||||
//edges = append(edges, output.Edges)
|
||||
edges = append(edges, output.Edges...)
|
||||
}
|
||||
}
|
||||
|
||||
return &interfaces.Output{
|
||||
Resources: resources,
|
||||
//Edges: edges,
|
||||
Edges: edges,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -83,6 +83,11 @@ func (obj *PrintRes) Watch() error {
|
||||
// CheckApply method for Print resource. Does nothing, returns happy!
|
||||
func (obj *PrintRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
log.Printf("%s: CheckApply: %t", obj, apply)
|
||||
if val, exists := obj.Recv["Msg"]; exists && val.Changed {
|
||||
// if we received on Msg, and it changed, log message
|
||||
log.Printf("CheckApply: Received `Msg` of: %s", obj.Msg)
|
||||
}
|
||||
|
||||
if obj.Refresh() {
|
||||
log.Printf("%s: Received a notification!", obj)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user