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:
James Shubin
2018-02-10 11:22:18 -05:00
parent 80784bb8f1
commit 6370f0cb95
14 changed files with 354 additions and 11 deletions

View File

@@ -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 The mgmt language is a declarative (immutable) functional, reactive programming
language. It is implemented in `golang`. A longer introduction to the language 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 ### Types
@@ -83,10 +83,68 @@ These docs will be expanded on when things are more certain to be stable.
### Statements ### Statements
Statements, and the `Stmt` interface need to be better documented. For now There are a very small number of statements in our language. They include:
please consume
[lang/interfaces/ast.go](https://github.com/purpleidea/mgmt/tree/master/lang/interfaces/ast.go). - **bind**: bind's an expression to a variable within that scope
These docs will be expanded on when things are more certain to be stable. - 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 ### Stages

14
examples/lang/edges0.mcl Normal file
View 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"]

View File

@@ -0,0 +1,8 @@
password "pass0" {
length => 8,
}
file "/tmp/mgmt/password" {
}
Password["pass0"].password -> File["/tmp/mgmt/password"].content

View 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

View 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"]

View 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

View 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),
}
}

View File

@@ -8,28 +8,43 @@ if $state == "one" || $state == "default" {
file "/tmp/mgmt/state" { file "/tmp/mgmt/state" {
content => "state: one\n", content => "state: one\n",
} }
exec "timer" {
cmd => "/usr/bin/sleep 1s",
}
kv "${ns}" { kv "${ns}" {
key => $ns, key => $ns,
value => "two", value => "two",
} }
Exec["timer"] -> Kv["${ns}"]
} }
if $state == "two" { if $state == "two" {
file "/tmp/mgmt/state" { file "/tmp/mgmt/state" {
content => "state: two\n", content => "state: two\n",
} }
exec "timer" {
cmd => "/usr/bin/sleep 1s",
}
kv "${ns}" { kv "${ns}" {
key => $ns, key => $ns,
value => "three", value => "three",
} }
Exec["timer"] -> Kv["${ns}"]
} }
if $state == "three" { if $state == "three" {
file "/tmp/mgmt/state" { file "/tmp/mgmt/state" {
content => "state: three\n", content => "state: three\n",
} }
exec "timer" {
cmd => "/usr/bin/sleep 1s",
}
kv "${ns}" { kv "${ns}" {
key => $ns, key => $ns,
value => "one", value => "one",
} }
Exec["timer"] -> Kv["${ns}"]
} }

View File

@@ -537,6 +537,43 @@ func TestAstInterpret0(t *testing.T) {
graph: graph, 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 for index, test := range values { // run all the tests
name, code, fail, exp := test.name, test.code, test.fail, test.graph name, code, fail, exp := test.name, test.code, test.fail, test.graph

View File

@@ -134,6 +134,16 @@
lval.str = yylex.Text() lval.str = yylex.Text()
return IN 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/ { /bool/ {
yylex.pos(lval) // our pos yylex.pos(lval) // our pos
lval.str = yylex.Text() lval.str = yylex.Text()
@@ -295,6 +305,13 @@
lval.str = s[1:len(s)] // remove the leading $ lval.str = s[1:len(s)] // remove the leading $
return VAR_IDENTIFIER 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]*/ /[a-z][a-z0-9]*/
{ {
yylex.pos(lval) // our pos yylex.pos(lval) // our pos

View File

@@ -752,6 +752,73 @@ func TestLexParse0(t *testing.T) {
exp: exp, 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 for index, test := range values { // run all the tests
name, code, fail, exp := test.name, test.code, test.fail, test.exp 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) { if !reflect.DeepEqual(ast, exp) {
t.Errorf("test #%d: AST did not match expected", index) t.Errorf("test #%d: AST did not match expected", index)
// TODO: consider making our own recursive print function // TODO: consider making our own recursive print function
t.Logf("test #%d: actual: \n%s", index, spew.Sdump(ast)) t.Logf("test #%d: actual: \n\n%s\n", index, spew.Sdump(ast))
t.Logf("test #%d: expected: \n%s", index, spew.Sdump(exp)) t.Logf("test #%d: expected: \n\n%s", index, spew.Sdump(exp))
continue continue
} }
} }

View File

@@ -65,6 +65,9 @@ func init() {
resFields []*StmtResField resFields []*StmtResField
resField *StmtResField resField *StmtResField
edgeHalfList []*StmtEdgeHalf
edgeHalf *StmtEdgeHalf
} }
%token OPEN_CURLY CLOSE_CURLY %token OPEN_CURLY CLOSE_CURLY
@@ -74,10 +77,10 @@ func init() {
%token STRING BOOL INTEGER FLOAT %token STRING BOOL INTEGER FLOAT
%token EQUALS %token EQUALS
%token COMMA COLON SEMICOLON %token COMMA COLON SEMICOLON
%token ROCKET %token ROCKET ARROW DOT
%token STR_IDENTIFIER BOOL_IDENTIFIER INT_IDENTIFIER FLOAT_IDENTIFIER %token STR_IDENTIFIER BOOL_IDENTIFIER INT_IDENTIFIER FLOAT_IDENTIFIER
%token STRUCT_IDENTIFIER VARIANT_IDENTIFIER VAR_IDENTIFIER IDENTIFIER %token STRUCT_IDENTIFIER VARIANT_IDENTIFIER VAR_IDENTIFIER IDENTIFIER
%token VAR_IDENTIFIER_HX %token VAR_IDENTIFIER_HX CAPITALIZED_IDENTIFIER
%token COMMENT ERROR %token COMMENT ERROR
// precedence table // precedence table
@@ -158,6 +161,11 @@ stmt:
posLast(yylex, yyDollar) // our pos posLast(yylex, yyDollar) // our pos
$$.stmt = $1.stmt $$.stmt = $1.stmt
} }
| edge
{
posLast(yylex, yyDollar) // our pos
$$.stmt = $1.stmt
}
| IF expr OPEN_CURLY prog CLOSE_CURLY | IF expr OPEN_CURLY prog CLOSE_CURLY
{ {
posLast(yylex, yyDollar) // our pos 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: type:
BOOL_IDENTIFIER BOOL_IDENTIFIER
{ {

View File

@@ -836,6 +836,7 @@ func (obj *StmtProg) Graph() (*pgraph.Graph, error) {
// called by this Output function if they are needed to produce the output. // called by this Output function if they are needed to produce the output.
func (obj *StmtProg) Output() (*interfaces.Output, error) { func (obj *StmtProg) Output() (*interfaces.Output, error) {
resources := []resources.Res{} resources := []resources.Res{}
edges := []*interfaces.Edge{}
for _, stmt := range obj.Prog { for _, stmt := range obj.Prog {
output, err := stmt.Output() output, err := stmt.Output()
@@ -844,13 +845,13 @@ func (obj *StmtProg) Output() (*interfaces.Output, error) {
} }
if output != nil { if output != nil {
resources = append(resources, output.Resources...) resources = append(resources, output.Resources...)
//edges = append(edges, output.Edges) edges = append(edges, output.Edges...)
} }
} }
return &interfaces.Output{ return &interfaces.Output{
Resources: resources, Resources: resources,
//Edges: edges, Edges: edges,
}, nil }, nil
} }

View File

@@ -83,6 +83,11 @@ func (obj *PrintRes) Watch() error {
// CheckApply method for Print resource. Does nothing, returns happy! // CheckApply method for Print resource. Does nothing, returns happy!
func (obj *PrintRes) CheckApply(apply bool) (checkOK bool, err error) { func (obj *PrintRes) CheckApply(apply bool) (checkOK bool, err error) {
log.Printf("%s: CheckApply: %t", obj, apply) 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() { if obj.Refresh() {
log.Printf("%s: Received a notification!", obj) log.Printf("%s: Received a notification!", obj)
} }