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 {