From 90fd8023dd71fccaba9433dedac420465b6ed3e9 Mon Sep 17 00:00:00 2001 From: James Shubin Date: Wed, 29 Jan 2020 10:49:48 -0500 Subject: [PATCH] lang, engine: Add a facility for resources to export constants Since we focus on safety, it would be nice to reduce the chance of any runtime errors if we made a typo for a resource parameter. With this patch, each resource can export constants into the global namespace so that typos would cause a compile error. Of course in the future if we had a more advanced type system, then we could support precise types for each individual resource param, but in an attempt to keep things simple, we'll leave that for another day. It would add complexity too if we ever wanted to store a parameter externally. Lastly, we might consider adding "special case" parsing so that directly specified fields would parse intelligently. For example, we could allow: file "/tmp/hello" { state => exists, # magic sugar! } This isn't supported for now, but if it works after all the other parser changes have been made, it might be something to consider. --- README.md | 2 +- docs/faq.md | 2 +- docs/language-guide.md | 2 +- engine/resources/file.go | 36 ++++- engine/resources/resources_test.go | 58 +++---- examples/lang/autoedges1.mcl | 4 +- examples/lang/contains0.mcl | 4 +- examples/lang/cron2.mcl | 2 +- examples/lang/cron3.mcl | 2 +- examples/lang/cron4.mcl | 2 +- examples/lang/datetime1.mcl | 2 +- examples/lang/datetime2.mcl | 2 +- examples/lang/datetime3.mcl | 2 +- examples/lang/datetime4.mcl | 2 +- examples/lang/elvis0.mcl | 2 +- examples/lang/exchange0.mcl | 2 +- examples/lang/exec0.mcl | 2 +- examples/lang/file0.mcl | 4 +- examples/lang/frag1.mcl | 12 +- examples/lang/hello0.mcl | 2 +- examples/lang/history1.mcl | 2 +- examples/lang/hostname0.mcl | 2 +- examples/lang/html.mcl | 2 +- examples/lang/hysteresis1.mcl | 2 +- examples/lang/interpolate1.mcl | 2 +- examples/lang/os.mcl | 8 +- examples/lang/password0.mcl | 2 +- examples/lang/purge1.mcl | 8 +- examples/lang/readfile1.mcl | 2 +- examples/lang/readonlyfriday.mcl | 2 +- examples/lang/reverse1.mcl | 4 +- examples/lang/reverse2.mcl | 4 +- examples/lang/reverse3.mcl | 4 +- examples/lang/runtime.mcl | 2 +- examples/lang/schedule0.mcl | 2 +- examples/lang/sendrecv0.mcl | 2 +- examples/lang/sendrecv1.mcl | 2 +- examples/lang/states0.mcl | 6 +- examples/lang/tftp0.mcl | 6 +- examples/lang/unicode.mcl | 2 +- examples/lang/virt2.mcl | 2 +- integration/basic_test.go | 8 +- lang/funcs/vars/vars.go | 136 ++++++++++++++++ lang/gapi.go | 19 ++- lang/interfaces/var.go | 28 ++++ lang/interpret_test.go | 41 +++-- .../interpret_test/TestAstFunc2/const1.output | 1 + .../TestAstFunc2/const1/main.mcl | 2 + .../TestAstFunc2/file-fragments1/main.mcl | 12 +- .../TestAstFunc2/send-recv-0/main.mcl | 2 +- .../TestAstFunc2/send-recv-1/main.mcl | 2 +- .../TestAstFunc2/send-recv-2/main.mcl | 2 +- lang/lang.go | 19 ++- lang/structs.go | 5 +- lang/unification_test.go | 22 ++- lang/util.go | 149 ++++++++++++++++++ test/shell/env0.mcl | 2 +- test/shell/exchange0.mcl | 2 +- test/shell/file-source.mcl | 2 +- test/shell/load0.sh | 2 +- util/code_test.go | 4 +- 61 files changed, 536 insertions(+), 138 deletions(-) create mode 100644 lang/funcs/vars/vars.go create mode 100644 lang/interfaces/var.go create mode 100644 lang/interpret_test/TestAstFunc2/const1.output create mode 100644 lang/interpret_test/TestAstFunc2/const1/main.mcl diff --git a/README.md b/README.md index fb893de6..bb83eb59 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ ensure that your file server is set to read-only when it's friday. import "datetime" $is_friday = datetime.weekday(datetime.now()) == "friday" file "/srv/files/" { - state => "exists", + state => $const.res.file.state.exists, mode => if $is_friday { # this updates the mode, the instant it changes! "0550" } else { diff --git a/docs/faq.md b/docs/faq.md index 926cadf4..819cbeca 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -242,7 +242,7 @@ gets created in case it is not present, then you must also specify the state: ``` file "/tmp/foo" { - state => "exists", + state => $const.res.file.state.exists, content => "hello world\n", } ``` diff --git a/docs/language-guide.md b/docs/language-guide.md index f6371324..09bd648e 100644 --- a/docs/language-guide.md +++ b/docs/language-guide.md @@ -206,7 +206,7 @@ value to use if that boolean is true. You can do this with the resource-specific $b = true # change me to false and then try editing the file manually file "/tmp/mgmt-elvis" { content => $b ?: "hello world\n", - state => "exists", + state => $const.res.file.state.exists, } ``` diff --git a/engine/resources/file.go b/engine/resources/file.go index 79383e3d..401f59b1 100644 --- a/engine/resources/file.go +++ b/engine/resources/file.go @@ -35,16 +35,46 @@ import ( "github.com/purpleidea/mgmt/engine" "github.com/purpleidea/mgmt/engine/traits" engineUtil "github.com/purpleidea/mgmt/engine/util" + "github.com/purpleidea/mgmt/lang/funcs/vars" + "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/lang/types" "github.com/purpleidea/mgmt/recwatch" "github.com/purpleidea/mgmt/util" "github.com/purpleidea/mgmt/util/errwrap" ) func init() { - engine.RegisterResource("file", func() engine.Res { return &FileRes{} }) + engine.RegisterResource(KindFile, func() engine.Res { return &FileRes{} }) + + // const.res.file.state.exists = "exists" + // const.res.file.state.absent = "absent" + vars.RegisterResourceParams(KindFile, map[string]map[string]func() interfaces.Var{ + ParamFileState: { + FileStateExists: func() interfaces.Var { + return &types.StrValue{ + V: FileStateExists, + } + }, + FileStateAbsent: func() interfaces.Var { + return &types.StrValue{ + V: FileStateAbsent, + } + }, + // TODO: consider removing this field entirely + "undefined": func() interfaces.Var { + return &types.StrValue{ + V: FileStateUndefined, // empty string + } + }, + }, + }) } const ( + // KindFile is the kind string used to identify this resource. + KindFile = "file" + // ParamFileState is the name of the state field parameter. + ParamFileState = "state" // FileStateExists is the string that represents that the file should be // present. FileStateExists = "exists" @@ -975,7 +1005,7 @@ func (obj *FileRes) sourceCheckApply(apply bool) (bool, error) { // programming error return false, fmt.Errorf("not a Res") } - if res.Kind() != "file" { + if res.Kind() != KindFile { continue // only interested in files } if res.Name() == obj.Name() { @@ -1582,7 +1612,7 @@ func (obj *FileRes) Reversed() (engine.ReversibleRes, error) { func (obj *FileRes) GraphQueryAllowed(opts ...engine.GraphQueryableOption) error { options := &engine.GraphQueryableOptions{} // default options options.Apply(opts...) // apply the options - if options.Kind != "file" { + if options.Kind != KindFile { return fmt.Errorf("only other files can access my information") } return nil diff --git a/engine/resources/resources_test.go b/engine/resources/resources_test.go index a03c2d42..e54f571b 100644 --- a/engine/resources/resources_test.go +++ b/engine/resources/resources_test.go @@ -228,7 +228,7 @@ func TestResources1(t *testing.T) { p := "/tmp/whatever" s := "hello, world\n" res.Path = p - res.State = "exists" + res.State = FileStateExists contents := s res.Content = &contents @@ -292,7 +292,7 @@ func TestResources1(t *testing.T) { res := r.(*FileRes) // if this panics, the test will panic p := "/tmp/emptyfile" res.Path = p - res.State = "exists" + res.State = FileStateExists timeline := []Step{ NewStartupStep(1000 * 60), // startup @@ -316,7 +316,7 @@ func TestResources1(t *testing.T) { res := r.(*FileRes) // if this panics, the test will panic p := "/tmp/existingfile" res.Path = p - res.State = "exists" + res.State = FileStateExists content := "some existing text\n" timeline := []Step{ @@ -811,14 +811,14 @@ func TestResources2(t *testing.T) { testCases := []test{} { //file "/tmp/somefile" { - // state => "exists", + // state => $const.res.file.state.exists, // content => "some new text\n", //} r1 := makeRes("file", "r1") res := r1.(*FileRes) // if this panics, the test will panic p := "/tmp/somefile" res.Path = p - res.State = "exists" + res.State = FileStateExists content := "some new text\n" res.Content = &content @@ -850,7 +850,7 @@ func TestResources2(t *testing.T) { res := r1.(*FileRes) // if this panics, the test will panic p := "/tmp/somefile" res.Path = p - //res.State = "exists" // not specified! + //res.State = FileStateExists // not specified! content := "some new text\n" res.Content = &content @@ -883,7 +883,7 @@ func TestResources2(t *testing.T) { res := r1.(*FileRes) // if this panics, the test will panic p := "/tmp/somefile" res.Path = p - //res.State = "exists" // not specified! + //res.State = FileStateExists // not specified! content := "some new text\n" res.Content = &content @@ -907,14 +907,14 @@ func TestResources2(t *testing.T) { } { //file "/tmp/somefile" { - // state => "absent", + // state => $const.res.file.state.absent, //} // and no existing file exists! r1 := makeRes("file", "r1") res := r1.(*FileRes) // if this panics, the test will panic p := "/tmp/somefile" res.Path = p - res.State = "absent" + res.State = FileStateAbsent timeline := []func() error{ fileRemove(p), // nothing here @@ -936,14 +936,14 @@ func TestResources2(t *testing.T) { } { //file "/tmp/somefile" { - // state => "absent", + // state => $const.res.file.state.absent, //} // and a file already exists! r1 := makeRes("file", "r1") res := r1.(*FileRes) // if this panics, the test will panic p := "/tmp/somefile" res.Path = p - res.State = "absent" + res.State = FileStateAbsent timeline := []func() error{ fileWrite(p, "whatever"), @@ -966,7 +966,7 @@ func TestResources2(t *testing.T) { { //file "/tmp/somefile" { // content => "some new text\n", - // state => "exists", + // state => $const.res.file.state.exists, // // Meta:reverse => true, //} @@ -974,7 +974,7 @@ func TestResources2(t *testing.T) { res := r1.(*FileRes) // if this panics, the test will panic p := "/tmp/somefile" res.Path = p - res.State = "exists" + res.State = FileStateExists content := "some new text\n" res.Content = &content original := "this is the original state\n" // original state @@ -1035,7 +1035,7 @@ func TestResources2(t *testing.T) { res := r1.(*FileRes) // if this panics, the test will panic p := "/tmp/somefile" res.Path = p - //res.State = "exists" // unspecified + //res.State = FileStateExists // unspecified content := "some new text\n" res.Content = &content original := "this is the original state\n" // original state @@ -1100,7 +1100,7 @@ func TestResources2(t *testing.T) { res := r1.(*FileRes) // if this panics, the test will panic p := "/tmp/somefile" res.Path = p - //res.State = "exists" // unspecified + //res.State = FileStateExists // unspecified content := "some new text\n" res.Content = &content var r2 engine.Res // future reversed resource @@ -1149,7 +1149,7 @@ func TestResources2(t *testing.T) { } { //file "/tmp/somefile" { - // state => "absent", + // state => $const.res.file.state.absent, // // Meta:reverse => true, //} @@ -1157,7 +1157,7 @@ func TestResources2(t *testing.T) { res := r1.(*FileRes) // if this panics, the test will panic p := "/tmp/somefile" res.Path = p - res.State = "absent" + res.State = FileStateAbsent original := "this is the original state\n" // original state var r2 engine.Res // future reversed resource @@ -1207,7 +1207,7 @@ func TestResources2(t *testing.T) { } { //file "/tmp/somefile" { - // state => "exists", + // state => $const.res.file.state.exists, // fragments => [ // "/tmp/frag1", // "/tmp/fragdir1/", @@ -1220,7 +1220,7 @@ func TestResources2(t *testing.T) { res := r1.(*FileRes) // if this panics, the test will panic p := "/tmp/somefile" res.Path = p - res.State = "exists" + res.State = FileStateExists res.Fragments = []string{ "/tmp/frag1", "/tmp/fragdir1/", @@ -1272,7 +1272,7 @@ func TestResources2(t *testing.T) { } { //file "/tmp/somefile" { - // state => "exists", + // state => $const.res.file.state.exists, // source => "/tmp/somefiletocopy", //} r1 := makeRes("file", "r1") @@ -1281,7 +1281,7 @@ func TestResources2(t *testing.T) { p2 := "/tmp/somefiletocopy" content := "hello this is some file to copy\n" res.Path = p - res.State = "exists" + res.State = FileStateExists res.Source = p2 timeline := []func() error{ @@ -1308,13 +1308,13 @@ func TestResources2(t *testing.T) { } { //file "/tmp/somedir/" { - // state => "exists", + // state => $const.res.file.state.exists, //} r1 := makeRes("file", "r1") res := r1.(*FileRes) // if this panics, the test will panic p := "/tmp/somedir/" res.Path = p - res.State = "exists" + res.State = FileStateExists timeline := []func() error{ fileAbsent(p), // ensure it's absent @@ -1337,7 +1337,7 @@ func TestResources2(t *testing.T) { } { //file "/tmp/somedir/" { - // state => "exists", + // state => $const.res.file.state.exists, // source => /tmp/somedirtocopy/, // recurse => true, //} @@ -1346,7 +1346,7 @@ func TestResources2(t *testing.T) { p := "/tmp/somedir/" p2 := "/tmp/somedirtocopy/" res.Path = p - res.State = "exists" + res.State = FileStateExists res.Source = p2 res.Recurse = true @@ -1408,7 +1408,7 @@ func TestResources2(t *testing.T) { } { //file "/tmp/somedir/" { - // state => "exists", + // state => $const.res.file.state.exists, // recurse => true, // purge => true, //} @@ -1416,7 +1416,7 @@ func TestResources2(t *testing.T) { res := r1.(*FileRes) // if this panics, the test will panic p := "/tmp/somedir/" res.Path = p - res.State = "exists" + res.State = FileStateExists res.Recurse = true res.Purge = true @@ -1474,7 +1474,7 @@ func TestResources2(t *testing.T) { } { //file "/tmp/somedir/" { - // state => "exists", + // state => $const.res.file.state.exists, // recurse => true, // purge => true, //} @@ -1489,7 +1489,7 @@ func TestResources2(t *testing.T) { res := r1.(*FileRes) // if this panics, the test will panic p := "/tmp/somedir/" res.Path = p - res.State = "exists" + res.State = FileStateExists res.Recurse = true res.Purge = true diff --git a/examples/lang/autoedges1.mcl b/examples/lang/autoedges1.mcl index 62e3ab29..ddbe5bcb 100644 --- a/examples/lang/autoedges1.mcl +++ b/examples/lang/autoedges1.mcl @@ -8,14 +8,14 @@ pkg "drbd-utils" { file "/etc/drbd.conf" { content => "this is an mgmt test", - state => "exists", + state => $const.res.file.state.exists, Meta:autoedge => true, Meta:noop => $noop, } file "/etc/drbd.d/" { - state => "exists", + state => $const.res.file.state.exists, Meta:autoedge => true, Meta:noop => $noop, diff --git a/examples/lang/contains0.mcl b/examples/lang/contains0.mcl index 58d94b66..5f4fd61c 100644 --- a/examples/lang/contains0.mcl +++ b/examples/lang/contains0.mcl @@ -11,7 +11,7 @@ $c4 = "b" in $set $s = fmt.printf("1: %t, 2: %t, 3: %t, 4: %t\n", $c1, $c2, $c3, $c4) file "/tmp/mgmt/contains" { - state => "exists", + state => $const.res.file.state.exists, content => $s, } @@ -22,6 +22,6 @@ $x = if sys.hostname() in ["h1", "h3",] { } file "/tmp/mgmt/hello-${sys.hostname()}" { - state => "exists", + state => $const.res.file.state.exists, content => $x, } diff --git a/examples/lang/cron2.mcl b/examples/lang/cron2.mcl index 5e0fd660..df4094d5 100644 --- a/examples/lang/cron2.mcl +++ b/examples/lang/cron2.mcl @@ -5,5 +5,5 @@ cron "purpleidea-oneshot" { svc "purpleidea-oneshot" {} -# TODO: do we need a state => "exists" specified here? +# TODO: do we need a state => $const.res.file.state.exists specified here? file "/etc/systemd/system/purpleidea-oneshot.service" {} diff --git a/examples/lang/cron3.mcl b/examples/lang/cron3.mcl index ea3f3850..c9b4db8b 100644 --- a/examples/lang/cron3.mcl +++ b/examples/lang/cron3.mcl @@ -10,5 +10,5 @@ svc "purpleidea-oneshot" { session => true, } -# TODO: do we need a state => "exists" specified here? +# TODO: do we need a state => $const.res.file.state.exists specified here? file printf("%s/.config/systemd/user/purpleidea-oneshot.service", $home) {} diff --git a/examples/lang/cron4.mcl b/examples/lang/cron4.mcl index 5a6357dc..aa2951b8 100644 --- a/examples/lang/cron4.mcl +++ b/examples/lang/cron4.mcl @@ -13,5 +13,5 @@ svc "purpleidea-oneshot" { } file printf("%s/.config/systemd/user/purpleidea-oneshot.service", $home) { - state => "absent", + state => $const.res.file.state.absent, } diff --git a/examples/lang/datetime1.mcl b/examples/lang/datetime1.mcl index 9dc5b7ac..fee9f0eb 100644 --- a/examples/lang/datetime1.mcl +++ b/examples/lang/datetime1.mcl @@ -2,6 +2,6 @@ import "datetime" $d = datetime.now() file "/tmp/mgmt/datetime" { - state => "exists", + state => $const.res.file.state.exists, content => template("Hello! It is now: {{ datetime_print . }}\n", $d), } diff --git a/examples/lang/datetime2.mcl b/examples/lang/datetime2.mcl index 735a71ba..1bc93367 100644 --- a/examples/lang/datetime2.mcl +++ b/examples/lang/datetime2.mcl @@ -12,7 +12,7 @@ $theload = structlookup(sys.load(), "x1") if 5 > 3 { file "/tmp/mgmt/datetime" { - state => "exists", + state => $const.res.file.state.exists, content => template("Now + 1 year is: {{ .year }} seconds, aka: {{ datetime_print .year }}\n\nload average: {{ .load }}\n", $tmplvalues), } } diff --git a/examples/lang/datetime3.mcl b/examples/lang/datetime3.mcl index 2b9fab00..8d6bec7c 100644 --- a/examples/lang/datetime3.mcl +++ b/examples/lang/datetime3.mcl @@ -14,6 +14,6 @@ $theload = structlookup(sys.load(), "x1") $vumeter = example.vumeter("====", 10, 0.9) file "/tmp/mgmt/datetime" { - state => "exists", + state => $const.res.file.state.exists, content => template("Now + 1 year is: {{ .year }} seconds, aka: {{ datetime_print .year }}\n\nload average: {{ .load }}\n\nvu: {{ .vumeter }}\n", $tmplvalues), } diff --git a/examples/lang/datetime4.mcl b/examples/lang/datetime4.mcl index 29a10de2..2810f8ff 100644 --- a/examples/lang/datetime4.mcl +++ b/examples/lang/datetime4.mcl @@ -5,6 +5,6 @@ import "example" $now = datetime.now() file "/tmp/mgmt-datetime" { - state => "exists", + state => $const.res.file.state.exists, content => template("Il est l'or Monseignor: {{ . }}\n", datetime.format($now, "15:04:05")), } diff --git a/examples/lang/elvis0.mcl b/examples/lang/elvis0.mcl index bfc916cd..35311b7b 100644 --- a/examples/lang/elvis0.mcl +++ b/examples/lang/elvis0.mcl @@ -1,5 +1,5 @@ $b = true # change me to false and then try editing the file manually file "/tmp/mgmt-elvis" { content => $b ?: "hello world\n", - state => "exists", + state => $const.res.file.state.exists, } diff --git a/examples/lang/exchange0.mcl b/examples/lang/exchange0.mcl index 933886e9..1f0af4ab 100644 --- a/examples/lang/exchange0.mcl +++ b/examples/lang/exchange0.mcl @@ -13,6 +13,6 @@ $rand = random1(8) $exchanged = world.exchange("keyns", $rand) file "/tmp/mgmt/exchange-${sys.hostname()}" { - state => "exists", + state => $const.res.file.state.exists, content => template("Found: {{ . }}\n", $exchanged), } diff --git a/examples/lang/exec0.mcl b/examples/lang/exec0.mcl index 3c3d0023..429cf8d7 100644 --- a/examples/lang/exec0.mcl +++ b/examples/lang/exec0.mcl @@ -5,7 +5,7 @@ exec "exec1" { } file "/tmp/whatever" { - state => "exists", + state => $const.res.file.state.exists, } exec "exec2" { diff --git a/examples/lang/file0.mcl b/examples/lang/file0.mcl index 51222f48..74bc5621 100644 --- a/examples/lang/file0.mcl +++ b/examples/lang/file0.mcl @@ -1,7 +1,7 @@ file "/tmp/file1" { - state => "exists", + state => $const.res.file.state.exists, } file "/tmp/dir1/" { - state => "exists", + state => $const.res.file.state.exists, } diff --git a/examples/lang/frag1.mcl b/examples/lang/frag1.mcl index a8dc481f..7d0991c7 100644 --- a/examples/lang/frag1.mcl +++ b/examples/lang/frag1.mcl @@ -1,31 +1,31 @@ file "/tmp/frags/" { - state => "exists", + state => $const.res.file.state.exists, } # fragments file "/tmp/frags/f1" { - state => "exists", + state => $const.res.file.state.exists, content => "i am f1\n", } file "/tmp/frags/f2" { - state => "exists", + state => $const.res.file.state.exists, content => "i am f2\n", } file "/tmp/frags/f3" { - state => "exists", + state => $const.res.file.state.exists, content => "i am f3\n", } # You can also drop in an unmanaged file into the frags dir for it to get used! # And of course you can hard-code the list of files to use like this one is... file "/tmp/bonus_file" { - state => "exists", + state => $const.res.file.state.exists, content => "i am a bonus file\n", } # automatic edges will get added! file "/tmp/whole1" { - state => "exists", + state => $const.res.file.state.exists, fragments => [ "/tmp/frags/", # pull from this dir "/tmp/bonus_file", # also pull this one file diff --git a/examples/lang/hello0.mcl b/examples/lang/hello0.mcl index 7c90489a..05d5f911 100644 --- a/examples/lang/hello0.mcl +++ b/examples/lang/hello0.mcl @@ -1,4 +1,4 @@ file "/tmp/mgmt-hello-world" { content => "hello world from @purpleidea\n", - state => "exists", + state => $const.res.file.state.exists, } diff --git a/examples/lang/history1.mcl b/examples/lang/history1.mcl index 3ce8bcb8..da68ba25 100644 --- a/examples/lang/history1.mcl +++ b/examples/lang/history1.mcl @@ -5,6 +5,6 @@ $dt = datetime.now() $hystvalues = {"ix0" => $dt, "ix1" => $dt{1}, "ix2" => $dt{2}, "ix3" => $dt{3},} file "/tmp/mgmt/history" { - state => "exists", + state => $const.res.file.state.exists, content => template("Index(0) {{.ix0}}: {{ datetime_print .ix0 }}\nIndex(1) {{.ix1}}: {{ datetime_print .ix1 }}\nIndex(2) {{.ix2}}: {{ datetime_print .ix2 }}\nIndex(3) {{.ix3}}: {{ datetime_print .ix3 }}\n", $hystvalues), } diff --git a/examples/lang/hostname0.mcl b/examples/lang/hostname0.mcl index 264e410a..9ebb4de5 100644 --- a/examples/lang/hostname0.mcl +++ b/examples/lang/hostname0.mcl @@ -2,5 +2,5 @@ import "sys" file "/tmp/mgmt/${sys.hostname()}" { content => "hello from ${sys.hostname()}!\n", - state => "exists", + state => $const.res.file.state.exists, } diff --git a/examples/lang/html.mcl b/examples/lang/html.mcl index e71cfc71..1f319135 100644 --- a/examples/lang/html.mcl +++ b/examples/lang/html.mcl @@ -5,6 +5,6 @@ $text1 = html.unescape_string("<h1>MGMT!</h1>") $text2 = html.escape_string("Test & Re-Test\n") file "/tmp/index.html" { - state => "exists", + state => $const.res.file.state.exists, content => "${text1}${text2}", } diff --git a/examples/lang/hysteresis1.mcl b/examples/lang/hysteresis1.mcl index 4e8ff119..ae643ddd 100644 --- a/examples/lang/hysteresis1.mcl +++ b/examples/lang/hysteresis1.mcl @@ -1,7 +1,7 @@ import "sys" file "/tmp/mgmt/systemload" { - state => "exists", + state => $const.res.file.state.exists, content => template("load average: {{ .load }} threshold: {{ .threshold }}\n", $tmplvalues), } diff --git a/examples/lang/interpolate1.mcl b/examples/lang/interpolate1.mcl index 41fa954c..4a458b07 100644 --- a/examples/lang/interpolate1.mcl +++ b/examples/lang/interpolate1.mcl @@ -1,5 +1,5 @@ $audience = "WORLD!" file "/tmp/mgmt/hello" { content => "hello ${audience}!\n", - state => "exists", + state => $const.res.file.state.exists, } diff --git a/examples/lang/os.mcl b/examples/lang/os.mcl index 937189c7..890f6e43 100644 --- a/examples/lang/os.mcl +++ b/examples/lang/os.mcl @@ -5,23 +5,23 @@ import "fmt" $tmpdir = os.temp_dir() file "${tmpdir}/execinfo" { - state => "exists", + state => $const.res.file.state.exists, content => fmt.printf("mgmt is at %s\n", os.executable()), } file "${tmpdir}/mgmtenv" { - state => "exists", + state => $const.res.file.state.exists, content => os.expand_env("$HOME sweet ${os.getenv(\"HOME\")}\n"), } file "${tmpdir}/mgmtos" { - state => "exists", + state => $const.res.file.state.exists, content => os.readlink("/bin"), } $rm = exec.look_path("rm") file "${tmpdir}/cache" { - state => "exists", + state => $const.res.file.state.exists, content => "Plz cache in ${os.user_cache_dir()}.\nYour home is ${os.user_home_dir()}. Remove with ${rm}\n", } diff --git a/examples/lang/password0.mcl b/examples/lang/password0.mcl index 8f3990fb..778af3a2 100644 --- a/examples/lang/password0.mcl +++ b/examples/lang/password0.mcl @@ -3,7 +3,7 @@ password "pass0" { } file "/tmp/mgmt/password" { - state => "exists", + state => $const.res.file.state.exists, } Password["pass0"].password -> File["/tmp/mgmt/password"].content diff --git a/examples/lang/purge1.mcl b/examples/lang/purge1.mcl index d84c1d35..d505b49b 100644 --- a/examples/lang/purge1.mcl +++ b/examples/lang/purge1.mcl @@ -1,19 +1,19 @@ file "/tmp/some_dir/" { - state => "exists", + state => $const.res.file.state.exists, #source => "", # this default means empty directory recurse => true, purge => true, } file "/tmp/some_dir/fileA" { - state => "exists", + state => $const.res.file.state.exists, content => "i am fileA\n", } file "/tmp/some_dir/fileB" { - state => "exists", + state => $const.res.file.state.exists, content => "i am fileB\n", } file "/tmp/some_dir/fileC" { - state => "exists", + state => $const.res.file.state.exists, content => "i am fileC\n", } diff --git a/examples/lang/readfile1.mcl b/examples/lang/readfile1.mcl index 37ae2701..161d58ca 100644 --- a/examples/lang/readfile1.mcl +++ b/examples/lang/readfile1.mcl @@ -2,6 +2,6 @@ import "os" # this copies the contents from /tmp/input and puts them in /tmp/output file "/tmp/output" { - state => "exists", + state => $const.res.file.state.exists, content => os.readfile("/tmp/input"), } diff --git a/examples/lang/readonlyfriday.mcl b/examples/lang/readonlyfriday.mcl index 196d103e..581cfa7b 100644 --- a/examples/lang/readonlyfriday.mcl +++ b/examples/lang/readonlyfriday.mcl @@ -21,7 +21,7 @@ print "msg" { } file "/tmp/files/" { - state => "exists", + state => $const.res.file.state.exists, mode => if $is_friday { # this updates the mode, the instant it changes! "0550" } else { diff --git a/examples/lang/reverse1.mcl b/examples/lang/reverse1.mcl index d1bb9364..8579ec52 100644 --- a/examples/lang/reverse1.mcl +++ b/examples/lang/reverse1.mcl @@ -11,14 +11,14 @@ $mod3 = math.mod($now, 8) == 3 $mod = $mod0 || $mod1 || $mod2 || $mod3 file "/tmp/mgmt/" { - state => "exists", + state => $const.res.file.state.exists, } # file should disappear and re-appear every four seconds if $mod { file "/tmp/mgmt/hello" { content => "please say abracadabra...\n", - state => "exists", + state => $const.res.file.state.exists, Meta:reverse => true, } diff --git a/examples/lang/reverse2.mcl b/examples/lang/reverse2.mcl index b362435d..5e6203e5 100644 --- a/examples/lang/reverse2.mcl +++ b/examples/lang/reverse2.mcl @@ -11,14 +11,14 @@ $mod3 = math.mod($now, 8) == 3 $mod = $mod0 || $mod1 || $mod2 || $mod3 file "/tmp/mgmt/" { - state => "exists", + state => $const.res.file.state.exists, } # file should re-appear and disappear every four seconds # it will even preserve and then restore the pre-existing content! if $mod { file "/tmp/mgmt/hello" { - state => "absent", # delete the file + state => $const.res.file.state.absent, # delete the file Meta:reverse => true, } diff --git a/examples/lang/reverse3.mcl b/examples/lang/reverse3.mcl index c442ba73..16b29437 100644 --- a/examples/lang/reverse3.mcl +++ b/examples/lang/reverse3.mcl @@ -11,7 +11,7 @@ $mod3 = math.mod($now, 8) == 3 $mod = $mod0 || $mod1 || $mod2 || $mod3 file "/tmp/mgmt/" { - state => "exists", + state => $const.res.file.state.exists, } # file should change the mode every four seconds @@ -19,7 +19,7 @@ file "/tmp/mgmt/" { # you should create the file before you run this if $mod { file "/tmp/mgmt/hello" { - #state => "exists", # omit to see it change the mode only! + #state => $const.res.file.state.exists, # omit to see it change the mode only! mode => "0777", Meta:reverse => true, diff --git a/examples/lang/runtime.mcl b/examples/lang/runtime.mcl index 273e9cd0..6e5e22f5 100644 --- a/examples/lang/runtime.mcl +++ b/examples/lang/runtime.mcl @@ -2,6 +2,6 @@ import "golang/runtime" import "fmt" file "/tmp/mgmtinfo" { - state => "exists", + state => $const.res.file.state.exists, content => fmt.printf("Hi from mgmt! mgmt is running with %s; and GOROOT is %s.\n", runtime.version(), runtime.goroot()), } diff --git a/examples/lang/schedule0.mcl b/examples/lang/schedule0.mcl index 576c1161..3783cad6 100644 --- a/examples/lang/schedule0.mcl +++ b/examples/lang/schedule0.mcl @@ -17,6 +17,6 @@ $set = world.schedule("xsched", $opts) #$set = world.schedule("xsched") file "/tmp/mgmt/scheduled-${sys.hostname()}" { - state => "exists", + state => $const.res.file.state.exists, content => template("set: {{ . }}\n", $set), } diff --git a/examples/lang/sendrecv0.mcl b/examples/lang/sendrecv0.mcl index 82fdc1cd..77ec9128 100644 --- a/examples/lang/sendrecv0.mcl +++ b/examples/lang/sendrecv0.mcl @@ -4,7 +4,7 @@ exec "exec0" { } file ["/tmp/command-output", "/tmp/command-stdout", "/tmp/command-stderr",] { - state => "exists", + state => $const.res.file.state.exists, } Exec["exec0"].output -> File["/tmp/command-output"].content diff --git a/examples/lang/sendrecv1.mcl b/examples/lang/sendrecv1.mcl index 47652f75..076846b2 100644 --- a/examples/lang/sendrecv1.mcl +++ b/examples/lang/sendrecv1.mcl @@ -19,7 +19,7 @@ Exec["exec0"].output -> Kv["kv0"].value if $state != "default" { file "/tmp/mgmt/state" { - state => "exists", + state => $const.res.file.state.exists, content => fmt.printf("state: %s\n", $state), } } diff --git a/examples/lang/states0.mcl b/examples/lang/states0.mcl index 81dcc529..1a3a59a1 100644 --- a/examples/lang/states0.mcl +++ b/examples/lang/states0.mcl @@ -7,7 +7,7 @@ $state = maplookup($exchanged, $hostname, "default") if $state == "one" || $state == "default" { file "/tmp/mgmt/state" { - state => "exists", + state => $const.res.file.state.exists, content => "state: one\n", } @@ -23,7 +23,7 @@ if $state == "one" || $state == "default" { if $state == "two" { file "/tmp/mgmt/state" { - state => "exists", + state => $const.res.file.state.exists, content => "state: two\n", } @@ -39,7 +39,7 @@ if $state == "two" { if $state == "three" { file "/tmp/mgmt/state" { - state => "exists", + state => $const.res.file.state.exists, content => "state: three\n", } diff --git a/examples/lang/tftp0.mcl b/examples/lang/tftp0.mcl index 99658eca..dbe0c2c2 100644 --- a/examples/lang/tftp0.mcl +++ b/examples/lang/tftp0.mcl @@ -1,10 +1,10 @@ $root = "/tmp/tftproot/" file $root { - state => "exists", + state => $const.res.file.state.exists, } file "${root}file0" { content => "i'm file0 in ${root}\n", - state => "exists", + state => $const.res.file.state.exists, } tftp:server ":1069" { # by default tftp uses :69 but using :1069 avoids needing root! @@ -22,7 +22,7 @@ tftp:file "/file1" { $f2 = "/tmp/some_file" file $f2 { content => "i'm a cool file in /tmp\n", - state => "exists", + state => $const.res.file.state.exists, } # you can point to it directly... diff --git a/examples/lang/unicode.mcl b/examples/lang/unicode.mcl index 0381891b..b18bd32a 100644 --- a/examples/lang/unicode.mcl +++ b/examples/lang/unicode.mcl @@ -3,6 +3,6 @@ print "unicode" { msg => $unicode, } file "/tmp/unicode" { - state => "exists", + state => $const.res.file.state.exists, content => $unicode + "\n", } diff --git a/examples/lang/virt2.mcl b/examples/lang/virt2.mcl index 18352e90..3dcefaa6 100644 --- a/examples/lang/virt2.mcl +++ b/examples/lang/virt2.mcl @@ -17,7 +17,7 @@ $count = if $input > 8 { } file "/tmp/output" { - state => "exists", + state => $const.res.file.state.exists, content => fmt.printf("requesting: %d cpus\n", $count), } diff --git a/integration/basic_test.go b/integration/basic_test.go index 2146f871..17836b24 100644 --- a/integration/basic_test.go +++ b/integration/basic_test.go @@ -37,7 +37,7 @@ func TestInstance0(t *testing.T) { file "${root}/mgmt-hello-world" { content => "hello world from @purpleidea\n", - state => "exists", + state => $const.res.file.state.exists, } ` m := Instance{ @@ -82,7 +82,7 @@ func TestInstance1(t *testing.T) { file "${root}/mgmt-hello-world" { content => "hello world from @purpleidea\n", - state => "exists", + state => $const.res.file.state.exists, } `) testCases = append(testCases, test{ @@ -170,7 +170,7 @@ func TestCluster1(t *testing.T) { file "${root}/mgmt-hostname" { content => "i am ${sys.hostname()}\n", - state => "exists", + state => $const.res.file.state.exists, } `) testCases = append(testCases, test{ @@ -195,7 +195,7 @@ func TestCluster1(t *testing.T) { file "${root}/mgmt-hostname" { content => "i am ${sys.hostname()}\n", - state => "exists", + state => $const.res.file.state.exists, } `) testCases = append(testCases, test{ diff --git a/lang/funcs/vars/vars.go b/lang/funcs/vars/vars.go new file mode 100644 index 00000000..b8f7efe2 --- /dev/null +++ b/lang/funcs/vars/vars.go @@ -0,0 +1,136 @@ +// Mgmt +// Copyright (C) 2013-2020+ James Shubin and the project contributors +// Written by James Shubin and the project contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// Package vars provides a framework for language vars. +package vars + +import ( + "fmt" + "strings" + + "github.com/purpleidea/mgmt/lang/interfaces" +) + +const ( + // ConstNamespace is the string prefix for all top-level built-in vars. + ConstNamespace = "const" + + // ResourceNamespace is the string prefix for all top-level resource + // specific built-in vars, that exist under the ConstNamespace header. + ResourceNamespace = "res" +) + +// registeredVars is a global map of all possible vars which can be used. You +// should never touch this map directly. Use methods like Register instead. +var registeredVars = make(map[string]func() interfaces.Var) // must initialize + +// Register takes a var and its name and makes it available for use. It is +// commonly called in the init() method of the var at program startup. There is +// no matching Unregister function. +func Register(name string, fn func() interfaces.Var) { + if _, ok := registeredVars[name]; ok { + panic(fmt.Sprintf("a var named %s is already registered", name)) + } + //gob.Register(fn()) + registeredVars[name] = fn +} + +// ModuleRegister is exactly like Register, except that it registers within a +// named module. This is a helper function. +//func ModuleRegister(module, name string, v func() interfaces.Var) { +// Register(module+interfaces.ModuleSep+name, v) +//} + +// resourceConstHelper is a helper function to manage the const topology. +func resourceConstHelper(kind, param, field string) string { + // const.res.file.state.exists = "exists" + // TODO: should it be: const.res.file.params.state.exists = "exists" ? + chunks := []string{ + ConstNamespace, + ResourceNamespace, + kind, + param, + field, + } + //return ConstNamespace + interfaces.ModuleSep + ResourceNamespace + interfaces.ModuleSep + kind + interfaces.ModuleSep + param + interfaces.ModuleSep + field + return strings.Join(chunks, interfaces.ModuleSep) // cleaner code +} + +// RegisterResourceParam registers a single const param for a resource. You +// might prefer to use RegisterResourceParams instead. +func RegisterResourceParam(kind, param, field string, value func() interfaces.Var) { + Register(resourceConstHelper(kind, param, field), value) +} + +// RegisterResourceParams registers a map of const params for a resource. The +// params mapping keys are the param name and the param field name. Finally, the +// value is the specific type value for that constant. +func RegisterResourceParams(kind string, params map[string]map[string]func() interfaces.Var) { + for param, mapping := range params { + for field, value := range mapping { + Register(resourceConstHelper(kind, param, field), value) + } + } +} + +// Lookup returns a pointer to the var implementation. +func Lookup(name string) (interfaces.Var, error) { + f, exists := registeredVars[name] + if !exists { + return nil, fmt.Errorf("not found") + } + return f(), nil +} + +// LookupPrefix returns a map of names to vars that start with a module prefix. +// This search automatically adds the period separator. So if you want vars in +// the `const` prefix, search for `const`, not `const.` and it will find all the +// correctly registered vars. This removes that prefix from the result in the +// map keys that it returns. If you search for an empty prefix, then this will +// return all the top-level functions that aren't in a module. +func LookupPrefix(prefix string) map[string]func() interfaces.Var { + result := make(map[string]func() interfaces.Var) + for name, f := range registeredVars { + // requested top-level vars, and no module separators... + if prefix == "" { + if !strings.Contains(name, interfaces.ModuleSep) { + result[name] = f // copy + } + continue + } + sep := prefix + interfaces.ModuleSep + if !strings.HasPrefix(name, sep) { + continue + } + s := strings.TrimPrefix(name, sep) // remove the prefix + result[s] = f // copy + } + return result +} + +// Map returns a map from all registered var names to a function to return that +// one. We return a copy of our internal registered var store so that this +// result can be manipulated safely. We return the vars that produce the Var +// interface because we might use this result to create multiple vars, and each +// one might need to have its own unique memory address to work properly. +func Map() map[string]func() interfaces.Var { + m := make(map[string]func() interfaces.Var) + for name, fn := range registeredVars { // copy + m[name] = fn + } + return m +} diff --git a/lang/gapi.go b/lang/gapi.go index b78aca0f..d57774a0 100644 --- a/lang/gapi.go +++ b/lang/gapi.go @@ -24,6 +24,7 @@ import ( "sync" "github.com/purpleidea/mgmt/gapi" + "github.com/purpleidea/mgmt/lang/funcs/vars" "github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/unification" "github.com/purpleidea/mgmt/pgraph" @@ -264,13 +265,21 @@ func (obj *GAPI) Cli(cliInfo *gapi.CliInfo) (*gapi.Deploy, error) { return nil, errwrap.Wrapf(err, "could not interpolate AST") } + variables := map[string]interfaces.Expr{ + "purpleidea": &ExprStr{V: "hello world!"}, // james says hi + // TODO: change to a func when we can change hostname dynamically! + "hostname": &ExprStr{V: ""}, // NOTE: empty b/c not used + } + consts := VarPrefixToVariablesScope(vars.ConstNamespace) // strips prefix! + addback := vars.ConstNamespace + interfaces.ModuleSep // add it back... + variables, err = MergeExprMaps(variables, consts, addback) + if err != nil { + return nil, errwrap.Wrapf(err, "couldn't merge in consts") + } + // top-level, built-in, initial global scope scope := &interfaces.Scope{ - Variables: map[string]interfaces.Expr{ - "purpleidea": &ExprStr{V: "hello world!"}, // james says hi - // TODO: change to a func when we can change hostname dynamically! - "hostname": &ExprStr{V: ""}, // NOTE: empty b/c not used - }, + Variables: variables, // all the built-in top-level, core functions enter here... Functions: FuncPrefixToFunctionsScope(""), // runs funcs.LookupPrefix } diff --git a/lang/interfaces/var.go b/lang/interfaces/var.go new file mode 100644 index 00000000..98dfe39a --- /dev/null +++ b/lang/interfaces/var.go @@ -0,0 +1,28 @@ +// Mgmt +// Copyright (C) 2013-2020+ James Shubin and the project contributors +// Written by James Shubin and the project contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package interfaces + +import ( + "github.com/purpleidea/mgmt/lang/types" +) + +// Var is the interface that any valid stand-alone variable must fulfill. +// NOTE: It's just a simple Value from our types library at the moment. +type Var interface { + types.Value +} diff --git a/lang/interpret_test.go b/lang/interpret_test.go index d523eaea..a7fbb589 100644 --- a/lang/interpret_test.go +++ b/lang/interpret_test.go @@ -33,6 +33,7 @@ import ( "github.com/purpleidea/mgmt/engine/resources" "github.com/purpleidea/mgmt/etcd" "github.com/purpleidea/mgmt/lang/funcs" + "github.com/purpleidea/mgmt/lang/funcs/vars" "github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/unification" "github.com/purpleidea/mgmt/pgraph" @@ -555,12 +556,22 @@ func TestAstFunc1(t *testing.T) { return } t.Logf("tests directory is: %s", dir) + + variables := map[string]interfaces.Expr{ + "purpleidea": &ExprStr{V: "hello world!"}, // james says hi + // TODO: change to a func when we can change hostname dynamically! + "hostname": &ExprStr{V: ""}, // NOTE: empty b/c not used + } + consts := VarPrefixToVariablesScope(vars.ConstNamespace) // strips prefix! + addback := vars.ConstNamespace + interfaces.ModuleSep // add it back... + variables, err = MergeExprMaps(variables, consts, addback) + if err != nil { + t.Errorf("couldn't merge in consts: %+v", err) + return + } + scope := &interfaces.Scope{ // global scope - Variables: map[string]interfaces.Expr{ - "purpleidea": &ExprStr{V: "hello world!"}, // james says hi - // TODO: change to a func when we can change hostname dynamically! - "hostname": &ExprStr{V: ""}, // NOTE: empty b/c not used - }, + Variables: variables, // all the built-in top-level, core functions enter here... Functions: FuncPrefixToFunctionsScope(""), // runs funcs.LookupPrefix } @@ -977,12 +988,22 @@ func TestAstFunc2(t *testing.T) { return } t.Logf("tests directory is: %s", dir) + + variables := map[string]interfaces.Expr{ + "purpleidea": &ExprStr{V: "hello world!"}, // james says hi + // TODO: change to a func when we can change hostname dynamically! + "hostname": &ExprStr{V: ""}, // NOTE: empty b/c not used + } + consts := VarPrefixToVariablesScope(vars.ConstNamespace) // strips prefix! + addback := vars.ConstNamespace + interfaces.ModuleSep // add it back... + variables, err = MergeExprMaps(variables, consts, addback) + if err != nil { + t.Errorf("couldn't merge in consts: %+v", err) + return + } + scope := &interfaces.Scope{ // global scope - Variables: map[string]interfaces.Expr{ - "purpleidea": &ExprStr{V: "hello world!"}, // james says hi - // TODO: change to a func when we can change hostname dynamically! - "hostname": &ExprStr{V: ""}, // NOTE: empty b/c not used - }, + Variables: variables, // all the built-in top-level, core functions enter here... Functions: FuncPrefixToFunctionsScope(""), // runs funcs.LookupPrefix } diff --git a/lang/interpret_test/TestAstFunc2/const1.output b/lang/interpret_test/TestAstFunc2/const1.output new file mode 100644 index 00000000..29383fea --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/const1.output @@ -0,0 +1 @@ +Vertex: test[exists] diff --git a/lang/interpret_test/TestAstFunc2/const1/main.mcl b/lang/interpret_test/TestAstFunc2/const1/main.mcl new file mode 100644 index 00000000..eeed4a76 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/const1/main.mcl @@ -0,0 +1,2 @@ +# this magic string variable should exist and be "exists" +test $const.res.file.state.exists {} diff --git a/lang/interpret_test/TestAstFunc2/file-fragments1/main.mcl b/lang/interpret_test/TestAstFunc2/file-fragments1/main.mcl index a8dc481f..7d0991c7 100644 --- a/lang/interpret_test/TestAstFunc2/file-fragments1/main.mcl +++ b/lang/interpret_test/TestAstFunc2/file-fragments1/main.mcl @@ -1,31 +1,31 @@ file "/tmp/frags/" { - state => "exists", + state => $const.res.file.state.exists, } # fragments file "/tmp/frags/f1" { - state => "exists", + state => $const.res.file.state.exists, content => "i am f1\n", } file "/tmp/frags/f2" { - state => "exists", + state => $const.res.file.state.exists, content => "i am f2\n", } file "/tmp/frags/f3" { - state => "exists", + state => $const.res.file.state.exists, content => "i am f3\n", } # You can also drop in an unmanaged file into the frags dir for it to get used! # And of course you can hard-code the list of files to use like this one is... file "/tmp/bonus_file" { - state => "exists", + state => $const.res.file.state.exists, content => "i am a bonus file\n", } # automatic edges will get added! file "/tmp/whole1" { - state => "exists", + state => $const.res.file.state.exists, fragments => [ "/tmp/frags/", # pull from this dir "/tmp/bonus_file", # also pull this one file diff --git a/lang/interpret_test/TestAstFunc2/send-recv-0/main.mcl b/lang/interpret_test/TestAstFunc2/send-recv-0/main.mcl index 82fdc1cd..77ec9128 100644 --- a/lang/interpret_test/TestAstFunc2/send-recv-0/main.mcl +++ b/lang/interpret_test/TestAstFunc2/send-recv-0/main.mcl @@ -4,7 +4,7 @@ exec "exec0" { } file ["/tmp/command-output", "/tmp/command-stdout", "/tmp/command-stderr",] { - state => "exists", + state => $const.res.file.state.exists, } Exec["exec0"].output -> File["/tmp/command-output"].content diff --git a/lang/interpret_test/TestAstFunc2/send-recv-1/main.mcl b/lang/interpret_test/TestAstFunc2/send-recv-1/main.mcl index bbb88f6a..1ba8ecd5 100644 --- a/lang/interpret_test/TestAstFunc2/send-recv-1/main.mcl +++ b/lang/interpret_test/TestAstFunc2/send-recv-1/main.mcl @@ -4,7 +4,7 @@ exec "exec0" { } file "/tmp/command-output" { - state => "exists", + state => $const.res.file.state.exists, } # this is an error because the shell send key doesn't exist in exec diff --git a/lang/interpret_test/TestAstFunc2/send-recv-2/main.mcl b/lang/interpret_test/TestAstFunc2/send-recv-2/main.mcl index f88187b9..75afaf3c 100644 --- a/lang/interpret_test/TestAstFunc2/send-recv-2/main.mcl +++ b/lang/interpret_test/TestAstFunc2/send-recv-2/main.mcl @@ -4,7 +4,7 @@ exec ["exec0", "exec1",] { } file "/tmp/command-output" { - state => "exists", + state => $const.res.file.state.exists, } # this is an error because two senders cannot send to the same receiver key diff --git a/lang/lang.go b/lang/lang.go index f677dcfa..c05ecfc7 100644 --- a/lang/lang.go +++ b/lang/lang.go @@ -25,6 +25,7 @@ import ( "github.com/purpleidea/mgmt/engine" "github.com/purpleidea/mgmt/lang/funcs" _ "github.com/purpleidea/mgmt/lang/funcs/core" // import so the funcs register + "github.com/purpleidea/mgmt/lang/funcs/vars" "github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/unification" "github.com/purpleidea/mgmt/pgraph" @@ -165,13 +166,21 @@ func (obj *Lang) Init() error { } obj.ast = interpolated + variables := map[string]interfaces.Expr{ + "purpleidea": &ExprStr{V: "hello world!"}, // james says hi + // TODO: change to a func when we can change hostname dynamically! + "hostname": &ExprStr{V: obj.Hostname}, + } + consts := VarPrefixToVariablesScope(vars.ConstNamespace) // strips prefix! + addback := vars.ConstNamespace + interfaces.ModuleSep // add it back... + variables, err = MergeExprMaps(variables, consts, addback) + if err != nil { + return errwrap.Wrapf(err, "couldn't merge in consts") + } + // top-level, built-in, initial global scope scope := &interfaces.Scope{ - Variables: map[string]interfaces.Expr{ - "purpleidea": &ExprStr{V: "hello world!"}, // james says hi - // TODO: change to a func when we can change hostname dynamically! - "hostname": &ExprStr{V: obj.Hostname}, - }, + Variables: variables, // all the built-in top-level, core functions enter here... Functions: FuncPrefixToFunctionsScope(""), // runs funcs.LookupPrefix } diff --git a/lang/structs.go b/lang/structs.go index ebab9e14..62efb553 100644 --- a/lang/structs.go +++ b/lang/structs.go @@ -3073,9 +3073,10 @@ func (obj *StmtProg) importSystemScope(name string) (*interfaces.Scope, error) { // initial scope, built from core golang code scope := &interfaces.Scope{ - // TODO: we could add core API's for variables and classes too! + // TODO: we could use the core API for variables somehow... //Variables: make(map[string]interfaces.Expr), - Functions: funcs, // map[string]Expr + Functions: funcs, // map[string]interfaces.Expr + // TODO: we could add a core API for classes too! //Classes: make(map[string]interfaces.Stmt), } diff --git a/lang/unification_test.go b/lang/unification_test.go index 97ef4622..67f13aea 100644 --- a/lang/unification_test.go +++ b/lang/unification_test.go @@ -24,6 +24,7 @@ import ( "strings" "testing" + "github.com/purpleidea/mgmt/lang/funcs/vars" "github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/types" "github.com/purpleidea/mgmt/lang/unification" @@ -858,12 +859,23 @@ func TestUnification1(t *testing.T) { //} //t.Logf("test #%d: astInterpolated: %+v", index, astInterpolated) + variables := map[string]interfaces.Expr{ + "purpleidea": &ExprStr{V: "hello world!"}, // james says hi + //"hostname": &ExprStr{V: obj.Hostname}, + } + consts := VarPrefixToVariablesScope(vars.ConstNamespace) // strips prefix! + addback := vars.ConstNamespace + interfaces.ModuleSep // add it back... + var err error + variables, err = MergeExprMaps(variables, consts, addback) + if err != nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: couldn't merge in consts: %+v", index, err) + return + } + // top-level, built-in, initial global scope scope := &interfaces.Scope{ - Variables: map[string]interfaces.Expr{ - "purpleidea": &ExprStr{V: "hello world!"}, // james says hi - //"hostname": &ExprStr{V: obj.Hostname}, - }, + Variables: variables, // all the built-in top-level, core functions enter here... Functions: FuncPrefixToFunctionsScope(""), // runs funcs.LookupPrefix } @@ -884,7 +896,7 @@ func TestUnification1(t *testing.T) { Debug: testing.Verbose(), Logf: logf, } - err := unifier.Unify() + err = unifier.Unify() // TODO: print out the AST's so that we can see the types t.Logf("\n\ntest #%d: AST (after): %+v\n", index, ast) diff --git a/lang/util.go b/lang/util.go index 2812cc4b..ac50aa60 100644 --- a/lang/util.go +++ b/lang/util.go @@ -18,9 +18,13 @@ package lang import ( + "fmt" + "strings" + "github.com/purpleidea/mgmt/lang/funcs" "github.com/purpleidea/mgmt/lang/funcs/simple" "github.com/purpleidea/mgmt/lang/funcs/simplepoly" + "github.com/purpleidea/mgmt/lang/funcs/vars" "github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/types" ) @@ -43,6 +47,7 @@ func FuncPrefixToFunctionsScope(prefix string) map[string]interfaces.Expr { Values: []*types.FuncValue{st.Fn}, // just one! } + // XXX: should we run fn.SetType(st.Fn.Type()) ? exprs[name] = fn continue } else if st, ok := x.(*simplepoly.WrappedFunc); simplepoly.DirectInterface && ok { @@ -66,3 +71,147 @@ func FuncPrefixToFunctionsScope(prefix string) map[string]interfaces.Expr { } return exprs } + +// VarPrefixToVariablesScope is a helper function to return the variables +// portion of the scope from a variable prefix lookup. Basically this is useful +// to pull out a portion of the variables we've defined by API. +func VarPrefixToVariablesScope(prefix string) map[string]interfaces.Expr { + fns := vars.LookupPrefix(prefix) // map[string]func() interfaces.Var + exprs := make(map[string]interfaces.Expr) + for name, f := range fns { + x := f() // inspect + expr, err := ValueToExpr(x) + if err != nil { + panic(fmt.Sprintf("could not build expr: %+v", err)) + } + exprs[name] = expr + } + return exprs +} + +// MergeExprMaps merges the two maps of Expr's, and errors if any overwriting +// would occur. If any prefix string is specified, that is added to the keys of +// the second "extra" map before doing the merge. This doesn't change the input +// maps. +func MergeExprMaps(m, extra map[string]interfaces.Expr, prefix ...string) (map[string]interfaces.Expr, error) { + p := strings.Join(prefix, "") // hack to have prefix be optional + + result := map[string]interfaces.Expr{} + for k, v := range m { + result[k] = v // copy + } + + for k, v := range extra { + name := p + k + if _, exists := result[name]; exists { + return nil, fmt.Errorf("duplicate variable: %s", name) + } + result[name] = v + } + + return result, nil +} + +// ValueToExpr converts a Value into the equivalent Expr. +// FIXME: Add some tests for this function. +func ValueToExpr(val types.Value) (interfaces.Expr, error) { + var expr interfaces.Expr + + switch x := val.(type) { + case *types.BoolValue: + expr = &ExprBool{ + V: x.Bool(), + } + + case *types.StrValue: + expr = &ExprStr{ + V: x.Str(), + } + + case *types.IntValue: + expr = &ExprInt{ + V: x.Int(), + } + + case *types.FloatValue: + expr = &ExprFloat{ + V: x.Float(), + } + + case *types.ListValue: + exprs := []interfaces.Expr{} + + for _, v := range x.List() { + e, err := ValueToExpr(v) + if err != nil { + return nil, err + } + exprs = append(exprs, e) + } + + expr = &ExprList{ + Elements: exprs, + } + + case *types.MapValue: + kvs := []*ExprMapKV{} + + for k, v := range x.Map() { + kx, err := ValueToExpr(k) + if err != nil { + return nil, err + } + vx, err := ValueToExpr(v) + if err != nil { + return nil, err + } + kv := &ExprMapKV{ + Key: kx, + Val: vx, + } + kvs = append(kvs, kv) + } + + expr = &ExprMap{ + KVs: kvs, + } + + case *types.StructValue: + fields := []*ExprStructField{} + + for k, v := range x.Struct() { + fx, err := ValueToExpr(v) + if err != nil { + return nil, err + } + field := &ExprStructField{ + Name: k, + Value: fx, + } + fields = append(fields, field) + } + + expr = &ExprStruct{ + Fields: fields, + } + + case *types.FuncValue: + // TODO: this particular case is particularly untested! + expr = &ExprFunc{ + Title: "", // TODO: change this? + // TODO: symmetrically, it would have used x.Func() here + Values: []*types.FuncValue{ + x, // just one! + }, + } + + case *types.VariantValue: + // TODO: should this be allowed, or should we unwrap them? + return nil, fmt.Errorf("variant values are not supported") + + default: + return nil, fmt.Errorf("unknown type (%T) for value: %+v", val, val) + } + + return expr, expr.SetType(val.Type()) +} diff --git a/test/shell/env0.mcl b/test/shell/env0.mcl index c05e09f6..26fec8f2 100644 --- a/test/shell/env0.mcl +++ b/test/shell/env0.mcl @@ -18,6 +18,6 @@ $env = sys.env() $m = maplookup($env, "TEST", "321") file "${tmpdir}/environ" { - state => "exists", + state => $const.res.file.state.exists, content => fmt.printf("%s,%s,%s:%s,%s,%s:%t,%t:%s", $x, $y, $z, $a, $b, $c, $t, $f, $m), } diff --git a/test/shell/exchange0.mcl b/test/shell/exchange0.mcl index 933886e9..1f0af4ab 100644 --- a/test/shell/exchange0.mcl +++ b/test/shell/exchange0.mcl @@ -13,6 +13,6 @@ $rand = random1(8) $exchanged = world.exchange("keyns", $rand) file "/tmp/mgmt/exchange-${sys.hostname()}" { - state => "exists", + state => $const.res.file.state.exists, content => template("Found: {{ . }}\n", $exchanged), } diff --git a/test/shell/file-source.mcl b/test/shell/file-source.mcl index b647e02f..097e5ed2 100644 --- a/test/shell/file-source.mcl +++ b/test/shell/file-source.mcl @@ -1,4 +1,4 @@ file "/tmp/mgmt/file-source.txt" { source => "file-source.txt", - state => "exists", + state => $const.res.file.state.exists, } diff --git a/test/shell/load0.sh b/test/shell/load0.sh index 8ea8c5b4..54a01ddb 100755 --- a/test/shell/load0.sh +++ b/test/shell/load0.sh @@ -33,7 +33,7 @@ import "sys" file "${tmpdir}/loadavg" { content => fmt.printf("load average: %f, %f, %f\n", \$x1, \$x5, \$x15), - state => "exists", + state => $const.res.file.state.exists, } EOF diff --git a/util/code_test.go b/util/code_test.go index 21f79b7f..c22f727f 100644 --- a/util/code_test.go +++ b/util/code_test.go @@ -30,7 +30,7 @@ func TestCodeIndent(t *testing.T) { file "${root}/mgmt-hello-world" { content => "hello world from @purpleidea\n", - state => "exists", + state => $const.res.file.state.exists, } `) c2 := @@ -38,7 +38,7 @@ func TestCodeIndent(t *testing.T) { file "${root}/mgmt-hello-world" { content => "hello world from @purpleidea\n", - state => "exists", + state => $const.res.file.state.exists, } ` if c1 != c2 {