diff --git a/docs/language-guide.md b/docs/language-guide.md index d891bc52..7aa44148 100644 --- a/docs/language-guide.md +++ b/docs/language-guide.md @@ -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 { + + } else { + # the else branch is optional for if 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 diff --git a/examples/lang/edges0.mcl b/examples/lang/edges0.mcl new file mode 100644 index 00000000..2fabe06a --- /dev/null +++ b/examples/lang/edges0.mcl @@ -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"] diff --git a/examples/lang/password0.mcl b/examples/lang/password0.mcl new file mode 100644 index 00000000..11a02fdc --- /dev/null +++ b/examples/lang/password0.mcl @@ -0,0 +1,8 @@ +password "pass0" { + length => 8, +} + +file "/tmp/mgmt/password" { +} + +Password["pass0"].password -> File["/tmp/mgmt/password"].content diff --git a/examples/lang/sendrecv0.mcl b/examples/lang/sendrecv0.mcl new file mode 100644 index 00000000..e75f9688 --- /dev/null +++ b/examples/lang/sendrecv0.mcl @@ -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 diff --git a/examples/lang/sendrecv1.mcl b/examples/lang/sendrecv1.mcl new file mode 100644 index 00000000..8f06af0f --- /dev/null +++ b/examples/lang/sendrecv1.mcl @@ -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"] diff --git a/examples/lang/sendrecv2.mcl b/examples/lang/sendrecv2.mcl new file mode 100644 index 00000000..7b16da1c --- /dev/null +++ b/examples/lang/sendrecv2.mcl @@ -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 diff --git a/examples/lang/sendrecv3.mcl b/examples/lang/sendrecv3.mcl new file mode 100644 index 00000000..898c1e98 --- /dev/null +++ b/examples/lang/sendrecv3.mcl @@ -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), + } +} diff --git a/examples/lang/states0.mcl b/examples/lang/states0.mcl index f862e715..d3e46fc9 100644 --- a/examples/lang/states0.mcl +++ b/examples/lang/states0.mcl @@ -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}"] } diff --git a/lang/interpret_test.go b/lang/interpret_test.go index dc0d0740..060425c0 100644 --- a/lang/interpret_test.go +++ b/lang/interpret_test.go @@ -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 diff --git a/lang/lexer.nex b/lang/lexer.nex index 13ca8268..1367c6d2 100644 --- a/lang/lexer.nex +++ b/lang/lexer.nex @@ -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 diff --git a/lang/lexparse_test.go b/lang/lexparse_test.go index 467a7a6d..c4477549 100644 --- a/lang/lexparse_test.go +++ b/lang/lexparse_test.go @@ -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 } } diff --git a/lang/parser.y b/lang/parser.y index 804d1c14..616b932e 100644 --- a/lang/parser.y +++ b/lang/parser.y @@ -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 { diff --git a/lang/structs.go b/lang/structs.go index af95c81e..1f7efb09 100644 --- a/lang/structs.go +++ b/lang/structs.go @@ -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 } diff --git a/resources/print.go b/resources/print.go index d0062fe2..9d25a1ff 100644 --- a/resources/print.go +++ b/resources/print.go @@ -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) }